remarque-tokens 0.25.0 → 0.27.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
@@ -58,7 +58,7 @@ These are not guidelines. Agents must follow them literally.
58
58
  ### Typography
59
59
 
60
60
  - Body text is never smaller than `--text-body` (17px / 1.0625rem)
61
- - Prose columns never exceed `--content-reading` (46rem)
61
+ - Prose columns never exceed `--content-reading` (38rem default — ~71 measured characters of 17px Inter)
62
62
  - Display/title text always uses `--font-display` (Newsreader)
63
63
  - Body text always uses `--font-body` (Inter)
64
64
  - Mono text is *only* for: metadata, code, labels, timestamps, technical accents
@@ -70,7 +70,7 @@ These are not guidelines. Agents must follow them literally.
70
70
  - The first screen contains only: title, metadata, and the start of content
71
71
  - Top padding before the main heading is at least `--space-9` (6rem)
72
72
  - The site uses a **single centered column** layout: BaseLayout constrains all content to `content-standard` (72rem) centered. Individual pages do NOT set their own outer width constraints.
73
- - Within the centered column, use `.content-reading` for prose-width sections (46rem, auto-centered within the 72rem column)
73
+ - Within the centered column, use `.content-reading` for prose-width sections (38rem default, auto-centered within the 72rem column)
74
74
  - Do NOT use Tailwind's `max-w-reading` or `max-w-standard` on page sections — these only set max-width without centering. Use the CSS classes `.content-reading` / `.content-standard` instead, which include `margin-inline: auto`
75
75
  - Code blocks (`<pre>`) inherit the width of their parent container — no special wrapping needed when the parent is already constrained
76
76
  - Stacking order is structural, not personalization: never author a bare `z-index` number — reference `tokens-core.css`'s `--z-*` scale (`--z-base`/`--z-sticky`/`--z-dropdown`/`--z-overlay`/`--z-modal`/`--z-toast`/`--z-skip-link`). Tailwind v4 has no `--z-index-*` theme namespace, so use arbitrary values (`z-[var(--z-modal)]`) rather than bare Tailwind numbers. See REMARQUE.md "Stacking."
@@ -186,7 +186,7 @@ Three further archetypes — Reference/Docs, Changelog, Gallery — are specifie
186
186
 
187
187
  1. **Tailwind spacing collision:** Never map Remarque's `--space-N` values onto Tailwind's default numeric spacing keys — in v4 `@theme` that means no `--spacing-9`…`--spacing-12`, and in the v3 config no `spacing: { "5"…"12" }` overrides — or `mt-12` produces 192px instead of 48px. Both shipped artifacts namespace instead: the v3 config exposes `mt-remarque-9` etc., the reference v4 `@theme` uses `--spacing-remarque-N`. Use those, or `var(--space-N)` in arbitrary values.
188
188
 
189
- 2. **Prose alignment:** The essay header and prose body must BOTH use centered max-width (`max-w-reading mx-auto`) to prevent misalignment. The `.content-reading` class auto-centers; match headers with `mx-auto`.
189
+ 2. **Prose alignment:** The essay header and prose body must BOTH sit in the same centered reading column to prevent misalignment — put `.content-reading` on both (it sets `max-width` and `margin-inline: auto`). Not Tailwind's `max-w-reading mx-auto`: it renders the same on screen, but `print.css`'s full-width reset is keyed on `.content-reading`, so a `max-w-reading` header prints indented from its body (the demo shipped exactly that until the 2026-10 design review).
190
190
 
191
191
  3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode. Since 0.24.0 (`light-dark()` + `color-scheme`, REMARQUE.md "Color Scheme & light-dark()"), the shipped default palette's own `[data-theme="light"]` override is just `color-scheme: light` — a consumer palette that still authors the older two-declaration convention needs the fuller override this rule originally describes.
192
192
 
@@ -198,6 +198,8 @@ Three further archetypes — Reference/Docs, Changelog, Gallery — are specifie
198
198
 
199
199
  7. **Tokens must be imported unlayered (Tailwind v4):** never `@import "remarque-tokens" layer(...)`. theme.css's `@theme inline` mappings emit same-named self-referencing declarations inside `@layer theme`; the real token values win only because unlayered declarations beat layered ones. Layering the tokens makes every mapped utility circular and invalid, with no build error.
200
200
 
201
+ 8. **Arbitrary type-scale sizes need the `length:` hint (Tailwind v4):** write `text-[length:var(--text-meta)]`, never `text-[var(--text-meta)]`. Tailwind cannot infer a bare `var()`'s type, treats `text-[…]` as a color, and emits `color: var(--text-meta)` — the size never applies and the invalid color clobbers the element's real color utility, with no build error. `theme.css` deliberately maps no `--text-*` keys (they would collide with the unlayered `.text-meta`/`.text-body` classes). `remarque-audit` fails on the ambiguous form (`srcScans.ambiguousTextSize`). The demo site shipped 134 of these until the 2026-10 design review.
202
+
201
203
  ---
202
204
 
203
205
  ## Quality Checklist
@@ -207,7 +209,7 @@ Before considering any implementation complete, verify:
207
209
  - [ ] Fonts load: Newsreader for display, Inter for body, JetBrains Mono for meta
208
210
  - [ ] Fonts are self-hosted (no Google CDN requests in network tab)
209
211
  - [ ] Body text is ≥17px on all viewports
210
- - [ ] Prose column is ≤46rem on desktop
212
+ - [ ] Prose column is ≤ `--content-reading` on desktop, and body lines measure a median of 66–72 rendered characters (REMARQUE.md "Measure Compensation" — count characters, not `ch`)
211
213
  - [ ] Prose header and body are aligned (both centered with `mx-auto` or both left-aligned)
212
214
  - [ ] Line height for body text is 1.75
213
215
  - [ ] Dark mode is readable and independently tuned (not just inverted)
@@ -235,6 +237,7 @@ Before considering any implementation complete, verify:
235
237
  - [ ] `<html lang="en">` attribute set
236
238
  - [ ] The site looks more like a book than a web app
237
239
  - [ ] Tailwind spacing utilities (`mt-12`, `pt-8`) produce expected values (not overridden by `@theme`)
240
+ - [ ] Arbitrary type-scale sizes use `text-[length:var(--text-*)]` — verify a computed `font-size` on one meta element, not just the class name (Pitfall #8)
238
241
 
239
242
  ---
240
243
 
@@ -280,6 +283,7 @@ npx remarque-audit --palette <file> --src <dir> --json
280
283
  unverifiableFontSize: Array<{ file: string; line: number; snippet: string }>;
281
284
  hardcodedColors: Array<{ file: string; line: number; snippet: string }>;
282
285
  oklchLiteral: Array<{ file: string; line: number; snippet: string }>;
286
+ ambiguousTextSize: Array<{ file: string; line: number; snippet: string }>; // Tailwind text-[var(--text-*)] (Pitfall #8)
283
287
  };
284
288
  failures: string[]; // every failure as a human-readable message, same text the non-JSON run prints
285
289
  }
@@ -325,9 +329,10 @@ the documented DTCG divergences.
325
329
 
326
330
  ## Installing the Skills
327
331
 
