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

29 KiB
Raw Blame History

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 — landed the navy-glass fallback + moss-green text treatment, scoped under .deepdrft-theme-dark / -light.
  • 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-containerMudGridMudItem.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.


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.


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.

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.

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.

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.

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)?