remarque-tokens 0.26.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 +8 -4
- package/CHANGELOG.md +26 -0
- package/README.md +2 -2
- package/REMARQUE.md +9 -7
- package/broadsheet.css +22 -16
- package/essay.css +33 -12
- package/package.json +1 -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/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
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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
|
+
|
|
7
33
|
## 0.26.0 — 2026-07-23
|
|
8
34
|
|
|
9
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.
|
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.
|
|
@@ -146,7 +146,7 @@ See the [live demo](https://williamzujkowski.github.io/remarque/) for the origin
|
|
|
146
146
|
|----------|-------|-----|
|
|
147
147
|
| Body size | 17px | Measurably improves reading comfort over 16px default |
|
|
148
148
|
| Line height | 1.75 | Generous — this is what makes prose readable |
|
|
149
|
-
| Reading width |
|
|
149
|
+
| Reading width | 38rem | ~71 measured chars/line at 17px Inter (median; p90 76) |
|
|
150
150
|
| Color space | OKLCH | Perceptual uniformity. Consistent across hues without manual tuning |
|
|
151
151
|
| Top padding | 6rem min | The breathing room that says "this is editorial, not a dashboard" |
|
|
152
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
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/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
|