diff --git a/PLAN.md b/PLAN.md index 23bedcf..b507582 100644 --- a/PLAN.md +++ b/PLAN.md @@ -342,6 +342,44 @@ the open-question set: `product-notes/phase-17-player-queue-view.md`. --- +## Phase 18 — Theme / Dark-Mode Remediation (DRY token pass) + +A punch-list of six theming symptoms Daniel reported — five in dark mode, one in light — +that all trace to **three** root causes in how component/page CSS bypasses the theme-aware +token layer and binds *constant* source tokens instead. Resolved as one coherent token pass, +not six per-component patches. Full design, architecture map, root-cause analysis, token +table, and track breakdown: `product-notes/theme-dark-mode-remediation.md`. + +**Root-cause collapse (six symptoms → three causes):** +- **Cause 1 — neutral surfaces don't invert.** Home hero-left + footer (#3) and About light + sections (#4) hardcode `background: var(--deepdrft-white)` / text on `--deepdrft-navy` — + brand *constants* that are identical in `:root` and `.deepdrft-theme-dark`, so they cannot + flip. Fix: bind a theme-aware `--deepdrft-page-surface` / `--deepdrft-page-text` alias. The + inversion must stay **neutral to the intentionally navy/green decorative sections** + (`.section-dark`, `.split-left`, `.cta-banner`, hero overlays) — a classify-then-recolor job. +- **Cause 2 — play chip binds a constant grey.** `PlayStateIcon.razor.css` `.icon-container` + hardcodes `--deepdrft-soft` (#e3e7ec). One shared component drives the release-hero chip, the + Cut track rows, *and* the player bar — so it reads "greyed-out" over dark heroes (#5) and "too + bright" on the navy player surface (#6). Fix: theme-aware `--deepdrft-play-chip` (moss-green + + navy glyph in dark) with a translucent `--deepdrft-play-chip-soft` override for the player bar. +- **Cause 3 — no theme-aware popover surface.** Light-mode default MudPopovers read "too dark" + (#1); there's no token for the wanted "desaturated navy." Fix: a `--deepdrft-popover-surface` + token; leave the bespoke `--deepdrft-panel-ground` panels alone. + +**Sequenced as four tracks, `T1 → {T2, T3, T4}`.** T1 (additive token foundation in +`deepdrft-tokens.css`) is the cold-start prerequisite; T2 (neutral-surface inversion), T3 +(play-chip theming), T4 (popover token) fan out behind it and are mutually independent. Pure +CSS-token pass — no source code, data layer, or streaming-seam changes. Prior art: +`product-notes/track-card-theming.md` solved this exact class of theme-aware recolor once +already; this generalizes the fix from one component to the pattern. + +**Open questions for Daniel (spec §5):** (1) dark neutral surface = navy *ground* (continuous +field — recommended for footer/hero) vs. *elevated* navy-mid (distinct panels); (2) popover +target distance from white (recommend a light `color-mix(navy ~8%, white)` wash). Exact green +opacity + muted-text mixes are tune-on-screen details, not decision gates. + +--- + ## Working with this file - **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing. diff --git a/product-notes/theme-dark-mode-remediation.md b/product-notes/theme-dark-mode-remediation.md new file mode 100644 index 0000000..48f6b0b --- /dev/null +++ b/product-notes/theme-dark-mode-remediation.md @@ -0,0 +1,243 @@ +# Theme / Dark-Mode Remediation — DRY token pass + +Status: proposed. Author: product-designer. Date: 2026-06-19. Implementer: TBD (separate delegation). + +A design analysis of the DeepDrft theme system, focused on the dark theme, with a DRY +remediation plan that resolves a punch-list of six reported symptoms through **shared +theme tokens** rather than per-component patches. Daniel reported the symptoms; this note +maps the architecture, isolates the root causes, and sequences the fix. + +Prior art this borrows from: `product-notes/track-card-theming.md` (landed 2026-06-05) — +the same class of problem (theme-aware recolor under `.deepdrft-theme-dark`, legible in +both palettes) solved once already with the same mechanism. This note generalizes that +fix from one component to the recurring pattern behind it. + +--- + +## 1. How the theme system is wired today (the map) + +There are **three** colour layers, and the bugs all live in how the third one bypasses the +first two. + +### Layer A — MudBlazor palettes (C#) +`DeepDrftShared.Client/Common/DeepDrftPalettes.cs` defines `PaletteLight Light`, +`PaletteDark Dark` (+ `CmsLight`, `EmbedLight`, `EmbedDark`). `MainLayout.razor` mounts +``. MudBlazor +injects these as `--mud-palette-*` CSS variables that **flip automatically** when +`IsDarkMode` toggles. This is the part that works: anything reading `--mud-palette-surface`, +`--mud-palette-background`, `--mud-palette-text-primary` inverts correctly for free. + +### Layer B — DeepDrft design tokens (CSS) +`DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css` defines two token families: + +- **Source tokens** — raw brand colours, *constant across both themes*: + `--deepdrft-navy (#112338)`, `--deepdrft-white (#FAFAF8)`, `--deepdrft-green-accent + (#3D7A68)`, `--deepdrft-soft (#e3e7ec)`, etc. These never change between light and dark. +- **Theme-aware aliases** — `--theme-surface`, `--theme-surface-soft`, `--theme-primary…senary`, + `--gradient-base/accent/warm/light`, `--deepdrft-surface`, `--deepdrft-background`. These + **are** redefined inside the `.deepdrft-theme-dark` block (the wrapper class + `MainLayout.ThemeWrapperClass` puts on the root div), so they flip. + +The token file's own header comment establishes the intended discipline: source tokens are +"source of truth"; theme-aware aliases are what page CSS is *supposed* to consume so it +"resolve[s] coherently across themes." + +### Layer C — component / page CSS +Scoped `*.razor.css` files and the global `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css`. +**This is where the discipline breaks.** Page sections that should track the theme surface +instead reach straight past Layer B and bind to the *constant source tokens* of Layer A +(`--deepdrft-white`, `--deepdrft-navy`, `--deepdrft-soft`). A constant cannot invert — so +these surfaces stay light-on-navy-site no matter the mode. + +--- + +## 2. Root causes (six symptoms → three causes) + +The six reported symptoms collapse to **three** root causes. That collapse is the whole +point of doing this as one coherent pass rather than six patches. + +### Cause 1 — "neutral surface" sections bind to constant source tokens, so they never invert +*(Symptoms: Home hero-left + footer (#3); About light sections (#4))* + +These rules are the smoking gun (all bind a constant, not a theme alias): + +- `Home.razor.css` — `.hero-left`, `.section`, `.section-divider`, `.section-body p`, + `.medium-card`, `.split-right`, `.connect-*` → `background: var(--deepdrft-white)`, + text `color: var(--deepdrft-navy)`. +- `About.razor.css` — `.hero-left`, `.hero-image-pane`, `.bio`/process gradients → + `background: var(--deepdrft-white)`, text on `--deepdrft-navy`. +- `DeepDrftFooter.razor.css` — `.deepdrft-footer` → `background: var(--deepdrft-white)`, + logo/links text on `--deepdrft-navy` / `--deepdrft-muted`. + +`--deepdrft-white` is `#FAFAF8` in **both** `:root` and `.deepdrft-theme-dark` — it is a +brand constant, never re-aliased. So in dark mode these read as bright off-white panels with +dark text floating in a navy site. The fix is **not** to hardcode a dark colour; it is to +**bind these surfaces to a theme-aware alias** that already inverts. + +**Critical nuance Daniel flagged:** the fix must be *neutral to the existing navy and green +accent sections.* The page already has sections that are **intentionally** navy/green in +both modes — `.section-dark` (navy), `.split-left` (green), `.cta-banner` (navy), the +`ReleaseHeroOverlay` (dark image). Those are decorative-by-design and must **not** be touched +by the inversion. Only the "default page surface" sections (the ones currently white-because- +light) should flip. This is a *classification* problem first, a recolor second: separate +"neutral surface" from "decorative accent" and only re-token the former. + +### Cause 2 — the play-icon chip background binds `--deepdrft-soft` (constant light grey) +*(Symptoms: greyed-out play icon on release heroes / track lists (#5); too-bright player-bar play button (#6))* + +`PlayStateIcon.razor.css` `.icon-container` hardcodes `background-color: var(--deepdrft-soft)` +(`#e3e7ec` — a light grey, constant across both themes). `PlayStateIcon` is the **single** +glyph component used by the release heroes, the Cut track rows, *and* the player bar. So one +constant drives all of these: + +- Over a **dark hero image / navy track list** → the light-grey chip reads dull and + "greyed-out" (#5). Daniel wants: **moss-green chip background, navy play glyph** in dark mode. +- On the **bright player-surface** → the same light-grey chip reads "very bright" against the + navy dock (#6). Daniel wants: **same green, much less opaque** (a translucent green wash, + not a solid bright fill). + +Both are the same `--deepdrft-soft` constant failing to be theme-aware. One component, one +token — fix the token's dark-mode value and both surfaces resolve. Note the two contexts want +*different green treatments* (solid green chip on the hero; translucent green wash in the +player bar), so the chip background should be a **token the player-bar context can override**, +not a single flat value — see §3. + +### Cause 3 — popover surface has no theme-aware token; light mode reads "too dark" +*(Symptom: light-theme popover background too dark, wants desaturated navy (#1))* + +Two different popover families exist and they are styled inconsistently: + +- **Bespoke panels** (visualizer controls, queue, privacy) deliberately use + `--deepdrft-panel-ground` (`#1a1c22`, a dark charcoal) for their dark-glass chrome. These + are *meant* to be dark in both modes — leave them. +- **MudBlazor default popovers** (selects, menus, tooltips, the share popover body) inherit + `--mud-palette-surface`. In light mode `Surface = #FAFAF8`, but elevation-overlay tinting + + the `--deepdrft-panel-ground` charcoal leaking through shared chrome is making them read + darker/muddier than intended. Daniel's ask — "a more desaturated navy" — says the *target* + isn't pure white; it's a **soft desaturated-navy surface**. There is no token for that today, + so each popover improvises. + +The fix is a **dedicated theme-aware popover-surface token** (`--deepdrft-popover-surface`) +with a desaturated-navy value in light mode and the existing panel-ground in dark mode, bound +once at the MudPopover surface so every default popover picks it up. + +--- + +## 3. The DRY remediation — token structure + +The unifying move: **page/component CSS must bind theme-aware aliases, and any surface that +must invert gets a named alias in `deepdrft-tokens.css` (defined twice — `:root` + `.deepdrft-theme-dark`).** +No surface colour is hardcoded at the component level. This is exactly the Layer-B discipline +the token file's header already declares; the work is making the consumers obey it. + +### New / clarified tokens (in `deepdrft-tokens.css`) + +| Token | Light (`:root`) | Dark (`.deepdrft-theme-dark`) | Replaces | +|---|---|---|---| +| `--deepdrft-page-surface` | `var(--deepdrft-white)` | `var(--deepdrft-navy)` (ground) or `--deepdrft-navy-mid` (elevated) | the literal `--deepdrft-white` on neutral page sections | +| `--deepdrft-page-text` | `var(--deepdrft-navy)` | `var(--deepdrft-white)` | the literal `--deepdrft-navy` text on neutral sections | +| `--deepdrft-page-text-muted` | `var(--deepdrft-muted)` | `color-mix(... lighter)` | muted body/eyebrow text that must stay legible on dark | +| `--deepdrft-play-chip` | `var(--deepdrft-soft)` | `var(--deepdrft-green-accent)` | `.icon-container` background | +| `--deepdrft-play-glyph` | (current) | `var(--deepdrft-navy)` | play glyph colour in dark | +| `--deepdrft-play-chip-soft` | derived | `color-mix(green-accent ~30%, transparent)` | player-bar translucent variant (#6) | +| `--deepdrft-popover-surface` | desaturated navy (e.g. `color-mix(navy 8%, white)`) | `var(--deepdrft-panel-ground)` | MudPopover default surface (#1) | + +Values above are *direction, not final*. Per project memory (decorative-palette contrast +targets the actual WCAG threshold for the element type — large text 3:1, pushing toward +vibrancy), the implementer should tune the exact mixes on screen; the **structure** is the +deliverable here, the hex is theirs to land. + +### Why tokens, not per-component fixes + +- **One source of truth per concept.** "Neutral page surface," "play chip," "popover surface" + each become *one* token. A future page that needs a neutral surface binds the token and + inverts for free — no new dark-mode rule to remember (the backfill-cliff smell the + *design-for-adaptability* memory warns against). +- **Neutrality to accents is structural, not vigilance-based.** Because only neutral-surface + sections get re-tokened and the decorative navy/green sections keep their explicit brand + colours, the inversion *cannot* accidentally flip a section that's meant to stay navy. The + classification is encoded in *which token a section binds*, not in a reviewer noticing. +- **Player-bar vs. hero divergence is expressible.** Cause 2 needs the same green in two + opacities. A `--deepdrft-play-chip` token + a `--deepdrft-play-chip-soft` override the + player-bar context sets means one green, two contexts, zero duplication. + +### What stays untouched (the neutrality guardrail) +`.section-dark`, `.split-left`, `.cta-banner` (Home + About), `ReleaseHeroOverlay` dark-image +chrome, and the bespoke `--deepdrft-panel-ground` panels (visualizer/queue/privacy) keep their +explicit brand colours. They are decorative-by-design and already correct in both modes. The +remediation must **not** route them through the new neutral-surface tokens. + +--- + +## 4. Track / wave breakdown (for clean dispatch) + +Sequenced so the token layer lands first and the component re-pointing fans out behind it. +Tracks T2–T4 are parallel once T1 is in. + +### T1 — Token foundation *(cold-start prerequisite)* +Add the theme-aware tokens from §3 to `deepdrft-tokens.css` — each defined in **both** `:root` +and `.deepdrft-theme-dark`. No component consumes them yet; this is a pure additive token +slice. Tune the dark-mode values on screen. **Load-bearing for everything below.** +- Scope: `DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css` only. +- Acceptance: tokens resolve to the right value in each mode (verify via devtools); no visual + change yet (nothing binds them). + +### T2 — Neutral-surface inversion *(Cause 1 → symptoms #3, #4)* +Re-point the neutral page-surface sections from constant source tokens to `--deepdrft-page-surface` +/ `--deepdrft-page-text` / `--deepdrft-page-text-muted`. **Classify first** — only the neutral +sections; leave `.section-dark` / `.split-left` / `.cta-banner` / hero-overlay alone. +- Scope: `Home.razor.css`, `About.razor.css`, `DeepDrftFooter.razor.css`. +- Acceptance: in dark mode the Home hero-left, the medium grid, the footer, and the About + light sections render dark-surface/light-text; the navy and green accent sections are + visually unchanged; light mode is pixel-identical to today. +- Risk: the appbar already has dark-mode handling (`deepdrft-styles.css §5`); confirm the + footer/hero changes don't double-invert anything the appbar rules already cover. + +### T3 — Play-chip theming *(Cause 2 → symptoms #5, #6)* +Re-point `.icon-container` background from `--deepdrft-soft` to `--deepdrft-play-chip`; set the +dark play glyph to `--deepdrft-play-glyph` (navy); in the **player-bar context only**, override +the chip to the translucent `--deepdrft-play-chip-soft`. +- Scope: `PlayStateIcon.razor.css` (+ a player-bar-scoped override, likely in + `AudioPlayerBar.razor.css` or a context class on the bar's `.icon-container`). +- Acceptance (dark mode): release-hero + Cut-track-row play chips are **moss-green with a navy + glyph**; the player-bar play button is the **same green but markedly less opaque**; light + mode unchanged. Confirm hover states still read. +- Note: `PlayStateIcon` is shared — verify the chip change is acceptable on **every** mount + (heroes, track rows, player bar) and that the player-bar override is the only context-specific + divergence. + +### T4 — Popover surface token *(Cause 3 → symptom #1)* +Introduce `--deepdrft-popover-surface` and bind MudBlazor's default popover surface to it so +light-mode popovers read as soft desaturated-navy rather than the current too-dark muddle. +**Do not** touch the bespoke `--deepdrft-panel-ground` panels. +- Scope: `deepdrft-styles.css` (a `.mud-popover` / popover-surface rule binding the new token); + token already added in T1. +- Acceptance: light-mode default popovers (selects/menus/share body) render desaturated-navy; + dark-mode popovers unchanged; the visualizer/queue/privacy panels are untouched. +- Open question (resolve during T4): confirm whether the "too dark" popover is a MudBlazor + elevation-overlay artifact or panel-ground leakage — the fix differs slightly (override the + overlay tint vs. set the surface). One devtools inspection settles it; flagged so the + implementer checks rather than guesses. + +### Dependency shape +`T1 → {T2, T3, T4}`. T1 is the only cold-start item. T2/T3/T4 are independent of each other +and can land in any order or in parallel once T1 is in. None of them touch source code, the +data layer, or the streaming seam — this is a pure CSS-token pass. + +--- + +## 5. Open questions for Daniel + +1. **Dark neutral-surface = ground or elevated?** Should the inverted Home/About/footer + surfaces be the navy *ground* (`--deepdrft-navy`, matching the site background — sections + dissolve into one continuous dark field) or *elevated* navy-mid (`--deepdrft-navy-mid` — + sections read as distinct raised panels)? Recommend **ground** for the footer/hero (continuous + field, less busy) and let the medium-cards stay as bordered panels on that ground. This is a + taste call; flag for Daniel. +2. **Popover target colour (#1).** "Desaturated navy" — how far from white? Recommend a light + wash (`color-mix(navy ~8%, white)`) so it stays clearly a light-mode surface, not a dark one. + Confirm direction on screen. +3. Everything else (exact green opacity for the player-bar chip, exact muted-text mix) is a + tune-on-screen detail, not a decision gate. + +These are the only items that change the shape of the work; the rest is mechanical.