remarque-tokens 0.12.0 → 0.13.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,58 @@ 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.13.0 — 2026-07-23
8
+
9
+ Syntax-highlighting palette — 9 `--color-syntax-*` slots, ANSI-derived, golden-gated (closes #53; consensus-ratified 3-0 with a binding taxonomy design record, 2026-07-23).
10
+
11
+ ### Added
12
+ - **`--color-syntax-keyword/string/constant/comment/function/type/punctuation/variable/link`** —
13
+ 9 new palette-tier slots in `tokens-palette.css`, hand-authored in both
14
+ themes, quiet/low-chroma (chroma capped at 0.14, the same ceiling as
15
+ `--color-accent`). Every slot is ≥ 4.5:1 against `--color-code-bg`
16
+ (not `--color-bg`) in both themes — a new dimension the audit didn't
17
+ cover before this release.
18
+ - **`scripts/audit.mjs`** — 9 new `CHECKS` pairings (each syntax slot vs.
19
+ `color-code-bg`, 4.5:1, both themes). `scripts/test-audit.mjs`'s
20
+ fixture palettes carry the 9 slots and a new must-fail case proves
21
+ they're actually wired into `CHECKS`, not just parsed.
22
+ - **`scripts/theme.mjs` (`remarque-theme`)** — derives all 9 slots from a
23
+ source theme's 16 ANSI colors (`keyword`←blue, `string`←green,
24
+ `constant`←yellow, `function`←purple, `type`←cyan, `comment`←`brightBlack`
25
+ in dark themes / a solved muted neutral in light themes,
26
+ `punctuation`←a derived neutral, `variable`←fg-adjacent,
27
+ `link`←the derived accent, aliased when it clears 4.5:1 on `code-bg`),
28
+ same keep-if-passing-else-solve pattern and in-gamut chroma clamping as
29
+ every other slot. `scripts/test-theme.mjs`'s 61-pair corpus (every
30
+ counterpart-paired theme in the installed `oklch-terminal-themes`
31
+ dataset) now runs the extended audit — 61/61 pass.
32
+ - **`scripts/palette-golden.mjs`** — extends the ΔE2000 ≤ 2.0 golden gate
33
+ to the 9 syntax slots (now 24 `--color-*` tokens × 2 themes). The
34
+ hand-authored defaults are the cleaned round-number serialization of
35
+ values derived from `remarque-light`/`remarque-dark`'s ANSI colors,
36
+ same identity/serialization contract as the rest of the default
37
+ palette (#76).
38
+ - **REMARQUE.md "Syntax Highlighting"** — the slot table with Shiki
39
+ `createCssVariablesTheme` and Prism class mappings, the exact Astro
40
+ wiring snippet (pass the theme *object*, never the `'css-variables'`
41
+ string — Astro renames its prefix), the CSS variable bridge required
42
+ to make that wiring actually render (Shiki's `variablePrefix` prepends
43
+ rather than renames its internal `token-*` names), a full Prism CSS
44
+ mapping block, the three documented divergences (`type` ≡ `function`
45
+ under Shiki, plain operators inherit foreground, diff markers out of
46
+ scope), and the comment-contrast policy (4.5:1 held; GitHub
47
+ light/dark-default and VS Code Dark+ all clear it).
48
+ - **Demo site (`site/`)** — wired `astro.config.mjs`'s `markdown.shikiConfig`
49
+ and the Type Specimen page's new "Syntax Highlighting" section with
50
+ the `createCssVariablesTheme` recipe and its CSS bridge
51
+ (`site/src/styles/globals.css`), so at least one built page exercises
52
+ every slot in both themes. `shiki` added as an explicit `site`
53
+ devDependency (Astro already carries it transitively; pinned
54
+ explicitly so the resolution is intentional, not an accident of
55
+ hoisting). Playwright baselines updated for the 4 `specimen-*`
56
+ screenshots (the only page with a highlighted code block); the other
57
+ 16 baselines are unchanged.
58
+
7
59
  ## 0.12.0 — 2026-07-23
8
60
 
9
61
  TypeScript types and a README "Used By" section (closes #34, closes #35).
package/REMARQUE.md CHANGED
@@ -80,7 +80,7 @@ The palette tier (all `--color-*` tokens) can be *supplied* rather than hand-aut
80
80
  npx remarque-theme <light-slug> [--dark <dark-slug>] [-o palette-override.css]
81
81
  ```
82
82
 
83
- Terminal themes carry only `background/foreground/cursor/selection` + 16 ANSI slots — a few of Remarque's 15 semantic slots map directly, and most themes fail the AAA `fg-muted` 7:1 line as authored. So `remarque-theme` *derives* rather than maps: **hue and chroma carry the theme's personality; lightness is KEEP-IF-PASSING**. For the slots that have a meaningful "theme's own value" — `fg`, `accent`, `accent-hover`, and (in dark) `selection-fg`/`code-fg` — the theme's own authored lightness is kept as-is when it already clears the slot's contrast target; lightness is solved (binary search, same targets as the Enforcement Checklist below, with in-gamut chroma clamping) *only* when it doesn't. Every other slot (`fg-muted`, `muted`, `border-bold`, the bg ladder, `selection-bg`, `accent-subtle`) is solved/derived unconditionally, as it always was — those don't carry an independent authored value to preserve. This means a well-designed input theme comes through close to its own feel instead of being flattened to the exact threshold; a theme that fails a target still gets a value that passes, exactly as before. The accent hue comes from the theme's cursor color if it's chromatic, otherwise the most saturated classic ANSI color, and is kept consistent between the light and dark half of a pair.
83
+ Terminal themes carry only `background/foreground/cursor/selection` + 16 ANSI slots — a few of Remarque's 24 semantic slots map directly, and most themes fail the AAA `fg-muted` 7:1 line as authored. So `remarque-theme` *derives* rather than maps: **hue and chroma carry the theme's personality; lightness is KEEP-IF-PASSING**. For the slots that have a meaningful "theme's own value" — `fg`, `accent`, `accent-hover`, and (in dark) `selection-fg`/`code-fg` — the theme's own authored lightness is kept as-is when it already clears the slot's contrast target; lightness is solved (binary search, same targets as the Enforcement Checklist below, with in-gamut chroma clamping) *only* when it doesn't. Every other slot (`fg-muted`, `muted`, `border-bold`, the bg ladder, `selection-bg`, `accent-subtle`) is solved/derived unconditionally, as it always was — those don't carry an independent authored value to preserve. This means a well-designed input theme comes through close to its own feel instead of being flattened to the exact threshold; a theme that fails a target still gets a value that passes, exactly as before. The accent hue comes from the theme's cursor color if it's chromatic, otherwise the most saturated classic ANSI color, and is kept consistent between the light and dark half of a pair.
84
84
 
85
85
  The audit remains the gate regardless of provenance — `remarque-theme` self-verifies its own output against the same checks before it will emit anything, but a site that hand-edits a generated palette (or points at a provider with pathological input colors) still runs through `remarque-audit` in CI like any other palette.
86
86
 
@@ -231,6 +231,113 @@ Charts are not yet a first-class Remarque surface, but tools built with the syst
231
231
 
232
232
  ---
233
233
 
234
+ ## Syntax Highlighting
235
+
236
+ Code blocks are a first-class Remarque surface, not a place to reach for a vendor Shiki/Prism theme: `tokens-palette.css` defines 9 `--color-syntax-*` slots, hand-authored in both themes and golden-gated against the upstream `remarque-light`/`remarque-dark` themes' ANSI colors the same way the rest of the default palette is (issue #53). Because they are ordinary palette-tier tokens, they inherit palette swaps and theme-deck runtime switches for free — a vendor theme never does.
237
+
238
+ ### Slots
239
+
240
+ | Slot | Shiki (`createCssVariablesTheme`) | Prism |
241
+ |---|---|---|
242
+ | `--color-syntax-keyword` | `token-keyword` | `.keyword`, `.atrule` |
243
+ | `--color-syntax-string` | `token-string` + `token-string-expression` (aliased — regex/template/interpolated are string-like) | `.string`, `.char`, `.attr-value` |
244
+ | `--color-syntax-constant` | `token-constant` | `.number`, `.boolean`, `.constant`, `.symbol` |
245
+ | `--color-syntax-comment` | `token-comment` | `.comment`, `.prolog`, `.doctype`, `.cdata` |
246
+ | `--color-syntax-function` | `token-function` | `.function` |
247
+ | `--color-syntax-type` | (same Shiki variable as `function` — see divergence below) | `.class-name` |
248
+ | `--color-syntax-punctuation` | `token-punctuation` | `.operator`, `.punctuation` |
249
+ | `--color-syntax-variable` | `token-parameter` | `.variable`, `.property`, `.tag`, `.attr-name` |
250
+ | `--color-syntax-link` | `token-link` | `.url` |
251
+
252
+ Slot names were validated against Shiki's `theme-css-variables.ts` and Prism's reference-theme token vocabulary before being frozen (panel condition on #53) — every name above maps to a real token both highlighters actually emit.
253
+
254
+ ### Derivation
255
+
256
+ `remarque-theme` derives all 9 slots from the source theme's 16 ANSI colors — the same keep-if-passing-else-solve pattern as every other slot, targeted at `--color-code-bg` (not `--color-bg`) at 4.5:1: `keyword`←blue, `string`←green, `constant`←yellow (the terminal convention for numbers/booleans), `function`←purple, `type`←cyan, `comment`←`brightBlack` in dark themes (solved to a muted neutral on the theme's own fg hue in light themes, where `brightBlack` is often too pale to clear 4.5:1 on `code-bg`), `punctuation`←a derived neutral in the muted family, `variable`←the theme's own fg hue/chroma with a slight lightness offset, and `link`←the already-derived accent triple, aliased with `var(--color-accent)` when it clears 4.5:1 on `code-bg` (solved standalone when it doesn't — accent's guarantee is against `--color-bg`, and `code-bg` sits close enough to drift below the line on some corpus themes). Chroma is capped at 0.14, the same ceiling as `--color-accent`, so syntax colors stay in the system's quiet, one-accent register. `scripts/palette-golden.mjs` extends its ΔE2000 ≤ 2.0 gate to all 9 slots, exactly like the rest of the default palette.
257
+
258
+ ### Astro / Shiki wiring
259
+
260
+ Shiki v1+ ships `createCssVariablesTheme` — pass it as an object, not the `'css-variables'` string (Astro renames the string form's variable prefix to `--astro-code-*`, which silently breaks the mapping):
261
+
262
+ ```js
263
+ import { createCssVariablesTheme } from 'shiki'; // or '@shikijs/core'
264
+
265
+ const theme = createCssVariablesTheme({
266
+ name: 'remarque',
267
+ variablePrefix: '--color-syntax-',
268
+ fontStyle: true,
269
+ });
270
+
271
+ // markdown/MDX: { shikiConfig: { theme } }
272
+ // <Code> component: <Code code={...} lang="..." theme={theme} />
273
+ ```
274
+
275
+ **The CSS bridge (required, not optional):** `createCssVariablesTheme`'s `variablePrefix` is a plain string prepend, not a rename — Shiki's own internal slot names already start with `token-` (`token-keyword`, `token-string-expression`, `token-constant`, …) plus a bare `foreground`/`background`, so `variablePrefix: '--color-syntax-'` makes Shiki bake `color:var(--color-syntax-token-keyword)` into every span, not `var(--color-syntax-keyword)`. Left undefined, that variable resolves to nothing and the span silently inherits its parent's color — the highlighting looks like it's missing, not broken. One small bridge block closes the gap (Astro's Shiki integration puts an `.astro-code` class on the `<pre>`; scope to it so these implementation-detail names don't leak into the page's global custom-property namespace):
276
+
277
+ ```css
278
+ .astro-code {
279
+ --color-syntax-foreground: var(--color-code-fg);
280
+ --color-syntax-background: var(--color-code-bg);
281
+ --color-syntax-token-keyword: var(--color-syntax-keyword);
282
+ --color-syntax-token-string: var(--color-syntax-string);
283
+ --color-syntax-token-string-expression: var(--color-syntax-string);
284
+ --color-syntax-token-constant: var(--color-syntax-constant);
285
+ --color-syntax-token-comment: var(--color-syntax-comment);
286
+ --color-syntax-token-function: var(--color-syntax-function);
287
+ --color-syntax-token-parameter: var(--color-syntax-variable);
288
+ --color-syntax-token-punctuation: var(--color-syntax-punctuation);
289
+ --color-syntax-token-link: var(--color-syntax-link);
290
+ /* token-inserted/-deleted/-changed intentionally left undefined —
291
+ diff markers, out of scope (see "Documented divergences" below);
292
+ they fall back to inherited foreground, which is a safe default. */
293
+ }
294
+ ```
295
+
296
+ There is no separate `token-type` — see the `type` ≡ `function` divergence below; `--color-syntax-type` is real for Prism but never appears as a Shiki variable.
297
+
298
+ ### Prism CSS mapping
299
+
300
+ Prism ships no CSS-variables mode — wire the classes directly:
301
+
302
+ ```css
303
+ .token.keyword,
304
+ .token.atrule { color: var(--color-syntax-keyword); }
305
+ .token.string,
306
+ .token.char,
307
+ .token.attr-value { color: var(--color-syntax-string); }
308
+ .token.number,
309
+ .token.boolean,
310
+ .token.constant,
311
+ .token.symbol { color: var(--color-syntax-constant); }
312
+ .token.comment,
313
+ .token.prolog,
314
+ .token.doctype,
315
+ .token.cdata { color: var(--color-syntax-comment); }
316
+ .token.function { color: var(--color-syntax-function); }
317
+ .token.class-name { color: var(--color-syntax-type); }
318
+ .token.operator,
319
+ .token.punctuation { color: var(--color-syntax-punctuation); }
320
+ .token.variable,
321
+ .token.property,
322
+ .token.tag,
323
+ .token.attr-name { color: var(--color-syntax-variable); }
324
+ .token.url { color: var(--color-syntax-link); }
325
+ ```
326
+
327
+ ### Documented divergences
328
+
329
+ These are deliberate, not bugs:
330
+
331
+ - **`type` ≡ `function` under Shiki.** Shiki's css-variables mode folds `entity.name.type` into the function-scoped variable, so the two slots render identically in Shiki output even though Prism (`.class-name` vs. `.function`) distinguishes them. The tokens stay separate because Prism needs them separate.
332
+ - **Plain operators inherit the foreground.** Shiki has no scope for a bare operator token; it renders in `--color-fg` like surrounding text rather than `--color-syntax-punctuation`. Prism's `.operator` class does map to punctuation.
333
+ - **Diff markers are out of scope.** `token-inserted`/`token-deleted`/`token-changed` (Shiki) and equivalent Prism diff classes are not covered by this token set — a future `--color-diff-*` family would own them if a concrete consumer needs one.
334
+
335
+ ### Comment-contrast policy
336
+
337
+ The 4.5:1 floor is held, not relaxed, for `--color-syntax-comment` — muted comments are an editorial convention, not a license to fail AA. Survey of shipping syntax themes: GitHub's light-default theme holds 4.55:1, GitHub's dark-default holds 6.15:1, and VS Code's Dark+ holds 5.0:1 — all pass. Only legacy/neglected themes fail (Prism's own default theme sits at 3.64:1; the legacy `github-dark` theme sits at 3.05:1). A muted-comment convention is achievable within AA; Remarque does not carry a documented exception for this slot.
338
+
339
+ ---
340
+
234
341
  ## Motion Rules
235
342
 
236
343
  Motion in Remarque is nearly invisible. The only permitted motion:
@@ -303,6 +410,7 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
303
410
  - [ ] `font-variant-numeric: tabular-nums lining-nums` on all metadata that mixes with dates/counts.
304
411
  - [ ] No hardcoded hex/rgb colors — only `var(--color-*)` tokens.
305
412
  - [ ] Body line-height ≥ 1.5 (Remarque target: 1.75).
413
+ - [ ] Every `--color-syntax-*` slot ≥ 4.5:1 against `--color-code-bg` in both themes (see "Syntax Highlighting").
306
414
 
307
415
  Agents reviewing PRs should reject changes that violate any line above without explicit rationale.
308
416
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
package/scripts/audit.mjs CHANGED
@@ -113,6 +113,15 @@ const CHECKS = [
113
113
  ['color-code-fg', 'color-code-bg', 4.5, 'code text'],
114
114
  ['color-border-bold', 'color-bg', 3.0, 'functional borders (WCAG 1.4.11)'],
115
115
  ['color-selection-fg', 'color-selection-bg', 4.5, 'selected text'],
116
+ ['color-syntax-keyword', 'color-code-bg', 4.5, 'syntax: keyword'],
117
+ ['color-syntax-string', 'color-code-bg', 4.5, 'syntax: string'],
118
+ ['color-syntax-constant', 'color-code-bg', 4.5, 'syntax: constant'],
119
+ ['color-syntax-comment', 'color-code-bg', 4.5, 'syntax: comment'],
120
+ ['color-syntax-function', 'color-code-bg', 4.5, 'syntax: function'],
121
+ ['color-syntax-type', 'color-code-bg', 4.5, 'syntax: type'],
122
+ ['color-syntax-punctuation', 'color-code-bg', 4.5, 'syntax: punctuation'],
123
+ ['color-syntax-variable', 'color-code-bg', 4.5, 'syntax: variable'],
124
+ ['color-syntax-link', 'color-code-bg', 4.5, 'syntax: link'],
116
125
  ];
117
126
 
118
127
  for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
package/scripts/theme.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  * npx remarque-theme <light-slug> --dark <dark-slug> [-o out.css]
8
8
  *
9
9
  * Terminal themes carry only background/foreground/cursor/selection + 16
10
- * ANSI slots — most of Remarque's 15 semantic slots don't map directly,
10
+ * ANSI slots — most of Remarque's 24 semantic slots don't map directly,
11
11
  * and most themes fail the AAA fg-muted 7:1 line as authored. So this
12
12
  * DERIVES rather than maps: hue + chroma come from the theme (its
13
13
  * personality); lightness is KEEP-IF-PASSING — a few load-bearing slots
@@ -19,9 +19,11 @@
19
19
  * still gets a value that passes, same as before (#76). Every other
20
20
  * slot (fg-muted, muted, border-bold, the bg ladder, selection-bg,
21
21
  * accent-subtle) is solved/derived as before — those don't have a
22
- * meaningful "theme's own value" to preserve. Output passes
23
- * remarque-audit BY CONSTRUCTION this script self-verifies the same
24
- * pairings before it will emit anything.
22
+ * meaningful "theme's own value" to preserve. The 9 --color-syntax-*
23
+ * slots (issue #53) are derived straight from the theme's 16 ANSI
24
+ * colors instead see the "Syntax-highlighting slots" section below.
25
+ * Output passes remarque-audit BY CONSTRUCTION — this script
26
+ * self-verifies the same pairings before it will emit anything.
25
27
  *
26
28
  * Security (non-negotiable, per the panel security review on #75):
27
29
  * - Both slugs are checked against the package's own index.json BEFORE
@@ -325,6 +327,66 @@ function withHeadroom([L0, , H], delta, dir, C) {
325
327
  return [L, fitChroma(L, C, H), r1(H)];
326
328
  }
327
329
 
330
+ /* ── Syntax-highlighting slots (issue #53) ───────────────────────────
331
+ * Terminal ANSI colors ARE syntax colors in their native domain, so the
332
+ * 9 --color-syntax-* slots are derived straight from a theme's 16 ANSI
333
+ * colors rather than invented fresh — zero new upstream schema work.
334
+ * Every slot targets --color-code-bg (not --color-bg) at 4.5:1, same
335
+ * keep-if-passing-else-solve pattern as every other slot above: the
336
+ * ANSI color's own lightness is kept when it already clears the ratio,
337
+ * solved (same binary search, same in-gamut chroma clamping) only when
338
+ * it doesn't. Chroma is capped at 0.14 — the same ceiling already used
339
+ * for --color-accent — so syntax colors stay in the system's "quiet,
340
+ * one-accent" register rather than turning carnival-bright. */
341
+
342
+ const SYNTAX_CHROMA_CAP = 0.14;
343
+
344
+ /* keyword/string/constant/function/type: a direct ANSI slot, kept if it
345
+ * already passes vs code-bg, solved (same hue) if it doesn't. */
346
+ function deriveSyntaxAnsi(t, ansiName, label, codeBg, dir) {
347
+ const [L0, C0, H0] = slotLch(t, ansiName, label);
348
+ const C = Math.min(C0, SYNTAX_CHROMA_CAP);
349
+ return keepOrSolve(L0, C, H0, codeBg, 4.5, dir);
350
+ }
351
+
352
+ /* comment: brightBlack in dark themes (the ANSI convention); in light
353
+ * themes brightBlack is often too pale to hold 4.5:1 on code-bg, so the
354
+ * fallback solves a muted neutral on the theme's own fg hue instead of
355
+ * brightBlack's hue. */
356
+ function deriveSyntaxComment(t, label, codeBg, dir, fallbackC, fallbackH) {
357
+ const [L0, C0, H0] = slotLch(t, 'brightBlack', label);
358
+ const C = Math.min(C0, SYNTAX_CHROMA_CAP);
359
+ const L = r3(Math.min(1, Math.max(0, L0)));
360
+ const c = fitChroma(L, C, H0);
361
+ if (ratio([L, c, H0], codeBg) >= 4.5) return [L, c, r1(H0)];
362
+ return solveL(fallbackC, fallbackH, codeBg, 4.5, dir);
363
+ }
364
+
365
+ /* punctuation: a derived neutral in the muted family (fg hue, low
366
+ * chroma), targeted at code-bg rather than surface. */
367
+ function deriveSyntaxPunctuation(fgC, fgH, codeBg, dir) {
368
+ return solveL(Math.min(fgC, 0.02), fgH, codeBg, 4.5, dir);
369
+ }
370
+
371
+ /* variable: fg-adjacent — the theme's own fg hue/chroma, offset a little
372
+ * so it reads as "quieter than plain text," verified (and solved if
373
+ * needed) against code-bg rather than assumed from the fg/surface pass. */
374
+ function deriveSyntaxVariable(fg, codeBg, dir, offset) {
375
+ return keepOrSolve(fg[0] + offset, fg[1], fg[2], codeBg, 4.5, dir);
376
+ }
377
+
378
+ /* link: alias the already-derived accent triple when it clears 4.5:1 on
379
+ * code-bg (accent is only guaranteed against --color-bg, and code-bg
380
+ * sits a little off from bg, so this isn't automatic) — solve a
381
+ * standalone value at the accent's own hue/chroma only when it doesn't,
382
+ * same as every other slot. Aliasing means a site's accent-hue swap
383
+ * (REMARQUE.md "Changing the Accent Hue") carries the link color with
384
+ * it for free whenever the alias holds. */
385
+ function deriveSyntaxLink(accent, codeBg, dir) {
386
+ if (ratio(accent, codeBg) >= 4.5) return { ref: 'color-accent' };
387
+ return keepOrSolve(accent[0], accent[1], accent[2], codeBg, 4.5, dir);
388
+ }
389
+
328
390
  /* ── LIGHT derivation ─────────────────────────────────────────────── */
329
391
 
330
392
  function deriveLight(t) {
@@ -351,6 +413,18 @@ function deriveLight(t) {
351
413
  // accent-hover: still the designed -0.08 offset from accent, but that
352
414
  // offset is verified against 4.5:1 on bg and solve-adjusted if it breaks.
353
415
  const accentHover = keepOrSolve(accent[0] - 0.08, acC * 0.8, ac.h, bg, 4.5, 'darker');
416
+ const codeBg = [r3(bg[0] - 0.03), bg[1], bg[2]];
417
+ // Syntax-highlighting slots (issue #53) — see the derivation helpers
418
+ // above. All verified against code-bg (not bg) at 4.5:1.
419
+ const synKeyword = deriveSyntaxAnsi(t, 'blue', 'light', codeBg, 'darker');
420
+ const synString = deriveSyntaxAnsi(t, 'green', 'light', codeBg, 'darker');
421
+ const synConstant = deriveSyntaxAnsi(t, 'yellow', 'light', codeBg, 'darker');
422
+ const synFunction = deriveSyntaxAnsi(t, 'purple', 'light', codeBg, 'darker');
423
+ const synType = deriveSyntaxAnsi(t, 'cyan', 'light', codeBg, 'darker');
424
+ const synComment = deriveSyntaxComment(t, 'light', codeBg, 'darker', fgC, fgH);
425
+ const synPunctuation = deriveSyntaxPunctuation(fgC, fgH, codeBg, 'darker');
426
+ const synVariable = deriveSyntaxVariable(fg, codeBg, 'darker', 0.08);
427
+ const synLink = deriveSyntaxLink(accent, codeBg, 'darker');
354
428
  const raw = {
355
429
  'color-bg': bg,
356
430
  'color-bg-subtle': [r3(bg[0] - 0.02), bg[1], bg[2]],
@@ -365,8 +439,17 @@ function deriveLight(t) {
365
439
  'color-accent-subtle': [0.95, fitChroma(0.95, 0.02, ac.h), r1(ac.h)],
366
440
  'color-selection-bg': [0.92, fitChroma(0.92, 0.04, ac.h), r1(ac.h)],
367
441
  'color-selection-fg': { ref: 'color-fg' },
368
- 'color-code-bg': [r3(bg[0] - 0.03), bg[1], bg[2]],
442
+ 'color-code-bg': codeBg,
369
443
  'color-code-fg': { ref: 'color-fg' },
444
+ 'color-syntax-keyword': synKeyword,
445
+ 'color-syntax-string': synString,
446
+ 'color-syntax-constant': synConstant,
447
+ 'color-syntax-comment': synComment,
448
+ 'color-syntax-function': synFunction,
449
+ 'color-syntax-type': synType,
450
+ 'color-syntax-punctuation': synPunctuation,
451
+ 'color-syntax-variable': synVariable,
452
+ 'color-syntax-link': synLink,
370
453
  };
371
454
  const tokens = {};
372
455
  for (const [name, v] of Object.entries(raw)) tokens[name] = v.ref ? `var(--${v.ref})` : fmt(v);
@@ -409,6 +492,17 @@ function deriveDark(t, accentHueLight) {
409
492
  // code-fg: "slightly dimmer than fg" — same pattern, -0.02, verified
410
493
  // against the AAA 7:1 line on code-bg.
411
494
  const codeFg = keepOrSolve(fg[0] - 0.02, fg[1], fg[2], codeBg, 7.0, 'lighter');
495
+ // Syntax-highlighting slots (issue #53) — mirrors the light derivation,
496
+ // direction flipped ('lighter': text on a dark code-bg).
497
+ const synKeyword = deriveSyntaxAnsi(t, 'blue', 'dark', codeBg, 'lighter');
498
+ const synString = deriveSyntaxAnsi(t, 'green', 'dark', codeBg, 'lighter');
499
+ const synConstant = deriveSyntaxAnsi(t, 'yellow', 'dark', codeBg, 'lighter');
500
+ const synFunction = deriveSyntaxAnsi(t, 'purple', 'dark', codeBg, 'lighter');
501
+ const synType = deriveSyntaxAnsi(t, 'cyan', 'dark', codeBg, 'lighter');
502
+ const synComment = deriveSyntaxComment(t, 'dark', codeBg, 'lighter', fgC, fgH);
503
+ const synPunctuation = deriveSyntaxPunctuation(fgC, fgH, codeBg, 'lighter');
504
+ const synVariable = deriveSyntaxVariable(fg, codeBg, 'lighter', -0.08);
505
+ const synLink = deriveSyntaxLink(accent, codeBg, 'lighter');
412
506
  const raw = {
413
507
  'color-bg': bg,
414
508
  'color-bg-subtle': surface,
@@ -425,9 +519,18 @@ function deriveDark(t, accentHueLight) {
425
519
  'color-selection-fg': selFg,
426
520
  'color-code-bg': codeBg,
427
521
  'color-code-fg': codeFg,
522
+ 'color-syntax-keyword': synKeyword,
523
+ 'color-syntax-string': synString,
524
+ 'color-syntax-constant': synConstant,
525
+ 'color-syntax-comment': synComment,
526
+ 'color-syntax-function': synFunction,
527
+ 'color-syntax-type': synType,
528
+ 'color-syntax-punctuation': synPunctuation,
529
+ 'color-syntax-variable': synVariable,
530
+ 'color-syntax-link': synLink,
428
531
  };
429
532
  const tokens = {};
430
- for (const [name, v] of Object.entries(raw)) tokens[name] = fmt(v);
533
+ for (const [name, v] of Object.entries(raw)) tokens[name] = v.ref ? `var(--${v.ref})` : fmt(v);
431
534
  tokens['weight-display'] = '500';
432
535
  return { raw, tokens };
433
536
  }
@@ -452,6 +555,15 @@ const CHECKS = [
452
555
  ['color-code-fg', 'color-code-bg', 4.5, 'code text'],
453
556
  ['color-border-bold', 'color-bg', 3.0, 'functional borders (WCAG 1.4.11)'],
454
557
  ['color-selection-fg', 'color-selection-bg', 4.5, 'selected text'],
558
+ ['color-syntax-keyword', 'color-code-bg', 4.5, 'syntax: keyword'],
559
+ ['color-syntax-string', 'color-code-bg', 4.5, 'syntax: string'],
560
+ ['color-syntax-constant', 'color-code-bg', 4.5, 'syntax: constant'],
561
+ ['color-syntax-comment', 'color-code-bg', 4.5, 'syntax: comment'],
562
+ ['color-syntax-function', 'color-code-bg', 4.5, 'syntax: function'],
563
+ ['color-syntax-type', 'color-code-bg', 4.5, 'syntax: type'],
564
+ ['color-syntax-punctuation', 'color-code-bg', 4.5, 'syntax: punctuation'],
565
+ ['color-syntax-variable', 'color-code-bg', 4.5, 'syntax: variable'],
566
+ ['color-syntax-link', 'color-code-bg', 4.5, 'syntax: link'],
455
567
  ];
456
568
 
457
569
  function resolveRaw(raw, name, seen = new Set()) {
@@ -16,6 +16,7 @@
16
16
  * - Colors: every replacement palette must pass the contrast audit
17
17
  * (npm run audit) — fg-muted ≥ 7:1, muted ≥ 4.5:1 on bg AND
18
18
  * surface, accent + accent-hover ≥ 4.5:1, border-bold ≥ 3:1,
19
+ * every --color-syntax-* slot ≥ 4.5:1 on --color-code-bg,
19
20
  * in BOTH themes.
20
21
  * - Accent: change the hue by re-deriving accent/accent-hover/
21
22
  * accent-subtle/selection at the SAME lightness values used here,
@@ -77,6 +78,22 @@
77
78
  --color-selection-fg: var(--color-fg);
78
79
  --color-code-bg: oklch(0.945 0.005 80); /* inline code background */
79
80
  --color-code-fg: var(--color-fg);
81
+
82
+ /* Syntax highlighting (issue #53) — quiet, low-chroma slots derived
83
+ from remarque-light's ANSI colors (scripts/theme.mjs), serialized
84
+ as round numbers (golden-gated: scripts/palette-golden.mjs). Every
85
+ slot ≥ 4.5:1 on --color-code-bg. Mapping recipe (Shiki css-variables
86
+ + Prism) is documented in REMARQUE.md's "Syntax Highlighting"
87
+ section. */
88
+ --color-syntax-keyword: oklch(0.51 0.12 250); /* ANSI blue — 4.88:1 on code-bg */
89
+ --color-syntax-string: oklch(0.50 0.12 145); /* ANSI green — 4.85:1 */
90
+ --color-syntax-constant: oklch(0.51 0.105 85); /* ANSI yellow (numbers/booleans convention) — 4.94:1 */
91
+ --color-syntax-comment: oklch(0.52 0.01 80); /* solved muted neutral (fg hue) — brightBlack is too pale on code-bg here — 4.69:1 */
92
+ --color-syntax-function: oklch(0.52 0.12 310); /* ANSI purple — 4.96:1 */
93
+ --color-syntax-type: oklch(0.50 0.085 196); /* ANSI cyan — 4.90:1 */
94
+ --color-syntax-punctuation: oklch(0.52 0.01 80); /* derived neutral, muted family — 4.69:1 */
95
+ --color-syntax-variable: oklch(0.26 0.01 80); /* fg-adjacent (slight L offset) — 13.23:1 */
96
+ --color-syntax-link: var(--color-accent); /* accent already clears code-bg — 4.5:1+ */
80
97
  }
81
98
 
82
99
  /* ─── Color: Dark theme ─────────────────────────────────── */
@@ -110,6 +127,18 @@
110
127
  --color-selection-fg: oklch(0.92 0.005 80);
111
128
  --color-code-bg: oklch(0.20 0.005 80);
112
129
  --color-code-fg: oklch(0.88 0.005 80);
130
+
131
+ /* Syntax highlighting (issue #53) — derived from remarque-dark's
132
+ ANSI colors; see the light block above for the full rationale. */
133
+ --color-syntax-keyword: oklch(0.61 0.11 250); /* ANSI blue — 4.80:1 on code-bg */
134
+ --color-syntax-string: oklch(0.60 0.11 145); /* ANSI green — 4.80:1 */
135
+ --color-syntax-constant: oklch(0.61 0.11 84); /* ANSI yellow — 4.73:1 */
136
+ --color-syntax-comment: oklch(0.60 0.005 80); /* ANSI brightBlack — 4.59:1 */
137
+ --color-syntax-function: oklch(0.62 0.11 310); /* ANSI purple — 4.75:1 */
138
+ --color-syntax-type: oklch(0.60 0.10 195); /* ANSI cyan — 4.81:1 */
139
+ --color-syntax-punctuation: oklch(0.60 0.005 80); /* derived neutral, muted family — 4.59:1 */
140
+ --color-syntax-variable: oklch(0.82 0.005 80); /* fg-adjacent (slight L offset) — 10.37:1 */
141
+ --color-syntax-link: var(--color-accent); /* accent already clears code-bg */
113
142
  }
114
143
  }
115
144
 
@@ -141,4 +170,16 @@
141
170
  --color-selection-fg: oklch(0.92 0.005 80);
142
171
  --color-code-bg: oklch(0.20 0.005 80);
143
172
  --color-code-fg: oklch(0.88 0.005 80);
173
+
174
+ /* Syntax highlighting (issue #53) — derived from remarque-dark's
175
+ ANSI colors; see the light block above for the full rationale. */
176
+ --color-syntax-keyword: oklch(0.61 0.11 250);
177
+ --color-syntax-string: oklch(0.60 0.11 145);
178
+ --color-syntax-constant: oklch(0.61 0.11 84);
179
+ --color-syntax-comment: oklch(0.60 0.005 80);
180
+ --color-syntax-function: oklch(0.62 0.11 310);
181
+ --color-syntax-type: oklch(0.60 0.10 195);
182
+ --color-syntax-punctuation: oklch(0.60 0.005 80);
183
+ --color-syntax-variable: oklch(0.82 0.005 80);
184
+ --color-syntax-link: var(--color-accent);
144
185
  }
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.12.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.13.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
@@ -81,7 +81,16 @@ export type RemarquePaletteToken =
81
81
  | 'color-selection-bg'
82
82
  | 'color-selection-fg'
83
83
  | 'color-code-bg'
84
- | 'color-code-fg';
84
+ | 'color-code-fg'
85
+ | 'color-syntax-keyword'
86
+ | 'color-syntax-string'
87
+ | 'color-syntax-constant'
88
+ | 'color-syntax-comment'
89
+ | 'color-syntax-function'
90
+ | 'color-syntax-type'
91
+ | 'color-syntax-punctuation'
92
+ | 'color-syntax-variable'
93
+ | 'color-syntax-link';
85
94
 
86
95
  /** Every Remarque token name, core + palette. */
87
96
  export type RemarqueToken = RemarqueCoreToken | RemarquePaletteToken;
@@ -287,6 +296,42 @@ export interface RemarqueTokenValues {
287
296
  readonly light: "var(--color-fg)";
288
297
  readonly dark: "oklch(0.88 0.005 80)";
289
298
  };
299
+ readonly 'color-syntax-keyword': {
300
+ readonly light: "oklch(0.51 0.12 250)";
301
+ readonly dark: "oklch(0.61 0.11 250)";
302
+ };
303
+ readonly 'color-syntax-string': {
304
+ readonly light: "oklch(0.50 0.12 145)";
305
+ readonly dark: "oklch(0.60 0.11 145)";
306
+ };
307
+ readonly 'color-syntax-constant': {
308
+ readonly light: "oklch(0.51 0.105 85)";
309
+ readonly dark: "oklch(0.61 0.11 84)";
310
+ };
311
+ readonly 'color-syntax-comment': {
312
+ readonly light: "oklch(0.52 0.01 80)";
313
+ readonly dark: "oklch(0.60 0.005 80)";
314
+ };
315
+ readonly 'color-syntax-function': {
316
+ readonly light: "oklch(0.52 0.12 310)";
317
+ readonly dark: "oklch(0.62 0.11 310)";
318
+ };
319
+ readonly 'color-syntax-type': {
320
+ readonly light: "oklch(0.50 0.085 196)";
321
+ readonly dark: "oklch(0.60 0.10 195)";
322
+ };
323
+ readonly 'color-syntax-punctuation': {
324
+ readonly light: "oklch(0.52 0.01 80)";
325
+ readonly dark: "oklch(0.60 0.005 80)";
326
+ };
327
+ readonly 'color-syntax-variable': {
328
+ readonly light: "oklch(0.26 0.01 80)";
329
+ readonly dark: "oklch(0.82 0.005 80)";
330
+ };
331
+ readonly 'color-syntax-link': {
332
+ readonly light: "var(--color-accent)";
333
+ readonly dark: "var(--color-accent)";
334
+ };
290
335
  };
291
336
  }
292
337
 
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.12.0",
5
+ "version": "0.13.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"
@@ -421,6 +421,87 @@
421
421
  "dark": {
422
422
  "$value": "oklch(0.88 0.005 80)"
423
423
  }
424
+ },
425
+ "color-syntax-keyword": {
426
+ "$type": "color",
427
+ "light": {
428
+ "$value": "oklch(0.51 0.12 250)"
429
+ },
430
+ "dark": {
431
+ "$value": "oklch(0.61 0.11 250)"
432
+ }
433
+ },
434
+ "color-syntax-string": {
435
+ "$type": "color",
436
+ "light": {
437
+ "$value": "oklch(0.50 0.12 145)"
438
+ },
439
+ "dark": {
440
+ "$value": "oklch(0.60 0.11 145)"
441
+ }
442
+ },
443
+ "color-syntax-constant": {
444
+ "$type": "color",
445
+ "light": {
446
+ "$value": "oklch(0.51 0.105 85)"
447
+ },
448
+ "dark": {
449
+ "$value": "oklch(0.61 0.11 84)"
450
+ }
451
+ },
452
+ "color-syntax-comment": {
453
+ "$type": "color",
454
+ "light": {
455
+ "$value": "oklch(0.52 0.01 80)"
456
+ },
457
+ "dark": {
458
+ "$value": "oklch(0.60 0.005 80)"
459
+ }
460
+ },
461
+ "color-syntax-function": {
462
+ "$type": "color",
463
+ "light": {
464
+ "$value": "oklch(0.52 0.12 310)"
465
+ },
466
+ "dark": {
467
+ "$value": "oklch(0.62 0.11 310)"
468
+ }
469
+ },
470
+ "color-syntax-type": {
471
+ "$type": "color",
472
+ "light": {
473
+ "$value": "oklch(0.50 0.085 196)"
474
+ },
475
+ "dark": {
476
+ "$value": "oklch(0.60 0.10 195)"
477
+ }
478
+ },
479
+ "color-syntax-punctuation": {
480
+ "$type": "color",
481
+ "light": {
482
+ "$value": "oklch(0.52 0.01 80)"
483
+ },
484
+ "dark": {
485
+ "$value": "oklch(0.60 0.005 80)"
486
+ }
487
+ },
488
+ "color-syntax-variable": {
489
+ "$type": "color",
490
+ "light": {
491
+ "$value": "oklch(0.26 0.01 80)"
492
+ },
493
+ "dark": {
494
+ "$value": "oklch(0.82 0.005 80)"
495
+ }
496
+ },
497
+ "color-syntax-link": {
498
+ "$type": "color",
499
+ "light": {
500
+ "$value": "var(--color-accent)"
501
+ },
502
+ "dark": {
503
+ "$value": "var(--color-accent)"
504
+ }
424
505
  }
425
506
  }
426
507
  }