remarque-tokens 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -4,6 +4,90 @@ 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.7.0 — 2026-07-21
8
+
9
+ Typographic-completeness release (design review, #50/#51/#54): the
10
+ prose element vocabulary, the editorial microtypography layer, and a
11
+ print stylesheet — three gaps the review found between what Remarque
12
+ claims to be ("rooted in book typography") and what it shipped.
13
+
14
+ ### Added
15
+ - **Prose vocabulary completion** (#50): `.remarque-prose` now styles
16
+ every remaining Markdown element — table/th/td/caption (+
17
+ `.scroll-wrap` overflow container, mono th with a 2px bottom rule,
18
+ 1px row rules, no zebra, `.num` opt-in for tabular-lining numeric
19
+ cells), dl/dt/dd (serif-italic term over a muted description, the
20
+ "specimen list" pattern), h4–h6 (a descent through the three font
21
+ slots), kbd (mono, 1px border), mark (`--color-accent-subtle`'s first
22
+ sanctioned use), abbr[title] (dotted underline), sup/sub (footnote-
23
+ marker-safe sizing), and details/summary (quiet mono triangle
24
+ marker). No Markdown element renders unstyled after this release.
25
+ - **Editorial microtypography** (#51): the numerals policy
26
+ (`--text-meta` → tabular-nums lining-nums; `.remarque-prose` →
27
+ oldstyle-nums proportional-nums — REMARQUE.md:179 mandated this but
28
+ no CSS ever implemented it); `.text-label` (true small caps via
29
+ `font-variant-caps`, replacing `text-transform: uppercase`);
30
+ `.remarque-prose--dropcap` (opt-in Essay first-paragraph drop cap);
31
+ `.pullquote` (display italic, hairline rules, distinct from
32
+ blockquote); wrapping/optical defaults (`text-wrap: balance` on
33
+ headings, `text-wrap: pretty` + `hanging-punctuation` on prose,
34
+ `font-synthesis: none`, `font-optical-sizing: auto`). New "Editorial
35
+ Microtypography" spec section in REMARQUE.md.
36
+ - **`remarque-tokens/print.css`** (#54): a print layer scoped entirely
37
+ inside `@page`/`@media print` — forces the light palette with true
38
+ black-on-white (ink economy), hides nav/footer/TOC/theme-toggle/skip-
39
+ link, expands content to full page width at 2cm margins, sets body
40
+ to 11pt, appends `(url)` after external links, and applies
41
+ orphans/widows/break-inside discipline to figures/tables/pre. Own
42
+ subpath — not included by the `tokens.css` aggregator; import
43
+ explicitly.
44
+ - **Drift-detection CI** (#47/#48, consensus-armed now that 2+ sites
45
+ consume the package — williamzujkowski.github.io and tsundoku both pull
46
+ `remarque-tokens` from npm): `scripts/drift-check.mjs` (shipped in the
47
+ package; `files` + a `remarque-drift` bin entry) compares a consumer's
48
+ stylesheet against the INSTALLED package's `tokens.json`. Reuses
49
+ `scripts/lib/css-tokens.mjs` rather than a third CSS parser.
50
+ - CORE-tier token redefined with a different value → **FAIL** (exit 1),
51
+ unless the consumer's `DESIGN-DEVIATIONS.md` or `DESIGN-NOTES.md`
52
+ names the token — then **WARN** with a pointer to that doc.
53
+ - PALETTE-tier divergence → **INFO** (sanctioned personalization,
54
+ listed, never blocking).
55
+ - CORE-tier token consumed only via `var()`, never redeclared → no-op
56
+ (it resolves from the package import).
57
+ - Also prints the installed `remarque-tokens` version.
58
+ - `.github/workflows/token-drift.yml`: reusable `workflow_call` wrapper
59
+ consumers can call cross-repo (`css-file`, `package-dir` inputs).
60
+ - `scripts/test-drift.mjs`: fixture tests for the classification rules
61
+ (9 cases), wired into `deploy.yml` alongside `test-audit.mjs`.
62
+
63
+ ### Changed
64
+ - `scripts/audit.mjs`'s token-file classification now recognizes
65
+ `print.css` (it carries literal `oklch()` values for the forced
66
+ print palette, same as a palette file).
67
+ - Demo site (`site/`): specimen page exercises the new table/dl/kbd/
68
+ mark/abbr/sup-sub/details vocabulary; the five faux-uppercase eyebrow
69
+ labels (`index.astro` x3, `tokens.astro`, `projects/remarque.astro`)
70
+ now use `.text-label`; `against-decoration.astro` demonstrates the
71
+ opt-in drop cap (`<Prose dropcap>`) and a `.pullquote`.
72
+
73
+ No core-tier token values changed in this release.
74
+
75
+ ## 0.6.1 — 2026-07-21
76
+
77
+ ### Fixed
78
+ Three audit/parser bugs from real-world adoption (#67, found by tsundoku):
79
+ - Body-less at-statements (`@import`, `@charset`) preceding a block no
80
+ longer pollute its prelude and misclassify it as an at-rule — palette
81
+ files may begin with imports.
82
+ - Selector classification accepts qualified root forms (`html:root`,
83
+ `html[data-theme="dark"]`, `body.dark`, …) instead of exact strings.
84
+ - `--src` scans now strip HTML `<!-- -->` comments (line-numbers
85
+ preserved) like CSS comments, ending issue-number false positives in
86
+ .astro/.svelte markup.
87
+
88
+ Two new convention fixtures (leading-import, qualified-selectors) lock
89
+ the fixes in CI. Consumers can drop their documented workarounds.
90
+
7
91
  ## 0.6.0 — 2026-07-20
8
92
 
9
93
  ### Added
package/REMARQUE.md CHANGED
@@ -100,6 +100,50 @@ This is why the measure is palette-tier: it is a property of the font choice, no
100
100
 
101
101
  ---
102
102
 
103
+ ## Editorial Microtypography
104
+
105
+ The book-craft layer: numerals, small caps, drop caps, pull quotes, and optical wrapping. These are the details that separate typeset prose from a web page that merely uses nice fonts.
106
+
107
+ ### Numerals
108
+
109
+ Two numeral registers, chosen by context:
110
+
111
+ - **`.remarque-prose`** uses `font-variant-numeric: oldstyle-nums proportional-nums` — text figures that sit quietly inside a sentence, the way numerals behave in well-set book prose.
112
+ - **`.text-meta`** (and anything sharing its role — dates, counts, table cells, timestamps) uses `font-variant-numeric: tabular-nums lining-nums` — full-height, fixed-width digits that align in columns and don't jitter as they change.
113
+
114
+ Never mix the two: a numeral inside running prose should never be tabular, and a numeral in a metadata row or table should never be oldstyle. Apply `.num` to any `<td>`/`<th>` holding numeric data to opt that cell into the tabular-lining register (see prose.css's table rules).
115
+
116
+ ### Small Caps
117
+
118
+ `font-variant-caps: all-small-caps` — not `text-transform: uppercase` — for any label that wants a caps-like treatment. Faux uppercase renders letters at cap-height with no optical correction and reads louder than intended; true small caps render at x-height with corrected weight and spacing. `.text-label` (mono, `--text-meta`, all-small-caps, `--tracking-caps`, `--color-muted`) is the sanctioned utility. `text-transform: uppercase` is disallowed anywhere a label could use `.text-label` instead.
119
+
120
+ ### Drop Caps (opt-in)
121
+
122
+ `.remarque-prose--dropcap`, applied alongside `.remarque-prose`, floats a large italic display-face initial letter on the first paragraph only — the way a printed essay opens its first page. Rules:
123
+
124
+ - Essay first paragraph only — never mid-article, never in Dossier/Notebook/Landing content.
125
+ - Uses `initial-letter` where supported, with a `float` + font-size fallback for browsers that lack it.
126
+ - Inherits `--color-fg` — the drop cap is not a place for accent color.
127
+ - Skipped automatically when the paragraph opens with a quote or inline code (the ornament assumes a plain capital letter).
128
+ - Opt-in only. Most pages should not use it; it is reserved for essays that want a printed, literary register.
129
+
130
+ ### Pull Quotes
131
+
132
+ `.pullquote` is not `.remarque-prose blockquote`. A blockquote cites someone else; a pullquote re-states the article's own words as a visual accent — a magazine convention. Rules:
133
+
134
+ - Display italic at `--text-section` size, not blockquote's body-italic.
135
+ - Hairline rules above and below (`--color-border`), no background, no accent color.
136
+ - If the pulled text duplicates body copy verbatim, mark the element `<aside aria-hidden="true">` so screen readers don't hear it twice.
137
+ - At most one per screen — it is a rare accent, not a recurring motif.
138
+
139
+ ### Wrapping & Optical Defaults
140
+
141
+ - Headings: `text-wrap: balance`, `font-optical-sizing: auto`, `font-synthesis: none` — headings never wrap ragged, optical sizing lets variable fonts pick the right grade, and browsers never fake bold/italic for faces that don't have them.
142
+ - Prose: `text-wrap: pretty` (avoids single-word orphan lines) and `hanging-punctuation: first last` (quotes and punctuation hang outside the measure rather than indenting the line).
143
+ - All four properties degrade safely — unsupported browsers silently ignore them and fall back to normal wrapping. Invisible when absent, unmistakable when present.
144
+
145
+ ---
146
+
103
147
  ## Visual Rules
104
148
 
105
149
  ### Do
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -22,10 +22,12 @@
22
22
  "homepage": "https://williamzujkowski.github.io/remarque/",
23
23
  "main": "tokens.css",
24
24
  "bin": {
25
- "remarque-audit": "scripts/audit.mjs"
25
+ "remarque-audit": "scripts/audit.mjs",
26
+ "remarque-drift": "scripts/drift-check.mjs"
26
27
  },
27
28
  "scripts": {
28
- "audit": "node scripts/audit.mjs"
29
+ "audit": "node scripts/audit.mjs",
30
+ "drift": "node scripts/drift-check.mjs"
29
31
  },
30
32
  "exports": {
31
33
  ".": "./tokens.css",
@@ -43,6 +45,8 @@
43
45
  "./tokens.json": "./tokens.json",
44
46
  "./prose": "./prose.css",
45
47
  "./prose.css": "./prose.css",
48
+ "./print": "./print.css",
49
+ "./print.css": "./print.css",
46
50
  "./package.json": "./package.json"
47
51
  },
48
52
  "files": [
@@ -52,6 +56,7 @@
52
56
  "fonts.css",
53
57
  "fonts/",
54
58
  "scripts/audit.mjs",
59
+ "scripts/drift-check.mjs",
55
60
  "tailwind.config.js",
56
61
  "REMARQUE.md",
57
62
  "AGENT_RULES.md",
@@ -61,6 +66,7 @@
61
66
  "tokens.json",
62
67
  "scripts/tokens-json.mjs",
63
68
  "prose.css",
69
+ "print.css",
64
70
  "scripts/lib/css-tokens.mjs"
65
71
  ]
66
72
  }
package/print.css ADDED
@@ -0,0 +1,140 @@
1
+ /*
2
+ * Remarque — Print Stylesheet
3
+ * ────────────────────────────
4
+ * A "rooted in book typography" system that prints screen chrome and
5
+ * dead links has not earned the claim. This file is scoped entirely
6
+ * inside @page/@media print — it has zero effect on screen rendering,
7
+ * so it is safe to import unconditionally alongside tokens.css.
8
+ *
9
+ * Own subpath (remarque-tokens/print, remarque-tokens/print.css) —
10
+ * NOT included by the tokens.css aggregator, since not every consumer
11
+ * wants it wired in automatically. Import explicitly:
12
+ * @import 'remarque-tokens/print.css';
13
+ * (string form — see AGENT_RULES.md Pitfall #6.)
14
+ *
15
+ * Selectors for screen chrome (nav/footer/TOC/theme toggle/skip link)
16
+ * target common conventions (<nav>, <footer>, #theme-toggle, the
17
+ * "Skip to content" anchor) rather than any one consumer's exact
18
+ * markup — a site with different class names should add its own
19
+ * @media print rule for anything this file doesn't catch.
20
+ *
21
+ * Sidenotes: this package does not yet ship a sidenotes module. When
22
+ * one lands, its narrow-viewport "inline" mode is exactly the right
23
+ * print behavior — print should never attempt the wide-margin
24
+ * side-rail layout a sidenotes module might use on desktop screens.
25
+ */
26
+
27
+ @page {
28
+ margin: 2cm;
29
+ }
30
+
31
+ @media print {
32
+ /* ─── Force the light palette (ink economy) ───────────────
33
+ * True black on white, not the screen theme's warm near-black/
34
+ * off-white — printed pages are read in ambient light with no theme
35
+ * toggle available, and a warm off-white background prints as a
36
+ * visible ink wash on many printers. Overrides regardless of which
37
+ * dark-mode convention put the site in dark mode. */
38
+ :root,
39
+ [data-theme="light"],
40
+ [data-theme="dark"],
41
+ :root.dark {
42
+ --color-bg: oklch(1 0 0);
43
+ --color-bg-subtle: oklch(1 0 0);
44
+ --color-fg: oklch(0 0 0);
45
+ --color-fg-muted: oklch(0.25 0 0);
46
+ --color-muted: oklch(0.35 0 0);
47
+ --color-border: oklch(0.7 0 0);
48
+ --color-border-bold: oklch(0.4 0 0);
49
+ --color-surface: oklch(1 0 0);
50
+ --color-accent: oklch(0 0 0);
51
+ --color-accent-hover: oklch(0 0 0);
52
+ --color-accent-subtle: oklch(1 0 0);
53
+ --color-code-bg: oklch(0.96 0 0);
54
+ --color-code-fg: oklch(0 0 0);
55
+ --color-selection-bg: oklch(1 0 0);
56
+ --color-selection-fg: oklch(0 0 0);
57
+ --weight-display: 400;
58
+ }
59
+
60
+ html,
61
+ body {
62
+ background: oklch(1 0 0) !important;
63
+ color: oklch(0 0 0) !important;
64
+ }
65
+
66
+ * {
67
+ box-shadow: none !important;
68
+ text-shadow: none !important;
69
+ }
70
+
71
+ /* ─── Hide screen-only chrome ──────────────────────────────
72
+ * Navigation, footer, table of contents, theme toggle, skip link. */
73
+ nav,
74
+ footer,
75
+ .toc,
76
+ [class*="toc-"],
77
+ #theme-toggle,
78
+ [data-theme-toggle],
79
+ .theme-toggle,
80
+ a[href="#main-content"],
81
+ a[href^="#main"],
82
+ .skip-link,
83
+ .copy-btn {
84
+ display: none !important;
85
+ }
86
+
87
+ /* ─── Full-width reading column ────────────────────────────
88
+ * The page itself is already narrow at 2cm margins — don't also
89
+ * constrain content to the on-screen reading measure. */
90
+ .content-reading,
91
+ .content-standard,
92
+ .content-wide,
93
+ main {
94
+ max-width: 100% !important;
95
+ margin-inline: 0 !important;
96
+ }
97
+
98
+ body {
99
+ font-size: 11pt;
100
+ line-height: 1.5;
101
+ }
102
+
103
+ /* ─── External links keep their URL on paper ─────────────── */
104
+ a[href^="http"]::after {
105
+ content: " (" attr(href) ")";
106
+ font-family: var(--font-mono);
107
+ font-size: 0.75em;
108
+ color: oklch(0 0 0);
109
+ word-break: break-all;
110
+ }
111
+
112
+ /* ─── Orphans, widows, and page-break discipline ──────────── */
113
+ p,
114
+ li,
115
+ dd,
116
+ blockquote {
117
+ orphans: 2;
118
+ widows: 2;
119
+ }
120
+
121
+ h1,
122
+ h2,
123
+ h3,
124
+ h4,
125
+ h5,
126
+ h6 {
127
+ break-after: avoid;
128
+ orphans: 3;
129
+ widows: 3;
130
+ }
131
+
132
+ figure,
133
+ table,
134
+ pre,
135
+ .remarque-prose pre,
136
+ .remarque-prose table,
137
+ .remarque-prose details {
138
+ break-inside: avoid;
139
+ }
140
+ }
package/prose.css CHANGED
@@ -20,6 +20,14 @@
20
20
  line-height: var(--leading-body);
21
21
  color: var(--color-fg);
22
22
  max-width: var(--content-reading);
23
+ /* Oldstyle proportional numerals — text figures that sit quietly
24
+ inside a sentence, the way numerals behave in well-set book prose.
25
+ Never mixed with .text-meta's tabular lining register (issue #51). */
26
+ font-variant-numeric: oldstyle-nums proportional-nums;
27
+ /* Optical defaults: avoid single-word orphan lines, let quotes and
28
+ punctuation hang outside the measure. Both degrade safely. */
29
+ text-wrap: pretty;
30
+ hanging-punctuation: first last;
23
31
  }
24
32
 
25
33
  .remarque-prose > * + * {
@@ -137,6 +145,300 @@
137
145
  margin-top: var(--space-2);
138
146
  }
139
147
 
148
+ /* ─── Headings, continued (h4–h6) ───────────────────────── */
149
+ /*
150
+ * h2/h3 are defined above. h4–h6 complete the vocabulary with a
151
+ * deliberate descent through the three font slots — h4 stays in the
152
+ * body voice (still prose), h5 borrows the display face's italic for a
153
+ * quieter sub-heading, h6 drops into the mono "technical label" voice
154
+ * used everywhere else for the deepest level of metadata. No
155
+ * text-transform: uppercase — see .text-label (tokens-core.css) for
156
+ * the true small-caps alternative.
157
+ */
158
+
159
+ .remarque-prose h4 {
160
+ font-family: var(--font-body);
161
+ font-size: var(--text-body);
162
+ font-weight: var(--weight-semibold);
163
+ line-height: var(--leading-title);
164
+ margin-top: var(--space-5);
165
+ margin-bottom: var(--space-2);
166
+ color: var(--color-fg);
167
+ }
168
+
169
+ .remarque-prose h5 {
170
+ font-family: var(--font-display);
171
+ font-style: italic;
172
+ font-size: var(--text-body);
173
+ font-weight: var(--weight-medium);
174
+ line-height: var(--leading-title);
175
+ margin-top: var(--space-4);
176
+ margin-bottom: var(--space-2);
177
+ color: var(--color-fg);
178
+ }
179
+
180
+ .remarque-prose h6 {
181
+ font-family: var(--font-mono);
182
+ font-size: var(--text-meta);
183
+ letter-spacing: var(--tracking-meta);
184
+ font-weight: var(--weight-semibold);
185
+ line-height: var(--leading-meta);
186
+ margin-top: var(--space-4);
187
+ margin-bottom: var(--space-2);
188
+ color: var(--color-muted);
189
+ }
190
+
191
+ /* Optical/wrapping defaults for prose headings (issue #51) — see the
192
+ matching rule on .text-display/.text-title/.text-section in
193
+ tokens-core.css. */
194
+ .remarque-prose h2,
195
+ .remarque-prose h3,
196
+ .remarque-prose h4,
197
+ .remarque-prose h5,
198
+ .remarque-prose h6 {
199
+ text-wrap: balance;
200
+ font-optical-sizing: auto;
201
+ font-synthesis: none;
202
+ }
203
+
204
+ /* ─── Tables ─────────────────────────────────────────────── */
205
+ /*
206
+ * Reading-width tables: mono th voice (metadata surface, matching
207
+ * figcaption/code/pre), a 2px rule under the header row, 1px row rules
208
+ * only (no vertical rules, no zebra striping — quiet by rule). Numeric
209
+ * columns opt in to tabular lining figures with the `.num` class on the
210
+ * relevant <th>/<td> (keeps digits from jittering as they change,
211
+ * matches the .text-meta numerals policy).
212
+ *
213
+ * Wide tables should not force the reading column wider than
214
+ * --content-reading — wrap them:
215
+ * <div class="scroll-wrap"><table>...</table></div>
216
+ */
217
+
218
+ .remarque-prose .scroll-wrap {
219
+ overflow-x: auto;
220
+ -webkit-overflow-scrolling: touch;
221
+ margin-top: var(--space-5);
222
+ }
223
+
224
+ .remarque-prose .scroll-wrap > table {
225
+ margin-top: 0;
226
+ }
227
+
228
+ .remarque-prose table {
229
+ width: 100%;
230
+ border-collapse: collapse;
231
+ font-size: var(--text-body);
232
+ }
233
+
234
+ .remarque-prose caption {
235
+ font-family: var(--font-mono);
236
+ font-size: var(--text-meta);
237
+ letter-spacing: var(--tracking-meta);
238
+ color: var(--color-muted);
239
+ text-align: left;
240
+ caption-side: top;
241
+ margin-bottom: var(--space-2);
242
+ }
243
+
244
+ .remarque-prose th {
245
+ font-family: var(--font-mono);
246
+ font-size: var(--text-meta);
247
+ letter-spacing: var(--tracking-meta);
248
+ font-weight: var(--weight-semibold);
249
+ text-align: left;
250
+ padding: var(--space-2) var(--space-3);
251
+ border-bottom: 2px solid var(--color-border-bold);
252
+ color: var(--color-fg);
253
+ }
254
+
255
+ .remarque-prose td {
256
+ padding: var(--space-2) var(--space-3);
257
+ border-bottom: var(--border-width) var(--border-style) var(--color-border);
258
+ color: var(--color-fg);
259
+ }
260
+
261
+ .remarque-prose th.num,
262
+ .remarque-prose td.num {
263
+ font-variant-numeric: tabular-nums lining-nums;
264
+ text-align: right;
265
+ }
266
+
267
+ /* ─── Definition Lists ───────────────────────────────────── */
268
+ /*
269
+ * The "specimen list" pattern: a serif-italic term (dt) over a muted
270
+ * body-face description (dd), row rules like a table. Nest a
271
+ * `.text-label`/`.text-meta` span inside dt for a metadata line under
272
+ * the term — the utility already renders it in the mono voice, no
273
+ * extra CSS needed.
274
+ */
275
+
276
+ .remarque-prose dl {
277
+ border-top: var(--border-width) var(--border-style) var(--color-border);
278
+ }
279
+
280
+ .remarque-prose dt {
281
+ font-family: var(--font-display);
282
+ font-style: italic;
283
+ font-size: var(--text-body-lg);
284
+ line-height: var(--leading-title);
285
+ color: var(--color-fg);
286
+ margin-top: var(--space-4);
287
+ }
288
+
289
+ .remarque-prose dt:first-child {
290
+ margin-top: var(--space-4);
291
+ }
292
+
293
+ .remarque-prose dd {
294
+ margin-left: 0;
295
+ margin-top: var(--space-1);
296
+ padding-bottom: var(--space-4);
297
+ color: var(--color-fg-muted);
298
+ border-bottom: var(--border-width) var(--border-style) var(--color-border);
299
+ }
300
+
301
+ /* ─── kbd, mark, abbr, sup/sub ───────────────────────────── */
302
+
303
+ .remarque-prose kbd {
304
+ font-family: var(--font-mono);
305
+ font-size: 0.85em;
306
+ padding: 0.1em 0.4em;
307
+ border: var(--border-width) var(--border-style) var(--color-border);
308
+ border-radius: var(--radius-sm);
309
+ background-color: var(--color-surface);
310
+ color: var(--color-fg);
311
+ }
312
+
313
+ /* --color-accent-subtle's first sanctioned use (issue #50) — a quiet
314
+ highlight, not a callout background. */
315
+ .remarque-prose mark {
316
+ background-color: var(--color-accent-subtle);
317
+ color: var(--color-fg);
318
+ padding: 0.05em 0.2em;
319
+ border-radius: var(--radius-sm);
320
+ }
321
+
322
+ .remarque-prose abbr[title] {
323
+ text-decoration: underline dotted;
324
+ text-underline-offset: 0.2em;
325
+ cursor: help;
326
+ }
327
+
328
+ .remarque-prose sup,
329
+ .remarque-prose sub {
330
+ font-size: 0.75em;
331
+ line-height: 0;
332
+ position: relative;
333
+ vertical-align: baseline;
334
+ }
335
+
336
+ .remarque-prose sup {
337
+ top: -0.5em;
338
+ }
339
+
340
+ .remarque-prose sub {
341
+ bottom: -0.25em;
342
+ }
343
+
344
+ /* ─── Details / Summary ──────────────────────────────────── */
345
+ /*
346
+ * Collapsible asides. Quiet marker: a small triangle in the mono
347
+ * metadata voice, no default browser disclosure widget.
348
+ */
349
+
350
+ .remarque-prose details {
351
+ border-top: var(--border-width) var(--border-style) var(--color-border);
352
+ padding-top: var(--space-3);
353
+ padding-bottom: var(--space-1);
354
+ }
355
+
356
+ .remarque-prose summary {
357
+ font-family: var(--font-mono);
358
+ font-size: var(--text-meta);
359
+ letter-spacing: var(--tracking-meta);
360
+ color: var(--color-muted);
361
+ cursor: pointer;
362
+ list-style: none;
363
+ }
364
+
365
+ .remarque-prose summary::-webkit-details-marker {
366
+ display: none;
367
+ }
368
+
369
+ .remarque-prose summary::before {
370
+ content: "\25B8";
371
+ display: inline-block;
372
+ margin-right: var(--space-2);
373
+ color: var(--color-muted);
374
+ transition: transform var(--motion-fast) var(--motion-easing);
375
+ }
376
+
377
+ .remarque-prose details[open] summary::before {
378
+ transform: rotate(90deg);
379
+ }
380
+
381
+ .remarque-prose details > :not(summary) {
382
+ margin-top: var(--space-3);
383
+ }
384
+
385
+
386
+ /* ─── Drop Cap (opt-in) ──────────────────────────────────── */
387
+ /*
388
+ * Essay first paragraph only. Add alongside .remarque-prose:
389
+ * <div class="remarque-prose remarque-prose--dropcap">
390
+ * `initial-letter` is used where supported, with a float + font-size
391
+ * fallback for browsers that lack it. Inherits --color-fg — the drop
392
+ * cap is not a place for accent color. Automatically skipped when the
393
+ * paragraph opens with a quote or inline code, since the ornament
394
+ * assumes a plain capital letter.
395
+ */
396
+
397
+ .remarque-prose--dropcap > p:first-of-type:not(:has(> q:first-child)):not(:has(> code:first-child))::first-letter {
398
+ font-family: var(--font-display);
399
+ font-style: italic;
400
+ font-size: 3.5em;
401
+ line-height: 0.85;
402
+ float: left;
403
+ padding: 0.08em 0.12em 0 0;
404
+ color: var(--color-fg);
405
+ }
406
+
407
+ @supports (initial-letter: 4) or (-webkit-initial-letter: 4) {
408
+ .remarque-prose--dropcap > p:first-of-type:not(:has(> q:first-child)):not(:has(> code:first-child))::first-letter {
409
+ initial-letter: 3.5;
410
+ -webkit-initial-letter: 3.5;
411
+ float: none;
412
+ padding: 0 0.08em 0 0;
413
+ }
414
+ }
415
+
416
+ /* ─── Pull Quote ─────────────────────────────────────────── */
417
+ /*
418
+ * Not .remarque-prose blockquote — a blockquote cites someone else; a
419
+ * pullquote re-states the article's own words as a visual accent (a
420
+ * magazine convention). Display italic at section size, hairline rules
421
+ * above/below, no background, no accent color. If the pulled text
422
+ * duplicates body copy verbatim, mark the element aria-hidden so
423
+ * screen readers don't hear it twice:
424
+ * <aside class="pullquote" aria-hidden="true">...</aside>
425
+ * At most one per screen.
426
+ */
427
+
428
+ .pullquote {
429
+ font-family: var(--font-display);
430
+ font-style: italic;
431
+ font-size: var(--text-section);
432
+ line-height: var(--leading-section);
433
+ color: var(--color-fg);
434
+ text-align: center;
435
+ max-width: var(--content-reading);
436
+ margin: var(--space-7) auto;
437
+ padding: var(--space-4) 0;
438
+ border-top: var(--border-width) var(--border-style) var(--color-border);
439
+ border-bottom: var(--border-width) var(--border-style) var(--color-border);
440
+ }
441
+
140
442
 
141
443
  /* ─── The Remarque Mark ─────────────────────────────────── */
142
444
  /*
package/scripts/audit.mjs CHANGED
@@ -157,7 +157,7 @@ const FONT_FLOOR_REM = 0.8125, FONT_FLOOR_PX = 13;
157
157
  const paletteRel = relative('.', PALETTE);
158
158
  const isTokenFile = (p) =>
159
159
  p === paletteRel ||
160
- /tokens(-core|-palette)?\.css$|fonts\.css$|globals?\.css$|theme-deck\.css$|pages[\/\\]tokens\.astro$/.test(p);
160
+ /tokens(-core|-palette)?\.css$|fonts\.css$|globals?\.css$|theme-deck\.css$|print\.css$|pages[\/\\]tokens\.astro$/.test(p);
161
161
 
162
162
  for (const file of walk(SRC)) {
163
163
  const rel = relative('.', file);
@@ -165,6 +165,7 @@ for (const file of walk(SRC)) {
165
165
  // prose like "(issue #324)" in a comment trips the hex-color regex.
166
166
  const text = readFileSync(file, 'utf8')
167
167
  .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
168
+ .replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, ' '))
168
169
  .replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + ' '.repeat(m.length - p1.length));
169
170
  const lines = text.split('\n');
170
171
  lines.forEach((line, i) => {
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * remarque-drift — token drift check for consumers of remarque-tokens.
4
+ *
5
+ * node scripts/drift-check.mjs --css-file <path> --package-dir <dir>
6
+ * npx remarque-drift --css-file src/styles/global.css --package-dir .
7
+ *
8
+ * Compares a consumer's stylesheet against the INSTALLED remarque-tokens
9
+ * package (node_modules/remarque-tokens under --package-dir), using the
10
+ * package's own generated tokens.json as the source of truth. Reuses
11
+ * scripts/lib/css-tokens.mjs (same brace-aware parser as audit.mjs /
12
+ * tokens-json.mjs) rather than a third CSS parser.
13
+ *
14
+ * Classification (see REMARQUE.md "Token Tiers"):
15
+ *
16
+ * (a) CORE-tier token redeclared with a DIFFERENT value:
17
+ * - FAIL (exit 1) — undocumented core-tier override. The site has
18
+ * forked, by definition.
19
+ * - WARN — if a DESIGN-DEVIATIONS.md or DESIGN-NOTES.md in the
20
+ * consumer mentions the token name, the fork is a ratified,
21
+ * documented deviation; still surfaced, not a build failure.
22
+ * (b) PALETTE-tier token redeclared with a different value:
23
+ * - INFO — sanctioned personalization. Listed, not warned.
24
+ * (c) CORE-tier token used via var(--x) but never declared anywhere in
25
+ * the consumer's stylesheet: no-op. It resolves from the package
26
+ * import at build time; there is nothing to compare.
27
+ *
28
+ * Exit code: 1 iff there is at least one undocumented (FAIL) core-tier
29
+ * mismatch. WARN and INFO never fail the build.
30
+ */
31
+
32
+ import { readFileSync, existsSync } from 'node:fs';
33
+ import { join, dirname, resolve } from 'node:path';
34
+ import { extractBlocks, declsOf, isLightRoot, isDarkBlock } from './lib/css-tokens.mjs';
35
+
36
+ const args = process.argv.slice(2);
37
+ function argOf(flag, dflt) {
38
+ const i = args.indexOf(flag);
39
+ return i !== -1 && args[i + 1] ? args[i + 1] : dflt;
40
+ }
41
+
42
+ const CSS_FILE = argOf('--css-file', null);
43
+ const PACKAGE_DIR = resolve(argOf('--package-dir', '.'));
44
+
45
+ if (!CSS_FILE) {
46
+ console.error('usage: drift-check.mjs --css-file <path> --package-dir <dir>');
47
+ process.exit(2);
48
+ }
49
+ if (!existsSync(CSS_FILE)) {
50
+ console.error(`consumer stylesheet not found: ${CSS_FILE}`);
51
+ process.exit(2);
52
+ }
53
+
54
+ const PKG_ROOT = join(PACKAGE_DIR, 'node_modules', 'remarque-tokens');
55
+ const PKG_JSON = join(PKG_ROOT, 'package.json');
56
+ const TOKENS_JSON = join(PKG_ROOT, 'tokens.json');
57
+
58
+ if (!existsSync(PKG_JSON) || !existsSync(TOKENS_JSON)) {
59
+ console.error(
60
+ `remarque-tokens not found under ${PACKAGE_DIR}/node_modules — ` +
61
+ `pass --package-dir pointing at the directory containing node_modules/remarque-tokens`
62
+ );
63
+ process.exit(2);
64
+ }
65
+
66
+ const installedVersion = JSON.parse(readFileSync(PKG_JSON, 'utf8')).version;
67
+ const tokens = JSON.parse(readFileSync(TOKENS_JSON, 'utf8'));
68
+
69
+ /* ── Deviation-doc lookup ──────────────────────────────────────────
70
+ * Accepts either filename (flagship uses DESIGN-DEVIATIONS.md; tsundoku
71
+ * uses DESIGN-NOTES.md — both are "this site's documented departures"
72
+ * docs, just named differently by each consumer). Searched in
73
+ * --package-dir, the consumer's cwd, and every directory between the
74
+ * stylesheet and the filesystem/git root, so it's found regardless of
75
+ * which directory happens to hold node_modules vs the doc. */
76
+ const DEVIATION_DOC_NAMES = ['DESIGN-DEVIATIONS.md', 'DESIGN-NOTES.md'];
77
+
78
+ function candidateDirs(cssFile, packageDir) {
79
+ const dirs = [packageDir, resolve('.')];
80
+ let dir = resolve(dirname(cssFile));
81
+ for (let i = 0; i < 8; i++) {
82
+ dirs.push(dir);
83
+ if (existsSync(join(dir, '.git'))) break;
84
+ const parent = dirname(dir);
85
+ if (parent === dir) break;
86
+ dir = parent;
87
+ }
88
+ return [...new Set(dirs)];
89
+ }
90
+
91
+ function findDeviationDoc(cssFile, packageDir) {
92
+ for (const dir of candidateDirs(cssFile, packageDir)) {
93
+ for (const name of DEVIATION_DOC_NAMES) {
94
+ const p = join(dir, name);
95
+ if (existsSync(p)) return p;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ const deviationDocPath = findDeviationDoc(CSS_FILE, PACKAGE_DIR);
102
+ const deviationDocText = deviationDocPath ? readFileSync(deviationDocPath, 'utf8') : '';
103
+
104
+ function isDocumented(tokenName) {
105
+ if (!deviationDocPath) return false;
106
+ return deviationDocText.includes(`--${tokenName}`) || deviationDocText.includes(tokenName);
107
+ }
108
+
109
+ /* ── Parse the consumer stylesheet ─────────────────────────────────── */
110
+
111
+ const blocks = extractBlocks(readFileSync(CSS_FILE, 'utf8'));
112
+ const lightDecls = declsOf(blocks, isLightRoot);
113
+ const darkOverrides = declsOf(blocks, isDarkBlock);
114
+
115
+ const norm = (v) => String(v).trim().replace(/\s+/g, ' ');
116
+
117
+ /* ── Classify ───────────────────────────────────────────────────────── */
118
+
119
+ const fails = [];
120
+ const warns = [];
121
+ const infos = [];
122
+
123
+ for (const [name, tok] of Object.entries(tokens.core || {})) {
124
+ const canonical = norm(tok.$value);
125
+ const declaredLight = Object.prototype.hasOwnProperty.call(lightDecls, name) ? lightDecls[name] : undefined;
126
+ const declaredDark = Object.prototype.hasOwnProperty.call(darkOverrides, name) ? darkOverrides[name] : undefined;
127
+ if (declaredLight === undefined && declaredDark === undefined) continue; // (c) no-op — resolves from the package import
128
+
129
+ const mismatches = [];
130
+ if (declaredLight !== undefined && norm(declaredLight) !== canonical) mismatches.push({ theme: 'light', value: declaredLight });
131
+ if (declaredDark !== undefined && norm(declaredDark) !== canonical) mismatches.push({ theme: 'dark', value: declaredDark });
132
+ if (mismatches.length === 0) continue; // declared but byte-identical to core — harmless duplicate, not a fork
133
+
134
+ const diffText = mismatches.map((m) => `${m.theme}: consumer defines "${m.value}"`).join('; ');
135
+ const entry = { name, canonical: tok.$value, mismatches };
136
+ if (isDocumented(name)) {
137
+ entry.doc = deviationDocPath;
138
+ warns.push(entry);
139
+ } else {
140
+ fails.push(entry);
141
+ }
142
+ void diffText;
143
+ }
144
+
145
+ for (const [name, tok] of Object.entries(tokens.palette || {})) {
146
+ const canonicalLight = norm(tok.light?.$value);
147
+ const canonicalDark = norm(tok.dark?.$value ?? tok.light?.$value);
148
+ const declaredLight = Object.prototype.hasOwnProperty.call(lightDecls, name) ? lightDecls[name] : undefined;
149
+ const declaredDarkRaw = Object.prototype.hasOwnProperty.call(darkOverrides, name) ? darkOverrides[name] : undefined;
150
+ if (declaredLight === undefined && declaredDarkRaw === undefined) continue; // not touched by the consumer at all
151
+
152
+ // CSS cascade: an undeclared dark value inherits the consumer's own light value.
153
+ const effectiveDark = declaredDarkRaw !== undefined ? declaredDarkRaw : declaredLight;
154
+
155
+ const diffs = [];
156
+ if (declaredLight !== undefined && norm(declaredLight) !== canonicalLight) diffs.push({ theme: 'light', value: declaredLight });
157
+ if (effectiveDark !== undefined && norm(effectiveDark) !== canonicalDark) diffs.push({ theme: 'dark', value: effectiveDark });
158
+ if (diffs.length === 0) continue; // declared, but matches the package default — no divergence to report
159
+
160
+ infos.push({ name, canonicalLight: tok.light?.$value, canonicalDark: tok.dark?.$value ?? tok.light?.$value, diffs });
161
+ }
162
+
163
+ /* ── Report ─────────────────────────────────────────────────────────── */
164
+
165
+ console.log('remarque-drift — token drift check');
166
+ console.log(` consumer stylesheet: ${CSS_FILE}`);
167
+ console.log(` installed remarque-tokens: ${installedVersion} (${PKG_ROOT})`);
168
+ console.log(` deviation doc: ${deviationDocPath || '(none found — DESIGN-DEVIATIONS.md / DESIGN-NOTES.md)'}`);
169
+
170
+ if (fails.length) {
171
+ console.log('\nFAIL — undocumented core-tier override(s) (this consumer has forked Remarque):');
172
+ for (const f of fails) {
173
+ for (const m of f.mismatches) {
174
+ console.log(` ✗ --${f.name} (${m.theme}): core ships "${f.canonical}", consumer defines "${m.value}"`);
175
+ }
176
+ console.log(` fix: revert to the core value, or document the deviation in DESIGN-DEVIATIONS.md / DESIGN-NOTES.md and mention "--${f.name}"`);
177
+ }
178
+ }
179
+
180
+ if (warns.length) {
181
+ console.log('\nWARN — documented core-tier deviation(s) (ratified, not a build failure):');
182
+ for (const w of warns) {
183
+ for (const m of w.mismatches) {
184
+ console.log(` ⚠ --${w.name} (${m.theme}): core ships "${w.canonical}", consumer defines "${m.value}" — see ${w.doc}`);
185
+ }
186
+ }
187
+ }
188
+
189
+ if (infos.length) {
190
+ console.log('\nINFO — palette-tier divergence(s) (sanctioned personalization):');
191
+ for (const i of infos) {
192
+ for (const d of i.diffs) {
193
+ const canonical = d.theme === 'light' ? i.canonicalLight : i.canonicalDark;
194
+ console.log(` ℹ --${i.name} (${d.theme}): package default "${canonical}", consumer defines "${d.value}"`);
195
+ }
196
+ }
197
+ }
198
+
199
+ console.log(`\nSummary: ${fails.length} FAIL, ${warns.length} WARN, ${infos.length} INFO\n`);
200
+
201
+ process.exit(fails.length ? 1 : 0);
@@ -16,7 +16,10 @@ export function extractBlocks(css) {
16
16
  while (i < text.length) {
17
17
  const open = text.indexOf('{', i);
18
18
  if (open === -1) break;
19
- const prelude = text.slice(i, open).trim().replace(/^[;\s]+/, '');
19
+ // Body-less at-statements (@import 'x'; @charset ...;) preceding a block
20
+ // would otherwise pollute its prelude and misclassify it as an at-rule
21
+ // (#67): keep only the segment after the last ';'.
22
+ const prelude = text.slice(i, open).split(';').pop().trim();
20
23
  let depth = 1, j = open + 1;
21
24
  while (j < text.length && depth > 0) {
22
25
  if (text[j] === '{') depth++;
@@ -49,12 +52,15 @@ export function declsOf(blocks, filterFn) {
49
52
  dark block in the class convention) as light. */
50
53
  const parts = (prelude) => prelude.split(',').map((s) => s.trim());
51
54
 
55
+ const DARKISH = /(\.dark\b|\[data-theme="dark"\])/;
56
+ const ROOTISH = /^(:root|html|body)\b/;
52
57
  export const isLightRoot = (b) =>
53
- b.context === '' && parts(b.prelude).some((s) => s === ':root' || s === ':root.light' || s === '[data-theme="light"]');
58
+ b.context === '' && parts(b.prelude).some((s) =>
59
+ (ROOTISH.test(s) && !DARKISH.test(s)) || s === '[data-theme="light"]');
54
60
 
55
61
  /* Dark: media-query :root, the canonical [data-theme="dark"], or the
56
62
  class-convention :root.dark / html.dark (compatibility bridge). */
57
63
  export const isDarkBlock = (b) =>
58
64
  (b.context.includes('prefers-color-scheme') && b.context.includes('dark') &&
59
- parts(b.prelude).some((s) => s === ':root')) ||
60
- parts(b.prelude).some((s) => s === '[data-theme="dark"]' || s === ':root.dark' || s === 'html.dark' || s === '.dark');
65
+ parts(b.prelude).some((s) => ROOTISH.test(s) && !DARKISH.test(s))) ||
66
+ parts(b.prelude).some((s) => DARKISH.test(s) && (ROOTISH.test(s) || s.startsWith('.') || s.startsWith('[')));
package/tokens-core.css CHANGED
@@ -38,7 +38,7 @@
38
38
  --tracking-title: -0.015em;
39
39
  --tracking-body: 0em;
40
40
  --tracking-meta: 0.02em; /* mono metadata gets slight tracking */
41
- --tracking-caps: 0.06em; /* for any rare uppercase labels */
41
+ --tracking-caps: 0.06em; /* letter-spacing for .text-label (true small caps, issue #51) */
42
42
 
43
43
  /* Font weights */
44
44
  --weight-regular: 400;
@@ -149,6 +149,19 @@ html {
149
149
  font-weight: var(--weight-medium);
150
150
  }
151
151
 
152
+ /* Optical/wrapping defaults for display-scale headings (issue #51):
153
+ balanced line breaks, optical sizing on variable display faces, and
154
+ no synthesized bold/italic for faces that lack a real one. All three
155
+ degrade safely — unsupported browsers silently fall back to normal
156
+ wrapping and the font's default rendering. */
157
+ .text-display,
158
+ .text-title,
159
+ .text-section {
160
+ text-wrap: balance;
161
+ font-optical-sizing: auto;
162
+ font-synthesis: none;
163
+ }
164
+
152
165
  .text-body {
153
166
  font-family: var(--font-body);
154
167
  font-size: var(--text-body);
@@ -172,6 +185,25 @@ html {
172
185
  letter-spacing: var(--tracking-meta);
173
186
  font-weight: var(--weight-regular);
174
187
  color: var(--color-muted);
188
+ /* Tabular lining numerals — dates/counts/timestamps align in a
189
+ column and don't jitter as digits change. Mirrored by
190
+ .remarque-prose's oldstyle proportional register below; the two
191
+ never mix (issue #51). */
192
+ font-variant-numeric: tabular-nums lining-nums;
193
+ }
194
+
195
+ /* True small caps for labels (issue #51) — font-variant-caps renders at
196
+ x-height with corrected weight/spacing; text-transform: uppercase
197
+ renders letters at cap-height with no optical correction and reads
198
+ louder than intended. Prefer .text-label over uppercase text
199
+ anywhere a label needs a caps-like treatment. */
200
+ .text-label {
201
+ font-family: var(--font-mono);
202
+ font-size: var(--text-meta);
203
+ line-height: var(--leading-meta);
204
+ font-variant-caps: all-small-caps;
205
+ letter-spacing: var(--tracking-caps);
206
+ color: var(--color-muted);
175
207
  }
176
208
 
177
209
  /* ─── Content Width Utilities ───────────────────────────── */
package/tokens.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$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.",
3
3
  "$extensions": {
4
4
  "remarque": {
5
- "version": "0.6.0",
5
+ "version": "0.7.0",
6
6
  "tiers": {
7
7
  "core": "immutable identity — overriding forks the system",
8
8
  "palette": "sanctioned personalization surface — override freely, then run remarque-audit"