remarque-tokens 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_RULES.md CHANGED
@@ -6,6 +6,17 @@ Read `REMARQUE.md` first for the full system specification. This file tells you
6
6
 
7
7
  ---
8
8
 
9
+ ## Prefer the Registry Over Transcribing Prose
10
+
11
+ For the Essay Module, the Broadsheet pattern, and Forms (`remarque-tokens/essay`, `remarque-tokens/broadsheet`, `remarque-tokens/forms`), a machine-readable markup-contract registry ships alongside the spec (REMARQUE.md "The Registry"; issue #100). **Agents building a Remarque page SHOULD fetch the relevant registry item and apply its `usage.html`/CSS `content` verbatim, rather than re-typing the markup from this file's or REMARQUE.md's prose.**
12
+
13
+ - Fetch `https://williamzujkowski.github.io/remarque/registry/<name>.json` (`essay`, `broadsheet`, `forms`, or `palette-deck`), or read it from an installed package at `node_modules/remarque-tokens/registry/<name>.json` — same content either way.
14
+ - The known-good markup lives in `files[].content` where `type` is `"remarque:markup"`; the CSS module (if any) is in the `"remarque:css"` entry. Both are the exact, versioned, hash-pinned (`integrity`) bytes — copy them, don't re-derive them from memory.
15
+ - This exists specifically to prevent transcription bugs like #89 (a sidenote `aria-label` that drifted out of DOM order because an agent hand-copied ~80 lines of spec prose instead of applying known-good markup). Reading REMARQUE.md's prose for the *reasoning* behind a contract is still expected; reading it as the *source of the markup you type* is the failure mode the registry exists to close.
16
+ - No CLI installer ships this round — there is no `npx` command that writes these files into a project for you. Fetch the JSON, then apply the markup with your own editing tools.
17
+
18
+ ---
19
+
9
20
  ## Build Order
10
21
 
11
22
  Execute in this exact order. Do not skip steps. Do not reorder.
@@ -177,7 +188,7 @@ Three further archetypes — Reference/Docs, Changelog, Gallery — are specifie
177
188
 
178
189
  2. **Prose alignment:** The essay header and prose body must BOTH use centered max-width (`max-w-reading mx-auto`) to prevent misalignment. The `.content-reading` class auto-centers; match headers with `mx-auto`.
179
190
 
180
- 3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode.
191
+ 3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode. Since 0.24.0 (`light-dark()` + `color-scheme`, REMARQUE.md "Color Scheme & light-dark()"), the shipped default palette's own `[data-theme="light"]` override is just `color-scheme: light` — a consumer palette that still authors the older two-declaration convention needs the fuller override this rule originally describes.
181
192
 
182
193
  4. **Self-hosted fonts:** Replace the Google Fonts CDN link with self-hosted woff2 files + `@font-face` declarations. Use `font-display: swap` and `<link rel="preload">` for performance.
183
194
 
package/CHANGELOG.md CHANGED
@@ -4,7 +4,49 @@ All notable changes to `remarque-tokens` are documented here. Token value
4
4
  changes always state the design rationale — downstream sites pin against
5
5
  these entries when syncing.
6
6
 
7
- ## 0.22.0 — 2026-07-23
7
+ ## 0.24.0 — 2026-07-23
8
+
9
+ `light-dark()` + `color-scheme` palette migration (closes #95) and a `:has()` zero-JS forms enhancement layer (closes #97) — the ratified program's last two items.
10
+
11
+ ### Browser support floor — raised (stated prominently per the new Governance & Deprecation "browser-floor" minor case)
12
+
13
+ `light-dark()` and `color-scheme` are baseline-stable but not ancient: this release raises remarque-tokens' minimum supported browser engine to **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. This is a **MINOR**, not a major — no token is removed or renamed, every consumer's `var(--color-x)`/`tokens.json` lookup keeps resolving exactly as before, and `tokens.json`/`tokens.d.ts`/`tokens.schema.json` are byte-identical to 0.23.0 (verified this release, version field aside). Only the rendering environment required to resolve the new CSS *correctly* changes. A site whose audience needs an older engine should stay on `<0.24.0`, or override the affected `--color-*` tokens with plain `oklch()` values (the audit accepts either authoring form). See REMARQUE.md's new "Color Scheme & light-dark()" section and its updated "Semver contract".
14
+
15
+ ### Added / Changed
16
+
17
+ - **`tokens-palette.css`: every `--color-*` token is now a single `light-dark(<light>, <dark>)` declaration** under `:root { color-scheme: light dark; }`, replacing the old convention of a full `:root` block plus a full, separately-maintained `@media (prefers-color-scheme: dark)` block re-listing every value a second time. System-preference default needs no color-specific rule at all now; `[data-theme="light"]`/`[data-theme="dark"]` become single `color-scheme` overrides instead of re-declaring every token's dark value; the `:root.dark` compatibility bridge (ratified sunset: 1.0, unchanged) carries the same minimal `color-scheme: dark` override. Values are unchanged — this is purely an authoring-form migration, golden-gated ΔE2000 = 0.000/0.000 against the previous values on every affected token (see `scripts/palette-golden.mjs` output).
18
+ - **`--weight-display` stays on the pre-migration mechanism, on purpose.** `light-dark()` is restricted to `<color>` values; `--weight-display` is a `font-weight` (400/500, dark-mode serif-hairline compensation, issue #59), so it cannot be expressed as `light-dark(400, 500)`. It keeps `@media (prefers-color-scheme: dark)` + `[data-theme="dark"], :root.dark` — now carrying only this one token instead of the whole palette. A registered-custom-property `light-dark()`-abuse workaround was considered and rejected: the conventional blocks are simpler, already audited, and now touch exactly one token — there's no problem left to solve by working around them.
19
+ - **`prefers-contrast: more` (issue #93) simplifies from three blocks to one.** Pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override. Under `light-dark()`, one un-scoped `light-dark(<light>, <dark>)` override per token replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there (system, explicit, or the bridge) — there is no dark-specific selector left to write.
20
+ - **The demo site's `[data-theme="light"]` mirror block is retired**, not merely updated. It used to hand-carry all 21 `--color-*` values from the package's light `:root` so a dark-OS visitor could still toggle back to light (AGENT_RULES Pitfall #3) — a known drift trap (`scripts/palette-golden.mjs`'s "site mirror" check). Explicit light selection is now a bare `color-scheme: light` override shipped IN the package itself; the color values live in exactly one place (the `light-dark()` declaration), so there is no second copy to drift. `site/src/styles/globals.css`'s block shrinks to `[data-theme="light"] { color-scheme: light; }`. `scripts/palette-golden.mjs`'s mirror check now asserts the *absence* of any `--color-*` redeclaration there, rather than comparing two copies of a value that no longer has a second copy.
21
+ - **Parser (`scripts/lib/css-tokens.mjs`): three new exports** — `splitLightDark(value)` (depth-aware split of a `light-dark(a, b)` value into its light/dark sides — a side that is itself `oklch(...)` or `var(...)` doesn't confuse the single top-level comma being looked for), `resolveSide(decls, side)` (collapses a flat decls map to one theme side; a no-op for any value that isn't a `light-dark()` call, which is what keeps every conventional-form palette parsing identically to before), and `darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls)` (merges `light-dark()` dark sides from the top-level `:root` block with whatever a conventional dark block still declares — mixed-form palettes are supported, with the conventional block winning on conflict, matching real CSS cascade order). `scripts/audit.mjs`, `scripts/palette-golden.mjs`, `scripts/drift-check.mjs`, and `scripts/tokens-json.mjs` all route through these; none changed their own contrast/golden/drift logic, only how they extract light/dark values first. **This arguably simplifies the parser long-term**, as the issue predicted: both values now come from one declaration instead of being scattered across two selectors.
22
+ - **Fixture coverage (`scripts/test-audit.mjs`):** a pure `light-dark()` palette fixture, a mixed-form palette fixture (half the tokens migrated, half still in the two-block convention, in the same file), and a must-fail fixture where a `light-dark()` value's DARK side (only) fails contrast — proving the parser actually extracts and enforces both sides of a `light-dark()` declaration, not just the light side. Every pre-existing conventional-form fixture (attribute, class, media, palette-deck, nested/compound-media-not-dark, …) stays green, unmodified.
23
+ - **`scripts/theme.mjs` (the `remarque-theme` bridge) is deliberately unchanged** — still emits the conventional two-block form. Its output is a palette override a consumer imports after this package's own tokens; keeping it on the older, universally-parseable convention is the lower-churn choice (every consumer regardless of remarque-tokens version can consume it, and the parser above already treats both forms identically). Modernizing the bridge's own emission to `light-dark()` is deferred to a later major.
24
+ - **`forms.css`: `:has()` zero-JS validation enhancement layer (issue #97).** A second progressive-enhancement layer over the existing `@supports selector(:user-invalid)` bonus border-recolor — `:user-invalid` alone can only style the input it matches (CSS has no "parent selector" without `:has()`), so it cannot reach the field's label. `.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label` extends the error recolor to the label; `.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label` brightens the label to `--color-fg` as a quiet secondary focus cue (not a replacement for the control's own native `:focus-visible` outline). Both guarded with `@supports selector(:has(...))`, same fallback shape as `:user-invalid` itself. `data-state` remains the canonical contract; neither block replaces authoring `data-state="error"` explicitly.
25
+ - **Registry regenerated** (`registry/forms.json`) — `forms.css`'s content changed (the new `:has()` blocks), single-sourced verbatim as always; no markup-contract change, so `usage.html` and the mechanical contract checks are untouched.
26
+ - **REMARQUE.md:** new "Color Scheme & light-dark()" section (under "Token Tiers") — the full mechanism, the browser-floor bump, the weight-display/mirror/bridge/parser decisions, and the `prefers-contrast` simplification. "Technology Stack" theming bullet, "DTCG Conformance", "Forced Colors & Contrast Preferences", "Governance & Deprecation" (semver contract's new browser-floor minor case; the `:root.dark` deprecation-table row's rationale), and "Forms" ("State wiring") all updated to match.
27
+ - Version-only regeneration of `tokens.json`/`tokens.d.ts` (no token name/value changes — verified byte-identical aside from the version field).
28
+
29
+ ## 0.23.0 — 2026-07-23
30
+
31
+ Machine-readable markup-contract registry (closes #100) — the ratified program's flagship item. Funded on the panel's amended rationale: it mechanically prevents the markup-contract transcription bug class (the sidenote DOM-order/aria-label failure found in the flagship migration, #89), not on "category-defining" positioning.
32
+
33
+ ### Added
34
+ - **`registry.json` + `registry/{essay,broadsheet,forms,palette-deck}.json`** — four items, shaped like shadcn/ui's `registry-item.json`/`registry.json` (`https://ui.shadcn.com/schema/registry-item.json`, `.../registry.json`), adopted where the shape fits and explicitly NOT a parallel invented schema. Per-item: `name`/`type`/`title`/`description`/`dependencies`/`cssVars`/`docs`/`files` kept from shadcn's vocabulary; `type` mints one honest value (`remarque:contract` — none of shadcn's `registry:lib/component/ui/hook/...` describe a CSS+HTML contract with no installable component); `cssVars` is repurposed from "values to merge" to "the flat list of `--custom-property` names this item's CSS reads via `var(...)`, mechanically grepped"; `docs` is repurposed from a markdown blob to a single HTTPS URL into REMARQUE.md. `registry.json` is a lightweight index (pointer + pinned version/hash per item), not shadcn's full-item embedding — full payloads live only in `registry/<name>.json`. Every field-level adaptation and omission (`tailwind`/`css`/`envVars`/`font`/`extends`/`style`/`iconLibrary`/`baseColor`/`theme`/`files[].target`/`meta` — all omitted, none apply) is documented inline in the two vendored schemas and in REMARQUE.md's new "The Registry" section.
35
+ - **Security (panel-mandated, blocking), all enforced by `scripts/test-registry.mjs`:** every item pins a `version` (the exact package release) and an `integrity` hash (`sha256-<base64>`, W3C Subresource Integrity's own shape, computed over `files` at build time and independently recomputed in the test gate — not just carried over); every `docs`/`homepage` URL is `https://`; no item contains executable content anywhere (`scripts/test-registry.mjs` asserts no file's `content`, across every item, contains the substring `<script`). **`deck.js`** — the Palette Deck's runtime module, the one JS asset this package ships — is deliberately EXCLUDED from the registry entirely rather than included-and-hashed: the panel's "no executable content... files are HTML/CSS data" condition reads as absolute, so the honest call is a zero-`<script>`-tag bar with no case-by-case exception. `registry/palette-deck.json` ships only the `data-theme`/`data-palette` HTML fragment, not the FOUC-restore `<script>` sample documented beside it in REMARQUE.md — consumers still get `deck.js` via the ordinary `remarque-tokens/deck` package import.
36
+ - **Single-sourcing.** Every item's CSS `content` is read verbatim from the real `.css` file at build time (`essay.css`/`broadsheet.css`/`forms.css`) — never a second hand-copied literal. Every item's `usage.html` is extracted from REMARQUE.md's own "Markup contract" fenced samples via a `<!-- registry-usage:<name> -->` marker comment (new `scripts/lib/registry-extract.mjs`, `extractUsageHtml`) — the spec prose stays the ONE hand-authored copy; the registry is a derived build artifact, the same relationship `tokens.json` already has to the CSS.
37
+ - **`scripts/build-registry.mjs`** (`--check` for CI freshness, mirroring `scripts/tokens-json.mjs`) generates `registry.json` + `registry/*.json` in one pass from the CSS + REMARQUE.md markers + `package.json`'s version.
38
+ - **`scripts/test-registry.mjs`** (the enforce-don't-instruct gate this issue is funded on) — schema validation (`ajv`, draft 2020-12, with negative fixtures proving the schemas actually reject malformed input) plus mechanical re-derivation of the exact contract lines #89 was about, parsed straight out of each generated `usage.html`: every essay `.remarque-sidenote-ref` carries `aria-label="Note N"` and refs/notes strictly alternate in DOM order; the essay TOC rail carries `aria-label`; every broadsheet `.remarque-entry-numeral` carries `data-entry-number`; every forms `<input id>` has a matching `<label for>` and every `aria-describedby` target id exists in the sample.
39
+ - **Vendored schemas** `registry-item.schema.json` + `registry.schema.json` (draft 2020-12, hand-authored/adapted from shadcn's — see "Added" above for the field-by-field rationale).
40
+ - **CI wiring** (`deploy.yml`): `registry.json`/`registry/*.json` freshness-gated alongside `tokens.json` (`node scripts/build-registry.mjs --check`); `scripts/test-registry.mjs` runs alongside the other fixture suites.
41
+ - **Served by the demo site** (`site/scripts/copy-tokens-json.mjs`, extended): `registry.json`, `registry/*.json`, and both schemas copy into `public/` alongside `tokens.json`, published at `/registry.json`, `/registry/<name>.json`, `/registry-item.schema.json`, `/registry.schema.json`.
42
+ - **npm packaging:** `package.json` `files`/`exports` extended — `remarque-tokens/registry.json`, `remarque-tokens/registry/*` (subpath pattern → `registry/*.json`), `remarque-tokens/registry-item.schema.json`, `remarque-tokens/registry.schema.json`.
43
+ - **Docs.** REMARQUE.md's new "The Registry" section (what it is, the shape adaptation, the security model, the single-sourcing mechanism, what it deliberately is NOT — no CLI installer, no archetype "starter-page" items this round — and the fetch URLs). AGENT_RULES.md's new "Prefer the Registry Over Transcribing Prose" section — agents building Remarque pages should fetch a registry item rather than re-typing markup from spec prose. README's "For AI Agents" section and Files table updated with the registry's live endpoints.
44
+ - **Scope note:** the mandatory four items (essay, broadsheet, forms, palette-deck) are the whole of this release — per-archetype "starter-page" items were floated as an IF-cheap extra and SKIPPED: archetype pages live as demo-specific Astro files, not as a reusable module with one canonical markup sample, so there was nothing to single-source without inventing a new "canonical" sample from scratch.
45
+ - Version-only regeneration of `tokens.json`/`tokens.d.ts` (no CSS/token changes this release).
46
+
47
+ ### Fixed
48
+ - **`peerDependencies` range for `@williamzujkowski/oklch-terminal-themes` was stale and broke every real consumer install.** Set to `>=0.1.0 <0.3.0` back when the dataset was at 0.1.0 and never widened as it shipped 0.4.0 (issue #94's dataviz fields) and 0.5.0 (the current devDependency pin) — `npm install remarque-tokens @williamzujkowski/oklch-terminal-themes@0.5.0` failed with `ERESOLVE` for any consumer following the documented "Color Providers" instructions, with no `--legacy-peer-deps` escape hatch offered or implied anywhere in the docs. Invisible from inside this repo because `npm ci` here installs both packages directly as siblings (root devDependency + peerDependency), which never exercises npm's peer-resolution algorithm the way a consumer's plain `npm install` does. Widened to `>=0.1.0 <1.0.0` — the dataset's changes have been strictly additive since 0.1.0 and `remarque-theme`/`remarque-audit` runtime-validate every field they read (see e.g. the dataviz-fallback error path added in 0.22.0), so the honest ceiling is the dataset's own 1.0 breaking-change boundary, not a minor-by-minor cap that recreates this exact break on every upstream release.
49
+ - **New `scripts/test-pack.mjs`** — a real consumer smoke test closing the gap that let the above go unnoticed: `npm pack`s the actual tarball (not the repo checkout), installs it plus the pinned dataset devDependency version into a scratch project with plain `npm install` (deliberately no `--legacy-peer-deps` — the assertion IS that peer resolution succeeds), then runs `npx remarque-theme remarque-light -o out.css` and `npx remarque-audit --palette out.css --src . --json` from that install and asserts `passed: true`. Wired into `deploy.yml` after the other gates (needs registry network access, which the existing devDependency-install step already has).
8
50
 
9
51
  Dataviz categorical tokens — bridge-derived, golden-gated (closes #94). The syntax-palette pattern's fourth application, closing the "Dataviz Tokens" section's long-standing "guidance, not a palette" gap now that the upstream dataset ships a per-theme `dataviz` block (`@williamzujkowski/oklch-terminal-themes` 0.5.0).
10
52
 
package/README.md CHANGED
@@ -71,6 +71,7 @@ Packaging for agent tooling:
71
71
  - **npm exports:** `remarque-tokens/agent-rules` (→ `AGENT_RULES.md`) and `remarque-tokens/spec` (→ `REMARQUE.md`), alongside the existing `remarque-tokens/tokens.json`, so a project can point an agent at `node_modules/remarque-tokens/AGENT_RULES.md` without hardcoding a filename.
72
72
  - **Claude Code skill:** [`.claude/skills/remarque/SKILL.md`](.claude/skills/remarque/SKILL.md) — triggers on "remarque" / "design system" / new-page work, loads all three files, and states the tier rules, the audit command, and the two build-time pitfalls (unlayered-token-import, string-form `@import`) that pass a green build while silently breaking.
73
73
  - **Live tokens endpoint:** the demo site serves the current `tokens.json` at **https://williamzujkowski.github.io/remarque/tokens.json**, and its schema at **https://williamzujkowski.github.io/remarque/tokens.schema.json** — a remote agent can fetch current token values (and validate their shape) directly instead of trusting training data.
74
+ - **Markup-contract registry:** a shadcn-`registry-item.json`-shaped, version-pinned, hash-verified registry of known-good markup for the Essay/Broadsheet/Forms/Palette Deck modules — **https://williamzujkowski.github.io/remarque/registry.json** (index) and **https://williamzujkowski.github.io/remarque/registry/essay.json** (per item; also `broadsheet`/`forms`/`palette-deck`). Fetch and apply the markup instead of transcribing it from spec prose — see REMARQUE.md's "The Registry" and AGENT_RULES.md's "Prefer the Registry Over Transcribing Prose."
74
75
 
75
76
  ## Files
76
77
 
@@ -86,6 +87,8 @@ Packaging for agent tooling:
86
87
  | `scripts/drift-check.mjs` | `npx remarque-drift` — token drift check for consumers (`--json` for structured output) |
87
88
  | `tokens.json` + `tokens.d.ts` | Generated machine-readable token inventory + TypeScript types (`scripts/tokens-json.mjs`) |
88
89
  | `tokens.schema.json` | Generated JSON Schema (draft 2020-12) for `tokens.json`, published alongside it |
90
+ | `registry.json` + `registry/` | Generated markup-contract registry index + per-item files for essay/broadsheet/forms/palette-deck (`scripts/build-registry.mjs`) — see REMARQUE.md "The Registry" |
91
+ | `registry-item.schema.json` + `registry.schema.json` | Generated JSON Schemas (draft 2020-12) for the per-item and index registry files |
89
92
  | `fonts.css` + `fonts/` | Self-hosted @font-face declarations and woff2 files (no CDN requests) |
90
93
  | `tailwind.config.js` | Tailwind CSS **v3** configuration (v4 projects use an `@theme` block instead) |
91
94
  | `package.json` | npm package manifest for `remarque-tokens` |
package/REMARQUE.md CHANGED
@@ -43,7 +43,7 @@ Remarque projects should feel like a modern technical publication — not a gene
43
43
  - **CSS framework:** Tailwind CSS — v4 via the shipped `remarque-tokens/theme.css` (`@theme inline` adapter; utilities reference the runtime tokens), or v3 via the shipped `tailwind.config.js`. One mechanism per project, never both.
44
44
  - **Component primitives:** shadcn/ui (when reusable components are needed)
45
45
  - **Markup:** Semantic HTML with ARIA landmarks
46
- - **Theming:** Light and dark mode via the `[data-theme]` attribute (canonical; system preference + manual toggle). `:root.dark` is supported as a compatibility bridge for class-keyed sites (sunset: 1.0) the audit parses both. Mind the specificity asymmetry: `[data-theme=\"dark\"]` is (0,1,0), `:root.dark` is (0,2,0). Platform endgame to watch: `color-scheme` + `light-dark()`
46
+ - **Theming:** Light and dark mode via `color-scheme` + `light-dark()` (issue #95, 0.24.0) — every `--color-*` token is one `light-dark(<light>, <dark>)` declaration under `:root { color-scheme: light dark; }`; system preference resolves automatically, the `[data-theme]` attribute (canonical) overrides `color-scheme` explicitly, and `:root.dark` is a compatibility bridge for class-keyed sites (sunset: 1.0) carrying the same override. **Raises the package's minimum browser floor** to Chrome/Edge 123+, Firefox 120+, Safari 17.5+ — see "Color Scheme & light-dark()". The audit parses both the new single-declaration form and the older two-declaration convention (mixed within one file, too). Mind the specificity asymmetry on the one remaining non-color theme-conditional token (`--weight-display`): `[data-theme=\"dark\"]` is (0,1,0), `:root.dark` is (0,2,0).
47
47
  - **Accessibility:** USWDS-informed. Keyboard navigation, skip-to-content link, ARIA labels, WCAG AA contrast compliance, 44px touch targets, 14px minimum small text
48
48
  - **Tokens:** Centralized CSS custom properties in two tiers — `tokens-core.css` (immutable identity) + `tokens-palette.css` (sanctioned personalization). `tokens.css` aggregates both. See "Token Tiers"
49
49
  - **Fonts:** Self-hosted woff2 (no CDN dependency). Preloaded via `<link rel="preload">`
@@ -68,6 +68,31 @@ Package subpaths for consumers: `remarque-tokens` (aggregator), `remarque-tokens
68
68
 
69
69
  This makes compliance mechanical: a site that overrides only palette-tier tokens is *authored*; one that touches core-tier tokens has *forked*. `tokens.css` imports both tiers, so existing consumers are unaffected.
70
70
 
71
+ ### Color Scheme & `light-dark()`
72
+
73
+ **0.24.0, closes #95.** Every `--color-*` token in `tokens-palette.css` is now authored as a single `light-dark(<light>, <dark>)` declaration under `:root { color-scheme: light dark; }`, replacing the previous convention of a full `:root` block plus a full, separately-maintained `@media (prefers-color-scheme: dark)` block re-listing every value a second time. This is the platform mechanism earlier releases of this file named as "the endgame to watch" — now adopted.
74
+
75
+ **Browser support floor — raised, stated prominently per "Governance & Deprecation":** `light-dark()` and `color-scheme` are baseline-stable but not ancient: **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. A site whose audience needs to support an older engine should stay on remarque-tokens `<0.24.0`, or override the affected `--color-*` tokens with plain `oklch()` values in its own palette (the audit accepts either form — see "How the parser handles this" below). This is the MINOR-with-rationale case the Governance section's semver contract now names explicitly: no token was removed or renamed, and every consumer's `var(--color-x)` still resolves — only the minimum browser engine that renders it *correctly* moved forward, which is why this ships as a minor rather than a major.
76
+
77
+ **How theme selection now works:**
78
+ - **System preference (no explicit choice).** `color-scheme: light dark` on `:root` is sufficient by itself — every `light-dark()` call resolves from `prefers-color-scheme` directly. No `@media (prefers-color-scheme: dark)` color block is needed anymore; it and its full duplicated value list are **retired**.
79
+ - **Explicit user choice.** `[data-theme="light"]`/`[data-theme="dark"]` now override `color-scheme` itself, once — not every token's dark value a second time. `[data-theme="dark"] { color-scheme: dark; }` / `[data-theme="light"] { color-scheme: light; }`.
80
+ - **`:root.dark` compatibility bridge.** Unchanged status — still bridges class-keyed consumer sites, still sunsets at 1.0 (see the Deprecation windows table). It carries the same `color-scheme: dark` override as `[data-theme="dark"]`, kept **minimal** by design: colors need nothing else now that they resolve through `color-scheme`.
81
+
82
+ **`--weight-display` is the one exception — and stays on the old mechanism, on purpose.** `light-dark()` is restricted to `<color>` values; `--weight-display` is a `font-weight` (400/500, dark-mode serif-hairline compensation, issue #59), so it cannot be expressed as `light-dark(400, 500)`. It keeps the pre-migration `@media (prefers-color-scheme: dark)` + `[data-theme="dark"], :root.dark` blocks — now carrying only this one token instead of the whole palette, which is the honest characterization: this isn't a compromise or a leftover, it is genuinely the smallest correct mechanism available for a non-color theme-conditional token. A registered-property `light-dark()`-abuse trick was considered and rejected — the conventional blocks are simpler, already-audited, and touch exactly one token; inventing a workaround to avoid two small blocks would be solving a non-problem.
83
+
84
+ **The `prefers-contrast: more` override (issue #93) simplifies from three blocks to one.** Pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override — three copies of two tokens, to cover system-default, explicit-dark, and system-dark-without-an-explicit-choice respectively. Under `light-dark()`, one un-scoped `light-dark(<light>, <dark>)` override per token replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there — there is no dark-specific selector left to write. See `tokens-palette.css`'s `@media (prefers-contrast: more)` block.
85
+
86
+ **The site's `[data-theme="light"]` mirror — retired, not merely updated.** Before this migration, an explicit light choice had no mechanism of its own, so the demo site hand-carried a full `[data-theme="light"]` block re-declaring every `--color-*` value (a known drift trap, guarded by `scripts/palette-golden.mjs`'s "site mirror" check). Explicit light selection is now a bare `color-scheme: light` override — the color values live in exactly one place (the `light-dark()` declaration itself) — so there is no second copy left to drift. The mirror block is deleted from `site/src/styles/globals.css`; `scripts/palette-golden.mjs`'s mirror check now asserts the *absence* of any `--color-*` redeclaration there instead of comparing two copies of a value that no longer has a second copy.
87
+
88
+ **How the parser handles this (`scripts/lib/css-tokens.mjs`).** `splitLightDark(value)` depth-aware-splits a `light-dark(a, b)` value into its light/dark sides (so a side that is itself `oklch(...)` or `var(...)` doesn't confuse the single top-level comma being looked for). `resolveSide(decls, side)` collapses a flat decls map to one theme side — a no-op for any value that isn't a `light-dark()` call, which is what makes this fully backward compatible: a palette using only the old two-block convention parses identically to before. `darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls)` merges the `light-dark()` dark sides found in the top-level `:root` block with whatever a conventional dark block still declares (mixed-form palettes are supported — a consumer may migrate some tokens and leave others in the old convention in the same file), with the conventional-block value winning on conflict — matching real CSS cascade order (a later dark-block declaration overrides an earlier `:root` one at equal specificity). `scripts/audit.mjs`, `scripts/palette-golden.mjs`, `scripts/drift-check.mjs`, and `scripts/tokens-json.mjs` all route through these three functions now; none of them changed their own contrast/golden/drift logic, only how they extract light/dark values first. Fixture-tested in `scripts/test-audit.mjs`: a pure `light-dark()` palette, a mixed-form palette (some tokens migrated, some not), a `light-dark()` value that fails contrast on its dark side, and every pre-existing conventional-form fixture, all green.
89
+
90
+ **`scripts/theme.mjs` (the `remarque-theme` bridge) is intentionally unchanged — still emits the conventional two-block form.** Its output is a *palette override* a consumer imports after this package's own tokens; keeping it on the older, more universally-parseable convention is the lower-churn choice — every consumer regardless of remarque-tokens version can consume it, and the parser above already treats both forms identically. It is not a compromise either: modernizing the bridge's own emission to `light-dark()` is deferred to a later major, noted here so it isn't silently forgotten.
91
+
92
+ **`tokens.json` is unaffected in content.** The authoring-form change is purely how the CSS expresses a value, not what the value or token name is — regenerating `tokens.json`/`tokens.d.ts`/`tokens.schema.json` from the migrated CSS produces byte-identical output (aside from the `version` field), verified in this release.
93
+
94
+ **Visual regression: zero baseline changes.** Every `--color-*` value is unchanged — only the CSS mechanism producing it moved. The Playwright visual-regression suite's screenshot baselines are expected to be pixel-identical; any diff would indicate a transcription error in the migration, not an intended appearance change.
95
+
71
96
  ### DTCG Conformance
72
97
 
73
98
  `tokens-core.css` and `tokens-palette.css` remain the single source of truth; `scripts/tokens-json.mjs` regenerates `tokens.json`, `tokens.d.ts`, and `tokens.schema.json` from them in one pass (`node scripts/tokens-json.mjs`; `--check` gates CI freshness). `tokens.json` carries a `$schema` pointer to the published schema (served by the demo site at `/tokens.schema.json`, alongside `/tokens.json` — also exported as `remarque-tokens/tokens.schema.json`).
@@ -75,7 +100,7 @@ This makes compliance mechanical: a site that overrides only palette-tier tokens
75
100
  `tokens.json` is **conformant in spirit** with the [Design Tokens Community Group (DTCG)](https://www.designtokens.org/) format — every token carries `$value`/`$type` — with two **deliberate** structural divergences, documented in the generated file itself (`$extensions.remarque.dtcg`) so a future regeneration can't lose the rationale:
76
101
 
77
102
  1. **Color values are `oklch()` CSS strings**, not the DTCG structured color object (`{ colorSpace, components, alpha }`). Remarque's color pipeline (audit, drift-check, `remarque-theme`) is built entirely on parsing/emitting `oklch(L C H)` strings; adopting the structured form would mean carrying two color representations in parallel for no present benefit. **Gated on:** the DTCG color `$type` structured-value format ratifying.
78
- 2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus a modes/resolver mechanism. The DTCG spec has no ratified multi-mode/theming primitive yet; this repo's own theming (`@media (prefers-color-scheme: dark)`, `[data-theme="dark"]`, the Palette Deck's `[data-palette]` scoping) predates any such draft. **Gated on:** the DTCG multi-mode / resolver draft ratifying.
103
+ 2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus a modes/resolver mechanism. The DTCG spec has no ratified multi-mode/theming primitive yet; this repo's own theming (`light-dark()` + `color-scheme`, `[data-theme="dark"]`, the Palette Deck's `[data-palette]` scoping — see "Color Scheme & light-dark()") predates any such draft. **Gated on:** the DTCG multi-mode / resolver draft ratifying.
79
104
 
80
105
  Full DTCG conformance is gated on those two drafts ratifying — a named, checkable trigger, not "someday." Until then, do not "fix" `tokens.json` toward either unratified draft; the CSS is the source of truth, and both divergences are load-bearing for the tooling that already depends on this shape.
81
106
 
@@ -146,6 +171,8 @@ Remarque uses a three-slot font system. Each slot has a strict role, and each sl
146
171
 
147
172
  This is why the measure is palette-tier: it is a property of the font choice, not of the system's identity. The *target character count* is the invariant.
148
173
 
174
+ **Known limitation: Latin script only.** The table above, and the `ch`-based measure math behind it, assume Latin-alphabet conventions — character width roughly proportional to em-height, word-space-separated tokens, left-to-right flow. None of that holds for CJK, Arabic, or Devanagari body text: CJK measure is conventionally counted in full-width characters per line, a different unit than a Latin `ch` entirely; Arabic's contextual letterforms and right-to-left flow change what "character count" even means; Devanagari's conjunct consonants and matras make per-character width too irregular for a single ch-target to describe. Remarque has not derived a measure-compensation row for any non-Latin script — a site setting CJK/Arabic/Devanagari body copy needs to work out its own measure the way this table works out Latin ones; the ch-target does not transfer by assumption. Named here honestly, in the same spirit as this spec's Butterick line-spacing deviation write-up further down, rather than left for a future non-Latin consumer to discover the hard way.
175
+
149
176
  ---
150
177
 
151
178
  ## Editorial Microtypography
@@ -494,8 +521,9 @@ Issue #93 — Remarque cites [USWDS](https://designsystem.digital.gov/documentat
494
521
 
495
522
  **`prefers-contrast: more`.** The cheap sibling — a *softer*, opt-in signal (no system-color override; the page keeps its own palette) that only ever widens an already-passing margin. Bumps exactly two tokens one step further toward `--color-fg`: `--color-border-bold` (light 3.39:1 → 4.52:1, dark 3.23:1 → 4.53:1) and `--color-fg-muted` (light 7.55:1 → 10.11:1, dark 7.26:1 → 9.70:1). `--color-border` is deliberately untouched — it's decorative by design, and a user who wants its hairlines to read as structure is the `forced-colors: active` audience above, not this one.
496
523
 
497
- - **Architectural home:** `tokens-palette.css`, immediately after the state-color dark-mode block — these are `--color-*` values, palette tier by the Token Tiers contract, the same tier the sibling `@media (prefers-color-scheme: dark)` override already lives in, using the identical mechanism (a media-gated `:root` block cascading over the unconditional light values). Not `tokens-core.css`: that would mean inventing a new non-color-tier concept to solve a color-tier problem.
498
- - **Audit-parser compatibility:** `scripts/lib/css-tokens.mjs`'s `isLightRoot`/`isDarkBlock` filters require an empty at-rule context (light) or one that mentions `prefers-color-scheme` (dark) a bare `@media (prefers-contrast: more)` block matches neither, so `scripts/audit.mjs` and `scripts/tokens-json.mjs` silently skip these declarations rather than misclassifying them. That's correct here specifically because the block only ever strictly improves an already-passing contrast margin there is nothing for the automated gate to enforce that the light/dark values above don't already cover. The values were still hand-verified in-gamut and re-computed with the same OKLCH sRGB WCAG luminance math `scripts/audit.mjs` uses (see the ratios in `tokens-palette.css`'s comment).
524
+ - **Architectural home:** `tokens-palette.css`, immediately after the theme-selection mechanism — these are `--color-*` values, palette tier by the Token Tiers contract, the same tier the base `light-dark()`/`color-scheme` regime already lives in.
525
+ - **Simplified to one block under `light-dark()` (0.24.0, issue #95):** pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override three copies of two tokens, covering system-default, explicit-dark, and system-dark-without-an-explicit-choice respectively. A single un-scoped `light-dark(<light>, <dark>)` override per token now replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there (system, explicit, or the `:root.dark` bridge) see "Color Scheme & light-dark()".
526
+ - **Audit-parser compatibility:** `scripts/lib/css-tokens.mjs`'s `isLightRoot` filter requires an empty at-rule context — a `@media (prefers-contrast: more)` block's context is that at-rule itself, so it matches neither `isLightRoot` nor `isDarkBlock`, and `scripts/audit.mjs`/`scripts/tokens-json.mjs` silently skip these declarations, unchanged from before the light-dark() migration. That's still correct: the block only ever strictly improves an already-passing contrast margin, so there is nothing for the automated gate to enforce beyond what the base palette already guarantees. The values are still hand-verified in-gamut and re-computed with the same OKLCH → sRGB → WCAG luminance math `scripts/audit.mjs` uses (see the ratios in `tokens-palette.css`'s comment).
499
527
 
500
528
  **CI gate.** `site/tests/forced-colors.spec.ts` runs Chromium with `page.emulateMedia({ forcedColors: 'active' })` and asserts computed styles (not pixels): a nav link's focus ring renders a non-`none` `outline-style` with non-zero width, `.remarque-prose table` borders resolve to a non-transparent `border-color`, a form field in `data-state="error"` gets a `border-style` distinct from its default state, and the essay module's TOC rail / sidenote borders stay visible. Wired into the existing `visual-regression.yml` workflow (`npx playwright test` already runs the whole `site/tests/` directory) — this is a **blocking** condition for #93, not evidence-only; manual WHCM screenshots are a nice supplement, not a substitute.
501
529
 
@@ -528,6 +556,14 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
528
556
 
529
557
  Agents reviewing PRs should reject changes that violate any line above without explicit rationale.
530
558
 
559
+ ### APCA: Considered, Not Adopted (2026-07-23)
560
+
561
+ APCA (Accessible Perceptual Contrast Algorithm) was pulled into the WCAG3 working draft in 2023 and remains exploratory — no ratified conformance model specifies it, and that status is unchanged as of this writing. Remarque holds **WCAG 2.x's ratio-based contrast** as the enforcement gate (see "Contrast Ratios" above and the Enforcement Checklist) rather than building the audit toward an unratified algorithm.
562
+
563
+ This is a deliberate policy/data split, not an oversight: the upstream `@williamzujkowski/oklch-terminal-themes` dataset is adding APCA Lc values as *data* on its themes (oklch-terminal-themes#151) — a reasonable thing for a dataset to expose so consumers can experiment. Remarque's own audit gate does not consume it. Data availability and policy adoption are two different questions, and this project is only answering one of them right now.
564
+
565
+ **Revisit trigger:** a ratified WCAG3 conformance model that specifies an actual APCA (or successor) algorithm and threshold set. Until then, this is dated rather than re-litigated on every PR that mentions contrast.
566
+
531
567
  ---
532
568
 
533
569
  ## Lineage: Butterick's Five Rules
@@ -753,6 +789,10 @@ for the value-by-value mapping).
753
789
 
754
790
  ### Markup contract
755
791
 
792
+ <!-- registry-usage:essay — scripts/build-registry.mjs (issue #100) extracts
793
+ the fenced block immediately below into registry/essay.json's
794
+ usage.html verbatim. Keep this the single, hand-authored known-good
795
+ sample — do not fork a second copy for the registry. -->
756
796
  ```html
757
797
  <article class="remarque-essay">
758
798
  <header class="content-reading">
@@ -934,6 +974,9 @@ Four primitives, composed independently — a page can use any subset:
934
974
 
935
975
  ### Markup contract
936
976
 
977
+ <!-- registry-usage:broadsheet — scripts/build-registry.mjs (issue #100)
978
+ extracts the fenced block immediately below into
979
+ registry/broadsheet.json's usage.html verbatim. -->
937
980
  ```html
938
981
  <header class="remarque-masthead">
939
982
  <p class="remarque-masthead-kicker"><span>Field Notes</span></p>
@@ -1064,6 +1107,9 @@ existing literal.
1064
1107
 
1065
1108
  ### Markup contract
1066
1109
 
1110
+ <!-- registry-usage:forms — scripts/build-registry.mjs (issue #100) extracts
1111
+ the fenced block immediately below into registry/forms.json's
1112
+ usage.html verbatim. -->
1067
1113
  ```html
1068
1114
  <div class="remarque-field">
1069
1115
  <label class="remarque-field-label" for="email">Email</label>
@@ -1129,6 +1175,22 @@ with an invalid field — it never replaces authoring `data-state="error"`
1129
1175
  explicitly, since `:user-invalid` alone has no matching text-message
1130
1176
  recolor and no `aria-invalid` wiring of its own.
1131
1177
 
1178
+ A second `:has()`-powered bonus layer (issue #97) goes one step further:
1179
+ `:user-invalid` alone can only style the input it matches — CSS has no
1180
+ "parent selector" without `:has()`, so it cannot reach the field's label.
1181
+ `.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label`
1182
+ recolors the label to `--color-error` too, and
1183
+ `.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label`
1184
+ brightens the label to `--color-fg` while any control in the field has
1185
+ keyboard focus — a quiet secondary cue, not a replacement for the
1186
+ control's own native `:focus-visible` outline. Both are guarded with
1187
+ `@supports selector(:has(...))` (the same "the browser lacking this
1188
+ falls back to `data-state`/`aria-invalid`" shape as `:user-invalid`
1189
+ itself) and, like that layer, are bonus visual cues rather than one of
1190
+ the canonical `data-state` states — `data-state` stays the one
1191
+ contract every consumer must author explicitly; `:has()` never replaces
1192
+ it, only adds a zero-JS enhancement on top for browsers that support it.
1193
+
1132
1194
  ### Restraint rules restated
1133
1195
 
1134
1196
  - **Border radius**: form controls cap at `--radius-sm` (4px) — a
@@ -1271,6 +1333,15 @@ Set (or clear) `data-palette` on `<html>`, alongside the existing
1271
1333
  carries *both* light and dark halves, so the light/dark toggle keeps
1272
1334
  working no matter which palette (or none) is active:
1273
1335
 
1336
+ <!-- registry-usage:palette-deck — scripts/build-registry.mjs (issue #100)
1337
+ extracts the fenced block immediately below into
1338
+ registry/palette-deck.json's usage.html verbatim. The FOUC-safe
1339
+ `<script>` snippet a few lines down is deliberately NOT marked for
1340
+ extraction — see REMARQUE.md "The Registry" and the PR description
1341
+ for why (the registry's blocking no-executable-content condition is
1342
+ held to an unambiguous zero-`<script>`-tag bar across every item;
1343
+ that snippet's full text still lives here, one section up, and is
1344
+ linked from the registry item's `docs` URL). -->
1274
1345
  ```html
1275
1346
  <html data-theme="dark" data-palette="gruvbox">
1276
1347
  ```
@@ -1323,6 +1394,175 @@ switching between them became the only piece worth a shared module.
1323
1394
 
1324
1395
  ---
1325
1396
 
1397
+ ## The Registry
1398
+
1399
+ A machine-readable markup-contract registry — `registry.json` (index) +
1400
+ `registry/<name>.json` (four full items: `essay`, `broadsheet`, `forms`,
1401
+ `palette-deck`) — shipped for the ratified program's flagship item
1402
+ (issue #100). The funding rationale is narrow and mechanical, not
1403
+ "category-defining": it exists to prevent the exact bug class found in
1404
+ the flagship migration's sidenote implementation (#89,
1405
+ williamzujkowski.github.io#380) — an aria-label that drifted out of DOM
1406
+ order because the markup contract lived only as ~80 lines of spec prose
1407
+ an agent had to transcribe by hand. A registry item is that same known-
1408
+ good markup as versioned, hash-pinned DATA an agent fetches and applies
1409
+ instead.
1410
+
1411
+ **Shape.** Adopted from shadcn/ui's `registry-item.json`/`registry.json`
1412
+ (`https://ui.shadcn.com/schema/registry-item.json`,
1413
+ `https://ui.shadcn.com/schema/registry.json`) — field NAMES that fit a
1414
+ CSS+HTML contract are kept as-is (`name`, `title`, `description`,
1415
+ `dependencies`, `registryDependencies`, `files[].path`/`content`/`type`,
1416
+ `cssVars`, `docs`); this is deliberately **not** a parallel invented
1417
+ schema. The two vendored, hand-authored schemas
1418
+ (`registry-item.schema.json`, `registry.schema.json` at the package
1419
+ root) document every adaptation and omission inline, in the property
1420
+ descriptions themselves — the summary:
1421
+
1422
+ - **`type` is `"remarque:contract"`**, not one of shadcn's `registry:*`
1423
+ values (`registry:lib`/`component`/`ui`/`hook`/`page`/...). None of
1424
+ those describe a dependency-free CSS module plus its markup sample —
1425
+ every shadcn value assumes an installable React-ecosystem artifact.
1426
+ One honest value covers all four items today, since they're all the
1427
+ same KIND of thing; a second value would only be minted for a future
1428
+ item that is a genuinely different kind of contract.
1429
+ - **`cssVars` is repurposed**: shadcn's `cssVars` holds theme
1430
+ {`theme`/`light`/`dark`} VALUES to merge into a consumer's stylesheet.
1431
+ This registry ships whole CSS files verbatim instead (single-sourced
1432
+ from the real `.css` at build time — see "Single-sourcing" below), so
1433
+ there's nothing to merge; `cssVars` here is the flat, deduped list of
1434
+ `--custom-property` NAMES the item's CSS actually reads via `var(...)`
1435
+ — the palette-tier tokens it consumes, mechanically grepped, never
1436
+ hand-maintained.
1437
+ - **`docs` is a URL, not markdown.** shadcn's `docs` is a documentation
1438
+ blob; this registry's `docs` is a single HTTPS link into this file's
1439
+ relevant section. The registry doesn't duplicate spec prose — it
1440
+ points back at the one place that prose lives.
1441
+ - **Omitted fields** (documented once here, not repeated per-field):
1442
+ `tailwind`/`css`/`envVars` (no Tailwind config, direct CSS injection,
1443
+ or env vars — these items ship whole files, not fragments to splice
1444
+ in), `font`/`extends`/`style`/`iconLibrary`/`baseColor`/`theme` (all
1445
+ `registry:font`/`registry:base`-specific — not applicable to any item
1446
+ here), `files[].target` (no CLI installer — see "What this deliberately
1447
+ is NOT"), `meta` (no per-item metadata that doesn't already have a
1448
+ named field).
1449
+ - **`registry.json` is a lightweight index**, not shadcn's full-item
1450
+ embedding. shadcn's `items` array holds complete registry-item objects
1451
+ inline; this index instead lists one pointer per item (`name`, `type`,
1452
+ `title`, `description`, `version`, `integrity`, `file`) and leaves the
1453
+ full payload — all file content — in `registry/<name>.json` only, so a
1454
+ consumer can discover what exists and check its pinned hash before
1455
+ deciding whether to fetch the full item.
1456
+
1457
+ **Security (panel-mandated, blocking).** Every item carries a `version`
1458
+ (the exact `remarque-tokens` release it was generated from) and an
1459
+ `integrity` hash (`sha256-<base64>`, the same shape as W3C Subresource
1460
+ Integrity — computed over the item's `files` at build time by
1461
+ `scripts/lib/registry-extract.mjs`'s `sha256OfFiles`); `registry.json`'s
1462
+ index mirrors both per item, so a consumer can check the pin before
1463
+ fetching the full payload and must recheck it against what it actually
1464
+ receives. Every `docs`/`homepage` URL is `https://`. No item contains
1465
+ executable content — `scripts/test-registry.mjs` asserts no file's
1466
+ `content`, across every item without exception, contains the substring
1467
+ `<script`.
1468
+
1469
+ That last rule is why `deck.js` — the Palette Deck's runtime module, a
1470
+ dependency-free ~60-line ESM file, and the one JS asset this package
1471
+ ships — is **not embedded in the registry at all**, even hashed and
1472
+ labeled. The panel's condition reads as an absolute ("no executable
1473
+ content in registry items ... files are HTML/CSS data"), and the honest
1474
+ call under a *blocking* security condition is to hold that line without
1475
+ exception rather than carve out a documented-but-included special case.
1476
+ Concretely: `registry/palette-deck.json` ships **only** the
1477
+ `data-theme`/`data-palette` HTML fragment from this file's "Markup/wiring
1478
+ contract" section — it does *not* include the FOUC-safe restore
1479
+ `<script>` sample documented a few lines below that fragment, even
1480
+ though that snippet is (arguably) still "just an HTML example." Holding
1481
+ every item to a zero-`<script>`-tag bar, with no case-by-case judgment
1482
+ call about which scripts "count," is a simpler rule to mechanically
1483
+ enforce forever than "no script tags, except this one, which is fine
1484
+ because—". Consumers keep getting `deck.js` the normal way — the
1485
+ `remarque-tokens/deck` package import, listed as an ordinary
1486
+ `dependencies` entry on every item — and the FOUC snippet's full text
1487
+ from this section, linked via the item's `docs` URL.
1488
+
1489
+ **Modeling the tokens dependency.** essay/broadsheet/forms all consume
1490
+ palette-tier tokens, and the issue asked whether `tokens` should be a
1491
+ fifth registry item or a documented external dependency. It's the
1492
+ latter: every item's `dependencies` array lists `"remarque-tokens"` — an
1493
+ ordinary npm dependency, the same field shadcn uses for npm packages a
1494
+ component needs. Modeling tokens as a *registry item* would mean
1495
+ re-embedding (a subset of, or all of) `tokens.json`'s content a second
1496
+ time in a differently-shaped artifact; `tokens.json`/`tokens.schema.json`
1497
+ already exist as the purpose-built machine-readable form of the tokens
1498
+ (issue #99), served at the same site alongside this registry. Depending
1499
+ on the package that ships them is the smaller, honest mechanism.
1500
+
1501
+ **Single-sourcing.** Every item's CSS `content` is read verbatim from the
1502
+ real `.css` file at build time (`essay.css`, `broadsheet.css`,
1503
+ `forms.css`) — never a second hand-copied literal. Every item's
1504
+ `usage.html` is extracted from THIS file's own "Markup contract" fenced
1505
+ ```html blocks via an HTML-comment marker
1506
+ (`<!-- registry-usage:<name> -->`) placed immediately before the fence —
1507
+ `scripts/lib/registry-extract.mjs`'s `extractUsageHtml` finds the marker,
1508
+ then the next fenced block, and lifts its content verbatim. This spec's
1509
+ prose stays the ONE hand-authored copy of each known-good sample; the
1510
+ registry is a build artifact derived from it, exactly like `tokens.json`
1511
+ is derived from the CSS. `scripts/build-registry.mjs --check` gates
1512
+ freshness in CI the same way `scripts/tokens-json.mjs --check` already
1513
+ does for tokens.
1514
+
1515
+ **Validation (the enforce-don't-instruct house pattern).**
1516
+ `scripts/test-registry.mjs` is the CI gate, wired alongside the other
1517
+ freshness/fixture checks in `deploy.yml`. It re-derives, mechanically,
1518
+ the exact contract lines this issue is funded on — not by trusting the
1519
+ prose, by parsing the generated `usage.html` itself:
1520
+
1521
+ - Every essay `.remarque-sidenote-ref` carries `aria-label="Note N"`, and
1522
+ refs/notes strictly alternate in DOM order — the precise shape of the
1523
+ #89 bug.
1524
+ - The essay TOC rail (`.remarque-toc-rail`) carries `aria-label`.
1525
+ - Every broadsheet `.remarque-entry-numeral` carries `data-entry-number`.
1526
+ - Every forms `<input id="...">` has a matching `<label for="...">`, and
1527
+ every `aria-describedby` target id exists in the sample.
1528
+ - No file, in any item, contains `<script` (the no-executable-content
1529
+ rule above).
1530
+
1531
+ Plus the structural gates any generated artifact needs: schema
1532
+ validation (via `ajv`, the same devDependency `tokens.schema.json`
1533
+ already uses) with negative fixtures proving the schemas actually reject
1534
+ malformed input, and an independent recomputation of every `integrity`
1535
+ hash proving it wasn't just carried over from a stale build.
1536
+
1537
+ **What this deliberately is NOT.** No CLI installer — there is no
1538
+ `npx remarque-registry add essay` command, and no `files[].target`
1539
+ field to resolve an install path against. An agent's expected workflow
1540
+ is fetch the JSON, read `usage.html`/the CSS `content`, and apply it with
1541
+ its own editing tools — the registry replaces transcription-from-prose,
1542
+ not an installer. No archetype "starter-page" items this round: the
1543
+ issue floated one per archetype (Essay, Dossier, Notebook, ...) as an
1544
+ IF-cheap extra, and the honest call is SKIP — archetype pages live as
1545
+ demo-specific Astro files (`site/src/pages/**`), not as a reusable
1546
+ module with one canonical markup sample the way the four shipped items
1547
+ are, so extracting one would mean inventing a new "canonical" sample
1548
+ rather than single-sourcing an existing one.
1549
+
1550
+ **Fetching it.** The demo site serves the registry the same way it
1551
+ serves `tokens.json` (`site/scripts/copy-tokens-json.mjs`, extended):
1552
+
1553
+ - Index: **https://williamzujkowski.github.io/remarque/registry.json**
1554
+ - Per-item: **https://williamzujkowski.github.io/remarque/registry/essay.json**
1555
+ (and `broadsheet.json`, `forms.json`, `palette-deck.json`)
1556
+ - Schemas: **https://williamzujkowski.github.io/remarque/registry-item.schema.json**,
1557
+ **https://williamzujkowski.github.io/remarque/registry.schema.json**
1558
+
1559
+ npm package exports mirror the same paths: `remarque-tokens/registry.json`,
1560
+ `remarque-tokens/registry/essay` (the `./registry/*` subpath maps to
1561
+ `./registry/*.json`), `remarque-tokens/registry-item.schema.json`,
1562
+ `remarque-tokens/registry.schema.json`.
1563
+
1564
+ ---
1565
+
1326
1566
  ## Signature Moves
1327
1567
 
1328
1568
  These are the repeatable visual tells that make a Remarque site recognizable:
@@ -1353,6 +1593,41 @@ An implementation succeeds when:
1353
1593
 
1354
1594
  ---
1355
1595
 
1596
+ ## Governance & Deprecation
1597
+
1598
+ Remarque is small enough that governance fits on a page, but it now ships as a published npm package with multiple consuming sites and a CI gate other repos call into — enough surface that the rules deserve writing down rather than living only in maintainer habit. The precedent here is deliberately modest: closer to how a single-maintainer Carbon or Polaris component documents its own contract than to either project's full RFC process.
1599
+
1600
+ ### Semver contract
1601
+
1602
+ Remarque ships as a CSS token system, not an application — "breaking" means something different than it does for a library with a JS API surface:
1603
+
1604
+ - **MAJOR** — a token is **removed** or **renamed**. Anything a consumer's `var(--color-x)` or a `tokens.json` lookup resolves today must keep resolving after a minor or patch bump; only a major bump may take that away.
1605
+ - **MINOR** — a token's **value changes**, or a token is **added**. Value changes always carry a stated rationale in the CHANGELOG entry (a contrast fix, a gamut correction, an upstream dataset repin) — the change is minor because the *name* stays stable and every consumer reads it through `var()` indirection, but it is never silent. Additive changes (new tokens, new optional CLI flags, new subpath exports) are minor by definition — nothing existing changes shape.
1606
+ - **PATCH** — tool/script fixes with no token or CSS output change (a `remarque-audit` parser bug, a `tokens-json.mjs` generation fix, a fixture correction). If `tokens.json`/`tokens.d.ts`'s content is byte-identical before and after, it's a patch.
1607
+ - **MINOR (browser-floor case)** — a change that raises the *minimum supported browser version* by adopting a newer CSS mechanism as load-bearing (0.24.0's `light-dark()`/`color-scheme` migration, issue #95, is the first instance). Still a minor, not a major: no token is removed or renamed, and every consumer's `var(--color-x)`/`tokens.json` lookup keeps resolving — only the rendering environment required to resolve it *correctly* changes. The new floor is stated prominently in both the CHANGELOG entry and the relevant REMARQUE.md section, not buried in prose.
1608
+
1609
+ The project is still pre-1.0 — under strict SemVer every 0.x release is technically allowed to break anything — but every release to date has in practice held to the contract above (no token has ever been removed or renamed; see the version history in CHANGELOG.md). 1.0 is the point where that becomes a promise instead of an observed pattern.
1610
+
1611
+ ### Deprecation windows
1612
+
1613
+ A token or convention slated for removal is announced in the CHANGELOG at least one MINOR release before it disappears, named explicitly with a target version — not left implicit in a commit message. The table below is the centralized registry of every open sunset commitment; a PR that adds a new one should add a row here in the same commit, not just mention it once in the CHANGELOG.
1614
+
1615
+ | Deprecated / bridged item | Introduced | Sunset target | Rationale |
1616
+ |---|---|---|---|
1617
+ | `:root.dark` compatibility selector (alongside canonical `[data-theme="dark"]`) | 0.5.0 | 1.0 | Bridges class-keyed consumer sites onto the attribute-keyed convention `remarque-audit` treats as canonical — the audit already parses both forms, so the bridge costs nothing today. `color-scheme` + `light-dark()` — once "the platform endgame to watch" — landed in 0.24.0 (see "Color Scheme & light-dark()"); the bridge itself is unaffected, carrying a `color-scheme: dark` override now instead of a full color list, and its sunset target is unchanged. |
1618
+
1619
+ No other open sunset commitments exist as of this writing (grepped against REMARQUE.md and CHANGELOG.md for "sunset"/"deprecated"). This table is meant to be the single place to look — but only if every future commitment is added here, not just announced in passing.
1620
+
1621
+ ### Decision-log habit
1622
+
1623
+ Every design decision with more than one reasonable answer — a token value, a deviation from a cited precedent (Butterick, Carbon, USWDS), a scope change to an in-flight issue — gets a recorded vote, not just an implementation. The convention has been practiced since 0.5.0; this section states it as policy rather than leaving it implicit:
1624
+
1625
+ - The CHANGELOG entry for the release states the vote count (e.g. "ratified 3-0 by consensus panel") and, when the outcome wasn't unanimous or shipped with conditions, what those conditions were.
1626
+ - REMARQUE.md carries the same treatment inline wherever a deviation is asserted against upstream guidance — see "Lineage: Butterick's Five Rules" below for the fullest example: the guidance, the deviation, the argued rationale, and the ratification line together.
1627
+ - The habit exists so a future maintainer — human or agent — can tell the difference between "nobody thought about this" and "this was considered and decided," without reconstructing the reasoning from a diff.
1628
+
1629
+ ---
1630
+
1356
1631
  ## What Remarque Is Named For
1357
1632
 
1358
1633
  In fine art printmaking, a *remarque* is a small original drawing made by the artist in the margin of a print. Found only in limited proof editions, the remarque is the mark that says: this was made by a specific person, with intention, by hand.
package/forms.css CHANGED
@@ -358,6 +358,54 @@ textarea.remarque-input {
358
358
  }
359
359
  }
360
360
 
361
+ /* ─── `:has()` zero-JS validation enhancement layer (issue #97) ──────────
362
+ * A second, purely additive progressive-enhancement layer over the
363
+ * `:user-invalid` layer just above — same native-first shape, one step
364
+ * further. `:user-invalid` alone can only style the INPUT it matches;
365
+ * CSS has no "parent selector" without `:has()`, so it has no way to
366
+ * reach the field's label or wrapper. `:has()` is exactly the piece that
367
+ * closes that gap: `.remarque-field` can react to a descendant control's
368
+ * validity/focus state directly, with no `data-state` attribute and no
369
+ * JavaScript. Guarded with `@supports selector(:has(...))` — the same
370
+ * "degrades to the no-JS/no-support baseline" shape the `:user-invalid`
371
+ * block above uses — so a browser missing either pseudo-class in the
372
+ * compound query falls back cleanly to whatever `data-state`/
373
+ * `aria-invalid` (and, where supported, the plain `:user-invalid` layer
374
+ * above) already provide. `data-state` remains the CANONICAL contract;
375
+ * neither block below replaces authoring `data-state="error"` explicitly
376
+ * — see REMARQUE.md "State wiring".
377
+ *
378
+ * Like the bare `:user-invalid` layer above, this is a bonus visual cue,
379
+ * not one of the canonical `data-state` states the forced-colors
380
+ * reinforcement below was scoped to fix (REMARQUE.md "Forced Colors &
381
+ * Contrast Preferences") — it is not itself a new color-only-affordance
382
+ * regression, since the real accessible signal (`aria-invalid`, the
383
+ * `data-state` border/message wiring) is unchanged and unaffected by
384
+ * whether this bonus layer's color survives forced-colors mode.
385
+ */
386
+ @supports selector(:has(:user-invalid)) {
387
+ /* Extends the :user-invalid border recolor (input only, above) up to
388
+ the field's label — the one thing plain :user-invalid cannot reach
389
+ on its own. */
390
+ .remarque-field:has(.remarque-input:user-invalid) .remarque-field-label {
391
+ color: var(--color-error);
392
+ }
393
+ }
394
+
395
+ /* Field-level focus affordance — a quiet secondary cue, NOT a
396
+ replacement for the control's own native `:focus-visible` outline
397
+ (tokens-core.css), which already renders on the control itself and is
398
+ forced-colors-safe by construction. Brightens the label from
399
+ `--color-muted` to `--color-fg` while any control inside the field has
400
+ keyboard focus — a restrained color wash, not a new border/background,
401
+ matching this module's "no component should be visually louder than
402
+ the content it contains" rule (AGENT_RULES.md Components). */
403
+ @supports selector(:has(:focus-visible)) {
404
+ .remarque-field:has(.remarque-input:focus-visible) .remarque-field-label {
405
+ color: var(--color-fg);
406
+ }
407
+ }
408
+
361
409
  /* ─── Forced-colors reinforcement (issue #93) ────────────────────────────
362
410
  * The validation-state wiring above is the textbook color-only-affordance
363
411
  * case the issue names explicitly: error/success/warning are distinguished
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -37,7 +37,7 @@
37
37
  "culori": "^4.0.2"
38
38
  },
39
39
  "peerDependencies": {
40
- "@williamzujkowski/oklch-terminal-themes": ">=0.1.0 <0.3.0"
40
+ "@williamzujkowski/oklch-terminal-themes": ">=0.1.0 <1.0.0"
41
41
  },
42
42
  "peerDependenciesMeta": {
43
43
  "@williamzujkowski/oklch-terminal-themes": {
@@ -80,6 +80,10 @@
80
80
  "./deck.js": "./deck.js",
81
81
  "./agent-rules": "./AGENT_RULES.md",
82
82
  "./spec": "./REMARQUE.md",
83
+ "./registry.json": "./registry.json",
84
+ "./registry-item.schema.json": "./registry-item.schema.json",
85
+ "./registry.schema.json": "./registry.schema.json",
86
+ "./registry/*": "./registry/*.json",
83
87
  "./package.json": "./package.json"
84
88
  },
85
89
  "files": [
@@ -107,6 +111,12 @@
107
111
  "broadsheet.css",
108
112
  "forms.css",
109
113
  "deck.js",
110
- "scripts/lib/css-tokens.mjs"
114
+ "scripts/lib/css-tokens.mjs",
115
+ "registry.json",
116
+ "registry/",
117
+ "registry-item.schema.json",
118
+ "registry.schema.json",
119
+ "scripts/build-registry.mjs",
120
+ "scripts/lib/registry-extract.mjs"
111
121
  ]
112
122
  }