# 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 `
`. 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)?