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 +26 -4
- package/CHANGELOG.md +79 -0
- package/README.md +24 -1
- package/REMARQUE.md +119 -28
- package/broadsheet.css +62 -14
- package/essay.css +47 -24
- package/fonts/features.json +100 -0
- package/forms.css +10 -6
- package/package.json +6 -3
- package/prose.css +47 -15
- package/registry/broadsheet.json +5 -4
- package/registry/essay.json +3 -3
- package/registry/forms.json +4 -4
- package/registry/palette-deck.json +1 -1
- package/registry.json +9 -9
- package/scripts/audit.mjs +73 -1
- package/scripts/build-registry.mjs +1 -1
- package/scripts/lib/font-features.mjs +148 -0
- package/scripts/lib/palette-coherence.mjs +234 -0
- package/scripts/theme.mjs +78 -9
- package/scripts/tokens-json.mjs +21 -15
- package/skills/remarque-new-page/SKILL.md +1 -1
- package/tokens-core.css +48 -7
- package/tokens.d.ts +1 -1
- package/tokens.json +11 -6
- package/tokens.schema.json +1 -1
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
|
|
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
|
|
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
|
|
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.
|
|
103
|
-
2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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,
|
|
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 `
|
|
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
|
|