mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
Dark mode grows from an 8-line recipe into a parity protocol
references/dark-mode.md: per-token re-derivation table (elevation gets LIGHTER, accent chroma drops as lightness rises, hue never moves, body weight -50), the exact dual-block emission shape ([data-mode=dark] plus a prefers-color-scheme auto block guarded by :root:not([data-mode]), with the no-flash toggle note and color-scheme), and the dark discipline: shadows become lightness or inset rings (glow stays a THEME carve-out, named), cards sit lighter than canvas, one heading colour, never CSS invert on raster, kit multiply assets skip dark. Both modes run the full gate sweep and the stamp records modes: light+dark. color.md's recipe shrinks to a pointer and its 12-16 vs 12-18 paper disagreement unifies at 12-18. Dual-mode stays strictly opt-in: a dark theme is not a dual-mode page. SKILL.md gains the Step 6 bullet and the conditional load entry.
This commit is contained in:
@@ -236,7 +236,7 @@ A custom system is complete (palette + pairing + axes), never a colour swap; its
|
||||
|
||||
**Every build:** [`references/typography.md`](references/typography.md) · [`references/color.md`](references/color.md) · [`references/layout-and-space.md`](references/layout-and-space.md) · [`references/motion.md`](references/motion.md) · [`references/copy.md`](references/copy.md) · [`references/anti-patterns.md`](references/anti-patterns.md).
|
||||
|
||||
**Conditionally (be honest, no defensive pre-loads):** [`references/microinteractions.md`](references/microinteractions.md) when anything is interactive (most pages); [`references/interaction-and-states.md`](references/interaction-and-states.md) for stateful UI; [`references/responsive.md`](references/responsive.md) when mobile is in scope; [`references/structure.md`](references/structure.md) only when deviating from a named macrostructure; [`references/assets.md`](references/assets.md) only when an enrichment needs an external asset; [`references/texture.md`](references/texture.md) only when the picked theme earns texture (Riso, Carnival, Arcade, faint Newsprint) or a custom draw has print lineage; [`references/scroll-choreography.md`](references/scroll-choreography.md) only when the brief asks for scroll story / cinematic pacing or the macro is Feature-stack / Narrative Workflow; [`references/custom-theme.md`](references/custom-theme.md) only on the custom route; [`references/design-md.md`](references/design-md.md) only when the user asks to lock the system; [`references/preview-examples.md`](references/preview-examples.md) only if the Step 5 spec is not scaffolding enough.
|
||||
**Conditionally (be honest, no defensive pre-loads):** [`references/microinteractions.md`](references/microinteractions.md) when anything is interactive (most pages); [`references/interaction-and-states.md`](references/interaction-and-states.md) for stateful UI; [`references/responsive.md`](references/responsive.md) when mobile is in scope; [`references/structure.md`](references/structure.md) only when deviating from a named macrostructure; [`references/assets.md`](references/assets.md) only when an enrichment needs an external asset; [`references/texture.md`](references/texture.md) only when the picked theme earns texture (Riso, Carnival, Arcade, faint Newsprint) or a custom draw has print lineage; [`references/scroll-choreography.md`](references/scroll-choreography.md) only when the brief asks for scroll story / cinematic pacing or the macro is Feature-stack / Narrative Workflow; [`references/dark-mode.md`](references/dark-mode.md) only when the user asks for both modes; [`references/custom-theme.md`](references/custom-theme.md) only on the custom route; [`references/design-md.md`](references/design-md.md) only when the user asks to lock the system; [`references/preview-examples.md`](references/preview-examples.md) only if the Step 5 spec is not scaffolding enough.
|
||||
|
||||
**At the end only:** [`references/slop-test.md`](references/slop-test.md) strictly at Step 7 (pre-loading it costs thousands of tokens for nothing; `anti-patterns.md` is the pre-emit list); [`references/contract.md`](references/contract.md) at handoff; [`references/export-formats.md`](references/export-formats.md) only on `design.md` projects.
|
||||
|
||||
@@ -290,6 +290,7 @@ Emit code that satisfies the tone and the structural fingerprint. Match code com
|
||||
- **Append to project memory:** update `.hallmark/log.json` (schema in Step 2), newest first, trimmed to 20. Create `.hallmark/` if needed; respect any existing `.gitignore`.
|
||||
- **Never clobber an existing global stylesheet.** Entry stylesheets (`app/globals.css`, `src/index.css`, `src/styles/global.css`) are **append-only**: keep `@tailwind` / `@import "tailwindcss"` directives in place, add Hallmark's `:root` block and base rules below them, keep any new `@import` at the very top above all rules, and reuse the project's own token names (`--background`, `--foreground`, a Tailwind `@theme`) where they exist. Full rewrite only on explicit request: silently removing a framework's CSS entry directives un-styles the entire app. See [`references/contract.md`](references/contract.md).
|
||||
- **Always emit `tokens.css`** at the project root with every `--color-*`, `--font-*`, `--space-*`, `--text-*`, `--ease-*`, `--dur-*`, `--rule-*`, and `--radius-*` token used, imported by the page CSS (or included by the project's entry point). Even single-page builds. On `design.md` projects, also refresh the `## Exports` section with all four formats (tokens.css, Tailwind v4 `@theme`, DTCG `tokens.json`, shadcn/ui variables) per [`references/export-formats.md`](references/export-formats.md).
|
||||
- **Dark mode is opt-in.** When the user asks for both modes, load [`references/dark-mode.md`](references/dark-mode.md) and emit the dual token blocks (parity by re-derivation, never inversion); the stamp records `modes: light+dark`. A dark THEME is not dual-mode.
|
||||
- **`design.md` stays opt-in.** Only the explicit ask ("lock the system", "give me a design.md", "make this portable") triggers [`references/design-md.md`](references/design-md.md); page scope only. If `design.md` exists, refresh its `## Exports` instead of overwriting.
|
||||
|
||||
### 7. The slop test
|
||||
|
||||
@@ -13,7 +13,7 @@ Most AI-generated UI fails on colour. It picks blue. It uses pure black. It draw
|
||||
|
||||
A complete Hallmark palette has four layers.
|
||||
|
||||
1. **Paper** — the base surface. `oklch(96–98% 0.005–0.015 <anchor hue>)` for light mode, `oklch(12–16% 0.008–0.015 <anchor hue>)` for dark.
|
||||
1. **Paper** — the base surface. `oklch(96–98% 0.005–0.015 <anchor hue>)` for light mode, `oklch(12–18% 0.008–0.015 <anchor hue>)` for dark.
|
||||
2. **Ink** — the primary text. `oklch(16–22% 0.005–0.015 <anchor hue>)` for light mode, `oklch(92–96% 0.005–0.01 <anchor hue>)` for dark.
|
||||
3. **Neutrals** — 5 to 9 steps between Paper and Ink, each with the anchor's chroma tint at low values (0.005–0.015).
|
||||
4. **Accent** — one saturated colour with meaningful chroma (0.12–0.22). Used for links, active states, highlights, focus rings. Never as a background fill that covers more than a few percent of the surface.
|
||||
@@ -63,12 +63,7 @@ Verify with the browser devtools vision-deficiency emulator before shipping. Whe
|
||||
|
||||
## Dark mode recipe
|
||||
|
||||
- Paper: lightness 12–18% (not `#000`).
|
||||
- Ink: lightness 92–96% (not `#fff`).
|
||||
- Body font-weight: reduce by 50 units (400 → 350) to compensate for the optical weight of light text on dark.
|
||||
- Accent: reduce chroma by 0.02–0.04; increase lightness by 5–10%.
|
||||
- Elevation: higher surfaces are *lighter*, not darker. Add ~3% lightness per level.
|
||||
- Never switch the hue between modes. Keep the anchor. Only lightness and chroma move.
|
||||
Paper 12-18% (never `#000`) · ink 92-96% (never `#fff`) · elevation is LIGHTER (+~3% L per level) · accent chroma down 0.02-0.04 with L up 5-10% · hue never moves. The full per-token derivation table, dual-mode emission blocks, and dark discipline (shadows, cards, images, charts) live in [`dark-mode.md`](dark-mode.md); load it whenever the user asks for both modes.
|
||||
|
||||
## Bans
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Dark mode - parity, never inversion
|
||||
|
||||
Loaded only when the user asks for both modes ("dark mode too", "light and dark") or the pre-flight finds an existing mode system to respect. A dark THEME (Midnight, Ledger, Aurora) is not a dual-mode page; this file is for pages that ship BOTH modes from one token system. Never emit dual-mode unprompted.
|
||||
|
||||
## The stance
|
||||
|
||||
Dark mode is a second, equally designed rendering of the same system - not `filter: invert()`, not "flip the greys". Every token is re-derived by recipe; every contrast target holds identically in both modes; the anchor hue never moves. If a token would fail its contrast pairing after re-derivation, fix the derivation, never the requirement.
|
||||
|
||||
## Token re-derivation (per token, from the light value)
|
||||
|
||||
| Token | Dark derivation |
|
||||
| --- | --- |
|
||||
| `--color-paper` | L 12-18%, anchor-tinted (chroma 0.008-0.015). Never `#000`. |
|
||||
| `--color-paper-2/3` | ELEVATION IS LIGHTER: +3% L per level above paper (light mode steps darker; dark mode steps lighter). |
|
||||
| `--color-ink` | L 92-96%, anchor-tinted. Never `#fff`. |
|
||||
| `--color-ink-2` / muted / neutral | re-space the L steps between the new paper and ink so the typographic depth reads even; keep every chroma tint. |
|
||||
| `--color-rule` | L ~26-34%; hairlines must survive on the dark ground (check against paper-2, not just paper). |
|
||||
| `--color-accent` | chroma -0.02 to -0.04, L +5-10% (saturated light-mode accents glare on dark). |
|
||||
| `--color-accent-ink` | re-verify: >= 4.5:1 body / 3:1 large on the new accent fill. |
|
||||
| `--color-focus` | follows the accent derivation; ring contrast >= 3:1 against BOTH papers. |
|
||||
| hue | NEVER moves between modes, on any token. |
|
||||
|
||||
Plus one non-token move: reduce body `font-weight` by 50 units (400 to 350) in dark; light-on-dark text reads optically heavier.
|
||||
|
||||
## Emission
|
||||
|
||||
`tokens.css` carries the light block as-is, then exactly two additions:
|
||||
|
||||
```css
|
||||
[data-mode="dark"] { /* only the tokens that move, re-derived per the table */ }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-mode]) { /* the same overrides, for OS-preference auto mode */ }
|
||||
}
|
||||
```
|
||||
|
||||
The `[data-mode]` attribute wins over the media query by construction (the media block only applies when no explicit mode is set). Toggle: one button setting `document.documentElement.dataset.mode`, persisted to `localStorage`, read before first paint in a 4-line inline script so the page never flashes the wrong mode. `color-scheme: light dark` on `:root` so form controls and scrollbars follow.
|
||||
|
||||
## Dark discipline (the rules that change)
|
||||
|
||||
- **Shadows off.** Elevation is lightness (the +3% ladder) or an inset hairline ring (`box-shadow: inset 0 0 0 1px var(--color-rule)`). A drop shadow on dark reads as glow; glow is legal only under the atmospheric genre carve-out (atmospheric.md § glow-lift), which is a THEME behaviour, not a mode behaviour.
|
||||
- **Cards lighter than canvas.** `paper-2` on `paper`, always; a card darker than its ground reads as a hole.
|
||||
- **One heading colour.** Headings all run `--color-ink`; per-heading colour variety that worked on light fragments on dark.
|
||||
- **Images.** Never CSS `invert`/`brightness` on raster images. Ship `-dark` asset variants (the imagery kit or tier-e generation), or wrap the image in a `paper-2` well so the light image reads as intentional plate. Kit brush/blob multiply assets skip dark mode entirely (multiply dies on dark grounds; texture.md notes the overlay swap for grain).
|
||||
- **Charts.** Marks re-derive like accents (chroma down, L up); gridlines from the dark `--color-rule`; see data-viz.md § Dark.
|
||||
|
||||
## Verification
|
||||
|
||||
Both modes run the full gate sweep: contrast gates 40-41 on every pairing in BOTH modes (the render tier only sees the default mode; check the dark pairings numerically), gate 7 (no pure black/white) binds on the dark block too, and the stamp records `modes: light+dark` so the log knows.
|
||||
Reference in New Issue
Block a user