Files
margin/simplify/design-system.md
T
2026-10-03 22:48:49 +05:30

29 KiB

The design system: @margin/tokens and @margin/fonts

Scope: CSS custom properties, the base layer, themes and the six bundled faces. The React primitives that consume them are in ui-kit.md; the glyphs are @margin/icons. Short names: margin = python/margin, calendar = python/margin-caledar, editor = rust/margin-editor, mail = rust/margin-mail, all under /Users/pj/Workspace/projects. Line numbers as of 2026-09-06.

Where the four token sets actually are

They are not four palettes. Calendar, the only app that does not depend on margin-shared at all and so had every opportunity to drift, matches shared/css/tokens.css on 49 of the 52 values it declares in common. Absent are --font-book, --pane-sidebar, --measure and --sidebar, which a calendar has no use for and no sidebar to paint. One value genuinely differs, --ink-faint.

So this is not a reconciliation. Three apps already import the shared file (margin/src/styles/tokens.css:3, editor/src/styles/tokens.css:5, mail/src/styles/tokens.css:6) and layer on top; the fourth is a hand-copy. The work is to move twenty tokens that two or three apps each declared separately into the one place, correct one value that is wrong in shared and right in two apps, and delete four copies of a reset.

What drift exists is almost entirely in margin, the only app with no test suite. Colour literals outside each app's token layer: margin 16 hex and 18 rgba, calendar 0 and 1 (a box-shadow at app.css:507), editor 0 and 0, mail 0 and 0. Margin's are mostly tokens it never adopted, named under each group below.

--ink-faint is the one that matters, because it is a legibility defect and not a preference. Shared's #9b9484 measures 2.91:1 on --paper, 2.56:1 on --shell and 2.43:1 on --sidebar, and the dark #756d5e 3.39:1 on --paper. Calendar (tokens.css:73, :126) and mail (mail.css:86, :142) independently moved to #6e675b and #8e8677, measuring 5.40 / 4.75 / 4.50 light and 4.81 / 5.14 / 4.51 dark. Margin and editor still ship the failing value.

The contract for @margin/tokens

Fifty six names: thirty five are what shared/css/tokens.css declares today, twenty one are promotions. Every one is declared in both palettes or in neither, because a token defined in light and missing in dark is the failure editor's src/theme.test.ts catches, and that test moves into the package.

Colour

Token Light Dark Status
--paper #fcfbf7 #1d1a16 shared
--shell #f1ece2 #151310 shared
--sidebar #ece6da #1a1713 shared
--raised #fbfaf6 #232019 shared
--ink #23201b #ece6da shared
--ink-soft #6b6458 #a89f8e shared
--ink-faint #6e675b #8e8677 corrected, was #9b9484 / #756d5e
--line #e3ddce #2c2823 shared
--line-strong #d6cfbd #3a352d shared
--accent #2a2622 #efe9dc shared
--accent-ink #100e0b #ffffff shared
--accent-wash rgba(35, 32, 27, 0.08) rgba(239, 233, 220, 0.12) shared
--accent-contrast #faf7f0 #1a1714 shared
--selection rgba(35, 32, 27, 0.14) rgba(239, 233, 220, 0.18) shared
--glass rgba(252, 251, 247, 0.86) rgba(33, 30, 25, 0.88) shared
--scrim rgba(35, 32, 27, 0.28) rgba(0, 0, 0, 0.58) promoted
--danger #b4453a #d97a6f shared
--danger-ink #963327 #e58e84 shared
--danger-wash rgba(180, 69, 58, 0.1) rgba(217, 122, 111, 0.16) shared
--danger-contrast #faf7f0 #1a1714 shared
--hue-1 to --hue-8 #6f6194 #47705f #a06044 #4e6484 #856434 #955767 #3a6b78 #667141 #a396c8 #7fae98 #d19a7c #8ba2c4 #c4a267 #cb909e #75a9b6 #a2ae77 promoted
--pdf-page #fff #fff promoted, identical in both on purpose

--ink-faint is the only promotion that changes what margin and editor look like, and it will be visible: every date stamp, word count, nav label and field label in both gets darker.

--scrim: calendar tokens.css:84/:138 and mail mail.css:88/:145 agree exactly; editor tokens.css:26 has rgba(0, 0, 0, 0.5) dark. Two to one, and both of the two wrote the reason down (a light scrim over a dark page dims nothing, so panels float on shadow alone). Margin writes the light value into .overlay at app.css:1275 and a second, unexplained rgba(35, 32, 27, 0.32) into .export-overlay at app.css:1449; both become var(--scrim).

