remarque-tokens 0.29.0 → 0.30.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
@@ -223,7 +223,7 @@ Before considering any implementation complete, verify:
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
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
225
  - [ ] Site chrome uses `remarque-tokens/chrome` instead of hand-rolled copies: `.remarque-skip-link` is the first focusable element and targets `<main id="main-content" tabindex="-1">`; a breadcrumb's current page is `<span aria-current="page">`, never a link, with no separator typed into the DOM; prev/next links carry `rel="prev"`/`rel="next"` and an endpoint with nowhere to go is omitted (or plain text in numbered pagination), never a disabled-looking link; tags are plain mono links with `rel="tag"`, no border, fill or radius (REMARQUE.md "Chrome Module")
226
- - [ ] 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
+ - [ ] 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; every state message says what's wrong in words (the shipped glyph has empty alt text and is the non-color cue, not the message); a pending submit disables the button, changes its label and announces through `role="status"`, never a spinner or `aria-busy` on the button; disabled controls use `--color-disabled`, never a state color
227
227
  - [ ] 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
228
228
  - [ ] Mobile version is roomy — not a compressed desktop layout
229
229
  - [ ] Mobile nav links have ≥44px touch targets (meets WCAG 2.5.8 AA; exceeds it, and meets 2.5.5 AAA)
@@ -272,6 +272,7 @@ npx remarque-audit --palette <file> --src <dir> --json
272
272
  required: number; // minimum ratio (e.g. 4.5, 7.0, 3.0)
273
273
  actual: number | null; // computed ratio rounded to 2dp, or null if fg/bg was unresolvable
274
274
  ok: boolean;
275
+ apcaLc?: number; // APCA Lc (SAPC 0.0.98G), 1dp, REPORT-ONLY: never affects ok/passed; + = dark text on light, − = light on dark. Added in 0.30.0
275
276
  error?: string; // present only when actual is null (unresolved var()/missing token)
276
277
  }>;
