docs(product): track-view CSS consolidation audit and spec

This commit is contained in:
daniel-c-harvey
2026-06-05 16:43:19 -04:00
parent 92f860897b
commit 9854d51940
@@ -0,0 +1,388 @@
# Track View CSS Consolidation — audit + consolidated architecture spec
Status: proposed. Author: product-designer. Date: 2026-06-05. Implementer: maintenance-engineer.
Predecessors (both landed 2026-06-05):
- [`track-card-theming.md`](track-card-theming.md) — landed the navy-glass fallback + moss-green
text treatment, scoped under `.deepdrft-theme-dark` / `-light`.
- [`track-card-css-architecture.md`](track-card-css-architecture.md) — replaced the MudCard/MudPaper
shell with plain `<div>`s and removed the four `!important` declarations from section 8.
This note is the consolidation pass *after* those two landed. The cards render correctly today; the
goal here is a single coherent description of the cascade as it now stands, identification of what is
redundant or fragile, and a prioritized set of changes. **Nothing here is urgent or broken** — it is
hygiene plus two genuine hierarchy issues (§5) that the predecessor notes flagged as optional and that
are now worth re-deciding because the design converged on green-everywhere.
The headline finding: **the architecture is already close to clean.** The plain-div shell removed the
hard MudBlazor fight. What remains is (a) one real cascade subtlety on `MudText` color, (b) an open-but-
unused CSS-isolation seam, (c) layout-container redundancy across three files, and (d) a green-on-green
hierarchy collapse now that artist text, the genre chip, and the play FAB are all the same accent green.
---
## §1. Inventory — every CSS declaration touching the track view
Render tree (confirmed): `MainLayout``<div class="@ThemeWrapperClass">` (the
`deepdrft-theme-dark` / `-light` ancestor, wraps `@Body`) → `TracksView` (`.tracks-page-wrapper` …)
`TracksGallery` (`MudContainer.tracks-gallery-container``MudGrid``MudItem`
`.deepdrft-track-gallery-item-center`) → `TrackCard` (plain-div shell).
Stylesheet load order (from `App.razor`, per the header comment in `deepdrft-styles.css`):
`deepdrft-tokens.css` (tokens) → `deepdrft-styles.css` (rules) → MudBlazor framework CSS + the
`MudThemeProvider`-injected `:root` palette variables. The `--mud-palette-*` vars are runtime-injected
and update on `IsDarkMode` toggle.
| # | Rule / selector | File | Targets | Conflict status |
|---|---|---|---|---|
| 1 | `.tracks-page-wrapper` (flex column) | `TracksView.razor.css` (scoped) | page outer wrapper | none — but **functionally inert**: `flex-direction: column` with no height constraint and a single flex child does nothing visible. See §4. |
| 2 | `.tracks-view-container` (flex column, `flex:1`, `padding:0 16px`) | `TracksView.razor.css` (scoped) | inner container | Horizontal padding here **stacks** with #5 and #6 (16px + 16px + 16px = 48px effective inset before the grid). See §4. |
| 3 | `.tracks-content` (flex, `flex-grow:1`, `padding-top:16px`) | `TracksView.razor.css` (scoped) | content row | `display:flex` on a block that contains a single full-width `MudContainer` — flex adds nothing here. Mild redundancy. |
| 4 | `.tracks-footer` (flex column, center, gap) | `TracksView.razor.css` (scoped) | pagination footer | none. Clean. |
| 5 | `.tracks-gallery-container` (`padding:16px`, `height:100%`, `box-sizing`) | `TracksGallery.razor.css` (scoped) | `MudContainer` root | `padding:16px` **competes with** #2's horizontal padding (double inset). `height:100%` is inert (no constrained-height ancestor chain). See §4. |
| 6 | `MudContainer MaxWidth="Large"` (MudBlazor) | MudBlazor framework | gallery container | MudBlazor's own `.mud-container` adds responsive horizontal padding + max-width. Stacks on top of #2 and #5**three** sources of horizontal inset. |
| 7 | `.deepdrft-track-gallery-item-center` (`display:flex; justify-content:center`) | `deepdrft-styles.css` §8 (global) | per-item wrapper div | none functionally, but **lives in global §8 while its only consumers are two scoped components** — see §3/§4 placement note. Appears twice (TracksView skeleton path + TracksGallery). |
| 8 | `.deepdrft-track-card-container` (250×250, relative, overflow hidden, `background:transparent`) | `deepdrft-styles.css` §8 (global) | card shell div | none — `!important` already removed; plain div wins by default. |
| 9 | `.deepdrft-theme-dark .deepdrft-track-card-container` (glass border) | §8 (global) | card shell, dark | none. **Ancestor-dependent** (needs `.deepdrft-theme-dark`) — cannot move to scoped CSS. |
| 10 | `.deepdrft-track-card-bg` (absolute full-bleed, `brightness(0.7)`) | §8 (global) | album-art div | none. Theme-agnostic — **could** move to scoped. |
| 11 | `.deepdrft-track-card-content` (relative, flex column, space-between, padding) | §8 (global) | content div | none. Theme-agnostic layout — **could** move to scoped. |
| 12 | `.deepdrft-theme-dark .deepdrft-track-card-content` (navy scrim gradient) | §8 (global) | content div, dark | none. **Ancestor-dependent** — stays global. |
| 13 | `.deepdrft-track-card-fallback` (absolute full-bleed, `background:navy-mid`) | §8 (global) | fallback div | none. Base paint is the no-wrapper-class flash guard. |
| 14 | `.deepdrft-theme-dark .deepdrft-track-card-fallback` (navy-glass, border, blur) | §8 (global) | fallback, dark | none. **Ancestor-dependent** — stays global. |
| 15 | `.deepdrft-theme-light .deepdrft-track-card-fallback` (navy-tint-on-white, border) | §8 (global) | fallback, light | none. **Ancestor-dependent** — stays global. |
| 16 | `.deepdrft-track-title { color: white }` (unconditional) | §8 (global) | title MudText | **competes with MudBlazor `.mud-typography-subtitle1` color.** See §2. |
| 17 | `.deepdrft-track-artist { color: green-accent }` (unconditional) | §8 (global) | artist MudText | **competes with MudBlazor `.mud-typography-caption` color.** See §2. |
| 18 | `.deepdrft-track-meta { color: rgba(white,.55) }` (unconditional) | §8 (global) | album/year MudText | **competes with MudBlazor `.mud-typography-caption` color.** See §2. |
| 19 | `.deepdrft-theme-light .deepdrft-track-title / -artist / -meta` (navy/green/muted) | §8 (global) | text, light | Light overrides of #1618. Correct intent; cascade verified in §2. |
| 20 | `.deepdrft-track-info-middle { margin: 8px 0 }` | §8 (global) | middle row | none. Theme-agnostic — could move to scoped. |
| 21 | `.deepdrft-track-info-bottom` (flex, space-between, center) | §8 (global) | bottom row | none. Theme-agnostic — could move to scoped. (Note: `.deepdrft-track-info-top` has **no** rule — relies on default block flow; fine.) |
| 22 | `.deepdrft-genre-chip { opacity:.9; margin-top:4px }` | §8 §9 (global) | genre MudChip | No color override — chip inherits `Color.Primary` = green in dark. See §5. |
| 23 | `MudChip Color="Color.Primary"` (MudBlazor) | MudBlazor framework | genre chip | Renders filled `--mud-palette-primary` = `#3D7A68` green (dark). **Collides with green artist text + green FAB.** See §5. |
| 24 | `MudFab Color="Color.Primary"` (MudBlazor) | MudBlazor framework | play button | Renders green (dark). Intended as primary interactive accent, but now one of three greens in the card. See §5. |
| 25 | `@media (max-width:480px) .deepdrft-track-card-container` (200×200) | §8 §13 (global) | card shell, small screens | none. Theme-agnostic — would move to scoped *with* the container rule if §3 adopted. |
| — | `MudText Typo` font-family (subtitle1 → Geist Mono, caption → Geist Mono) | `DeepDrftPalettes.Typography` (MudBlazor theme) | all card text | Not a conflict — font comes from the theme typography object, color comes from our §8 rules. The two concerns are cleanly separated **once §2 confirms color wins.** |
---
## §2. MudText cascade analysis — per element, both themes
This is the part Daniel flagged. The question: do our single-class `.deepdrft-track-*` color rules
reliably beat MudBlazor's `.mud-typography-*` color rules, and do they behave correctly in **both**
themes given the unconditional-default + light-override structure?
### The two competing selectors
For each card text element MudBlazor renders `<p class="mud-typography mud-typography-{typo} deepdrft-track-{role} ...">`. Two color sources apply:
- **MudBlazor:** `.mud-typography-subtitle1` / `.mud-typography-caption` — these set font properties.
The **color** for body/caption/subtitle typography comes from `.mud-typography` (or the element
default) resolving to `color: var(--mud-palette-text-primary)`. Specificity: single class, `0,1,0`.
(MudBlazor does not put a per-`Typo` color on `.mud-typography-caption`; caption/subtitle inherit the
text-primary color. The relevant competing declaration is therefore `0,1,0` — a single class.)
- **Ours, dark (unconditional default):** `.deepdrft-track-title { color: white }``0,1,0`.
- **Ours, light (theme override):** `.deepdrft-theme-light .deepdrft-track-title { color: navy }``0,2,0`.
### Why ours wins in dark — the load-order fact
In **dark mode**, ours (`0,1,0`) ties MudBlazor's (`0,1,0`) on specificity, so **source order decides.**
`deepdrft-styles.css` is authored to load, and the MudBlazor framework sheet loads as a separate link.
The decisive detail: the *color* MudBlazor would otherwise apply is `var(--mud-palette-text-primary)`,
and our rule sets an explicit color literal. **As long as `deepdrft-styles.css` is linked after the
MudBlazor framework CSS, ours wins the tie.** Per `App.razor` ordering (tokens → our styles, with the
MudThemeProvider injecting palette vars), this holds today — but it is a **source-order dependency, not
a specificity guarantee.** That is the one fragility in the dark path: it works because of link order,
the same class of latent risk the predecessor note removed for backgrounds. It is lower-risk here
(both are `0,1,0`, and the visible result is correct), but it is the same smell.
### Per-element cascade table
| Element | Typo | Dark winner | Dark correct? | Light winner | Light correct? |
|---|---|---|---|---|---|
| Title (`-track-title`) | subtitle1 | `.deepdrft-track-title` (white, `0,1,0`, wins tie by load order) | ✅ off-white on navy-glass | `.deepdrft-theme-light .deepdrft-track-title` (navy, `0,2,0`) | ✅ navy on light fallback |
| Artist (`-track-artist`) | caption | `.deepdrft-track-artist` (green-accent, `0,1,0`) | ✅ legible, but see §5 (green collision) | `.deepdrft-theme-light .deepdrft-track-artist` (deep green, `0,2,0`) | ✅ deep green on light |
| Album (`-track-meta`) | caption | `.deepdrft-track-meta` (rgba white .55, `0,1,0`) | ✅ muted off-white | `.deepdrft-theme-light .deepdrft-track-meta` (muted, `0,2,0`) | ✅ muted on light |
| Year (`-track-meta`) | caption | same as album | ✅ | same as album | ✅ |
### Does the "unconditional default + light override" structure behave correctly?
**Yes, in both themes — with one caveat.** The structure is:
- **Dark:** unconditional defaults apply (no `.deepdrft-theme-dark` guard). They win the `0,1,0` tie
against MudBlazor by load order. ✅
- **Light:** the `.deepdrft-theme-light` overrides (`0,2,0`) beat the unconditional defaults (`0,1,0`).
✅ — this is why the unconditional dark defaults do **not** leak into light mode. The light overrides
correctly win on specificity, not load order, so light is *more* robust than dark.
**Caveat — the no-wrapper hydration window.** The unconditional defaults exist specifically to cover the
brief WASM-hydration window where neither `.deepdrft-theme-dark` nor `.deepdrft-theme-light` is on the
ancestor yet (the predecessor "blue text flash" fix). During that window the card shows dark-default
colors (white title, green artist) regardless of the user's actual theme. For a **light-mode** user this
means a sub-second flash of dark-intended text before the `-light` override attaches. It is the
symmetric cost of choosing dark as the unconditional default. Acceptable (the flash is brief and the
fallback panel underneath is also painted by a base navy-mid rule, so the card is internally
consistent during the flash), but worth naming: **the flash guard optimizes for the dark-mode user at
the light-mode user's slight expense.**
### Better alternative to the unconditional-default approach
There is a cleaner structure that removes both the load-order tie *and* the asymmetric flash, at the
cost of one extra rule per element. See §6 R2 — promote the dark treatment to a `.deepdrft-theme-dark`
guard and keep a theme-neutral safe default. Recommended only if the load-order dependency is judged
worth hardening; the current code is visually correct.
---
## §3. Recommended scoped/global split for TrackCard
The predecessor architecture note established the key fact: with a plain-div shell, **Blazor CSS
isolation now works** for elements Blazor renders (the shell divs), but **cannot select ancestors**
so any rule keyed on `.deepdrft-theme-dark` / `-light` must stay global. The seam is open but unused.
The clean seam: **scoped CSS owns theme-agnostic structure/layout; global §8 owns everything that
depends on the theme-wrapper ancestor (all paint/color that differs by theme).**
### What CAN move to a new `TrackCard.razor.css` (theme-agnostic, Blazor-rendered divs)
| Rule | Why it can move |
|---|---|
| `.deepdrft-track-card-container` (size, position, overflow, `background:transparent`) | No theme dependency. Note: the `@media 480px` override (#25) must move **with** it. |
| `.deepdrft-track-card-bg` (album-art positioning + brightness) | Pure layout/filter, theme-agnostic. |
| `.deepdrft-track-card-content` (base flex/padding/z-index — **not** the dark scrim) | Base layout is theme-agnostic. |
| `.deepdrft-track-info-middle` / `-info-bottom` | Pure layout. |
### What MUST stay global in §8 (ancestor-dependent — scoped CSS can't reach `.deepdrft-theme-*`)
| Rule | Why it stays |
|---|---|
| `.deepdrft-theme-dark .deepdrft-track-card-container` (glass border) | Selects the theme ancestor. |
| `.deepdrft-theme-dark .deepdrft-track-card-content` (scrim gradient) | Selects the theme ancestor. |
| `.deepdrft-track-card-fallback` + both theme variants | Theme ancestor (and the base flash-guard rule pairs with them — keep the set together). |
| All `.deepdrft-track-title / -artist / -meta` color rules (both default and `-light`) | Theme-dependent color; the `-light` variants select the ancestor. Keeping the unconditional defaults global alongside them keeps the whole color story in one place. |
| `.deepdrft-genre-chip` color treatment (if added per §5) | If theme-aware, ancestor-dependent. |
### Is there a clean seam? — Recommendation
**Yes, but it is a judgment call whether to take it.** Two coherent end-states:
- **Option S-1 (split): introduce `TrackCard.razor.css`** holding the four theme-agnostic structural
rules above; leave all theme/color rules in §8. *Pro:* co-locates structure with the component,
uses the seam the predecessor note reopened, shrinks §8. *Con:* splits the card's CSS across two
files — a maintainer now looks in two places, and the split line ("is this theme-dependent?") is a
subtlety that invites future mis-filing. It also lives in `DeepDrftShared.Client` (the component's
home), which means the CMS would now ship these structural rules too (harmless — they are layout
only — but a new coupling).
- **Option S-2 (consolidate, recommended): keep everything in §8, do not create a scoped file.**
The card's CSS is small (~15 rules) and *already* all in one place. The strongest property of the
current setup is that **all track-card CSS lives in exactly one section of one file.** Splitting it
to use isolation "because we can" trades that single-location clarity for architectural tidiness
that buys little — the theme-dependent majority must stay global regardless, so a split leaves the
bulk in §8 and scatters only four layout rules. **Recommend S-2.** Keep the seam *open and
documented* (it is a real option for future per-state styling — hover/selected/now-playing — which
may be theme-agnostic and numerous enough to justify a scoped file later), but do not split now.
This matches the predecessor note's own framing: §8's public-only scoping is "still convenient"; the
isolation seam is a reopened *option*, not a mandate.
---
## §4. TracksView / TracksGallery layout assessment — competing
This is the one place with genuine redundancy and mild competition. Three layers each contribute
horizontal inset and flex behavior that doesn't compose cleanly:
### The triple horizontal inset
A track card's left edge is pushed in by **three stacked paddings**:
1. `.tracks-view-container { padding: 0 16px }` (TracksView scoped)
2. `MudContainer`'s own responsive padding + `MaxWidth.Large` cap (MudBlazor)
3. `.tracks-gallery-container { padding: 16px }` (TracksGallery scoped)
Three sources of "indent the gallery" with no single owner. Not *broken* (it renders; the grid just
sits inside a compounded margin), but it is exactly the "where does this spacing come from?" confusion
that consolidation should remove. **One layer should own horizontal inset.** Since `MudContainer`
already provides centered, capped, responsive padding (its entire purpose), the two hand-rolled 16px
paddings are redundant with it.
### Inert / no-op rules
- `.tracks-page-wrapper` `flex-direction: column` — single child, no height target. Does nothing.
- `.tracks-view-container` `flex: 1` and `.tracks-content` `flex-grow: 1` — these presume a constrained-
height flex parent so the content stretches and the footer pins to the bottom. But `.tracks-page-
wrapper` has **no height** (no `min-height: 100vh`, no `flex: 1` against a sized parent). The flex-grow
chain has nothing to grow into, so the "sticky footer" intent these rules encode is **not actually
achieved** — the footer sits directly under the content regardless. Dead intent.
- `.tracks-gallery-container { height: 100% }` — no constrained-height ancestor, so `100%` resolves to
content height. Inert.
- `.tracks-content { display: flex }` — wraps a single full-width `MudContainer`; flex changes nothing.
### Centering
`.deepdrft-track-gallery-item-center` (global §8) + `MudGrid Justify="Justify.Center"` (TracksGallery)
+ `MudItem` breakpoints. These **do** cooperate correctly — the per-item flex-center plus grid justify
center the cards within their columns and center the row. No conflict here; this part works.
### Recommendation (detail in §6 R3)
Collapse to a single ownership model: let `MudContainer` own horizontal inset and max-width; drop the
redundant paddings; either *commit* to the sticky-footer intent (give the wrapper a real height target
so the flex chain works) or *remove* the inert flex rules. The skeleton-loading path in `TracksView`
(which uses its own `MudGrid` + `.deepdrft-track-gallery-item-center`, bypassing `TracksGallery`)
should match whatever container model the loaded path uses, so the layout doesn't shift on load.
---
## §5. Genre chip and FAB hierarchy — re-assessment
The predecessor note flagged the genre chip as §3a (optional) and explicitly blessed the FAB as
correct (§3b). **That assessment was made before the design converged on green-for-everything.** Re-
evaluating now that artist text is *also* green-accent:
### The collapse
In **dark mode**, three elements in the lower half of every card are the **same** `#3D7A68` green-
accent:
1. **Artist text** (`.deepdrft-track-artist` → green-accent)
2. **Genre chip** (`MudChip Color="Color.Primary"` → filled green)
3. **Play FAB** (`MudFab Color="Color.Primary"` → green)
Three greens stacked in the card's lower two rows flattens the visual hierarchy: the *interactive*
element (the FAB — the only thing you click) no longer stands out from the *informational* green
(artist) and the *categorical* green (genre tag). The eye can't tell what's actionable. This is a
real regression introduced by the convergence on green, not a pre-existing nit.
### Resolving it — assign each green a distinct job
The fix is to **let only one of the three keep the saturated green**, and give it to the interactive
element (the FAB), since green-accent is defined as the dark palette's *primary interactive* color.
Demote the other two:
- **FAB → keep `Color.Primary` (green).** It is the action; it earns the accent. ✅ Confirmed correct
*only if* the other two greens step back. The predecessor's "FAB is fine" verdict holds **conditional
on fixing the chip and artist**.
- **Genre chip → stop being a filled green.** Make it a *tag*, not a button-lookalike. Recommend an
outlined / low-emphasis treatment: transparent (or navy-mid) ground, green-accent border + text, so
it reads as a category label distinct from the solid green FAB. This is the predecessor §3a proposal,
now upgraded from "optional" to **recommended** because the collision is no longer hypothetical.
- **Artist text → consider demoting from green to muted off-white.** This is the genuinely open
design call. Two readings (the predecessor's §2b ambiguity resurfacing): (a) **artist stays green**
as the card's identity color and we rely on the chip-outline + FAB-fill contrast to separate the
three; or (b) **artist goes muted off-white** (like `.np-sub` in NowPlayingCard, `rgba(250,250,248,.45.55)`),
leaving green as a *purely interactive/accent* signal (FAB + chip border). **Recommend (b).** It
matches NowPlayingCard's actual hierarchy most closely — there, green is the *label/accent* color
(`.np-label`, waveform), the title is off-white, and the sub is muted; artist-as-sub should be muted,
not accent. Reading (b) also reserves green for "this means something / do something," which is the
cleaner semantic. This reverses the predecessor's "reading 1" default — justified because that
default was chosen before the three-green collision was visible.
### Light mode
Less acute: in light, `Color.Primary` = navy `#0D1B2A`, artist = deep green `#1A3C34`, so the three
elements are navy chip / navy FAB / green artist — already two distinct hues. The chip-vs-FAB
sameness (both navy) is the only light-mode echo of the problem; the outlined-chip treatment fixes it
there too. No separate light fix needed beyond the theme-aware chip rule.
---
## §6. Recommended changes — prioritized, ready for maintenance-engineer
Ordered by value. R1 and R4 are pure cleanup (safe, low-risk). R5 is the real design change. R2 and
R3 are judgment calls flagged for Daniel.
### R1 — Resolve the triple horizontal inset (cleanup, recommended)
**Priority: high (clarity). Risk: low (visual nudge only).**
Pick `MudContainer` as the single owner of horizontal inset + max-width. In
`TracksView.razor.css`: change `.tracks-view-container { padding: 0 16px }``padding: 0`. In
`TracksGallery.razor.css`: remove `padding: 16px` from `.tracks-gallery-container` (keep `box-sizing`;
drop `height: 100%` as inert — see R3). The cards will sit at MudContainer's natural inset. Verify the
gallery still has breathing room at the page edge; if MudContainer's default padding feels too tight,
add a *single* deliberate gutter on `.tracks-gallery-container` and document it as the one owner.
### R2 — Harden the MudText color cascade (judgment call, optional)
**Priority: medium. Risk: low. Decision needed from Daniel.**
Today the dark text colors win MudBlazor by **load order** (a `0,1,0` tie). It renders correctly but is
the same latent fragility the predecessor note removed for backgrounds. Two ways to harden, if desired:
- **R2a (minimal):** leave as-is, add a comment in §8 noting the load-order dependency and that
`deepdrft-styles.css` must stay linked after the MudBlazor framework sheet. Zero visual change.
- **R2b (structural):** restructure to remove the tie *and* the asymmetric light-flash:
give each text element a theme-neutral safe default (e.g. `inherit` / muted) at `0,1,0`, then guard
**both** dark and light treatments under their respective `.deepdrft-theme-*` ancestors (`0,2,0`).
This makes both themes win on **specificity, not load order**, and removes the dark-defaults-leak
during a light user's hydration window (the flash becomes a neutral-to-light transition, not dark-to-
light). Cost: one extra rule per element (3 default + 3 dark + 3 light instead of 3 default + 3 light).
**Recommend R2b only if Daniel wants the cascade hardened; otherwise R2a.** The current code is not
wrong — this is hardening, not a bug fix.
### R3 — Decide the sticky-footer intent (judgment call)
**Priority: medium. Risk: low.**
The `flex: 1` / `flex-grow: 1` / `height: 100%` chain in `TracksView.razor.css` +
`TracksGallery.razor.css` encodes a sticky-footer-at-viewport-bottom intent that **does not currently
work** (no height target on `.tracks-page-wrapper`). Resolve one way:
- **R3a (commit):** add `min-height: 100vh` (or `flex: 1` against a sized layout parent) to
`.tracks-page-wrapper` so the flex-grow chain actually pins the pagination footer to the bottom on
short pages. Then the existing flex rules become meaningful.
- **R3b (remove, recommended):** delete the inert flex/height rules (`.tracks-page-wrapper`
flex-direction; `.tracks-view-container` flex/flex:1; `.tracks-content` display:flex/flex-grow;
`.tracks-gallery-container` height:100%). The page already lays out fine via normal block flow + the
footer's own centering. **Recommend R3b** — the sticky-footer intent isn't visibly needed on a
paginated gallery (content fills the page), and removing dead rules is cleaner than reviving an
unused behavior. If Daniel *wants* the footer pinned on sparse pages, do R3a instead.
### R4 — Tidy redundant layout rules (cleanup, recommended)
**Priority: low. Risk: none.**
Independent of R3: `.tracks-content { display: flex }` wraps a single full-width child — drop
`display: flex` (keep `padding-top: 16px`). These are no-ops being removed, not behavior changes. Roll
into the R3 pass.
### R5 — Break the three-green collision (design change, recommended)
**Priority: high (visual quality). Risk: low-medium (visible design change — wants Daniel's eye).**
This is the substantive one. In dark mode, artist text + genre chip + play FAB are all the same green.
Reassign:
- **Genre chip:** add a theme-aware `.deepdrft-genre-chip` treatment — outlined / low-emphasis
(transparent or navy-mid ground, green-accent border + text in dark; navy border + text in light) so
it reads as a *tag*, not a filled button. Keep the existing `opacity:.9; margin-top:4px`. Because the
treatment is theme-aware it stays in global §8 (ancestor-dependent). Likely also requires changing the
Razor `MudChip` from `Variant.Filled Color.Primary` to `Variant.Outlined` (or `Variant.Text`) so
MudBlazor stops painting a filled green ground that our CSS then has to fight — *prefer changing the
Variant in Razor over CSS-overriding a filled chip*, consistent with the predecessor's "don't fight
MudBlazor, don't invite the fight" principle. **Razor edit owned by maintenance-engineer; flagged
here as part of the change.**
- **Artist text:** demote `.deepdrft-track-artist` from green-accent to muted off-white in dark
(`rgba(250,250,248,.55)`, matching `.deepdrft-track-meta` or slightly brighter for hierarchy), leaving
green as a purely accent/interactive signal. **This reverses the predecessor's "reading 1" default —
call it out to Daniel explicitly; it's a taste call he should confirm.** Light mode artist can stay
deep green (no collision there) or follow the same demotion for consistency.
- **FAB:** no change — keep `Color.Primary` green. It is now the *only* saturated green in the lower
card, so it correctly reads as the action.
**Net effect:** title off-white, artist muted, genre = outlined green tag, FAB = solid green action.
A clear three-tier hierarchy (identity / info / action) instead of a green wash. This is the
NowPlayingCard vocabulary applied correctly.
### R6 — Do NOT split TrackCard CSS into a scoped file (decision: hold)
**Priority: n/a (a decision to *not* act). Risk: n/a.**
Per §3, keep all track-card CSS in §8 (Option S-2). Document the open isolation seam in a §8 comment so
a future per-state styling pass (hover/selected/now-playing) knows a `TrackCard.razor.css` is available
for theme-agnostic state rules if they grow numerous. Do not create the file now.
### Things explicitly NOT to change
- The plain-div shell and absence of `!important` (predecessor work — correct, leave alone).
- The base flash-guard rules (`.deepdrft-track-card-fallback` base navy-mid; unconditional text
defaults) — these are load-bearing for the no-wrapper hydration window. R2b *restructures* them but
preserves the guard; do not simply delete them.
- `.deepdrft-track-gallery-item-center` location — it is used by both the loaded path (TracksGallery)
and the skeleton path (TracksView), so it correctly stays global. Leave in §8.
### Suggested sequencing
1. R1 + R4 + R3b together (one cleanup pass on the two scoped layout files — safe, no design risk).
2. R5 (the design change — wants Daniel's eye on the result; the artist-demotion is a taste call).
3. R2 only if Daniel elects to harden the cascade (R2a comment is near-free; R2b is the fuller fix).
---
## Open questions for Daniel
1. **R5 artist color:** demote artist from green to muted off-white (recommended), or keep artist green
and rely on chip-outline + FAB-fill to separate the three greens? This reverses a prior default.
2. **R3 sticky footer:** pin the pagination footer to viewport bottom on sparse pages (R3a), or remove
the dead flex rules and let it flow (R3b, recommended)?
3. **R2 cascade hardening:** leave the dark text colors winning by load order with a comment (R2a), or
restructure to win by specificity and remove the light-user hydration flash (R2b)?