The hue ramp is calendar's --cal-1..8 (tokens.css:106-113, :160-167) and mail's --hue-1..8 (mail.css:114-121, :159-166), the same sixteen hexes under two names. Mail's wins, because --cal- names a product; calendar renames on adoption, keeping its unrelated --cal-h, --cal-sat and --cal-fill HSL derivations in grid.css. --pdf-page editor tokenised at tokens.css:22 because it was a literal in two stylesheets; margin still has that literal at app.css:1238.

Not promoted, one consumer each: mail's --check / --check-tick, --note-surface, --row-hover, --row-selected, --card-ring, --message-*; calendar's --grid-*, --fold-*, --event-*; editor's --code-* and --doc-rule*; margin's twelve proofing colours at app.css:2367-2451. The proofing pair is worth comparing separately, since editor's styles/proofing.css draws the same feature out of --danger.

Type scale

Token Value Status
--font-ui "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif shared
--font-book "Literata", Georgia, "Times New Roman", serif shared
--font-heading "Literata", Georgia, "Times New Roman", serif shared
--font-mono ui-monospace, "SF Mono", Menlo, monospace promoted
--t-1 11px shared
--t-2 12px shared
--t-3 13px shared
--t-4 15px shared
--t-5 16px promoted
--measure 46em shared

--t-5 is identical in calendar tokens.css:34, editor tokens.css:13 and mail mail.css:17, and calendar's and mail's comments are nearly word for word the same (iOS Safari zooms a focused field under 16px and the locked viewport gives no way back). --font-mono is a token only in editor (tokens.css:9); calendar writes ui-monospace, SFMono-Regular, Menlo, monospace as a literal at details.css:183 and rich.css:42, so editor's wins as the one already tokenised.

--font-ui, --font-book and --font-heading are the three slots an app may override on the root at runtime, and the contract says so: editor writes --font-book per document from store/useDocumentFonts.ts:101, mail writes the other two from a setting in index.html:31-32. It is why .panel-head h2 takes --font-heading below.

Spacing

There is no spacing scale in any of the four and this plan does not invent one. Padding and gap are literals everywhere and inconsistent (panel bodies 20px 22px 24px, heads 16px 14px 16px 22px, feet 14px 22px), and what does repeat repeats inside a rule that is itself being shared. A scale imposed now would be a rewrite of eighteen thousand lines of CSS with no defect behind it.

Radius

Token Value Status
--r-sm 5px shared
--r-md 8px shared
--r-lg 12px shared
--r-pill 999px promoted

--r-pill is identical in calendar tokens.css:8, editor tokens.css:10 and mail mail.css:13: four apps, one value, three declarations and nine literals in the fourth.

Shadow

Token Light Dark Status
--shadow-page 0 1px 2px rgba(35, 32, 27, 0.06), 0 14px 30px rgba(35, 32, 27, 0.09) 0 1px 2px rgba(0, 0, 0, 0.4), 0 14px 34px rgba(0, 0, 0, 0.5) shared
--shadow-pop 0 8px 24px rgba(35, 32, 27, 0.14) 0 8px 24px rgba(0, 0, 0, 0.55) shared
--shadow-raised 0 1px 2px rgba(35, 32, 27, 0.05) 0 1px 2px rgba(0, 0, 0, 0.35) promoted

Only editor declares --shadow-raised (tokens.css:15, :27); margin writes the light value literally at app.css:272 and :1839, and calendar's one rgba literal, 0 1px 2px rgba(0, 0, 0, 0.06) at app.css:507, is a fourth alpha for the same effect. Margin's .card:hover shadow at app.css:1547 is a two layer lift neither token expresses and stays a literal.

Motion

Token Value Status
--ease cubic-bezier(0.22, 0.61, 0.36, 1) shared
--dur 120ms new

--dur is the one name here that exists nowhere today, and it is a promotion of a measured constant rather than an invented scale: 120ms appears 54 times in margin's CSS, 64 in calendar's, 83 in editor's and 58 in mail's, and every other duration in all four together under 40 times. The shared sheets use it; app sweeps happen when somebody next touches the rule.

Layout metrics

Token Value Status
--titlebar-h 46px shared
--pane-sidebar 248px shared
--traffic-pad 84px promoted
--touch-h 44px promoted
--phonebar-h 48px promoted
--tabbar-h 56px promoted
--sheet-max-h 88dvh promoted

