Files
deepdrft/product-notes/track-view-css-consolidation.md
T
2026-06-05 17:00:36 -04:00

389 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.
# Track View CSS Consolidation — audit + consolidated architecture spec
Status: completed. Author: product-designer. Date: 2026-06-05. Implementer: maintenance-engineer. Landed: 2026-06-05.
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)?