remarque-tokens 0.23.0 → 0.24.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 +1 -1
- package/CHANGELOG.md +22 -0
- package/REMARQUE.md +48 -5
- package/forms.css +48 -0
- package/package.json +1 -1
- package/registry/broadsheet.json +1 -1
- package/registry/essay.json +1 -1
- package/registry/forms.json +3 -3
- package/registry/palette-deck.json +1 -1
- package/registry.json +6 -6
- package/scripts/audit.mjs +10 -4
- package/scripts/drift-check.mjs +8 -3
- package/scripts/lib/css-tokens.mjs +71 -0
- package/scripts/tokens-json.mjs +10 -3
- package/tokens-palette.css +193 -232
- package/tokens.d.ts +1 -1
- package/tokens.json +1 -1
package/AGENT_RULES.md
CHANGED
|
@@ -188,7 +188,7 @@ Three further archetypes — Reference/Docs, Changelog, Gallery — are specifie
|
|
|
188
188
|
|
|
189
189
|
2. **Prose alignment:** The essay header and prose body must BOTH use centered max-width (`max-w-reading mx-auto`) to prevent misalignment. The `.content-reading` class auto-centers; match headers with `mx-auto`.
|
|
190
190
|
|
|
191
|
-
3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode.
|
|
191
|
+
3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode. Since 0.24.0 (`light-dark()` + `color-scheme`, REMARQUE.md "Color Scheme & light-dark()"), the shipped default palette's own `[data-theme="light"]` override is just `color-scheme: light` — a consumer palette that still authors the older two-declaration convention needs the fuller override this rule originally describes.
|
|
192
192
|
|
|
193
193
|
4. **Self-hosted fonts:** Replace the Google Fonts CDN link with self-hosted woff2 files + `@font-face` declarations. Use `font-display: swap` and `<link rel="preload">` for performance.
|
|
194
194
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.24.0 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
`light-dark()` + `color-scheme` palette migration (closes #95) and a `:has()` zero-JS forms enhancement layer (closes #97) — the ratified program's last two items.
|
|
10
|
+
|
|
11
|
+
### Browser support floor — raised (stated prominently per the new Governance & Deprecation "browser-floor" minor case)
|
|
12
|
+
|
|
13
|
+
`light-dark()` and `color-scheme` are baseline-stable but not ancient: this release raises remarque-tokens' minimum supported browser engine to **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. This is a **MINOR**, not a major — no token is removed or renamed, every consumer's `var(--color-x)`/`tokens.json` lookup keeps resolving exactly as before, and `tokens.json`/`tokens.d.ts`/`tokens.schema.json` are byte-identical to 0.23.0 (verified this release, version field aside). Only the rendering environment required to resolve the new CSS *correctly* changes. A site whose audience needs an older engine should stay on `<0.24.0`, or override the affected `--color-*` tokens with plain `oklch()` values (the audit accepts either authoring form). See REMARQUE.md's new "Color Scheme & light-dark()" section and its updated "Semver contract".
|
|
14
|
+
|
|
15
|
+
### Added / Changed
|
|
16
|
+
|
|
17
|
+
- **`tokens-palette.css`: every `--color-*` token is now a single `light-dark(<light>, <dark>)` declaration** under `:root { color-scheme: light dark; }`, replacing the old convention of a full `:root` block plus a full, separately-maintained `@media (prefers-color-scheme: dark)` block re-listing every value a second time. System-preference default needs no color-specific rule at all now; `[data-theme="light"]`/`[data-theme="dark"]` become single `color-scheme` overrides instead of re-declaring every token's dark value; the `:root.dark` compatibility bridge (ratified sunset: 1.0, unchanged) carries the same minimal `color-scheme: dark` override. Values are unchanged — this is purely an authoring-form migration, golden-gated ΔE2000 = 0.000/0.000 against the previous values on every affected token (see `scripts/palette-golden.mjs` output).
|
|
18
|
+
- **`--weight-display` stays on the pre-migration mechanism, on purpose.** `light-dark()` is restricted to `<color>` values; `--weight-display` is a `font-weight` (400/500, dark-mode serif-hairline compensation, issue #59), so it cannot be expressed as `light-dark(400, 500)`. It keeps `@media (prefers-color-scheme: dark)` + `[data-theme="dark"], :root.dark` — now carrying only this one token instead of the whole palette. A registered-custom-property `light-dark()`-abuse workaround was considered and rejected: the conventional blocks are simpler, already audited, and now touch exactly one token — there's no problem left to solve by working around them.
|
|
19
|
+
- **`prefers-contrast: more` (issue #93) simplifies from three blocks to one.** Pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override. Under `light-dark()`, one un-scoped `light-dark(<light>, <dark>)` override per token replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there (system, explicit, or the bridge) — there is no dark-specific selector left to write.
|
|
20
|
+
- **The demo site's `[data-theme="light"]` mirror block is retired**, not merely updated. It used to hand-carry all 21 `--color-*` values from the package's light `:root` so a dark-OS visitor could still toggle back to light (AGENT_RULES Pitfall #3) — a known drift trap (`scripts/palette-golden.mjs`'s "site mirror" check). Explicit light selection is now a bare `color-scheme: light` override shipped IN the package itself; the color values live in exactly one place (the `light-dark()` declaration), so there is no second copy to drift. `site/src/styles/globals.css`'s block shrinks to `[data-theme="light"] { color-scheme: light; }`. `scripts/palette-golden.mjs`'s mirror check now asserts the *absence* of any `--color-*` redeclaration there, rather than comparing two copies of a value that no longer has a second copy.
|
|
21
|
+
- **Parser (`scripts/lib/css-tokens.mjs`): three new exports** — `splitLightDark(value)` (depth-aware split of a `light-dark(a, b)` value into its light/dark sides — a side that is itself `oklch(...)` or `var(...)` doesn't confuse the single top-level comma being looked for), `resolveSide(decls, side)` (collapses a flat decls map to one theme side; a no-op for any value that isn't a `light-dark()` call, which is what keeps every conventional-form palette parsing identically to before), and `darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls)` (merges `light-dark()` dark sides from the top-level `:root` block with whatever a conventional dark block still declares — mixed-form palettes are supported, with the conventional block winning on conflict, matching real CSS cascade order). `scripts/audit.mjs`, `scripts/palette-golden.mjs`, `scripts/drift-check.mjs`, and `scripts/tokens-json.mjs` all route through these; none changed their own contrast/golden/drift logic, only how they extract light/dark values first. **This arguably simplifies the parser long-term**, as the issue predicted: both values now come from one declaration instead of being scattered across two selectors.
|
|
22
|
+
- **Fixture coverage (`scripts/test-audit.mjs`):** a pure `light-dark()` palette fixture, a mixed-form palette fixture (half the tokens migrated, half still in the two-block convention, in the same file), and a must-fail fixture where a `light-dark()` value's DARK side (only) fails contrast — proving the parser actually extracts and enforces both sides of a `light-dark()` declaration, not just the light side. Every pre-existing conventional-form fixture (attribute, class, media, palette-deck, nested/compound-media-not-dark, …) stays green, unmodified.
|
|
23
|
+
- **`scripts/theme.mjs` (the `remarque-theme` bridge) is deliberately unchanged** — still emits the conventional two-block form. Its output is a palette override a consumer imports after this package's own tokens; keeping it on the older, universally-parseable convention is the lower-churn choice (every consumer regardless of remarque-tokens version can consume it, and the parser above already treats both forms identically). Modernizing the bridge's own emission to `light-dark()` is deferred to a later major.
|
|
24
|
+
- **`forms.css`: `:has()` zero-JS validation enhancement layer (issue #97).** A second progressive-enhancement layer over the existing `@supports selector(:user-invalid)` bonus border-recolor — `:user-invalid` alone can only style the input it matches (CSS has no "parent selector" without `:has()`), so it cannot reach the field's label. `.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label` extends the error recolor to the label; `.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label` brightens the label to `--color-fg` as a quiet secondary focus cue (not a replacement for the control's own native `:focus-visible` outline). Both guarded with `@supports selector(:has(...))`, same fallback shape as `:user-invalid` itself. `data-state` remains the canonical contract; neither block replaces authoring `data-state="error"` explicitly.
|
|
25
|
+
- **Registry regenerated** (`registry/forms.json`) — `forms.css`'s content changed (the new `:has()` blocks), single-sourced verbatim as always; no markup-contract change, so `usage.html` and the mechanical contract checks are untouched.
|
|
26
|
+
- **REMARQUE.md:** new "Color Scheme & light-dark()" section (under "Token Tiers") — the full mechanism, the browser-floor bump, the weight-display/mirror/bridge/parser decisions, and the `prefers-contrast` simplification. "Technology Stack" theming bullet, "DTCG Conformance", "Forced Colors & Contrast Preferences", "Governance & Deprecation" (semver contract's new browser-floor minor case; the `:root.dark` deprecation-table row's rationale), and "Forms" ("State wiring") all updated to match.
|
|
27
|
+
- Version-only regeneration of `tokens.json`/`tokens.d.ts` (no token name/value changes — verified byte-identical aside from the version field).
|
|
28
|
+
|
|
7
29
|
## 0.23.0 — 2026-07-23
|
|
8
30
|
|
|
9
31
|
Machine-readable markup-contract registry (closes #100) — the ratified program's flagship item. Funded on the panel's amended rationale: it mechanically prevents the markup-contract transcription bug class (the sidenote DOM-order/aria-label failure found in the flagship migration, #89), not on "category-defining" positioning.
|
package/REMARQUE.md
CHANGED
|
@@ -43,7 +43,7 @@ Remarque projects should feel like a modern technical publication — not a gene
|
|
|
43
43
|
- **CSS framework:** Tailwind CSS — v4 via the shipped `remarque-tokens/theme.css` (`@theme inline` adapter; utilities reference the runtime tokens), or v3 via the shipped `tailwind.config.js`. One mechanism per project, never both.
|
|
44
44
|
- **Component primitives:** shadcn/ui (when reusable components are needed)
|
|
45
45
|
- **Markup:** Semantic HTML with ARIA landmarks
|
|
46
|
-
- **Theming:** Light and dark mode via the `[data-theme]` attribute (canonical
|
|
46
|
+
- **Theming:** Light and dark mode via `color-scheme` + `light-dark()` (issue #95, 0.24.0) — every `--color-*` token is one `light-dark(<light>, <dark>)` declaration under `:root { color-scheme: light dark; }`; system preference resolves automatically, the `[data-theme]` attribute (canonical) overrides `color-scheme` explicitly, and `:root.dark` is a compatibility bridge for class-keyed sites (sunset: 1.0) carrying the same override. **Raises the package's minimum browser floor** to Chrome/Edge 123+, Firefox 120+, Safari 17.5+ — see "Color Scheme & light-dark()". The audit parses both the new single-declaration form and the older two-declaration convention (mixed within one file, too). Mind the specificity asymmetry on the one remaining non-color theme-conditional token (`--weight-display`): `[data-theme=\"dark\"]` is (0,1,0), `:root.dark` is (0,2,0).
|
|
47
47
|
- **Accessibility:** USWDS-informed. Keyboard navigation, skip-to-content link, ARIA labels, WCAG AA contrast compliance, 44px touch targets, 14px minimum small text
|
|
48
48
|
- **Tokens:** Centralized CSS custom properties in two tiers — `tokens-core.css` (immutable identity) + `tokens-palette.css` (sanctioned personalization). `tokens.css` aggregates both. See "Token Tiers"
|
|
49
49
|
- **Fonts:** Self-hosted woff2 (no CDN dependency). Preloaded via `<link rel="preload">`
|
|
@@ -68,6 +68,31 @@ Package subpaths for consumers: `remarque-tokens` (aggregator), `remarque-tokens
|
|
|
68
68
|
|
|
69
69
|
This makes compliance mechanical: a site that overrides only palette-tier tokens is *authored*; one that touches core-tier tokens has *forked*. `tokens.css` imports both tiers, so existing consumers are unaffected.
|
|
70
70
|
|
|
71
|
+
### Color Scheme & `light-dark()`
|
|
72
|
+
|
|
73
|
+
**0.24.0, closes #95.** Every `--color-*` token in `tokens-palette.css` is now authored as a single `light-dark(<light>, <dark>)` declaration under `:root { color-scheme: light dark; }`, replacing the previous convention of a full `:root` block plus a full, separately-maintained `@media (prefers-color-scheme: dark)` block re-listing every value a second time. This is the platform mechanism earlier releases of this file named as "the endgame to watch" — now adopted.
|
|
74
|
+
|
|
75
|
+
**Browser support floor — raised, stated prominently per "Governance & Deprecation":** `light-dark()` and `color-scheme` are baseline-stable but not ancient: **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. A site whose audience needs to support an older engine should stay on remarque-tokens `<0.24.0`, or override the affected `--color-*` tokens with plain `oklch()` values in its own palette (the audit accepts either form — see "How the parser handles this" below). This is the MINOR-with-rationale case the Governance section's semver contract now names explicitly: no token was removed or renamed, and every consumer's `var(--color-x)` still resolves — only the minimum browser engine that renders it *correctly* moved forward, which is why this ships as a minor rather than a major.
|
|
76
|
+
|
|
77
|
+
**How theme selection now works:**
|
|
78
|
+
- **System preference (no explicit choice).** `color-scheme: light dark` on `:root` is sufficient by itself — every `light-dark()` call resolves from `prefers-color-scheme` directly. No `@media (prefers-color-scheme: dark)` color block is needed anymore; it and its full duplicated value list are **retired**.
|
|
79
|
+
- **Explicit user choice.** `[data-theme="light"]`/`[data-theme="dark"]` now override `color-scheme` itself, once — not every token's dark value a second time. `[data-theme="dark"] { color-scheme: dark; }` / `[data-theme="light"] { color-scheme: light; }`.
|
|
80
|
+
- **`:root.dark` compatibility bridge.** Unchanged status — still bridges class-keyed consumer sites, still sunsets at 1.0 (see the Deprecation windows table). It carries the same `color-scheme: dark` override as `[data-theme="dark"]`, kept **minimal** by design: colors need nothing else now that they resolve through `color-scheme`.
|
|
81
|
+
|
|
82
|
+
**`--weight-display` is the one exception — and stays on the old mechanism, on purpose.** `light-dark()` is restricted to `<color>` values; `--weight-display` is a `font-weight` (400/500, dark-mode serif-hairline compensation, issue #59), so it cannot be expressed as `light-dark(400, 500)`. It keeps the pre-migration `@media (prefers-color-scheme: dark)` + `[data-theme="dark"], :root.dark` blocks — now carrying only this one token instead of the whole palette, which is the honest characterization: this isn't a compromise or a leftover, it is genuinely the smallest correct mechanism available for a non-color theme-conditional token. A registered-property `light-dark()`-abuse trick was considered and rejected — the conventional blocks are simpler, already-audited, and touch exactly one token; inventing a workaround to avoid two small blocks would be solving a non-problem.
|
|
83
|
+
|
|
84
|
+
**The `prefers-contrast: more` override (issue #93) simplifies from three blocks to one.** Pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override — three copies of two tokens, to cover system-default, explicit-dark, and system-dark-without-an-explicit-choice respectively. Under `light-dark()`, one un-scoped `light-dark(<light>, <dark>)` override per token replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there — there is no dark-specific selector left to write. See `tokens-palette.css`'s `@media (prefers-contrast: more)` block.
|
|
85
|
+
|
|
86
|
+
**The site's `[data-theme="light"]` mirror — retired, not merely updated.** Before this migration, an explicit light choice had no mechanism of its own, so the demo site hand-carried a full `[data-theme="light"]` block re-declaring every `--color-*` value (a known drift trap, guarded by `scripts/palette-golden.mjs`'s "site mirror" check). Explicit light selection is now a bare `color-scheme: light` override — the color values live in exactly one place (the `light-dark()` declaration itself) — so there is no second copy left to drift. The mirror block is deleted from `site/src/styles/globals.css`; `scripts/palette-golden.mjs`'s mirror check now asserts the *absence* of any `--color-*` redeclaration there instead of comparing two copies of a value that no longer has a second copy.
|
|
87
|
+
|
|
88
|
+
**How the parser handles this (`scripts/lib/css-tokens.mjs`).** `splitLightDark(value)` depth-aware-splits a `light-dark(a, b)` value into its light/dark sides (so a side that is itself `oklch(...)` or `var(...)` doesn't confuse the single top-level comma being looked for). `resolveSide(decls, side)` collapses a flat decls map to one theme side — a no-op for any value that isn't a `light-dark()` call, which is what makes this fully backward compatible: a palette using only the old two-block convention parses identically to before. `darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls)` merges the `light-dark()` dark sides found in the top-level `:root` block with whatever a conventional dark block still declares (mixed-form palettes are supported — a consumer may migrate some tokens and leave others in the old convention in the same file), with the conventional-block value winning on conflict — matching real CSS cascade order (a later dark-block declaration overrides an earlier `:root` one at equal specificity). `scripts/audit.mjs`, `scripts/palette-golden.mjs`, `scripts/drift-check.mjs`, and `scripts/tokens-json.mjs` all route through these three functions now; none of them changed their own contrast/golden/drift logic, only how they extract light/dark values first. Fixture-tested in `scripts/test-audit.mjs`: a pure `light-dark()` palette, a mixed-form palette (some tokens migrated, some not), a `light-dark()` value that fails contrast on its dark side, and every pre-existing conventional-form fixture, all green.
|
|
89
|
+
|
|
90
|
+
**`scripts/theme.mjs` (the `remarque-theme` bridge) is intentionally unchanged — still emits the conventional two-block form.** Its output is a *palette override* a consumer imports after this package's own tokens; keeping it on the older, more universally-parseable convention is the lower-churn choice — every consumer regardless of remarque-tokens version can consume it, and the parser above already treats both forms identically. It is not a compromise either: modernizing the bridge's own emission to `light-dark()` is deferred to a later major, noted here so it isn't silently forgotten.
|
|
91
|
+
|
|
92
|
+
**`tokens.json` is unaffected in content.** The authoring-form change is purely how the CSS expresses a value, not what the value or token name is — regenerating `tokens.json`/`tokens.d.ts`/`tokens.schema.json` from the migrated CSS produces byte-identical output (aside from the `version` field), verified in this release.
|
|
93
|
+
|
|
94
|
+
**Visual regression: zero baseline changes.** Every `--color-*` value is unchanged — only the CSS mechanism producing it moved. The Playwright visual-regression suite's screenshot baselines are expected to be pixel-identical; any diff would indicate a transcription error in the migration, not an intended appearance change.
|
|
95
|
+
|
|
71
96
|
### DTCG Conformance
|
|
72
97
|
|
|
73
98
|
`tokens-core.css` and `tokens-palette.css` remain the single source of truth; `scripts/tokens-json.mjs` regenerates `tokens.json`, `tokens.d.ts`, and `tokens.schema.json` from them in one pass (`node scripts/tokens-json.mjs`; `--check` gates CI freshness). `tokens.json` carries a `$schema` pointer to the published schema (served by the demo site at `/tokens.schema.json`, alongside `/tokens.json` — also exported as `remarque-tokens/tokens.schema.json`).
|
|
@@ -75,7 +100,7 @@ This makes compliance mechanical: a site that overrides only palette-tier tokens
|
|
|
75
100
|
`tokens.json` is **conformant in spirit** with the [Design Tokens Community Group (DTCG)](https://www.designtokens.org/) format — every token carries `$value`/`$type` — with two **deliberate** structural divergences, documented in the generated file itself (`$extensions.remarque.dtcg`) so a future regeneration can't lose the rationale:
|
|
76
101
|
|
|
77
102
|
1. **Color values are `oklch()` CSS strings**, not the DTCG structured color object (`{ colorSpace, components, alpha }`). Remarque's color pipeline (audit, drift-check, `remarque-theme`) is built entirely on parsing/emitting `oklch(L C H)` strings; adopting the structured form would mean carrying two color representations in parallel for no present benefit. **Gated on:** the DTCG color `$type` structured-value format ratifying.
|
|
78
|
-
2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus a modes/resolver mechanism. The DTCG spec has no ratified multi-mode/theming primitive yet; this repo's own theming (
|
|
103
|
+
2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus a modes/resolver mechanism. The DTCG spec has no ratified multi-mode/theming primitive yet; this repo's own theming (`light-dark()` + `color-scheme`, `[data-theme="dark"]`, the Palette Deck's `[data-palette]` scoping — see "Color Scheme & light-dark()") predates any such draft. **Gated on:** the DTCG multi-mode / resolver draft ratifying.
|
|
79
104
|
|
|
80
105
|
Full DTCG conformance is gated on those two drafts ratifying — a named, checkable trigger, not "someday." Until then, do not "fix" `tokens.json` toward either unratified draft; the CSS is the source of truth, and both divergences are load-bearing for the tooling that already depends on this shape.
|
|
81
106
|
|
|
@@ -496,8 +521,9 @@ Issue #93 — Remarque cites [USWDS](https://designsystem.digital.gov/documentat
|
|
|
496
521
|
|
|
497
522
|
**`prefers-contrast: more`.** The cheap sibling — a *softer*, opt-in signal (no system-color override; the page keeps its own palette) that only ever widens an already-passing margin. Bumps exactly two tokens one step further toward `--color-fg`: `--color-border-bold` (light 3.39:1 → 4.52:1, dark 3.23:1 → 4.53:1) and `--color-fg-muted` (light 7.55:1 → 10.11:1, dark 7.26:1 → 9.70:1). `--color-border` is deliberately untouched — it's decorative by design, and a user who wants its hairlines to read as structure is the `forced-colors: active` audience above, not this one.
|
|
498
523
|
|
|
499
|
-
- **Architectural home:** `tokens-palette.css`, immediately after the
|
|
500
|
-
- **
|
|
524
|
+
- **Architectural home:** `tokens-palette.css`, immediately after the theme-selection mechanism — these are `--color-*` values, palette tier by the Token Tiers contract, the same tier the base `light-dark()`/`color-scheme` regime already lives in.
|
|
525
|
+
- **Simplified to one block under `light-dark()` (0.24.0, issue #95):** pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override — three copies of two tokens, covering system-default, explicit-dark, and system-dark-without-an-explicit-choice respectively. A single un-scoped `light-dark(<light>, <dark>)` override per token now replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there (system, explicit, or the `:root.dark` bridge) — see "Color Scheme & light-dark()".
|
|
526
|
+
- **Audit-parser compatibility:** `scripts/lib/css-tokens.mjs`'s `isLightRoot` filter requires an empty at-rule context — a `@media (prefers-contrast: more)` block's context is that at-rule itself, so it matches neither `isLightRoot` nor `isDarkBlock`, and `scripts/audit.mjs`/`scripts/tokens-json.mjs` silently skip these declarations, unchanged from before the light-dark() migration. That's still correct: the block only ever strictly improves an already-passing contrast margin, so there is nothing for the automated gate to enforce beyond what the base palette already guarantees. The values are still hand-verified in-gamut and re-computed with the same OKLCH → sRGB → WCAG luminance math `scripts/audit.mjs` uses (see the ratios in `tokens-palette.css`'s comment).
|
|
501
527
|
|
|
502
528
|
**CI gate.** `site/tests/forced-colors.spec.ts` runs Chromium with `page.emulateMedia({ forcedColors: 'active' })` and asserts computed styles (not pixels): a nav link's focus ring renders a non-`none` `outline-style` with non-zero width, `.remarque-prose table` borders resolve to a non-transparent `border-color`, a form field in `data-state="error"` gets a `border-style` distinct from its default state, and the essay module's TOC rail / sidenote borders stay visible. Wired into the existing `visual-regression.yml` workflow (`npx playwright test` already runs the whole `site/tests/` directory) — this is a **blocking** condition for #93, not evidence-only; manual WHCM screenshots are a nice supplement, not a substitute.
|
|
503
529
|
|
|
@@ -1149,6 +1175,22 @@ with an invalid field — it never replaces authoring `data-state="error"`
|
|
|
1149
1175
|
explicitly, since `:user-invalid` alone has no matching text-message
|
|
1150
1176
|
recolor and no `aria-invalid` wiring of its own.
|
|
1151
1177
|
|
|
1178
|
+
A second `:has()`-powered bonus layer (issue #97) goes one step further:
|
|
1179
|
+
`:user-invalid` alone can only style the input it matches — CSS has no
|
|
1180
|
+
"parent selector" without `:has()`, so it cannot reach the field's label.
|
|
1181
|
+
`.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label`
|
|
1182
|
+
recolors the label to `--color-error` too, and
|
|
1183
|
+
`.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label`
|
|
1184
|
+
brightens the label to `--color-fg` while any control in the field has
|
|
1185
|
+
keyboard focus — a quiet secondary cue, not a replacement for the
|
|
1186
|
+
control's own native `:focus-visible` outline. Both are guarded with
|
|
1187
|
+
`@supports selector(:has(...))` (the same "the browser lacking this
|
|
1188
|
+
falls back to `data-state`/`aria-invalid`" shape as `:user-invalid`
|
|
1189
|
+
itself) and, like that layer, are bonus visual cues rather than one of
|
|
1190
|
+
the canonical `data-state` states — `data-state` stays the one
|
|
1191
|
+
contract every consumer must author explicitly; `:has()` never replaces
|
|
1192
|
+
it, only adds a zero-JS enhancement on top for browsers that support it.
|
|
1193
|
+
|
|
1152
1194
|
### Restraint rules restated
|
|
1153
1195
|
|
|
1154
1196
|
- **Border radius**: form controls cap at `--radius-sm` (4px) — a
|
|
@@ -1562,6 +1604,7 @@ Remarque ships as a CSS token system, not an application — "breaking" means so
|
|
|
1562
1604
|
- **MAJOR** — a token is **removed** or **renamed**. Anything a consumer's `var(--color-x)` or a `tokens.json` lookup resolves today must keep resolving after a minor or patch bump; only a major bump may take that away.
|
|
1563
1605
|
- **MINOR** — a token's **value changes**, or a token is **added**. Value changes always carry a stated rationale in the CHANGELOG entry (a contrast fix, a gamut correction, an upstream dataset repin) — the change is minor because the *name* stays stable and every consumer reads it through `var()` indirection, but it is never silent. Additive changes (new tokens, new optional CLI flags, new subpath exports) are minor by definition — nothing existing changes shape.
|
|
1564
1606
|
- **PATCH** — tool/script fixes with no token or CSS output change (a `remarque-audit` parser bug, a `tokens-json.mjs` generation fix, a fixture correction). If `tokens.json`/`tokens.d.ts`'s content is byte-identical before and after, it's a patch.
|
|
1607
|
+
- **MINOR (browser-floor case)** — a change that raises the *minimum supported browser version* by adopting a newer CSS mechanism as load-bearing (0.24.0's `light-dark()`/`color-scheme` migration, issue #95, is the first instance). Still a minor, not a major: no token is removed or renamed, and every consumer's `var(--color-x)`/`tokens.json` lookup keeps resolving — only the rendering environment required to resolve it *correctly* changes. The new floor is stated prominently in both the CHANGELOG entry and the relevant REMARQUE.md section, not buried in prose.
|
|
1565
1608
|
|
|
1566
1609
|
The project is still pre-1.0 — under strict SemVer every 0.x release is technically allowed to break anything — but every release to date has in practice held to the contract above (no token has ever been removed or renamed; see the version history in CHANGELOG.md). 1.0 is the point where that becomes a promise instead of an observed pattern.
|
|
1567
1610
|
|
|
@@ -1571,7 +1614,7 @@ A token or convention slated for removal is announced in the CHANGELOG at least
|
|
|
1571
1614
|
|
|
1572
1615
|
| Deprecated / bridged item | Introduced | Sunset target | Rationale |
|
|
1573
1616
|
|---|---|---|---|
|
|
1574
|
-
| `:root.dark` compatibility selector (alongside canonical `[data-theme="dark"]`) | 0.5.0 | 1.0 | Bridges class-keyed consumer sites onto the attribute-keyed convention `remarque-audit` treats as canonical — the audit already parses both forms, so the bridge costs nothing today.
|
|
1617
|
+
| `:root.dark` compatibility selector (alongside canonical `[data-theme="dark"]`) | 0.5.0 | 1.0 | Bridges class-keyed consumer sites onto the attribute-keyed convention `remarque-audit` treats as canonical — the audit already parses both forms, so the bridge costs nothing today. `color-scheme` + `light-dark()` — once "the platform endgame to watch" — landed in 0.24.0 (see "Color Scheme & light-dark()"); the bridge itself is unaffected, carrying a `color-scheme: dark` override now instead of a full color list, and its sunset target is unchanged. |
|
|
1575
1618
|
|
|
1576
1619
|
No other open sunset commitments exist as of this writing (grepped against REMARQUE.md and CHANGELOG.md for "sunset"/"deprecated"). This table is meant to be the single place to look — but only if every future commitment is added here, not just announced in passing.
|
|
1577
1620
|
|
package/forms.css
CHANGED
|
@@ -358,6 +358,54 @@ textarea.remarque-input {
|
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/* ─── `:has()` zero-JS validation enhancement layer (issue #97) ──────────
|
|
362
|
+
* A second, purely additive progressive-enhancement layer over the
|
|
363
|
+
* `:user-invalid` layer just above — same native-first shape, one step
|
|
364
|
+
* further. `:user-invalid` alone can only style the INPUT it matches;
|
|
365
|
+
* CSS has no "parent selector" without `:has()`, so it has no way to
|
|
366
|
+
* reach the field's label or wrapper. `:has()` is exactly the piece that
|
|
367
|
+
* closes that gap: `.remarque-field` can react to a descendant control's
|
|
368
|
+
* validity/focus state directly, with no `data-state` attribute and no
|
|
369
|
+
* JavaScript. Guarded with `@supports selector(:has(...))` — the same
|
|
370
|
+
* "degrades to the no-JS/no-support baseline" shape the `:user-invalid`
|
|
371
|
+
* block above uses — so a browser missing either pseudo-class in the
|
|
372
|
+
* compound query falls back cleanly to whatever `data-state`/
|
|
373
|
+
* `aria-invalid` (and, where supported, the plain `:user-invalid` layer
|
|
374
|
+
* above) already provide. `data-state` remains the CANONICAL contract;
|
|
375
|
+
* neither block below replaces authoring `data-state="error"` explicitly
|
|
376
|
+
* — see REMARQUE.md "State wiring".
|
|
377
|
+
*
|
|
378
|
+
* Like the bare `:user-invalid` layer above, this is a bonus visual cue,
|
|
379
|
+
* not one of the canonical `data-state` states the forced-colors
|
|
380
|
+
* reinforcement below was scoped to fix (REMARQUE.md "Forced Colors &
|
|
381
|
+
* Contrast Preferences") — it is not itself a new color-only-affordance
|
|
382
|
+
* regression, since the real accessible signal (`aria-invalid`, the
|
|
383
|
+
* `data-state` border/message wiring) is unchanged and unaffected by
|
|
384
|
+
* whether this bonus layer's color survives forced-colors mode.
|
|
385
|
+
*/
|
|
386
|
+
@supports selector(:has(:user-invalid)) {
|
|
387
|
+
/* Extends the :user-invalid border recolor (input only, above) up to
|
|
388
|
+
the field's label — the one thing plain :user-invalid cannot reach
|
|
389
|
+
on its own. */
|
|
390
|
+
.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label {
|
|
391
|
+
color: var(--color-error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* Field-level focus affordance — a quiet secondary cue, NOT a
|
|
396
|
+
replacement for the control's own native `:focus-visible` outline
|
|
397
|
+
(tokens-core.css), which already renders on the control itself and is
|
|
398
|
+
forced-colors-safe by construction. Brightens the label from
|
|
399
|
+
`--color-muted` to `--color-fg` while any control inside the field has
|
|
400
|
+
keyboard focus — a restrained color wash, not a new border/background,
|
|
401
|
+
matching this module's "no component should be visually louder than
|
|
402
|
+
the content it contains" rule (AGENT_RULES.md Components). */
|
|
403
|
+
@supports selector(:has(:focus-visible)) {
|
|
404
|
+
.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label {
|
|
405
|
+
color: var(--color-fg);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
361
409
|
/* ─── Forced-colors reinforcement (issue #93) ────────────────────────────
|
|
362
410
|
* The validation-state wiring above is the textbook color-only-affordance
|
|
363
411
|
* case the issue names explicitly: error/success/warning are distinguished
|
package/package.json
CHANGED
package/registry/broadsheet.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "remarque:contract",
|
|
5
5
|
"title": "Broadsheet Pattern",
|
|
6
6
|
"description": "The editorial Landing/archive pattern: masthead, lead article, numbered entry list, and post-header kicker. Entry numerals are generated from `data-entry-number` via `attr()`, never `counter()`, and every kicker/dateline row is true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.24.0",
|
|
8
8
|
"integrity": "sha256-BZea38PRdob4OfmdGN0eDw3U0adl7SEsdrjKgSewK9A=",
|
|
9
9
|
"dependencies": [
|
|
10
10
|
"remarque-tokens"
|
package/registry/essay.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "remarque:contract",
|
|
5
5
|
"title": "Essay Module",
|
|
6
6
|
"description": "Sidenotes + sticky TOC rail for long-form Essay pages: margin notes relocated into real DOM order, right after the paragraph that cites them, numbered by CSS counter and labeled `aria-label=\"Note N\"` in the same order the counter advances — the mechanism that prevents the sidenote aria-label/DOM-order transcription bug found in the flagship migration (#89, williamzujkowski.github.io#380).",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.24.0",
|
|
8
8
|
"integrity": "sha256-zr04H0+0xf01OWLk6B2A9ziZOYJd4Sq5q5/cib65lbU=",
|
|
9
9
|
"dependencies": [
|
|
10
10
|
"remarque-tokens"
|
package/registry/forms.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"type": "remarque:contract",
|
|
5
5
|
"title": "Forms Primitives",
|
|
6
6
|
"description": "Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.",
|
|
7
|
-
"version": "0.
|
|
8
|
-
"integrity": "sha256-
|
|
7
|
+
"version": "0.24.0",
|
|
8
|
+
"integrity": "sha256-WfYjcf04Dxxx7X21U5b6DrY90Qw95p0zkCQszoh77tE=",
|
|
9
9
|
"dependencies": [
|
|
10
10
|
"remarque-tokens"
|
|
11
11
|
],
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"files": [
|
|
50
50
|
{
|
|
51
51
|
"path": "forms.css",
|
|
52
|
-
"content": "/*\n * Remarque — Form Control Primitives (issue #27)\n * ────────────────────────────────────────────────\n * Namespaced classes that style native form elements — no fake replacement\n * widgets. A `<select>` is a `<select>`, a checkbox stays a checkbox with\n * `accent-color` doing the recoloring work a browser already supports\n * natively. This module exists because Remarque is editorial-first: forms\n * are for the contact/search/newsletter moments a written-word site\n * occasionally needs, not a component library for building app UIs. See\n * \"When NOT to use\" in REMARQUE.md's \"Forms\" section before reaching for\n * this on a page that wants more than a handful of fields. Also carries a\n * small standalone-table section (`.remarque-table`) at the end of this\n * file — see that section's header comment for why it lives here rather\n * than in prose.css (issue #30's reference `Table.astro` needs the same\n * table visual language OUTSIDE `.remarque-prose`).\n *\n * PROVENANCE — spec-native, not graduated from a downstream site (unlike\n * essay.css/broadsheet.css). Built directly from issue #27; there was no\n * prior flagship implementation to re-express in token vocabulary, so\n * every value below is original to this module rather than a mapped-over\n * site literal.\n *\n * Own subpath (remarque-tokens/forms, remarque-tokens/forms.css) — NOT\n * included by the tokens.css aggregator, matching essay.css/broadsheet.css/\n * print.css: most Remarque pages have zero forms. Import explicitly, after\n * prose.css if both are in use:\n * @import 'remarque-tokens/prose.css';\n * @import 'remarque-tokens/forms.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <div class=\"remarque-field\">\n * <label class=\"remarque-field-label\" for=\"email\">Email</label>\n * <input class=\"remarque-input\" type=\"email\" id=\"email\" name=\"email\"\n * aria-describedby=\"email-help\" />\n * <p class=\"remarque-field-message\" id=\"email-help\">\n * We'll never share your email.\n * </p>\n * </div>\n *\n * <div class=\"remarque-field\" data-state=\"error\">\n * <label class=\"remarque-field-label\" for=\"email2\">Email</label>\n * <input class=\"remarque-input\" type=\"email\" id=\"email2\" name=\"email2\"\n * aria-invalid=\"true\" aria-describedby=\"email2-message\" required />\n * <p class=\"remarque-field-message\" id=\"email2-message\" role=\"alert\">\n * Enter a valid email address.\n * </p>\n * </div>\n *\n * <label class=\"remarque-checkbox\">\n * <input type=\"checkbox\" name=\"subscribe\" />\n * <span>Subscribe to updates</span>\n * </label>\n *\n * <label class=\"remarque-radio\">\n * <input type=\"radio\" name=\"plan\" value=\"a\" />\n * <span>Plan A</span>\n * </label>\n *\n * <button class=\"remarque-button\" type=\"button\">Cancel</button>\n * <button class=\"remarque-button remarque-button--primary\" type=\"submit\">\n * Submit\n * </button>\n *\n * `data-state` lives on `.remarque-field` (not the input) — it recolors\n * both the control's border AND the message text together from one\n * attribute, the same \"one attribute drives several descendants\" shape\n * essay.css's counter-driven numbering uses. `aria-invalid=\"true\"` is the\n * REAL accessibility signal (screen readers announce it); `data-state` is\n * the visual layer on top. Author both together — see \"State wiring\"\n * below for what happens if a consumer only sets one.\n */\n\n/* ─── Field wrapper: label + control + help/error text stack ────────────\n * A vertical rhythm of three parts. `--space-2` between label and control\n * (tight — they read as one unit), `--space-2` again before the message\n * line. No border, no background on the wrapper itself — `.remarque-field`\n * is layout only, never a bordered \"card\" around the control (Components:\n * \"no component should be visually louder than the content it contains\").\n */\n.remarque-field {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n max-width: var(--content-reading);\n}\n\n.remarque-field + .remarque-field {\n margin-top: var(--space-5);\n}\n\n/* Label voice: same declarations as `.text-label` (tokens-core.css),\n repeated rather than depended-on-as-a-mixin — the same non-mixin\n precedent essay.css's TOC summary and broadsheet.css's kicker rows\n establish. True small caps (`font-variant-caps`), never `text-transform:\n uppercase` (REMARQUE.md \"Small Caps\"). Quiet mono metadata voice for the\n label, not the body face — a form label identifies the field, the VALUE\n typed into it is the content, and that distinction is the same one\n `.text-label` already draws for dates/tags/status elsewhere. */\n.remarque-field-label {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n font-variant-caps: all-small-caps;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n}\n\n/* Required-field marker: a plain asterisk in the accent color is the one\n sanctioned use here (it is unambiguously \"this is the interactive thing\n that needs attention right now\" — not decoration). Author it inside the\n label: <label>Email <span class=\"remarque-field-required\"\n aria-hidden=\"true\">*</span></label> — aria-hidden because `required` on\n the input itself is the real signal a screen reader announces. */\n.remarque-field-required {\n color: var(--color-accent);\n}\n\n/* Help/error/success message line — meta voice, body-face-adjacent size\n floor (--text-meta, 14px, clears the USWDS small-text minimum). Neutral\n (muted) by default; recolored by `.remarque-field[data-state]` below.\n `text-wrap: pretty` matches `.remarque-prose`'s optical default for\n short justified-feeling message copy. */\n.remarque-field-message {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n letter-spacing: var(--tracking-meta);\n color: var(--color-muted);\n margin: 0;\n text-wrap: pretty;\n}\n\n/* ─── Text inputs: input / textarea / select ─────────────────────────────\n * One shared class across all three native elements — same border,\n * radius, spacing, and body-voice typography (Font Slots: \"Body ... UI\n * labels, navigation, buttons\" — form values are UI labels' closest\n * relative here, not metadata). `--color-border-bold`, not `-bold`'s\n * quieter sibling: an input's boundary is a FUNCTIONAL edge (WCAG 1.4.11\n * non-text contrast), the same reasoning `.remarque-sidenote`'s narrow-\n * viewport border and `.remarque-toc-rail`'s hairline both already apply\n * decoratively-vs-functionally in essay.css.\n */\n.remarque-input {\n display: block;\n width: 100%;\n min-height: 44px; /* USWDS/WCAG 2.5.5 touch-target floor — see AGENT_RULES.md */\n padding: var(--space-2) var(--space-3);\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-title);\n color: var(--color-fg);\n background-color: var(--color-bg);\n border: var(--border-width) var(--border-style) var(--color-border-bold);\n /* Restraint rule (AGENT_RULES.md \"Border radius never exceeds\n --radius-md\" + this module's own tighter floor): form controls stay\n at --radius-sm, never the 8px ceiling other chrome is allowed. A\n smaller control reads as more precise at a smaller radius. */\n border-radius: var(--radius-sm);\n transition: border-color var(--motion-fast) var(--motion-easing);\n}\n\ntextarea.remarque-input {\n min-height: 6rem; /* several lines tall by default — still clears the 44px floor */\n resize: vertical;\n}\n\n/* Native select's disclosure triangle is left as the platform default —\n restyling it means either `appearance: none` (which then requires a\n hand-drawn replacement icon, exactly the \"fake replacement\" this module\n avoids) or accepting a single browser's rendering as the reference,\n neither of which is worth it for one triangle. */\n.remarque-input:hover {\n border-color: var(--color-fg-muted);\n}\n\n/* Placeholder: --color-muted, the same 4.5:1-on-bg/-surface token used\n everywhere else at this contrast tier — a conscious choice, not the\n browser default (which is often far below AA). Placeholder text is\n supplementary (never the only label — every `.remarque-input` pairs\n with a real `<label>`), so 4.5:1 rather than `--color-fg-muted`'s 7:1\n is the correct tier: AA is sufficient for non-essential hint copy. */\n.remarque-input::placeholder {\n color: var(--color-muted);\n opacity: 1; /* Firefox lowers placeholder opacity by default; pin it so the token value is what actually renders */\n}\n\n.remarque-input:disabled {\n color: var(--color-disabled);\n background-color: var(--color-bg-subtle);\n border-color: var(--color-border);\n cursor: not-allowed;\n}\n\n/* ─── Checkbox / Radio — accent-color, no fake replacements ──────────────\n * `accent-color` is the ENTIRE recoloring mechanism: no `appearance:\n * none`, no hand-drawn checkmark/dot, no absolutely-positioned pseudo-\n * element standing in for the native control. This is a deliberate\n * ceiling on customization, not an oversight — every browser Remarque\n * targets supports `accent-color` on checkboxes/radios, and a real native\n * control keeps every platform accessibility behavior (keyboard,\n * high-contrast mode, forced-colors mode, screen-reader state\n * announcements) for free, at the cost of not controlling the checkmark\n * glyph's exact shape.\n *\n * Sizing: --space-5 (1.5rem / 24px) — comfortably inside the 44px touch\n * target once the wrapping <label> below adds its own min-height, and\n * large enough to keep the accent-colored fill legible (a 16px default\n * checkbox reads as fussy at this system's generous spacing scale).\n */\n.remarque-checkbox,\n.remarque-radio {\n display: inline-flex;\n align-items: center;\n gap: var(--space-2);\n min-height: 44px; /* touch-target floor on the whole label, same shape as .nav-link */\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-title);\n color: var(--color-fg);\n cursor: pointer;\n}\n\n.remarque-checkbox input,\n.remarque-radio input {\n accent-color: var(--color-accent);\n width: var(--space-5);\n height: var(--space-5);\n margin: 0;\n flex-shrink: 0;\n}\n\n/* `:has()` is a light progressive enhancement here, not a functional\n dependency — a browser without it still gets a fully working, natively\n disabled control; it just skips the muted label-text recolor. */\n.remarque-checkbox:has(input:disabled),\n.remarque-radio:has(input:disabled) {\n color: var(--color-disabled);\n cursor: not-allowed;\n}\n\n.remarque-checkbox input:disabled,\n.remarque-radio input:disabled {\n cursor: not-allowed;\n}\n\n/* ─── Buttons ─────────────────────────────────────────────────────────────\n * Quiet by rule (AGENT_RULES.md Components: \"Buttons are quiet: text-only\n * or subtle bordered. Never filled/solid as default.\"). The default\n * variant is bordered, transparent background, body-voice text — reads as\n * \"a clickable label,\" not a colored block. `--primary` is the ONE\n * sanctioned accent placement per viewport (Visual Rules: accent for\n * \"inline links and one interactive element per viewport\") — even there,\n * it stays unfilled: accent text + accent border, never a solid accent\n * fill, so the same restraint the default variant holds carries through\n * to the emphasized one. Hover on `--primary` washes in\n * `--color-accent-subtle` (the same quiet-highlight token\n * `.remarque-prose mark` already uses) rather than darkening to a filled\n * block — a wash, not a fill.\n */\n.remarque-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-2);\n min-height: 44px;\n padding: var(--space-2) var(--space-4);\n font-family: var(--font-body);\n font-size: var(--text-body);\n font-weight: var(--weight-medium);\n line-height: var(--leading-title);\n color: var(--color-fg);\n background-color: transparent;\n border: var(--border-width) var(--border-style) var(--color-border-bold);\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition:\n color var(--motion-fast) var(--motion-easing),\n border-color var(--motion-fast) var(--motion-easing),\n background-color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-button:hover:not(:disabled) {\n border-color: var(--color-fg-muted);\n color: var(--color-fg);\n}\n\n.remarque-button--primary {\n color: var(--color-accent);\n border-color: var(--color-accent);\n}\n\n.remarque-button--primary:hover:not(:disabled) {\n color: var(--color-accent-hover);\n border-color: var(--color-accent-hover);\n background-color: var(--color-accent-subtle);\n}\n\n.remarque-button:disabled {\n color: var(--color-disabled);\n border-color: var(--color-border);\n background-color: transparent;\n cursor: not-allowed;\n}\n\n/* ─── Validation state wiring (0.17.0 state-color tokens) ────────────────\n * `data-state` lives on `.remarque-field`, cascading down to the input's\n * border and the message line's color together — author it alongside a\n * real `aria-invalid=\"true\"`/`aria-describedby` pair on the input (the\n * markup contract above); `data-state` alone changes only the paint,\n * `aria-invalid` is what a screen reader actually announces. `--warning`\n * covers a caution/needs-attention message that isn't yet a hard failure\n * (REMARQUE.md \"State Colors\" — \"states needing attention before\n * proceeding\"), e.g. a password-strength hint. No `-subtle` background is\n * used here: `-subtle` is sized for banner/callout backgrounds (REMARQUE.md\n * \"State Colors\"), and painting a per-field background would make the\n * field louder than the content it holds (Components rule) — border +\n * message-text color is enough signal at this scale.\n */\n.remarque-field[data-state=\"error\"] .remarque-input,\n.remarque-input[aria-invalid=\"true\"] {\n border-color: var(--color-error);\n}\n\n.remarque-field[data-state=\"error\"] .remarque-field-message {\n color: var(--color-error);\n}\n\n.remarque-field[data-state=\"success\"] .remarque-input {\n border-color: var(--color-success);\n}\n\n.remarque-field[data-state=\"success\"] .remarque-field-message {\n color: var(--color-success);\n}\n\n.remarque-field[data-state=\"warning\"] .remarque-input {\n border-color: var(--color-warning);\n}\n\n.remarque-field[data-state=\"warning\"] .remarque-field-message {\n color: var(--color-warning);\n}\n\n/* `:user-invalid` — native, zero-JS validity feedback (matches only after\n the user has interacted with the field and left it invalid, unlike\n `:invalid`, which can match a required-but-untouched field on page\n load). Guarded with `@supports selector(...)`: a browser lacking the\n pseudo-class simply never matches this block and falls back to\n whatever `data-state`/`aria-invalid` already provide — the same\n \"degrades to the no-JS/no-support baseline\" shape essay.css's `@media`\n gate uses for its own progressive enhancement. This is a bonus layer,\n not a replacement for authoring `data-state=\"error\"` explicitly. */\n@supports selector(:user-invalid) {\n .remarque-input:user-invalid {\n border-color: var(--color-error);\n }\n}\n\n/* ─── Forced-colors reinforcement (issue #93) ────────────────────────────\n * The validation-state wiring above is the textbook color-only-affordance\n * case the issue names explicitly: error/success/warning are distinguished\n * ENTIRELY by `border-color`/`color`, both of which `forced-colors: active`\n * remaps to a system color regardless of which state authored them — so\n * all three states (and the plain, untouched control) would render\n * IDENTICALLY under forced colors, exactly the failure `aria-invalid` +\n * `role=\"alert\"` already prevent for screen-reader users but do nothing\n * for a sighted low-vision user in Windows High Contrast Mode.\n *\n * Fix is geometry, not color, since color is exactly what forced-colors\n * discards: each state gets a distinct `border-style`/`border-width`\n * combination on the control itself (a shape that reads under ANY forced\n * system border color), plus a small glyph prefixed to the message line\n * — scoped to this media query only, so the quiet default (message text\n * alone) is unchanged everywhere else. The glyph is supplementary, not\n * the primary signal (a screen reader user already gets `aria-invalid` +\n * the message text; a sighted forced-colors user gets the border shape);\n * it is not marked `aria-hidden` because CSS generated content cannot\n * carry that attribute, but it is inert in the accessibility tree by\n * default in every current UA/AT combination tested (Chromium+NVDA,\n * Chromium+VoiceOver) — it is not read as if it were message content.\n */\n@media (forced-colors: active) {\n .remarque-field[data-state=\"error\"] .remarque-input,\n .remarque-input[aria-invalid=\"true\"] {\n border-style: double;\n border-width: 3px;\n }\n\n .remarque-field[data-state=\"warning\"] .remarque-input {\n border-style: dashed;\n border-width: 2px;\n }\n\n .remarque-field[data-state=\"success\"] .remarque-input {\n border-style: solid;\n border-width: 2px;\n }\n\n .remarque-field[data-state=\"error\"] .remarque-field-message::before {\n content: \"\\2715\\0020\"; /* ✕ */\n }\n\n .remarque-field[data-state=\"warning\"] .remarque-field-message::before {\n content: \"\\26A0\\0020\"; /* ⚠ */\n }\n\n .remarque-field[data-state=\"success\"] .remarque-field-message::before {\n content: \"\\2713\\0020\"; /* ✓ */\n }\n}\n\n/* ─── Standalone data tables (issue #30) ──────────────────────────────────\n * `.remarque-table` re-scopes prose.css's table rules (mono `th` voice,\n * 2px `border-bottom` under the header row, 1px row rules only, `.num`\n * for tabular-lining right-aligned numeric columns) under a top-level\n * class instead of `.remarque-prose table`. This is intentionally a\n * near-verbatim copy of those declarations, not a shared mixin (CSS has\n * none) and not a requirement to wrap a standalone data table in the full\n * `.remarque-prose` container — `.remarque-prose` also applies oldstyle\n * proportional numerals, `> * + *` paragraph-spacing rhythm, and prose\n * max-width, none of which a reference data grid (e.g. `Table.astro`)\n * wants. Living in forms.css rather than a new subpath: this module is\n * already \"the supplementary-UI module\" in AGENT_RULES.md's build order\n * (step 7 groups buttons, tags, cards, code blocks, AND tables together),\n * and a table this small does not justify its own subpath the way the\n * Essay/Broadsheet modules' much larger surface area did.\n *\n * Wrap a wide table exactly like prose.css's convention:\n * <div class=\"remarque-table-wrap\"><table class=\"remarque-table\">...</table></div>\n */\n.remarque-table-wrap {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.remarque-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--text-body);\n}\n\n.remarque-table caption {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-meta);\n color: var(--color-muted);\n text-align: left;\n caption-side: top;\n margin-bottom: var(--space-2);\n}\n\n.remarque-table th {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-meta);\n font-weight: var(--weight-semibold);\n text-align: left;\n padding: var(--space-2) var(--space-3);\n border-bottom: 2px solid var(--color-border-bold);\n color: var(--color-fg);\n}\n\n.remarque-table td {\n font-family: var(--font-body);\n font-size: var(--text-body);\n padding: var(--space-2) var(--space-3);\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n color: var(--color-fg);\n}\n\n.remarque-table th.num,\n.remarque-table td.num {\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n",
|
|
52
|
+
"content": "/*\n * Remarque — Form Control Primitives (issue #27)\n * ────────────────────────────────────────────────\n * Namespaced classes that style native form elements — no fake replacement\n * widgets. A `<select>` is a `<select>`, a checkbox stays a checkbox with\n * `accent-color` doing the recoloring work a browser already supports\n * natively. This module exists because Remarque is editorial-first: forms\n * are for the contact/search/newsletter moments a written-word site\n * occasionally needs, not a component library for building app UIs. See\n * \"When NOT to use\" in REMARQUE.md's \"Forms\" section before reaching for\n * this on a page that wants more than a handful of fields. Also carries a\n * small standalone-table section (`.remarque-table`) at the end of this\n * file — see that section's header comment for why it lives here rather\n * than in prose.css (issue #30's reference `Table.astro` needs the same\n * table visual language OUTSIDE `.remarque-prose`).\n *\n * PROVENANCE — spec-native, not graduated from a downstream site (unlike\n * essay.css/broadsheet.css). Built directly from issue #27; there was no\n * prior flagship implementation to re-express in token vocabulary, so\n * every value below is original to this module rather than a mapped-over\n * site literal.\n *\n * Own subpath (remarque-tokens/forms, remarque-tokens/forms.css) — NOT\n * included by the tokens.css aggregator, matching essay.css/broadsheet.css/\n * print.css: most Remarque pages have zero forms. Import explicitly, after\n * prose.css if both are in use:\n * @import 'remarque-tokens/prose.css';\n * @import 'remarque-tokens/forms.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <div class=\"remarque-field\">\n * <label class=\"remarque-field-label\" for=\"email\">Email</label>\n * <input class=\"remarque-input\" type=\"email\" id=\"email\" name=\"email\"\n * aria-describedby=\"email-help\" />\n * <p class=\"remarque-field-message\" id=\"email-help\">\n * We'll never share your email.\n * </p>\n * </div>\n *\n * <div class=\"remarque-field\" data-state=\"error\">\n * <label class=\"remarque-field-label\" for=\"email2\">Email</label>\n * <input class=\"remarque-input\" type=\"email\" id=\"email2\" name=\"email2\"\n * aria-invalid=\"true\" aria-describedby=\"email2-message\" required />\n * <p class=\"remarque-field-message\" id=\"email2-message\" role=\"alert\">\n * Enter a valid email address.\n * </p>\n * </div>\n *\n * <label class=\"remarque-checkbox\">\n * <input type=\"checkbox\" name=\"subscribe\" />\n * <span>Subscribe to updates</span>\n * </label>\n *\n * <label class=\"remarque-radio\">\n * <input type=\"radio\" name=\"plan\" value=\"a\" />\n * <span>Plan A</span>\n * </label>\n *\n * <button class=\"remarque-button\" type=\"button\">Cancel</button>\n * <button class=\"remarque-button remarque-button--primary\" type=\"submit\">\n * Submit\n * </button>\n *\n * `data-state` lives on `.remarque-field` (not the input) — it recolors\n * both the control's border AND the message text together from one\n * attribute, the same \"one attribute drives several descendants\" shape\n * essay.css's counter-driven numbering uses. `aria-invalid=\"true\"` is the\n * REAL accessibility signal (screen readers announce it); `data-state` is\n * the visual layer on top. Author both together — see \"State wiring\"\n * below for what happens if a consumer only sets one.\n */\n\n/* ─── Field wrapper: label + control + help/error text stack ────────────\n * A vertical rhythm of three parts. `--space-2` between label and control\n * (tight — they read as one unit), `--space-2` again before the message\n * line. No border, no background on the wrapper itself — `.remarque-field`\n * is layout only, never a bordered \"card\" around the control (Components:\n * \"no component should be visually louder than the content it contains\").\n */\n.remarque-field {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n max-width: var(--content-reading);\n}\n\n.remarque-field + .remarque-field {\n margin-top: var(--space-5);\n}\n\n/* Label voice: same declarations as `.text-label` (tokens-core.css),\n repeated rather than depended-on-as-a-mixin — the same non-mixin\n precedent essay.css's TOC summary and broadsheet.css's kicker rows\n establish. True small caps (`font-variant-caps`), never `text-transform:\n uppercase` (REMARQUE.md \"Small Caps\"). Quiet mono metadata voice for the\n label, not the body face — a form label identifies the field, the VALUE\n typed into it is the content, and that distinction is the same one\n `.text-label` already draws for dates/tags/status elsewhere. */\n.remarque-field-label {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n font-variant-caps: all-small-caps;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n}\n\n/* Required-field marker: a plain asterisk in the accent color is the one\n sanctioned use here (it is unambiguously \"this is the interactive thing\n that needs attention right now\" — not decoration). Author it inside the\n label: <label>Email <span class=\"remarque-field-required\"\n aria-hidden=\"true\">*</span></label> — aria-hidden because `required` on\n the input itself is the real signal a screen reader announces. */\n.remarque-field-required {\n color: var(--color-accent);\n}\n\n/* Help/error/success message line — meta voice, body-face-adjacent size\n floor (--text-meta, 14px, clears the USWDS small-text minimum). Neutral\n (muted) by default; recolored by `.remarque-field[data-state]` below.\n `text-wrap: pretty` matches `.remarque-prose`'s optical default for\n short justified-feeling message copy. */\n.remarque-field-message {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n letter-spacing: var(--tracking-meta);\n color: var(--color-muted);\n margin: 0;\n text-wrap: pretty;\n}\n\n/* ─── Text inputs: input / textarea / select ─────────────────────────────\n * One shared class across all three native elements — same border,\n * radius, spacing, and body-voice typography (Font Slots: \"Body ... UI\n * labels, navigation, buttons\" — form values are UI labels' closest\n * relative here, not metadata). `--color-border-bold`, not `-bold`'s\n * quieter sibling: an input's boundary is a FUNCTIONAL edge (WCAG 1.4.11\n * non-text contrast), the same reasoning `.remarque-sidenote`'s narrow-\n * viewport border and `.remarque-toc-rail`'s hairline both already apply\n * decoratively-vs-functionally in essay.css.\n */\n.remarque-input {\n display: block;\n width: 100%;\n min-height: 44px; /* USWDS/WCAG 2.5.5 touch-target floor — see AGENT_RULES.md */\n padding: var(--space-2) var(--space-3);\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-title);\n color: var(--color-fg);\n background-color: var(--color-bg);\n border: var(--border-width) var(--border-style) var(--color-border-bold);\n /* Restraint rule (AGENT_RULES.md \"Border radius never exceeds\n --radius-md\" + this module's own tighter floor): form controls stay\n at --radius-sm, never the 8px ceiling other chrome is allowed. A\n smaller control reads as more precise at a smaller radius. */\n border-radius: var(--radius-sm);\n transition: border-color var(--motion-fast) var(--motion-easing);\n}\n\ntextarea.remarque-input {\n min-height: 6rem; /* several lines tall by default — still clears the 44px floor */\n resize: vertical;\n}\n\n/* Native select's disclosure triangle is left as the platform default —\n restyling it means either `appearance: none` (which then requires a\n hand-drawn replacement icon, exactly the \"fake replacement\" this module\n avoids) or accepting a single browser's rendering as the reference,\n neither of which is worth it for one triangle. */\n.remarque-input:hover {\n border-color: var(--color-fg-muted);\n}\n\n/* Placeholder: --color-muted, the same 4.5:1-on-bg/-surface token used\n everywhere else at this contrast tier — a conscious choice, not the\n browser default (which is often far below AA). Placeholder text is\n supplementary (never the only label — every `.remarque-input` pairs\n with a real `<label>`), so 4.5:1 rather than `--color-fg-muted`'s 7:1\n is the correct tier: AA is sufficient for non-essential hint copy. */\n.remarque-input::placeholder {\n color: var(--color-muted);\n opacity: 1; /* Firefox lowers placeholder opacity by default; pin it so the token value is what actually renders */\n}\n\n.remarque-input:disabled {\n color: var(--color-disabled);\n background-color: var(--color-bg-subtle);\n border-color: var(--color-border);\n cursor: not-allowed;\n}\n\n/* ─── Checkbox / Radio — accent-color, no fake replacements ──────────────\n * `accent-color` is the ENTIRE recoloring mechanism: no `appearance:\n * none`, no hand-drawn checkmark/dot, no absolutely-positioned pseudo-\n * element standing in for the native control. This is a deliberate\n * ceiling on customization, not an oversight — every browser Remarque\n * targets supports `accent-color` on checkboxes/radios, and a real native\n * control keeps every platform accessibility behavior (keyboard,\n * high-contrast mode, forced-colors mode, screen-reader state\n * announcements) for free, at the cost of not controlling the checkmark\n * glyph's exact shape.\n *\n * Sizing: --space-5 (1.5rem / 24px) — comfortably inside the 44px touch\n * target once the wrapping <label> below adds its own min-height, and\n * large enough to keep the accent-colored fill legible (a 16px default\n * checkbox reads as fussy at this system's generous spacing scale).\n */\n.remarque-checkbox,\n.remarque-radio {\n display: inline-flex;\n align-items: center;\n gap: var(--space-2);\n min-height: 44px; /* touch-target floor on the whole label, same shape as .nav-link */\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-title);\n color: var(--color-fg);\n cursor: pointer;\n}\n\n.remarque-checkbox input,\n.remarque-radio input {\n accent-color: var(--color-accent);\n width: var(--space-5);\n height: var(--space-5);\n margin: 0;\n flex-shrink: 0;\n}\n\n/* `:has()` is a light progressive enhancement here, not a functional\n dependency — a browser without it still gets a fully working, natively\n disabled control; it just skips the muted label-text recolor. */\n.remarque-checkbox:has(input:disabled),\n.remarque-radio:has(input:disabled) {\n color: var(--color-disabled);\n cursor: not-allowed;\n}\n\n.remarque-checkbox input:disabled,\n.remarque-radio input:disabled {\n cursor: not-allowed;\n}\n\n/* ─── Buttons ─────────────────────────────────────────────────────────────\n * Quiet by rule (AGENT_RULES.md Components: \"Buttons are quiet: text-only\n * or subtle bordered. Never filled/solid as default.\"). The default\n * variant is bordered, transparent background, body-voice text — reads as\n * \"a clickable label,\" not a colored block. `--primary` is the ONE\n * sanctioned accent placement per viewport (Visual Rules: accent for\n * \"inline links and one interactive element per viewport\") — even there,\n * it stays unfilled: accent text + accent border, never a solid accent\n * fill, so the same restraint the default variant holds carries through\n * to the emphasized one. Hover on `--primary` washes in\n * `--color-accent-subtle` (the same quiet-highlight token\n * `.remarque-prose mark` already uses) rather than darkening to a filled\n * block — a wash, not a fill.\n */\n.remarque-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-2);\n min-height: 44px;\n padding: var(--space-2) var(--space-4);\n font-family: var(--font-body);\n font-size: var(--text-body);\n font-weight: var(--weight-medium);\n line-height: var(--leading-title);\n color: var(--color-fg);\n background-color: transparent;\n border: var(--border-width) var(--border-style) var(--color-border-bold);\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition:\n color var(--motion-fast) var(--motion-easing),\n border-color var(--motion-fast) var(--motion-easing),\n background-color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-button:hover:not(:disabled) {\n border-color: var(--color-fg-muted);\n color: var(--color-fg);\n}\n\n.remarque-button--primary {\n color: var(--color-accent);\n border-color: var(--color-accent);\n}\n\n.remarque-button--primary:hover:not(:disabled) {\n color: var(--color-accent-hover);\n border-color: var(--color-accent-hover);\n background-color: var(--color-accent-subtle);\n}\n\n.remarque-button:disabled {\n color: var(--color-disabled);\n border-color: var(--color-border);\n background-color: transparent;\n cursor: not-allowed;\n}\n\n/* ─── Validation state wiring (0.17.0 state-color tokens) ────────────────\n * `data-state` lives on `.remarque-field`, cascading down to the input's\n * border and the message line's color together — author it alongside a\n * real `aria-invalid=\"true\"`/`aria-describedby` pair on the input (the\n * markup contract above); `data-state` alone changes only the paint,\n * `aria-invalid` is what a screen reader actually announces. `--warning`\n * covers a caution/needs-attention message that isn't yet a hard failure\n * (REMARQUE.md \"State Colors\" — \"states needing attention before\n * proceeding\"), e.g. a password-strength hint. No `-subtle` background is\n * used here: `-subtle` is sized for banner/callout backgrounds (REMARQUE.md\n * \"State Colors\"), and painting a per-field background would make the\n * field louder than the content it holds (Components rule) — border +\n * message-text color is enough signal at this scale.\n */\n.remarque-field[data-state=\"error\"] .remarque-input,\n.remarque-input[aria-invalid=\"true\"] {\n border-color: var(--color-error);\n}\n\n.remarque-field[data-state=\"error\"] .remarque-field-message {\n color: var(--color-error);\n}\n\n.remarque-field[data-state=\"success\"] .remarque-input {\n border-color: var(--color-success);\n}\n\n.remarque-field[data-state=\"success\"] .remarque-field-message {\n color: var(--color-success);\n}\n\n.remarque-field[data-state=\"warning\"] .remarque-input {\n border-color: var(--color-warning);\n}\n\n.remarque-field[data-state=\"warning\"] .remarque-field-message {\n color: var(--color-warning);\n}\n\n/* `:user-invalid` — native, zero-JS validity feedback (matches only after\n the user has interacted with the field and left it invalid, unlike\n `:invalid`, which can match a required-but-untouched field on page\n load). Guarded with `@supports selector(...)`: a browser lacking the\n pseudo-class simply never matches this block and falls back to\n whatever `data-state`/`aria-invalid` already provide — the same\n \"degrades to the no-JS/no-support baseline\" shape essay.css's `@media`\n gate uses for its own progressive enhancement. This is a bonus layer,\n not a replacement for authoring `data-state=\"error\"` explicitly. */\n@supports selector(:user-invalid) {\n .remarque-input:user-invalid {\n border-color: var(--color-error);\n }\n}\n\n/* ─── `:has()` zero-JS validation enhancement layer (issue #97) ──────────\n * A second, purely additive progressive-enhancement layer over the\n * `:user-invalid` layer just above — same native-first shape, one step\n * further. `:user-invalid` alone can only style the INPUT it matches;\n * CSS has no \"parent selector\" without `:has()`, so it has no way to\n * reach the field's label or wrapper. `:has()` is exactly the piece that\n * closes that gap: `.remarque-field` can react to a descendant control's\n * validity/focus state directly, with no `data-state` attribute and no\n * JavaScript. Guarded with `@supports selector(:has(...))` — the same\n * \"degrades to the no-JS/no-support baseline\" shape the `:user-invalid`\n * block above uses — so a browser missing either pseudo-class in the\n * compound query falls back cleanly to whatever `data-state`/\n * `aria-invalid` (and, where supported, the plain `:user-invalid` layer\n * above) already provide. `data-state` remains the CANONICAL contract;\n * neither block below replaces authoring `data-state=\"error\"` explicitly\n * — see REMARQUE.md \"State wiring\".\n *\n * Like the bare `:user-invalid` layer above, this is a bonus visual cue,\n * not one of the canonical `data-state` states the forced-colors\n * reinforcement below was scoped to fix (REMARQUE.md \"Forced Colors &\n * Contrast Preferences\") — it is not itself a new color-only-affordance\n * regression, since the real accessible signal (`aria-invalid`, the\n * `data-state` border/message wiring) is unchanged and unaffected by\n * whether this bonus layer's color survives forced-colors mode.\n */\n@supports selector(:has(:user-invalid)) {\n /* Extends the :user-invalid border recolor (input only, above) up to\n the field's label — the one thing plain :user-invalid cannot reach\n on its own. */\n .remarque-field:has(.remarque-input:user-invalid) .remarque-field-label {\n color: var(--color-error);\n }\n}\n\n/* Field-level focus affordance — a quiet secondary cue, NOT a\n replacement for the control's own native `:focus-visible` outline\n (tokens-core.css), which already renders on the control itself and is\n forced-colors-safe by construction. Brightens the label from\n `--color-muted` to `--color-fg` while any control inside the field has\n keyboard focus — a restrained color wash, not a new border/background,\n matching this module's \"no component should be visually louder than\n the content it contains\" rule (AGENT_RULES.md Components). */\n@supports selector(:has(:focus-visible)) {\n .remarque-field:has(.remarque-input:focus-visible) .remarque-field-label {\n color: var(--color-fg);\n }\n}\n\n/* ─── Forced-colors reinforcement (issue #93) ────────────────────────────\n * The validation-state wiring above is the textbook color-only-affordance\n * case the issue names explicitly: error/success/warning are distinguished\n * ENTIRELY by `border-color`/`color`, both of which `forced-colors: active`\n * remaps to a system color regardless of which state authored them — so\n * all three states (and the plain, untouched control) would render\n * IDENTICALLY under forced colors, exactly the failure `aria-invalid` +\n * `role=\"alert\"` already prevent for screen-reader users but do nothing\n * for a sighted low-vision user in Windows High Contrast Mode.\n *\n * Fix is geometry, not color, since color is exactly what forced-colors\n * discards: each state gets a distinct `border-style`/`border-width`\n * combination on the control itself (a shape that reads under ANY forced\n * system border color), plus a small glyph prefixed to the message line\n * — scoped to this media query only, so the quiet default (message text\n * alone) is unchanged everywhere else. The glyph is supplementary, not\n * the primary signal (a screen reader user already gets `aria-invalid` +\n * the message text; a sighted forced-colors user gets the border shape);\n * it is not marked `aria-hidden` because CSS generated content cannot\n * carry that attribute, but it is inert in the accessibility tree by\n * default in every current UA/AT combination tested (Chromium+NVDA,\n * Chromium+VoiceOver) — it is not read as if it were message content.\n */\n@media (forced-colors: active) {\n .remarque-field[data-state=\"error\"] .remarque-input,\n .remarque-input[aria-invalid=\"true\"] {\n border-style: double;\n border-width: 3px;\n }\n\n .remarque-field[data-state=\"warning\"] .remarque-input {\n border-style: dashed;\n border-width: 2px;\n }\n\n .remarque-field[data-state=\"success\"] .remarque-input {\n border-style: solid;\n border-width: 2px;\n }\n\n .remarque-field[data-state=\"error\"] .remarque-field-message::before {\n content: \"\\2715\\0020\"; /* ✕ */\n }\n\n .remarque-field[data-state=\"warning\"] .remarque-field-message::before {\n content: \"\\26A0\\0020\"; /* ⚠ */\n }\n\n .remarque-field[data-state=\"success\"] .remarque-field-message::before {\n content: \"\\2713\\0020\"; /* ✓ */\n }\n}\n\n/* ─── Standalone data tables (issue #30) ──────────────────────────────────\n * `.remarque-table` re-scopes prose.css's table rules (mono `th` voice,\n * 2px `border-bottom` under the header row, 1px row rules only, `.num`\n * for tabular-lining right-aligned numeric columns) under a top-level\n * class instead of `.remarque-prose table`. This is intentionally a\n * near-verbatim copy of those declarations, not a shared mixin (CSS has\n * none) and not a requirement to wrap a standalone data table in the full\n * `.remarque-prose` container — `.remarque-prose` also applies oldstyle\n * proportional numerals, `> * + *` paragraph-spacing rhythm, and prose\n * max-width, none of which a reference data grid (e.g. `Table.astro`)\n * wants. Living in forms.css rather than a new subpath: this module is\n * already \"the supplementary-UI module\" in AGENT_RULES.md's build order\n * (step 7 groups buttons, tags, cards, code blocks, AND tables together),\n * and a table this small does not justify its own subpath the way the\n * Essay/Broadsheet modules' much larger surface area did.\n *\n * Wrap a wide table exactly like prose.css's convention:\n * <div class=\"remarque-table-wrap\"><table class=\"remarque-table\">...</table></div>\n */\n.remarque-table-wrap {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.remarque-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--text-body);\n}\n\n.remarque-table caption {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-meta);\n color: var(--color-muted);\n text-align: left;\n caption-side: top;\n margin-bottom: var(--space-2);\n}\n\n.remarque-table th {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-meta);\n font-weight: var(--weight-semibold);\n text-align: left;\n padding: var(--space-2) var(--space-3);\n border-bottom: 2px solid var(--color-border-bold);\n color: var(--color-fg);\n}\n\n.remarque-table td {\n font-family: var(--font-body);\n font-size: var(--text-body);\n padding: var(--space-2) var(--space-3);\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n color: var(--color-fg);\n}\n\n.remarque-table th.num,\n.remarque-table td.num {\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n",
|
|
53
53
|
"type": "remarque:css"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "remarque:contract",
|
|
5
5
|
"title": "Palette Deck (markup contract only — no executable content)",
|
|
6
6
|
"description": "Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.24.0",
|
|
8
8
|
"integrity": "sha256-WGgBZ2j0VnLPRAZlzv/4G1Wu9rKqTdXf1536dWWu8nU=",
|
|
9
9
|
"dependencies": [
|
|
10
10
|
"remarque-tokens"
|
package/registry.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"$schema": "https://williamzujkowski.github.io/remarque/registry.schema.json",
|
|
3
3
|
"name": "remarque-tokens",
|
|
4
4
|
"homepage": "https://williamzujkowski.github.io/remarque/",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.24.0",
|
|
6
6
|
"items": [
|
|
7
7
|
{
|
|
8
8
|
"name": "essay",
|
|
9
9
|
"type": "remarque:contract",
|
|
10
10
|
"title": "Essay Module",
|
|
11
11
|
"description": "Sidenotes + sticky TOC rail for long-form Essay pages: margin notes relocated into real DOM order, right after the paragraph that cites them, numbered by CSS counter and labeled `aria-label=\"Note N\"` in the same order the counter advances — the mechanism that prevents the sidenote aria-label/DOM-order transcription bug found in the flagship migration (#89, williamzujkowski.github.io#380).",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.24.0",
|
|
13
13
|
"integrity": "sha256-zr04H0+0xf01OWLk6B2A9ziZOYJd4Sq5q5/cib65lbU=",
|
|
14
14
|
"file": "registry/essay.json"
|
|
15
15
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"type": "remarque:contract",
|
|
19
19
|
"title": "Broadsheet Pattern",
|
|
20
20
|
"description": "The editorial Landing/archive pattern: masthead, lead article, numbered entry list, and post-header kicker. Entry numerals are generated from `data-entry-number` via `attr()`, never `counter()`, and every kicker/dateline row is true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.",
|
|
21
|
-
"version": "0.
|
|
21
|
+
"version": "0.24.0",
|
|
22
22
|
"integrity": "sha256-BZea38PRdob4OfmdGN0eDw3U0adl7SEsdrjKgSewK9A=",
|
|
23
23
|
"file": "registry/broadsheet.json"
|
|
24
24
|
},
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"type": "remarque:contract",
|
|
28
28
|
"title": "Forms Primitives",
|
|
29
29
|
"description": "Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.",
|
|
30
|
-
"version": "0.
|
|
31
|
-
"integrity": "sha256-
|
|
30
|
+
"version": "0.24.0",
|
|
31
|
+
"integrity": "sha256-WfYjcf04Dxxx7X21U5b6DrY90Qw95p0zkCQszoh77tE=",
|
|
32
32
|
"file": "registry/forms.json"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"type": "remarque:contract",
|
|
37
37
|
"title": "Palette Deck (markup contract only — no executable content)",
|
|
38
38
|
"description": "Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.",
|
|
39
|
-
"version": "0.
|
|
39
|
+
"version": "0.24.0",
|
|
40
40
|
"integrity": "sha256-WGgBZ2j0VnLPRAZlzv/4G1Wu9rKqTdXf1536dWWu8nU=",
|
|
41
41
|
"file": "registry/palette-deck.json"
|
|
42
42
|
}
|
package/scripts/audit.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
30
30
|
import { join, relative, dirname } from 'node:path';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
|
-
import { extractBlocks, declsOf, isLightRoot, isDarkBlock } from './lib/css-tokens.mjs';
|
|
32
|
+
import { extractBlocks, declsOf, isLightRoot, isDarkBlock, resolveSide, darkOverridesOf } from './lib/css-tokens.mjs';
|
|
33
33
|
|
|
34
34
|
const args = process.argv.slice(2);
|
|
35
35
|
function argOf(flag, dflt) {
|
|
@@ -110,10 +110,16 @@ function ratio(c1, c2) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const blocks = extractBlocks(readFileSync(PALETTE, 'utf8'));
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// Raw extraction, then resolved through the light-dark() splitter (issue
|
|
114
|
+
// #95) — a no-op over palettes that only use the conventional two-block
|
|
115
|
+
// form, so both forms parse identically from here on. See
|
|
116
|
+
// scripts/lib/css-tokens.mjs "light-dark() support".
|
|
117
|
+
const rawLightRoot = declsOf(blocks, isLightRoot);
|
|
118
|
+
const rawDarkBlock = declsOf(blocks, isDarkBlock);
|
|
119
|
+
const lightDecls = resolveSide(rawLightRoot, 'light');
|
|
120
|
+
const darkOverrides = darkOverridesOf(rawLightRoot, rawDarkBlock);
|
|
115
121
|
if (Object.keys(lightDecls).length === 0) fail(`no top-level :root declarations found in ${PALETTE}`);
|
|
116
|
-
if (Object.keys(darkOverrides).length === 0) fail(`no dark-theme declarations found in ${PALETTE} (@media prefers-color-scheme: dark or [data-theme="dark"])`);
|
|
122
|
+
if (Object.keys(darkOverrides).length === 0) fail(`no dark-theme declarations found in ${PALETTE} (light-dark(), @media prefers-color-scheme: dark, or [data-theme="dark"])`);
|
|
117
123
|
// CSS cascade: dark blocks override light; unset dark tokens inherit light values.
|
|
118
124
|
const darkDecls = { ...lightDecls, ...darkOverrides };
|
|
119
125
|
|
package/scripts/drift-check.mjs
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { readFileSync, existsSync } from 'node:fs';
|
|
38
38
|
import { join, dirname, resolve } from 'node:path';
|
|
39
|
-
import { extractBlocks, declsOf, isLightRoot, isDarkBlock } from './lib/css-tokens.mjs';
|
|
39
|
+
import { extractBlocks, declsOf, isLightRoot, isDarkBlock, resolveSide, darkOverridesOf } from './lib/css-tokens.mjs';
|
|
40
40
|
|
|
41
41
|
const args = process.argv.slice(2);
|
|
42
42
|
function argOf(flag, dflt) {
|
|
@@ -115,8 +115,13 @@ function isDocumented(tokenName) {
|
|
|
115
115
|
/* ── Parse the consumer stylesheet ─────────────────────────────────── */
|
|
116
116
|
|
|
117
117
|
const blocks = extractBlocks(readFileSync(CSS_FILE, 'utf8'));
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// resolveSide/darkOverridesOf (issue #95) — a consumer stylesheet may
|
|
119
|
+
// declare an override token via light-dark() as well as (or instead of)
|
|
120
|
+
// the conventional two-block form; both must classify identically.
|
|
121
|
+
const rawLightRoot = declsOf(blocks, isLightRoot);
|
|
122
|
+
const rawDarkBlock = declsOf(blocks, isDarkBlock);
|
|
123
|
+
const lightDecls = resolveSide(rawLightRoot, 'light');
|
|
124
|
+
const darkOverrides = darkOverridesOf(rawLightRoot, rawDarkBlock);
|
|
120
125
|
|
|
121
126
|
const norm = (v) => String(v).trim().replace(/\s+/g, ' ');
|
|
122
127
|
|
|
@@ -95,3 +95,74 @@ export const isDarkBlock = (b) =>
|
|
|
95
95
|
parts(b.prelude).some((s) => ROOTISH.test(s) && !DARKISH.test(s))) ||
|
|
96
96
|
(b.context === '' &&
|
|
97
97
|
parts(b.prelude).some((s) => DARKISH.test(s) && (ROOTISH.test(s) || s.startsWith('.') || s.startsWith('['))));
|
|
98
|
+
|
|
99
|
+
/* ── light-dark() support (issue #95) ──────────────────────────────────
|
|
100
|
+
* The migration collapses a token's light AND dark values into ONE
|
|
101
|
+
* declaration — `--color-x: light-dark(<light>, <dark>);` — living in the
|
|
102
|
+
* top-level `:root` block (isLightRoot territory), rather than two
|
|
103
|
+
* declarations split across the :root/dark-block convention above. The
|
|
104
|
+
* conventional two-block form is NOT going away (mixed-form palettes are
|
|
105
|
+
* supported — a consumer may migrate some tokens and leave others in the
|
|
106
|
+
* old convention, and the bridge's own generated output keeps emitting
|
|
107
|
+
* the old convention indefinitely; see REMARQUE.md "Color Scheme &
|
|
108
|
+
* light-dark()"), so both forms must parse side by side.
|
|
109
|
+
*
|
|
110
|
+
* splitLightDark() does the actual splitting: depth-aware (so a light or
|
|
111
|
+
* dark side that is itself a function call — `oklch(...)`, `var(...)` —
|
|
112
|
+
* doesn't confuse the single top-level comma this looks for). Returns
|
|
113
|
+
* null for any value that isn't a light-dark() call, so callers can
|
|
114
|
+
* treat it as a plain passthrough. */
|
|
115
|
+
export function splitLightDark(value) {
|
|
116
|
+
const v = value.trim();
|
|
117
|
+
const m = v.match(/^light-dark\(\s*/i);
|
|
118
|
+
if (!m || !v.endsWith(')')) return null;
|
|
119
|
+
const inner = v.slice(m[0].length, -1);
|
|
120
|
+
let depth = 0, splitAt = -1;
|
|
121
|
+
for (let i = 0; i < inner.length; i++) {
|
|
122
|
+
const c = inner[i];
|
|
123
|
+
if (c === '(') depth++;
|
|
124
|
+
else if (c === ')') depth--;
|
|
125
|
+
else if (c === ',' && depth === 0) { splitAt = i; break; }
|
|
126
|
+
}
|
|
127
|
+
if (splitAt === -1) return null;
|
|
128
|
+
return { light: inner.slice(0, splitAt).trim(), dark: inner.slice(splitAt + 1).trim() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Resolve a flat decls map (as returned by declsOf) to one theme "side".
|
|
132
|
+
* A `light-dark(a, b)` value collapses to `a` (side 'light') or `b` (side
|
|
133
|
+
* 'dark'); every other value — a plain oklch() literal, a var() alias, a
|
|
134
|
+
* non-color token like --weight-display — passes through UNCHANGED. That
|
|
135
|
+
* makes this a no-op over a palette that hasn't adopted light-dark() at
|
|
136
|
+
* all, which is exactly the backward-compat property the parser needs:
|
|
137
|
+
* every existing conventional-form fixture keeps resolving identically. */
|
|
138
|
+
export function resolveSide(decls, side) {
|
|
139
|
+
const out = {};
|
|
140
|
+
for (const [name, value] of Object.entries(decls)) {
|
|
141
|
+
const ld = splitLightDark(value);
|
|
142
|
+
out[name] = ld ? ld[side] : value;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Compute the merged "dark override" map the four parser consumers each
|
|
148
|
+
* need, from the two raw declsOf() extractions:
|
|
149
|
+
* - rawLightRootDecls: declsOf(blocks, isLightRoot) — the top-level
|
|
150
|
+
* :root block, where a light-dark() declaration's DARK side now
|
|
151
|
+
* lives (alongside its light side).
|
|
152
|
+
* - rawDarkBlockDecls: declsOf(blocks, isDarkBlock) — the conventional
|
|
153
|
+
* dark-block override (@media prefers-color-scheme:dark and/or
|
|
154
|
+
* [data-theme="dark"]/:root.dark), still used for non-color tokens
|
|
155
|
+
* (--weight-display) and any token a palette hasn't migrated yet.
|
|
156
|
+
*
|
|
157
|
+
* Precedence matches real CSS cascade: when the SAME token is declared
|
|
158
|
+
* both ways (a light-dark() value in :root AND an explicit override in a
|
|
159
|
+
* later dark block), the dark block — which appears later in the
|
|
160
|
+
* stylesheet at equal specificity — wins, so it is spread last here. */
|
|
161
|
+
export function darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls) {
|
|
162
|
+
const fromLightDark = {};
|
|
163
|
+
for (const [name, value] of Object.entries(rawLightRootDecls)) {
|
|
164
|
+
const ld = splitLightDark(value);
|
|
165
|
+
if (ld) fromLightDark[name] = ld.dark;
|
|
166
|
+
}
|
|
167
|
+
return { ...fromLightDark, ...resolveSide(rawDarkBlockDecls, 'dark') };
|
|
168
|
+
}
|
package/scripts/tokens-json.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
22
|
-
import { extractBlocks, declsOf, isLightRoot, isDarkBlock } from './lib/css-tokens.mjs';
|
|
22
|
+
import { extractBlocks, declsOf, isLightRoot, isDarkBlock, resolveSide, darkOverridesOf } from './lib/css-tokens.mjs';
|
|
23
23
|
|
|
24
24
|
function typeOf(name, value) {
|
|
25
25
|
if (name.startsWith('color-')) return 'color';
|
|
@@ -50,8 +50,15 @@ const SCHEMA_URL = 'https://williamzujkowski.github.io/remarque/tokens.schema.js
|
|
|
50
50
|
const version = JSON.parse(readFileSync('package.json', 'utf8')).version;
|
|
51
51
|
const coreDecls = declsOf(extractBlocks(readFileSync('tokens-core.css', 'utf8')), isLightRoot);
|
|
52
52
|
const paletteBlocks = extractBlocks(readFileSync('tokens-palette.css', 'utf8'));
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// resolveSide/darkOverridesOf (issue #95) — tokens-palette.css's --color-*
|
|
54
|
+
// tokens are now authored as light-dark(<light>, <dark>) single
|
|
55
|
+
// declarations; this resolves each side to the same flat shape the rest
|
|
56
|
+
// of this generator already expects, so tokens.json's OUTPUT is
|
|
57
|
+
// unaffected by the authoring-form change (same names, same values).
|
|
58
|
+
const rawLightRoot = declsOf(paletteBlocks, isLightRoot);
|
|
59
|
+
const rawDarkBlock = declsOf(paletteBlocks, isDarkBlock);
|
|
60
|
+
const lightDecls = resolveSide(rawLightRoot, 'light');
|
|
61
|
+
const darkOverrides = darkOverridesOf(rawLightRoot, rawDarkBlock);
|
|
55
62
|
|
|
56
63
|
// DTCG conformance note (issue #99, ratified option ii): documented IN the
|
|
57
64
|
// generated artifact, not just in prose, so a future agent regenerating
|
package/tokens-palette.css
CHANGED
|
@@ -25,6 +25,18 @@
|
|
|
25
25
|
* then validate — lightness carries the contrast, hue carries the
|
|
26
26
|
* personality. Not every hue passes at every lightness (yellows/
|
|
27
27
|
* cyans behave differently than blues); the audit is the gate.
|
|
28
|
+
*
|
|
29
|
+
* AUTHORING FORM (issue #95): every --color-* token below is a single
|
|
30
|
+
* `light-dark(<light>, <dark>)` declaration, not two separate light/dark
|
|
31
|
+
* declarations — see "Color: light-dark() palette" below and
|
|
32
|
+
* REMARQUE.md "Color Scheme & light-dark()" for the full mechanism,
|
|
33
|
+
* the browser-support floor this raises, and why --weight-display (a
|
|
34
|
+
* font-weight, not a color) is the one token that keeps the older
|
|
35
|
+
* @media/[data-theme] mechanism. A palette override may still use the
|
|
36
|
+
* OLD two-declaration convention instead — scripts/audit.mjs and every
|
|
37
|
+
* other parser consumer accept both forms, including mixed within the
|
|
38
|
+
* same file, so this is a purely additive authoring option, not a
|
|
39
|
+
* requirement.
|
|
28
40
|
*/
|
|
29
41
|
|
|
30
42
|
/* ─── Font Slots ────────────────────────────────────────── */
|
|
@@ -41,232 +53,194 @@
|
|
|
41
53
|
/* Display weight — palette-tier because it is theme-conditional:
|
|
42
54
|
light-on-dark text renders optically thinner (halation), so dark
|
|
43
55
|
mode bumps display faces one step to keep serif hairlines alive.
|
|
44
|
-
(issue #59)
|
|
56
|
+
(issue #59) NOT expressed as light-dark() below with the rest of
|
|
57
|
+
the palette — light-dark() is a <color> function and this is a
|
|
58
|
+
font-weight; see "Theme selection mechanism" for where its dark
|
|
59
|
+
value actually lives. */
|
|
45
60
|
--weight-display: 400;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
/* ─── Color:
|
|
63
|
+
/* ─── Color: light-dark() palette (issue #95) ──────────── */
|
|
49
64
|
/*
|
|
50
|
-
* Colors use OKLCH for perceptual uniformity
|
|
65
|
+
* Colors use OKLCH for perceptual uniformity, expressed as single
|
|
66
|
+
* `light-dark(<light>, <dark>)` declarations under `color-scheme: light
|
|
67
|
+
* dark` — the platform mechanism this file previously named as "the
|
|
68
|
+
* endgame to watch." Baseline-stable (Chrome 123+, Firefox 120+, Safari
|
|
69
|
+
* 17.5+) — see REMARQUE.md "Color Scheme & light-dark()" for the full
|
|
70
|
+
* migration rationale and the browser-floor bump this raises for the
|
|
71
|
+
* package (stated prominently there and in CHANGELOG.md, per
|
|
72
|
+
* "Governance & Deprecation").
|
|
73
|
+
*
|
|
74
|
+
* How theme selection works now:
|
|
75
|
+
* - System preference (no explicit choice) — `color-scheme: light
|
|
76
|
+
* dark` on :root lets the UA resolve every light-dark() call
|
|
77
|
+
* directly from `prefers-color-scheme`. No `@media
|
|
78
|
+
* (prefers-color-scheme: dark)` block is needed for colors anymore
|
|
79
|
+
* — that block, and its full duplicated color list, is RETIRED.
|
|
80
|
+
* - Explicit user choice — `[data-theme="light"]`/`[data-theme=
|
|
81
|
+
* "dark"]` (see "Theme selection mechanism" below) override
|
|
82
|
+
* `color-scheme` itself, once, rather than re-listing every token's
|
|
83
|
+
* dark value a second time under the attribute selector. Same for
|
|
84
|
+
* the `:root.dark` compatibility bridge.
|
|
85
|
+
*
|
|
51
86
|
* The palette is deliberately narrow: warm neutrals + one accent.
|
|
52
87
|
*
|
|
53
88
|
* Accent rule: used for inline links and ONE interactive element per
|
|
54
89
|
* viewport. No accent backgrounds, no accent borders, no accent fills.
|
|
55
90
|
*
|
|
56
|
-
* Contrast ratios in comments are computed WCAG 2.x values
|
|
57
|
-
*
|
|
58
|
-
*
|
|
91
|
+
* Contrast ratios in comments are computed WCAG 2.x values (OKLCH →
|
|
92
|
+
* linear sRGB → relative luminance) for EACH side of a light-dark()
|
|
93
|
+
* pair — light ratio first, dark ratio second — recomputed and enforced
|
|
94
|
+
* in CI by scripts/audit.mjs, which extracts both values from one
|
|
95
|
+
* declaration (scripts/lib/css-tokens.mjs, `splitLightDark`).
|
|
96
|
+
*
|
|
97
|
+
* A slot that is a pure `var()` alias to an already theme-aware slot
|
|
98
|
+
* (`--color-link`, `--color-focus-ring`, `--color-syntax-link`,
|
|
99
|
+
* `--color-disabled`, …) is declared ONCE, not wrapped in light-dark() —
|
|
100
|
+
* the var() indirection already re-resolves per theme through the slot
|
|
101
|
+
* it points at, so wrapping it would be inert token clutter.
|
|
59
102
|
*/
|
|
60
103
|
|
|
61
104
|
:root {
|
|
62
|
-
|
|
63
|
-
--color-bg-subtle: oklch(0.955 0.005 80); /* slightly darker for alternating sections */
|
|
64
|
-
--color-fg: oklch(0.18 0.01 80); /* near-black, warm undertone */
|
|
65
|
-
--color-fg-muted: oklch(0.43 0.015 80); /* secondary text — 7.55:1 bg / 7.33:1 surface (AAA) */
|
|
66
|
-
--color-muted: oklch(0.54 0.01 80); /* tertiary text — 4.71:1 bg / 4.57:1 surface (AA) */
|
|
67
|
-
--color-border: oklch(0.88 0.005 80); /* quiet borders — DECORATIVE ONLY (1.34:1, below WCAG 1.4.11's 3:1); functional boundaries use --color-border-bold */
|
|
68
|
-
--color-border-bold: oklch(0.62 0.01 80); /* functional/emphasized borders — 3.39:1 (meets WCAG 1.4.11) */
|
|
69
|
-
--color-surface: oklch(0.965 0.005 80); /* card/surface backgrounds */
|
|
105
|
+
color-scheme: light dark;
|
|
70
106
|
|
|
71
|
-
--color-
|
|
72
|
-
--color-
|
|
73
|
-
--color-
|
|
107
|
+
--color-bg: light-dark(oklch(0.975 0.005 80), oklch(0.16 0.01 80)); /* warm off-white / warm charcoal, never pure white or black */
|
|
108
|
+
--color-bg-subtle: light-dark(oklch(0.955 0.005 80), oklch(0.19 0.01 80)); /* slightly darker (light) / lighter (dark) for alternating sections */
|
|
109
|
+
--color-fg: light-dark(oklch(0.18 0.01 80), oklch(0.90 0.005 80)); /* near-black warm undertone / soft white, not pure */
|
|
110
|
+
--color-fg-muted: light-dark(oklch(0.43 0.015 80), oklch(0.70 0.01 80)); /* secondary text — 7.55:1 bg / 7.33:1 surface light (AAA); 7.26:1 bg dark (AAA) */
|
|
111
|
+
--color-muted: light-dark(oklch(0.54 0.01 80), oklch(0.60 0.01 80)); /* tertiary text — 4.71:1 bg / 4.57:1 surface light (AA); 4.92:1 bg / 4.68:1 surface dark (AA, with margin) */
|
|
112
|
+
--color-border: light-dark(oklch(0.88 0.005 80), oklch(0.25 0.005 80)); /* quiet borders — DECORATIVE ONLY (below WCAG 1.4.11's 3:1 in both themes); functional boundaries use --color-border-bold */
|
|
113
|
+
--color-border-bold: light-dark(oklch(0.62 0.01 80), oklch(0.50 0.01 80)); /* functional/emphasized borders — 3.39:1 light / 3.23:1 dark (meets WCAG 1.4.11) */
|
|
114
|
+
--color-surface: light-dark(oklch(0.965 0.005 80), oklch(0.19 0.01 80)); /* card/surface backgrounds */
|
|
74
115
|
|
|
75
|
-
/*
|
|
116
|
+
--color-accent: light-dark(oklch(0.50 0.14 250), oklch(0.68 0.12 250)); /* muted blue light / brighter in dark for contrast — links and one interactive element */
|
|
117
|
+
--color-accent-hover: light-dark(oklch(0.42 0.11 250), oklch(0.75 0.12 250)); /* darker (light) / lighter (dark) on hover — 7.87:1 light, in-gamut both (chroma clipped) */
|
|
118
|
+
--color-accent-subtle: light-dark(oklch(0.95 0.02 250), oklch(0.22 0.04 250)); /* very faint accent background — in-gamut both (chroma clipped) */
|
|
119
|
+
|
|
120
|
+
/* Semantic mappings — pure var() aliases, declared once (see the
|
|
121
|
+
header note above: the aliased slot is already theme-aware). */
|
|
76
122
|
--color-link: var(--color-accent);
|
|
77
123
|
--color-link-hover: var(--color-accent-hover);
|
|
78
124
|
--color-focus-ring: var(--color-accent);
|
|
79
|
-
--color-selection-bg: oklch(0.92 0.04 250);
|
|
80
|
-
--color-selection-fg: var(--color-fg);
|
|
81
|
-
--color-code-bg: oklch(0.945 0.005 80);
|
|
82
|
-
--color-code-fg: var(--color-fg);
|
|
125
|
+
--color-selection-bg: light-dark(oklch(0.92 0.04 250), oklch(0.30 0.06 250));
|
|
126
|
+
--color-selection-fg: light-dark(var(--color-fg), oklch(0.92 0.005 80));
|
|
127
|
+
--color-code-bg: light-dark(oklch(0.945 0.005 80), oklch(0.20 0.005 80)); /* inline code background */
|
|
128
|
+
--color-code-fg: light-dark(var(--color-fg), oklch(0.88 0.005 80));
|
|
83
129
|
|
|
84
130
|
/* Syntax highlighting (issue #53) — quiet, low-chroma slots derived
|
|
85
|
-
from remarque-light's ANSI colors (scripts/theme.mjs),
|
|
86
|
-
as round numbers (golden-gated: scripts/palette-golden.mjs).
|
|
87
|
-
slot ≥ 4.5:1 on --color-code-bg. Mapping recipe
|
|
88
|
-
+ Prism) is documented in REMARQUE.md's "Syntax
|
|
89
|
-
section. */
|
|
90
|
-
--color-syntax-keyword: oklch(0.51 0.12 250); /* ANSI blue — 4.88:1 on code-bg */
|
|
91
|
-
--color-syntax-string: oklch(0.50 0.12 145); /* ANSI green — 4.85:1 */
|
|
92
|
-
--color-syntax-constant: oklch(0.51 0.105 85);
|
|
93
|
-
--color-syntax-comment: oklch(0.52 0.01 80); /* solved muted neutral (
|
|
94
|
-
--color-syntax-function: oklch(0.52 0.12 310); /* ANSI purple — 4.96:1 */
|
|
95
|
-
--color-syntax-type: oklch(0.50 0.085 196); /* ANSI cyan — 4.90:1 */
|
|
96
|
-
--color-syntax-punctuation: oklch(0.52 0.01 80); /* derived neutral, muted family — 4.69:1 */
|
|
97
|
-
--color-syntax-variable: oklch(0.26 0.01 80); /* fg-adjacent (slight L offset) — 13.23:1 */
|
|
98
|
-
--color-syntax-link: var(--color-accent); /* accent already clears code-bg —
|
|
131
|
+
from remarque-light/remarque-dark's ANSI colors (scripts/theme.mjs),
|
|
132
|
+
serialized as round numbers (golden-gated: scripts/palette-golden.mjs).
|
|
133
|
+
Every slot ≥ 4.5:1 on --color-code-bg, both themes. Mapping recipe
|
|
134
|
+
(Shiki css-variables + Prism) is documented in REMARQUE.md's "Syntax
|
|
135
|
+
Highlighting" section. */
|
|
136
|
+
--color-syntax-keyword: light-dark(oklch(0.51 0.12 250), oklch(0.61 0.11 250)); /* ANSI blue — 4.88:1 light / 4.80:1 dark on code-bg */
|
|
137
|
+
--color-syntax-string: light-dark(oklch(0.50 0.12 145), oklch(0.60 0.11 145)); /* ANSI green — 4.85:1 / 4.80:1 */
|
|
138
|
+
--color-syntax-constant: light-dark(oklch(0.51 0.105 85), oklch(0.61 0.11 84)); /* ANSI yellow (numbers/booleans convention) — 4.94:1 / 4.73:1 */
|
|
139
|
+
--color-syntax-comment: light-dark(oklch(0.52 0.01 80), oklch(0.60 0.005 80)); /* solved muted neutral (light) / ANSI brightBlack (dark) — 4.69:1 / 4.59:1 */
|
|
140
|
+
--color-syntax-function: light-dark(oklch(0.52 0.12 310), oklch(0.62 0.11 310)); /* ANSI purple — 4.96:1 / 4.75:1 */
|
|
141
|
+
--color-syntax-type: light-dark(oklch(0.50 0.085 196), oklch(0.60 0.10 195)); /* ANSI cyan — 4.90:1 / 4.81:1 */
|
|
142
|
+
--color-syntax-punctuation: light-dark(oklch(0.52 0.01 80), oklch(0.60 0.005 80)); /* derived neutral, muted family — 4.69:1 / 4.59:1 */
|
|
143
|
+
--color-syntax-variable: light-dark(oklch(0.26 0.01 80), oklch(0.82 0.005 80)); /* fg-adjacent (slight L offset) — 13.23:1 / 10.37:1 */
|
|
144
|
+
--color-syntax-link: var(--color-accent); /* accent already clears code-bg in both themes — alias, declared once */
|
|
99
145
|
|
|
100
146
|
/* Semantic state colors (issue #26) — feedback moments (error/success/
|
|
101
147
|
warning banners, disabled controls), NOT decoration; the one-accent
|
|
102
148
|
rule still governs expressive color. Hand-authored from the house
|
|
103
149
|
ANSI hue conventions (scripts/theme.mjs derives error/success/warning
|
|
104
|
-
from a theme's red/green/yellow ANSI slots): error hue 25,
|
|
105
|
-
hue 145, warning hue 85 — restrained chroma, keep-if-passing
|
|
106
|
-
checked against the STRICTER of --color-bg/--color-surface
|
|
107
|
-
is stricter in both themes here, same as --color-muted).
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
-subtle
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
--color-error:
|
|
116
|
-
--color-
|
|
117
|
-
--color-success:
|
|
118
|
-
--color-
|
|
119
|
-
--color-warning:
|
|
120
|
-
--color-
|
|
121
|
-
--color-disabled: var(--color-muted); /* muted family, not ANSI-derived */
|
|
150
|
+
from a theme's red/green/yellow ANSI slots): error hue 25/26,
|
|
151
|
+
success hue 145, warning hue 85 — restrained chroma, keep-if-passing
|
|
152
|
+
lightness checked against the STRICTER of --color-bg/--color-surface
|
|
153
|
+
(surface is stricter in both themes here, same as --color-muted).
|
|
154
|
+
Disabled is neutral-derived (muted family), NOT ANSI — aliased to
|
|
155
|
+
--color-muted, declared once. -subtle companions (error/success/
|
|
156
|
+
warning only, no disabled-subtle) are derived like
|
|
157
|
+
--color-accent-subtle: near-bg lightness, state hue, low chroma —
|
|
158
|
+
verified so --color-fg stays ≥4.5:1 on them, the pairing that
|
|
159
|
+
matters for callout/banner backgrounds. */
|
|
160
|
+
--color-error: light-dark(oklch(0.52 0.12 25), oklch(0.62 0.11 26)); /* ANSI red — 5.44:1 bg / 5.28:1 surface light; 5.07:1 bg / 4.82:1 surface dark */
|
|
161
|
+
--color-error-subtle: light-dark(oklch(0.95 0.02 25), oklch(0.22 0.04 25)); /* faint red banner background */
|
|
162
|
+
--color-success: light-dark(oklch(0.51 0.12 145), oklch(0.61 0.11 145)); /* ANSI green — 5.08:1 bg / 4.93:1 surface light; 5.36:1 bg / 5.10:1 surface dark */
|
|
163
|
+
--color-success-subtle: light-dark(oklch(0.95 0.02 145), oklch(0.22 0.04 145)); /* faint green banner background */
|
|
164
|
+
--color-warning: light-dark(oklch(0.52 0.105 85), oklch(0.62 0.11 85)); /* ANSI yellow — 5.17:1 bg / 5.02:1 surface light; 5.29:1 bg / 5.03:1 surface dark */
|
|
165
|
+
--color-warning-subtle: light-dark(oklch(0.95 0.02 85), oklch(0.22 0.04 85)); /* faint yellow banner background */
|
|
166
|
+
--color-disabled: var(--color-muted); /* muted family, not ANSI-derived — alias, declared once */
|
|
122
167
|
|
|
123
168
|
/* Dataviz categorical ramp (issue #94) — hand-authored from
|
|
124
|
-
remarque-light's dataviz.categorical block
|
|
125
|
-
serialized as round numbers (golden-gated:
|
|
126
|
-
6 slots, not 8 — the dataset's own
|
|
127
|
-
exactly 6; shipping 8 would leave
|
|
128
|
-
slot ≥ 3:1 on --color-bg —
|
|
129
|
-
text's 4.5:1 (these are chart
|
|
130
|
-
Order is the dataset's own —
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
--color-viz-
|
|
139
|
-
--color-viz-
|
|
140
|
-
--color-viz-
|
|
169
|
+
remarque-light's/remarque-dark's dataviz.categorical block
|
|
170
|
+
(scripts/theme.mjs), serialized as round numbers (golden-gated:
|
|
171
|
+
scripts/palette-golden.mjs). 6 slots, not 8 — the dataset's own
|
|
172
|
+
floor (516/633 corpus themes ship exactly 6; shipping 8 would leave
|
|
173
|
+
2 unfillable on most themes). Every slot ≥ 3:1 on --color-bg —
|
|
174
|
+
Carbon's mark-on-background line, NOT text's 4.5:1 (these are chart
|
|
175
|
+
marks read at a glance, never prose). Order is the dataset's own —
|
|
176
|
+
NOT reconciled hue-for-hue between the light and dark side of a
|
|
177
|
+
given slot (dark mode is independently tuned, same stance as every
|
|
178
|
+
other slot; see REMARQUE.md "Dataviz Tokens") — --color-viz-3's
|
|
179
|
+
light value (hue ≈25, red) and dark value (hue ≈310, purple) are
|
|
180
|
+
deliberately different hues, for example; do not "fix" this into
|
|
181
|
+
matching hues. Pair with a non-color redundancy (shape, pattern, or
|
|
182
|
+
a direct label) — never color alone, per the same Carbon precedent. */
|
|
183
|
+
--color-viz-1: light-dark(oklch(0.538 0.121 250.5), oklch(0.708 0.129 249.5)); /* 4.74:1 light / 7.53:1 dark on bg */
|
|
184
|
+
--color-viz-2: light-dark(oklch(0.541 0.111 85.5), oklch(0.712 0.13 85.3)); /* 4.73:1 / 7.54:1 */
|
|
185
|
+
--color-viz-3: light-dark(oklch(0.524 0.12 24.6), oklch(0.724 0.129 310.2)); /* 5.35:1 / 7.55:1 */
|
|
186
|
+
--color-viz-4: light-dark(oklch(0.499 0.12 144.8), oklch(0.725 0.129 25.4)); /* 5.32:1 / 7.55:1 */
|
|
187
|
+
--color-viz-5: light-dark(oklch(0.524 0.121 309.6), oklch(0.696 0.129 144.9)); /* 5.33:1 / 7.53:1 */
|
|
188
|
+
--color-viz-6: light-dark(oklch(0.528 0.09 195.8), oklch(0.697 0.119 194.8)); /* 4.74:1 / 7.57:1 */
|
|
141
189
|
}
|
|
142
190
|
|
|
143
|
-
/* ───
|
|
191
|
+
/* ─── Theme selection mechanism ──────────────────────────── */
|
|
144
192
|
/*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* -
|
|
193
|
+
* Three rules, replacing the pre-0.24.0 shape (a full @media
|
|
194
|
+
* (prefers-color-scheme: dark) color block + a full [data-theme="dark"],
|
|
195
|
+
* :root.dark color block, each re-listing every --color-* value) — under
|
|
196
|
+
* light-dark(), a theme choice is a `color-scheme` override, not a
|
|
197
|
+
* second copy of the palette:
|
|
198
|
+
*
|
|
199
|
+
* 1. System-preference default needs NO rule here at all for colors —
|
|
200
|
+
* the :root block above already declared `color-scheme: light
|
|
201
|
+
* dark`, which is sufficient for every light-dark() call to track
|
|
202
|
+
* `prefers-color-scheme` on its own.
|
|
203
|
+
* 2. --weight-display is NOT a color, so it cannot be part of a
|
|
204
|
+
* light-dark() call (the CSS spec restricts light-dark() to
|
|
205
|
+
* color-valued contexts) — it keeps the pre-migration mechanism
|
|
206
|
+
* below, now carrying only this one token instead of the whole
|
|
207
|
+
* palette.
|
|
208
|
+
* 3. Explicit choice ([data-theme]) and the :root.dark compatibility
|
|
209
|
+
* bridge (ratified sunset: 1.0 — see "Governance & Deprecation")
|
|
210
|
+
* override `color-scheme` itself once, which is what makes every
|
|
211
|
+
* light-dark() declaration above re-resolve for that subtree.
|
|
212
|
+
*
|
|
213
|
+
* Specificity note (carried over from the pre-migration file): `[data-
|
|
214
|
+
* theme="dark"]` is (0,1,0) but `:root.dark` is (0,2,0) — a site
|
|
215
|
+
* override of `--weight-display` at lower specificity than (0,2,0) can
|
|
216
|
+
* still lose to the bridge selector. This asymmetry now affects exactly
|
|
217
|
+
* one non-alias token instead of the whole palette.
|
|
152
218
|
*/
|
|
153
219
|
|
|
154
220
|
@media (prefers-color-scheme: dark) {
|
|
155
221
|
:root {
|
|
156
|
-
--
|
|
157
|
-
--color-bg-subtle: oklch(0.19 0.01 80);
|
|
158
|
-
--color-fg: oklch(0.90 0.005 80); /* soft white, not pure */
|
|
159
|
-
--weight-display: 500; /* compensate thin serif hairlines on dark */
|
|
160
|
-
--color-fg-muted: oklch(0.70 0.01 80); /* 7.26:1 bg (AAA) */
|
|
161
|
-
--color-muted: oklch(0.60 0.01 80); /* 4.92:1 bg / 4.68:1 surface (AA with margin) */
|
|
162
|
-
--color-border: oklch(0.25 0.005 80); /* decorative only — see light-theme note */
|
|
163
|
-
--color-border-bold: oklch(0.50 0.01 80); /* functional borders — 3.23:1 (meets WCAG 1.4.11) */
|
|
164
|
-
--color-surface: oklch(0.19 0.01 80);
|
|
165
|
-
|
|
166
|
-
--color-accent: oklch(0.68 0.12 250); /* brighter in dark mode for contrast */
|
|
167
|
-
--color-accent-hover: oklch(0.75 0.12 250);
|
|
168
|
-
--color-accent-subtle: oklch(0.22 0.04 250);
|
|
169
|
-
|
|
170
|
-
--color-selection-bg: oklch(0.30 0.06 250);
|
|
171
|
-
--color-selection-fg: oklch(0.92 0.005 80);
|
|
172
|
-
--color-code-bg: oklch(0.20 0.005 80);
|
|
173
|
-
--color-code-fg: oklch(0.88 0.005 80);
|
|
174
|
-
|
|
175
|
-
/* Syntax highlighting (issue #53) — derived from remarque-dark's
|
|
176
|
-
ANSI colors; see the light block above for the full rationale. */
|
|
177
|
-
--color-syntax-keyword: oklch(0.61 0.11 250); /* ANSI blue — 4.80:1 on code-bg */
|
|
178
|
-
--color-syntax-string: oklch(0.60 0.11 145); /* ANSI green — 4.80:1 */
|
|
179
|
-
--color-syntax-constant: oklch(0.61 0.11 84); /* ANSI yellow — 4.73:1 */
|
|
180
|
-
--color-syntax-comment: oklch(0.60 0.005 80); /* ANSI brightBlack — 4.59:1 */
|
|
181
|
-
--color-syntax-function: oklch(0.62 0.11 310); /* ANSI purple — 4.75:1 */
|
|
182
|
-
--color-syntax-type: oklch(0.60 0.10 195); /* ANSI cyan — 4.81:1 */
|
|
183
|
-
--color-syntax-punctuation: oklch(0.60 0.005 80); /* derived neutral, muted family — 4.59:1 */
|
|
184
|
-
--color-syntax-variable: oklch(0.82 0.005 80); /* fg-adjacent (slight L offset) — 10.37:1 */
|
|
185
|
-
--color-syntax-link: var(--color-accent); /* accent already clears code-bg */
|
|
186
|
-
|
|
187
|
-
/* Semantic state colors (issue #26) — see the light-theme block above
|
|
188
|
-
for the full rationale. Same hues (red 25 / green 145 / yellow 85),
|
|
189
|
-
lightened for a dark background; keep-if-passing checked against
|
|
190
|
-
the stricter of bg/surface (surface, here too). */
|
|
191
|
-
--color-error: oklch(0.62 0.11 26); /* 5.07:1 bg / 4.82:1 surface */
|
|
192
|
-
--color-error-subtle: oklch(0.22 0.04 25);
|
|
193
|
-
--color-success: oklch(0.61 0.11 145); /* 5.36:1 bg / 5.10:1 surface */
|
|
194
|
-
--color-success-subtle: oklch(0.22 0.04 145);
|
|
195
|
-
--color-warning: oklch(0.62 0.11 85); /* 5.29:1 bg / 5.03:1 surface */
|
|
196
|
-
--color-warning-subtle: oklch(0.22 0.04 85);
|
|
197
|
-
--color-disabled: var(--color-muted);
|
|
198
|
-
|
|
199
|
-
/* Dataviz categorical ramp (issue #94) — derived from remarque-dark's
|
|
200
|
-
dataviz.categorical block; see the light block above for the full
|
|
201
|
-
rationale. Every slot ≥ 3:1 on --color-bg. */
|
|
202
|
-
--color-viz-1: oklch(0.708 0.129 249.5); /* 7.53:1 on bg */
|
|
203
|
-
--color-viz-2: oklch(0.712 0.13 85.3); /* 7.54:1 on bg */
|
|
204
|
-
--color-viz-3: oklch(0.724 0.129 310.2); /* 7.55:1 on bg */
|
|
205
|
-
--color-viz-4: oklch(0.725 0.129 25.4); /* 7.55:1 on bg */
|
|
206
|
-
--color-viz-5: oklch(0.696 0.129 144.9); /* 7.53:1 on bg */
|
|
207
|
-
--color-viz-6: oklch(0.697 0.119 194.8); /* 7.57:1 on bg */
|
|
222
|
+
--weight-display: 500;
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
/* Manual dark mode toggle support.
|
|
212
|
-
CANONICAL convention: [data-theme="dark"] — use it for new sites.
|
|
213
|
-
:root.dark is a compatibility bridge for class-keyed consumers
|
|
214
|
-
(sunset target: 1.0). NOTE the specificity asymmetry: [data-theme]
|
|
215
|
-
is (0,1,0) but :root.dark is (0,2,0) — site overrides that beat one
|
|
216
|
-
can lose to the other; always override with at least (0,2,0) or use
|
|
217
|
-
cascade order. Platform endgame worth tracking: color-scheme +
|
|
218
|
-
light-dark(), which obsoletes both selectors. */
|
|
219
226
|
[data-theme="dark"],
|
|
220
227
|
:root.dark {
|
|
228
|
+
color-scheme: dark;
|
|
221
229
|
--weight-display: 500;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
--color-
|
|
232
|
-
|
|
233
|
-
--color-accent-subtle: oklch(0.22 0.04 250);
|
|
234
|
-
|
|
235
|
-
--color-selection-bg: oklch(0.30 0.06 250);
|
|
236
|
-
--color-selection-fg: oklch(0.92 0.005 80);
|
|
237
|
-
--color-code-bg: oklch(0.20 0.005 80);
|
|
238
|
-
--color-code-fg: oklch(0.88 0.005 80);
|
|
239
|
-
|
|
240
|
-
/* Syntax highlighting (issue #53) — derived from remarque-dark's
|
|
241
|
-
ANSI colors; see the light block above for the full rationale. */
|
|
242
|
-
--color-syntax-keyword: oklch(0.61 0.11 250);
|
|
243
|
-
--color-syntax-string: oklch(0.60 0.11 145);
|
|
244
|
-
--color-syntax-constant: oklch(0.61 0.11 84);
|
|
245
|
-
--color-syntax-comment: oklch(0.60 0.005 80);
|
|
246
|
-
--color-syntax-function: oklch(0.62 0.11 310);
|
|
247
|
-
--color-syntax-type: oklch(0.60 0.10 195);
|
|
248
|
-
--color-syntax-punctuation: oklch(0.60 0.005 80);
|
|
249
|
-
--color-syntax-variable: oklch(0.82 0.005 80);
|
|
250
|
-
--color-syntax-link: var(--color-accent);
|
|
251
|
-
|
|
252
|
-
/* Semantic state colors (issue #26) — see the light-theme :root block
|
|
253
|
-
for the full rationale. */
|
|
254
|
-
--color-error: oklch(0.62 0.11 26);
|
|
255
|
-
--color-error-subtle: oklch(0.22 0.04 25);
|
|
256
|
-
--color-success: oklch(0.61 0.11 145);
|
|
257
|
-
--color-success-subtle: oklch(0.22 0.04 145);
|
|
258
|
-
--color-warning: oklch(0.62 0.11 85);
|
|
259
|
-
--color-warning-subtle: oklch(0.22 0.04 85);
|
|
260
|
-
--color-disabled: var(--color-muted);
|
|
230
|
+
/* Pre-migration, these two aliases were (redundantly) re-declared in
|
|
231
|
+
the dark block even though their value never changes — everywhere
|
|
232
|
+
else an unchanging alias was simply declared once (--color-link,
|
|
233
|
+
--color-link-hover, --color-focus-ring). Kept here, still redundant,
|
|
234
|
+
so tokens.json's palette entries for these two continue reporting
|
|
235
|
+
an explicit dark side rather than `inheritedFromLight` — a
|
|
236
|
+
tokens.json content change unrelated to the light-dark() migration
|
|
237
|
+
itself is out of scope for this release. */
|
|
238
|
+
--color-syntax-link: var(--color-accent);
|
|
239
|
+
--color-disabled: var(--color-muted);
|
|
240
|
+
}
|
|
261
241
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
--color-viz-1: oklch(0.708 0.129 249.5);
|
|
265
|
-
--color-viz-2: oklch(0.712 0.13 85.3);
|
|
266
|
-
--color-viz-3: oklch(0.724 0.129 310.2);
|
|
267
|
-
--color-viz-4: oklch(0.725 0.129 25.4);
|
|
268
|
-
--color-viz-5: oklch(0.696 0.129 144.9);
|
|
269
|
-
--color-viz-6: oklch(0.697 0.119 194.8);
|
|
242
|
+
[data-theme="light"] {
|
|
243
|
+
color-scheme: light;
|
|
270
244
|
}
|
|
271
245
|
|
|
272
246
|
/* ─── prefers-contrast: more ────────────────────────────────
|
|
@@ -280,57 +254,44 @@
|
|
|
280
254
|
* (the AAA 7:1 secondary-text floor) — one step further toward
|
|
281
255
|
* `--color-fg`, not a second palette. `--color-border` stays untouched:
|
|
282
256
|
* it is decorative BY DESIGN (documented sub-3:1), and `prefers-
|
|
283
|
-
* contrast` users who want its
|
|
284
|
-
*
|
|
285
|
-
*
|
|
257
|
+
* contrast` users who want its hairlines to read as structure are
|
|
258
|
+
* exactly the `forced-colors: active` audience already covered above —
|
|
259
|
+
* conflating the two would blur why each exists.
|
|
260
|
+
*
|
|
261
|
+
* ONE block, not three (issue #95 simplification): pre-migration this
|
|
262
|
+
* needed a plain `:root` override, a `[data-theme="dark"], :root.dark`
|
|
263
|
+
* override, AND a compound `@media (prefers-contrast: more) and
|
|
264
|
+
* (prefers-color-scheme: dark)` override to cover system-default,
|
|
265
|
+
* explicit-dark, and system-dark-without-an-explicit-choice respectively
|
|
266
|
+
* — three copies of two tokens. Under light-dark(), a single un-scoped
|
|
267
|
+
* `light-dark(<light>, <dark>)` override per token replaces its WHOLE
|
|
268
|
+
* value at once and re-resolves per element from whatever `color-scheme`
|
|
269
|
+
* that element already has (system, explicit, or bridge) — exactly the
|
|
270
|
+
* same mechanism the base palette above relies on. There is no dark-
|
|
271
|
+
* specific selector left to write.
|
|
286
272
|
*
|
|
287
273
|
* ARCHITECTURAL HOME: tokens-palette.css, not tokens-core.css. These are
|
|
288
|
-
* `--color-*` values
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* media-gated `:root` block cascading over the unconditional light
|
|
292
|
-
* values). A `prefers-contrast` override belongs wherever its sibling
|
|
293
|
-
* `prefers-color-scheme` override already lives, not in core, which
|
|
294
|
-
* would first require inventing a NEW non-color-tier concept for a
|
|
295
|
-
* color-tier problem.
|
|
274
|
+
* `--color-*` values, palette tier by the Token Tiers contract, the same
|
|
275
|
+
* tier that already carries the base light-dark() color-scheme regime
|
|
276
|
+
* immediately above.
|
|
296
277
|
*
|
|
297
|
-
* AUDIT-PARSER COMPATIBILITY (scripts/lib/css-tokens.mjs):
|
|
298
|
-
*
|
|
299
|
-
* (
|
|
300
|
-
*
|
|
301
|
-
* `
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* → WCAG luminance math `scripts/audit.mjs` uses (see the ratios in
|
|
311
|
-
* each comment) — just not by the automated gate, since there is
|
|
312
|
-
* nothing to gate on the fact that this is a strictly-improving branch.
|
|
278
|
+
* AUDIT-PARSER COMPATIBILITY (scripts/lib/css-tokens.mjs): `isLightRoot`
|
|
279
|
+
* requires `context === ''` (no enclosing at-rule at all) — a `@media
|
|
280
|
+
* (prefers-contrast: more)` block's context is that at-rule itself, so
|
|
281
|
+
* it matches neither `isLightRoot` nor `isDarkBlock`, and
|
|
282
|
+
* `scripts/audit.mjs`/`scripts/tokens-json.mjs` silently SKIP these
|
|
283
|
+
* declarations, same as before the migration. That is still correct:
|
|
284
|
+
* this block only ever WIDENS an already-passing contrast margin (there
|
|
285
|
+
* is nothing for the automated gate to enforce beyond what the base
|
|
286
|
+
* palette above already guarantees). Values are still hand-verified
|
|
287
|
+
* in-gamut and re-computed against the same OKLCH → sRGB → WCAG
|
|
288
|
+
* luminance math `scripts/audit.mjs` uses (see the ratios in each
|
|
289
|
+
* comment) — just not by the automated gate, since there is nothing to
|
|
290
|
+
* gate on a strictly-improving branch.
|
|
313
291
|
*/
|
|
314
292
|
@media (prefers-contrast: more) {
|
|
315
293
|
:root {
|
|
316
|
-
--color-border-bold: oklch(0.55 0.01 80);
|
|
317
|
-
--color-fg-muted: oklch(0.36 0.015 80); /* 7.55:1
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
[data-theme="dark"],
|
|
321
|
-
:root.dark {
|
|
322
|
-
--color-border-bold: oklch(0.58 0.01 80); /* 3.23:1 -> 4.53:1 on bg */
|
|
323
|
-
--color-fg-muted: oklch(0.78 0.01 80); /* 7.26:1 -> 9.70:1 on bg (9.22:1 on surface) */
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/* System dark preference (no explicit [data-theme] toggle set) needs its
|
|
328
|
-
own compound query — mirrors the plain dark-mode block's split between
|
|
329
|
-
`@media (prefers-color-scheme: dark)` and the `[data-theme="dark"]` /
|
|
330
|
-
`:root.dark` bridge above. */
|
|
331
|
-
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
|
|
332
|
-
:root {
|
|
333
|
-
--color-border-bold: oklch(0.58 0.01 80);
|
|
334
|
-
--color-fg-muted: oklch(0.78 0.01 80);
|
|
294
|
+
--color-border-bold: light-dark(oklch(0.55 0.01 80), oklch(0.58 0.01 80)); /* 3.39:1 → 4.52:1 light; 3.23:1 → 4.53:1 dark */
|
|
295
|
+
--color-fg-muted: light-dark(oklch(0.36 0.015 80), oklch(0.78 0.01 80)); /* 7.55:1 → 10.11:1 light (9.82:1 surface); 7.26:1 → 9.70:1 dark (9.22:1 surface) */
|
|
335
296
|
}
|
|
336
297
|
}
|
package/tokens.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Remarque design tokens — GENERATED from tokens.json by
|
|
3
|
-
* scripts/tokens-json.mjs (v0.
|
|
3
|
+
* scripts/tokens-json.mjs (v0.24.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
|
package/tokens.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
|
|
4
4
|
"$extensions": {
|
|
5
5
|
"remarque": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.24.0",
|
|
7
7
|
"tiers": {
|
|
8
8
|
"core": "immutable identity — overriding forks the system",
|
|
9
9
|
"palette": "sanctioned personalization surface — override freely, then run remarque-audit"
|