All five promotions are byte-identical where they exist, with near-identical comments: --traffic-pad at calendar tokens.css:11, editor tokens.css:11, mail mail.css:36; --touch-h at calendar tokens.css:22, editor tokens.css:12, mail mail.css:31; the phone trio at calendar tokens.css:16-26 and mail mail.css:26-34.

Margin has no --traffic-pad and hardcodes the lane at app.css:75, padding: 0 14px 0 84px on .titlebar, with no data-traffic gate. The other three gate it (calendar app.css:126, editor app.css:97, mail screens/header.css:24) and calendar's comment at app.css:121-125 says what the ungated version costs: 84px of nothing on Linux, Windows, an iPad and a browser, pushing the centred title off centre. Margin also redefines --titlebar-h at app.css:2881 as calc(46px + env(safe-area-inset-top)); it becomes calc(var(--titlebar-h) + var(--safe-top)), which is calendar's .titlebar at app.css:106.

Not promoted: --row-h, a 48px calendar grid row (calendar tokens.css:44) and a 46px message list row (mail mail.css:45). One name, two meanings, and the clearest reason not to promote geometry.

Safe area

Token Value Status
--safe-top env(safe-area-inset-top, 0px) promoted
--safe-bottom env(safe-area-inset-bottom, 0px) promoted

Calendar tokens.css:16-17 and mail mail.css:26-27, identical. The 0px fallback is what makes a desktop read both as zero without a media query; margin and editor read env() inline.

The base layer

Parsing each app.css into selector and body pairs and comparing bodies exactly, five rules are byte-identical in all four with no exceptions: * (box-sizing: border-box), html, body, #root (height: 100%), the eleven line body block, ::selection, and :focus-visible, the same three lines at margin app.css:43, calendar app.css:80, editor app.css:51 and mail app.css:92.

@margin/tokens ships css/tokens.css, css/base.css and css/overlay.css plus a css/index.css importing all three in that order. index.css is what an app imports, first, before anything else; the split exists so the audit script can parse the token file alone, not so an app can pick.

css/base.css holds those five plus the rules below, taking the version named:

Rule Winner What the losers are missing
html calendar app.css:37, mail app.css:23 -webkit-tap-highlight-color: transparent, without which a webview reads every tap as the start of a selection
button mail app.css:70 font-size: inherit (margin app.css:34, calendar app.css:63, editor app.css:34 are otherwise identical)
input, textarea, select calendar app.css:72, editor app.css:43, mail app.css:80 margin has no such rule, so its fields take the webview's own font
svg { display: block } mail app.css:88 the inline baseline gap under every glyph, patched per component in the other three
.app calendar app.css:86, editor app.css:58, mail app.css:100 margin app.css:59 still uses height: 100vh; height: 100dvh
:root[data-touch] selection rules, :root[data-phone] field size calendar app.css:44-60, mail app.css:44-61 absent in margin and editor
::-webkit-scrollbar and its three parts margin app.css:693-712 the other three take the webview default

Two need a sentence. On .app, calendar's comment at app.css:89-97 explains that UIKit shrinks the layout viewport by the safe areas while leaving it anchored at y 0, so a viewport unit lays the tab bar out below the clip body { position: fixed } imposes, where it is invisible and untappable. The scrollbar is 11px, thumb in --line-strong with a 3px transparent border and background-clip: content-box, hover --ink-faint, transparent track.

css/overlay.css holds the box every app builds its dialogs from, the largest near duplicate in the four codebases. .overlay is byte-identical at calendar app.css:330, editor app.css:451 and mail app.css:110, and margin app.css:1271 is the same rule with the scrim written out. .panel and .panel-body are byte-identical in all four (margin app.css:1283, calendar app.css:342, editor app.css:463, mail app.css:129). .panel-foot is identical at margin app.css:1622, calendar app.css:415 and editor app.css:502, and mail app.css:168 adds flex: none, which ships. .overlay[data-align="top"] { align-items: start; padding-top: 12vh } appears at calendar palette.css:4, editor palette.css:9 and mail app.css:124; margin has no palette and gains an inert rule.

