Files
deepdrft/product-notes/track-card-css-architecture.md
2026-06-05 16:27:51 -04:00

446 lines
22 KiB
Markdown
Raw Permalink 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 Card CSS Architecture — eliminate `!important` from section 8
Status: completed. Author: product-designer. Date: 2026-06-05. Implementer: daniel-c-harvey. Landed: 2026-06-05.
Predecessor: [`track-card-theming.md`](track-card-theming.md) (completed 2026-06-05) landed the
navy-glass + moss-green treatment and, in doing so, introduced the two `!important` rules this
spec exists to remove. That note is the *what it should look like*; this note is the *how the
CSS should be structured so it looks like that without `!important`*. Nothing here changes the
visual design — only the mechanism.
---
## §1. Root cause analysis — why MudCard/MudPaper fight our CSS
### 1.1 The two `!important` rules in question
`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` section 8:
```css
.deepdrft-track-card-container { ... background: transparent !important; } /* line 222 */
.deepdrft-track-card-fallback { ... background: var(--deepdrft-navy-mid, #162437) !important; } /* line 268 */
.deepdrft-theme-dark .deepdrft-track-card-fallback { ... !important; } /* line 273 */
.deepdrft-theme-light .deepdrft-track-card-fallback { ... !important; } /* line 280 */
```
Four `!important` declarations, all on `background`, all on elements rendered by MudBlazor
(`MudCard` → container, `MudPaper` → fallback).
### 1.2 What MudBlazor paints and with what selector
MudBlazor ships a single global `<style>` block (injected by `MudThemeProvider`, served as
`_content/MudBlazor/MudBlazor.min.css`). The relevant rules are roughly:
```css
.mud-paper {
background-color: var(--mud-palette-surface); /* the conflict */
color: var(--mud-palette-text-primary);
...
}
.mud-card { /* composes .mud-paper */ }
```
`MudCard` and `MudPaper` both render a root element carrying the `.mud-paper` class, and
`--mud-palette-surface` resolves to `DeepDrftPalettes.Surface``#162437` (navy-mid) in dark,
`#FAFAF8` (off-white) in light. So **every** track card root gets a painted surface color we do
not want, because we layer our own album-art div / fallback glass / scrim underneath the content.
### 1.3 Why our scoped CSS can't reach it and `!important` was the escape hatch
Two compounding facts:
1. **Specificity / source-order.** `.mud-paper { background-color: ... }` is a single class
selector (specificity `0,1,0`). Our `.deepdrft-track-card-container { background: ... }` is
*also* a single class selector (`0,1,0`). When specificity ties, **source order wins** — and
MudBlazor's stylesheet load order relative to `deepdrft-styles.css` is not something section 8
controls reliably (framework CSS, theme-injected CSS, and our sheet are separate links). To
guarantee we win regardless of order, the author reached for `!important`. That is the smell:
`!important` is being used to paper over an *ordering/specificity* uncertainty, not a genuine
need.
2. **CSS isolation cannot help here.** This is the deeper structural reason. Blazor scoped CSS
(`*.razor.css`) works by stamping a `b-<hash>` attribute onto elements *that Blazor renders*
and rewriting the scoped selector to `.foo[b-<hash>]` (specificity `0,2,0` — would beat
`.mud-paper`). But `MudCard`/`MudPaper` render their **own** inner HTML; Blazor never stamps
the scope attribute onto MudBlazor's internal elements. So a `TrackCard.razor.css` rule
targeting `.mud-paper` would compile to `.mud-paper[b-<hash>]` and match **nothing**. (This is
also why no `TrackCard.razor.css` exists today — it would be inert against the very elements
that need overriding.) The team correctly fell back to global rules in section 8 — but global
single-class rules don't beat MudBlazor without either higher specificity or `!important`.
**The fundamental mismatch:** we are using MudBlazor surface components (`MudCard`, `MudPaper`)
as *layout shells* while wanting to own their backgrounds entirely. MudBlazor's whole contract is
"I paint the surface from the palette." We are fighting the component's purpose. Every option
below is a different way to stop fighting it.
### 1.4 What we actually need from these two elements
Stripped to first principles, the container and fallback need:
- **Container:** a `position: relative`, fixed-size, `overflow: hidden` box. A *transparent*
stacking context so the album-art div, fallback glass, and content scrim layer correctly.
No surface paint, no Material elevation (already `Elevation="0"`), no ripple.
- **Fallback:** an absolutely-positioned full-bleed panel painted with our navy-glass (dark) or
navy-tint (light) treatment, with a border + `backdrop-filter`.
Neither needs anything MudBlazor's surface model provides. That observation drives the
recommendation.
---
## §2. Option A — Replace container + fallback with plain `<div>`
Drop `MudCard``<div class="deepdrft-track-card-container">` and the fallback `MudPaper`
`<div class="deepdrft-track-card-fallback">`. Keep `MudCardContent`, `MudText`, `MudChip`,
`MudFab` (none of these own a competing background once the card root is a plain div — and the
text/chip/fab already work today).
**Note:** `MudCardContent` technically expects a `MudCard` ancestor, but it only emits a
`<div class="mud-card-content">` with padding — no background, no JS, no required parent context.
It renders fine inside a plain div. If we want zero MudBlazor coupling in the shell, it can also
become a plain `<div class="deepdrft-track-card-content">` (the class already carries all the
real styling — padding, flex, z-index, scrim). Recommend replacing it too, for consistency.
### Pros
- **Removes the conflict at the source.** No `.mud-paper` background means no `!important`
anywhere. Section 8 backgrounds become plain single-class rules that win by default.
- **Re-enables CSS isolation as an option.** With plain divs, a future `TrackCard.razor.css`
*would* work (Blazor stamps the scope attribute on divs it renders). We are not required to use
it — section 8's public-only scoping is still convenient per the predecessor note — but the door
reopens. This satisfies the "design the seam" instinct: we are not just fixing today, we are
removing a structural block.
- **Precedent already exists in this codebase.** `NowPlayingCard.razor` is built entirely from
plain `<div>`s with a `.razor.css` and achieves the *exact aesthetic these cards are imitating*.
This option makes TrackCard structurally consistent with the component it was told to match.
The two should be siblings in construction, not one Material and one hand-rolled.
- **Lighter DOM + less CSS the browser must override.** Removes `.mud-card` / `.mud-paper`
rulesets from applying to these nodes entirely.
### Cons — what MudBlazor functionality is lost
- **Elevation shadow:** already `Elevation="0"` on both elements (current Razor). Nothing lost —
the design explicitly moved to flat-glass (predecessor §3d). A plain div is the honest
expression of `Elevation="0"`.
- **Ripple:** `MudCard`/`MudPaper` do **not** ripple (ripple is a `MudButton`/`MudFab`/`MudIconButton`
behavior). The card is not clickable as a whole — the only interactive element is the `MudFab`,
which stays and keeps its ripple. Nothing lost.
- **ARIA / semantics:** `MudCard` renders a plain `<div>` with no implicit role — it adds no ARIA
semantics a plain div lacks. The card conveys meaning through its text content and the labeled
FAB, not through a container role. Nothing lost. (If anything, a future enhancement could add an
explicit `role`/`aria-label` to the div — easier on a plain element than fighting MudBlazor's.)
- **Theme reactivity of the surface color:** we *want* to lose this — the whole problem is that
MudBlazor repaints the surface from the palette. Our section-8 rules are already theme-aware via
`.deepdrft-theme-dark` / `-light` wrappers, so theme switching still works.
- **Migration cost:** ~6 lines of Razor changed. Trivial.
**Assessment:** acceptable in full. There is no MudBlazor capability the track-card shell actually
uses. This is the option that resolves the root cause rather than the symptom.
---
## §3. Option B — Inline `Style="background: transparent"` on MudCard/MudPaper
Keep the MudBlazor components; pass an inline style to clear their background:
```razor
<MudCard Class="deepdrft-track-card-container" Elevation="0" Style="background: transparent;">
<MudPaper Class="deepdrft-track-card-fallback" Elevation="0" Style="background: transparent;">
```
Inline styles have specificity above any selector (and above `!important` from a stylesheet, for
non-`!important` inline declarations the inline wins over normal stylesheet rules; MudBlazor's
`.mud-paper` background is *not* `!important`, so inline transparent beats it cleanly).
But note: the fallback's *real* navy-glass paint then has to come from **somewhere**. If we set
the MudPaper transparent inline and paint via `.deepdrft-track-card-fallback` in CSS, we are
back to a CSS class trying to beat `.mud-paper` — except now `.mud-paper` is already transparent
(inline), so our class wins without `!important`. That works. The container is simpler: it just
wants transparent, which the inline gives directly.
### Pros
- No `!important`. Inline transparent neutralizes `.mud-paper`, then our classes paint normally.
- Smaller change than Option A (no element swap), keeps MudBlazor components if there's a reason
to (there isn't a strong one here).
- No risk of `MudCardContent`-without-`MudCard` edge cases.
### Cons
- **Scatters styling concern into Razor.** The whole point of section 8 is centralized,
theme-aware card styling. An inline `Style` on the component is a second place card appearance
is decided — exactly the maintenance split the team should avoid. Next person editing the card
background looks in section 8, doesn't find the override, and is confused.
- **Doesn't address the root mismatch.** We're still using a surface component as a layout shell
and then telling it "don't be a surface." It reads as an apology for the wrong component choice.
- **CSS isolation stays blocked** — MudBlazor still renders the inner HTML, so a `.razor.css`
still can't reach it. The seam stays closed.
- **Inline `transparent` is itself a quiet "magic override."** It's `!important` by another name —
a higher-specificity hammer applied at the element. Marginally cleaner than `!important` in a
sheet, but the same category of move: beating MudBlazor by force rather than not inviting the
fight.
**Assessment:** cleaner than `!important` strictly speaking, but trades one smell (sheet
`!important`) for a milder one (scattered inline overrides) and leaves the structural mismatch and
the blocked isolation seam intact. A lateral move, not a fix.
---
## §4. Option C — Override `--mud-palette-surface` locally on the container
Scope a CSS custom-property override to the card so MudBlazor's own
`background-color: var(--mud-palette-surface)` resolves to transparent inside the card subtree:
```css
.deepdrft-track-card-container {
--mud-palette-surface: transparent;
}
```
Because `.mud-paper` reads `var(--mud-palette-surface)` and custom properties inherit, both the
container's own paint and the nested `MudPaper` fallback would inherit the transparent value — no
`!important` needed (we're changing the *input* to MudBlazor's rule, not overriding the rule).
### Pros
- No `!important`, no inline styles, no element swap. One declaration.
- Works *with* MudBlazor's mechanism instead of against it — arguably the most "correct" CSS-vars
approach. We feed the palette variable the value we want for this subtree.
- Centralized in section 8.
### Cons
- **Blast radius is the whole subtree.** `--mud-palette-surface` is read by *many* MudBlazor
components, not just paper. Any MudBlazor component nested in the card that relies on
`--mud-palette-surface` (now or in future) silently renders transparent. Today the card nests
`MudChip` and `MudFab` — both read other palette vars (`--mud-palette-primary` etc.), but a
future addition (a `MudMenu`, a `MudTooltip` surface, a `MudPaper` accent) would inherit the
transparent surface and break subtly. This is a **spooky-action-at-a-distance** override: it
works today but plants a trap.
- **Fragile against MudBlazor internals.** It depends on `.mud-paper` continuing to read exactly
`--mud-palette-surface` for its background. If a MudBlazor version computes the surface
differently (e.g. a derived var, or `color-mix`), the override misses. Coupling our fix to a
framework implementation detail is a maintenance liability — the kind of thing that breaks on a
minor version bump with no compile error.
- **Still doesn't reopen CSS isolation.** Same as B — MudBlazor renders the HTML.
- **Obscure.** The next maintainer sees a card rendering transparent and has no obvious thread to
pull — the cause is a custom-property override three rules up, not a `background` declaration.
Low discoverability.
**Assessment:** the most elegant-looking, the most dangerous. It's a clever override that depends
on framework internals and leaks into every descendant. Reject for the trap it sets, despite the
aesthetics.
---
## §5. Option D — Keep `!important` with a clear comment
Leave the four `!important`s, add a comment explaining the MudBlazor specificity conflict, accept
it as documented technical debt.
### Pros
- Zero change, zero risk. The cards render correctly today.
- A good comment turns a mystery smell into a known, explained trade-off — which is genuinely
better than an *unexplained* `!important`.
### Cons
- **Doesn't meet the stated goal.** The task is explicitly to *eliminate* `!important` from
section 8. Documenting it is not eliminating it.
- **`!important` is contagious.** Once the card backgrounds are `!important`, any future rule that
needs to legitimately override them (a "now playing" highlight state on a card, a selected
state, a hover treatment) must *also* be `!important`, escalating the war. The predecessor note
already floats a genre-chip treatment and skeleton tints — feature growth on this card is
expected, and each new state inherits the `!important` tax.
- **Leaves the structural mismatch and blocked isolation seam in place** — same as B and C.
**Assessment:** honest, but it's the do-nothing option dressed as a decision. Acceptable only if
all other options were blocked, which they are not. The comment is worth keeping *as a fallback*
if Option A is somehow rejected.
---
## §6. Recommendation — Option A (plain `<div>` shell)
**Replace `MudCard` and the fallback `MudPaper` with plain `<div>`s. Replace `MudCardContent` with
a plain `<div>` too (it carries no behavior). Keep `MudText`, `MudChip`, `MudFab`.** This removes
all four `!important` declarations, preserves the glass-card aesthetic exactly (the section-8 paint
rules are unchanged except for dropping `!important`), is the most maintainable, and aligns
TrackCard structurally with `NowPlayingCard` — the very component it was designed to imitate.
Why A over the others, in one line each:
- **Over B (inline transparent):** A fixes the root mismatch and reopens the isolation seam; B
scatters overrides and leaves both problems.
- **Over C (palette-var override):** A has no blast radius and no coupling to MudBlazor internals;
C plants a transparent-surface trap for every future nested component.
- **Over D (keep `!important`):** A meets the stated goal; D doesn't.
Trade-off being accepted with A: TrackCard's shell stops being a MudBlazor component. That is a
*feature* here (the card never used MudBlazor's surface behavior), but it does mean the shell no
longer auto-reacts to a future palette change to `Surface` — which is exactly what we want, since
our theme-aware section-8 rules own the card's appearance. No real cost.
---
### 6.1 Exact Razor changes — `DeepDrftShared.Client/Components/TrackCard.razor`
Replace the current file body with the following. Only the three shell elements change
(`MudCard``div`, `MudPaper``div`, `MudCardContent``div`); all class hooks, `MudText`,
`MudChip`, and `MudFab` are preserved verbatim from the current file.
```razor
<div class="deepdrft-track-card-container">
@if (!string.IsNullOrEmpty(TrackModel?.ImagePath))
{
<div class="deepdrft-track-card-bg" style="background-image: url('@TrackModel.ImagePath');">
</div>
}
else
{
<div class="deepdrft-track-card-fallback"></div>
}
<div class="deepdrft-track-card-content">
<div class="deepdrft-track-info-top">
<MudText Typo="Typo.subtitle1"
Class="deepdrft-track-title text-truncate mb-1">
@TrackModel?.TrackName
</MudText>
<MudText Typo="Typo.caption"
Class="deepdrft-track-artist text-truncate mb-2">
@TrackModel?.Artist
</MudText>
</div>
<div class="deepdrft-track-info-middle">
@if (!string.IsNullOrEmpty(TrackModel?.Album))
{
<MudText Typo="Typo.caption"
Class="deepdrft-track-meta text-truncate">
@TrackModel.Album
</MudText>
}
@if (!string.IsNullOrEmpty(TrackModel?.Genre))
{
<MudChip T="string"
Size="Size.Small"
Variant="Variant.Filled"
Color="Color.Primary"
Class="deepdrft-genre-chip">
@TrackModel.Genre
</MudChip>
}
</div>
<div class="deepdrft-track-info-bottom">
@if (TrackModel?.ReleaseDate.HasValue == true)
{
<MudText Typo="Typo.caption"
Class="deepdrft-track-meta">
@TrackModel.ReleaseDate.Value.Year
</MudText>
}
else
{
<div></div>
}
<MudFab Color="Color.Primary"
Size="Size.Medium"
StartIcon="@PlayPauseIcon"
OnClick="@PlayClick"/>
</div>
</div>
</div>
```
Changes, precisely:
- Line 12: `<MudCard Class="deepdrft-track-card-container" Elevation="0">`
`<div class="deepdrft-track-card-container">`. (Drop `Elevation="0"` — meaningless on a div; the
flat look is the default.)
- Line 1113: `<MudPaper Class="deepdrft-track-card-fallback" Elevation="0"></MudPaper>`
`<div class="deepdrft-track-card-fallback"></div>`.
- Line 16: `<MudCardContent Class="deepdrft-track-card-content">`
`<div class="deepdrft-track-card-content">`.
- Line 72 (`</MudCard>`) → `</div>`; the `</MudCardContent>` close (line 70) → `</div>`.
No change to `TrackCard.razor.cs` — it references no MudBlazor card types (only `Icons`,
`TrackDto`, `EventCallback`). The `using MudBlazor;` stays (still used by `Icons.Material`).
### 6.2 Exact CSS changes — `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` section 8
Remove `!important` from all four declarations. No other change — the colors, layout, and
theme-aware scoping all stay exactly as they are.
**Line 216223** — container:
```css
.deepdrft-track-card-container {
width: 250px;
height: 250px;
min-width: 250px;
position: relative;
overflow: hidden;
background: transparent; /* was: transparent !important */
}
```
**Line 262269** — fallback base:
```css
.deepdrft-track-card-fallback {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--deepdrft-navy-mid, #162437); /* was: ... !important */
}
```
**Line 272276** — fallback dark:
```css
.deepdrft-theme-dark .deepdrft-track-card-fallback {
background: color-mix(in srgb, var(--deepdrft-navy) 55%, transparent); /* was: ... !important */
border: 1px solid rgba(250, 250, 248, 0.12);
backdrop-filter: blur(8px);
}
```
**Line 279282** — fallback light:
```css
.deepdrft-theme-light .deepdrft-track-card-fallback {
background: color-mix(in srgb, var(--deepdrft-navy) 8%, var(--deepdrft-white)); /* was: ... !important */
border: 1px solid var(--deepdrft-border);
}
```
After this change, the four backgrounds are plain single-class (and class-under-theme-wrapper)
rules painting plain `<div>`s. Nothing competes; nothing needs `!important`. The
`.deepdrft-theme-dark .deepdrft-track-card-fallback` selector is `0,2,0` and the base is `0,1,0`,
so the theme override correctly beats the base — the normal cascade, working as intended.
### 6.3 Verification checklist for the implementer
- Dark public site: card with album art shows art + scrim + green/off-white text; card without
art shows navy-glass fallback. No grey/teal surface flash on hydration (the base
`--deepdrft-navy-mid` fallback rule still covers the no-wrapper-class window).
- Light public site: fallback shows the navy-tint-on-white panel; text legible.
- Toggle dark/light: card repaints correctly (theme-wrapper-scoped rules still drive it).
- Confirm no other component references `.deepdrft-track-card-container` / `-fallback` expecting a
`.mud-paper` ancestor (grep — `TracksGallery.razor` wraps cards but only positions them via
`.deepdrft-track-gallery-item-center`).
- Confirm the rendered card root is now a plain `<div>` (devtools) with **no** `.mud-paper` /
`.mud-card` class, and that no `background ... !important` remains anywhere in section 8.
### 6.4 What this unblocks (not in scope, noted for the seam)
With a plain-div shell, the deferred polish items from the predecessor note become cleaner to
implement and could, if desired, migrate to a real `TrackCard.razor.css` (now functional):
genre-chip distinct treatment (predecessor §3a), skeleton tints (§3c), card hover/selected/now-
playing states. None are required by this spec; flagged so a future pass knows the seam is open.
---
## §7. Fallback position
If Option A is rejected for a reason not surfaced here (e.g. a policy that shell components must
remain MudBlazor for consistency with other cards), the next-best is **Option D with comments**
not B or C. B scatters the override and C couples to framework internals; a *documented*
`!important` is more honest and more maintainable than either clever hack. But A has no real cost
identified, so this fallback should not be needed.
```