remarque-tokens 0.27.0 → 0.28.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
@@ -221,11 +221,12 @@ Before considering any implementation complete, verify:
221
221
  - [ ] Every image has a 1px border and mono caption
222
222
  - [ ] Every page maps to an archetype (Essay, Dossier, Notebook, Landing, Reference/Docs, Changelog, Gallery)
223
223
  - [ ] If the Essay uses sidenotes/a TOC rail (`remarque-tokens/essay`): `.remarque-sidenote-ref`/`.remarque-sidenote` alternate in strict DOM order, the rail never intrudes into `.remarque-prose`'s own measure, and the page renders correctly with `essay.css`'s `@media` block deleted (the narrow-viewport/no-JS fallback)
224
- - [ ] If a Landing/archive page uses the Broadsheet pattern (`remarque-tokens/broadsheet`): entry numerals are generated from `data-entry-number` via `attr()` (not `counter()`), the entry list stays a `<ul>` (not `<ol>` — the numeral is `aria-hidden` and decorative), and every kicker/dateline row uses `font-variant-caps: all-small-caps`, never `text-transform: uppercase`
225
- - [ ] If a page uses form controls (`remarque-tokens/forms`): checkboxes/radios are styled with `accent-color` only — never `appearance: none` plus a hand-drawn replacement; every control (input, button, checkbox/radio label) is ≥44×44px; validation state lives on `.remarque-field[data-state]` AND a real `aria-invalid`/`aria-describedby` pair on the input, not `data-state` alone; disabled controls use `--color-disabled`, never a state color
224
+ - [ ] If a Landing/archive page uses the Broadsheet pattern (`remarque-tokens/broadsheet`): entry numerals are generated from `data-entry-number` via `attr()` (not `counter()`), the entry list stays a `<ul>` (not `<ol>` — the numeral is `aria-hidden` and decorative), and every kicker/dateline row uses the label voice: tracked capitals (`text-transform: uppercase` + `--tracking-caps`), because the default mono face has no small caps; `font-variant-caps: all-small-caps` only on a label face that has `smcp` + `c2sc` (REMARQUE.md "Small Caps"; `remarque-audit` fails synthesized small caps)
225
+ - [ ] If a page uses form controls (`remarque-tokens/forms`): checkboxes/radios are styled with `accent-color` only — never `appearance: none` plus a hand-drawn replacement; every control (input, button, checkbox/radio label) is ≥44×44px; every input collecting known personal data has a valid `autocomplete` token and paste is never blocked (WCAG 3.3.7/3.3.8); validation state lives on `.remarque-field[data-state]` AND a real `aria-invalid`/`aria-describedby` pair on the input, not `data-state` alone; disabled controls use `--color-disabled`, never a state color
226
226
  - [ ] If a page uses the Palette Deck (`remarque-tokens/deck`): `data-palette` is set/cleared on the same root element as `data-theme` (they compose, never conflict); the FOUC-safe restore is a synchronous classic `<script>` physically in `<head>` (never `type="module"`, which defers); the switcher control itself is a native, keyboard-operable element (e.g. `.remarque-input` `<select>`) at ≥44×44px, not a hand-rolled widget; every registered palette was generated via `remarque-theme --scope`, never hand-authored
227
227
  - [ ] Mobile version is roomy — not a compressed desktop layout
228
- - [ ] Mobile nav links have ≥44px touch targets
228
+ - [ ] Mobile nav links have ≥44px touch targets (meets WCAG 2.5.8 AA; exceeds it, and meets 2.5.5 AAA)
229
+ - [ ] WCAG 2.2 AA (not 2.1): any consumer sticky/fixed chrome has `scroll-padding-top`/`-bottom` so focus is never obscured (2.4.11); inputs collecting known personal data carry a valid `autocomplete` token and never block paste (3.3.7, 3.3.8); `cd site && npx playwright test tests/a11y.spec.ts` (axe-core) passes. See REMARQUE.md "WCAG 2.2 AA Criterion Map"
229
230
  - [ ] No pure white or pure black backgrounds
230
231
  - [ ] `npm run audit` passes (contrast, gamut, font floors, hardcoded colors)
231
232
  - [ ] Muted text placed on `--color-surface` still meets 4.5:1 (check the surface pairing, not just bg)
@@ -261,7 +262,7 @@ npx remarque-audit --palette <file> --src <dir> --json
261
262
  version: string; // the remarque-tokens package version running the audit
262
263
  palette: string; // the --palette path as given
263
264
  src: string; // the --src path as given
264
- passed: boolean; // true iff failures.length === 0
265
+ passed: boolean; // true iff failures.length === 0 (warnings do not count)
265
266
  contrast: Array<{
266
267
  theme: 'light' | 'dark';
267
268
  fg: string; // fg token name, no leading --
@@ -286,9 +287,30 @@ npx remarque-audit --palette <file> --src <dir> --json
286
287
  ambiguousTextSize: Array<{ file: string; line: number; snippet: string }>; // Tailwind text-[var(--text-*)] (Pitfall #8)
287
288
  };
288
289
  failures: string[]; // every failure as a human-readable message, same text the non-JSON run prints
290
+ warnings: Array<{ // palette-coherence warnings (REMARQUE.md "Palette Coherence"); added in 0.28.0, additive
291
+ id: string; // 'text-tier-monotonic' | 'accent-vs-fg' | 'accent-vs-error' | 'accent-vs-viz-1' | 'error-vs-success-deutan' | 'surface-step' | 'viz-cvd-normal' | 'viz-cvd-protan' | 'viz-cvd-deutan' | 'viz-cvd-tritan'
292
+ theme: 'light' | 'dark';
293
+ tokens: string[]; // the token names involved, no leading --
294
+ actual: number; // measured value (ΔE2000, or the contrast shortfall for text tiers), 2dp
295
+ threshold: number; // the minimum it was compared to
296
+ message: string; // human-readable, same text the non-JSON run prints
297
+ }>;
298
+ fontFeatures: Array<{ // font-feature requests the face can't render (REMARQUE.md "Small Caps"); added in 0.28.0, additive
299
+ file: string; // 'remarque-tokens/<module>.css' for the package's own modules, else the --src-relative path
300
+ line: number; selector: string;
301
+ family: string | null; // first family of the rule's font-family (or --font-body when the rule sets none)
302
+ inferred: boolean; // true when the family was inferred as --font-body
303
+ tag: string; // OpenType feature tag, e.g. 'smcp', 'onum'
304
+ via: string; // the declaration that asked for it
305
+ status: 'missing' | 'conditional' | 'unverifiable'; // only 'missing' is also a failure
306
+ }>;
289
307
  }