Two reconciliations in the head. Editor's padding: 16px 16px 16px 22px beats the other three's 16px 14px 16px 22px on editor's own argument at app.css:474-478: on the closing end sits a 28px icon button carrying a 16px glyph 6px in from its own edge, so 16 plus 6 puts the cross on the same right edge as the body text and the footer buttons, and all four put a close button there. And .panel-head h2 takes var(--font-heading), as calendar app.css:401 and mail app.css:152 do, not var(--font-book) as margin app.css:1301 and editor app.css:489 do. Both resolve to Literata today, but editor overwrites --font-book on the root per document, so a document set in EB Garamond currently retitles every dialog in EB Garamond, and a dialog title is chrome. Mail's other head extras ship: flex: none, gap: 10px, and flex: 1; min-width: 0 on the h2 so a long title truncates rather than shoving the close button off the edge.

Phone docking is written twice today, identically and with the same comments, at calendar app.css:353-390 and mail app.css:174-212: :root[data-phone] variants of .overlay (z-index 50, padding 0, place-items: end center), .panel (full width, --sheet-max-h, top border only, radius var(--r-lg) var(--r-lg) 0 0, padding-bottom: var(--safe-bottom), animation: sheet-up 180ms var(--ease)) and .panel-body (overscroll-behavior: contain), plus @keyframes sheet-up. All of it ships, including mail's @media (prefers-reduced-motion: reduce) guard, which calendar lacks: mail has 11 such guards, editor 1, calendar 0 and margin 0, while calendar and margin both animate with nothing. Every keyframe in the shared sheets carries a guard, and the apps inherit that with it.

Theming

data-theme on the root, written from JavaScript, never a @media (prefers-color-scheme) rule in CSS. All four already do this: one decision taken four times, and the right one, since the system preference is an input to the setting rather than the setting itself and a user who picked light at four in the afternoon keeps it.

color-scheme is declared, in both shared palette blocks: light on :root, :root[data-theme="light"] and dark on :root[data-theme="dark"]. Only editor has it today (themes.css:24-31), which is why the caret, the native form controls and the default scrollbar come out light on a dark page in the other three. Calendar's workaround at create.css:53 and :57 is deleted as redundant. Mail's rules on .msg-frame at screens/pane.css:249-261 stay: an embedded document takes its scheme from the element embedding it, and a sender's HTML is pinned only light.

The multi-palette design stays local to editor, which ships seven palettes (light, sepia, mist, contrast, dark, graphite, midnight) in a 323 line themes.css, each block declaring the same thirty five properties and enforced by src/theme.test.ts. Generalising it means every app's stylesheet answering sepia and midnight or falling through to the light :root block, and for calendar that is seven variants each of --grid-*, --fold-*, --event-* and the hue ramp, roughly 140 new declarations for a feature it has not asked for. The mechanism is shared, not the palettes, and an app that later wants sepia adds a block and a row in its list.

One piece of themes.css does move. Lines 307-322 copy six hexes of the shared palettes so the picker's preview tiles can draw them, with a comment saying they are copied because "this repo may not reach into" margin-shared. It can, and does. The two shared palette blocks gain .theme-swatch[data-theme="light"] and .theme-swatch[data-theme="dark"] to their selector lists, which gives a swatch all twenty colours rather than six and deletes both blocks.

The hook lives in @margin/hooks, per repo-layout.md, and covers both shapes:

useTheme<T extends string>(options: {
  key: string;                 // "marginmail-theme"
  themes: readonly T[];        // ["light", "dark"], or editor's seven
  schemeOf?: (id: T) => "light" | "dark";
}): { theme: T; choice: T | "system"; scheme: "light" | "dark";
      setChoice(next: T | "system"): void }

It reads data-theme off the root first (the boot script has already run), then storage, then matchMedia. Storage goes through try/catch, because a webview with storage denied should still theme and merely forget between launches; editor's theme.ts:105-122 has that and the other three do not. It remembers a light half and a dark half so "system" lands on the palettes the user actually picks, and registers the matchMedia listener. Margin, calendar and mail pass a two element list and get what they have now plus the storage guard; editor passes seven and its Preference type goes.

The boot script is the other half and cannot import a bundle, so @margin/hooks exports themeBootScript(options) returning the source as a string, injected by each app's Vite config through transformIndexHtml. It writes data-theme, data-phone and data-touch, the part identical across calendar index.html:12-22 and mail index.html:12-22, and each app appends its own lines: calendar's data-view, mail's data-no-pane and font slots, editor's data-sidebar and data-width, margin's --measure and pane widths.

Fonts