328
- This package ships two Claude Code skills under the npm subpath exports
329
- `remarque-tokens/skills/remarque` and `remarque-tokens/skills/adopt`
330
- (resolving to `skills/remarque/SKILL.md` and `skills/remarque-adopt/
332
+ This package ships three Claude Code skills under the npm subpath
333
+ exports `remarque-tokens/skills/remarque`, `remarque-tokens/skills/adopt`,
334
+ and `remarque-tokens/skills/new-page` (resolving to `skills/remarque/
335
+ SKILL.md`, `skills/remarque-adopt/SKILL.md`, and `skills/remarque-new-page/
331
336
  SKILL.md` in the installed package). Claude Code only discovers skills
332
337
  from a project's own `.claude/skills/` (or `~/.claude/skills/`), never
333
338
  from `node_modules` — copy them in explicitly, one `cp` per skill:
@@ -336,14 +341,16 @@ from `node_modules` — copy them in explicitly, one `cp` per skill:
336
341
  mkdir -p .claude/skills
337
342
  cp -r node_modules/remarque-tokens/skills/remarque .claude/skills/remarque
338
343
  cp -r node_modules/remarque-tokens/skills/remarque-adopt .claude/skills/remarque-adopt
344
+ cp -r node_modules/remarque-tokens/skills/remarque-new-page .claude/skills/remarque-new-page
339
345
  ```
340
346
 
341
347
  The copies are versioned with the installed package, not auto-updating —
342
- re-copy both after a MAJOR bump, or any time this file's
348
+ re-copy all three after a MAJOR bump, or any time this file's
343
349
  "Machine-Readable Output" shape changes.
344
350
 
345
351
  - `remarque` — the build/review contract for pages on the current version (tier rules, archetypes, pitfalls #6/#7).
346
352
  - `remarque-adopt` — the version-bump/migration playbook (this file's "Machine-Readable Output" section is its main dependency): verify the resolved version past the 0.x caret freeze, discover newly-required tokens from `remarque-audit --json`, solve missing values against the consumer's own backgrounds, classify `remarque-drift --json` output, report against a fixed PR-body contract.
353
+ - `remarque-new-page` — the page-building procedure, companion to (not a restatement of) the `remarque` loader skill: pick the archetype, fetch-and-verify the relevant registry item instead of transcribing its markup, wire the page per the pitfalls above, gate acceptance on `remarque-audit --json` plus the registry item's own markup-contract assertions.
347
354
 
348
355
  ---
349
356
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ 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.27.0 — 2026-10-23
8
+
9
+ 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.
10
+
11
+ ### Changed — default reading measure (consumer-visible; Governance "token value" minor case)
12
+
13
+ - **`--content-reading` default 46rem → 38rem.** REMARQUE.md calls the *measured character count* the invariant: a median of 66–72 per line. 46rem was documented as "≈ 70 ch at 17px Inter", but measured over rendered body lines (one `Range` per character, the paragraph's last line dropped) it set a **median of 86** (p90 93, past Butterick's 90). The claim came from `ch` arithmetic, and `ch` is the width of "0". 38rem measures a median of 71 (p90 76). Source Serif's reference consumer independently landed on 38rem ≈ 73 (#116). Ratified 7–0 (vote-1792782673313-gff1bdc): fix the value and keep the invariant. **Every consumer that doesn't override `--content-reading` gets a narrower prose column.** To keep the old measure, set `--content-reading: 46rem` in your palette override. It's palette tier, so one line does it.
14
+ - REMARQUE.md "Measure Compensation" is now stated in measured median/p90 characters, and documents how to measure them. AGENT_RULES, the README, the Butterick conformance row and the tailwind comment are updated. A new computed-style gate asserts that the demo essays measure a median of 66–72.
15
+ - **Essay sidenote gutter `--space-10` → `--space-11` (8 → 10rem).** At 8rem a mono note held about 15 characters per line, so a two-sentence note ran 19 lines tall. 10rem still clears the 80rem breakpoint with a 46rem column.
16
+
17
+ ### Added
18
+
19
+ - **`remarque-audit` fails on `text-[var(--text-*)]`** (new `srcScans.ambiguousTextSize`; AGENT_RULES Pitfall #8). Tailwind v4 can't infer a bare `var()`'s type. It compiles `text-[var(--text-meta)]` to **`color: var(--text-meta)`**, so no size is ever set and the invalid color clobbers the element's real color utility, with no build error. The demo shipped 134 of these: every date, lede, footer line and code block rendered at 17px full contrast instead of the 14px muted meta register. Use `text-[length:var(--text-meta)]`. `theme.css` documents why it deliberately maps no `--text-*` keys: they would collide with the unlayered `.text-meta`/`.text-body` classes.
20
+ - **Prose `a:visited`** shifts only `text-decoration-color` to `--color-fg-muted`. The link text keeps the accent, per the one-accent rule. There's no new token, so the upstream themes, `remarque-theme`, the audit and the golden gate are untouched. Ratified 7–0 (vote-1792782672108-js2he54).
21
+
22
+ ### Fixed
23
+
24
+ - **essay.css: TOC rail focus rings were clipped (#115).** The sticky rail's `overflow-y` made it a scroll container, which clipped the left edge of every link's 2px+2px ring and three sides of the summary's. The rail now pads past the ring's reach and pulls back by the same amount. TOC links and the summary get a 44px min-height (they were 18px tall below 80rem), and the disclosure marker sits beside its label.
25
+ - **broadsheet.css:** the lead's drop cap renders only from 40rem up. On a phone column the 3.5em italic capital left the deck a sliver about 20 characters wide.
26
+ - **prose.css:** `th.num`/`td.num` never wrap a value ("44–80" was breaking after its en dash).
27
+ - **AGENT_RULES Pitfall #2** recommended `max-w-reading mx-auto`, which its own Layout section forbids. That pattern also escapes `print.css`'s full-width reset, so printed headers were indented. It now recommends `.content-reading`.
28
+
29
+ ### Demo site (not part of the package)
30
+
31
+ No horizontal scroll at 375px on any page; nav "you are here" (`aria-current`) and Components reachable on mobile; the theme toggle exposes `aria-pressed`; an on-brand 404; onward links from essays, the dossier and Start; typographic quotes throughout; 44px hit areas; no autofocus scroll-jump on Components; the Specimen states the real meta/micro sizes; the tokens page prints its oklch values with tabular numerals; titles share their content's axis. New computed-style regression gates cover each of these, and the visual baselines were regenerated.
32
+
33
+ ## 0.26.0 — 2026-07-23
34
+
35
+ `remarque-new-page` skill (closes #108) + `token-drift.yml` optional audit input (closes #110) — the last two items of the skill-suite/CI program.
36
+
37
+ ### Added
38
+
39
+ - **`.claude/skills/remarque-new-page/SKILL.md`** — a third Claude Code skill, a companion to (not a restatement of) the existing `remarque` loader skill: five steps, each ending in a machine gate. (1) load the loader skill's three files rather than build from training-data token values; (2) pick one of REMARQUE.md's seven archetypes, never an invented eighth; (3) for module-backed pages (Essay/Broadsheet/Forms/Palette Deck) fetch the registry item — `node_modules/remarque-tokens/registry/<name>.json` or the served URL — and **verify its sha256 against the `integrity` pin before applying anything** (the same transcription bug issue #89 documents, a hand-copied sidenote `aria-label` that drifted out of DOM order); (4) wire the page per AGENT_RULES pitfalls #6 (string-form `@import` only)/#7 (unlayered tokens import) and the `.remarque-prose`/`.content-reading` pairing (`.remarque-prose` supplies typography only, never centers the column); (5) gate acceptance on `remarque-audit --json` reporting `passed: true` **and** the registry item's own markup-contract assertions (sidenote DOM-order, `data-entry-number`/`attr()`, label/`aria-describedby` pairing, `data-palette`/`data-theme` co-location — per module).
40
+ - **All three skills now ship in the npm tarball.** `scripts/lib/skills.mjs`'s shared `SKILLS` table gains the new skill (canonical `.claude/skills/remarque-new-page/SKILL.md`, packaged `skills/remarque-new-page/SKILL.md`) — `scripts/build-skills.mjs`/`scripts/test-skills.mjs` cover it automatically through that shared table, no separate wiring needed for the freshness/frontmatter/flag/path checks. `scripts/test-pack.mjs`'s hardcoded tarball-listing and installed-package assertions are extended to check for the third skill explicitly (that script doesn't read the shared table). New export: `remarque-tokens/skills/new-page` (→ `skills/remarque-new-page/SKILL.md`). `package.json`'s existing `skills/` files entry already covers the new directory; no `files` change needed.
41
+ - **README + AGENT_RULES.md "Installing the Skills"** — both updated for three skills (three `cp` commands instead of two) and both skill-listing bullets extended with a `remarque-new-page` entry.
42
+ - **`.github/workflows/token-drift.yml`: three new OPTIONAL `workflow_call` inputs** — `run-audit` (boolean, default `false`), `palette-path` (string, default `''`), `src-dir` (string, default `'.'`). When `run-audit: true`, a new step runs `npx remarque-audit --palette <palette-path> --src <src-dir>` from the consumer's own checkout, against the remarque-tokens the consumer already installed under `package-dir` — one workflow call now covers both the drift and audit gates instead of a caller wiring a second script step by hand. `palette-path`/`src-dir` are relative to `package-dir` (the audit step's working directory), matching how `npx remarque-audit` is normally invoked locally. The step fails fast with a clear `::error::` if `run-audit: true` but `palette-path` is left blank (workflow_call has no conditional-required-input mechanism). **Backward compatible by construction:** `run-audit` defaults to `false`, and both existing callers — williamzujkowski.github.io's `audits.yml` and tsundoku's `deploy.yml`, both read on their own `main` branches for this release — call this workflow today with only `css-file`/`package-dir`/`remarque-ref`, so neither is affected without opting in.
43
+ - **`run-golden` deliberately NOT added**, despite the issue text raising it alongside `run-audit`. `scripts/palette-golden.mjs` is remarque-internal tooling: it binds THIS package's own `tokens-palette.css` to the `remarque-light`/`remarque-dark` reference palettes from `@williamzujkowski/oklch-terminal-themes` and checks remarque's own release hasn't drifted from its golden values. It ships no CLI bin (`package.json`'s `bin` only exports `remarque-audit`/`remarque-drift`/`remarque-theme`) and takes no `--palette`/`--src` arguments a generic consumer could point at their own stylesheet — there is no consumer-facing analog to generify, unlike `run-audit`/`remarque-audit`. Documented inline in the workflow's own header comment.
44
+ - Version-only regeneration of `tokens.json`/`tokens.d.ts`/`registry.json`/`registry/*.json` (no token, CSS, or registry-content changes this release — `tokens.schema.json` unchanged).
45
+
7
46
  ## 0.25.0 — 2026-07-23
8
47
 
9
48
  `remarque-adopt` skill + skills npm export (closes #107) — ratified 3-0 (skill-suite vote, 2026-07-23), the anchor item of the skill suite. Codifies the consumer-conformance playbook run five times in one day as hand-written agent briefs (williamzujkowski.github.io#381, tsundoku#243/#237, remarque-starter#7/#8) into a procedural, machine-gated skill.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Most developer sites inherit the visual language of SaaS dashboards or component
14
14
 
15
15
  - **Typography is the interface** — not color, not illustration, not animation. Three fonts (Newsreader, Inter, JetBrains Mono), each with a strict role.
16
16
  - **17px minimum body text** — one pixel above the industry default. The difference in reading comfort is immediate.
17
- - **46rem reading column** — derived from the typographic standard of 45-75 characters per line.
17
+ - **38rem reading column** — ~71 measured characters per line at 17px Inter, inside the typographic standard of 45–75.
18
18
  - **OKLCH color space** — perceptually uniform. A lightness of 0.50 actually looks mid-brightness.
19
19
  - **Self-hosted fonts** — no Google CDN dependency. Strict CSP. GDPR-compliant.
20
20
  - **AI-native** — designed to be consumed by Claude Code, Cursor, Copilot, and other AI coding tools with zero aesthetic drift.
@@ -69,11 +69,12 @@ The agent rules define build order, non-negotiable rules, disallowed patterns, a
69
69
 
70
70
  Packaging for agent tooling:
71
71
  - **npm exports:** `remarque-tokens/agent-rules` (→ `AGENT_RULES.md`) and `remarque-tokens/spec` (→ `REMARQUE.md`), alongside the existing `remarque-tokens/tokens.json`, so a project can point an agent at `node_modules/remarque-tokens/AGENT_RULES.md` without hardcoding a filename.
72
- - **Claude Code skills (two):**
72
+ - **Claude Code skills (three):**
73
73
  - [`.claude/skills/remarque/SKILL.md`](.claude/skills/remarque/SKILL.md) — triggers on "remarque" / "design system" / new-page work, loads all three files, and states the tier rules, the audit command, and the two build-time pitfalls (unlayered-token-import, string-form `@import`) that pass a green build while silently breaking.
74
74
  - [`.claude/skills/remarque-adopt/SKILL.md`](.claude/skills/remarque-adopt/SKILL.md) — triggers on "bump remarque-tokens" / "upgrade the design system" / "adopt remarque in an existing site" / an audit that starts failing after a version bump. The consumer-conformance playbook (issue #107): verify the resolved version past the 0.x caret freeze, discover newly-required tokens from `remarque-audit --json` mechanically, solve missing values against the *consumer's own* backgrounds (keep-if-passing), classify `remarque-drift --json` output (FAIL/WARN/INFO), and report against a fixed PR-body contract. Every step ends in a machine gate — `passed: true`, not agent judgment.
75
+ - [`.claude/skills/remarque-new-page/SKILL.md`](.claude/skills/remarque-new-page/SKILL.md) — triggers on "new page" / "add a page" / "build an essay/archive/landing page" / "use an archetype". A companion to the `remarque` loader skill, not a parallel restatement of it: pick the archetype from REMARQUE.md, fetch the relevant registry item and verify its sha256 against the `registry.json` pin before applying it (issue #89's transcription-bug prevention), wire the page per AGENT_RULES pitfalls, and gate acceptance on `remarque-audit --json` plus the registry item's own markup-contract assertions (issue #108).
75
76
 
76
- Both ship in the npm tarball under `skills/` (see "Installing the skills" below) as well as living at `.claude/skills/` in this repo.
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.
77
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.
78
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
80
 
@@ -88,11 +89,12 @@ in explicitly, one `cp` per skill:
88
89
  mkdir -p .claude/skills
89
90
  cp -r node_modules/remarque-tokens/skills/remarque .claude/skills/remarque
90
91
  cp -r node_modules/remarque-tokens/skills/remarque-adopt .claude/skills/remarque-adopt
92
+ cp -r node_modules/remarque-tokens/skills/remarque-new-page .claude/skills/remarque-new-page
91
93
  ```
92
94
 
93
95
  The copies are versioned with whatever `remarque-tokens` version is
94
- installed at copy time, not auto-updating — re-run both `cp` commands
95
- after any MAJOR bump (and any time `AGENT_RULES.md`'s "Machine-Readable
96
+ installed at copy time, not auto-updating — re-run all three `cp`
97
+ commands after any MAJOR bump (and any time `AGENT_RULES.md`'s "Machine-Readable
96
98
  Output" shape changes) to pick up the current playbook.
97
99
 
98
100
  ## Files
@@ -144,7 +146,7 @@ See the [live demo](https://williamzujkowski.github.io/remarque/) for the origin
144
146
  |----------|-------|-----|
145
147
  | Body size | 17px | Measurably improves reading comfort over 16px default |
146
148
  | Line height | 1.75 | Generous — this is what makes prose readable |
147
- | Reading width | 46rem | ~70 chars/line at 17px Inter. Upper end of comfortable range for technical content |
149
+ | Reading width | 38rem | ~71 measured chars/line at 17px Inter (median; p90 76) |
148
150
  | Color space | OKLCH | Perceptual uniformity. Consistent across hues without manual tuning |
149
151
  | Top padding | 6rem min | The breathing room that says "this is editorial, not a dashboard" |
150
152
  | Border radius | 0.5rem max | Restrained. No "friendly SaaS" over-rounding |
package/REMARQUE.md CHANGED
@@ -161,13 +161,15 @@ Remarque uses a three-slot font system. Each slot has a strict role, and each sl
161
161
 
162
162
  ### Measure Compensation
163
163
 
164
- `--content-reading` is calibrated to the body font — 46rem gives ~70 characters per line at 17px **Inter**, but the same width in a text serif runs ~85 characters, well past comfortable. When you swap the body slot, change the measure with it (target 66–72 ch/line at `--text-body`):
164
+ `--content-reading` is calibrated to the body font — 38rem gives a median of ~71 characters per line at 17px **Inter**. When you swap the body slot, change the measure with it (target: a **median of 66–72 measured characters per line** at `--text-body`, p90 under ~78):
165
165
 
166
- | Body face | `--content-reading` | Rationale |
166
+ | Body face | `--content-reading` | Measured at 17px (median / p90 chars per line) |
167
167
  |-----------|--------------------:|-----------|
168
- | Inter (default) | 46rem | ~70 ch/line at 17px |
169
- | system-ui stack | 46rem | metrics close to Inter |
170
- | Source Serif 4 | 40rem | ~71 ch/line — measured on williamzujkowski.github.io, the reference consumer |
168
+ | Inter (default) | 38rem | 71 / 76 — this repo's demo essays, 2026-10 |
169
+ | system-ui stack | 38rem | metrics close to Inter — measure your target platform's face |
170
+ | Source Serif 4 | 38rem | 73 / 78 — measured on williamzujkowski.github.io, the reference consumer (#116) |
171
+
172
+ **Measure in rendered characters, never in `ch`.** `1ch` is the advance width of the digit "0", which is wider than the average letter-plus-space of proportional text, so `ch` arithmetic overstates how few characters fit. Until 0.27.0 this table said 46rem ≈ 70 ch for Inter; rendered, 46rem set a median of **86** characters per line (p90 93) — past Butterick's 90 at the tail. To measure: for each body paragraph, walk its text nodes with a `Range` per character, group `getClientRects()[0].top` into lines, drop each paragraph's last (short) line, and take the median and p90 across the page (spaces count; skip `code`/`sup`). The demo's computed-style suite runs exactly this check.
171
173
 
172
174
  This is why the measure is palette-tier: it is a property of the font choice, not of the system's identity. The *target character count* is the invariant.
173
175
 
@@ -233,7 +235,7 @@ Never mix the two: a numeral inside running prose should never be tabular, and a
233
235
 
234
236
  - Use spacious layouts with generous whitespace
235
237
  - Use a slightly oversized type scale (body text at 17px minimum)
236
- - Constrain reading widths for prose (max 46rem / ~740px)
238
+ - Constrain reading widths for prose (max 38rem / ~608px — about 71 characters of Inter)
237
239
  - Use quiet, subtle borders over shadows
238
240
  - Use strong vertical rhythm and consistent spacing
239
241
  - Keep components visually quiet and subordinate to content
@@ -578,7 +580,7 @@ deviate — values below are asserted against `tokens.json` in CI:
578
580
  | 1. Body text first | Typographic quality is determined largely by body text | AGENT_RULES.md Build Order steps 1–4 are tokens → typography → shell → prose, before any component | Aligned by construction |
579
581
  | 2. Point size | 15–25 px on the web | `--text-body: 1.0625rem` (17px); lead paragraphs `--text-body-lg: 1.1875rem` (19px) | Aligned |
580
582
  | 3. Line spacing | 120–145% of point size | `--leading-body: 1.75` (175%) | **Deliberate deviation** — see below |
581
- | 4. Line length | 45–90 characters per line | `--content-reading: 46rem` ≈ 70 ch at 17px Inter; the measure-compensation table targets 66–72 ch for every approved body face | Aligned |
583
+ | 4. Line length | 45–90 characters per line | `--content-reading: 38rem` ≈ 71 measured characters (median, p90 76) at 17px Inter; the measure-compensation table targets a 66–72 median for every approved body face | Aligned |
582
584
  | 5. Font choice | Professional fonts, never default system faces | Newsreader (Production Type), Inter (Rasmus Andersson), JetBrains Mono — professionally designed, self-hosted | Aligned in spirit: these are free but professional-grade faces; Butterick's stricter preference is paid fonts |
583
585
 
584
586
  ### The line-spacing deviation, argued
package/broadsheet.css CHANGED
@@ -319,23 +319,29 @@
319
319
  (3.5em / 0.85 line-height / initial-letter progressive enhancement)
320
320
  for one consistent drop-cap voice across the system, rather than a
321
321
  second bespoke number. */
322
- .remarque-lead-lede::first-letter {
323
- font-family: var(--font-display);
324
- font-style: italic;
325
- font-weight: var(--weight-display, var(--weight-regular));
326
- font-size: 3.5em;
327
- line-height: 0.85;
328
- float: left;
329
- padding: 0.08em 0.12em 0 0;
330
- color: var(--color-fg);
331
- }
332
-
333
- @supports (initial-letter: 4) or (-webkit-initial-letter: 4) {
322
+ /* Wide viewports only (2026-10 design review): on a phone-width column
323
+ the 3.5em italic capital (~110px for a "W") left the italic deck a
324
+ ~20-character sliver for four lines and sat against the title above
325
+ it. Below 40rem the lede reads as plain italic text. */
326
+ @media (min-width: 40rem) {
334
327
  .remarque-lead-lede::first-letter {
335
- initial-letter: 3.5;
336
- -webkit-initial-letter: 3.5;
337
- float: none;
338
- padding: 0 0.08em 0 0;
328
+ font-family: var(--font-display);
329
+ font-style: italic;
330
+ font-weight: var(--weight-display, var(--weight-regular));
331
+ font-size: 3.5em;
332
+ line-height: 0.85;
333
+ float: left;
334
+ padding: 0.08em 0.12em 0 0;
335
+ color: var(--color-fg);
336
+ }
337
+
338
+ @supports (initial-letter: 4) or (-webkit-initial-letter: 4) {
339
+ .remarque-lead-lede::first-letter {
340
+ initial-letter: 3.5;
341
+ -webkit-initial-letter: 3.5;
342
+ float: none;
343
+ padding: 0 0.08em 0 0;
344
+ }
339
345
  }
340
346
  }
341
347
 
package/essay.css CHANGED
@@ -77,9 +77,9 @@
77
77
  * this package's own tokens instead of asserted as a bare magic number:
78
78
  * `--content-standard` (72rem) is the widest a Remarque page's content
79
79
  * ever grows, and the rail/gutter tracks below need `--space-11`+
80
- * `--space-12` (10-12rem) and `--space-10` (8rem) of clearance on top of
81
- * `--content-reading` (46rem default) plus two `--space-6` gaps — call it
82
- * roughly 70-76rem of content width before both tracks have their
80
+ * `--space-12` (10-12rem) and `--space-11` (10rem) of clearance on top of
81
+ * `--content-reading` (38rem default; up to 46rem for wider faces) plus two
82
+ * `--space-6` gaps — call it roughly 62-76rem of content width before both tracks have their
83
83
  * documented minimum room, plus headroom for a scrollbar and window
84
84
  * chrome. 80rem clears that with margin to spare across every approved
85
85
  * body-face/measure pairing (see REMARQUE.md's Measure Compensation
@@ -191,18 +191,21 @@
191
191
  * landing in the grid's column-1 gutter. `clear: left` stacks consecutive
192
192
  * notes without any JS-computed offset.
193
193
  *
194
- * Width (`--space-10`, 8rem) and pull-out distance (that width plus
194
+ * Width (`--space-11`, 10rem) and pull-out distance (that width plus
195
195
  * `--space-6`, the same gap the grid below uses between tracks) are both
196
196
  * drawn from the core spacing scale rather than asserting a bespoke
197
197
  * sidebar-width value — this is the token-vocabulary re-expression of the
198
- * flagship's hand-measured `11.5rem`/`13rem` pair.
198
+ * flagship's hand-measured `11.5rem`/`13rem` pair. (Was `--space-10`, 8rem,
199
+ * until 0.27: about 15 mono characters per line, so a two-sentence note
200
+ * ran 19 lines tall — 2026-10 design review. 10rem still clears the
201
+ * 80rem breakpoint with a 46rem reading column.)
199
202
  */
200
203
  @media (min-width: 80rem) {
201
204
  .remarque-sidenote {
202
205
  float: left;
203
206
  clear: left;
204
- width: var(--space-10);
205
- margin-inline-start: calc(-1 * (var(--space-10) + var(--space-6)));
207
+ width: var(--space-11);
208
+ margin-inline-start: calc(-1 * (var(--space-11) + var(--space-6)));
206
209
  margin-block: var(--space-1) var(--space-4);
207
210
  padding-inline-start: 0;
208
211
  padding-inline-end: var(--space-2);
@@ -306,8 +309,12 @@
306
309
  list-style: none;
307
310
  display: flex;
308
311
  align-items: center;
309
- justify-content: space-between;
312
+ /* Marker sits beside its label, not at the far end of the row — a
313
+ triangle stranded 190px from its text reads as a stray glyph and
314
+ the row stops reading as a control (2026-10 design review). */
315
+ justify-content: flex-start;
310
316
  gap: var(--space-2);
317
+ min-height: 44px; /* touch-target floor — same shape as .nav-link */
311
318
  padding-block: var(--space-2);
312
319
  border-bottom: var(--border-width) var(--border-style) var(--color-border);
313
320
  /* Mono meta voice — same declarations as .text-label (tokens-core.css),
@@ -349,11 +356,17 @@
349
356
  margin: var(--space-3) 0 0;
350
357
  }
351
358
 
352
- .remarque-toc-rail li + li {
353
- margin-top: var(--space-1);
354
- }
359
+ /* No li + li margin: each link's own 44px min-height is the rhythm. */
355
360
 
356
361
  .remarque-toc-rail a {
362
+ /* Block-level flex so every entry is a full-width, >= 44px touch
363
+ target (AGENT_RULES.md quality checklist) — these are navigation,
364
+ not inline links in running text, so the WCAG 2.5.8 inline
365
+ exception doesn't cover them. Multi-line entries grow past the
366
+ floor naturally. */
367
+ display: flex;
368
+ align-items: center;
369
+ min-height: 44px;
357
370
  font-family: var(--font-mono);
358
371
  font-size: var(--text-meta);
359
372
  color: var(--color-fg-muted);
@@ -441,8 +454,16 @@
441
454
  number in consumer CSS. */
442
455
  z-index: var(--z-sticky);
443
456
  top: var(--space-6);
444
- width: 100%;
445
457
  max-height: calc(100vh - var(--space-8));
446
458
  overflow-y: auto;
459
+ /* overflow-y makes the rail a scroll container, which clips anything
460
+ painted outside its padding box — including the 2px ring +
461
+ 2px offset on a focused link or summary (issue #115). Pad the
462
+ container past the ring's reach and pull it back by the same
463
+ amount so the rail's text stays on the track edge. */
464
+ box-sizing: border-box;
465
+ padding: var(--space-2);
466
+ margin-inline-start: calc(-1 * var(--space-2));
467
+ width: calc(100% + var(--space-2));
447
468
  }
448
469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -87,6 +87,7 @@
87
87
  "./registry/*": "./registry/*.json",
88
88
  "./skills/remarque": "./skills/remarque/SKILL.md",
89
89
  "./skills/adopt": "./skills/remarque-adopt/SKILL.md",
90
+ "./skills/new-page": "./skills/remarque-new-page/SKILL.md",
90
91
  "./package.json": "./package.json"
91
92
  },
92
93
  "files": [
package/prose.css CHANGED
@@ -90,6 +90,15 @@
90
90
  color: var(--color-link-hover);
91
91
  }
92
92
 
93
+ /* Visited links keep accent text (the one-accent rule) and shift only the
94
+ underline to an existing, audited neutral — a color-only change, the
95
+ one kind browsers honor on :visited. No new palette token, so every
96
+ remarque-theme palette gets it for free. Ratified 7-0 (2026-10 design
97
+ review); forced-colors maps it to the system VisitedText color. */
98
+ .remarque-prose a:visited {
99
+ text-decoration-color: var(--color-fg-muted);
100
+ }
101
+
93
102
  .remarque-prose strong {
94
103
  font-weight: var(--weight-semibold);
95
104
  }
@@ -281,6 +290,10 @@
281
290
  .remarque-prose td.num {
282
291
  font-variant-numeric: tabular-nums lining-nums;
283
292
  text-align: right;
293
+ /* A value never breaks across lines — "44–80" split after its en
294
+ dash reads as two numbers (2026-10 design review). Numeric cells
295
+ are short; a genuinely wide table still belongs in .scroll-wrap. */
296
+ white-space: nowrap;
284
297
  }
285
298
 
286
299
  /* ─── Definition Lists ───────────────────────────────────── */
@@ -4,8 +4,8 @@
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 true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.",
7
- "version": "0.25.0",
8
- "integrity": "sha256-BZea38PRdob4OfmdGN0eDw3U0adl7SEsdrjKgSewK9A=",
7
+ "version": "0.27.0",
8
+ "integrity": "sha256-aweD48hO4kVgvjm2ZFC8u124Az9AvrJ/zribu5BPZ/g=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"
11
11
  ],
@@ -54,7 +54,7 @@
54
54
  "files": [
55
55
  {
56
56
  "path": "broadsheet.css",
57
- "content": "/*\n * Remarque — Broadsheet Pattern (masthead, lead, entry list, post kicker)\n * ────────────────────────────────────────────────────────────────────\n * Four editorial-layout primitives for Landing/archive contexts — the\n * typography-maximalist end of the system. Newspaper-broadsheet voice:\n * a nameplate, a featured lead story, a numbered contents list, and a\n * post-header kicker line, built entirely from tokens already shipped\n * in tokens-core.css / tokens-palette.css.\n *\n * PROVENANCE — graduated, not invented here. Validated downstream on\n * williamzujkowski.github.io (landing PR #213, archive/tag/post-header\n * PR #214), ratified for upstreaming in issue #36. Source: `astro-site/\n * src/components/BroadsheetEntry.astro` + `global.css`'s \"Broadsheet\n * layout\" block. As with the essay module (#52), no site-specific\n * literal was copied — every dimension below is re-derived from this\n * package's own tokens; see the inline comments for the value-by-value\n * mapping and the few places the re-expression corrects the source\n * (`text-transform: uppercase` -> `font-variant-caps: all-small-caps`,\n * per REMARQUE.md \"Small Caps\", predates that rule same as essay.css's\n * TOC summary did) or simplifies it (font-weight 300 has no Remarque\n * core token; the italic/lede treatments below lean on color + style\n * alone rather than introduce an un-tokenized weight).\n *\n * Own subpath (remarque-tokens/broadsheet, remarque-tokens/broadsheet.css)\n * — NOT included by the tokens.css aggregator: the Landing/archive\n * pattern is opt-in, most Remarque pages want none of it. Import\n * explicitly, after prose.css if both are in use:\n * @import 'remarque-tokens/broadsheet.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <header class=\"remarque-masthead\">\n * <p class=\"remarque-masthead-kicker\"><span>Field Notes</span></p>\n * <h1 class=\"remarque-masthead-title\">Quiet <em>Signals</em></h1>\n * <p class=\"remarque-masthead-dateline\">\n * <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n * <span>July 23, 2026</span>\n * <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n * </p>\n * </header>\n *\n * <article class=\"remarque-lead\">\n * <p class=\"remarque-lead-kicker\">\n * <span class=\"remarque-lead-section\">Essay</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n * </p>\n * <h2 class=\"remarque-lead-title\"><a href=\"/writing/x\">Title</a></h2>\n * <p class=\"remarque-lead-lede\">Lede paragraph...</p>\n * </article>\n *\n * <ul class=\"remarque-entry-list\">\n * <li class=\"remarque-entry\">\n * <span class=\"remarque-entry-numeral\" data-entry-number=\"2\"\n * aria-hidden=\"true\"></span>\n * <div class=\"remarque-entry-body\">\n * <p class=\"remarque-entry-kicker\">...</p>\n * <h3 class=\"remarque-entry-title\"><a href=\"/writing/y\">Title</a></h3>\n * <p class=\"remarque-entry-excerpt\">Excerpt...</p>\n * </div>\n * </li>\n * </ul>\n *\n * <header class=\"remarque-post-header\">\n * <p class=\"remarque-post-kicker\">\n * <span class=\"remarque-post-kicker-section\">Essay</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n * </p>\n * <h1 class=\"text-display font-display\">Post Title</h1>\n * </header>\n *\n * `<ul>` (not `<ol>`), deliberately, for the entry list: the visual\n * numeral is decorative (`aria-hidden`, generated from `data-entry-\n * number` via `attr()` — see below), and an `<ol>` would have screen\n * readers announce \"1 of 2\" alongside it, doubling up on the same\n * information in two voices. Matches the flagship's own choice.\n */\n\n/* ─── Shared meta-voice bits ─────────────────────────────────────────\n * Every kicker/dateline row below repeats these declarations rather\n * than depending on `.text-label` as a mixin (tokens-core.css) — same\n * non-mixin precedent as essay.css's TOC summary. All-small-caps, not\n * `text-transform: uppercase` (REMARQUE.md \"Small Caps\": faux uppercase\n * renders at cap-height with no optical correction; the flagship\n * predates that rule and used `text-transform: uppercase` throughout\n * this pattern). `--text-meta` (14px), not `--text-micro` (13px) — the\n * words in a kicker (\"Essay\", \"Field Notes\") are read as content, not\n * bare timestamps/fine print, the same distinction essay.css's sidenote\n * marker already draws against the 13px floor. Tabular lining figures\n * on every numeral here (dates, counts) per REMARQUE.md's Numerals\n * rule — never mixed with the oldstyle register `.remarque-entry-\n * numeral` uses below for the same reason `.text-meta` carries it.\n */\n.remarque-masthead-kicker,\n.remarque-masthead-dateline,\n.remarque-lead-kicker,\n.remarque-entry-kicker,\n.remarque-post-kicker {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n font-variant-caps: all-small-caps;\n font-variant-numeric: tabular-nums lining-nums;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n margin: 0;\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5em;\n}\n\n.remarque-dot {\n color: var(--color-border-bold);\n}\n\n.remarque-lead-section,\n.remarque-entry-section,\n.remarque-post-kicker-section {\n color: var(--color-accent);\n font-weight: var(--weight-medium);\n}\n\n/* Hairline flanking the masthead dateline — decorative only, so a\n 1px border-color rule (not -bold) is the right register; nothing\n depends on it as a boundary.\n Authored as a BORDER, not a `background` fill, on purpose (issue #93\n forced-colors audit finding): this is a bare, non-semantic `<span>`\n with no text content and no border of its own otherwise — under\n `forced-colors: active`, a plain element's `background-color` is\n generally forced to `Canvas` (the page background), which would make\n a background-fill divider disappear entirely, while `border-color` on\n a real border is forced to a visible system border color instead.\n Same 1px visual result at rest (a zero-height box with a 1px top\n border reads identically to a 1px-tall filled box); only the forced-\n colors behavior differs. */\n.remarque-rule {\n display: inline-block;\n width: var(--space-6);\n height: 0;\n border-top: var(--border-width) var(--border-style) var(--color-border-bold);\n}\n\n/* Hairline divider between broadsheet sections (masthead/lead/entry-list) */\n.remarque-broadsheet-rule {\n max-width: var(--content-reading);\n margin: var(--space-6) auto;\n border: 0;\n border-top: var(--border-width) var(--border-style) var(--color-border);\n}\n\n/* ─── 1. Masthead ─────────────────────────────────────────────────────\n * The nameplate. Centered, oversized, the one place in Remarque a\n * title outgrows `--text-display` (tokens-core.css's largest type-\n * scale rung, clamp(2.75rem, 5.5vw, 5rem)) — a Landing masthead is a\n * register above even a Landing page's own `<h1>`.\n *\n * FLUID-TYPE DECISION (issue #36's required call): the existing scale\n * does not reach the flagship's validated clamp(3.5rem, 9vw, 7.5rem).\n * Rather than assert those as bare literals (or add a new core\n * `--text-masthead` token, which the issue explicitly rules out), this\n * is a documented, file-scoped fluid exception whose min/max BOUNDS are\n * arithmetic on core spacing tokens already in tokens-core.css — not\n * new constants:\n * min = --space-7 (3rem) + --space-2 (0.5rem) = 3.5rem\n * max = --space-10 (8rem) - --space-2 (0.5rem) = 7.5rem\n * both resolve to the exact flagship values that were already\n * validated (contrast/gamut/font-floor audits + axe) downstream. The\n * 9vw fluid rate is kept as-is: it continues the same escalating-per-\n * tier vw progression tokens-core.css's own scale already uses\n * (2.25vw section -> 3.5vw title -> 5.5vw display), one step further\n * for a register one step past display. This custom property lives\n * here, not in tokens-core.css — it is NOT a new core token, it is\n * local arithmetic on existing ones, scoped to this opt-in pattern.\n */\n.remarque-masthead {\n --remarque-masthead-size: clamp(\n calc(var(--space-7) + var(--space-2)),\n 9vw,\n calc(var(--space-10) - var(--space-2))\n );\n max-width: var(--content-reading);\n margin: var(--space-8) auto var(--space-6);\n text-align: center;\n}\n\n.remarque-masthead-title {\n font-family: var(--font-display);\n font-size: var(--remarque-masthead-size);\n /* Reuses --leading-display/--tracking-display rather than the\n flagship's bespoke 0.92/-0.035em — a fully tokenized simplification\n at a scale where the difference is not perceptible at normal\n reading distance. */\n line-height: var(--leading-display);\n letter-spacing: var(--tracking-display);\n /* Same --weight-display escape hatch .text-display/.text-title use\n (tokens-core.css) — a palette can compensate thin serif hairlines\n in dark mode (tokens-palette.css already does, 400 light / 500\n dark) and the masthead picks that up for free. */\n font-weight: var(--weight-display, var(--weight-regular));\n color: var(--color-fg);\n margin: 0;\n font-feature-settings: \"liga\" 1, \"kern\" 1;\n font-optical-sizing: auto;\n font-synthesis: none;\n text-wrap: balance;\n}\n\n.remarque-masthead-title em {\n font-style: italic;\n color: var(--color-fg-muted);\n}\n\n.remarque-masthead-dateline {\n margin-block-start: var(--space-4);\n justify-content: center;\n}\n\n/* ─── 2. Lead Article ─────────────────────────────────────────────────\n * The featured story below the masthead (or standing alone atop an\n * archive). Title register: --text-display reaches close enough to\n * the flagship's clamp(2.25rem, 4.5vw, 3.75rem) (2.75-5rem vs.\n * 2.25-3.75rem) that no exception is needed here — \"express via\n * existing type tokens\" (issue #36), the masthead above is the one\n * register that genuinely doesn't fit.\n */\n.remarque-lead {\n max-width: var(--content-reading);\n margin: var(--space-7) auto var(--space-6);\n}\n\n.remarque-lead-kicker {\n margin-block-end: var(--space-3);\n}\n\n.remarque-lead-title {\n font-family: var(--font-display);\n font-size: var(--text-display);\n line-height: var(--leading-display);\n letter-spacing: var(--tracking-display);\n font-weight: var(--weight-display, var(--weight-regular));\n margin: 0 0 var(--space-4);\n text-wrap: balance;\n font-optical-sizing: auto;\n font-synthesis: none;\n}\n\n.remarque-lead-title a,\n.remarque-entry-title a {\n color: var(--color-fg);\n text-decoration: none;\n /* Underline-grow hover (issue #36 \"Findings\"): AA-compliant, and\n reduced-motion-safe for free — tokens-core.css zeroes\n --motion-normal under prefers-reduced-motion, so this needs no\n separate @media guard the way an opacity/transform animation\n would. --motion-easing/--motion-normal replace the flagship's\n hardcoded `220ms ease`. */\n background-image: linear-gradient(var(--color-fg), var(--color-fg));\n background-size: 0 1px;\n background-repeat: no-repeat;\n background-position: 0 95%;\n transition: background-size var(--motion-normal) var(--motion-easing);\n}\n\n.remarque-lead-title a:hover,\n.remarque-lead-title a:focus-visible,\n.remarque-entry-title a:hover,\n.remarque-entry-title a:focus-visible {\n background-size: 100% 1px;\n}\n\n/* Forced-colors fallback (issue #93 audit finding — affordance-loss,\n the clearest one in this file): the underline-grow hover/focus\n affordance above is painted entirely with `background-image:\n linear-gradient(...)`. CSS gradients are on the forced list — under\n `forced-colors: active` a gradient background-image is forced to\n `none`, so the ENTIRE hover/focus signal for these title links would\n otherwise disappear (the `:focus-visible` outline from tokens-core.css\n still fires for keyboard focus, but mouse hover would have no visual\n affordance at all, and even focus would lose this link-specific cue).\n `text-decoration: underline` is not on the forced list and needs no\n override to render visibly, so it stands in as the same \"this is a\n link, and it is the active one\" signal the gradient trick provides\n everywhere else. Scoped to this media query only — the gradient\n underline-grow stays the sole affordance outside forced-colors mode,\n unchanged. */\n@media (forced-colors: active) {\n .remarque-lead-title a:hover,\n .remarque-lead-title a:focus-visible,\n .remarque-entry-title a:hover,\n .remarque-entry-title a:focus-visible {\n text-decoration: underline;\n }\n}\n\n.remarque-lead-lede {\n font-family: var(--font-display);\n font-size: var(--text-body-lg);\n line-height: var(--leading-body);\n font-style: italic;\n font-weight: var(--weight-regular);\n color: var(--color-fg-muted);\n margin: 0;\n text-wrap: pretty;\n hanging-punctuation: first last;\n}\n\n/* Drop cap on italic text needs an explicit ::first-letter override\n (issue #36 \"Findings\") — it cannot just inherit italic from the\n parent's font-style the way a roman-text drop cap can, since this\n pseudo-element re-asserts every typographic property of its own.\n Same mechanics/sizing as prose.css's `.remarque-prose--dropcap`\n (3.5em / 0.85 line-height / initial-letter progressive enhancement)\n for one consistent drop-cap voice across the system, rather than a\n second bespoke number. */\n.remarque-lead-lede::first-letter {\n font-family: var(--font-display);\n font-style: italic;\n font-weight: var(--weight-display, var(--weight-regular));\n font-size: 3.5em;\n line-height: 0.85;\n float: left;\n padding: 0.08em 0.12em 0 0;\n color: var(--color-fg);\n}\n\n@supports (initial-letter: 4) or (-webkit-initial-letter: 4) {\n .remarque-lead-lede::first-letter {\n initial-letter: 3.5;\n -webkit-initial-letter: 3.5;\n float: none;\n padding: 0 0.08em 0 0;\n }\n}\n\n/* ─── 3. Numbered Entry List ──────────────────────────────────────────\n * `container-type: inline-size` — the 2-column/1-column collapse below\n * responds to how much room the list itself has, not the viewport (a\n * clean fit for a container query, matching the flagship's choice; it\n * degrades correctly if this list is ever embedded in a narrower\n * column than the page itself).\n */\n.remarque-entry-list {\n container-type: inline-size;\n max-width: var(--content-reading);\n margin: 0 auto;\n padding: 0;\n list-style: none;\n}\n\n.remarque-entry {\n display: grid;\n /* Numeral column sized in `ch` (intrinsic to the numeral glyphs\n themselves), not an asserted rem literal — the flagship's\n `minmax(3.5rem, 5rem)` re-expressed as \"just wide enough for the\n content,\" the more typographically disciplined choice per\n REMARQUE.md's Decision Protocol when a core token doesn't already\n cover a dimension. */\n grid-template-columns: minmax(2.5ch, 4ch) 1fr;\n gap: var(--space-4);\n padding: var(--space-5) 0;\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n align-items: start;\n}\n\n.remarque-entry:last-child {\n border-bottom: 0;\n}\n\n/* Oldstyle italic numerals in the outer margin, generated from\n `data-entry-number` via `attr()` — NOT CSS `counter()` (issue #36\n \"Findings\": counter-generated content does not receive the OpenType\n oldstyle-figure treatment reliably across Newsreader's weights, only\n real text/attr() content does). `attr()` reads from the element the\n pseudo-content is generated on, so `data-entry-number` lives on\n `.remarque-entry-numeral` itself, not the parent `<li>`. Both\n `font-variant-numeric: oldstyle-nums proportional-nums` AND\n `font-feature-settings: \"onum\" 1, \"pnum\" 1` are required together\n for reliable rendering across weights — either alone is\n inconsistent (issue #36 \"Findings\"). */\n.remarque-entry-numeral {\n display: block;\n}\n\n.remarque-entry-numeral::before {\n content: attr(data-entry-number);\n font-family: var(--font-display);\n font-style: italic;\n font-size: var(--text-title);\n line-height: 0.95;\n color: var(--color-border-bold);\n font-variant-numeric: oldstyle-nums proportional-nums;\n font-feature-settings: \"onum\" 1, \"pnum\" 1;\n text-align: right;\n display: block;\n padding-block-start: 0.1em;\n user-select: none;\n transition: color var(--motion-normal) var(--motion-easing);\n}\n\n.remarque-entry:hover .remarque-entry-numeral::before,\n.remarque-entry:focus-within .remarque-entry-numeral::before {\n color: var(--color-accent);\n}\n\n.remarque-entry-body {\n min-width: 0; /* allow the grid child to shrink below its content's intrinsic width */\n}\n\n.remarque-entry-kicker {\n margin-block-end: var(--space-2);\n}\n\n.remarque-entry-title {\n font-family: var(--font-display);\n font-size: var(--text-section);\n line-height: var(--leading-section);\n letter-spacing: var(--tracking-title);\n font-weight: var(--weight-medium);\n margin: 0 0 var(--space-2);\n text-wrap: balance;\n font-optical-sizing: auto;\n font-synthesis: none;\n}\n\n.remarque-entry-excerpt {\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-body);\n color: var(--color-fg-muted);\n margin: 0;\n text-wrap: pretty;\n}\n\n/* Collapse to one column on a narrow container — `40rem`, exactly\n `--space-10` (8rem) x 5. Container-query conditions can't reference\n custom properties or `calc()` (unlike a plain declaration value, the\n condition itself is parsed before the cascade resolves custom\n properties, and Lightning CSS's minifier rejects a `calc()` there\n outright) so this is spelled as the literal it resolves to, with the\n token-multiple noted here rather than in the syntax — matches the\n flagship's own `640px` breakpoint exactly. */\n@container (max-width: 40rem) {\n .remarque-entry {\n grid-template-columns: 1fr;\n gap: var(--space-2);\n }\n\n .remarque-entry-numeral::before {\n text-align: left;\n font-size: var(--text-section);\n padding-block-start: 0;\n }\n}\n\n/* ─── 4. Post-Header Kicker ───────────────────────────────────────────\n * The mono \"Section · Reading time · Date\" line above an individual\n * post's title — the same kicker voice as the lead/entry rows above,\n * scaled down to a single-line header. Pair with any Remarque heading\n * class (`.text-display`, `.text-title`); this rule only styles the\n * kicker line itself, not the title beneath it.\n */\n.remarque-post-header {\n margin-block-end: var(--space-7);\n}\n\n.remarque-post-kicker {\n margin-block-end: var(--space-3);\n}\n",
57
+ "content": "/*\n * Remarque — Broadsheet Pattern (masthead, lead, entry list, post kicker)\n * ────────────────────────────────────────────────────────────────────\n * Four editorial-layout primitives for Landing/archive contexts — the\n * typography-maximalist end of the system. Newspaper-broadsheet voice:\n * a nameplate, a featured lead story, a numbered contents list, and a\n * post-header kicker line, built entirely from tokens already shipped\n * in tokens-core.css / tokens-palette.css.\n *\n * PROVENANCE — graduated, not invented here. Validated downstream on\n * williamzujkowski.github.io (landing PR #213, archive/tag/post-header\n * PR #214), ratified for upstreaming in issue #36. Source: `astro-site/\n * src/components/BroadsheetEntry.astro` + `global.css`'s \"Broadsheet\n * layout\" block. As with the essay module (#52), no site-specific\n * literal was copied — every dimension below is re-derived from this\n * package's own tokens; see the inline comments for the value-by-value\n * mapping and the few places the re-expression corrects the source\n * (`text-transform: uppercase` -> `font-variant-caps: all-small-caps`,\n * per REMARQUE.md \"Small Caps\", predates that rule same as essay.css's\n * TOC summary did) or simplifies it (font-weight 300 has no Remarque\n * core token; the italic/lede treatments below lean on color + style\n * alone rather than introduce an un-tokenized weight).\n *\n * Own subpath (remarque-tokens/broadsheet, remarque-tokens/broadsheet.css)\n * — NOT included by the tokens.css aggregator: the Landing/archive\n * pattern is opt-in, most Remarque pages want none of it. Import\n * explicitly, after prose.css if both are in use:\n * @import 'remarque-tokens/broadsheet.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <header class=\"remarque-masthead\">\n * <p class=\"remarque-masthead-kicker\"><span>Field Notes</span></p>\n * <h1 class=\"remarque-masthead-title\">Quiet <em>Signals</em></h1>\n * <p class=\"remarque-masthead-dateline\">\n * <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n * <span>July 23, 2026</span>\n * <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n * </p>\n * </header>\n *\n * <article class=\"remarque-lead\">\n * <p class=\"remarque-lead-kicker\">\n * <span class=\"remarque-lead-section\">Essay</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n * </p>\n * <h2 class=\"remarque-lead-title\"><a href=\"/writing/x\">Title</a></h2>\n * <p class=\"remarque-lead-lede\">Lede paragraph...</p>\n * </article>\n *\n * <ul class=\"remarque-entry-list\">\n * <li class=\"remarque-entry\">\n * <span class=\"remarque-entry-numeral\" data-entry-number=\"2\"\n * aria-hidden=\"true\"></span>\n * <div class=\"remarque-entry-body\">\n * <p class=\"remarque-entry-kicker\">...</p>\n * <h3 class=\"remarque-entry-title\"><a href=\"/writing/y\">Title</a></h3>\n * <p class=\"remarque-entry-excerpt\">Excerpt...</p>\n * </div>\n * </li>\n * </ul>\n *\n * <header class=\"remarque-post-header\">\n * <p class=\"remarque-post-kicker\">\n * <span class=\"remarque-post-kicker-section\">Essay</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">&middot;</span>\n * <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n * </p>\n * <h1 class=\"text-display font-display\">Post Title</h1>\n * </header>\n *\n * `<ul>` (not `<ol>`), deliberately, for the entry list: the visual\n * numeral is decorative (`aria-hidden`, generated from `data-entry-\n * number` via `attr()` — see below), and an `<ol>` would have screen\n * readers announce \"1 of 2\" alongside it, doubling up on the same\n * information in two voices. Matches the flagship's own choice.\n */\n\n/* ─── Shared meta-voice bits ─────────────────────────────────────────\n * Every kicker/dateline row below repeats these declarations rather\n * than depending on `.text-label` as a mixin (tokens-core.css) — same\n * non-mixin precedent as essay.css's TOC summary. All-small-caps, not\n * `text-transform: uppercase` (REMARQUE.md \"Small Caps\": faux uppercase\n * renders at cap-height with no optical correction; the flagship\n * predates that rule and used `text-transform: uppercase` throughout\n * this pattern). `--text-meta` (14px), not `--text-micro` (13px) — the\n * words in a kicker (\"Essay\", \"Field Notes\") are read as content, not\n * bare timestamps/fine print, the same distinction essay.css's sidenote\n * marker already draws against the 13px floor. Tabular lining figures\n * on every numeral here (dates, counts) per REMARQUE.md's Numerals\n * rule — never mixed with the oldstyle register `.remarque-entry-\n * numeral` uses below for the same reason `.text-meta` carries it.\n */\n.remarque-masthead-kicker,\n.remarque-masthead-dateline,\n.remarque-lead-kicker,\n.remarque-entry-kicker,\n.remarque-post-kicker {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n font-variant-caps: all-small-caps;\n font-variant-numeric: tabular-nums lining-nums;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n margin: 0;\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5em;\n}\n\n.remarque-dot {\n color: var(--color-border-bold);\n}\n\n.remarque-lead-section,\n.remarque-entry-section,\n.remarque-post-kicker-section {\n color: var(--color-accent);\n font-weight: var(--weight-medium);\n}\n\n/* Hairline flanking the masthead dateline — decorative only, so a\n 1px border-color rule (not -bold) is the right register; nothing\n depends on it as a boundary.\n Authored as a BORDER, not a `background` fill, on purpose (issue #93\n forced-colors audit finding): this is a bare, non-semantic `<span>`\n with no text content and no border of its own otherwise — under\n `forced-colors: active`, a plain element's `background-color` is\n generally forced to `Canvas` (the page background), which would make\n a background-fill divider disappear entirely, while `border-color` on\n a real border is forced to a visible system border color instead.\n Same 1px visual result at rest (a zero-height box with a 1px top\n border reads identically to a 1px-tall filled box); only the forced-\n colors behavior differs. */\n.remarque-rule {\n display: inline-block;\n width: var(--space-6);\n height: 0;\n border-top: var(--border-width) var(--border-style) var(--color-border-bold);\n}\n\n/* Hairline divider between broadsheet sections (masthead/lead/entry-list) */\n.remarque-broadsheet-rule {\n max-width: var(--content-reading);\n margin: var(--space-6) auto;\n border: 0;\n border-top: var(--border-width) var(--border-style) var(--color-border);\n}\n\n/* ─── 1. Masthead ─────────────────────────────────────────────────────\n * The nameplate. Centered, oversized, the one place in Remarque a\n * title outgrows `--text-display` (tokens-core.css's largest type-\n * scale rung, clamp(2.75rem, 5.5vw, 5rem)) — a Landing masthead is a\n * register above even a Landing page's own `<h1>`.\n *\n * FLUID-TYPE DECISION (issue #36's required call): the existing scale\n * does not reach the flagship's validated clamp(3.5rem, 9vw, 7.5rem).\n * Rather than assert those as bare literals (or add a new core\n * `--text-masthead` token, which the issue explicitly rules out), this\n * is a documented, file-scoped fluid exception whose min/max BOUNDS are\n * arithmetic on core spacing tokens already in tokens-core.css — not\n * new constants:\n * min = --space-7 (3rem) + --space-2 (0.5rem) = 3.5rem\n * max = --space-10 (8rem) - --space-2 (0.5rem) = 7.5rem\n * both resolve to the exact flagship values that were already\n * validated (contrast/gamut/font-floor audits + axe) downstream. The\n * 9vw fluid rate is kept as-is: it continues the same escalating-per-\n * tier vw progression tokens-core.css's own scale already uses\n * (2.25vw section -> 3.5vw title -> 5.5vw display), one step further\n * for a register one step past display. This custom property lives\n * here, not in tokens-core.css — it is NOT a new core token, it is\n * local arithmetic on existing ones, scoped to this opt-in pattern.\n */\n.remarque-masthead {\n --remarque-masthead-size: clamp(\n calc(var(--space-7) + var(--space-2)),\n 9vw,\n calc(var(--space-10) - var(--space-2))\n );\n max-width: var(--content-reading);\n margin: var(--space-8) auto var(--space-6);\n text-align: center;\n}\n\n.remarque-masthead-title {\n font-family: var(--font-display);\n font-size: var(--remarque-masthead-size);\n /* Reuses --leading-display/--tracking-display rather than the\n flagship's bespoke 0.92/-0.035em — a fully tokenized simplification\n at a scale where the difference is not perceptible at normal\n reading distance. */\n line-height: var(--leading-display);\n letter-spacing: var(--tracking-display);\n /* Same --weight-display escape hatch .text-display/.text-title use\n (tokens-core.css) — a palette can compensate thin serif hairlines\n in dark mode (tokens-palette.css already does, 400 light / 500\n dark) and the masthead picks that up for free. */\n font-weight: var(--weight-display, var(--weight-regular));\n color: var(--color-fg);\n margin: 0;\n font-feature-settings: \"liga\" 1, \"kern\" 1;\n font-optical-sizing: auto;\n font-synthesis: none;\n text-wrap: balance;\n}\n\n.remarque-masthead-title em {\n font-style: italic;\n color: var(--color-fg-muted);\n}\n\n.remarque-masthead-dateline {\n margin-block-start: var(--space-4);\n justify-content: center;\n}\n\n/* ─── 2. Lead Article ─────────────────────────────────────────────────\n * The featured story below the masthead (or standing alone atop an\n * archive). Title register: --text-display reaches close enough to\n * the flagship's clamp(2.25rem, 4.5vw, 3.75rem) (2.75-5rem vs.\n * 2.25-3.75rem) that no exception is needed here — \"express via\n * existing type tokens\" (issue #36), the masthead above is the one\n * register that genuinely doesn't fit.\n */\n.remarque-lead {\n max-width: var(--content-reading);\n margin: var(--space-7) auto var(--space-6);\n}\n\n.remarque-lead-kicker {\n margin-block-end: var(--space-3);\n}\n\n.remarque-lead-title {\n font-family: var(--font-display);\n font-size: var(--text-display);\n line-height: var(--leading-display);\n letter-spacing: var(--tracking-display);\n font-weight: var(--weight-display, var(--weight-regular));\n margin: 0 0 var(--space-4);\n text-wrap: balance;\n font-optical-sizing: auto;\n font-synthesis: none;\n}\n\n.remarque-lead-title a,\n.remarque-entry-title a {\n color: var(--color-fg);\n text-decoration: none;\n /* Underline-grow hover (issue #36 \"Findings\"): AA-compliant, and\n reduced-motion-safe for free — tokens-core.css zeroes\n --motion-normal under prefers-reduced-motion, so this needs no\n separate @media guard the way an opacity/transform animation\n would. --motion-easing/--motion-normal replace the flagship's\n hardcoded `220ms ease`. */\n background-image: linear-gradient(var(--color-fg), var(--color-fg));\n background-size: 0 1px;\n background-repeat: no-repeat;\n background-position: 0 95%;\n transition: background-size var(--motion-normal) var(--motion-easing);\n}\n\n.remarque-lead-title a:hover,\n.remarque-lead-title a:focus-visible,\n.remarque-entry-title a:hover,\n.remarque-entry-title a:focus-visible {\n background-size: 100% 1px;\n}\n\n/* Forced-colors fallback (issue #93 audit finding — affordance-loss,\n the clearest one in this file): the underline-grow hover/focus\n affordance above is painted entirely with `background-image:\n linear-gradient(...)`. CSS gradients are on the forced list — under\n `forced-colors: active` a gradient background-image is forced to\n `none`, so the ENTIRE hover/focus signal for these title links would\n otherwise disappear (the `:focus-visible` outline from tokens-core.css\n still fires for keyboard focus, but mouse hover would have no visual\n affordance at all, and even focus would lose this link-specific cue).\n `text-decoration: underline` is not on the forced list and needs no\n override to render visibly, so it stands in as the same \"this is a\n link, and it is the active one\" signal the gradient trick provides\n everywhere else. Scoped to this media query only — the gradient\n underline-grow stays the sole affordance outside forced-colors mode,\n unchanged. */\n@media (forced-colors: active) {\n .remarque-lead-title a:hover,\n .remarque-lead-title a:focus-visible,\n .remarque-entry-title a:hover,\n .remarque-entry-title a:focus-visible {\n text-decoration: underline;\n }\n}\n\n.remarque-lead-lede {\n font-family: var(--font-display);\n font-size: var(--text-body-lg);\n line-height: var(--leading-body);\n font-style: italic;\n font-weight: var(--weight-regular);\n color: var(--color-fg-muted);\n margin: 0;\n text-wrap: pretty;\n hanging-punctuation: first last;\n}\n\n/* Drop cap on italic text needs an explicit ::first-letter override\n (issue #36 \"Findings\") — it cannot just inherit italic from the\n parent's font-style the way a roman-text drop cap can, since this\n pseudo-element re-asserts every typographic property of its own.\n Same mechanics/sizing as prose.css's `.remarque-prose--dropcap`\n (3.5em / 0.85 line-height / initial-letter progressive enhancement)\n for one consistent drop-cap voice across the system, rather than a\n second bespoke number. */\n/* Wide viewports only (2026-10 design review): on a phone-width column\n the 3.5em italic capital (~110px for a \"W\") left the italic deck a\n ~20-character sliver for four lines and sat against the title above\n it. Below 40rem the lede reads as plain italic text. */\n@media (min-width: 40rem) {\n .remarque-lead-lede::first-letter {\n font-family: var(--font-display);\n font-style: italic;\n font-weight: var(--weight-display, var(--weight-regular));\n font-size: 3.5em;\n line-height: 0.85;\n float: left;\n padding: 0.08em 0.12em 0 0;\n color: var(--color-fg);\n }\n\n @supports (initial-letter: 4) or (-webkit-initial-letter: 4) {\n .remarque-lead-lede::first-letter {\n initial-letter: 3.5;\n -webkit-initial-letter: 3.5;\n float: none;\n padding: 0 0.08em 0 0;\n }\n }\n}\n\n/* ─── 3. Numbered Entry List ──────────────────────────────────────────\n * `container-type: inline-size` — the 2-column/1-column collapse below\n * responds to how much room the list itself has, not the viewport (a\n * clean fit for a container query, matching the flagship's choice; it\n * degrades correctly if this list is ever embedded in a narrower\n * column than the page itself).\n */\n.remarque-entry-list {\n container-type: inline-size;\n max-width: var(--content-reading);\n margin: 0 auto;\n padding: 0;\n list-style: none;\n}\n\n.remarque-entry {\n display: grid;\n /* Numeral column sized in `ch` (intrinsic to the numeral glyphs\n themselves), not an asserted rem literal — the flagship's\n `minmax(3.5rem, 5rem)` re-expressed as \"just wide enough for the\n content,\" the more typographically disciplined choice per\n REMARQUE.md's Decision Protocol when a core token doesn't already\n cover a dimension. */\n grid-template-columns: minmax(2.5ch, 4ch) 1fr;\n gap: var(--space-4);\n padding: var(--space-5) 0;\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n align-items: start;\n}\n\n.remarque-entry:last-child {\n border-bottom: 0;\n}\n\n/* Oldstyle italic numerals in the outer margin, generated from\n `data-entry-number` via `attr()` — NOT CSS `counter()` (issue #36\n \"Findings\": counter-generated content does not receive the OpenType\n oldstyle-figure treatment reliably across Newsreader's weights, only\n real text/attr() content does). `attr()` reads from the element the\n pseudo-content is generated on, so `data-entry-number` lives on\n `.remarque-entry-numeral` itself, not the parent `<li>`. Both\n `font-variant-numeric: oldstyle-nums proportional-nums` AND\n `font-feature-settings: \"onum\" 1, \"pnum\" 1` are required together\n for reliable rendering across weights — either alone is\n inconsistent (issue #36 \"Findings\"). */\n.remarque-entry-numeral {\n display: block;\n}\n\n.remarque-entry-numeral::before {\n content: attr(data-entry-number);\n font-family: var(--font-display);\n font-style: italic;\n font-size: var(--text-title);\n line-height: 0.95;\n color: var(--color-border-bold);\n font-variant-numeric: oldstyle-nums proportional-nums;\n font-feature-settings: \"onum\" 1, \"pnum\" 1;\n text-align: right;\n display: block;\n padding-block-start: 0.1em;\n user-select: none;\n transition: color var(--motion-normal) var(--motion-easing);\n}\n\n.remarque-entry:hover .remarque-entry-numeral::before,\n.remarque-entry:focus-within .remarque-entry-numeral::before {\n color: var(--color-accent);\n}\n\n.remarque-entry-body {\n min-width: 0; /* allow the grid child to shrink below its content's intrinsic width */\n}\n\n.remarque-entry-kicker {\n margin-block-end: var(--space-2);\n}\n\n.remarque-entry-title {\n font-family: var(--font-display);\n font-size: var(--text-section);\n line-height: var(--leading-section);\n letter-spacing: var(--tracking-title);\n font-weight: var(--weight-medium);\n margin: 0 0 var(--space-2);\n text-wrap: balance;\n font-optical-sizing: auto;\n font-synthesis: none;\n}\n\n.remarque-entry-excerpt {\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-body);\n color: var(--color-fg-muted);\n margin: 0;\n text-wrap: pretty;\n}\n\n/* Collapse to one column on a narrow container — `40rem`, exactly\n `--space-10` (8rem) x 5. Container-query conditions can't reference\n custom properties or `calc()` (unlike a plain declaration value, the\n condition itself is parsed before the cascade resolves custom\n properties, and Lightning CSS's minifier rejects a `calc()` there\n outright) so this is spelled as the literal it resolves to, with the\n token-multiple noted here rather than in the syntax — matches the\n flagship's own `640px` breakpoint exactly. */\n@container (max-width: 40rem) {\n .remarque-entry {\n grid-template-columns: 1fr;\n gap: var(--space-2);\n }\n\n .remarque-entry-numeral::before {\n text-align: left;\n font-size: var(--text-section);\n padding-block-start: 0;\n }\n}\n\n/* ─── 4. Post-Header Kicker ───────────────────────────────────────────\n * The mono \"Section · Reading time · Date\" line above an individual\n * post's title — the same kicker voice as the lead/entry rows above,\n * scaled down to a single-line header. Pair with any Remarque heading\n * class (`.text-display`, `.text-title`); this rule only styles the\n * kicker line itself, not the title beneath it.\n */\n.remarque-post-header {\n margin-block-end: var(--space-7);\n}\n\n.remarque-post-kicker {\n margin-block-end: var(--space-3);\n}\n",
58
58
  "type": "remarque:css"
59
59
  },
60
60
  {
@@ -4,8 +4,8 @@
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.25.0",
8
- "integrity": "sha256-zr04H0+0xf01OWLk6B2A9ziZOYJd4Sq5q5/cib65lbU=",
7
+ "version": "0.27.0",
8
+ "integrity": "sha256-pKYm8/x5kXsq6pvu5h+vifVWxR8uwoaU8Yrg4zHbEwQ=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"
11
11
  ],
@@ -25,7 +25,6 @@
25
25
  "--motion-easing",
26
26
  "--motion-fast",
27
27
  "--space-1",
28
- "--space-10",
29
28
  "--space-11",
30
29
  "--space-12",
31
30
  "--space-2",
@@ -44,7 +43,7 @@
44
43
  "files": [
45
44
  {
46
45
  "path": "essay.css",
47
- "content": "/*\n * Remarque — Essay Module (sidenotes + sticky TOC rail)\n * ───────────────────────────────────────────────────────\n * Optional Essay-archetype module: footnotes become in-flow margin notes\n * (Tufte/gwern-style), and an \"on this page\" table of contents pins to a\n * sticky rail alongside the reading column. Both are progressive\n * enhancements over a plain Essay page — CSS-only, no JavaScript required\n * at either breakpoint.\n *\n * PROVENANCE — graduated, not invented here. This module re-expresses, in\n * Remarque's own token vocabulary, a design first built and shipped on\n * williamzujkowski.github.io (`astro-site/src/lib/rehype-sidenotes.mjs` +\n * `global.css`'s \"Sidenotes\"/\"Sticky TOC side rail\" blocks). Ratified for\n * upstreaming in issue #52. No site-specific value (the flagship's literal\n * `11.5rem`/`13rem`/`14rem`/`1280px` measurements) was copied — every\n * dimension below is re-derived from tokens already in tokens-core.css /\n * tokens-palette.css. See REMARQUE.md's \"Essay Module\" section for the\n * full markup contract, and this file's inline comments for exactly which\n * site value maps to which token and why a few mechanics changed shape\n * (CSS counters instead of a build-time numbering step; `<details open>`\n * by default instead of a JS breakpoint listener) on the way upstream.\n *\n * Own subpath (remarque-tokens/essay, remarque-tokens/essay.css) — NOT\n * included by the tokens.css aggregator, and not required by prose.css:\n * plenty of Essay pages want neither sidenotes nor a TOC rail. Import\n * explicitly, after prose.css:\n * @import 'remarque-tokens/prose.css';\n * @import 'remarque-tokens/essay.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <article class=\"remarque-essay\">\n * <header class=\"content-reading\">...</header>\n *\n * <nav class=\"remarque-toc-rail\" aria-label=\"Table of contents\">\n * <details open>\n * <summary>On this page</summary>\n * <ul>\n * <li><a href=\"#section-one\">Section One</a></li>\n * </ul>\n * </details>\n * </nav>\n *\n * <div class=\"remarque-prose content-reading\">\n * <p>\n * A claim worth a citation<a href=\"#note-1\" id=\"note-1-ref\"\n * class=\"remarque-sidenote-ref\" aria-describedby=\"note-1\"></a>.\n * </p>\n * <aside class=\"remarque-sidenote\" id=\"note-1\" role=\"note\">\n * The note text, in real DOM order right after the paragraph that\n * cites it — not collected into an end-of-document section.\n * </aside>\n * </div>\n *\n * <footer class=\"content-reading\">...</footer>\n * </article>\n *\n * DOM order is the accessibility requirement, not a presentational nicety:\n * a screen reader hits a sidenote's content exactly where its reference\n * occurs. `.remarque-sidenote-ref` and its matching `.remarque-sidenote`\n * MUST alternate in strict DOM order (ref, then note, then the next ref,\n * then its note...) — the counter mechanics below depend on it. `role=\n * \"note\"` keeps a page with several sidenotes from scattering that many\n * unlabeled `complementary` landmarks through the document (`<aside>`'s\n * implicit role) — the same fix the flagship's rehype plugin documents.\n *\n * This module is deliberately silent on HOW that markup gets produced. A\n * build-time transform (the flagship's rehype-sidenotes.mjs is one worked\n * example, not a dependency of this package) or hand-authored HTML both\n * satisfy the contract equally — see REMARQUE.md for the no-build-step\n * fallback (`.remarque-footnotes`) if you don't have one.\n *\n * ─── BREAKPOINT ─────────────────────────────────────────────────────────\n * Gated on >= 80rem (1280px, at the platform default 16px root) viewport\n * width — the same threshold the flagship ratified, re-derived here from\n * this package's own tokens instead of asserted as a bare magic number:\n * `--content-standard` (72rem) is the widest a Remarque page's content\n * ever grows, and the rail/gutter tracks below need `--space-11`+\n * `--space-12` (10-12rem) and `--space-10` (8rem) of clearance on top of\n * `--content-reading` (46rem default) plus two `--space-6` gaps — call it\n * roughly 70-76rem of content width before both tracks have their\n * documented minimum room, plus headroom for a scrollbar and window\n * chrome. 80rem clears that with margin to spare across every approved\n * body-face/measure pairing (see REMARQUE.md's Measure Compensation\n * table) without needing a `--breakpoint-*` token this package doesn't\n * otherwise have. Below it: sidenotes render as small-print inline blocks\n * exactly where the plain document flow already places them, and the TOC\n * is a collapsible `<details>` in the reading column — this is also the\n * ONLY rendering a consumer who skips this module's `@media` block ever\n * sees, so nothing here is required for the page to work.\n */\n\n/* ─── Sidenotes — default / narrow-viewport mode ────────────────────────\n * `.remarque-sidenote` renders as a bordered small-print block right in\n * the text flow by default. This IS the narrow-viewport fallback the\n * spec calls for — zero extra markup, zero JS: the note just isn't\n * floated into a margin yet.\n *\n * Numbering is generated, not authored: `counter-reset` on\n * `.remarque-prose`, `counter-increment` on `.remarque-sidenote-ref` only,\n * and `.remarque-sidenote` simply READING that same counter (no increment\n * of its own — see the comment on that rule) keep the visible number in\n * sync across the reference and its note with no number typed into\n * either element — the flagship's rehype plugin injects a literal\n * `<span class=\"sidenote-number\">1</span>`/text node at build time; this\n * package ships no build step, so CSS counters do the same job for a\n * pure-CSS module. A repeat citation of an already-noted footnote should\n * use `.remarque-sidenote-ref--repeat` (no `.remarque-sidenote` follows\n * it) so the shared counter isn't advanced for a note that was already\n * printed — mirrors the flagship's \"only the first reference gets a\n * sidenote\" rule.\n */\n.remarque-prose {\n counter-reset: remarque-sidenote;\n}\n\n.remarque-sidenote-ref {\n counter-increment: remarque-sidenote;\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n /* USWDS floor rationale: --text-micro is reserved for timestamps/fine\n print only (tokens-core.css, REMARQUE.md \"Font Size Floors\") — the\n flagship's own marker used that size, but this module holds to\n Remarque's stricter 14px floor for anything read as content. */\n line-height: 0;\n vertical-align: super;\n color: var(--color-accent);\n text-decoration: none;\n}\n\n.remarque-sidenote-ref::after {\n content: counter(remarque-sidenote);\n}\n\n.remarque-sidenote-ref:hover {\n color: var(--color-accent-hover);\n text-decoration: underline;\n}\n\n.remarque-sidenote-ref--repeat {\n counter-increment: none;\n}\n\n.remarque-sidenote {\n /* No counter-increment here: `.remarque-sidenote-ref` already advanced\n `remarque-sidenote` immediately before this element in DOM order —\n incrementing again would double-count every pair (ref shows N, the\n note that follows it would show N+1 instead of N). This element only\n READS the counter its ref just set. */\n display: block;\n margin-block: var(--space-3);\n padding-inline-start: var(--space-3);\n /* border-bold: this is the note's ONLY visual separator from running\n prose at narrow widths, so it must clear the 3:1 functional-border\n minimum (WCAG 1.4.11) — same reasoning as .remarque-prose blockquote. */\n border-inline-start: var(--border-width) var(--border-style) var(--color-border-bold);\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n color: var(--color-fg-muted);\n}\n\n/* The generated number precedes whatever content the note holds. If that\n content is a single run of inline text (no wrapping <p>), the number\n sits inline with the opening words — the flagship's \"1 Note text...\"\n pairing. A `<p>`-wrapped note (multi-paragraph notes need it) instead\n reads as a small label above the paragraph — a pure-CSS module can't\n reach inside an arbitrary child the way a build-time transform can, so\n this is the honest degradation: prefer bare text for single-line notes. */\n.remarque-sidenote::before {\n content: counter(remarque-sidenote) \"\\2002\";\n color: var(--color-accent);\n font-weight: var(--weight-semibold);\n}\n\n.remarque-sidenote :where(p) {\n margin: 0;\n}\n\n.remarque-sidenote :where(p) + :where(p) {\n margin-top: var(--space-2);\n}\n\n/* ─── Sidenotes — wide-viewport mode: float into the left margin ────────\n * Classic Tufte CSS technique: `.remarque-sidenote` isn't a grid child\n * itself (it's nested inside `.remarque-prose`, which IS grid-column 2 —\n * see `.remarque-essay` below) so grid placement doesn't apply to it\n * directly. Instead it floats left with a negative inline-start margin\n * exactly wide enough to clear its own box plus one column-gap unit,\n * landing in the grid's column-1 gutter. `clear: left` stacks consecutive\n * notes without any JS-computed offset.\n *\n * Width (`--space-10`, 8rem) and pull-out distance (that width plus\n * `--space-6`, the same gap the grid below uses between tracks) are both\n * drawn from the core spacing scale rather than asserting a bespoke\n * sidebar-width value — this is the token-vocabulary re-expression of the\n * flagship's hand-measured `11.5rem`/`13rem` pair.\n */\n@media (min-width: 80rem) {\n .remarque-sidenote {\n float: left;\n clear: left;\n width: var(--space-10);\n margin-inline-start: calc(-1 * (var(--space-10) + var(--space-6)));\n margin-block: var(--space-1) var(--space-4);\n padding-inline-start: 0;\n padding-inline-end: var(--space-2);\n border-inline-start: none;\n /* Decorative-only at this width (the gutter's own whitespace is the\n real separator now) — `--color-border`, not `-bold`, matching the\n flagship's own choice to relax the border once it's no longer the\n sole boundary cue. */\n border-inline-end: var(--border-width) var(--border-style) var(--color-border);\n text-align: start;\n }\n}\n\n/* ─── Footnotes fallback (no relocation build step) ─────────────────────\n * `.remarque-footnotes` styles a traditional end-of-document footnotes\n * section — e.g. GFM/mdast-util-to-hast's default\n * `<section data-footnotes><h2>Footnotes</h2><ol>...</ol></section>`\n * output — in the same meta voice as `.remarque-sidenote`, for a consumer\n * who hasn't built (or doesn't want) a DOM-reordering transform. It is a\n * same-voice fallback, not a lesser feature: use `.remarque-sidenote`\n * whenever you can relocate notes into DOM reading order (the real a11y\n * upgrade this module exists for); reach for `.remarque-footnotes` only\n * when that isn't available yet.\n *\n * <section class=\"remarque-footnotes\">\n * <h2>Notes</h2>\n * <ol>\n * <li id=\"note-1\">The note text. <a href=\"#note-1-ref\">&#8617;</a></li>\n * </ol>\n * </section>\n */\n.remarque-footnotes {\n margin-top: var(--space-7);\n padding-top: var(--space-3);\n border-top: var(--border-width) var(--border-style) var(--color-border);\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n color: var(--color-fg-muted);\n}\n\n.remarque-footnotes h2 {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-caps);\n font-variant-caps: all-small-caps;\n color: var(--color-muted);\n margin: 0 0 var(--space-3);\n}\n\n.remarque-footnotes ol {\n padding-inline-start: var(--space-5);\n margin: 0;\n}\n\n.remarque-footnotes li + li {\n margin-top: var(--space-2);\n}\n\n.remarque-footnotes a {\n color: var(--color-accent);\n text-decoration: none;\n}\n\n.remarque-footnotes a:hover {\n color: var(--color-accent-hover);\n text-decoration: underline;\n}\n\n/* ─── Sticky TOC rail ────────────────────────────────────────────────────\n * `.remarque-toc-rail` wraps a plain collapsible `<details>` — the SAME\n * markup at every viewport, no JS breakpoint listener. Author it\n * `<details open>` by default: below the rail breakpoint this is an\n * inline collapsible block a reader can close to save space (native\n * disclosure behavior, unchanged); at/above it, it reads as an\n * already-expanded sticky rail. The flagship instead runs a small inline\n * script that force-opens the native `open` attribute on every breakpoint\n * crossing — this module holds to \"no JS required,\" so it authors the\n * open state up front instead of syncing it at runtime. The one\n * degradation this trades away: a reader who manually collapses the TOC\n * below the breakpoint and then widens their window keeps it collapsed\n * until they click it again — a fully static, CSS-only cost worth paying\n * for not requiring a script tag.\n *\n * Marker: reuses the same quiet mono disclosure triangle as\n * `.remarque-prose details summary::before` (prose.css) rather than\n * asking consumers to embed an inline SVG chevron, as the flagship's\n * TableOfContents.astro does.\n */\n.remarque-toc-rail {\n margin-block: var(--space-5);\n}\n\n.remarque-toc-rail details {\n border: none;\n padding: 0;\n}\n\n.remarque-toc-rail summary {\n cursor: pointer;\n list-style: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-2);\n padding-block: var(--space-2);\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n /* Mono meta voice — same declarations as .text-label (tokens-core.css),\n repeated rather than shared because .text-label isn't a mixin: true\n small caps, never text-transform: uppercase (REMARQUE.md \"Small\n Caps\"). The flagship predates that rule and used `text-transform:\n uppercase`; this is the one place the re-expression corrects the\n source rather than just relocating it. */\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n font-variant-caps: all-small-caps;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n transition: color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail summary::-webkit-details-marker {\n display: none;\n}\n\n.remarque-toc-rail summary::before {\n content: \"\\25B8\";\n display: inline-block;\n color: var(--color-muted);\n transition: transform var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail details[open] summary::before {\n transform: rotate(90deg);\n}\n\n.remarque-toc-rail summary:hover {\n color: var(--color-fg);\n}\n\n.remarque-toc-rail ul {\n list-style: none;\n padding: 0;\n margin: var(--space-3) 0 0;\n}\n\n.remarque-toc-rail li + li {\n margin-top: var(--space-1);\n}\n\n.remarque-toc-rail a {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n color: var(--color-fg-muted);\n text-decoration: none;\n transition: color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail a:hover {\n color: var(--color-fg);\n}\n\n/* Forced-colors fallback (issue #93 audit finding): the TOC rail's\n summary toggle and its links both signal hover with a color-only\n change (`--color-muted`/`--color-fg-muted` -> `--color-fg`). Structure\n itself survives forced-colors for free (`.remarque-toc-rail summary`'s\n `border-bottom` and `.remarque-sidenote`'s `border-inline-*` both use\n real `border-color`, forced to a visible system border regardless of\n the authored value — see REMARQUE.md \"Forced Colors & Contrast\n Preferences\"), so this block is scoped narrowly to the two hover\n affordances that have no non-color signal at all otherwise. */\n@media (forced-colors: active) {\n .remarque-toc-rail summary:hover,\n .remarque-toc-rail a:hover {\n text-decoration: underline;\n }\n}\n\n/* ─── Essay grid shell — wide-viewport rail layout ───────────────────────\n * `.remarque-essay` is the opt-in grid container an Essay page's <article>\n * adds to place a sticky TOC rail beside the reading column, mirroring\n * this module's sidenote gutter on the OTHER side (right rail = TOC, left\n * gutter = sidenotes — never the same side, so the two mechanisms never\n * fight over space). Below the breakpoint it is a no-op: every child\n * keeps whatever block-flow centering it already had from `.content-\n * reading` / `.remarque-prose` — this rule only exists inside the\n * `@media` block.\n *\n * Track widths: the center track is `minmax(0, var(--content-reading))` —\n * IDENTICAL to the plain `max-width: content-reading; margin-inline:\n * auto` centering used everywhere else (same visual column, just a grid\n * track instead of block flow), so nothing here changes the reading\n * measure the \"prose measure\" computed-style check already covers. The\n * right track (`minmax(--space-11, --space-12)`, 10-12rem) is this\n * module's re-expression of the flagship's bespoke `minmax(12rem, 14rem)`\n * rail — same shape (a comfortable range, not a fixed width), core\n * spacing tokens instead of asserted rem literals.\n *\n * Children are placed by exclusion (`:not(.remarque-toc-rail)` -> column\n * 2) rather than an explicit tag list, deliberately: nothing about this\n * rule touches `max-width` or `margin-inline` on any child, so each\n * element's own `.content-reading` / `.remarque-prose` centering keeps\n * working exactly as it does outside a grid — the grid track is already\n * sized to match it.\n */\n@media (min-width: 80rem) {\n .remarque-essay {\n display: grid;\n grid-template-columns:\n minmax(0, 1fr)\n minmax(0, var(--content-reading))\n minmax(var(--space-11), var(--space-12));\n column-gap: var(--space-6);\n }\n\n .remarque-essay > :not(.remarque-toc-rail) {\n grid-column: 2;\n }\n\n .remarque-essay > .remarque-toc-rail {\n grid-column: 3;\n /* `1 / -1` resolves to row 1 ONLY when the grid has no explicit\n `grid-template-rows` (as here) — `-1` means \"the last explicit\n row line,\" which with zero explicit rows IS row 1. That inflates\n row 1 to the rail's own height instead of spanning the real\n content rows, shoving everything after it down a gap the height\n of the TOC. (Exactly the bug the flagship shipped and fixed,\n 2026-07-20 — captured here so this module doesn't reintroduce\n it.) A generous explicit span covers any Essay's block count;\n empty implicit rows stay 0-height. */\n grid-row: 1 / span 999;\n align-self: start;\n justify-self: start;\n position: sticky;\n /* --z-sticky (tokens-core.css, issue #29) — never a bare z-index\n number in consumer CSS. */\n z-index: var(--z-sticky);\n top: var(--space-6);\n width: 100%;\n max-height: calc(100vh - var(--space-8));\n overflow-y: auto;\n }\n}\n",
46
+ "content": "/*\n * Remarque — Essay Module (sidenotes + sticky TOC rail)\n * ───────────────────────────────────────────────────────\n * Optional Essay-archetype module: footnotes become in-flow margin notes\n * (Tufte/gwern-style), and an \"on this page\" table of contents pins to a\n * sticky rail alongside the reading column. Both are progressive\n * enhancements over a plain Essay page — CSS-only, no JavaScript required\n * at either breakpoint.\n *\n * PROVENANCE — graduated, not invented here. This module re-expresses, in\n * Remarque's own token vocabulary, a design first built and shipped on\n * williamzujkowski.github.io (`astro-site/src/lib/rehype-sidenotes.mjs` +\n * `global.css`'s \"Sidenotes\"/\"Sticky TOC side rail\" blocks). Ratified for\n * upstreaming in issue #52. No site-specific value (the flagship's literal\n * `11.5rem`/`13rem`/`14rem`/`1280px` measurements) was copied — every\n * dimension below is re-derived from tokens already in tokens-core.css /\n * tokens-palette.css. See REMARQUE.md's \"Essay Module\" section for the\n * full markup contract, and this file's inline comments for exactly which\n * site value maps to which token and why a few mechanics changed shape\n * (CSS counters instead of a build-time numbering step; `<details open>`\n * by default instead of a JS breakpoint listener) on the way upstream.\n *\n * Own subpath (remarque-tokens/essay, remarque-tokens/essay.css) — NOT\n * included by the tokens.css aggregator, and not required by prose.css:\n * plenty of Essay pages want neither sidenotes nor a TOC rail. Import\n * explicitly, after prose.css:\n * @import 'remarque-tokens/prose.css';\n * @import 'remarque-tokens/essay.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <article class=\"remarque-essay\">\n * <header class=\"content-reading\">...</header>\n *\n * <nav class=\"remarque-toc-rail\" aria-label=\"Table of contents\">\n * <details open>\n * <summary>On this page</summary>\n * <ul>\n * <li><a href=\"#section-one\">Section One</a></li>\n * </ul>\n * </details>\n * </nav>\n *\n * <div class=\"remarque-prose content-reading\">\n * <p>\n * A claim worth a citation<a href=\"#note-1\" id=\"note-1-ref\"\n * class=\"remarque-sidenote-ref\" aria-describedby=\"note-1\"></a>.\n * </p>\n * <aside class=\"remarque-sidenote\" id=\"note-1\" role=\"note\">\n * The note text, in real DOM order right after the paragraph that\n * cites it — not collected into an end-of-document section.\n * </aside>\n * </div>\n *\n * <footer class=\"content-reading\">...</footer>\n * </article>\n *\n * DOM order is the accessibility requirement, not a presentational nicety:\n * a screen reader hits a sidenote's content exactly where its reference\n * occurs. `.remarque-sidenote-ref` and its matching `.remarque-sidenote`\n * MUST alternate in strict DOM order (ref, then note, then the next ref,\n * then its note...) — the counter mechanics below depend on it. `role=\n * \"note\"` keeps a page with several sidenotes from scattering that many\n * unlabeled `complementary` landmarks through the document (`<aside>`'s\n * implicit role) — the same fix the flagship's rehype plugin documents.\n *\n * This module is deliberately silent on HOW that markup gets produced. A\n * build-time transform (the flagship's rehype-sidenotes.mjs is one worked\n * example, not a dependency of this package) or hand-authored HTML both\n * satisfy the contract equally — see REMARQUE.md for the no-build-step\n * fallback (`.remarque-footnotes`) if you don't have one.\n *\n * ─── BREAKPOINT ─────────────────────────────────────────────────────────\n * Gated on >= 80rem (1280px, at the platform default 16px root) viewport\n * width — the same threshold the flagship ratified, re-derived here from\n * this package's own tokens instead of asserted as a bare magic number:\n * `--content-standard` (72rem) is the widest a Remarque page's content\n * ever grows, and the rail/gutter tracks below need `--space-11`+\n * `--space-12` (10-12rem) and `--space-11` (10rem) of clearance on top of\n * `--content-reading` (38rem default; up to 46rem for wider faces) plus two\n * `--space-6` gaps — call it roughly 62-76rem of content width before both tracks have their\n * documented minimum room, plus headroom for a scrollbar and window\n * chrome. 80rem clears that with margin to spare across every approved\n * body-face/measure pairing (see REMARQUE.md's Measure Compensation\n * table) without needing a `--breakpoint-*` token this package doesn't\n * otherwise have. Below it: sidenotes render as small-print inline blocks\n * exactly where the plain document flow already places them, and the TOC\n * is a collapsible `<details>` in the reading column — this is also the\n * ONLY rendering a consumer who skips this module's `@media` block ever\n * sees, so nothing here is required for the page to work.\n */\n\n/* ─── Sidenotes — default / narrow-viewport mode ────────────────────────\n * `.remarque-sidenote` renders as a bordered small-print block right in\n * the text flow by default. This IS the narrow-viewport fallback the\n * spec calls for — zero extra markup, zero JS: the note just isn't\n * floated into a margin yet.\n *\n * Numbering is generated, not authored: `counter-reset` on\n * `.remarque-prose`, `counter-increment` on `.remarque-sidenote-ref` only,\n * and `.remarque-sidenote` simply READING that same counter (no increment\n * of its own — see the comment on that rule) keep the visible number in\n * sync across the reference and its note with no number typed into\n * either element — the flagship's rehype plugin injects a literal\n * `<span class=\"sidenote-number\">1</span>`/text node at build time; this\n * package ships no build step, so CSS counters do the same job for a\n * pure-CSS module. A repeat citation of an already-noted footnote should\n * use `.remarque-sidenote-ref--repeat` (no `.remarque-sidenote` follows\n * it) so the shared counter isn't advanced for a note that was already\n * printed — mirrors the flagship's \"only the first reference gets a\n * sidenote\" rule.\n */\n.remarque-prose {\n counter-reset: remarque-sidenote;\n}\n\n.remarque-sidenote-ref {\n counter-increment: remarque-sidenote;\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n /* USWDS floor rationale: --text-micro is reserved for timestamps/fine\n print only (tokens-core.css, REMARQUE.md \"Font Size Floors\") — the\n flagship's own marker used that size, but this module holds to\n Remarque's stricter 14px floor for anything read as content. */\n line-height: 0;\n vertical-align: super;\n color: var(--color-accent);\n text-decoration: none;\n}\n\n.remarque-sidenote-ref::after {\n content: counter(remarque-sidenote);\n}\n\n.remarque-sidenote-ref:hover {\n color: var(--color-accent-hover);\n text-decoration: underline;\n}\n\n.remarque-sidenote-ref--repeat {\n counter-increment: none;\n}\n\n.remarque-sidenote {\n /* No counter-increment here: `.remarque-sidenote-ref` already advanced\n `remarque-sidenote` immediately before this element in DOM order —\n incrementing again would double-count every pair (ref shows N, the\n note that follows it would show N+1 instead of N). This element only\n READS the counter its ref just set. */\n display: block;\n margin-block: var(--space-3);\n padding-inline-start: var(--space-3);\n /* border-bold: this is the note's ONLY visual separator from running\n prose at narrow widths, so it must clear the 3:1 functional-border\n minimum (WCAG 1.4.11) — same reasoning as .remarque-prose blockquote. */\n border-inline-start: var(--border-width) var(--border-style) var(--color-border-bold);\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n color: var(--color-fg-muted);\n}\n\n/* The generated number precedes whatever content the note holds. If that\n content is a single run of inline text (no wrapping <p>), the number\n sits inline with the opening words — the flagship's \"1 Note text...\"\n pairing. A `<p>`-wrapped note (multi-paragraph notes need it) instead\n reads as a small label above the paragraph — a pure-CSS module can't\n reach inside an arbitrary child the way a build-time transform can, so\n this is the honest degradation: prefer bare text for single-line notes. */\n.remarque-sidenote::before {\n content: counter(remarque-sidenote) \"\\2002\";\n color: var(--color-accent);\n font-weight: var(--weight-semibold);\n}\n\n.remarque-sidenote :where(p) {\n margin: 0;\n}\n\n.remarque-sidenote :where(p) + :where(p) {\n margin-top: var(--space-2);\n}\n\n/* ─── Sidenotes — wide-viewport mode: float into the left margin ────────\n * Classic Tufte CSS technique: `.remarque-sidenote` isn't a grid child\n * itself (it's nested inside `.remarque-prose`, which IS grid-column 2 —\n * see `.remarque-essay` below) so grid placement doesn't apply to it\n * directly. Instead it floats left with a negative inline-start margin\n * exactly wide enough to clear its own box plus one column-gap unit,\n * landing in the grid's column-1 gutter. `clear: left` stacks consecutive\n * notes without any JS-computed offset.\n *\n * Width (`--space-11`, 10rem) and pull-out distance (that width plus\n * `--space-6`, the same gap the grid below uses between tracks) are both\n * drawn from the core spacing scale rather than asserting a bespoke\n * sidebar-width value — this is the token-vocabulary re-expression of the\n * flagship's hand-measured `11.5rem`/`13rem` pair. (Was `--space-10`, 8rem,\n * until 0.27: about 15 mono characters per line, so a two-sentence note\n * ran 19 lines tall — 2026-10 design review. 10rem still clears the\n * 80rem breakpoint with a 46rem reading column.)\n */\n@media (min-width: 80rem) {\n .remarque-sidenote {\n float: left;\n clear: left;\n width: var(--space-11);\n margin-inline-start: calc(-1 * (var(--space-11) + var(--space-6)));\n margin-block: var(--space-1) var(--space-4);\n padding-inline-start: 0;\n padding-inline-end: var(--space-2);\n border-inline-start: none;\n /* Decorative-only at this width (the gutter's own whitespace is the\n real separator now) — `--color-border`, not `-bold`, matching the\n flagship's own choice to relax the border once it's no longer the\n sole boundary cue. */\n border-inline-end: var(--border-width) var(--border-style) var(--color-border);\n text-align: start;\n }\n}\n\n/* ─── Footnotes fallback (no relocation build step) ─────────────────────\n * `.remarque-footnotes` styles a traditional end-of-document footnotes\n * section — e.g. GFM/mdast-util-to-hast's default\n * `<section data-footnotes><h2>Footnotes</h2><ol>...</ol></section>`\n * output — in the same meta voice as `.remarque-sidenote`, for a consumer\n * who hasn't built (or doesn't want) a DOM-reordering transform. It is a\n * same-voice fallback, not a lesser feature: use `.remarque-sidenote`\n * whenever you can relocate notes into DOM reading order (the real a11y\n * upgrade this module exists for); reach for `.remarque-footnotes` only\n * when that isn't available yet.\n *\n * <section class=\"remarque-footnotes\">\n * <h2>Notes</h2>\n * <ol>\n * <li id=\"note-1\">The note text. <a href=\"#note-1-ref\">&#8617;</a></li>\n * </ol>\n * </section>\n */\n.remarque-footnotes {\n margin-top: var(--space-7);\n padding-top: var(--space-3);\n border-top: var(--border-width) var(--border-style) var(--color-border);\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n color: var(--color-fg-muted);\n}\n\n.remarque-footnotes h2 {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-caps);\n font-variant-caps: all-small-caps;\n color: var(--color-muted);\n margin: 0 0 var(--space-3);\n}\n\n.remarque-footnotes ol {\n padding-inline-start: var(--space-5);\n margin: 0;\n}\n\n.remarque-footnotes li + li {\n margin-top: var(--space-2);\n}\n\n.remarque-footnotes a {\n color: var(--color-accent);\n text-decoration: none;\n}\n\n.remarque-footnotes a:hover {\n color: var(--color-accent-hover);\n text-decoration: underline;\n}\n\n/* ─── Sticky TOC rail ────────────────────────────────────────────────────\n * `.remarque-toc-rail` wraps a plain collapsible `<details>` — the SAME\n * markup at every viewport, no JS breakpoint listener. Author it\n * `<details open>` by default: below the rail breakpoint this is an\n * inline collapsible block a reader can close to save space (native\n * disclosure behavior, unchanged); at/above it, it reads as an\n * already-expanded sticky rail. The flagship instead runs a small inline\n * script that force-opens the native `open` attribute on every breakpoint\n * crossing — this module holds to \"no JS required,\" so it authors the\n * open state up front instead of syncing it at runtime. The one\n * degradation this trades away: a reader who manually collapses the TOC\n * below the breakpoint and then widens their window keeps it collapsed\n * until they click it again — a fully static, CSS-only cost worth paying\n * for not requiring a script tag.\n *\n * Marker: reuses the same quiet mono disclosure triangle as\n * `.remarque-prose details summary::before` (prose.css) rather than\n * asking consumers to embed an inline SVG chevron, as the flagship's\n * TableOfContents.astro does.\n */\n.remarque-toc-rail {\n margin-block: var(--space-5);\n}\n\n.remarque-toc-rail details {\n border: none;\n padding: 0;\n}\n\n.remarque-toc-rail summary {\n cursor: pointer;\n list-style: none;\n display: flex;\n align-items: center;\n /* Marker sits beside its label, not at the far end of the row — a\n triangle stranded 190px from its text reads as a stray glyph and\n the row stops reading as a control (2026-10 design review). */\n justify-content: flex-start;\n gap: var(--space-2);\n min-height: 44px; /* touch-target floor — same shape as .nav-link */\n padding-block: var(--space-2);\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n /* Mono meta voice — same declarations as .text-label (tokens-core.css),\n repeated rather than shared because .text-label isn't a mixin: true\n small caps, never text-transform: uppercase (REMARQUE.md \"Small\n Caps\"). The flagship predates that rule and used `text-transform:\n uppercase`; this is the one place the re-expression corrects the\n source rather than just relocating it. */\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n font-variant-caps: all-small-caps;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n transition: color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail summary::-webkit-details-marker {\n display: none;\n}\n\n.remarque-toc-rail summary::before {\n content: \"\\25B8\";\n display: inline-block;\n color: var(--color-muted);\n transition: transform var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail details[open] summary::before {\n transform: rotate(90deg);\n}\n\n.remarque-toc-rail summary:hover {\n color: var(--color-fg);\n}\n\n.remarque-toc-rail ul {\n list-style: none;\n padding: 0;\n margin: var(--space-3) 0 0;\n}\n\n/* No li + li margin: each link's own 44px min-height is the rhythm. */\n\n.remarque-toc-rail a {\n /* Block-level flex so every entry is a full-width, >= 44px touch\n target (AGENT_RULES.md quality checklist) — these are navigation,\n not inline links in running text, so the WCAG 2.5.8 inline\n exception doesn't cover them. Multi-line entries grow past the\n floor naturally. */\n display: flex;\n align-items: center;\n min-height: 44px;\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n color: var(--color-fg-muted);\n text-decoration: none;\n transition: color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail a:hover {\n color: var(--color-fg);\n}\n\n/* Forced-colors fallback (issue #93 audit finding): the TOC rail's\n summary toggle and its links both signal hover with a color-only\n change (`--color-muted`/`--color-fg-muted` -> `--color-fg`). Structure\n itself survives forced-colors for free (`.remarque-toc-rail summary`'s\n `border-bottom` and `.remarque-sidenote`'s `border-inline-*` both use\n real `border-color`, forced to a visible system border regardless of\n the authored value — see REMARQUE.md \"Forced Colors & Contrast\n Preferences\"), so this block is scoped narrowly to the two hover\n affordances that have no non-color signal at all otherwise. */\n@media (forced-colors: active) {\n .remarque-toc-rail summary:hover,\n .remarque-toc-rail a:hover {\n text-decoration: underline;\n }\n}\n\n/* ─── Essay grid shell — wide-viewport rail layout ───────────────────────\n * `.remarque-essay` is the opt-in grid container an Essay page's <article>\n * adds to place a sticky TOC rail beside the reading column, mirroring\n * this module's sidenote gutter on the OTHER side (right rail = TOC, left\n * gutter = sidenotes — never the same side, so the two mechanisms never\n * fight over space). Below the breakpoint it is a no-op: every child\n * keeps whatever block-flow centering it already had from `.content-\n * reading` / `.remarque-prose` — this rule only exists inside the\n * `@media` block.\n *\n * Track widths: the center track is `minmax(0, var(--content-reading))` —\n * IDENTICAL to the plain `max-width: content-reading; margin-inline:\n * auto` centering used everywhere else (same visual column, just a grid\n * track instead of block flow), so nothing here changes the reading\n * measure the \"prose measure\" computed-style check already covers. The\n * right track (`minmax(--space-11, --space-12)`, 10-12rem) is this\n * module's re-expression of the flagship's bespoke `minmax(12rem, 14rem)`\n * rail — same shape (a comfortable range, not a fixed width), core\n * spacing tokens instead of asserted rem literals.\n *\n * Children are placed by exclusion (`:not(.remarque-toc-rail)` -> column\n * 2) rather than an explicit tag list, deliberately: nothing about this\n * rule touches `max-width` or `margin-inline` on any child, so each\n * element's own `.content-reading` / `.remarque-prose` centering keeps\n * working exactly as it does outside a grid — the grid track is already\n * sized to match it.\n */\n@media (min-width: 80rem) {\n .remarque-essay {\n display: grid;\n grid-template-columns:\n minmax(0, 1fr)\n minmax(0, var(--content-reading))\n minmax(var(--space-11), var(--space-12));\n column-gap: var(--space-6);\n }\n\n .remarque-essay > :not(.remarque-toc-rail) {\n grid-column: 2;\n }\n\n .remarque-essay > .remarque-toc-rail {\n grid-column: 3;\n /* `1 / -1` resolves to row 1 ONLY when the grid has no explicit\n `grid-template-rows` (as here) — `-1` means \"the last explicit\n row line,\" which with zero explicit rows IS row 1. That inflates\n row 1 to the rail's own height instead of spanning the real\n content rows, shoving everything after it down a gap the height\n of the TOC. (Exactly the bug the flagship shipped and fixed,\n 2026-07-20 — captured here so this module doesn't reintroduce\n it.) A generous explicit span covers any Essay's block count;\n empty implicit rows stay 0-height. */\n grid-row: 1 / span 999;\n align-self: start;\n justify-self: start;\n position: sticky;\n /* --z-sticky (tokens-core.css, issue #29) — never a bare z-index\n number in consumer CSS. */\n z-index: var(--z-sticky);\n top: var(--space-6);\n max-height: calc(100vh - var(--space-8));\n overflow-y: auto;\n /* overflow-y makes the rail a scroll container, which clips anything\n painted outside its padding box — including the 2px ring +\n 2px offset on a focused link or summary (issue #115). Pad the\n container past the ring's reach and pull it back by the same\n amount so the rail's text stays on the track edge. */\n box-sizing: border-box;\n padding: var(--space-2);\n margin-inline-start: calc(-1 * var(--space-2));\n width: calc(100% + var(--space-2));\n }\n}\n",
48
47
  "type": "remarque:css"
49
48
  },
50
49
  {
@@ -4,7 +4,7 @@
4
4
  "type": "remarque:contract",
5
5
  "title": "Forms Primitives",
6
6
  "description": "Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.",
7
- "version": "0.25.0",
7
+ "version": "0.27.0",
8
8
  "integrity": "sha256-WfYjcf04Dxxx7X21U5b6DrY90Qw95p0zkCQszoh77tE=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"
@@ -4,7 +4,7 @@
4
4
  "type": "remarque:contract",
5
5
  "title": "Palette Deck (markup contract only — no executable content)",
6
6
  "description": "Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.",
7
- "version": "0.25.0",
7
+ "version": "0.27.0",
8
8
  "integrity": "sha256-WGgBZ2j0VnLPRAZlzv/4G1Wu9rKqTdXf1536dWWu8nU=",
9
9
  "dependencies": [
10
10
  "remarque-tokens"
package/registry.json CHANGED
@@ -2,15 +2,15 @@
2
2
  "$schema": "https://williamzujkowski.github.io/remarque/registry.schema.json",
3
3
  "name": "remarque-tokens",
4
4
  "homepage": "https://williamzujkowski.github.io/remarque/",
5
- "version": "0.25.0",
5
+ "version": "0.27.0",
6
6
  "items": [
7
7
  {
8
8
  "name": "essay",
9
9
  "type": "remarque:contract",
10
10
  "title": "Essay Module",
11
11
  "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).",
12
- "version": "0.25.0",
13
- "integrity": "sha256-zr04H0+0xf01OWLk6B2A9ziZOYJd4Sq5q5/cib65lbU=",
12
+ "version": "0.27.0",
13
+ "integrity": "sha256-pKYm8/x5kXsq6pvu5h+vifVWxR8uwoaU8Yrg4zHbEwQ=",
14
14
  "file": "registry/essay.json"
15
15
  },
16
16
  {
@@ -18,8 +18,8 @@
18
18
  "type": "remarque:contract",
19
19
  "title": "Broadsheet Pattern",
20
20
  "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 true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.",
21
- "version": "0.25.0",
22
- "integrity": "sha256-BZea38PRdob4OfmdGN0eDw3U0adl7SEsdrjKgSewK9A=",
21
+ "version": "0.27.0",
22
+ "integrity": "sha256-aweD48hO4kVgvjm2ZFC8u124Az9AvrJ/zribu5BPZ/g=",
23
23
  "file": "registry/broadsheet.json"
24
24
  },
25
25
  {
@@ -27,7 +27,7 @@
27
27
  "type": "remarque:contract",
28
28
  "title": "Forms Primitives",
29
29
  "description": "Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.",
30
- "version": "0.25.0",
30
+ "version": "0.27.0",
31
31
  "integrity": "sha256-WfYjcf04Dxxx7X21U5b6DrY90Qw95p0zkCQszoh77tE=",
32
32
  "file": "registry/forms.json"
33
33
  },
@@ -36,7 +36,7 @@
36
36
  "type": "remarque:contract",
37
37
  "title": "Palette Deck (markup contract only — no executable content)",
38
38
  "description": "Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.",
39
- "version": "0.25.0",
39
+ "version": "0.27.0",
40
40
  "integrity": "sha256-WGgBZ2j0VnLPRAZlzv/4G1Wu9rKqTdXf1536dWWu8nU=",
41
41
  "file": "registry/palette-deck.json"
42
42
  }
package/scripts/audit.mjs CHANGED
@@ -54,7 +54,7 @@ const report = {
54
54
  passed: false,
55
55
  contrast: [],
56
56
  gamut: [],
57
- srcScans: { fontFloor: [], unverifiableFontSize: [], hardcodedColors: [], oklchLiteral: [] },
57
+ srcScans: { fontFloor: [], unverifiableFontSize: [], hardcodedColors: [], oklchLiteral: [], ambiguousTextSize: [] },
58
58
  failures: [],
59
59
  };
60
60
 
@@ -271,6 +271,16 @@ for (const file of walk(SRC)) {
271
271
  report.srcScans.oklchLiteral.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
272
272
  fail(`${rel}:${i + 1} oklch() literal outside token files: ${line.trim().slice(0, 80)}`);
273
273
  }
274
+ // 4. Tailwind v4 ambiguous arbitrary value: `text-[var(--text-meta)]`
275
+ // cannot be type-inferred from a bare var(), so Tailwind emits it as a
276
+ // COLOR (`color: var(--text-meta)`) — the size never applies and the
277
+ // invalid color clobbers the element's real color utility. No build
278
+ // error, no audit signal until now. The size form needs the `length:`
279
+ // hint: `text-[length:var(--text-meta)]` (or `text-(length:--text-meta)`).
280
+ for (const m of line.matchAll(/\btext-(\[var\(--text-[\w-]+\)\]|\(--text-[\w-]+\))/g)) {
281
+ report.srcScans.ambiguousTextSize.push({ file: rel, line: i + 1, snippet: m[0] });
282
+ fail(`${rel}:${i + 1} ambiguous Tailwind ${m[0]} compiles to a color, not a font-size — write text-[length:var(--text-…)]`);
283
+ }
274
284
  });
275
285
  }
276
286
 
@@ -1,7 +1,8 @@
1
1
  /*
2
- * Shared table + tiny helpers for the two Claude Code skills this package
3
- * ships and packages — used by scripts/build-skills.mjs (prepack copy +
4
- * CI freshness gate) and scripts/test-skills.mjs (content validation).
2
+ * Shared table + tiny helpers for the three Claude Code skills this
3
+ * package ships and packages — used by scripts/build-skills.mjs (prepack
4
+ * copy + CI freshness gate) and scripts/test-skills.mjs (content
5
+ * validation).
5
6
  *
6
7
  * `.claude/skills/<name>/SKILL.md` is the ONE canonical, hand-authored
7
8
  * copy of each skill (also what a local Claude Code session in this repo
@@ -24,6 +25,12 @@ export const SKILLS = [
24
25
  packaged: 'skills/remarque-adopt/SKILL.md',
25
26
  exportName: './skills/adopt',
26
27
  },
28
+ {
29
+ name: 'remarque-new-page',
30
+ canonical: '.claude/skills/remarque-new-page/SKILL.md',
31
+ packaged: 'skills/remarque-new-page/SKILL.md',
32
+ exportName: './skills/new-page',
33
+ },
27
34
  ];
28
35
 
29
36
  /*
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: remarque-new-page
3
+ description: >
4
+ Build a new page in a project that consumes `remarque-tokens` — pick the
5
+ right archetype, fetch registry items instead of transcribing markup,
6
+ wire the page per AGENT_RULES pitfalls, and gate on remarque-audit plus
7
+ the registry's own markup-contract assertions. Use when the user says
8
+ "new page", "add a page", "build an essay", "build an archive page",
9
+ "build a landing page", "use an archetype", or asks for a page in a
10
+ Remarque-consuming project. Companion to the `remarque` loader skill —
11
+ load that one first for the tier rules and the two build-time pitfalls;
12
+ this skill is the page-building procedure on top of it, not a
13
+ restatement of it.
14
+ license: MIT
15
+ ---
16
+
17
+ # remarque-new-page
18
+
19
+ Five steps, each ending in a gate. This sits on top of the `remarque`
20
+ loader skill and does not restate its tier rules or pitfalls — it applies
21
+ them to the specific job of adding one new page.
22
+
23
+ ## 1. Load the loader skill's three files
24
+
25
+ Run the `remarque` skill first. If skill resolution isn't available in
26
+ this context, read `AGENT_RULES.md`, `REMARQUE.md`, and `tokens.json`
27
+ yourself — see that skill's file table for the consumer-project vs.
28
+ in-repo paths. Do not build from training-data memory of token values or
29
+ tier rules; both change across releases and the current values live only
30
+ in those three files.
31
+
32
+ - Gate: you can name which tier (`core`/`palette`) governs the tokens
33
+ this page will touch, and you have the CURRENT `tokens.json` open —
34
+ not a cached mental model from an older version.
35
+
36
+ ## 2. Pick the archetype
37
+
38
+ `REMARQUE.md`'s "Page Archetypes" section defines seven: Essay, Project
39
+ Dossier, Notebook, Landing, Reference/Docs, Changelog, Gallery. Match the
40
+ request to exactly one — "essay"/"blog post" → Essay; "archive"/"index of
41
+ posts" → Landing (often paired with the Broadsheet pattern below);
42
+ "docs"/"reference" → Reference/Docs. Do not invent an eighth.
43
+
44
+ - Gate: you can quote the chosen archetype's "Always includes"/"Never
45
+ includes" lists from the spec, not paraphrase them from memory.
46
+
47
+ ## 3. Module-backed pages: fetch the registry item, verify, apply
48
+
49
+ If the archetype uses one of the four registry-backed modules — Essay
50
+ (sidenotes/TOC rail), Broadsheet (Landing/archive pattern), Forms, or
51
+ Palette Deck — do not transcribe markup from REMARQUE.md's prose. That is
52
+ the exact failure mode issue #89 documents: a hand-copied sidenote
53
+ `aria-label` drifted out of DOM order. Fetch the item instead:
54
+
55
+ ```bash
56
+ cat node_modules/remarque-tokens/registry/essay.json # or broadsheet/forms/palette-deck
57
+ # or, if not installed locally:
58
+ curl -s https://williamzujkowski.github.io/remarque/registry/essay.json
59
+ ```
60
+
61
+ Then **verify integrity before applying anything** — recompute the sha256
62
+ over the fetched item's `files[].content` and compare it to that item's
63
+ own `integrity` field (both are the pin recorded in `registry.json`'s
64
+ index):
65
+
66
+ ```bash
67
+ node -e '
68
+ const fs = require("fs"), crypto = require("crypto");
69
+ const item = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
70
+ const bytes = Buffer.from(item.files.map(f => f.content).join(""));
71
+ console.log("sha256-" + crypto.createHash("sha256").update(bytes).digest("base64"));
72
+ ' node_modules/remarque-tokens/registry/essay.json
73
+ # compare stdout, byte for byte, to that same file's "integrity" field
74
+ ```
75
+
76
+ Apply `files[].content` verbatim — the `remarque:css` entry as the CSS
77
+ module, the `remarque:markup` entry's `usage.html` as the page's starting
78
+ DOM — rather than retyping either from memory.
79
+
80
+ - Gate: the computed hash matches `integrity` exactly. A mismatch means
81
+ stop and re-fetch; never apply an unverified payload.
82
+
83
+ ## 4. Wire it per AGENT_RULES pitfalls
84
+
85
+ Three mistakes pass a green build with no warning, so verify them
86
+ explicitly:
87
+
88
+ - **Unlayered tokens import (Pitfall #7).** Never `@import
89
+ "remarque-tokens" layer(...)` — layering makes every `@theme inline`
90
+ mapped utility circular and invalid.
91
+ - **String-form `@import` only (Pitfall #6).** `@import './tokens.css';`,
92
+ never `@import url(...)` — Tailwind v4/Lightning CSS silently drops the
93
+ latter for local files. Verify the built CSS contains `.remarque-prose`
94
+ after touching any import.
95
+ - **`.remarque-prose` + `.content-reading` pairing.** `.remarque-prose`
96
+ supplies typography only; it does not center the column. Every reading
97
+ column needs both classes together, and (Pitfall #2) the page's header
98
+ must share the same centered measure the prose body uses, or the two
99
+ misalign.
100
+
101
+ - Gate: grep the built CSS for `.remarque-prose` (proves the import
102
+ survived), and confirm every prose block in the new page carries
103
+ `content-reading` alongside it.
104
+
105
+ ## 5. Acceptance
106
+
107
+ ```bash
108
+ npx remarque-audit --palette <file> --src <dir> --json
109
+ ```
110
+
111
+ Requires `passed: true` in the JSON — not "looks close." If the page used
112
+ a registry module, also re-check it against that module's own
113
+ markup-contract rule from AGENT_RULES.md's Quality Checklist: Essay's
114
+ sidenote/TOC-rail DOM-order + `aria-label` rule, Broadsheet's
115
+ `data-entry-number`/`attr()` + small-caps rule, Forms' label/
116
+ `aria-describedby` pairing rule, or Palette Deck's `data-palette`/
117
+ `data-theme` co-location rule.
118
+
119
+ - Gate: `remarque-audit --json` reports `passed: true`, AND the
120
+ module-specific markup-contract rule above holds. Either failing means
121
+ the page isn't done.
@@ -82,7 +82,7 @@ module.exports = {
82
82
  * container (e.g. a lead paragraph).
83
83
  */
84
84
  maxWidth: {
85
- "reading": "var(--content-reading)", // 46rem — prose
85
+ "reading": "var(--content-reading)", // 38rem — prose
86
86
  "standard": "var(--content-standard)", // 72rem — project pages
87
87
  "wide": "var(--content-wide)", // 88rem — outer container
88
88
  },
package/theme.css CHANGED
@@ -28,6 +28,15 @@
28
28
  * (mapped below from --motion-easing) — without it Tailwind falls back
29
29
  * to its default `ease` timing function instead of the token (issue #31).
30
30
  *
31
+ * Type-scale sizes: there are deliberately NO --text-* mappings here —
32
+ * a Tailwind text-meta/text-body utility would collide with the
33
+ * unlayered .text-meta/.text-body semantic classes in tokens-core.css
34
+ * (which also set font-family and color, and always win). For a size
35
+ * alone use an arbitrary value WITH the length: hint —
36
+ * text-[length:var(--text-meta)]. The bare text-[var(--text-meta)] form
37
+ * is ambiguous to Tailwind, compiles to `color: var(--text-meta)`, and
38
+ * silently applies no size (remarque-audit fails on it).
39
+ *
31
40
  * Z-index utilities: Tailwind v4 ships z-* as a static, non-themable
32
41
  * scale (there is no --z-index-* entry in its default @theme, so there
33
42
  * is nothing here to map into). Use arbitrary values with the tokens
@@ -47,8 +47,11 @@
47
47
  --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", "Fira Code", monospace;
48
48
 
49
49
  /* Reading measure — tuned to the body font (see REMARQUE.md measure table).
50
- 46rem ≈ 70 characters per line at 17px Inter. */
51
- --content-reading: 46rem;
50
+ 38rem ≈ 71 characters per line at 17px Inter — MEASURED (median of
51
+ rendered body lines), not computed from the `ch` unit, which is the
52
+ width of "0" and overstates Inter's average glyph (46rem, the pre-0.27
53
+ default, measured a median of 86). */
54
+ --content-reading: 38rem;
52
55
 
53
56
  /* Display weight — palette-tier because it is theme-conditional:
54
57
  light-on-dark text renders optically thinner (halation), so dark
package/tokens.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Remarque design tokens — GENERATED from tokens.json by
3
- * scripts/tokens-json.mjs (v0.25.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.27.0). Do not edit — the CSS
4
4
  * (tokens-core.css + tokens-palette.css) is the source of truth;
5
5
  * tokens.json is the intermediate machine-readable form this file is
6
6
  * generated from. Regenerate with: node scripts/tokens-json.mjs
@@ -255,8 +255,8 @@ export interface RemarqueTokenValues {
255
255
  readonly dark: "\"JetBrains Mono\", ui-monospace, \"Cascadia Code\", \"Fira Code\", monospace";
256
256
  };
257
257
  readonly 'content-reading': {
258
- readonly light: "46rem";
259
- readonly dark: "46rem";
258
+ readonly light: "38rem";
259
+ readonly dark: "38rem";
260
260
  };
261
261
  readonly 'weight-display': {
262
262
  readonly light: 400;
package/tokens.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
4
4
  "$extensions": {
5
5
  "remarque": {
6
- "version": "0.25.0",
6
+ "version": "0.27.0",
7
7
  "tiers": {
8
8
  "core": "immutable identity — overriding forks the system",
9
9
  "palette": "sanctioned personalization surface — override freely, then run remarque-audit"
@@ -270,10 +270,10 @@
270
270
  "content-reading": {
271
271
  "$type": "dimension",
272
272
  "light": {
273
- "$value": "46rem"
273
+ "$value": "38rem"
274
274
  },
275
275
  "dark": {
276
- "$value": "46rem",
276
+ "$value": "38rem",
277
277
  "$extensions": {
278
278
  "remarque": {
279
279
  "inheritedFromLight": true