A light theme, switchable — all twenty pages - #51
Merged
Conversation
Seven decisions recorded, and the measurement that forced the main one: the token block's four-stop ramp cannot be reused on a light ground. --c-mid paints every link and falls from 7.48:1 to 2.47:1; --c-firm to 1.49:1. So the ramp is re-picked and its axis flips — on dark brightness is confidence, on light depth is. Scope is all twenty pages. A light theme means every colour a page paints must come from a swappable token, and 172 literals sit outside the token declarations today — 37 on prose pages, 135 on the four decks. The palette is eleven values; the 172 are the work. Two deliberate exceptions, both recorded so they are not read as oversights: --lcd does not flip, because the transport is a physical object and a real readout is dark whatever the body is made of; and --c-weak stays below 3:1 in both themes, non-text-only, because darkening it until it passes stops it reading as tentative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five tasks. Three rulings recorded, the load-bearing one being that this needs two fences rather than one: the pre-paint script must run in <head> above the stylesheet while every other script on these pages sits at the end of the body, so they cannot share a block. The measurement that makes the site work small: the 37 prose literals are only three things — .seg's pressed background on all sixteen pages, a primary button hover on three, and eighteen SVG fills. The first two are one colour typed twice, #1b2231 and #1b2333, two in the blue channel apart; they become one token rather than two light values for one visual idea. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pre-flight scan finding. blockFor splices the closing brace immediately before the end marker when the variant equals closes, which is "page" — so whichever rule is written last is the one closed for pages and left open for decks, because a deck adds --warn, --slab and --lcd and closes :root itself. Written after the dark rule as this task originally said, the light half would be the one left hanging and a deck's three extra tokens would land in the light palette. Order costs nothing: :root[data-theme="light"] is (0,2,0) against :root's (0,1,0), so the light values win wherever they sit. Also shows the dark rule in full so the no-closing-brace contract is visible, and names header contract's next version (v4) instead of leaving it implicit.
Re-pin @robertblust/design to v0.10.0, add rb-theme beside rb-lang in design.config.json, and run npm run design so every prose page carries the v5 design tokens plus the new theme boot and theme fences. Each page declares var theme next to var lang, wires #thLight/#thDark beside the language control, and tokenises its remaining literal colours (.btn.primary:hover's background and the inline mark's fill) so nothing outside :root is hardcoded any more. The language control's sibling div now sits inside a .switchers wrapper that carries id="langind": the pinned navOrder check requires the language switcher to be nav's last child, and adding a second segmented control after it would otherwise displace it. verify/check.mjs turns on contrast and noFlash (keyed on rb-theme) for every prose page; export-og.mjs clears any stored theme before each screenshot so share cards render dark regardless of a later default change. privacy/ names rb-theme beside rb-lang, in both languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patch release fixes noFlash so it actually catches a theme-boot script that runs after a stylesheet. No fence changes, so design:check stays at a fixed point.
…k in fact - design tokens fence bumped to v6 (light --press #E2E8F2 -> #E7ECF4, clears 4.52:1 against --c-mid) and theme fence to v2 (carryTheme now carries the theme param only when themeStored() returns a value), regenerated on all eight pages via `npm run design` against @robertblust/design. - export-og.mjs now sets rb-theme to "dark" explicitly instead of clearing the key, which only ever inherited whatever the default happened to be. og-check.mjs gained a dependency-free PNG decoder that fails if a committed card's background is the light ground rather than the pinned dark one. og.sha stamps moved because the recipe hashes page bytes and the fence bump changed those bytes; og.png bytes are unchanged — verified byte-for-byte against HEAD before and after re-rendering. Verified against a local build of @robertblust/design v0.10.2; package.json and package-lock.json remain pinned to v0.10.1 until the real tag is cut.
Fences were already synced to the v0.10.2 tokens/theme fixes; this just moves the pinned tag forward to match.
The deck chrome's 126 literals collapse to 24 distinct values, of which 17 are shared by all four decks and become tokens. One of the 17 is #1b2231, already shipped as --press in Plan A, so twelve of the hundred occurrences are a substitution rather than a new name. Three false-positive classes were excluded from that count and are recorded because the first sweep counted all three: token declarations, HTML entities (“ is a curly quote whose #8220 looks exactly like a hex colour), and hex values quoted inside a fence's own prose explaining a palette decision. --lcd is declared in both halves with the same value rather than omitted from the light one. A real machine with a pale body still has a dark readout; if the light half simply lacked --lcd, a future reader could not tell a decision from an oversight, and the next person to complete the palette would add one. The seventeen light values are proposals derived from the approved Device mockup, and they are the part of this plan to look at first, on a rendered deck, before anything else is built on them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pre-flight scan finding, and it is Critical. The Device decision keeps --lcd dark in both themes, but the digits inside it are painted with var(--c-mid) and the message with var(--c-flag), and both of those flip. Measured against the permanently dark --lcd, the light theme would print 0.76rem digits at 3.67:1 and a message at 3.33:1 — inside the one element the whole metaphor exists to preserve. The separator is worse and already is: #4c566e on --lcd is 2.68:1 today, in both themes. The readout gets its own constant palette instead: --lcd-ink, --lcd-faint, --lcd-flag, declared identically in both halves beside --lcd. A real instrument's digits do not change colour because the room did — so this makes the metaphor truer rather than weaker, and it fixes a separator that has been failing since before the theme work started.
#4c566e is painted only on the LCD, so --deck-faint had no remaining use once the readout got its own constant palette; it becomes --lcd-faint outright, and rises from 2.68:1 to 5.25:1 in the process. #9db0ff is confirmed not painted on the readout, so --deck-accent stands as a token that flips.
It survived in the two token declarations and the test's name list after the table already said it had become --lcd-faint. Sixteen deck tokens that flip, four invariant readout tokens, and no name that means two things.
Giving a fence variants when it already shipped without them breaks every deployed page. lib/sync.mjs reads the marker's variant word as input; the sixteen prose pages all say 'shared'; 'shared' is no longer legal. planFences throws before either design or design:check writes or compares anything, so npm run design cannot repair it and CI goes red for the whole repo. The rewrite is unambiguous today because all sixteen are the page variant — the decks do not carry these fences until Step 2 — so it is a sed, done before the sync rather than discovered by it.
Pin @robertblust/design v0.11.0, which ships theme boot/theme fences with page and deck variants, and sync it across all sixteen prose pages and both decks. Each deck gains the theme control in its transport bar, drops its own --lcd declaration in favour of the package's, and re-points every literal colour (its own diagram marks and the speaker-notes drawer) at the existing --deck-* / base tokens instead. verify/check.mjs now asserts contrast and noFlash on both decks. The transport's slab does not yet pale in light theme --deck-* theming reached the readout and diagram content but --slab is still a single, page-owned, theme-invariant value; left alone per plan and reported rather than patched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The package now owns --slab and --warn; drop each deck's own declarations so the shared block's light-theme values (a pale slab, a still-dark LCD) actually take effect instead of being shadowed by the page.
design tokens v9 changed every fenced page's bytes, so og:check reported the cards stale. Re-rendered: every og.png came back byte-identical and the new dark-card check passes, so only the stamps moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.11.2 fixes the deck transport bar overflowing at phone widths behind
body{overflow:hidden}, leaving pushed-off controls unreachable — including
the theme control itself. Re-pinning rewrites deck transport to v7 on both
decks; re-stamping their share cards is a byte-identical render, not a
visual change. transportFits turns the new check on for both decks at
320-430px so a future overflow like this one fails verify instead of
shipping silently.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan A of the theme spec. This site's prose pages now carry a light/dark switch, from
@robertblust/designv0.10.2.How it works. A
.segcontrol beside the language toggle, sun and moon as inline SVG. The choice lives inlocalStorageunderrb-themeand rides to the sibling domains on links the way the language already does — added at click time, onmousedownas well asclick, so a middle-click carries it too; the arriving page adopts it, stores it, and cleans the address bar.Dark stays the default and
prefers-color-schemeis never read: dark is the design, and light is something a reader asks for.Two fences, because one of them has to beat first paint.
theme bootruns in<head>above the stylesheet — a browser applies CSS as it parses, so a theme applied after that repaints the whole page in the wrong palette on every navigation.themehandles storage, carry and the control at the end of the body.The palette. Reusing the dark ramp on a light ground was measured and refused:
--c-midpaints every link and falls to 2.45:1,--c-firmto 1.49:1. So the ramp's axis flips — on dark brightness is confidence, on light depth is.--c-weakstays deliberately below 3:1 in both themes, because its job is "a candidate, considered but not accepted".The literals are gone. Every colour this site painted outside its token declarations is now a token: the
.segpressed background, the primary button's hover, and eighteen inline SVG fills.#1b2231and#1b2333turned out to be one colour typed twice, two apart in the blue channel; they became--press. Zero literals remain on any prose page. The four decks keep theirs — they are a separate plan.What the reviews caught, which is most of the story.
noFlashwas a check that could not fail. Moving a real page's boot script below its<style>and running the suite as CI does reported "all checks pass" — on a loopback server the whole document has executed before Playwright's earliest navigation event. It now asserts document order and script form instead of racing a paint.--c-midon--pressmeasured 4.35:1 in light — sub-AA on the pressed EN and the pressed sun/moon, the only affordances saying what is active. The palette had been validated token-against-background, never token-against-token.rb-theme=dark, and a check fails if a committed card renders light.theme=darkwhen dark was merely the default, and the receiver stored it — so a reader who chose light on one domain could have it destroyed by a domain where they chose nothing. It now carries only an actual choice.🤖 Generated with Claude Code
Plan B — the four decks (added to this branch)
All twenty pages now carry the switch. The decks painted 126 literal colours, which were only 24 distinct values — 100 of them just 17 values every deck shares, one of which was already
--press. Zero literals remain on any page.The transport is a physical object. Its own source calls
--slab"the slab it is milled from" and--lcd"the recessed window the track number sits in". In light the slab becomes pale milled metal (#EDEAE2) and the readout stays dark, because a real machine with an aluminium body still has a dark display. Everything printed on the readout is invariant too — the digits, the separator, the message, the progress track, the fill, and the shadow that seats it.That last part was a correction, not a plan. The digits were
var(--c-mid)and the messagevar(--c-flag), both of which flip: against a permanently dark readout the light theme would have printed 0.76rem digits at 3.67:1 and a message at 3.33:1, and the separator was already failing at 2.68:1 in both themes. The progress track would have gone#D8D4CA— a pale strip inside a near-black window.Two Criticals were caught by review and fixed before merge:
--slabwas page-owned with a single value, so a light deck was a black bar on a pale page with the LCD at 1.11:1 against it — a dark box inside a dark box, the metaphor gone. It moved into the shared block with--warn.body{overflow:hidden}made the overflow unreachable. At 414px#thDarkwas 0% reachable — tap Light and you could not get back, because the choice persists — and#tUpwas gone too while the lockup is hidden below 860px, so the deck had no way out at all. At 390px#tNoteswas entirely off-screen. Breakpoints re-tuned, and atransportFitscheck now asserts every control is inside the viewport and hit-testable at 320/350/360/390/393/414/430.Package: v0.10.0 → v0.11.2, 173 → 193 tests.