remarque-tokens 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_RULES.md CHANGED
@@ -62,12 +62,14 @@ These are not guidelines. Agents must follow them literally.
62
62
  - Within the centered column, use `.content-reading` for prose-width sections (46rem, auto-centered within the 72rem column)
63
63
  - 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`
64
64
  - Code blocks (`<pre>`) inherit the width of their parent container — no special wrapping needed when the parent is already constrained
65
+ - 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."
65
66
 
66
67
  ### Color
67
68
 
68
69
  - Accent color is used for exactly two things: inline links and one interactive element per viewport
69
70
  - Dark mode is not an inverted light mode — it must be independently tuned for readability
70
71
  - Background colors use `--color-bg`, never pure white (#fff) or pure black (#000)
72
+ - State colors (`--color-error`/`--color-success`/`--color-warning`/`--color-disabled`, plus the `-subtle` banner-background companions on the first three) are for feedback moments only — form validation, status banners, disabled controls — never for decoration. The one-accent rule above still governs everything else. See REMARQUE.md "State Colors."
71
73
 
72
74
  ### Components
73
75
 
@@ -212,6 +214,8 @@ Before considering any implementation complete, verify:
212
214
  - [ ] Muted text placed on `--color-surface` still meets 4.5:1 (check the surface pairing, not just bg)
213
215
  - [ ] All transitions use the motion duration tokens (reduced-motion support depends on it)
214
216
  - [ ] Skip-to-content link present and functional
217
+ - [ ] If state colors are used (error/success/warning/disabled): only for feedback moments (validation, banners, disabled controls), never decoration; every state text color and `-subtle` companion passes `npm run audit`
218
+ - [ ] No bare `z-index` numbers anywhere in the page's CSS — every stacking layer references `tokens-core.css`'s `--z-*` scale
215
219
  - [ ] OG meta tags present (og:title, og:description, og:image)
216
220
  - [ ] `<html lang="en">` attribute set
217
221
  - [ ] The site looks more like a book than a web app
package/CHANGELOG.md CHANGED
@@ -4,6 +4,68 @@ 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.17.0 — 2026-07-23
8
+
9
+ Semantic state colors + z-index scale (closes #26, closes #29).
10
+
11
+ ### Added
12
+ - **State colors (`tokens-palette.css`, PALETTE tier)** — four semantic slots for feedback
13
+ moments (form validation, status banners, disabled controls), never decoration; the
14
+ one-accent rule still governs everything else. Hand-authored from the house ANSI hue
15
+ conventions, both themes, all three dark-mode conventions (`@media
16
+ prefers-color-scheme: dark`, `[data-theme="dark"]`, `:root.dark`):
17
+ - `--color-error` (hue 25, ANSI red) — light `oklch(0.52 0.12 25)` 5.44:1 bg / 5.28:1
18
+ surface; dark `oklch(0.62 0.11 26)` 5.07:1 bg / 4.82:1 surface.
19
+ - `--color-success` (hue 145, ANSI green) — light `oklch(0.51 0.12 145)` 5.08:1 bg /
20
+ 4.93:1 surface; dark `oklch(0.61 0.11 145)` 5.36:1 bg / 5.10:1 surface.
21
+ - `--color-warning` (hue 85, ANSI yellow) — light `oklch(0.52 0.105 85)` 5.17:1 bg /
22
+ 5.02:1 surface; dark `oklch(0.62 0.11 85)` 5.29:1 bg / 5.03:1 surface. The hard case:
23
+ yellow's low luminance-contrast means this slot solves noticeably darker in light
24
+ mode than error/success, the same shape as `--color-syntax-constant`'s solve.
25
+ - `--color-disabled` — aliased to `var(--color-muted)` in both themes: neutral
26
+ hue-80 muted family, deliberately **not** ANSI-derived.
27
+ - `-subtle` banner-background companions on the first three (`--color-error-subtle`/
28
+ `--color-success-subtle`/`--color-warning-subtle`, modeled on `--color-accent-subtle`
29
+ — near-bg lightness, state hue, low chroma) verified so `--color-fg` stays ≥4.5:1 on
30
+ them (the pairing that matters for callout/banner body text). No `-subtle` for
31
+ `--color-disabled` — a disabled control is quieter, not tinted.
32
+ - Every state text color ≥4.5:1 against `--color-bg` **and** `--color-surface`, both
33
+ themes; `scripts/audit.mjs` gains the 11 new pairings, `scripts/test-audit.mjs`'s
34
+ fixtures cover all seven tokens plus a must-fail case (`state-color-fails.css`).
35
+ - `scripts/theme.mjs` (`remarque-theme`) derives error/success/warning from a source
36
+ theme's red/green/yellow ANSI slots — keep-if-passing lightness checked against the
37
+ **stricter** of `--color-bg`/`--color-surface` at once (a new dual-target solver,
38
+ `keepOrSolveDual`/`solveDual`), not just one target like every other slot. Disabled
39
+ aliases the already-derived `--color-muted`. `-subtle` companions derive like
40
+ `--color-accent-subtle` (fixed 0.95 / bg+0.06 starting lightness — not the theme's
41
+ own solved bg lightness, which can exceed 0.95), nudged toward the extreme only if a
42
+ pathological theme doesn't already clear 4.5:1 against `--color-fg`. Self-verify
43
+ `CHECKS` mirrors `audit.mjs`'s new pairings exactly.
44
+ - `scripts/palette-golden.mjs` golden-gates all seven new tokens for free (it already
45
+ iterates whatever the bridge emits) — ΔE2000 ≤ 2.0 against `remarque-light`/
46
+ `remarque-dark`'s ANSI derivation, same identity/serialization contract as the rest
47
+ of the default palette. All new tokens land at ΔE ≤ 0.5 (see the PR description for
48
+ the full per-token table).
49
+ - REMARQUE.md "State Colors" — semantics, usage table, derivation note; Enforcement
50
+ Checklist gains the state-color pairing line.
51
+ - **Z-index scale (`tokens-core.css`, CORE tier)** — stacking is structural, not
52
+ personalization. A small ordinal scale, sparse gaps of 10: `--z-base` (0), `--z-sticky`
53
+ (10), `--z-dropdown` (20), `--z-overlay` (30), `--z-modal` (40), `--z-toast` (50),
54
+ `--z-skip-link` (60, tops everything). Wired into `essay.css`'s sticky TOC rail
55
+ (`z-index: var(--z-sticky)`) and the demo site's skip-to-content link
56
+ (`focus:z-[var(--z-skip-link)]`, replacing a hardcoded Tailwind `focus:z-50`).
57
+ `theme.css`'s header comment documents the Tailwind v4 workaround — v4 has no
58
+ `--z-index-*` theme namespace, so `z-*` utilities can't map to these directly; use
59
+ arbitrary values (`z-[var(--z-modal)]`) instead, same pattern as the motion durations.
60
+ REMARQUE.md "Stacking" — the scale, and the rule that consumer CSS must never author a
61
+ bare `z-index` number. AGENT_RULES.md's Layout/Quality Checklist sections gain matching
62
+ lines.
63
+
64
+ ### Changed
65
+ - **`scripts/tokens-json.mjs`** — `typeOf()` now types `--z-*` tokens as `number`
66
+ (unitless stacking-order integers), matching the existing `--leading-*` treatment,
67
+ instead of falling through to the generic `string` type.
68
+
7
69
  ## 0.16.0 — 2026-07-23
8
70
 
9
71
  Broadsheet pattern — masthead, lead article, numbered entry list, post-header kicker, graduated from the flagship (closes #36).
package/REMARQUE.md CHANGED
@@ -90,6 +90,23 @@ The audit remains the gate regardless of provenance — `remarque-theme` self-ve
90
90
 
91
91
  ---
92
92
 
93
+ ## State Colors
94
+
95
+ Four semantic slots — `--color-error`, `--color-success`, `--color-warning`, `--color-disabled` — for feedback moments: form validation, status banners, disabled controls. **These are for feedback, not decoration.** The one-accent rule ("Visual Rules" / "Changing the Accent Hue") still governs expressive color everywhere else — a state color says "this specific thing failed/succeeded/needs attention/can't be used right now," not "this section is important." Don't recolor headings, borders, or chrome with a state color; reserve it for the actual feedback element (an inline error message, a success toast, a disabled button's text).
96
+
97
+ Three of the four carry a `-subtle` companion — `--color-error-subtle`, `--color-success-subtle`, `--color-warning-subtle` — a faint tinted background for callout/banner-style feedback (an error banner, a success toast), sized the same way `--color-accent-subtle` is: near-`--color-bg` lightness, the state's hue, low chroma. `--color-disabled` has no `-subtle` companion — a disabled control is quieter than its enabled state, not tinted; use `--color-surface`/`--color-bg-subtle` for a disabled control's background if it needs one.
98
+
99
+ | Slot | Use for | `-subtle` companion |
100
+ |---|---|---|
101
+ | `--color-error` | Validation errors, failure states, destructive-action confirmation text | `--color-error-subtle` (banner/callout background) |
102
+ | `--color-success` | Confirmation messages, completed states | `--color-success-subtle` |
103
+ | `--color-warning` | Caution messages, states needing attention before proceeding | `--color-warning-subtle` |
104
+ | `--color-disabled` | Text/icon color for a disabled control | — (no subtle background; see above) |
105
+
106
+ **Derivation:** hand-authored in `tokens-palette.css` from the house ANSI hue conventions — error hue 25 (red), success hue 145 (green), warning hue 85 (yellow) — the same conventions `--color-syntax-string`/`--color-syntax-constant` already use for green/yellow. `scripts/remarque-theme` derives all three from a source theme's `red`/`green`/`yellow` ANSI slots (keep-if-passing lightness, checked against the **stricter** of `--color-bg` and `--color-surface` at once, not just one), exactly like every other slot in "Color Providers." `--color-disabled` is deliberately **not** ANSI-derived — it aliases the already-derived neutral `--color-muted` family, since a disabled state is a muted/quiet register, not a hue. Warning (hue 85, yellow) is the hard case in light mode — yellow has low luminance-contrast against a light background, so its solved lightness sits noticeably darker than the other two states, the same shape as `--color-syntax-constant`'s solve. Every state text color holds ≥ 4.5:1 against `--color-bg` **and** `--color-surface` in both themes; every `-subtle` background holds `--color-fg` ≥ 4.5:1 on it (the pairing that matters for a callout's body text) — both enforced by `scripts/audit.mjs` and golden-gated (ΔE2000 ≤ 2.0) against the upstream themes like the rest of the default palette.
107
+
108
+ ---
109
+
93
110
  ## Font Slots
94
111
 
95
112
  Remarque uses a three-slot font system. Each slot has a strict role, and each slot accepts a small set of approved faces — swap the face, keep the role.
@@ -365,6 +382,26 @@ Motion in Remarque is nearly invisible. The only permitted motion:
365
382
 
366
383
  ---
367
384
 
385
+ ## Stacking
386
+
387
+ Stacking order is structural, not personalization — `tokens-core.css` (CORE tier, never overridden) carries a small ordinal `--z-*` scale:
388
+
389
+ | Token | Value | Use for |
390
+ |---|---:|---|
391
+ | `--z-base` | 0 | Default stacking context — the implicit baseline |
392
+ | `--z-sticky` | 10 | Sticky headers/rails (essay.css's TOC rail) |
393
+ | `--z-dropdown` | 20 | Dropdown/select menus |
394
+ | `--z-overlay` | 30 | Overlays/backdrops |
395
+ | `--z-modal` | 40 | Modal dialogs |
396
+ | `--z-toast` | 50 | Toast/notification stacks |
397
+ | `--z-skip-link` | 60 | The skip-to-content link — tops everything; it must never be obscured when focused |
398
+
399
+ **The rule: never a bare `z-index` number in consumer CSS.** Reference one of these seven tokens instead — an unnamed `z-index: 999` (or any other asserted integer) can't be reasoned about against the rest of the page's stacking contexts, and it's the same "hardcoded value bypasses the token system" problem as a literal hex color or a bare font-size. The scale is deliberately sparse (gaps of 10) so a future layer can be inserted between two existing rungs without renumbering everything above it.
400
+
401
+ Tailwind v4 ships `z-*` as a static, non-themable utility scale — there's no `--z-index-*` entry in its default `@theme` to map these into. `theme.css` (the Tailwind v4 adapter) documents the workaround in its header comment: use arbitrary values with the tokens, `z-[var(--z-modal)]` / `focus:z-[var(--z-skip-link)]`, the same pattern already used for motion durations (`duration-[var(--motion-fast)]`).
402
+
403
+ ---
404
+
368
405
  ## USWDS Accessibility Compliance
369
406
 
370
407
  Remarque adopts typography and accessibility standards from the [US Web Design System (USWDS)](https://designsystem.digital.gov/documentation/accessibility/) and [WCAG 2.1](https://www.w3.org/WAI/WCAG21/Understanding/):
@@ -419,6 +456,8 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
419
456
  - [ ] No hardcoded hex/rgb colors — only `var(--color-*)` tokens.
420
457
  - [ ] Body line-height ≥ 1.5 (Remarque target: 1.75).
421
458
  - [ ] Every `--color-syntax-*` slot ≥ 4.5:1 against `--color-code-bg` in both themes (see "Syntax Highlighting").
459
+ - [ ] Every `--color-error`/`--color-success`/`--color-warning`/`--color-disabled` ≥ 4.5:1 against `--color-bg` **and** `--color-surface` in both themes; every `-subtle` background keeps `--color-fg` ≥ 4.5:1 on it (see "State Colors"). State colors used only for feedback moments, never decoration.
460
+ - [ ] No bare `z-index` number in consumer CSS — reference `tokens-core.css`'s `--z-*` scale (see "Stacking").
422
461
 
423
462
  Agents reviewing PRs should reject changes that violate any line above without explicit rationale.
424
463
 
package/essay.css CHANGED
@@ -421,6 +421,9 @@
421
421
  align-self: start;
422
422
  justify-self: start;
423
423
  position: sticky;
424
+ /* --z-sticky (tokens-core.css, issue #29) — never a bare z-index
425
+ number in consumer CSS. */
426
+ z-index: var(--z-sticky);
424
427
  top: var(--space-6);
425
428
  width: 100%;
426
429
  max-height: calc(100vh - var(--space-8));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
package/scripts/audit.mjs CHANGED
@@ -122,6 +122,20 @@ const CHECKS = [
122
122
  ['color-syntax-punctuation', 'color-code-bg', 4.5, 'syntax: punctuation'],
123
123
  ['color-syntax-variable', 'color-code-bg', 4.5, 'syntax: variable'],
124
124
  ['color-syntax-link', 'color-code-bg', 4.5, 'syntax: link'],
125
+ // Semantic state colors (issue #26) — feedback moments, not decoration.
126
+ ['color-error', 'color-bg', 4.5, 'state: error text'],
127
+ ['color-error', 'color-surface', 4.5, 'state: error text on surface'],
128
+ ['color-success', 'color-bg', 4.5, 'state: success text'],
129
+ ['color-success', 'color-surface', 4.5, 'state: success text on surface'],
130
+ ['color-warning', 'color-bg', 4.5, 'state: warning text'],
131
+ ['color-warning', 'color-surface', 4.5, 'state: warning text on surface'],
132
+ ['color-disabled', 'color-bg', 4.5, 'state: disabled text'],
133
+ ['color-disabled', 'color-surface', 4.5, 'state: disabled text on surface'],
134
+ // -subtle banner backgrounds: the pairing that matters is --color-fg on
135
+ // the subtle background (callout/banner body text), not the state color.
136
+ ['color-fg', 'color-error-subtle', 4.5, 'state: fg on error-subtle banner bg'],
137
+ ['color-fg', 'color-success-subtle', 4.5, 'state: fg on success-subtle banner bg'],
138
+ ['color-fg', 'color-warning-subtle', 4.5, 'state: fg on warning-subtle banner bg'],
125
139
  ];
126
140
 
127
141
  for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
package/scripts/theme.mjs CHANGED
@@ -387,6 +387,101 @@ function deriveSyntaxLink(accent, codeBg, dir) {
387
387
  return keepOrSolve(accent[0], accent[1], accent[2], codeBg, 4.5, dir);
388
388
  }
389
389
 
390
+ /* ── Semantic state colors (issue #26) ────────────────────────────────
391
+ * error/success/warning are derived from the theme's red/green/yellow ANSI
392
+ * slots — the same "ANSI colors already carry this domain's personality"
393
+ * argument as the syntax slots above, applied to feedback-moment colors
394
+ * instead of code tokens. Unlike every other keep-if-passing slot, the
395
+ * target here is TWO backgrounds at once (--color-bg and --color-surface,
396
+ * per the #26 scope comment: "keep-if-passing vs both") — a state color
397
+ * must hold 4.5:1 against whichever of the two is stricter, not just one.
398
+ * disabled is NOT ANSI — it aliases the already-derived neutral muted
399
+ * family, same as tokens-palette.css's hand-authored default.
400
+ */
401
+
402
+ /* Dual-target variant of keepOrSolve/solveL: passes only when BOTH bg and
403
+ * surface clear `target` at the candidate L. Mirrors their guarded-nudge
404
+ * fallback shape, just checking two ratios instead of one. */
405
+ function solveLDual(C, H, bg, surface, target, dir) {
406
+ const searchTarget = target * 1.03;
407
+ let lo = 0, hi = 1;
408
+ for (let i = 0; i < 40; i++) {
409
+ const mid = (lo + hi) / 2;
410
+ const c = fitChroma(mid, C, H);
411
+ const passes = ratio([mid, c, H], bg) >= searchTarget && ratio([mid, c, H], surface) >= searchTarget;
412
+ if (dir === 'darker') { if (passes) lo = mid; else hi = mid; }
413
+ else { if (passes) hi = mid; else lo = mid; }
414
+ }
415
+ let L = r3(dir === 'darker' ? lo : hi);
416
+ let c = fitChroma(L, C, H);
417
+ let guard = 0;
418
+ while ((ratio([L, c, H], bg) < target || ratio([L, c, H], surface) < target) && guard < 100) {
419
+ const next = dir === 'darker' ? r3(L - 0.001) : r3(L + 0.001);
420
+ if (next < 0 || next > 1 || next === L) break;
421
+ L = next;
422
+ c = fitChroma(L, C, H);
423
+ guard++;
424
+ }
425
+ const ok = ratio([L, c, H], bg) >= target && ratio([L, c, H], surface) >= target;
426
+ if (ok) return { L, C: c, H: r1(H), ok };
427
+ return { L, C: c, H: r1(H), ok: false };
428
+ }
429
+
430
+ function solveDual(C, H, bg, surface, target, dir) {
431
+ let r = solveLDual(C, H, bg, surface, target, dir);
432
+ if (r.ok) return [r.L, r.C, r.H];
433
+ // Chroma-collapse fallback, same rationale as solveL's.
434
+ r = solveLDual(0, H, bg, surface, target, dir);
435
+ if (r.ok) return [r.L, r.C, r.H];
436
+ die(
437
+ `internal error: cannot solve a state color for a ${target}:1 target against both bg and surface ` +
438
+ `(hue ${H.toFixed(1)}, direction ${dir}) even with chroma collapsed to 0 — pathological theme colors`
439
+ );
440
+ }
441
+
442
+ /* keep-if-passing against BOTH bg and surface at once. */
443
+ function keepOrSolveDual(L0, C, H, bg, surface, target, dir) {
444
+ const L = Math.min(1, Math.max(0, r3(L0)));
445
+ const c = fitChroma(L, C, H);
446
+ if (ratio([L, c, H], bg) >= target && ratio([L, c, H], surface) >= target) return [L, c, r1(H)];
447
+ return solveDual(C, H, bg, surface, target, dir);
448
+ }
449
+
450
+ const STATE_CHROMA_CAP = 0.14; // same restrained ceiling as --color-accent / syntax slots
451
+
452
+ function deriveStateColor(t, ansiName, label, bg, surface, dir) {
453
+ const [L0, C0, H0] = slotLch(t, ansiName, label);
454
+ const C = Math.min(C0, STATE_CHROMA_CAP);
455
+ return keepOrSolveDual(L0, C, H0, bg, surface, 4.5, dir);
456
+ }
457
+
458
+ /* -subtle companions: near-bg lightness (mirrors --color-accent-subtle
459
+ * EXACTLY — same fixed 0.95 / bg+0.06 starting points, not a theme-
460
+ * relative "bg's own lightness" — accent-subtle is the identity this
461
+ * mirrors, and its light-side constant is 0.95 regardless of how high a
462
+ * given theme's own solved bg lightness lands), state hue, low chroma —
463
+ * verified so --color-fg stays >= 4.5:1 on the result (the pairing that
464
+ * matters for a callout/banner's body text), not re-verified against bg/
465
+ * surface (it's a background, not a text color). `dir` 'darker' (light
466
+ * theme): subtle starts at 0.95. 'lighter' (dark theme): subtle starts at
467
+ * bg + 0.06. Nudged further toward the extreme (whiter in light, blacker
468
+ * in dark) only if a pathological theme's hue/chroma combination doesn't
469
+ * already clear 4.5:1 against fg at that starting lightness. */
470
+ function deriveStateSubtle(bgL, H, chromaCap, fg, dir) {
471
+ let L = dir === 'darker' ? 0.95 : r3(Math.min(bgL + 0.06, 1));
472
+ let c = fitChroma(L, chromaCap, H);
473
+ let guard = 0;
474
+ while (ratio(fg, [L, c, H]) < 4.5 && guard < 200) {
475
+ L = dir === 'darker' ? r3(Math.min(1, L + 0.001)) : r3(Math.max(0, L - 0.001));
476
+ c = fitChroma(L, chromaCap, H);
477
+ guard++;
478
+ }
479
+ if (ratio(fg, [L, c, H]) < 4.5) {
480
+ die(`internal error: cannot solve a state-subtle background (hue ${H.toFixed(1)}, dir ${dir}) that holds --color-fg >= 4.5:1 — pathological theme colors`);
481
+ }
482
+ return [L, c, r1(H)];
483
+ }
484
+
390
485
  /* ── LIGHT derivation ─────────────────────────────────────────────── */
391
486
 
392
487
  function deriveLight(t) {
@@ -425,6 +520,15 @@ function deriveLight(t) {
425
520
  const synPunctuation = deriveSyntaxPunctuation(fgC, fgH, codeBg, 'darker');
426
521
  const synVariable = deriveSyntaxVariable(fg, codeBg, 'darker', 0.08);
427
522
  const synLink = deriveSyntaxLink(accent, codeBg, 'darker');
523
+ // Semantic state colors (issue #26) — see the derivation helpers above.
524
+ // error/success/warning: keep-if-passing vs BOTH bg and surface at once;
525
+ // disabled: aliased to the already-derived muted family, not ANSI.
526
+ const stateError = deriveStateColor(t, 'red', 'light', bg, surface, 'darker');
527
+ const stateSuccess = deriveStateColor(t, 'green', 'light', bg, surface, 'darker');
528
+ const stateWarning = deriveStateColor(t, 'yellow', 'light', bg, surface, 'darker');
529
+ const stateErrorSubtle = deriveStateSubtle(bg[0], stateError[2], 0.02, fg, 'darker');
530
+ const stateSuccessSubtle = deriveStateSubtle(bg[0], stateSuccess[2], 0.02, fg, 'darker');
531
+ const stateWarningSubtle = deriveStateSubtle(bg[0], stateWarning[2], 0.02, fg, 'darker');
428
532
  const raw = {
429
533
  'color-bg': bg,
430
534
  'color-bg-subtle': [r3(bg[0] - 0.02), bg[1], bg[2]],
@@ -450,6 +554,13 @@ function deriveLight(t) {
450
554
  'color-syntax-punctuation': synPunctuation,
451
555
  'color-syntax-variable': synVariable,
452
556
  'color-syntax-link': synLink,
557
+ 'color-error': stateError,
558
+ 'color-error-subtle': stateErrorSubtle,
559
+ 'color-success': stateSuccess,
560
+ 'color-success-subtle': stateSuccessSubtle,
561
+ 'color-warning': stateWarning,
562
+ 'color-warning-subtle': stateWarningSubtle,
563
+ 'color-disabled': { ref: 'color-muted' },
453
564
  };
454
565
  const tokens = {};
455
566
  for (const [name, v] of Object.entries(raw)) tokens[name] = v.ref ? `var(--${v.ref})` : fmt(v);
@@ -503,6 +614,14 @@ function deriveDark(t, accentHueLight) {
503
614
  const synPunctuation = deriveSyntaxPunctuation(fgC, fgH, codeBg, 'lighter');
504
615
  const synVariable = deriveSyntaxVariable(fg, codeBg, 'lighter', -0.08);
505
616
  const synLink = deriveSyntaxLink(accent, codeBg, 'lighter');
617
+ // Semantic state colors (issue #26) — mirrors the light derivation,
618
+ // direction flipped ('lighter': text on a dark bg/surface).
619
+ const stateError = deriveStateColor(t, 'red', 'dark', bg, surface, 'lighter');
620
+ const stateSuccess = deriveStateColor(t, 'green', 'dark', bg, surface, 'lighter');
621
+ const stateWarning = deriveStateColor(t, 'yellow', 'dark', bg, surface, 'lighter');
622
+ const stateErrorSubtle = deriveStateSubtle(bg[0], stateError[2], 0.04, fg, 'lighter');
623
+ const stateSuccessSubtle = deriveStateSubtle(bg[0], stateSuccess[2], 0.04, fg, 'lighter');
624
+ const stateWarningSubtle = deriveStateSubtle(bg[0], stateWarning[2], 0.04, fg, 'lighter');
506
625
  const raw = {
507
626
  'color-bg': bg,
508
627
  'color-bg-subtle': surface,
@@ -528,6 +647,13 @@ function deriveDark(t, accentHueLight) {
528
647
  'color-syntax-punctuation': synPunctuation,
529
648
  'color-syntax-variable': synVariable,
530
649
  'color-syntax-link': synLink,
650
+ 'color-error': stateError,
651
+ 'color-error-subtle': stateErrorSubtle,
652
+ 'color-success': stateSuccess,
653
+ 'color-success-subtle': stateSuccessSubtle,
654
+ 'color-warning': stateWarning,
655
+ 'color-warning-subtle': stateWarningSubtle,
656
+ 'color-disabled': { ref: 'color-muted' },
531
657
  };
532
658
  const tokens = {};
533
659
  for (const [name, v] of Object.entries(raw)) tokens[name] = v.ref ? `var(--${v.ref})` : fmt(v);
@@ -564,6 +690,17 @@ const CHECKS = [
564
690
  ['color-syntax-punctuation', 'color-code-bg', 4.5, 'syntax: punctuation'],
565
691
  ['color-syntax-variable', 'color-code-bg', 4.5, 'syntax: variable'],
566
692
  ['color-syntax-link', 'color-code-bg', 4.5, 'syntax: link'],
693
+ ['color-error', 'color-bg', 4.5, 'state: error text'],
694
+ ['color-error', 'color-surface', 4.5, 'state: error text on surface'],
695
+ ['color-success', 'color-bg', 4.5, 'state: success text'],
696
+ ['color-success', 'color-surface', 4.5, 'state: success text on surface'],
697
+ ['color-warning', 'color-bg', 4.5, 'state: warning text'],
698
+ ['color-warning', 'color-surface', 4.5, 'state: warning text on surface'],
699
+ ['color-disabled', 'color-bg', 4.5, 'state: disabled text'],
700
+ ['color-disabled', 'color-surface', 4.5, 'state: disabled text on surface'],
701
+ ['color-fg', 'color-error-subtle', 4.5, 'state: fg on error-subtle banner bg'],
702
+ ['color-fg', 'color-success-subtle', 4.5, 'state: fg on success-subtle banner bg'],
703
+ ['color-fg', 'color-warning-subtle', 4.5, 'state: fg on warning-subtle banner bg'],
567
704
  ];
568
705
 
569
706
  function resolveRaw(raw, name, seen = new Set()) {
@@ -22,6 +22,7 @@ function typeOf(name, value) {
22
22
  if (name.startsWith('weight-')) return 'fontWeight';
23
23
  if (name.startsWith('motion-') && /ms$/.test(value)) return 'duration';
24
24
  if (name.startsWith('leading-')) return 'number';
25
+ if (name.startsWith('z-')) return 'number'; // unitless stacking-order integers, not a length
25
26
  if (value === '0') return 'dimension';
26
27
  if (/(^|\s)(rem|px|em|%)/.test(value) || /^-?[\d.]+(rem|px|em)$/.test(value) || value.startsWith('clamp(')) return 'dimension';
27
28
  return 'string';
package/theme.css CHANGED
@@ -27,6 +27,14 @@
27
27
  * Pair every duration-[var(--motion-*)] utility with ease-remarque
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
+ *
31
+ * Z-index utilities: Tailwind v4 ships z-* as a static, non-themable
32
+ * scale (there is no --z-index-* entry in its default @theme, so there
33
+ * is nothing here to map into). Use arbitrary values with the tokens
34
+ * instead — z-[var(--z-modal)], focus:z-[var(--z-skip-link)] — same
35
+ * pattern as the motion durations above, so every stacking layer stays
36
+ * traceable to tokens-core.css's --z-* scale (issue #29) instead of a
37
+ * bare Tailwind number.
30
38
  */
31
39
 
32
40
  @theme inline {
package/tokens-core.css CHANGED
@@ -80,6 +80,22 @@
80
80
  --motion-fast: 120ms; /* hover states, focus rings */
81
81
  --motion-normal: 180ms; /* theme transitions */
82
82
  --motion-easing: ease-out;
83
+
84
+ /* Z-Index Scale (issue #29) — stacking is structural, not
85
+ personalization: CORE tier, never overridden. A small ordinal scale,
86
+ sparse gaps of 10 so a future layer can be inserted between two
87
+ existing rungs without renumbering everything above it. Consumer CSS
88
+ must never author a bare z-index number — reference one of these
89
+ seven tokens instead (Tailwind v4 has no --z-index-* theme namespace,
90
+ so its z-* utilities can't reference these directly; see theme.css's
91
+ header comment for the arbitrary-value bridge, z-[var(--z-modal)]). */
92
+ --z-base: 0;
93
+ --z-sticky: 10; /* essay.css's TOC rail, sticky headers/rails */
94
+ --z-dropdown: 20;
95
+ --z-overlay: 30;
96
+ --z-modal: 40;
97
+ --z-toast: 50;
98
+ --z-skip-link: 60; /* tops everything — a focused skip-link must never be obscured */
83
99
  }
84
100
 
85
101
  /* ─── Reduced Motion ────────────────────────────────────── */
@@ -94,6 +94,29 @@
94
94
  --color-syntax-punctuation: oklch(0.52 0.01 80); /* derived neutral, muted family — 4.69:1 */
95
95
  --color-syntax-variable: oklch(0.26 0.01 80); /* fg-adjacent (slight L offset) — 13.23:1 */
96
96
  --color-syntax-link: var(--color-accent); /* accent already clears code-bg — 4.5:1+ */
97
+
98
+ /* Semantic state colors (issue #26) — feedback moments (error/success/
99
+ warning banners, disabled controls), NOT decoration; the one-accent
100
+ rule still governs expressive color. Hand-authored from the house
101
+ ANSI hue conventions (scripts/theme.mjs derives error/success/warning
102
+ from a theme's red/green/yellow ANSI slots): error hue 25, success
103
+ hue 145, warning hue 85 — restrained chroma, keep-if-passing lightness
104
+ checked against the STRICTER of --color-bg/--color-surface (surface
105
+ is stricter in both themes here, same as --color-muted). Warning at
106
+ hue 85 is the hard case in light mode (low luminance-contrast yellow)
107
+ — solved dark, same register as --color-syntax-constant. Disabled is
108
+ neutral-derived (muted family), NOT ANSI — aliased to --color-muted.
109
+ -subtle companions (error/success/warning only, no disabled-subtle)
110
+ are derived like --color-accent-subtle: near-bg lightness, state hue,
111
+ low chroma — verified so --color-fg stays ≥4.5:1 on them, the pairing
112
+ that matters for callout/banner backgrounds. */
113
+ --color-error: oklch(0.52 0.12 25); /* ANSI red — 5.44:1 bg / 5.28:1 surface */
114
+ --color-error-subtle: oklch(0.95 0.02 25); /* faint red banner background */
115
+ --color-success: oklch(0.51 0.12 145); /* ANSI green — 5.08:1 bg / 4.93:1 surface */
116
+ --color-success-subtle: oklch(0.95 0.02 145); /* faint green banner background */
117
+ --color-warning: oklch(0.52 0.105 85); /* ANSI yellow — 5.17:1 bg / 5.02:1 surface */
118
+ --color-warning-subtle: oklch(0.95 0.02 85); /* faint yellow banner background */
119
+ --color-disabled: var(--color-muted); /* muted family, not ANSI-derived */
97
120
  }
98
121
 
99
122
  /* ─── Color: Dark theme ─────────────────────────────────── */
@@ -139,6 +162,18 @@
139
162
  --color-syntax-punctuation: oklch(0.60 0.005 80); /* derived neutral, muted family — 4.59:1 */
140
163
  --color-syntax-variable: oklch(0.82 0.005 80); /* fg-adjacent (slight L offset) — 10.37:1 */
141
164
  --color-syntax-link: var(--color-accent); /* accent already clears code-bg */
165
+
166
+ /* Semantic state colors (issue #26) — see the light-theme block above
167
+ for the full rationale. Same hues (red 25 / green 145 / yellow 85),
168
+ lightened for a dark background; keep-if-passing checked against
169
+ the stricter of bg/surface (surface, here too). */
170
+ --color-error: oklch(0.62 0.11 26); /* 5.07:1 bg / 4.82:1 surface */
171
+ --color-error-subtle: oklch(0.22 0.04 25);
172
+ --color-success: oklch(0.61 0.11 145); /* 5.36:1 bg / 5.10:1 surface */
173
+ --color-success-subtle: oklch(0.22 0.04 145);
174
+ --color-warning: oklch(0.62 0.11 85); /* 5.29:1 bg / 5.03:1 surface */
175
+ --color-warning-subtle: oklch(0.22 0.04 85);
176
+ --color-disabled: var(--color-muted);
142
177
  }
143
178
  }
144
179
 
@@ -182,4 +217,14 @@
182
217
  --color-syntax-punctuation: oklch(0.60 0.005 80);
183
218
  --color-syntax-variable: oklch(0.82 0.005 80);
184
219
  --color-syntax-link: var(--color-accent);
220
+
221
+ /* Semantic state colors (issue #26) — see the light-theme :root block
222
+ for the full rationale. */
223
+ --color-error: oklch(0.62 0.11 26);
224
+ --color-error-subtle: oklch(0.22 0.04 25);
225
+ --color-success: oklch(0.61 0.11 145);
226
+ --color-success-subtle: oklch(0.22 0.04 145);
227
+ --color-warning: oklch(0.62 0.11 85);
228
+ --color-warning-subtle: oklch(0.22 0.04 85);
229
+ --color-disabled: var(--color-muted);
185
230
  }
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.16.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.17.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
@@ -55,7 +55,14 @@ export type RemarqueCoreToken =
55
55
  | 'border-style'
56
56
  | 'motion-fast'
57
57
  | 'motion-normal'
58
- | 'motion-easing';
58
+ | 'motion-easing'
59
+ | 'z-base'
60
+ | 'z-sticky'
61
+ | 'z-dropdown'
62
+ | 'z-overlay'
63
+ | 'z-modal'
64
+ | 'z-toast'
65
+ | 'z-skip-link';
59
66
 
60
67
  /** All palette-tier token names (tokens-palette.css). The sanctioned personalization surface. */
61
68
  export type RemarquePaletteToken =
@@ -90,7 +97,14 @@ export type RemarquePaletteToken =
90
97
  | 'color-syntax-type'
91
98
  | 'color-syntax-punctuation'
92
99
  | 'color-syntax-variable'
93
- | 'color-syntax-link';
100
+ | 'color-syntax-link'
101
+ | 'color-error'
102
+ | 'color-error-subtle'
103
+ | 'color-success'
104
+ | 'color-success-subtle'
105
+ | 'color-warning'
106
+ | 'color-warning-subtle'
107
+ | 'color-disabled';
94
108
 
95
109
  /** Every Remarque token name, core + palette. */
96
110
  export type RemarqueToken = RemarqueCoreToken | RemarquePaletteToken;
@@ -202,6 +216,13 @@ export interface RemarqueTokenValues {
202
216
  readonly 'motion-fast': "120ms";
203
217
  readonly 'motion-normal': "180ms";
204
218
  readonly 'motion-easing': "ease-out";
219
+ readonly 'z-base': 0;
220
+ readonly 'z-sticky': 10;
221
+ readonly 'z-dropdown': 20;
222
+ readonly 'z-overlay': 30;
223
+ readonly 'z-modal': 40;
224
+ readonly 'z-toast': 50;
225
+ readonly 'z-skip-link': 60;
205
226
  };
206
227
  readonly palette: {
207
228
  readonly 'font-display': {
@@ -332,6 +353,34 @@ export interface RemarqueTokenValues {
332
353
  readonly light: "var(--color-accent)";
333
354
  readonly dark: "var(--color-accent)";
334
355
  };
356
+ readonly 'color-error': {
357
+ readonly light: "oklch(0.52 0.12 25)";
358
+ readonly dark: "oklch(0.62 0.11 26)";
359
+ };
360
+ readonly 'color-error-subtle': {
361
+ readonly light: "oklch(0.95 0.02 25)";
362
+ readonly dark: "oklch(0.22 0.04 25)";
363
+ };
364
+ readonly 'color-success': {
365
+ readonly light: "oklch(0.51 0.12 145)";
366
+ readonly dark: "oklch(0.61 0.11 145)";
367
+ };
368
+ readonly 'color-success-subtle': {
369
+ readonly light: "oklch(0.95 0.02 145)";
370
+ readonly dark: "oklch(0.22 0.04 145)";
371
+ };
372
+ readonly 'color-warning': {
373
+ readonly light: "oklch(0.52 0.105 85)";
374
+ readonly dark: "oklch(0.62 0.11 85)";
375
+ };
376
+ readonly 'color-warning-subtle': {
377
+ readonly light: "oklch(0.95 0.02 85)";
378
+ readonly dark: "oklch(0.22 0.04 85)";
379
+ };
380
+ readonly 'color-disabled': {
381
+ readonly light: "var(--color-muted)";
382
+ readonly dark: "var(--color-muted)";
383
+ };
335
384
  };
336
385
  }
337
386
 
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.16.0",
5
+ "version": "0.17.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"
@@ -177,6 +177,34 @@
177
177
  "motion-easing": {
178
178
  "$value": "ease-out",
179
179
  "$type": "string"
180
+ },
181
+ "z-base": {
182
+ "$value": 0,
183
+ "$type": "number"
184
+ },
185
+ "z-sticky": {
186
+ "$value": 10,
187
+ "$type": "number"
188
+ },
189
+ "z-dropdown": {
190
+ "$value": 20,
191
+ "$type": "number"
192
+ },
193
+ "z-overlay": {
194
+ "$value": 30,
195
+ "$type": "number"
196
+ },
197
+ "z-modal": {
198
+ "$value": 40,
199
+ "$type": "number"
200
+ },
201
+ "z-toast": {
202
+ "$value": 50,
203
+ "$type": "number"
204
+ },
205
+ "z-skip-link": {
206
+ "$value": 60,
207
+ "$type": "number"
180
208
  }
181
209
  },
182
210
  "palette": {
@@ -502,6 +530,69 @@
502
530
  "dark": {
503
531
  "$value": "var(--color-accent)"
504
532
  }
533
+ },
534
+ "color-error": {
535
+ "$type": "color",
536
+ "light": {
537
+ "$value": "oklch(0.52 0.12 25)"
538
+ },
539
+ "dark": {
540
+ "$value": "oklch(0.62 0.11 26)"
541
+ }
542
+ },
543
+ "color-error-subtle": {
544
+ "$type": "color",
545
+ "light": {
546
+ "$value": "oklch(0.95 0.02 25)"
547
+ },
548
+ "dark": {
549
+ "$value": "oklch(0.22 0.04 25)"
550
+ }
551
+ },
552
+ "color-success": {
553
+ "$type": "color",
554
+ "light": {
555
+ "$value": "oklch(0.51 0.12 145)"
556
+ },
557
+ "dark": {
558
+ "$value": "oklch(0.61 0.11 145)"
559
+ }
560
+ },
561
+ "color-success-subtle": {
562
+ "$type": "color",
563
+ "light": {
564
+ "$value": "oklch(0.95 0.02 145)"
565
+ },
566
+ "dark": {
567
+ "$value": "oklch(0.22 0.04 145)"
568
+ }
569
+ },
570
+ "color-warning": {
571
+ "$type": "color",
572
+ "light": {
573
+ "$value": "oklch(0.52 0.105 85)"
574
+ },
575
+ "dark": {
576
+ "$value": "oklch(0.62 0.11 85)"
577
+ }
578
+ },
579
+ "color-warning-subtle": {
580
+ "$type": "color",
581
+ "light": {
582
+ "$value": "oklch(0.95 0.02 85)"
583
+ },
584
+ "dark": {
585
+ "$value": "oklch(0.22 0.04 85)"
586
+ }
587
+ },
588
+ "color-disabled": {
589
+ "$type": "color",
590
+ "light": {
591
+ "$value": "var(--color-muted)"
592
+ },
593
+ "dark": {
594
+ "$value": "var(--color-muted)"
595
+ }
505
596
  }
506
597
  }
507
598
  }