277
278
  gamut: Array<{
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ 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.30.0 — 2026-10-24
8
+
9
+ The demand-gated tail of the 2026-10 roadmap (#133), worked through item by item. The identity questions went to nexus panels first. **Consumer-visible changes:**
10
+ - prose `h4` is set in the display face (#131);
11
+ - form state messages carry a glyph in every mode (#128);
12
+ - hover washes appear only where hover exists, and buttons get a pressed state (#129);
13
+ - the spec's lede samples are in full foreground (#131).
14
+
15
+ Additive: code-block titles, figure indent, opt-in external-link marks, readonly and auto-growing fields (#129), the shadcn-installable registry and `llms-full.txt` (#130), and an advisory APCA column in `remarque-audit` (#128). **Default palette values are unchanged:** the panel held them behind the demand gate 7–0 and documented opt-in recipes instead (#128). No token was added, removed or renamed.
16
+
17
+ ### Changed — the heading ladder and the lede (#131; consumer-visible)
18
+
19
+ - **`.remarque-prose h4` is set in the display face** (Newsreader roman, 17px, weight 500) instead of Inter 17/600. Next to Inter 19/600 for h3, it differed by 2px and read as the same level. It now alternates face with its neighbours (h3 sans, h4 serif roman, h5 serif italic, h6 mono). That survives print, `prefers-contrast` and forced colors, where a color-only difference would collapse. Sizes are unchanged. Ratified 5–2 in `vote-1792892296073-vxi53q7` (option A over a color-only change). Per the panel: under the `:lang()` script guards, where h5 sets upright and would match h4, h5 steps down to `--color-fg-muted`.
20
+ - **New REMARQUE.md "Type Scale" section.** The text rungs are floors, not a modular ratio; the display rungs are fluid and open from about 1.15–1.47 steps on phones to about 1.5 on desktops (measured at 375, 1280 and 1920px). It adds the heading-ladder table and the rule that rung values only change by vote.
21
+ - **The lede is in `--color-fg`.** The spec's samples and the demo set the 19px standfirst in `fg-muted`, which read quieter than the body below it. `.text-body-lg` itself sets no color, so no token changes.
22
+ - Demo: byline tags render as a `.remarque-tags` list instead of a run-together phrase. New `site/tests/type-scale.spec.ts`.
23
+
24
+ ### Added — prose and forms details (#129) and state cues (#128)
25
+
26
+ - **Forms: a non-color state cue in every mode (#128, WCAG 1.4.1).** The ✕/⚠/✓ glyphs before state messages, which until now appeared only under forced colors, render everywhere, with empty alt text. Under simulated deuteranopia the default error and success colors are only ΔE2000 2.7 (light) and 1.7 (dark) apart. **Consumer-visible:** state messages gain a leading glyph.
27
+ - **Forms: hover only where hover exists.** Input and button hover washes moved inside `@media (hover: hover)`, so a tap no longer leaves a control looking hovered. Buttons gain an `:active` pressed state (border to `--color-fg`).
28
+ - **Forms: `readonly` inputs** get a quiet `--color-bg-subtle` fill. **`.remarque-input--autosize`** opts a textarea into `field-sizing: content`, from the 6rem minimum to a 24rem cap. The text-only pending-submit pattern is documented.
29
+ - **Prose:**
30
+ - `pre[data-title]` renders a mono filename line (the flagship hand-rolls a filled tab for this);
31
+ - `[data-highlighted-line]` inside a `pre` gets the `--color-accent-subtle` wash;
32
+ - `figure` loses the browser's 40px side indent;
33
+ - the opt-in `.remarque-prose--external-marks` adds a screen-only ↗ after `rel="external"` links, without clobbering `print.css`'s printed URLs.
34
+ - **Default palette values are unchanged (#128, ratified 7–0 in `vote-1792892374374-55pw50q`).** REMARQUE.md "Dataviz Tokens" now documents opt-in recipes instead: a verified viz lightness ladder (worst-pair ΔE2000 under color-vision simulation rises from 3.5 to 11.7 in light and from 2.7 to 13.6 in dark, every slot ≥ 3.4:1 on bg and surface) and a dark `--color-muted` lift (5.77:1, APCA Lc −40.3). It also records the revisit triggers: a consumer request, or the coherence warnings being promoted to failures. The default change would then go through an upstream release first, never a golden-gate exception.
35
+ - **`remarque-audit` prints an advisory APCA Lc beside every contrast ratio** (new `apcaLc` field in each `contrast` record; SAPC 0.0.98G constants, checked against the reference values). It's report-only: WCAG 2.x stays the gate, because the WCAG 3 working draft still leaves its contrast algorithm undetermined. It shows what the ratio hides: default dark `--color-muted` passes at 4.92:1 but measures Lc −34.5 against light's 70.0.
36
+
37
+ ### Added — distribution (#130)
38
+
39
+ - shadcn-installable registry, additive: `registry/shadcn/<name>.json` plus an index `registry/shadcn/registry.json`, generated from the same item definitions and freshness-checked by `build-registry.mjs --check`. `type: "registry:item"`, `registry:file` entries with `src/styles/remarque/*` and `src/components/remarque/*` targets, `remarque-tokens` under `dependencies`, Remarque-only fields under `meta`. Validated in `test-registry.mjs` against the vendored official shadcn schemas (`scripts/vendor/`). Existing `registry/*.json` and the `remarque:contract` type are unchanged. Published beside them on the demo site.
40
+ - `llms-full.txt` on the demo site (AGENT_RULES.md plus a token table with light and dark values), generated at site build time and linked from `llms.txt`.
41
+
7
42
  ## 0.29.0 — 2026-10-24
8
43
 
9
44
  The "chrome" minor of the 2026-10 roadmap (#133, ratified 7–0 in `vote-1792819887868-esau595`): a new `remarque-tokens/chrome` module and four registry items. Additive: nothing existing changes for consumers who don't import it.
package/README.md CHANGED
@@ -76,7 +76,8 @@ Packaging for agent tooling:
76
76
 
77
77
  All three ship in the npm tarball under `skills/` (see "Installing the skills" below) as well as living at `.claude/skills/` in this repo.
78
78
  - **Live tokens endpoint:** the demo site serves the current `tokens.json` at **https://williamzujkowski.github.io/remarque/tokens.json**, and its schema at **https://williamzujkowski.github.io/remarque/tokens.schema.json** — a remote agent can fetch current token values (and validate their shape) directly instead of trusting training data.
79
- - **Markup-contract registry:** a shadcn-`registry-item.json`-shaped, version-pinned, hash-verified registry of known-good markup for the Essay/Broadsheet/Forms/Palette Deck modules — **https://williamzujkowski.github.io/remarque/registry.json** (index) and **https://williamzujkowski.github.io/remarque/registry/essay.json** (per item; also `broadsheet`/`forms`/`palette-deck`). Fetch and apply the markup instead of transcribing it from spec prose — see REMARQUE.md's "The Registry" and AGENT_RULES.md's "Prefer the Registry Over Transcribing Prose."
79
+ - **Markup-contract registry:** a shadcn-`registry-item.json`-shaped, version-pinned, hash-verified registry of known-good markup for the Essay/Broadsheet/Forms/Palette Deck modules — **https://williamzujkowski.github.io/remarque/registry.json** (index) and **https://williamzujkowski.github.io/remarque/registry/essay.json** (per item; also `broadsheet`/`forms`/`palette-deck`). Fetch and apply the markup instead of transcribing it from spec prose — see REMARQUE.md's "The Registry" and AGENT_RULES.md's "Prefer the Registry Over Transcribing Prose." shadcn users: `npx shadcn@latest add https://williamzujkowski.github.io/remarque/registry/shadcn/essay.json`, or register `"@remarque": "https://williamzujkowski.github.io/remarque/registry/shadcn/{name}.json"` under `registries` in `components.json` and run `npx shadcn@latest add @remarque/essay`.
80
+ - **`llms-full.txt`:** **https://williamzujkowski.github.io/remarque/llms-full.txt** — AGENT_RULES.md plus a compact token table (name, tier, light, dark) in one fetch, generated at site build time.
80
81
 
81
82
  ### Installing the skills
82
83
 
@@ -125,13 +126,13 @@ Gzip (level 9) / brotli (max) bytes per shipped stylesheet, enforced by `scripts
125
126
 
126
127
  | File | gzip | brotli |
127
128
  |------|-----:|-------:|
128
- | `tokens.css` (effective: core + palette + prose) | 15,788 | 13,269 |
129
+ | `tokens.css` (effective: core + palette + prose) | 16,628 | 13,973 |
129
130
  | `tokens-core.css` | 4,790 | 3,995 |
130
131
  | `tokens-palette.css` | 7,036 | 5,848 |
131
- | `prose.css` | 4,949 | 4,139 |
132
+ | `prose.css` | 5,792 | 4,852 |
132
133
  | `essay.css` | 7,914 | 6,680 |
133
134
  | `broadsheet.css` | 7,463 | 6,241 |
134
- | `forms.css` | 7,709 | 6,378 |
135
+ | `forms.css` | 8,412 | 6,987 |
135
136
  | `chrome.css` | 3,296 | 2,772 |
136
137
  | `print.css` | 2,542 | 2,095 |
137
138
  | `theme.css` | 1,744 | 1,452 |
package/REMARQUE.md CHANGED
@@ -187,6 +187,30 @@ This is why the measure is palette-tier: it is a property of the font choice, no
187
187
 
188
188
  ---
189
189
 
190
+ ## Type Scale
191
+
192
+ Seven rungs, in two kinds (0.30, #131). The **text rungs** are fixed: 13, 14, 17 and 19px (`--text-micro`, `--text-meta`, `--text-body`, `--text-body-lg`). Their steps (1.08, 1.21, 1.12) aren't a modular ratio; each rung is a floor. 13 and 14 are the USWDS minimums for fine print and small text, 17 is Remarque's body minimum, and 19 is the lede. The **display rungs** are fluid: `--text-section`, `--text-title` and `--text-display` scale with the viewport between fixed bounds. So the scale changes *shape* with the viewport, not only size, and that's deliberate. On a phone the headings sit close to the text (steps 1.16, 1.36, 1.47) so a title doesn't eat the screen; on a desktop they open out to about a 1.5 ratio:
193
+
194
+ | Viewport | section | title | display | steps from the 19px lede up |
195
+ |---|---|---|---|---|
196
+ | 375px | 22px | 30px | 44px | 1.16 · 1.36 · 1.47 |
197
+ | 1280px | 28.8px | 44.8px | 70.4px | 1.52 · 1.56 · 1.57 |
198
+ | 1920px | 32px | 48px | 80px | 1.68 · 1.50 · 1.67 |
199
+
200
+ There's no rung between 19 and the section heading (1.52 at 1280px); headings are meant to be a clear jump from text, not a gradient. Any change to a rung's *value* is a core-tier change and goes to a vote first.
201
+
202
+ **Heading ladder in prose.** Adjacent levels differ in face or style, not only in size, so each still reads as its own level in print and under forced colors:
203
+
204
+ | Level | Face | Size / weight |
205
+ |---|---|---|
206
+ | `h2` | display (Newsreader) roman | `--text-section` / medium |
207
+ | `h3` | body (Inter) | 19px / semibold |
208
+ | `h4` | display roman (0.30; was Inter 17/600, which read as h3) | 17px / medium |
209
+ | `h5` | display italic | 17px / medium |
210
+ | `h6` | mono | 14px |
211
+
212
+ **The lede is in `--color-fg`.** `.text-body-lg` sets no color. Give a standfirst full foreground; its size and leading already set it apart, and in `--color-fg-muted` it read *quieter* than the body below it, which inverts the hierarchy. (The demo and this spec's samples used `fg-muted` until 0.30.)
213
+
190
214
  ## Editorial Microtypography
191
215
 
192
216
  The book-craft layer: numerals, small caps, drop caps, pull quotes, and optical wrapping. These are the details that separate typeset prose from a web page that merely uses nice fonts.
@@ -218,6 +242,14 @@ Labels that want a caps-like treatment use **real capitals with added tracking**
218
242
  - **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
243
  - Uppercase for *running text* (a shouted sentence, a heading set in caps) is still out: the rule is about labels.
220
244
 
245
+ ### Code Blocks, Figures and Links
246
+
247
+ - **Code-block titles.** `data-title="src/config.ts"` on a `<pre>` inside `.remarque-prose` renders a mono filename line across the top of the block, with a hairline under it and no filled tab (0.30, #129). Shiki and rehype-pretty-code can both emit the attribute.
248
+ - **Line highlighting** belongs to your highlighter's markup. Remarque supplies only the color: an element marked `data-highlighted-line` (rehype-pretty-code's attribute) gets the `--color-accent-subtle` wash that `mark` uses.
249
+ - **Figures** sit in the column: `.remarque-prose figure` cancels the browser's 40px side indent, and its vertical rhythm comes from the prose flow like any other block.
250
+ - **External-link marks (opt-in).** `.remarque-prose--external-marks` adds a small ↗ after links marked `rel="external"`, on screen only. `print.css` prints each link's URL with `::after`, and a screen marker on the same pseudo-element would replace it. The arrow has empty alt text; `rel` carries the meaning.
251
+ - Code blocks never wrap; they scroll in their own box (see "Wrapping & Optical Defaults").
252
+
221
253
  ### Drop Caps (opt-in)
222
254
 
223
255
  `.remarque-prose--dropcap`, applied alongside `.remarque-prose`, floats a large italic display-face initial letter on the first paragraph only — the way a printed essay opens its first page. Rules:
@@ -352,6 +384,27 @@ Sequential and diverging ramps are **not** shipped as tokens — a fixed `--colo
352
384
  Each entry carries `oklch` (structured `{l, c, h}`) and `oklchCss` (a ready-to-use `oklch(...)` string) — no parsing required.
353
385
  2. **`remarque-theme --dataviz`** — emits `--viz-sequential-1..N` and `--viz-diverging-1..N` custom properties (`N` = the source theme's own ramp length) alongside the normal derived palette. Gamut-clamped but **not** contrast-solved (a sequential ramp's low end is designed to sit near `--color-bg`; solving it to a contrast floor would defeat the ramp) — advisory output, not audited or golden-gated like `--color-viz-1..6`. A future `remarque-audit --json`/`remarque-theme --json` could expose the same arrays as structured output; today, `--dataviz`'s CSS-property emission and direct JSON consumption above cover the need.
354
386
 
387
+ ### Color-vision robustness: opt-in recipes (0.30, #128)
388
+
389
+ Since 0.28, `remarque-audit` warns when the categorical slots are hard to tell apart under simulated color-vision deficiency, and the default palette triggers it. The six slots span only 0.042 L (light) and 0.029 L (dark), so hue is their only channel, and the worst pair under simulation is ΔE2000 3.5 (light, deuteranopia) and 2.7 (dark, protanopia); below about 5, marks blur together. **The defaults stay as they are** (ratified 7–0, `vote-1792892374374-55pw50q`): a value change would recolor every consumer's charts, no consumer has asked, and the defaults are pinned to the upstream `oklch-terminal-themes` palettes by the golden gate. The "never color alone" rule above (labels, markers, dash patterns) is what keeps a default-palette chart readable.
390
+
391
+ If your charts lean on color, override the slots with this **lightness ladder**: same hues and chroma, lightness alternated so that neighbouring slots differ in both hue and lightness. Every slot clears 3.4:1 on `--color-bg` and `--color-surface`. It is a palette-tier override, so `remarque-drift` reports it as INFO, and it's verified by the same coherence math the audit uses:
392
+
393
+ | Slot | Light | Dark |
394
+ |---|---|---|
395
+ | `--color-viz-1` | `oklch(0.46 0.121 250.5)` | `oklch(0.72 0.129 249.5)` |
396
+ | `--color-viz-2` | `oklch(0.61 0.111 85.5)` | `oklch(0.88 0.13 85.3)` |
397
+ | `--color-viz-3` | `oklch(0.52 0.12 24.6)` | `oklch(0.56 0.129 310.2)` |
398
+ | `--color-viz-4` | `oklch(0.40 0.12 144.8)` | `oklch(0.72 0.129 25.4)` |
399
+ | `--color-viz-5` | `oklch(0.61 0.121 309.6)` | `oklch(0.56 0.129 144.9)` |
400
+ | `--color-viz-6` | `oklch(0.55 0.09 195.8)` | `oklch(0.88 0.119 194.8)` |
401
+
402
+ The worst pair under normal, protan, deutan and tritan simulation rises to **ΔE2000 11.7 (light) and 13.6 (dark)**. The lightness alternates rather than ramps, so the series don't read as an ordered scale; keep it that way if you reorder them. The trade-off is that the lightest slots read as brighter marks than the rest. Run against the audit, the recipe clears every `viz-cvd-*` warning; `accent-vs-viz-1` still warns, because viz-1 keeps the accent's hue (rotate it if links sit next to your charts).
403
+
404
+ **Dark tertiary text.** Dark `--color-muted` (L 0.60) passes WCAG 2.x at 4.92:1 on `--color-bg`, but the advisory APCA column measures it at Lc −34.5, about half its light twin's 70.0. `--color-muted: oklch(0.64 0.01 80)` in your dark block gives 5.77:1 on bg, 5.49:1 on surface, and Lc −40.3; it moves muted closer to `--color-fg-muted` (0.70), so check the audit's text-tier warnings after.
405
+
406
+ **When the defaults change.** Revisit when a consumer asks, or when the coherence warnings are promoted to failures (the default palette would then fail its own audit). The change then goes through an upstream `oklch-terminal-themes` release first and a pin bump here, so the golden gate stays exact; never as a per-token exception in the golden gate.
407
+
355
408
  ---
356
409
 
357
410
  ## Syntax Highlighting
@@ -1261,6 +1314,14 @@ disabled control never gets a `data-state` — it uses the native
1261
1314
  register, not one of the three feedback hues (REMARQUE.md "State Colors":
1262
1315
  disabled is deliberately not ANSI-derived).
1263
1316
 
1317
+ **State is never color alone (0.30, #128; WCAG 1.4.1).** Each state message carries a glyph in every mode: ✕ error, ⚠ warning, ✓ success. Under simulated deuteranopia the default error and success colors are only ΔE2000 2.7 (light) and 1.7 (dark) apart, so the glyph is what tells them apart. It has empty alt text, so the message has to say what's wrong in words ("Enter an email address", not "Invalid"), and `aria-invalid` still carries the state for screen readers. Before 0.30 the glyphs appeared only under forced colors.
1318
+
1319
+ **Read-only, pending and auto-growing fields (0.30, #129).**
1320
+ - `readonly` inputs get a quiet `--color-bg-subtle` fill: visibly not editable, still legible, focusable and copyable. The selector is `[readonly]`, not `:read-only`, which also matches checkboxes, selects and buttons.
1321
+ - **Pending submit is text, not a spinner.** Disable the button, change its label ("Send" → "Sending…"), and announce completion or failure through a polite live region (`<p role="status">`). Don't put `aria-busy` on the button (it's for regions being updated, and AT support on controls is poor), and never add a spinner (AGENT_RULES bans decorative motion).
1322
+ - `.remarque-input--autosize` on a `<textarea>` grows with its content (`field-sizing: content`, Baseline 2026-06), from the 6rem minimum up to a 24rem cap. Older engines keep the fixed, scrollable textarea.
1323
+ - **Hover only where hover exists.** Button and input hover washes sit inside `@media (hover: hover)`, so a tap doesn't leave a control looking hovered. `:active` gives buttons a pressed state (border to `--color-fg`) on every input type.
1324
+
1264
1325
  As a zero-JS bonus layer, `.remarque-input:user-invalid` (guarded with
1265
1326
  `@supports selector(:user-invalid)`) picks up the same error border once
1266
1327
  a browser supports the pseudo-class and the user has actually interacted
@@ -1719,9 +1780,42 @@ already uses) with negative fixtures proving the schemas actually reject
1719
1780
  malformed input, and an independent recomputation of every `integrity`
1720
1781
  hash proving it wasn't just carried over from a stale build.
1721
1782
 
1722
- **What this deliberately is NOT.** No CLI installer — there is no
1723
- `npx remarque-registry add essay` command, and no `files[].target`
1724
- field to resolve an install path against. An agent's expected workflow
1783
+ **Installing with the shadcn CLI (#130).** The `remarque:contract` items
1784
+ above fail shadcn's `type` enum, so a parallel, schema-valid copy is
1785
+ generated from the same definitions into `registry/shadcn/` (an index
1786
+ `registry.json` plus one `<name>.json` per item, freshness-checked by
1787
+ `build-registry.mjs --check`). Each is `type: "registry:item"` (the
1788
+ schema's catch-all for a bundle that is neither a component nor a theme),
1789
+ with `files[]` entries of `type: "registry:file"` and a `target`
1790
+ (`src/styles/remarque/<name>.css`, `src/components/remarque/<name>.usage.html`;
1791
+ the palette-deck item is markup-only). `remarque-tokens` is listed under
1792
+ `dependencies` (npm packages, which the CLI installs), and Remarque-only
1793
+ fields (`version`, `integrity`, `docs` URL, `cssVars` names) sit under
1794
+ `meta`. Verified against `shadcn@latest` (needs a `components.json`):
1795
+
1796
+ ```sh
1797
+ npx shadcn@latest add https://williamzujkowski.github.io/remarque/registry/shadcn/essay.json
1798
+ ```
1799
+
1800
+ or, with `"registries": { "@remarque": "https://williamzujkowski.github.io/remarque/registry/shadcn/{name}.json" }`
1801
+ in `components.json`:
1802
+
1803
+ ```sh
1804
+ npx shadcn@latest add @remarque/essay
1805
+ ```
1806
+
1807
+ The CLI copies files only; import the stylesheet after `remarque-tokens`
1808
+ yourself. The chrome items (`skip-link`, `breadcrumb`, `pager`, `tags`)
1809
+ share one `chrome.css`, so adding several writes the same file. The
1810
+ validating schemas are vendored from shadcn in `scripts/vendor/` and
1811
+ enforced in `scripts/test-registry.mjs`. The package export
1812
+ `remarque-tokens/registry/shadcn/essay` resolves via the existing
1813
+ `./registry/*` wildcard.
1814
+
1815
+ **What this deliberately is NOT.** No Remarque-specific installer — there
1816
+ is no `npx remarque-registry add essay` command (the shadcn CLI above is
1817
+ the optional install path for the Tailwind/Astro subset; the
1818
+ `remarque:contract` items carry no `files[].target`). An agent's expected workflow
1725
1819
  is fetch the JSON, read `usage.html`/the CSS `content`, and apply it with
1726
1820
  its own editing tools — the registry replaces transcription-from-prose,
1727
1821
  not an installer. No archetype "starter-page" items this round: the
package/forms.css CHANGED
@@ -171,8 +171,30 @@ textarea.remarque-input {
171
171
  hand-drawn replacement icon, exactly the "fake replacement" this module
172
172
  avoids) or accepting a single browser's rendering as the reference,
173
173
  neither of which is worth it for one triangle. */
174
- .remarque-input:hover {
175
- border-color: var(--color-fg-muted);
174
+ /* Hover washes only where hover exists: on touch, a tap leaves :hover
175
+ stuck on the last control until the next tap elsewhere (#129). */
176
+ @media (hover: hover) {
177
+ .remarque-input:hover {
178
+ border-color: var(--color-fg-muted);
179
+ }
180
+ }
181
+
182
+ /* Read-only: visibly not editable, still fully legible and focusable
183
+ (the value can be selected and copied). A quiet fill, no new token.
184
+ [readonly], not :read-only, which also matches checkboxes, selects
185
+ and buttons. */
186
+ .remarque-input[readonly] {
187
+ background-color: var(--color-bg-subtle);
188
+ cursor: default;
189
+ }
190
+
191
+ /* Opt-in auto-growing textarea: field-sizing (Baseline 2026-06: Chrome
192
+ 123, Firefox 152, Safari 26.2) replaces the JS resize snippet. Keeps
193
+ the textarea minimum below and caps the growth; older engines keep a
194
+ fixed, scrollable textarea. */
195
+ .remarque-input--autosize {
196
+ field-sizing: content;
197
+ max-height: 24rem;
176
198
  }
177
199
 
178
200
  /* Placeholder: --color-muted, the same 4.5:1-on-bg/-surface token used
@@ -285,8 +307,17 @@ textarea.remarque-input {
285
307
  background-color var(--motion-fast) var(--motion-easing);
286
308
  }
287
309
 
288
- .remarque-button:hover:not(:disabled) {
289
- border-color: var(--color-fg-muted);
310
+ @media (hover: hover) {
311
+ .remarque-button:hover:not(:disabled) {
312
+ border-color: var(--color-fg-muted);
313
+ color: var(--color-fg);
314
+ }
315
+ }
316
+
317
+ /* Pressed: the border goes to full fg for the duration of the press, on
318
+ every input type (touch has no hover to precede it). */
319
+ .remarque-button:active:not(:disabled) {
320
+ border-color: var(--color-fg);
290
321
  color: var(--color-fg);
291
322
  }
292
323
 
@@ -295,7 +326,15 @@ textarea.remarque-input {
295
326
  border-color: var(--color-accent);
296
327
  }
297
328
 
298
- .remarque-button--primary:hover:not(:disabled) {
329
+ @media (hover: hover) {
330
+ .remarque-button--primary:hover:not(:disabled) {
331
+ color: var(--color-accent-hover);
332
+ border-color: var(--color-accent-hover);
333
+ background-color: var(--color-accent-subtle);
334
+ }
335
+ }
336
+
337
+ .remarque-button--primary:active:not(:disabled) {
299
338
  color: var(--color-accent-hover);
300
339
  border-color: var(--color-accent-hover);
301
340
  background-color: var(--color-accent-subtle);
@@ -410,6 +449,30 @@ textarea.remarque-input {
410
449
  }
411
450
  }
412
451
 
452
+ /* ─── Non-color state cue, every mode (issue #128; WCAG 1.4.1) ──────────
453
+ * A glyph before each state message, so error, warning and success are
454
+ * told apart by shape, not only by color. Until 0.30 the glyphs existed
455
+ * only under forced colors (below); under deuteranopia simulation the
456
+ * default error and success colors are only ΔE2000 2.7 (light) and 1.7
457
+ * (dark) apart, so ordinary rendering needed them too. Empty alt text:
458
+ * the message text and aria-invalid carry the meaning for screen
459
+ * readers, and the glyph would only add noise. The message must still
460
+ * say what's wrong in words (REMARQUE.md "Forms"). */
461
+ .remarque-field[data-state="error"] .remarque-field-message::before {
462
+ content: "\2715\0020"; /* ✕ */
463
+ content: "\2715\0020" / "";
464
+ }
465
+
466
+ .remarque-field[data-state="warning"] .remarque-field-message::before {
467
+ content: "\26A0\0020"; /* ⚠ */
468
+ content: "\26A0\0020" / "";
469
+ }
470
+
471
+ .remarque-field[data-state="success"] .remarque-field-message::before {
472
+ content: "\2713\0020"; /* ✓ */
473
+ content: "\2713\0020" / "";
474
+ }
475
+
413
476
  /* ─── Forced-colors reinforcement (issue #93) ────────────────────────────
414
477
  * The validation-state wiring above is the textbook color-only-affordance
415
478
  * case the issue names explicitly: error/success/warning are distinguished
@@ -423,9 +486,8 @@ textarea.remarque-input {
423
486
  * Fix is geometry, not color, since color is exactly what forced-colors
424
487
  * discards: each state gets a distinct `border-style`/`border-width`
425
488
  * combination on the control itself (a shape that reads under ANY forced
426
- * system border color), plus a small glyph prefixed to the message line
427
- * — scoped to this media query only, so the quiet default (message text
428
- * alone) is unchanged everywhere else. The glyph is supplementary, not
489
+ * system border color), on top of the glyph prefixed to the message line
490
+ * in every mode (above; forced-colors-only until 0.30). The glyph is supplementary, not
429
491
  * the primary signal (a screen reader user already gets `aria-invalid` +
430
492
  * the message text; a sighted forced-colors user gets the border shape);
431
493
  * it is not marked `aria-hidden` because CSS generated content cannot
@@ -450,17 +512,6 @@ textarea.remarque-input {
450
512
  border-width: 2px;
451
513
  }
452
514
 
453
- .remarque-field[data-state="error"] .remarque-field-message::before {
454
- content: "\2715\0020"; /* ✕ */
455
- }
456
-
457
- .remarque-field[data-state="warning"] .remarque-field-message::before {
458
- content: "\26A0\0020"; /* ⚠ */
459
- }
460
-
461
- .remarque-field[data-state="success"] .remarque-field-message::before {
462
- content: "\2713\0020"; /* ✓ */
463
- }
464
515
  }
465
516
 
466
517
  /* ─── Standalone data tables (issue #30) ──────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
package/prose.css CHANGED
@@ -148,6 +148,28 @@
148
148
  overflow-x: auto;
149
149
  }
150
150
 
151
+ .remarque-prose pre[data-title]::before {
152
+ /* Code-block title (#129): a mono filename line at the top of the
153
+ block, in the no-chrome voice: a hairline under it, no filled tab.
154
+ Set data-title="src/config.ts" on the <pre> (Shiki and
155
+ rehype-pretty-code both can). The negative margins reach the block's
156
+ edges through its padding. */
157
+ content: attr(data-title);
158
+ display: block;
159
+ margin: calc(-1 * var(--space-5)) calc(-1 * var(--space-5)) var(--space-4);
160
+ padding: var(--space-2) var(--space-5);
161
+ border-block-end: var(--border-width) var(--border-style) var(--color-border);
162
+ letter-spacing: var(--tracking-meta);
163
+ color: var(--color-muted);
164
+ }
165
+
166
+ /* Line highlighting is the highlighter's markup; this is only the color
167
+ hook: mark a line with data-highlighted-line (rehype-pretty-code's
168
+ attribute) and it gets the quiet accent wash prose `mark` uses. */
169
+ .remarque-prose pre [data-highlighted-line] {
170
+ background-color: var(--color-accent-subtle);
171
+ }
172
+
151
173
  .remarque-prose pre code {
152
174
  background: none;
153
175
  padding: 0;
@@ -168,6 +190,12 @@
168
190
  border-radius: var(--radius-sm);
169
191
  }
170
192
 
193
+ .remarque-prose figure {
194
+ /* The UA indents figures by 40px on both sides; a figure belongs to the
195
+ column. Its block rhythm comes from the `> * + *` rule above. */
196
+ margin-inline: 0;
197
+ }
198
+
171
199
  .remarque-prose figcaption {
172
200
  font-family: var(--font-mono);
173
201
  font-size: var(--text-meta);
@@ -179,19 +207,24 @@
179
207
 
180
208
  /* ─── Headings, continued (h4–h6) ───────────────────────── */
181
209
  /*
182
- * h2/h3 are defined above. h4–h6 complete the vocabulary with a
183
- * deliberate descent through the three font slots — h4 stays in the
184
- * body voice (still prose), h5 borrows the display face's italic for a
185
- * quieter sub-heading, h6 drops into the mono "technical label" voice
186
- * used everywhere else for the deepest level of metadata. No
187
- * text-transform: uppercase — see .text-label (tokens-core.css) for
188
- * the true small-caps alternative.
210
+ * h2/h3 are defined above. h4–h6 complete the vocabulary by alternating
211
+ * the font slots, so each level differs from its neighbour in face or
212
+ * style, not only in size: h3 is the body face at 19px semibold, h4 the
213
+ * display face's roman at 17px medium, h5 the display face's italic,
214
+ * h6 the mono "technical label" voice used everywhere else for the
215
+ * deepest level of metadata.
216
+ *
217
+ * h4 moved from the body face in 0.30 (#131, ratified in
218
+ * vote-1792892296073-vxi53q7): as Inter 17/600 under Inter 19/600 it
219
+ * differed from h3 by 2px and read as the same level. A face change
220
+ * survives print, prefers-contrast and forced colors, where a color
221
+ * difference would collapse. Sizes are unchanged.
189
222
  */
190
223
 
191
224
  .remarque-prose h4 {
192
- font-family: var(--font-body);
225
+ font-family: var(--font-display);
193
226
  font-size: var(--text-body);
194
- font-weight: var(--weight-semibold);
227
+ font-weight: var(--weight-medium);
195
228
  line-height: var(--leading-title);
196
229
  margin-top: var(--space-5);
197
230
  margin-bottom: var(--space-2);
@@ -493,6 +526,22 @@
493
526
  * it intentionally falls through to the serif fallback stack.
494
527
  */
495
528
 
529
+ /* ─── External-link marks (opt-in, #129) ──────────────────────────────
530
+ * .remarque-prose--external-marks adds a small ↗ after links marked
531
+ * rel="external". Screen only: print.css prints every link's URL after
532
+ * it with ::after, and a marker on the same pseudo-element would replace
533
+ * that (the bug the flagship hit hand-rolling this). The arrow is
534
+ * decoration with empty alt text; rel carries the meaning. */
535
+ @media screen {
536
+ .remarque-prose--external-marks a[rel~="external"]::after {
537
+ content: "\00a0\2197";
538
+ content: "\00a0\2197" / "";
539
+ display: inline-block;
540
+ color: var(--color-muted);
541
+ text-decoration: none;
542
+ }
543
+ }
544
+
496
545
  /* ─── Script-aware italics (issue #123) ───────────────────────────────
497
546
  * Arabic, Persian, Urdu, Hebrew and CJK have no italic tradition, and
498
547
  * browsers fake one by slanting the glyphs. The decorative italics
@@ -508,6 +557,13 @@
508
557
  font-style: normal;
509
558
  }
510
559
 
560
+ /* Upright h5 would now match h4 (both the display face's roman, and in
561
+ these scripts both fall back to the same face), so h5 steps down in
562
+ color instead (#131). */
563
+ :is(:lang(ar), :lang(fa), :lang(ur), :lang(he), :lang(zh), :lang(ja), :lang(ko)):is(.remarque-prose h5) {
564
+ color: var(--color-fg-muted);
565
+ }
566
+
511
567
  :is(:lang(ar), :lang(fa), :lang(ur), :lang(he), :lang(zh), :lang(ja), :lang(ko)):is(.remarque-prose em) {
512
568
  font-style: normal;
513
569
  font-weight: var(--weight-semibold);
@@ -4,7 +4,7 @@
4
4
  "type": "remarque:contract",
5
5
  "title": "Breadcrumb (chrome.css)",
6
6
  "description": "The Reference/Docs breadcrumb kicker: `nav[aria-label=\"Breadcrumb\"] > ol`, the current page last as `<span aria-current=\"page\">` (never a link), separators generated by CSS with empty alt text and never typed into the DOM.",
7
- "version": "0.29.0",
7
+ "version": "0.30.0",
8
8
  "integrity": "sha256-V+FqUVopXBpnu9jfd8frbZJcs3NWmjv8KBuU/Uknaco=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"
@@ -4,7 +4,7 @@
4
4
  "type": "remarque:contract",
5
5
  "title": "Broadsheet Pattern",
6
6
  "description": "The editorial Landing/archive pattern: masthead, lead article, numbered entry list, and post-header kicker. Entry numerals are generated from `data-entry-number` via `attr()`, never `counter()`, and every kicker/dateline row is the label voice: tracked capitals, because the default mono face has no small caps (`font-variant-caps: all-small-caps` only on a face with `smcp` + `c2sc`; remarque-audit checks).",
7
- "version": "0.29.0",
7
+ "version": "0.30.0",
8
8
  "integrity": "sha256-0gdOLXhIStXi2nrbSvWuyaOvCfjAdTdJAeZkprafekU=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"
@@ -4,7 +4,7 @@
4
4
  "type": "remarque:contract",
5
5
  "title": "Essay Module",
6
6
  "description": "Sidenotes + sticky TOC rail for long-form Essay pages: margin notes relocated into real DOM order, right after the paragraph that cites them, numbered by CSS counter and labeled `aria-label=\"Note N\"` in the same order the counter advances — the mechanism that prevents the sidenote aria-label/DOM-order transcription bug found in the flagship migration (#89, williamzujkowski.github.io#380).",
7
- "version": "0.29.0",
7
+ "version": "0.30.0",
8
8
  "integrity": "sha256-HwTP/hiT/oid/YHBlzSECSvG5wSg9ICYVfns7luc6SA=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"