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 +15 -8
- package/CHANGELOG.md +39 -0
- package/README.md +8 -6
- package/REMARQUE.md +9 -7
- package/broadsheet.css +22 -16
- package/essay.css +33 -12
- package/package.json +2 -1
- package/prose.css +13 -0
- package/registry/broadsheet.json +3 -3
- package/registry/essay.json +3 -4
- package/registry/forms.json +1 -1
- package/registry/palette-deck.json +1 -1
- package/registry.json +7 -7
- package/scripts/audit.mjs +11 -1
- package/scripts/lib/skills.mjs +10 -3
- package/skills/remarque-new-page/SKILL.md +121 -0
- package/tailwind.config.js +1 -1
- package/theme.css +9 -0
- package/tokens-palette.css +5 -2
- package/tokens.d.ts +3 -3
- package/tokens.json +3 -3
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` (
|
|
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 (
|
|
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
|
|
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 ≤
|
|
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
|
|
329
|
-
`remarque-tokens/skills/remarque
|
|
330
|
-
|
|
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
|
|
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
|
-
- **
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 |
|
|
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 —
|
|
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` |
|
|
166
|
+
| Body face | `--content-reading` | Measured at 17px (median / p90 chars per line) |
|
|
167
167
|
|-----------|--------------------:|-----------|
|
|
168
|
-
| Inter (default) |
|
|
169
|
-
| system-ui stack |
|
|
170
|
-
| Source Serif 4 |
|
|
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
|
|
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:
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
336
|
-
-
|
|
337
|
-
|
|
338
|
-
|
|
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-
|
|
81
|
-
* `--content-reading` (
|
|
82
|
-
* roughly
|
|
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-
|
|
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-
|
|
205
|
-
margin-inline-start: calc(-1 * (var(--space-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 ───────────────────────────────────── */
|
package/registry/broadsheet.json
CHANGED
|
@@ -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.
|
|
8
|
-
"integrity": "sha256-
|
|
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\">·</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</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\">·</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</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\">·</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</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\">·</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</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
|
{
|
package/registry/essay.json
CHANGED
|
@@ -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.
|
|
8
|
-
"integrity": "sha256-
|
|
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\">↩</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\">↩</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
|
{
|
package/registry/forms.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
13
|
-
"integrity": "sha256-
|
|
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.
|
|
22
|
-
"integrity": "sha256-
|
|
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.
|
|
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.
|
|
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
|
|
package/scripts/lib/skills.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Shared table + tiny helpers for the
|
|
3
|
-
* ships and packages — used by scripts/build-skills.mjs (prepack
|
|
4
|
-
* CI freshness gate) and scripts/test-skills.mjs (content
|
|
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.
|
package/tailwind.config.js
CHANGED
|
@@ -82,7 +82,7 @@ module.exports = {
|
|
|
82
82
|
* container (e.g. a lead paragraph).
|
|
83
83
|
*/
|
|
84
84
|
maxWidth: {
|
|
85
|
-
"reading": "var(--content-reading)", //
|
|
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
|
package/tokens-palette.css
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
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.
|
|
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: "
|
|
259
|
-
readonly dark: "
|
|
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.
|
|
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": "
|
|
273
|
+
"$value": "38rem"
|
|
274
274
|
},
|
|
275
275
|
"dark": {
|
|
276
|
-
"$value": "
|
|
276
|
+
"$value": "38rem",
|
|
277
277
|
"$extensions": {
|
|
278
278
|
"remarque": {
|
|
279
279
|
"inheritedFromLight": true
|