@margin/fonts is today's shared/src/fonts.ts (233 lines: BUNDLED_FONTS, FontRef, FontPair, FONT_PAIRINGS, fontStack, fontsUsed), shared/css/fonts.css (twelve @font-face rules over six families), shared/fonts/ (twelve variable TTFs and six OFL notices, 13.8 MB) and shared/bin/sync-fonts.mjs, moved intact. Nothing about it is wrong. All 18 files in shared/fonts are byte-identical to the copies in margin, editor and mail's public/fonts (verified 18/18 each with cmp), and calendar's 4 are byte-identical too.

The vendored copies stay and the reason narrows. Margin's src-tauri/src/pdf.rs:9-20 reads all twelve variable files with include_bytes! from ../../public/fonts, so the bytes must be at a path cargo can see before any npm install has run. Editor no longer does: src-tauri/src/pdf.rs:33-40 reads ten static instances out of src-tauri/fonts, and calendar and mail read no fonts from Rust at all. The package's comment saying "both apps' pdf.rs" is out of date by one app; the copies now exist for margin's exporter and for the webview, which serves them from /fonts/Literata-VF.ttf.

sync-fonts keeps its behaviour, which is right: copy every .ttf and .txt, or with --check compare and exit 1 on any difference, reporting rather than deleting a file the app has and the package does not (lines 53-59). It gains one flag, --core, restricting the set to Literata and Hanken Grotesk and their two OFL notices, and @margin/fonts exports two stylesheets to match: css/fonts.css with all twelve faces and css/core.css with the four. Otherwise an app declares faces it does not vendor and fonts:check passes against a @font-face pointing at a 404.

Calendar is what changes. It has no dependency on the package, no fonts:sync or fonts:check, no way to notice drift, and 31 lines of hand-written @font-face in src/styles/fonts.css that are character for character shared/css/fonts.css:17-47. It ships no OFL notices, which is the one real problem here, since shipping a face without its licence is a licensing defect. It takes the dependency, replaces fonts.css with @import "@margin/fonts/css/core.css" and adds the two scripts running --core, gaining two OFL notices and 4 KB rather than 11.7 MB it has no picker to offer.

Per app

margin. Deletes tokens.css:1-3 (the import moves to @margin/tokens/css/index.css), keeping the file for --pane-dock: 384px alone, and deletes src/styles/fonts.css for @margin/fonts/css/fonts.css. Deletes app.css:1-58 (the reset, keeping the two app-specific :focus-visible overrides at :47-57), :59-65 (.app), :693-712 (the scrollbar, which moves up), :1271-1316 and :1622-1628 (overlay and panel), about 110 lines. Adopts --scrim, --shadow-raised, --r-pill, --traffic-pad, --safe-top, --pdf-page and --dur in place of 30-odd literals, and gains the data-traffic gate it never had. src/theme.ts goes for the hook. This app changes most, being the one that never adopted the last three rounds of shared work.

calendar. Deletes tokens.css:1-36 and :63-146 (the :root block and both palettes), keeping the grid geometry and the --grid-*, --fold-* and --event-* blocks: 84 of 168 lines. Renames --cal-1..8 to --hue-1..8 at its 20-odd call sites. Deletes src/styles/fonts.css, app.css:1-99 and :329-420, plus create.css:53 and :57. Gains a shared dependency for the first time, plus fonts:sync --core and fonts:check --core, and two OFL notices in public/fonts.

editor. Deletes tokens.css:8-28 (the shape and scale block, --scrim, --shadow-raised, --pdf-page), keeping the document scale and code surface from :30 down: 21 of 108 lines. Deletes app.css:1-62 and :451-508, and themes.css:24-31 and :296-322. Its src/theme.test.ts moves into @margin/tokens as the audit script and gains the other three apps as inputs; src/theme.ts collapses into the hook, keeping only THEMES and the labels. src-tauri/fonts is untouched: a typesetting decision, in typesetting.md.

mail. Deletes src/styles/app.css entirely, all 212 lines, the cleanest single deletion in this plan: the file is the reset, the window, the overlay and the phone docking and nothing else. Deletes mail.css:10-36 (--r-pill, --t-5, phone chrome, --traffic-pad) and :86-88 and :142-145 (the --ink-faint and --scrim overrides, now upstream), keeping mail geometry, the row surfaces, the note surface, --check, --card-ring and --message-*: 120 of 167 lines. tokens.css becomes a two line seam and src/theme.ts goes for the hook.

Every app keeps exactly one app-local token file, and the rule mail's seam already states becomes the family rule: nothing outside the token layer may declare a token, and nothing outside it may write a literal colour, radius or size.

