Dark Mode
Voice Complex
This skill governs BROWSER VISUAL RENDERING — warm palette, CSS token system, theme behavior. The warm-brown base (#1D1611) and terracotta accent (#D4875A) are the visual expression of the same warmth that runs through all Egregore registers.
Shared aesthetic: voice-bedrock (.claude/rules/voice-bedrock.md).
For terminal rendering: tui-design skill.
For product copy in rendered pages: product-voice skill.
For prose content in rendered pages: egregore-voice skill.
This skill is a behavioral constraint system for Egregore visual output. It exists to stop the most common failure mode before it happens: an otherwise correct dark palette being defeated by SSR-emitted inline styles.
When to invoke
User says: "dark mode", "theme toggle", "color system", "card surface", "make this work in dark mode", or any request to generate or modify browser-rendered visual output (HTML, CSS, React SSR, Egregore artifacts, design tokens)
Not this: TUI terminal output → tui-design · plain markdown files with no styled HTML
The Bitter Lesson
If React SSR renders style="color:#2A2A2A", dark mode loses. CSS variable overrides only work when the rendered value is itself a variable reference. If a color must adapt across themes, emit var(--token) at the render site. Not in comments. Not in a later stylesheet. In the actual rendered style.
Treat this as a hard constraint:
- Every theme-sensitive color in CSS or inline
style props must use var(--token).
- Theme state lives on
<html data-theme="...">.
- Theme restoration happens before paint, in
<head>.
Everything else is secondary.
When To Invoke
- Generating standalone HTML
- Building or editing React SSR output
- Writing CSS for Egregore artifacts
- Modifying markdown renderers that emit inline styles
- Adding a theme toggle
- Extending the design system with dark surfaces
- Producing any browser-rendered visual artifact
Not This
- TUI terminal output; use
/tui-design
- Plain markdown files that do not render styled HTML
- Backend or CLI code with no visual surface
- Non-rendered config changes unrelated to color or theme behavior
The Egregore Dark Palette
The palette stays warm. The base dark background is #1D1611, not pure black. Accent colors stay the same in both modes because terracotta and muted blue already survive the transition cleanly.
Core CSS Variables
| Token |
Light |
Dark |
Role |
--cream |
#F5F2ED |
#1D1611 |
Page background; inverted selection text in dark |
--black |
#16100B |
rgba(255, 255, 255, 0.92) |
Primary text, titles, completed badges |
--dark |
#2A2A2A |
rgba(255, 255, 255, 0.75) |
Body text, secondary copy, blocked badge fill |
--border |
#E0D8CC |
rgba(255, 255, 255, 0.06) |
Rules, card borders, checkbox outlines |
--muted |
#8a8578 |
rgba(255, 255, 255, 0.50) |
Metadata, labels, de-emphasized text |
--warm-gray |
#d4cfc5 |
rgba(255, 255, 255, 0.30) |
Tertiary marks, dim text, faint structure |
--terminal-bg |
#1D1611 |
#161210 |
Code blocks and terminal surfaces |
Dark-Only Surface Helpers
Use these when a component needs separation beyond the base variable set. The current artifacts shell uses --card-bg and --code-bg; --elevated-bg and --strong-border come from the same shipped token palette and are the right extensions when extra depth is needed.
| Token |
Light Fallback |
Dark |
Role |
--card-bg |
#FFFFFF |
#241E19 |
Cards, pills, theme toggle background |
--code-bg |
rgba(59, 45, 33, 0.06) |
rgba(212, 135, 90, 0.08) |
Inline code background |
--elevated-bg |
#FFFFFF |
#2A231D |
Raised panels or overlays above cards |
--strong-border |
#d4cfc5 |
rgba(255, 255, 255, 0.10) |
Emphasized borders when --border is too faint |
Palette Rules
- Warm character first: dark surfaces are brown-black, not blue-black and not pure black.
- Text opacity hierarchy is fixed:
0.92 primary, 0.75 secondary, 0.50 muted, 0.30 dim.
--terracotta stays #D4875A in both modes.
--blue-muted stays #7B9DB7 in both modes.
- Selection inverts: terracotta highlight remains, but dark mode switches the selected text to
#1D1611.
Anti-Patterns
| Pattern |
Why It Breaks |
Instead |
style={{ color: colors.black }} |
React resolves colors.black to a hex during SSR, so dark CSS cannot override it later |
style={{ color: 'var(--black)' }} or a class that resolves to var(--black) |
style={{ color: colors.muted }} in markdown or components |
Same SSR trap, just less obvious because muted copy often escapes review |
Emit style={{ color: 'var(--muted)' }} |
background: white on cards |
The card stays white in dark mode and blows out contrast |
background: var(--card-bg, white) with a dark override on [data-theme="dark"] |
border-bottom: 1px solid rgba(224, 216, 204, 0.5) |
You hardcoded a light border into a supposedly themeable component |
border-bottom: 1px solid var(--border) or var(--strong-border, var(--border)) |
#000000 or #111111 for dark backgrounds |
Cold, dead black clashes with Egregore's warm substrate |
Use #1D1611 or var(--cream) once dark mode remaps it |
opacity: 0.5 on already-colored text |
Multiplies contrast unpredictably and usually makes dark text too faint |
Use the precomputed text tokens: var(--muted) or var(--warm-gray) |
@media (prefers-color-scheme: dark) as the only mechanism |
No manual override, no persistence, no explicit state |
Use a three-state light / auto / dark system |
Separate .dark-* classes for every component |
Duplicates the design system and drifts fast |
Override variables once under [data-theme="dark"] and keep component styles shared |
Putting theme state on body or a wrapper div |
Inconsistent scope; portals, overlays, and SSR shells can fall out of sync |
Put data-theme on <html> |
| Restoring the saved theme after the page renders |
Causes a flash of the wrong theme |
Apply the saved theme in a blocking <head> script before paint |
Converting CSS files to variables but leaving inline colors.* references behind |
Mixed models create false confidence; one stray inline color is enough to break dark mode |
Grep every render path for colors. and hardcoded hex in inline styles, then replace with var(--token) |
Signature Patterns
- Variable-first inline styles. Inline styles are allowed only when they emit CSS variables, never resolved palette values.
data-theme on <html>. No body.dark, no component-local theme root.
- Three-state theme toggle. Support
light, auto, and dark, with persistence.
- Auto resolves OS preference first, then local-clock fallback. If the system exposes no preference, dark runs from
19:00 to 07:00.
- No-flash initialization. Restore theme state in
<head> before anything visible paints.
- Card elevation through background lightness, not shadow stacks. In dark mode, depth comes from surface contrast.
- Accent colors stay stable. Terracotta and muted blue do not get separate dark variants unless there is a proven contrast failure.
- Selection highlight inversion. The highlight stays terracotta; selected text flips to the dark background color.
Context Modes
Egregore Artifacts (React SSR)
- Start from
packages/egregore-artifacts/lib/shell.js, markdown.js, components.js, and tokens.js.
- In render functions, replace
colors.black, colors.muted, colors.dark, and similar inline values with 'var(--black)', 'var(--muted)', 'var(--dark)', and so on.
- Keep structural values such as spacing, font families, and dimensions in JS as normal. The rule is specifically about theme-sensitive colors.
- Put the dark variable remap in the shell, not on individual components.
Standalone HTML
- Emit a full
:root block plus a [data-theme="dark"] override block.
- Put the theme restore script in
<head> before visible markup.
- Mount the toggle in fixed position and hide it in print.
- Use CSS variable strings in any inline HTML styles you generate.
Tailwind CSS
- Configure
darkMode: ['selector', '[data-theme=\"dark\"]'].
- Keep the root selector on
<html>.
- Prefer CSS custom properties for brand tokens, then map Tailwind utilities to those variables.
- Do not sneak theme-sensitive hex colors into JSX
style props just because the rest of the page uses Tailwind.
Vanilla CSS
- Define light tokens in
:root.
- Override only variables under
[data-theme="dark"].
- Component rules should read like
color: var(--black) and border-color: var(--border).
Email HTML
- This is the one context where
@media (prefers-color-scheme: dark) with !important is correct.
- Do not build a JS toggle for email.
- Accept that support is partial and optimize for legibility, not full parity.
Calibration Examples
1. React Component Color Reference
Off target
h('span', { style: { color: colors.muted, fontSize: '13px' } }, `(${author})`)
On point
h('span', { style: { color: 'var(--muted)', fontSize: '13px' } }, `(${author})`)
Why: the first version serializes to a fixed hex during SSR. The second survives theme remapping.
2. Card Background And Elevation
Off target
.card {
background: white;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);
}
On point
.card {
background: var(--card-bg, white);
border: 1px solid var(--border);
}
[data-theme="dark"] .card {
--card-bg: #241E19;
}
Why: dark separation comes from a warmer surface step, not a generic shadow preset.
3. Code Block In A Markdown Renderer
Off target
h('code', {
style: {
background: 'rgba(59, 45, 33, 0.06)',
color: colors.black,
padding: '2px 5px',
},
}, text)
On point
h('code', {
className: 'eg-code',
}, text)
.eg-code {
background: var(--code-bg, rgba(59, 45, 33, 0.06));
color: var(--black);
}
[data-theme="dark"] .eg-code {
--code-bg: rgba(212, 135, 90, 0.08);
color: var(--terracotta);
}
Why: inline code needs a dedicated surface token in dark mode; a light-only rgba wash will disappear.
Toggle Reference Implementation
This is the canonical Egregore pattern: one HTML attribute, one variable override block, one small engine. Keep the structure; adapt only if the host environment forces it.
JS Engine
<script>
(function() {
var MODES = ['light', 'auto', 'dark'];
var ICONS = ['\u2600', '\u25D0', '\u263D'];
var LABELS = ['light', 'auto', 'dark'];
function resolveAuto() {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
if (window.matchMedia('(prefers-color-scheme: light)').matches) return 'light';
var h = new Date().getHours();
return (h >= 19 || h < 7) ? 'dark' : 'light';
}
function applyTheme(mode) {
var resolved = mode === 'auto' ? resolveAuto() : mode;
document.documentElement.setAttribute('data-theme', resolved);
}
function updateToggle(mode) {
var idx = MODES.indexOf(mode);
var btn = document.querySelector('.eg-theme-toggle');
if (!btn) return;
btn.querySelector('.eg-t-icon').textContent = ICONS[idx];
btn.querySelector('.eg-t-label').textContent = LABELS[idx];
}
var saved = localStorage.getItem('eg-theme-mode') || 'auto';
applyTheme(saved);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
if ((localStorage.getItem('eg-theme-mode') || 'auto') === 'auto') applyTheme('auto');
});
window.__egTheme = {
cycle: function() {
var cur = localStorage.getItem('eg-theme-mode') || 'auto';
var next = MODES[(MODES.indexOf(cur) + 1) % 3];
localStorage.setItem('eg-theme-mode', next);
applyTheme(next);
updateToggle(next);
},
init: function() { updateToggle(saved); }
};
})();
</script>
CSS Shell
:root {
--cream: #F5F2ED;
--black: #16100B;
--dark: #2A2A2A;
--border: #E0D8CC;
--muted: #8a8578;
--warm-gray: #d4cfc5;
--terminal-bg: #1D1611;
--terracotta: #D4875A;
--blue-muted: #7B9DB7;
}
[data-theme="dark"] {
--cream: #1D1611;
--black: rgba(255, 255, 255, 0.92);
--dark: rgba(255, 255, 255, 0.75);
--border: rgba(255, 255, 255, 0.06);
--muted: rgba(255, 255, 255, 0.50);
--warm-gray: rgba(255, 255, 255, 0.30);
--terminal-bg: #161210;
}
::selection { background: var(--terracotta); color: var(--cream); }
[data-theme="dark"] ::selection { color: #1D1611; }
.eg-theme-toggle {
position: fixed;
top: 1.25rem;
right: 1.25rem;
width: 36px;
height: 36px;
border-radius: 50px;
border: 1px solid var(--border);
background: var(--card-bg, white);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
line-height: 1;
transition: background 0.2s, border-color 0.2s, width 0.2s;
z-index: 100;
padding: 0;
gap: 6px;
overflow: hidden;
}
.eg-theme-toggle:hover {
border-color: var(--terracotta);
width: 92px;
padding: 0 12px;
}
[data-theme="dark"] .eg-theme-toggle { --card-bg: #241E19; }
.eg-theme-toggle .eg-t-icon { flex-shrink: 0; }
.eg-theme-toggle .eg-t-label {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.04em;
color: var(--muted);
white-space: nowrap;
width: 0;
overflow: hidden;
transition: width 0.2s, opacity 0.2s;
opacity: 0;
}
.eg-theme-toggle:hover .eg-t-label { width: 32px; opacity: 1; }
@media print {
body { background: white; }
.eg-artifact { padding: 1rem; max-width: none; }
.eg-card { break-inside: avoid; }
.eg-theme-toggle { display: none; }
}
@media (max-width: 640px) {
.eg-artifact { padding: 2rem 1.25rem 3rem; }
.eg-title { font-size: 28px; }
.eg-section-title { font-size: 20px; }
.eg-meta-row { gap: 10px; }
.eg-theme-toggle { top: 0.75rem; right: 0.75rem; width: 32px; height: 32px; font-size: 14px; }
}
HTML Hookup
<html lang="en" data-theme="light">
<head>
<!-- theme restore script goes here -->
</head>
<body>
<button class="eg-theme-toggle" aria-label="Toggle theme"
<span class="eg-t-icon">◐</span>
<span class="eg-t-label">auto</span>
</button>
<script>__egTheme.init();</script>
</body>
</html>
Pre-Ship Checklist
- Grep for hardcoded hex colors and
colors. references in render paths that emit inline styles.
- Verify every theme-sensitive inline color is a
var(--token) string.
- Verify both
:root and [data-theme="dark"] blocks exist.
- Verify
data-theme is set on <html>, not body.
- Verify the saved theme is restored in
<head> before visible markup paints.
- Verify the toggle cycles
light -> auto -> dark and persists to localStorage.
- Verify auto mode follows OS preference first and local time second.
- Verify no surface uses pure black or a cool gray dark background.
- Verify cards, code, and elevated surfaces separate by warm lightness steps rather than shadow spam.
- Verify selection, print, and mobile behavior still work after the theme system is added.
Reference Files
Read these conditionally, depending on the task:
packages/egregore-artifacts/lib/shell.js for the canonical shell structure, variable overrides, and toggle wiring.
packages/egregore-artifacts/lib/tokens.js for the shipped light and dark token values.
packages/egregore-artifacts/lib/markdown.js for the inline-style var(--token) conversion pattern.
packages/egregore-artifacts/lib/components.js for component-level inline style usage that must stay variable-first.
packages/design-system/tokens.css for the broader design-system token vocabulary.
If the current checkout does not yet contain the merged dark-mode implementation in those artifact files, inspect the shipped worktree copies under .claude/worktrees/soul-document-design/packages/egregore-artifacts/lib/.
1---2name: dark-mode-23description: Use when generating or modifying any visual output that renders in a browser or artifact viewer: HTML pages, CSS, React SSR components, Egregore artifacts, markdown renderers, web components, design-system tokens, Tailwind themes, or standalone demos. Triggers on requests involving dark mode, theme toggles, color systems, card surfaces, browser rendering, or visual polish. The load-bearing lesson: if React SSR emits an inline hex color through `style={}`, CSS dark-mode overrides cannot reach it, so every theme-sensitive color must be emitted as `var(--token)` rather than a resolved hex. Do not use for TUI output, plain markdown files, or non-visual code.4---56# Dark Mode78## Voice Complex910This skill governs BROWSER VISUAL RENDERING — warm palette, CSS token system, theme behavior. The warm-brown base (#1D1611) and terracotta accent (#D4875A) are the visual expression of the same warmth that runs through all Egregore registers.11Shared aesthetic: voice-bedrock (`.claude/rules/voice-bedrock.md`).12For terminal rendering: `tui-design` skill.13For product copy in rendered pages: `product-voice` skill.14For prose content in rendered pages: `egregore-voice` skill.1516This skill is a behavioral constraint system for Egregore visual output. It exists to stop the most common failure mode before it happens: an otherwise correct dark palette being defeated by SSR-emitted inline styles.1718## When to invoke1920User says: "dark mode", "theme toggle", "color system", "card surface", "make this work in dark mode", or any request to generate or modify browser-rendered visual output (HTML, CSS, React SSR, Egregore artifacts, design tokens)21Not this: TUI terminal output → `tui-design` · plain markdown files with no styled HTML2223## The Bitter Lesson2425**If React SSR renders `style="color:#2A2A2A"`, dark mode loses.** CSS variable overrides only work when the rendered value is itself a variable reference. If a color must adapt across themes, emit `var(--token)` at the render site. Not in comments. Not in a later stylesheet. In the actual rendered style.2627Treat this as a hard constraint:28291. Every theme-sensitive color in CSS or inline `style` props must use `var(--token)`.302. Theme state lives on `<html data-theme="...">`.313. Theme restoration happens before paint, in `<head>`.3233Everything else is secondary.3435## When To Invoke3637- Generating standalone HTML38- Building or editing React SSR output39- Writing CSS for Egregore artifacts40- Modifying markdown renderers that emit inline styles41- Adding a theme toggle42- Extending the design system with dark surfaces43- Producing any browser-rendered visual artifact4445## Not This4647- TUI terminal output; use `/tui-design`48- Plain markdown files that do not render styled HTML49- Backend or CLI code with no visual surface50- Non-rendered config changes unrelated to color or theme behavior5152## The Egregore Dark Palette5354The palette stays warm. The base dark background is `#1D1611`, not pure black. Accent colors stay the same in both modes because terracotta and muted blue already survive the transition cleanly.5556### Core CSS Variables5758| Token | Light | Dark | Role |59|---|---|---|---|60| `--cream` | `#F5F2ED` | `#1D1611` | Page background; inverted selection text in dark |61| `--black` | `#16100B` | `rgba(255, 255, 255, 0.92)` | Primary text, titles, completed badges |62| `--dark` | `#2A2A2A` | `rgba(255, 255, 255, 0.75)` | Body text, secondary copy, blocked badge fill |63| `--border` | `#E0D8CC` | `rgba(255, 255, 255, 0.06)` | Rules, card borders, checkbox outlines |64| `--muted` | `#8a8578` | `rgba(255, 255, 255, 0.50)` | Metadata, labels, de-emphasized text |65| `--warm-gray` | `#d4cfc5` | `rgba(255, 255, 255, 0.30)` | Tertiary marks, dim text, faint structure |66| `--terminal-bg` | `#1D1611` | `#161210` | Code blocks and terminal surfaces |6768### Dark-Only Surface Helpers6970Use these when a component needs separation beyond the base variable set. The current artifacts shell uses `--card-bg` and `--code-bg`; `--elevated-bg` and `--strong-border` come from the same shipped token palette and are the right extensions when extra depth is needed.7172| Token | Light Fallback | Dark | Role |73|---|---|---|---|74| `--card-bg` | `#FFFFFF` | `#241E19` | Cards, pills, theme toggle background |75| `--code-bg` | `rgba(59, 45, 33, 0.06)` | `rgba(212, 135, 90, 0.08)` | Inline code background |76| `--elevated-bg` | `#FFFFFF` | `#2A231D` | Raised panels or overlays above cards |77| `--strong-border` | `#d4cfc5` | `rgba(255, 255, 255, 0.10)` | Emphasized borders when `--border` is too faint |7879### Palette Rules8081- Warm character first: dark surfaces are brown-black, not blue-black and not pure black.82- Text opacity hierarchy is fixed: `0.92` primary, `0.75` secondary, `0.50` muted, `0.30` dim.83- `--terracotta` stays `#D4875A` in both modes.84- `--blue-muted` stays `#7B9DB7` in both modes.85- Selection inverts: terracotta highlight remains, but dark mode switches the selected text to `#1D1611`.8687## Anti-Patterns8889| Pattern | Why It Breaks | Instead |90|---|---|---|91| `style={{ color: colors.black }}` | React resolves `colors.black` to a hex during SSR, so dark CSS cannot override it later | `style={{ color: 'var(--black)' }}` or a class that resolves to `var(--black)` |92| `style={{ color: colors.muted }}` in markdown or components | Same SSR trap, just less obvious because muted copy often escapes review | Emit `style={{ color: 'var(--muted)' }}` |93| `background: white` on cards | The card stays white in dark mode and blows out contrast | `background: var(--card-bg, white)` with a dark override on `[data-theme="dark"]` |94| `border-bottom: 1px solid rgba(224, 216, 204, 0.5)` | You hardcoded a light border into a supposedly themeable component | `border-bottom: 1px solid var(--border)` or `var(--strong-border, var(--border))` |95| `#000000` or `#111111` for dark backgrounds | Cold, dead black clashes with Egregore's warm substrate | Use `#1D1611` or `var(--cream)` once dark mode remaps it |96| `opacity: 0.5` on already-colored text | Multiplies contrast unpredictably and usually makes dark text too faint | Use the precomputed text tokens: `var(--muted)` or `var(--warm-gray)` |97| `@media (prefers-color-scheme: dark)` as the only mechanism | No manual override, no persistence, no explicit state | Use a three-state `light / auto / dark` system |98| Separate `.dark-*` classes for every component | Duplicates the design system and drifts fast | Override variables once under `[data-theme="dark"]` and keep component styles shared |99| Putting theme state on `body` or a wrapper div | Inconsistent scope; portals, overlays, and SSR shells can fall out of sync | Put `data-theme` on `<html>` |100| Restoring the saved theme after the page renders | Causes a flash of the wrong theme | Apply the saved theme in a blocking `<head>` script before paint |101| Converting CSS files to variables but leaving inline `colors.*` references behind | Mixed models create false confidence; one stray inline color is enough to break dark mode | Grep every render path for `colors.` and hardcoded hex in inline styles, then replace with `var(--token)` |102103## Signature Patterns1041051. Variable-first inline styles. Inline styles are allowed only when they emit CSS variables, never resolved palette values.1062. `data-theme` on `<html>`. No `body.dark`, no component-local theme root.1073. Three-state theme toggle. Support `light`, `auto`, and `dark`, with persistence.1084. Auto resolves OS preference first, then local-clock fallback. If the system exposes no preference, dark runs from `19:00` to `07:00`.1095. No-flash initialization. Restore theme state in `<head>` before anything visible paints.1106. Card elevation through background lightness, not shadow stacks. In dark mode, depth comes from surface contrast.1117. Accent colors stay stable. Terracotta and muted blue do not get separate dark variants unless there is a proven contrast failure.1128. Selection highlight inversion. The highlight stays terracotta; selected text flips to the dark background color.113114## Context Modes115116### Egregore Artifacts (React SSR)117118- Start from `packages/egregore-artifacts/lib/shell.js`, `markdown.js`, `components.js`, and `tokens.js`.119- In render functions, replace `colors.black`, `colors.muted`, `colors.dark`, and similar inline values with `'var(--black)'`, `'var(--muted)'`, `'var(--dark)'`, and so on.120- Keep structural values such as spacing, font families, and dimensions in JS as normal. The rule is specifically about theme-sensitive colors.121- Put the dark variable remap in the shell, not on individual components.122123### Standalone HTML124125- Emit a full `:root` block plus a `[data-theme="dark"]` override block.126- Put the theme restore script in `<head>` before visible markup.127- Mount the toggle in fixed position and hide it in print.128- Use CSS variable strings in any inline HTML styles you generate.129130### Tailwind CSS131132- Configure `darkMode: ['selector', '[data-theme=\"dark\"]']`.133- Keep the root selector on `<html>`.134- Prefer CSS custom properties for brand tokens, then map Tailwind utilities to those variables.135- Do not sneak theme-sensitive hex colors into JSX `style` props just because the rest of the page uses Tailwind.136137### Vanilla CSS138139- Define light tokens in `:root`.140- Override only variables under `[data-theme="dark"]`.141- Component rules should read like `color: var(--black)` and `border-color: var(--border)`.142143### Email HTML144145- This is the one context where `@media (prefers-color-scheme: dark)` with `!important` is correct.146- Do not build a JS toggle for email.147- Accept that support is partial and optimize for legibility, not full parity.148149## Calibration Examples150151### 1. React Component Color Reference152153**Off target**154155```js156h('span', { style: { color: colors.muted, fontSize: '13px' } }, `(${author})`)157```158159**On point**160161```js162h('span', { style: { color: 'var(--muted)', fontSize: '13px' } }, `(${author})`)163```164165Why: the first version serializes to a fixed hex during SSR. The second survives theme remapping.166167### 2. Card Background And Elevation168169**Off target**170171```css172.card {173 background: white;174 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);175}176```177178**On point**179180```css181.card {182 background: var(--card-bg, white);183 border: 1px solid var(--border);184}185186[data-theme="dark"] .card {187 --card-bg: #241E19;188}189```190191Why: dark separation comes from a warmer surface step, not a generic shadow preset.192193### 3. Code Block In A Markdown Renderer194195**Off target**196197```js198h('code', {199 style: {200 background: 'rgba(59, 45, 33, 0.06)',201 color: colors.black,202 padding: '2px 5px',203 },204}, text)205```206207**On point**208209```js210h('code', {211 className: 'eg-code',212}, text)213```214215```css216.eg-code {217 background: var(--code-bg, rgba(59, 45, 33, 0.06));218 color: var(--black);219}220221[data-theme="dark"] .eg-code {222 --code-bg: rgba(212, 135, 90, 0.08);223 color: var(--terracotta);224}225```226227Why: inline code needs a dedicated surface token in dark mode; a light-only rgba wash will disappear.228229## Toggle Reference Implementation230231This is the canonical Egregore pattern: one HTML attribute, one variable override block, one small engine. Keep the structure; adapt only if the host environment forces it.232233### JS Engine234235```html236<script>237 (function() {238 var MODES = ['light', 'auto', 'dark'];239 var ICONS = ['\u2600', '\u25D0', '\u263D'];240 var LABELS = ['light', 'auto', 'dark'];241242 function resolveAuto() {243 if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';244 if (window.matchMedia('(prefers-color-scheme: light)').matches) return 'light';245 var h = new Date().getHours();246 return (h >= 19 || h < 7) ? 'dark' : 'light';247 }248249 function applyTheme(mode) {250 var resolved = mode === 'auto' ? resolveAuto() : mode;251 document.documentElement.setAttribute('data-theme', resolved);252 }253254 function updateToggle(mode) {255 var idx = MODES.indexOf(mode);256 var btn = document.querySelector('.eg-theme-toggle');257 if (!btn) return;258 btn.querySelector('.eg-t-icon').textContent = ICONS[idx];259 btn.querySelector('.eg-t-label').textContent = LABELS[idx];260 }261262 var saved = localStorage.getItem('eg-theme-mode') || 'auto';263 applyTheme(saved);264265 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {266 if ((localStorage.getItem('eg-theme-mode') || 'auto') === 'auto') applyTheme('auto');267 });268269 window.__egTheme = {270 cycle: function() {271 var cur = localStorage.getItem('eg-theme-mode') || 'auto';272 var next = MODES[(MODES.indexOf(cur) + 1) % 3];273 localStorage.setItem('eg-theme-mode', next);274 applyTheme(next);275 updateToggle(next);276 },277 init: function() { updateToggle(saved); }278 };279 })();280</script>281```282283### CSS Shell284285```css286:root {287 --cream: #F5F2ED;288 --black: #16100B;289 --dark: #2A2A2A;290 --border: #E0D8CC;291 --muted: #8a8578;292 --warm-gray: #d4cfc5;293 --terminal-bg: #1D1611;294 --terracotta: #D4875A;295 --blue-muted: #7B9DB7;296}297298[data-theme="dark"] {299 --cream: #1D1611;300 --black: rgba(255, 255, 255, 0.92);301 --dark: rgba(255, 255, 255, 0.75);302 --border: rgba(255, 255, 255, 0.06);303 --muted: rgba(255, 255, 255, 0.50);304 --warm-gray: rgba(255, 255, 255, 0.30);305 --terminal-bg: #161210;306}307308::selection { background: var(--terracotta); color: var(--cream); }309[data-theme="dark"] ::selection { color: #1D1611; }310311.eg-theme-toggle {312 position: fixed;313 top: 1.25rem;314 right: 1.25rem;315 width: 36px;316 height: 36px;317 border-radius: 50px;318 border: 1px solid var(--border);319 background: var(--card-bg, white);320 cursor: pointer;321 display: flex;322 align-items: center;323 justify-content: center;324 font-size: 15px;325 line-height: 1;326 transition: background 0.2s, border-color 0.2s, width 0.2s;327 z-index: 100;328 padding: 0;329 gap: 6px;330 overflow: hidden;331}332.eg-theme-toggle:hover {333 border-color: var(--terracotta);334 width: 92px;335 padding: 0 12px;336}337[data-theme="dark"] .eg-theme-toggle { --card-bg: #241E19; }338.eg-theme-toggle .eg-t-icon { flex-shrink: 0; }339.eg-theme-toggle .eg-t-label {340 font-family: var(--font-mono);341 font-size: 10px;342 letter-spacing: 0.04em;343 color: var(--muted);344 white-space: nowrap;345 width: 0;346 overflow: hidden;347 transition: width 0.2s, opacity 0.2s;348 opacity: 0;349}350.eg-theme-toggle:hover .eg-t-label { width: 32px; opacity: 1; }351352@media print {353 body { background: white; }354 .eg-artifact { padding: 1rem; max-width: none; }355 .eg-card { break-inside: avoid; }356 .eg-theme-toggle { display: none; }357}358359@media (max-width: 640px) {360 .eg-artifact { padding: 2rem 1.25rem 3rem; }361 .eg-title { font-size: 28px; }362 .eg-section-title { font-size: 20px; }363 .eg-meta-row { gap: 10px; }364 .eg-theme-toggle { top: 0.75rem; right: 0.75rem; width: 32px; height: 32px; font-size: 14px; }365}366```367368### HTML Hookup369370```html371<html lang="en" data-theme="light">372 <head>373 <!-- theme restore script goes here -->374 </head>375 <body>376 <button class="eg-theme-toggle" aria-label="Toggle theme" onclick="__egTheme.cycle()">377 <span class="eg-t-icon">◐</span>378 <span class="eg-t-label">auto</span>379 </button>380 <script>__egTheme.init();</script>381 </body>382</html>383```384385## Pre-Ship Checklist3863871. Grep for hardcoded hex colors and `colors.` references in render paths that emit inline styles.3882. Verify every theme-sensitive inline color is a `var(--token)` string.3893. Verify both `:root` and `[data-theme="dark"]` blocks exist.3904. Verify `data-theme` is set on `<html>`, not `body`.3915. Verify the saved theme is restored in `<head>` before visible markup paints.3926. Verify the toggle cycles `light -> auto -> dark` and persists to `localStorage`.3937. Verify auto mode follows OS preference first and local time second.3948. Verify no surface uses pure black or a cool gray dark background.3959. Verify cards, code, and elevated surfaces separate by warm lightness steps rather than shadow spam.39610. Verify selection, print, and mobile behavior still work after the theme system is added.397398## Reference Files399400Read these conditionally, depending on the task:401402- `packages/egregore-artifacts/lib/shell.js` for the canonical shell structure, variable overrides, and toggle wiring.403- `packages/egregore-artifacts/lib/tokens.js` for the shipped light and dark token values.404- `packages/egregore-artifacts/lib/markdown.js` for the inline-style `var(--token)` conversion pattern.405- `packages/egregore-artifacts/lib/components.js` for component-level inline style usage that must stay variable-first.406- `packages/design-system/tokens.css` for the broader design-system token vocabulary.407408If the current checkout does not yet contain the merged dark-mode implementation in those artifact files, inspect the shipped worktree copies under `.claude/worktrees/soul-document-design/packages/egregore-artifacts/lib/`.