290
308
  ```
291
309
 
310
+ `--font-features <manifest.json>` (repeatable) adds your own faces to the check, in the shape of the package's `fonts/features.json` (`{ families: { "<family>": { figures: { style, spacing }, files: { "<file>": { features: [tags] } } } } }`). Faces missing from every manifest are reported `unverifiable` and never fail.
311
+
312
+ `warnings` never affects `passed` or the exit code in this release — only `failures` does. The warning set is expected to grow and to be promoted to failures in a later minor, so treat an unknown `id` as informational.
313
+
292
314
  `contrast` and `gamut` list EVERY check performed (passing and failing);
293
315
  only `srcScans` and `failures` are violation-only lists, since a source
294
316
  scan that finds nothing has nothing to report per line.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,85 @@ 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.28.0 — 2026-10-24
8
+
9
+ The "truth" minor of the 2026-10 roadmap (#133): it makes the spec's claims true on the default stack. It was researched with Open Design and nexus-agents, ratified 7–0 (`vote-1792819887868-esau595`), and landed as one PR per issue, each with its own regression test proven red on 0.27.0. **Consumer-visible changes:**
10
+ - labels render in tracked capitals instead of synthesized small caps (#124);
11
+ - the broadsheet masthead box widens to `--content-standard`, with its title sized from that box (#121);
12
+ - the essay column moves onto the page axis (#119);
13
+ - long strings wrap (#122);
14
+ - RTL and non-Latin scripts get logical properties and `:lang()` guards (#123);
15
+ - `remarque-audit` gains a failing font-feature check (#124) and palette-coherence warnings (#125);
16
+ - `html` reserves the scrollbar gutter.
17
+
18
+ No token was removed or renamed, and no token value changed.
19
+
20
+ ### Changed — Node engines
21
+
22
+ - **`engines.node` raised from `>=18` to `>=22`** (#120). Node 18 and 20 are end-of-life, and CI now tests 22 and 24. The CSS itself has no Node requirement; this covers the bins (`remarque-audit`, `remarque-drift`, `remarque-theme`).
23
+
24
+ ### Changed — labels in tracked capitals; font-feature gate (#124; consumer-visible, ratified 7–0)
25
+
26
+ - **Labels set in real capitals, not synthesized small caps.** `.text-label` and every module label (broadsheet kickers and datelines, the TOC summary, the footnotes heading, `.remarque-field-label`) now use `text-transform: uppercase` + `--tracking-caps` instead of `font-variant-caps: all-small-caps`. None of the shipped fonts has `smcp` or `c2sc` (nor do their full upstream releases), so the browser faked small caps by shrinking capitals: a 14px label drew its caps at about 9.9px, under the 13px floor. Labels keep the 14px size. **Labels look different**: larger capitals in the same mono voice. If your label face has real `smcp` + `c2sc`, you can restore `font-variant-caps: all-small-caps; text-transform: none` yourself (REMARQUE.md "Small Caps"). Ratified 7–0 in `vote-1792819660668-a5kb6q3`. This replaces the #51 rule, rewritten in the same change in REMARQUE.md, AGENT_RULES.md, the registry description and the new-page skill.
27
+ - **Oldstyle figures are conditional.** `.remarque-prose` and the broadsheet entry numerals keep `oldstyle-nums`, now documented as "when the face has `onum`". Inter and Newsreader don't, so default figures are lining, and this always was the rendering. A face with `onum` (the flagship's Source Serif 4) still gets text figures.
28
+ - **New `remarque-audit` check: font features.** Every feature a stylesheet asks for (`font-variant-caps`, `font-variant-numeric`, `font-variant`, `font-feature-settings`), in the package's modules and in every `.css` under `--src`, is checked against the face the rule renders in, from the new shipped manifest `fonts/features.json` (OpenType GSUB+GPOS tags per face, generated with fontTools by `scripts/font-features.py`). A request the face can't render **fails**, unless the rule carries a `remarque-feature: conditional` comment. The check accounts for default figures (lining faces need no `lnum`; tabular faces no `tnum`). `--font-features <manifest>` adds a consumer's faces; faces in no manifest are reported, never failed. New JSON key `fontFeatures` (additive). The check landed red on the 0.27.0 tree first: 8 failures across 6 small-caps rules and 2 oldstyle rules.
29
+ - `scripts/test-font-features.mjs` covers manifest freshness (a sha256 per shipped woff2, so re-subsetting a font without regenerating the manifest fails), each status, and the CLI end to end, including a consumer manifest clearing a request.
30
+ - REMARQUE.md "Wrapping & Optical Defaults" gains a feature-support table (webstatus.dev, 2026-10-24): `text-wrap: pretty` and `hanging-punctuation` have no Firefox support, and `initial-letter` is Chromium plus prefixed Safari.
31
+
32
+ ### Documentation — DTCG (#132)
33
+
34
+ - **The #99 DTCG trigger fired on 2025-10-28.** The Format, Color and Resolver modules were published as Final Community Group Reports. REMARQUE.md's "DTCG Conformance" now records this, retires the rule against fixing `tokens.json` toward unratified drafts, and does not word the gate as waiting for a W3C Recommendation (a CG cannot produce one).
35
+ - **All three divergences are documented**, in REMARQUE.md, the generated `tokens.json` `$extensions.remarque.dtcg` note, the `tokens.schema.json` description and the README: oklch() color strings, per-token light/dark nesting, and the new third one, CSS-string dimension/duration values (`clamp()` has no DTCG form).
36
+ - **A demand gate replaces the ratification gate.** A `tokens.dtcg.json` export is built only when a consumer asks for a DTCG 2025.10-shaped file, or Style Dictionary or Terrazzo documents full 2025.10 support including resolver output. Nothing is built now.
37
+ - Metadata only: no token value, name, CSS output or `tokens.d.ts` change. `test-types.mjs` §6 now requires at least three divergences, a dimension/duration aspect, and no `gatedOn` still saying "ratifying". Ships with 0.28.0.
38
+
39
+ ### Fixed — RTL and non-Latin scripts (#123; LTR rendering unchanged)
40
+
41
+ - **essay.css: wide sidenotes float to `inline-start`.** A physical `float: left` paired with a logical negative `margin-inline-start` printed every note on top of the paragraph under `dir="rtl"` (measured at 1440px: note at x 400–560 over prose at 400–1008). It now lands in the right-hand gutter in RTL, and `float: left` stays as the fallback for engines without logical float values.
42
+ - **A logical-property sweep** of every physical left/right left in the modules. The blockquote bar, list indent, `dd` and table header/caption alignment, numeric cells (`text-align: end`), the details marker margin, the prose and lead drop caps (`float: inline-start`, logical padding), and the broadsheet numerals now mirror. In RTL the blockquote bar had stayed on the left, and lists and `dd` kept the browser's 40px start indent on the right. The closed disclosure marker flips under `:dir(rtl)`.
43
+ - **Script-aware guards.** Under `:lang(ar|fa|ur)` the tracking tokens are 0 (Remarque's `-0.02em` display tracking breaks cursive joining). Under `:lang(zh|ja|ko)` display tracking is 0 and heading leading is 1.35–1.4. The decorative italics set upright for `ar`, `fa`, `ur`, `he`, `zh`, `ja` and `ko`, and `em` uses weight instead. REMARQUE.md's Latin-only note now says what is and isn't guarded.
44
+ - New `site/tests/rtl.spec.ts` gates the RTL sidenotes and rail, the prose mirroring and the script guards, with an English control. 4 of its 5 tests fail on 0.27.0.
45
+ - `budgets.json` was raised. `tokens-core.css` and `broadsheet.css` passed their 10% headroom (brotli 3627 vs 3600; gzip 7301 vs 7300). The budget test caught it, as intended.
46
+
47
+ ### Fixed — page axis and the masthead (#119, #121; consumer-visible)
48
+
49
+ - **broadsheet.css: the masthead title is sized from its nameplate, not the viewport (#121, a 0.27.0 regression).** The title grew at `9vw` inside a box capped at `--content-reading`. When that token went to 38rem in 0.27.0, the box stopped growing at about 640px of viewport while the title kept growing to 7.5rem, so a four-word title wrapped to four lines at 1280px. `.remarque-masthead` is now an inline-size container with a `--content-standard` (72rem) box, and the title rate is `9cqi`, with `9vw` kept as the fallback; the bounds are unchanged. At 1280px and up the title is about 98px, four- to seven-word titles hold to two lines, and the title stays above the `--text-display` lead title. A 38rem box would have put the masthead below the lead, which is why the nameplate widened instead (ratified 7–0, `vote-1792889632787-bm0nogp`). The lead and entry list stay at the reading measure. **If you set `--remarque-masthead-size` yourself, nothing changes for you.**
50
+ - **essay.css: the reading column sits on the page axis (#119).** At 80rem and up the grid was `1fr | reading | 10–12rem`, so the column was centered only when the left slack happened to equal the rail: 48px off-axis at 1440px on the demo. Both outer tracks are now `minmax(--space-11, 1fr)` and the rail is capped at `--space-12` plus its focus-ring padding. Sidenotes and the rail are unchanged.
51
+ - **tokens-core.css: `html { scrollbar-gutter: stable; }`.** On classic-scrollbar platforms a centered column no longer shifts by about 15px between short and long pages. Overlay scrollbars (macOS, phones) are unaffected.
52
+ - New gates in `site/tests/page-axis.spec.ts`: the essay column is centered like a plain page at 1280, 1440 and 1920px; sidenotes and the rail clear the prose; a four-word masthead title is at most two lines and larger than the lead title at 768, 1280 and 1920px.
53
+
54
+ ### Fixed — long strings reflow (#122; consumer-visible)
55
+
56
+ - **`html { overflow-wrap: break-word; }` in `tokens-core.css`.** No layer set `overflow-wrap`, so one unbreakable string (a URL, a hash, a package path, long inline `code`) widened the whole page at phone width, failing WCAG 1.4.10. Measured on the demo at 320px with fixtures: 788px of page overflow in prose, 1291px across the broadsheet titles. It changes nothing for text that fits, and it inherits into consumer chrome too.
57
+ - **`overflow-wrap: anywhere` on flex labels** (TOC rail links and summary, `.remarque-button`, `.remarque-checkbox`, `.remarque-radio`). Their text is an anonymous flex item whose min-content is its longest word, which `break-word` can't shrink: a long TOC entry pushed the rail 528px wide.
58
+ - `<pre>` is unchanged on purpose: it keeps `white-space: pre` and scrolls inside its own box. REMARQUE.md "Wrapping & Optical Defaults" documents the difference.
59
+ - New `site/tests/reflow.spec.ts`: every demo page at 320 CSS px; long-string fixtures in prose, broadsheet titles, forms messages, buttons and labels, and the TOC rail; and the WCAG 1.4.12 text-spacing override set, with no clipped controls, TOC entries or sidenotes. The WCAG 2.2 criterion map now points at it.
60
+
61
+ ### Accessibility — WCAG 2.2 AA (#126)
62
+
63
+ - **Spec moves from WCAG 2.1 AA to 2.2 AA.** REMARQUE.md's accessibility section and enforcement checklist now cite 2.2, with a new "WCAG 2.2 AA Criterion Map" (criterion, mechanism, enforcement) and a "Regulatory Context" paragraph (EN 301 549 V4.1.1, the EAA, Section 508). 4.1.1 Parsing is noted as removed in 2.2. No CSS output change.
64
+ - The 44px touch-target line now cites 2.5.8 (AA) as the criterion met, with 2.5.5 (AAA) as the exceeded one; before, it cited only 2.5.5.
65
+ - **2.4.11 note:** consumer sticky/fixed chrome needs `scroll-padding-top`.
66
+ - **Forms contract (3.3.7 / 3.3.8):** inputs that collect known personal data carry a valid `autocomplete` token and never block paste. The forms markup sample now shows `autocomplete="email"`. Mirrored in AGENT_RULES.md.
67
+ - **axe-core gate.** `site/tests/a11y.spec.ts` runs `@axe-core/playwright` (4.13.0) with the wcag2a/2aa/21a/21aa/22aa tags over every demo page in both themes, plus a forced-colors pass, and fails on serious/critical violations. The only exclusion is `color-contrast` in the forced-colors pass (justified in the spec).
68
+ - Demo fix found by axe: the inline link in the Notebook page's body text was distinguished by color alone (2.13:1 against the surrounding text in dark); it is now underlined (`link-in-text-block`).
69
+
70
+ ### CI and release (not part of the package; #120, roadmap Step 0)
71
+
72
+ - **Every root gate now runs on pull requests.** The gates moved verbatim from `deploy.yml` into a reusable `.github/workflows/ci.yml` (Node 22 and 24). `deploy.yml` and `release.yml` both `need` it. Before this, pull requests ran only visual regression, and a `v*` tag ran only `audit.mjs`, so a tag on a commit with a stale `tokens.json` or a failing fixture suite would have published.
73
+ - **`release.yml`** pins npm to an exact version instead of `npm@latest` and creates a GitHub Release whose notes are this file's matching section. A tag with no `## X.Y.Z ` section fails before publishing.
74
+ - **`npm test`** chains the same gates for local runs. `scripts/test-pack.mjs` now strips the parent's `npm_*` environment from its child `npm`/`npx` calls; under `npm test` they inherited npm's local prefix and installed into this repo instead of the scratch consumer.
75
+ - **`.github/dependabot.yml`**: weekly version updates for the root and `site/` npm trees and for the SHA-pinned actions, with a 7-day cooldown. `@williamzujkowski/oklch-terminal-themes` stays a hand bump because `palette-golden.mjs` gates it.
76
+ - `main` is now branch-protected and requires the CI and visual-regression checks.
77
+ - **`scripts/test-budget.mjs`** (picked up by `npm test` and run in CI): gzip and brotli size budgets for every shipped CSS file plus the effective bare `tokens.css` import, against a committed, unshipped `budgets.json` (current size + 10%; `--update` rewrites it), and a module-closure check that every `var(--x)` in `prose`/`essay`/`broadsheet`/`forms`/`print` resolves to a core, palette or module-local custom property.
78
+
79
+ ### Added — palette coherence warnings (#125)
80
+
81
+ - **`remarque-audit` now checks palette coherence**, as **warnings** for this minor (printed in the human output and in `--json` under a new additive `warnings` array; `passed` and the exit code are unchanged). Rules: monotonic text tiers (`fg` ≥ `fg-muted` ≥ `muted` contrast); accent vs `fg` ≥ 3:1 or ΔE2000 ≥ 15; accent vs `error` and vs `viz-1` ΔE ≥ 8; `error` vs `success` ΔE ≥ 5 under deuteranopia; adjacent `bg`/`surface`/`bg-subtle`/`code-bg` steps ΔE ≥ 3; minimum pairwise ΔE ≥ 5 across `--color-viz-1..6` under normal, protan, deutan and tritan simulation. Thresholds sit in one block, `THRESHOLDS` in `scripts/lib/palette-coherence.mjs`; rationale in REMARQUE.md "Palette Coherence". The warnings are slated to become failures in a later minor.
82
+ - **No new runtime dependency.** The audit ships to consumers who do not install culori, so the color math (OKLCH to sRGB, Machado CVD matrices, CIEDE2000) is implemented in the new shipped `scripts/lib/palette-coherence.mjs`; `scripts/test-coherence-math.mjs` cross-checks it against culori's `differenceCiede2000` and `filterDeficiency*` so the two cannot drift.
83
+ - **`remarque-theme` enforces the accent guards when generating.** If the default accent source collides with the derived error color (ΔE < 8) or with `fg`, it rotates to the nearest-hue other chromatic ANSI color that clears both, and it keeps `fg` at least as high-contrast as `fg-muted`. This fixes the Palette Deck's gruvbox (accent == error) and rose-pine (accent ≈ fg, fg-muted above fg) palettes on regeneration. `scripts/test-theme.mjs` now asserts the guards across the whole terminal-theme corpus.
84
+ - The default palette emits warnings today (viz-1 near accent, dark `surface` == `bg-subtle`, viz slots under CVD). That is the evidence base for #128; no default token values change here.
85
+
7
86
  ## 0.27.0 — 2026-10-23