Order, and what proves each step

Nothing starts until mail and editor's 123 uncommitted files each are committed and mail has a remote. risks.md treats that as a precondition and it is.

1. Correct the values in place. Change --ink-faint in python/margin/shared/css/tokens.css and add the twenty other promotions to that same file, before anything moves repository. The dependency is still a symlink here, so margin, editor and mail see it on the next dev restart and each visual diff has exactly one cause. Check: run the contrast assertion over every ink-on-surface pair in both palettes and require 4.5:1; screenshot margin's chapter list and editor's document tree in both themes and confirm the only difference is faint ink getting darker.

2. Land the audit script. Promote editor's src/theme.test.ts into the package. It parses the stylesheets rather than rendering them, builds the set of properties each palette declares, and fails when a block is short or a data-theme block has no color-scheme. Extend it with the literal lint risks.md asks for: any hex or rgba( outside the token layer is a failure. Check: it reports 16 hex and 18 rgba in margin, 1 rgba in calendar, 0 and 0 in editor and mail. After step 5, four zeroes.

3. Calendar joins. Add the dependency, delete its palette blocks, import the shared tokens, rename --cal-*, take core.css and the two font scripts. Check: the audit reports zero drift between calendar and shared; pnpm fonts:check --core passes; pnpm test:ui passes, in particular tests/legibility.spec.ts, which renders the week grid in both themes and would catch an hour axis that lost its contrast. Compare the week grid on a Monday with overlapping events.

4. Extract the base sheet. css/base.css and css/overlay.css, one app at a time in the order mail, editor, calendar, margin: mail first because its version is the superset and its app.css is a clean delete, margin last because it needs the most reconciliation and has no suite to catch a mistake. Check per app: mail tests/shell.spec.ts and tests/kit.spec.ts; editor tests/smoke.spec.ts and tests/_rule.spec.ts; calendar tests/overlays.spec.ts, tests/phone.spec.ts and tests/touch.spec.ts. The screen to compare is a dialog over the main view at 1280 wide and at 390: mail's Settings sheet over the thread list, editor's Document Setup over an open document, calendar's quick create over the week, margin's Settings panel over the chapter list. Margin has no Playwright config, so its check is a manual pair of screenshots per theme; giving it a suite belongs in testing.md and should happen before this step rather than after.

5. Land the scrollbar and --dur. Separately from step 4, because the scrollbar is the one base rule that changes layout: an 11px classic scrollbar where the webview drew an overlay one takes 11px from whatever it is inside. On macOS it only shows under "Show scroll bars: Always", which is why nobody noticed margin had it. Check: screenshot every scrolling surface with that setting forced on, specifically mail's virtualised list, calendar's grid and editor's tree.

6. Rename and repackage. margin-shared becomes @margin/tokens and @margin/fonts in the new repository and all four apps move from a file: path to a version. This changes no CSS and should produce no visual diff at all, which is the point of doing it last. Check: the audit and all three suites pass unchanged, and pnpm install succeeds in a fresh clone of each app with no sibling checkout present, which is the failure that motivated the whole exercise.

What stays per app

Editor keeps its document scale (--doc-h1 through --doc-h6, --doc-size, --doc-lead), its four measures, its sheet padding, its code surface and its six syntax hues, and it keeps themes.css. Those 51 tokens describe a page of prose, and a calendar and a mail client have no page.

Mail keeps its geometry (--list-w, --row-h, --avatar, --pile-h, --compose-w, --feed-w), its row surfaces, --note-surface, --check, --card-ring and the five --message-* values. The --message-* set must not be promoted: it is the light palette's values written a second time, deliberately outside both theme blocks, because HTML mail is authored for a white page and a newsletter that sets a dark text colour and no background is unreadable on a dark surface. A shared token that ignores the theme would contradict the contract.

Calendar keeps its grid geometry and the --grid-*, --fold-* and --event-* palettes, plus the per-event HSL derivations at grid.css:184-223, a colour system inside a colour system that exists because a calendar draws dozens of tinted blocks at once. Margin keeps --pane-dock: 384px, the proofing colours at app.css:2367-2451, and the device frame's --dv-* set, which comes from components/DeviceFrame.tsx:22-24 rather than from CSS at all.

And the thing not to share at all: geometry by name. A shared --row-h would be a token every consumer overrides, which is a name with no value in it, and the next reader would reasonably assume the calendar grid and the mail list were meant to line up.