8.0 KiB
T34 Spike: Theme Dark-Mode Feasibility Findings
Date: 2026-06-18
Task: Verify whether the external ZB.MOM.WW.Theme shell can go dark via data-bs-theme CSS override.
1. CSS Load Order
Confirmed from src/ZB.MOM.WW.ScadaBridge.Host/Components/App.razor:
bootstrap.min.css (line 8)
bootstrap-icons.css (line 9)
<ThemeHead /> (line 10) → injects theme.css then layout.css
Host.styles.css (line 11)
site.css (line 12) ← our override point
site.css loads after theme.css and layout.css. A [data-bs-theme="dark"] block in site.css has sufficient specificity to shadow every :root token defined in theme.css, because [attr] selector specificity (0,1,0) beats :root (0,0,1) at equal cascade layer.
2. Token Inventory (theme.css :root)
All tokens defined in the :root block of ~/.nuget/packages/zb.mom.ww.theme/0.3.1/staticwebassets/css/theme.css:
Surfaces & ink
| Token | Light value | Role |
|---|---|---|
--paper |
#f4f4f1 |
page background |
--card |
#ffffff |
raised surfaces, bars, table heads |
--ink |
#1b1d21 |
primary text |
--ink-soft |
#5a6066 |
secondary text, labels |
--ink-faint |
#8b9097 |
tertiary text, captions |
--rule |
#e4e4df |
hairline borders, row dividers |
--rule-strong |
#d2d2cb |
emphasised hairlines, bar underline, pills |
Accent
| Token | Light value | Role |
|---|---|---|
--accent |
#2f5fd0 |
links, sort arrows, primary actions |
--accent-deep |
#1e3f99 |
hover/pressed accent |
Status — foreground
| Token | Light value |
|---|---|
--ok |
#2f9e44 |
--warn |
#e8920c |
--bad |
#e03131 |
--idle |
#868e96 |
Status — tinted backgrounds
| Token | Light value |
|---|---|
--ok-bg |
#e9f6ec |
--warn-bg |
#fdf1dd |
--bad-bg |
#fceaea |
--idle-bg |
#eef0f2 |
Status — borders
| Token | Light value |
|---|---|
--ok-border |
#c6e6cd |
--warn-border |
#efd6a6 |
--bad-border |
#eec3c3 |
--ok-border-soft |
#bfe3c6 |
--warn-border-soft |
#f0d9ab |
--bad-border-soft |
#f0c0c0 |
Warning ink
| Token | Light value |
|---|---|
--warn-ink |
#b56a00 |
--warn-ink-deep |
#8a5a00 |
Info tint
| Token | Light value |
|---|---|
--info-bg |
#e7ecfb |
--info-border |
#cdd9f7 |
Neutral surface washes
| Token | Light value | Role |
|---|---|---|
--zebra-bg |
#fbfbf9 |
even-row / sticky-head fill |
--hover-bg |
#f3f6fd |
row / rail-link hover |
--active-bg |
#eef2fc |
active rail-link fill |
Bootstrap overrides (derive from above)
| Token | Value |
|---|---|
--bs-body-bg |
var(--paper) |
--bs-body-color |
var(--ink) |
--bs-body-font-family |
var(--sans) |
--bs-body-font-size |
0.9rem |
--bs-primary |
var(--accent) |
--bs-border-color |
var(--rule) |
--bs-emphasis-color |
var(--ink) |
3. Rail Tokenization Audit (layout.css)
Every colour-bearing rule in the side-rail uses CSS custom properties. No hard-coded hex or rgb values appear anywhere in layout.css. Key rules:
Rail background and border:
.side-rail {
background: var(--card);
border-right: 1px solid var(--rule-strong);
}
Brand block:
.side-rail .brand {
color: var(--ink);
border-bottom: 1px solid var(--rule);
}
.side-rail .brand .mark { color: var(--accent); }
Section eyebrow / toggle:
.rail-eyebrow-toggle { color: var(--ink-faint); }
.rail-eyebrow-toggle:hover { color: var(--ink); }
.rail-section > summary::before { color: var(--ink-faint); }
Nav links (default / hover / active):
.rail-link { color: var(--ink-soft); }
.rail-link:hover { background: var(--hover-bg); color: var(--ink); }
.rail-link.active { background: var(--active-bg); border-left-color: var(--accent); color: var(--accent-deep); }
Rail foot / session block:
.rail-foot { border-top: 1px solid var(--rule); }
.rail-btn { color: var(--ink-soft); background: var(--card); border: 1px solid var(--rule-strong); }
.rail-btn:hover { border-color: var(--accent); color: var(--accent); }
.rail-roles { color: var(--ink-faint); }
.rail-ico { color: var(--ink-faint); }
Login title:
.login-title { color: var(--ink); }
4. VERDICT
RAIL TOKENIZES → dark feasible by token override.
Every rail background, text, and border colour in layout.css resolves through a CSS custom property from the token set. Overriding the token values under [data-bs-theme="dark"] in site.css (which loads after theme.css/layout.css) will fully recolour the side-rail, app chrome, panels, data tables, chips, and status elements — no Theme-package coordination required.
5. Concrete Dark Token Override List
Paste this entire block under [data-bs-theme="dark"] in site.css. Accent colour (--accent) is kept at #4d7fe8 — a lighter shade of the brand blue that reads well on dark backgrounds while preserving brand identity.
[data-bs-theme="dark"] {
/* Surfaces & ink */
--paper: #1b1d21;
--card: #25282d;
--ink: #e6e8ec;
--ink-soft: #9aa0aa;
--ink-faint: #6b727c;
--rule: #33373e;
--rule-strong: #3a3f46;
/* Accent — lightened for contrast on dark */
--accent: #4d7fe8;
--accent-deep: #7aa0ef;
/* Status — foregrounds unchanged (inherently visible on dark) */
--ok: #40b856;
--warn: #f0a030;
--bad: #f05050;
--idle: #7a8290;
/* Status — tinted backgrounds (dark surface tints) */
--ok-bg: #1a2e1e;
--warn-bg: #2e2210;
--bad-bg: #2e1616;
--idle-bg: #25282d;
/* Status — borders on dark tints */
--ok-border: #2a5030;
--warn-border: #4a3510;
--bad-border: #4a2020;
--ok-border-soft: #264a2c;
--warn-border-soft: #44300e;
--bad-border-soft: #44201e;
/* Warning ink — brightened amber for dark contrast */
--warn-ink: #d4891a;
--warn-ink-deep: #b8741a;
/* Info tint */
--info-bg: #1a2240;
--info-border: #2a3660;
/* Neutral surface washes */
--zebra-bg: #202328;
--hover-bg: #2a2e38;
--active-bg: #1e2640;
/* Bootstrap bridge tokens */
--bs-body-bg: var(--paper);
--bs-body-color: var(--ink);
--bs-border-color: var(--rule);
--bs-emphasis-color: var(--ink);
}
6. Caveats
-
Bootstrap intrinsic dark utilities: Bootstrap 5.3 has its own
[data-bs-theme="dark"]rules for components (alerts, badges, modals, etc.). Our token block sits in site.css at cascade level 0; Bootstrap's own dark-mode block in bootstrap.min.css (loaded earlier) will also fire. The Bootstrap rules and our token rules are additive and complementary — they should not conflict, but any Bootstrap component that ignores our tokens and uses Bootstrap's built-in dark-mode palette may have a slightly different feel. Inspect on first run. -
bodygradient in theme.css: Thebodyrule in theme.css is:body { background: radial-gradient(1200px 480px at 88% -8%, var(--card) 0%, rgba(255,255,255,0) 70%), var(--paper); }On dark mode,
rgba(255,255,255,0)in the gradient is transparent white and will remain unchanged. The gradient will still render;var(--card)will resolve to the dark card value and the effect will be a subtle lighter sheen in the top-right corner — acceptable, but the gradient can be suppressed in site.css for a flatter dark background if preferred. -
Status tints on dark: The status background tokens (
--ok-bg,--warn-bg,--bad-bg) are deliberately dark-surface equivalents (very low-saturation tints). Verify contrast ratios of status chip text against these backgrounds before shipping. -
data-bs-themeplacement: The attribute must be set on<html>(or<body>) so the CSS selector matches all descendants. The toggle implementation (next task) should writedocument.documentElement.setAttribute('data-bs-theme', 'dark').