8
87
 
9
88
  Design review of the system and its demo site, run with Open Design's `design-review` methodology (gstack's 10-category / ~80-item rubric, calibrated against REMARQUE.md and AGENT_RULES.md): five independent reviewers, about 50 findings, AI-slop grade A throughout. The defects were in implementation, not identity. Two identity-level calls were put to a nexus consensus vote, and both passed 7–0. Closes #115 and #116.
package/README.md CHANGED
@@ -54,7 +54,7 @@ Add `--json` to either `remarque-audit` or `remarque-drift` for a single structu
54
54
  @import "remarque-tokens/theme.css";
55
55
  ```
56
56
 
57
- **Tailwind v3** projects use the shipped config (`remarque-tokens/tailwind`) instead. A machine-readable token inventory ships as `remarque-tokens/tokens.json` (generated from the CSS — core/palette tiers, light+dark values) for tooling and AI agents, with a published JSON Schema at `remarque-tokens/tokens.schema.json` (also referenced by tokens.json's own `$schema` field) so tooling can validate it structurally instead of hand-parsing. tokens.json is conformant in spirit with the [Design Tokens Community Group](https://www.designtokens.org/) format (`$value`/`$type` on every token), with two deliberate divergences — see REMARQUE.md's "DTCG Conformance" section. Prefer copy-paste? Grab `fonts.css`, `tokens.css`, `tokens-core.css`, `tokens-palette.css`, and `fonts/` directly — `tokens.css` aggregates the two tier files, so all three CSS files travel together. Use string-form `@import './tokens.css'` only (some bundlers silently drop `@import url(...)` for local files).
57
+ **Tailwind v3** projects use the shipped config (`remarque-tokens/tailwind`) instead. A machine-readable token inventory ships as `remarque-tokens/tokens.json` (generated from the CSS — core/palette tiers, light+dark values) for tooling and AI agents, with a published JSON Schema at `remarque-tokens/tokens.schema.json` (also referenced by tokens.json's own `$schema` field) so tooling can validate it structurally instead of hand-parsing. tokens.json is conformant in spirit with the [Design Tokens Community Group](https://www.designtokens.org/) format (`$value`/`$type` on every token), with three deliberate divergences (oklch() color strings, per-token light/dark nesting, CSS-string dimension/duration values) — see REMARQUE.md's "DTCG Conformance" section. Prefer copy-paste? Grab `fonts.css`, `tokens.css`, `tokens-core.css`, `tokens-palette.css`, and `fonts/` directly — `tokens.css` aggregates the two tier files, so all three CSS files travel together. Use string-form `@import './tokens.css'` only (some bundlers silently drop `@import url(...)` for local files).
58
58
 
59
59
  ## For AI Agents
60
60
 
@@ -118,6 +118,25 @@ Output" shape changes) to pick up the current playbook.
118
118
  | `tailwind.config.js` | Tailwind CSS **v3** configuration (v4 projects use an `@theme` block instead) |
119
119
  | `package.json` | npm package manifest for `remarque-tokens` |
120
120
 
121
+ ### Sizes
122
+
123
+ Gzip (level 9) / brotli (max) bytes per shipped stylesheet, enforced by `scripts/test-budget.mjs` against `budgets.json` (current size + 10%, repo-only, not in the package). Bare `remarque-tokens` imports core + palette + prose.
124
+
125
+ | File | gzip | brotli |
126
+ |------|-----:|-------:|
127
+ | `tokens.css` (effective: core + palette + prose) | 15,788 | 13,269 |
128
+ | `tokens-core.css` | 4,790 | 3,995 |
129
+ | `tokens-palette.css` | 7,036 | 5,848 |
130
+ | `prose.css` | 4,949 | 4,139 |
131
+ | `essay.css` | 7,914 | 6,680 |
132
+ | `broadsheet.css` | 7,463 | 6,241 |
133
+ | `forms.css` | 7,709 | 6,378 |
134
+ | `print.css` | 2,529 | 2,093 |
135
+ | `theme.css` | 1,744 | 1,452 |
136
+ | `fonts.css` | 612 | 484 |
137
+
138
+ Minimal import: `tokens-core.css` + `tokens-palette.css` alone (about 12 KB gzip) work without `prose.css`; the same test checks that every `var(--x)` in the optional modules resolves to a core or palette token.
139
+
121
140
  ## Three-Font System
122
141
 
123
142
  | Role | Font | Usage |
@@ -191,6 +210,10 @@ deliberately stayed local.
191
210
  | Ambient/decorative animation | Site-local only, never in system modules — and the `prefers-reduced-motion` guard is mandatory, not advisory. A future audit scan may flag unguarded infinite animations (as a heuristic check, not a soundness proof). |
192
211
  | Landing-page entry counts | Permitted as meta-voice data in the Landing archetype **only when computed from real inventory at build time**. A hardcoded count is a violation — counts drift (see the upstream count-drift bug class this system just fixed in its own theme provider). |
193
212
 
213
+ ## Development
214
+
215
+ `npm test` runs every gate CI runs: the token audit, the freshness checks for the generated files (`tokens.json`, the registry, the packaged skills), the palette golden gate, and every `scripts/test-*.mjs` fixture suite, including the consumer pack smoke test, which needs the network. `.github/workflows/ci.yml` runs the same gates on every pull request on Node 22 and 24, and both the Pages deploy and the npm release `need` it, so nothing deploys or publishes from a red commit.
216
+
194
217
  ## Named For
195
218
 
196
219
  In fine art printmaking, a *remarque* is a small original drawing made by the artist in the margin of a proof print. Found only in limited editions, it is the mark that says: this was made by someone specific, with intention.
package/REMARQUE.md CHANGED
@@ -97,12 +97,20 @@ This makes compliance mechanical: a site that overrides only palette-tier tokens
97
97
 
98
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`).
99
99
 
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:
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 three **deliberate** structural divergences from the DTCG 2025.10 format, documented in the generated file itself (`$extensions.remarque.dtcg`) so a future regeneration can't lose the rationale:
101
101
 
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.
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.
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.
103
+ 2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus the Resolver module's `theme` modifier with `contexts`. 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 the Resolver module and is what the tooling depends on.
104
+ 3. **Dimension and duration `$value`s are CSS strings**, not `{ value, unit }` objects. For example `core.text-display` is `"clamp(2.75rem, 5.5vw, 5rem)"` with `$type: dimension`, and `core.motion-fast` is `"120ms"`. DTCG 2025.10 requires `{ value, unit }`, and `clamp()` has no DTCG form, so any DTCG export would have to flatten or drop fluid values.
104
105
 
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.
106
+ **The #99 trigger fired on 2025-10-28.** The DTCG [Format](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/), [Color](https://www.w3.org/community/reports/design-tokens/CG-FINAL-color-20251028/) and [Resolver](https://www.w3.org/community/reports/design-tokens/CG-FINAL-resolver-20251028/) modules were published as Final Community Group Reports. A Community Group cannot produce a W3C Recommendation, so a Final CG Report is the furthest a CG document goes and the gate is not "pending a Recommendation". Conformance is therefore **permitted**, and the earlier rule against "fixing" `tokens.json` toward unratified drafts is retired. It is still **not built**: the CSS remains the source of truth, and the three divergences stay load-bearing for the tooling that depends on this shape.
107
+
108
+ **Demand gate for a DTCG export.** Build an additive `tokens.dtcg.json` (and a resolver file if useful) only when either holds:
109
+
110
+ - **(a)** a consumer files an issue asking for a DTCG 2025.10-shaped file; or
111
+ - **(b)** Style Dictionary or Terrazzo documents full 2025.10 support *including resolver output*.
112
+
113
+ Branch (b) measures tooling readiness rather than demand and is kept because it can be checked independently. The export would be additive, generated from the CSS by `scripts/tokens-json.mjs`; `tokens.json`, `tokens.d.ts` and `remarque-drift` are not changed by it. Decided in #132.
106
114
 
107
115
  ---
108
116
 
@@ -175,6 +183,8 @@ This is why the measure is palette-tier: it is a property of the font choice, no
175
183
 
176
184
  **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.
177
185
 
186
+ **What Remarque does guard (0.28, #123).** Layout is written in logical properties (`margin-inline-start`, `float: inline-start`, `text-align: start`/`end`), so `dir="rtl"` mirrors the essay grid, sidenotes, TOC rail, blockquote bar, lists, tables and drop caps with no extra CSS. The tracking and display-leading tokens are reset under `:lang()`: `ar`, `fa` and `ur` get zero tracking (negative or positive tracking breaks cursive joining); `zh`, `ja` and `ko` get zero display tracking and 1.35–1.4 heading leading. The decorative italics (blockquote, pullquote, `h5`, `dt`, drop caps, the masthead's italic half, the lead's lede) set upright for `ar`, `fa`, `ur`, `he`, `zh`, `ja` and `ko`, which have no italic tradition and get a faked slant otherwise, and `em` carries its emphasis in weight. The fonts have no glyphs for these scripts, so the guards only stop the tokens from harming whatever face the fallback stack picks; choosing that face, and the measure, is still the consumer's job. `site/tests/rtl.spec.ts` gates the mirroring and the guards.
187
+
178
188
  ---
179
189
 
180
190
  ## Editorial Microtypography
@@ -193,14 +203,20 @@ The book-craft layer: numerals, small caps, drop caps, pull quotes, and optical
193
203
 
194
204
  Two numeral registers, chosen by context:
195
205
 
196
- - **`.remarque-prose`** uses `font-variant-numeric: oldstyle-nums proportional-nums` — text figures that sit quietly inside a sentence, the way numerals behave in well-set book prose.
206
+ - **`.remarque-prose`** uses `font-variant-numeric: oldstyle-nums proportional-nums` — text figures that sit quietly inside a sentence, the way numerals behave in well-set book prose — **when the body face has them** (an `onum` feature). The default body face, Inter, has no `onum`, and neither does the default display face, Newsreader, so on the default stack prose and the broadsheet entry numerals set **lining** figures; the request is a no-op there, not a fake. A body face that has `onum` (the flagship's Source Serif 4) gets text figures from the same declaration with no change. Both rules carry a `remarque-feature: conditional` marker, so `remarque-audit` reports them as conditional rather than failing them (0.28, #124).
197
207
  - **`.text-meta`** (and anything sharing its role — dates, counts, table cells, timestamps) uses `font-variant-numeric: tabular-nums lining-nums` — full-height, fixed-width digits that align in columns and don't jitter as they change.
198
208
 
199
209
  Never mix the two: a numeral inside running prose should never be tabular, and a numeral in a metadata row or table should never be oldstyle. Apply `.num` to any `<td>`/`<th>` holding numeric data to opt that cell into the tabular-lining register (see prose.css's table rules).
200
210
 
201
211
  ### Small Caps
202
212
 
203
- `font-variant-caps: all-small-caps` — not `text-transform: uppercase` — for any label that wants a caps-like treatment. Faux uppercase renders letters at cap-height with no optical correction and reads louder than intended; true small caps render at x-height with corrected weight and spacing. `.text-label` (mono, `--text-meta`, all-small-caps, `--tracking-caps`, `--color-muted`) is the sanctioned utility. `text-transform: uppercase` is disallowed anywhere a label could use `.text-label` instead.
213
+ Labels that want a caps-like treatment use **real capitals with added tracking** on faces without small caps, and **true small caps only on a face that has them** (0.28, #124, ratified 7–0 in `vote-1792819660668-a5kb6q3`; this replaces the #51 rule).
214
+
215
+ - **The default stack has no small caps.** None of the shipped fonts (Inter, Newsreader, JetBrains Mono) has `smcp` or `c2sc`, and neither do their full upstream releases. When CSS asks for `font-variant-caps: all-small-caps` on such a face, the browser *synthesizes* small caps by shrinking capitals: no corrected weight or spacing, and a 14px label draws its caps at about 9.9px, under the 13px floor. Until 0.28 that was every Remarque label.
216
+ - **So the default is capitals:** `text-transform: uppercase` plus `letter-spacing: var(--tracking-caps)` (0.06em), Butterick's rule for faces without small caps. `.text-label` (mono, `--text-meta`, uppercase, `--tracking-caps`, `--color-muted`) is the sanctioned utility, and the module labels repeat it: kickers and datelines, the TOC summary, the footnotes heading, form labels. Keep the 14px (`--text-meta`) size; `--text-micro` stays reserved for timestamps.
217
+ - **Real small caps are still the finer choice where they exist.** If your label face has both `smcp` and `c2sc`, set `font-variant-caps: all-small-caps; text-transform: none;` on `.text-label` in your own CSS, and give `remarque-audit` a manifest for that face (`--font-features`) so the gate can see the features.
218
+ - **Enforced.** `remarque-audit` fails any rule that asks a face for a feature it lacks (small caps, oldstyle figures, `font-feature-settings` tags), checked against `fonts/features.json`. That's the OpenType feature list of every shipped face, generated with fontTools by `scripts/font-features.py`. A rule that is deliberately face-dependent says so with a `remarque-feature: conditional` comment and is reported, not failed. Consumers add their own faces with `--font-features <manifest.json>` (same shape). Defaults count: a face whose default figures are lining satisfies `lining-nums` without `lnum`, and a tabular-by-default face satisfies `tabular-nums` without `tnum`.
219
+ - Uppercase for *running text* (a shouted sentence, a heading set in caps) is still out: the rule is about labels.
204
220
 
205
221
  ### Drop Caps (opt-in)
206
222
 
@@ -226,6 +242,17 @@ Never mix the two: a numeral inside running prose should never be tabular, and a
226
242
  - Headings: `text-wrap: balance`, `font-optical-sizing: auto`, `font-synthesis: none` — headings never wrap ragged, optical sizing lets variable fonts pick the right grade, and browsers never fake bold/italic for faces that don't have them.
227
243
  - Prose: `text-wrap: pretty` (avoids single-word orphan lines) and `hanging-punctuation: first last` (quotes and punctuation hang outside the measure rather than indenting the line).
228
244
  - All four properties degrade safely — unsupported browsers silently ignore them and fall back to normal wrapping. Invisible when absent, unmistakable when present.
245
+ - What renders where (webstatus.dev, checked 2026-10-24). These are enhancements, so a reader on an unsupported browser gets ordinary, correct text, not a broken page:
246
+
247
+ | Property | Chrome / Edge | Firefox | Safari | Remarque's fallback |
248
+ |---|---|---|---|---|
249
+ | `text-wrap: balance` | 114 | 121 | 17.5 | normal wrapping (Baseline 2024) |
250
+ | `text-wrap: pretty` | 117 | — | 26 | normal wrapping; orphans possible |
251
+ | `hanging-punctuation` | — | — | yes | quotes sit inside the measure |
252
+ | `initial-letter` (drop caps) | 110 | — | prefixed (`-webkit-initial-letter`) | the `float` drop cap in prose.css |
253
+ | `font-variant-caps` small caps | needs `smcp`/`c2sc` in the font | same | same | synthesized; why labels are capitals (see "Small Caps") |
254
+ | `oldstyle-nums` | needs `onum` in the font | same | same | lining figures (see "Numerals") |
255
+ - Long strings: `html { overflow-wrap: break-word; }` (`tokens-core.css`, 0.28, #122). A URL, hash, package path or long inline `code` that can't fit its line breaks instead of widening the page (WCAG 1.4.10 Reflow). It changes nothing for text that fits and inherits into every layer and consumer chrome. `break-word` doesn't shrink an element's min-content width, so the flex labels that must narrow past a long word (TOC rail links and summary, buttons, checkbox and radio labels) use `overflow-wrap: anywhere`; do the same in your own flex or grid chrome. Code blocks are the exception on purpose: `<pre>` keeps `white-space: pre` and scrolls horizontally inside its own box (`overflow-x: auto`), because re-wrapping code changes what it says. Wide tables likewise scroll inside `.scroll-wrap` (prose) or `.remarque-table-wrap` (forms); a bare wide table still widens the page.
229
256
 
230
257
  ---
231
258
 
@@ -475,7 +502,7 @@ Tailwind v4 ships `z-*` as a static, non-themable utility scale — there's no `
475
502
 
476
503
  ## USWDS Accessibility Compliance
477
504
 
478
- Remarque adopts typography and accessibility standards from the [US Web Design System (USWDS)](https://designsystem.digital.gov/documentation/accessibility/) and [WCAG 2.1](https://www.w3.org/WAI/WCAG21/Understanding/):
505
+ Remarque adopts typography and accessibility standards from the [US Web Design System (USWDS)](https://designsystem.digital.gov/documentation/accessibility/) and [WCAG 2.2](https://www.w3.org/WAI/WCAG22/Understanding/) (Level AA; a superset of 2.1 AA, so 2.1 consumers are covered):
479
506
 
480
507
  ### Font Size Floors
481
508
 
@@ -485,7 +512,7 @@ Remarque adopts typography and accessibility standards from the [US Web Design S
485
512
  | `--text-meta` | 14px (0.875rem) | USWDS minimum for small text. Used sparingly for metadata, labels, captions. |
486
513
  | `--text-micro` | 13px (0.8125rem) | USWDS floor for timestamps and fine print only. Never for body content. |
487
514
 
488
- ### Contrast Ratios (WCAG 2.1 AA)
515
+ ### Contrast Ratios (WCAG 2.2 AA)
489
516
 
490
517
  | Pairing | Requirement | Notes |
491
518
  |---------|-------------|-------|
@@ -495,7 +522,29 @@ Remarque adopts typography and accessibility standards from the [US Web Design S
495
522
 
496
523
  ### Touch Targets
497
524
 
498
- All interactive elements (links, buttons, inputs) must have a minimum touch target of **44x44px** ([WCAG 2.5.5](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html) AAA). This applies to nav links, theme toggles, footer links, chips, and any clickable element.
525
+ All interactive elements (links, buttons, inputs) must have a minimum touch target of **44x44px**. That satisfies [WCAG 2.5.8 Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) (AA, 24x24px) with margin and also meets the stricter [2.5.5 Target Size (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html) (AAA, 44x44px) and the USWDS floor. This applies to nav links, theme toggles, footer links, chips, and any clickable element.
526
+
527
+ ### WCAG 2.2 AA Criterion Map
528
+
529
+ Remarque already meets most of WCAG 2.2 AA by construction. Each row names the mechanism and what fails the build if it regresses. Pointers are to shipped CSS and the demo's Playwright suite (`site/tests/`).
530
+
531
+ | SC | Remarque's mechanism | Enforced by |
532
+ |---|---|---|
533
+ | 1.4.3 Contrast (Minimum), 1.4.11 Non-text Contrast | Palette tiers verified at 4.5:1 text / 3:1 functional borders and dataviz marks; `--color-border` decorative only | `npm run audit` (`scripts/audit.mjs`); axe `color-contrast` in `site/tests/a11y.spec.ts` |
534
+ | 2.4.7 Focus Visible, 2.4.13 Focus Appearance (AAA, exceeded) | Global `:focus-visible` 2px outline, 2px offset (`tokens-core.css`, "Focus Styles"); `outline-color: Highlight` under forced-colors | `forced-colors.spec.ts` focus-ring assertions |
535
+ | 2.4.11 Focus Not Obscured (Minimum) | No sticky/fixed chrome ships; the essay TOC rail sticks in its own column, never over the reading column. **Consumers:** any sticky or fixed header/footer you add needs a matching `scroll-padding-top` (or `scroll-padding-bottom`) on `html` so focused elements are not hidden behind it | Convention (AGENT_RULES bans sticky headers); not machine-checked |
536
+ | 2.5.8 Target Size (Minimum) | 44px floor on every interactive element (`forms.css` `.remarque-input`, checkbox/radio label, `.remarque-button`; `essay.css` TOC links; demo `.nav-link`) | `computed-style.spec.ts` "interactive target size (>= 44x44 CSS px)"; axe `target-size` |
537
+ | 1.4.10 Reflow, 1.4.12 Text Spacing | Measure and spacing authored in relative units (`rem`/`ch`/`em`), unitless line-heights; `overflow-wrap: break-word` on `html`, `anywhere` on flex labels; `<pre>` and wide tables scroll in their own box | `site/tests/reflow.spec.ts`: every demo page at 320px, long-string fixtures in prose, broadsheet titles, forms and the TOC rail, and the 1.4.12 override set with no clipped controls |
538
+ | 3.3.7 Redundant Entry, 3.3.8 Accessible Authentication (Minimum) | Forms contract: known-personal-data inputs carry a valid `autocomplete` token; paste is never blocked (see "Forms") | Convention + review (AGENT_RULES forms line) |
539
+ | 3.2.6 Consistent Help | Consumer concern: if a page offers help (contact, chat, FAQ link), place it in the same relative position on every page | Consumer review |
540
+ | 1.3.1, 4.1.2 (ARIA contracts) | Registry markup contracts: sidenote `aria-describedby`, TOC `aria-label`, forms `aria-describedby`/`aria-invalid` | axe on every demo page, both themes, plus a forced-colors pass |
541
+ | 4.1.1 Parsing | **Removed in WCAG 2.2** (obsolete; browsers handle parsing errors uniformly). Nothing to enforce | n/a |
542
+
543
+ axe-core catches only the machine-checkable subset of WCAG; it complements the computed-style and forced-colors suites rather than replacing manual review.
544
+
545
+ ### Regulatory Context
546
+
547
+ [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/) V4.1.1 (published 2026-09-02) is the EU harmonised standard behind the European Accessibility Act (EAA). It moves the web clauses from WCAG 2.1 AA to WCAG 2.2 AA and adds EAA mapping annexes. Until the Commission cites V4.1.1 in the Official Journal of the EU, V3.2.1 (WCAG 2.1 AA) remains the presumption-of-conformity baseline; because 2.2 AA is a superset, a Remarque page that meets 2.2 AA meets both. US Section 508 references WCAG 2.0 A/AA, which this section exceeds. Remarque is a styling layer: conformance claims about a whole site (content, captions, authored alt text, 3.2.6 help placement) remain the consumer's.
499
548
 
500
549
  ### Line Height
501
550
 
@@ -546,7 +595,10 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
546
595
  - [ ] `--color-muted` contrast ≥ 4.5:1 against `--color-bg` **and** `--color-surface` in both themes.
547
596
  - [ ] `--color-fg-muted` contrast ≥ 7:1 (AAA) for body-adjacent prose.
548
597
  - [ ] All transitions use `--motion-fast`/`--motion-normal` tokens (reduced-motion support depends on it).
549
- - [ ] Every interactive element is ≥ 44×44px (use min-height/padding, not font-size to reach the floor).
598
+ - [ ] Every interactive element is ≥ 44×44px (use min-height/padding, not font-size to reach the floor). Meets WCAG 2.5.8 AA (24px) and 2.5.5 AAA.
599
+ - [ ] Any consumer sticky/fixed chrome has a matching `scroll-padding-top`/`-bottom` so focus is never obscured (WCAG 2.4.11).
600
+ - [ ] Inputs collecting known personal data carry a valid `autocomplete` token and never block paste (WCAG 3.3.7, 3.3.8).
601
+ - [ ] `cd site && npx playwright test tests/a11y.spec.ts` passes: axe-core, WCAG 2.0–2.2 A/AA tags, no serious/critical violations on any demo page in either theme.
550
602
  - [ ] `font-variant-numeric: tabular-nums lining-nums` on all metadata that mixes with dates/counts.
551
603
  - [ ] No hardcoded hex/rgb colors — only `var(--color-*)` tokens.
552
604
  - [ ] Body line-height ≥ 1.5 (Remarque target: 1.75).
@@ -558,6 +610,22 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
558
610
 
559
611
  Agents reviewing PRs should reject changes that violate any line above without explicit rationale.
560
612
 
613
+ ### Palette Coherence (warnings, issue #125)
614
+
615
+ Contrast floors say each color is readable; they do not say the roles that must look *different* actually do. `remarque-audit` therefore also checks palette **coherence**, as **warnings** in this release (they print in the human output and appear in `--json` under `warnings`; they never change `passed` or the exit code). They are scheduled to be promoted to failures in a later minor. CIEDE2000 (ΔE) is computed on the sRGB-rendered color; "under protan/deutan/tritan" means after a Machado et al. 2009 severity-1.0 simulation (the same matrices as culori's `filterDeficiencyProt/Deuter/Trit`). All thresholds live in one block, `THRESHOLDS` in `scripts/lib/palette-coherence.mjs`.
616
+
617
+ | Rule (id) | Warns when | Why |
618
+ |---|---|---|
619
+ | Monotonic text tiers (`text-tier-monotonic`) | contrast(`fg`) < contrast(`fg-muted`), or contrast(`fg-muted`) < contrast(`muted`), against `bg` | Primary text must never be fainter than secondary (rose-pine light had fg 7.42:1 under fg-muted 7.52:1). |
620
+ | Accent vs body text (`accent-vs-fg`) | accent/fg < 3:1 **and** ΔE < 15 | A link must differ from body text by value or by hue; hue alone at ~1:1 is invisible. |
621
+ | Accent vs error (`accent-vs-error`) | ΔE < 8 | A link and an error message must not be the same color (gruvbox shipped accent == error). |
622
+ | Accent vs chart 1 (`accent-vs-viz-1`) | ΔE < 8 | Enforces "chart color is never mistaken for the system's one interactive signal" (see "Dataviz Tokens"). |
623
+ | Error vs success (`error-vs-success-deutan`) | ΔE < 5 under deuteranopia | The red/green state pair is the classic CVD collapse. |
624
+ | Surface ramp (`surface-step`) | any adjacent step in `bg` → `surface` → `bg-subtle` → `code-bg` has ΔE < 3 (the warning names the pair) | Redundant surfaces are noise; separate the steps or drop the token. |
625
+ | Viz under CVD (`viz-cvd-normal/protan/deutan/tritan`) | minimum pairwise ΔE across `--color-viz-1..6` < 5 under that vision | ~5 is where marks become confusable. The six slots differ mostly by hue, so below this "never color alone" is the only thing separating series: label directly, or vary marker/dash. |
626
+
627
+ `remarque-theme` applies the accent guards as generation constraints: when the default accent source lands within ΔE 8 of the derived `--color-error`, or indistinguishable from `--color-fg`, in either theme, it rotates to the nearest-hue other chromatic ANSI color that clears both (and says so on stderr); it also keeps `fg` at least as high-contrast as `fg-muted`. Generated palettes therefore never emit `accent-vs-error` or `text-tier-monotonic`. The viz/surface rules are not generator guards: viz comes from the dataset's categorical block, and the surface ramp is the fixed offsets above.
628
+
561
629
  ### APCA: Considered, Not Adopted (2026-07-23)
562
630
 
563
631
  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.
@@ -895,7 +963,7 @@ breakpoint, no JS:
895
963
  | Viewport | Sidenotes | TOC |
896
964
  |---|---|---|
897
965
  | `< 80rem` (narrow) | Small-print block, in-flow, right after the citing paragraph — bordered with `--color-border-bold` (a 3:1 functional border, since it's the note's only visual separator at this width) | Inline collapsible `<details>` in the reading column, native disclosure triangle |
898
- | `>= 80rem` (wide) | Floats into the LEFT margin gutter via a Tufte-style negative-margin technique, `clear: left` stacks consecutive notes, border relaxes to decorative `--color-border` (whitespace is now the real separator) | `position: sticky` in the RIGHT rail, `.remarque-essay` becomes a 3-column grid (gutter / reading column / rail) |
966
+ | `>= 80rem` (wide) | Floats into the inline-start margin gutter (left in LTR, right in RTL) via a Tufte-style negative-margin technique, `clear: inline-start` stacks consecutive notes, border relaxes to decorative `--color-border` (whitespace is now the real separator) | `position: sticky` in the inline-end rail (right in LTR), `.remarque-essay` becomes a 3-column grid (gutter / reading column / rail) |
899
967
 
900
968
  The rail and margin notes always live OUTSIDE `--content-reading` — that
901
969
  is the entire point of the module. The center grid track is
@@ -960,7 +1028,7 @@ value-by-value mapping and the fluid-type decision below).
960
1028
  Four primitives, composed independently — a page can use any subset:
961
1029
 
962
1030
  1. **Masthead** (`.remarque-masthead`) — centered nameplate: mono
963
- small-caps kicker, an oversized mixed roman + italic `--font-display`
1031
+ tracked-capitals kicker, an oversized mixed roman + italic `--font-display`
964
1032
  title, mono dateline with hairline-rule separators.
965
1033
  2. **Lead article** (`.remarque-lead`) — a featured story: mono kicker
966
1034
  (`Section · N min read · Date`), a large `--font-display` title with
@@ -1045,10 +1113,10 @@ Requirements the CSS depends on:
1045
1113
  - Every kicker/dateline row (`.remarque-masthead-kicker`,
1046
1114
  `.remarque-masthead-dateline`, `.remarque-lead-kicker`,
1047
1115
  `.remarque-entry-kicker`, `.remarque-post-kicker`) uses
1048
- `font-variant-caps: all-small-caps` — never `text-transform: uppercase`
1049
- (the flagship predates the Small Caps rule and used uppercase
1050
- throughout this pattern; corrected on the way upstream, same as the
1051
- Essay Module's TOC summary).
1116
+ the label voice: tracked capitals (`text-transform: uppercase` +
1117
+ `--tracking-caps`), because the default mono face has no small caps
1118
+ (see "Small Caps"; #124). Until 0.28 these were synthesized
1119
+ `all-small-caps`.
1052
1120
 
1053
1121
  ### Fluid-type decision: the masthead title
1054
1122
 
@@ -1072,13 +1140,35 @@ tokens already in `tokens-core.css`, not new literals:
1072
1140
  ```
1073
1141
 
1074
1142
  Both bounds resolve to the exact flagship values already validated
1075
- downstream (contrast/gamut/font-floor audits + axe). The `9vw` rate is
1076
- unchanged from the flagship — it continues the same escalating per-tier
1077
- vw progression `tokens-core.css`'s own scale already uses (2.25vw
1078
- section → 3.5vw title → 5.5vw display), one step further for a register
1079
- one step past display. This property is declared inside `broadsheet.css`
1080
- only, never added to `tokens-core.css` — it is local arithmetic on
1081
- existing core tokens, not a new one.
1143
+ downstream (contrast/gamut/font-floor audits + axe). The rate 9 continues
1144
+ the same escalating per-tier progression `tokens-core.css`'s own scale
1145
+ already uses (2.25vw section → 3.5vw title → 5.5vw display), one step
1146
+ further for a register one step past display. This property is declared
1147
+ inside `broadsheet.css` only, never added to `tokens-core.css` — it is
1148
+ local arithmetic on existing core tokens, not a new one.
1149
+
1150
+ **Sized from the nameplate, not the viewport (0.28, #121; ratified 7–0,
1151
+ `vote-1792889632787-bm0nogp`).** Type should be sized from the measure it
1152
+ sits in (Tim Brown, *Flexible Typography*), so the rate is `9cqi` of the
1153
+ nameplate's own box. `.remarque-masthead` is an inline-size container
1154
+ whose box is `--content-standard` (72rem), the page-width register a
1155
+ newspaper nameplate spans; the lead article and entry list below it stay
1156
+ at `--content-reading`. Inside `@supports (width: 1cqi)` the property is
1157
+ redeclared with `9cqi`; the `9vw` declaration above stays as the
1158
+ fallback. Because the property is unregistered, `cqi` resolves where the
1159
+ title uses it, against the nameplate box.
1160
+
1161
+ Why not keep the reading-measure box: until 0.28 the title grew at `9vw`
1162
+ inside a box capped at `--content-reading`. When that token went to 38rem
1163
+ in 0.27 the box stopped at about 640px of viewport while the title kept
1164
+ growing to 7.5rem, so "Notes Toward a Quieter Typography" wrapped to four
1165
+ lines at 1280px. Sizing from a 38rem box instead tops out near 67px,
1166
+ *below* the `--text-display` lead title beneath it (70–80px), which
1167
+ inverts the hierarchy this pattern exists for. Measured at 1280px and
1168
+ up, the 72rem nameplate sets the title at about 98px, holds four- to
1169
+ seven-word titles to two lines, and keeps it above the lead.
1170
+ `site/tests/page-axis.spec.ts` gates both (at most 2 lines for a
1171
+ four-word title at 768, 1280 and 1920px; masthead larger than lead).
1082
1172
 
1083
1173
  Everywhere else in the pattern, an existing token already covers the
1084
1174
  register: `.remarque-lead-title` uses `--text-display` as-is (its
@@ -1116,7 +1206,7 @@ existing literal.
1116
1206
  <div class="remarque-field">
1117
1207
  <label class="remarque-field-label" for="email">Email</label>
1118
1208
  <input class="remarque-input" type="email" id="email" name="email"
1119
- aria-describedby="email-help" />
1209
+ autocomplete="email" aria-describedby="email-help" />
1120
1210
  <p class="remarque-field-message" id="email-help">
1121
1211
  We'll never share your email.
1122
1212
  </p>
@@ -1125,7 +1215,7 @@ existing literal.
1125
1215
  <div class="remarque-field" data-state="error">
1126
1216
  <label class="remarque-field-label" for="email2">Email</label>
1127
1217
  <input class="remarque-input" type="email" id="email2" name="email2"
1128
- aria-invalid="true" aria-describedby="email2-message" required />
1218
+ autocomplete="email" aria-invalid="true" aria-describedby="email2-message" required />
1129
1219
  <p class="remarque-field-message" id="email2-message" role="alert">
1130
1220
  Enter a valid email address.
1131
1221
  </p>
@@ -1148,9 +1238,10 @@ existing literal.
1148
1238
  | Class | Applies to | Notes |
1149
1239
  |---|---|---|
1150
1240
  | `.remarque-field` | wrapping `<div>` | Label + control + message vertical stack. Layout only — no border/background of its own. |
1151
- | `.remarque-field-label` | `<label>` | Meta voice (mono, small caps, `--color-muted`) — same declarations as `.text-label`, repeated per the non-mixin precedent essay.css/broadsheet.css already set. |
1241
+ | `.remarque-field-label` | `<label>` | Meta voice (mono, tracked capitals, `--color-muted`) — same declarations as `.text-label`, repeated per the non-mixin precedent essay.css/broadsheet.css already set. |
1152
1242
  | `.remarque-field-required` | `<span aria-hidden="true">` inside the label | Accent-colored asterisk. `aria-hidden` — the input's own `required` attribute is the real signal a screen reader announces. |
1153
1243
  | `.remarque-field-message` | `<p>` after the control | Help text by default; recolored by the parent's `data-state`. Wire `aria-describedby` on the input to this element's `id` in every case, valid or not. |
1244
+ | `autocomplete` (attribute) | `.remarque-input` collecting known personal data | WCAG 3.3.7 / 3.3.8: inputs that collect known personal data (name, email, tel, address, `current-password`, `one-time-code`, ...) carry a valid [`autocomplete` token](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill), and never block paste (no `onpaste` handlers that cancel it). |
1154
1245
  | `.remarque-input` | `<input>` / `<textarea>` / `<select>` | Body-voice text, `--color-border-bold` boundary, `--radius-sm`, ≥44px tall. |
1155
1246
  | `.remarque-checkbox` / `.remarque-radio` | wrapping `<label>` | `accent-color` on the native control — no `appearance: none`, no hand-drawn replacement. The wrapping label carries the 44px touch target; the control itself stays at its natural (enlarged) size. |
1156
1247
  | `.remarque-button` | `<button>` | Quiet default: bordered, transparent, body-voice text. |
@@ -1201,7 +1292,7 @@ it, only adds a zero-JS enhancement on top for browsers that support it.
1201
1292
  a control the user directly manipulates.
1202
1293
  - **Touch targets**: every interactive control — input, textarea,
1203
1294
  select, button, and the `<label>` wrapping a checkbox/radio — is
1204
- ≥44×44px (WCAG 2.5.5 AAA, USWDS floor; see "Touch Targets" above and
1295
+ ≥44×44px (exceeds WCAG 2.5.8 AA's 24px minimum and meets 2.5.5 AAA, USWDS floor; see "Touch Targets" above and
1205
1296
  `.nav-link`'s identical convention in the demo site's `globals.css`).
1206
1297
  - **No fake replacements**: checkboxes and radios stay native elements
1207
1298
  recolored with `accent-color` only. No `appearance: none` plus a
@@ -1516,7 +1607,7 @@ does for tokens.
1516
1607
 
1517
1608
  **Validation (the enforce-don't-instruct house pattern).**
1518
1609
  `scripts/test-registry.mjs` is the CI gate, wired alongside the other
1519
- freshness/fixture checks in `deploy.yml`. It re-derives, mechanically,
1610
+ freshness/fixture checks in `ci.yml`. It re-derives, mechanically,
1520
1611
  the exact contract lines this issue is funded on — not by trusting the
1521
1612
  prose, by parsing the generated `usage.html` itself:
1522
1613