remarque-tokens 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_RULES.md CHANGED
@@ -145,6 +145,7 @@ project/
145
145
  ├── prose.css # .remarque-prose long-form styling (aggregated by tokens.css; own subpath for opt-out)
146
146
  ├── essay.css # Optional Essay module: sidenotes + sticky TOC rail (own subpath, NOT aggregated — import explicitly)
147
147
  ├── broadsheet.css # Optional Broadsheet pattern: masthead, lead, entry list, post kicker (own subpath, NOT aggregated — import explicitly)
148
+ ├── forms.css # Optional form control primitives: field/input/checkbox/radio/button, state-color wiring (own subpath, NOT aggregated — import explicitly)
148
149
  ├── print.css # Print stylesheet (own subpath, NOT aggregated — import explicitly)
149
150
  ├── theme.css # Tailwind v4 adapter (@theme inline) — import after tailwindcss + tokens
150
151
  ├── tailwind.config.js # Tailwind v3 ONLY — v4 projects use theme.css instead
@@ -207,6 +208,7 @@ Before considering any implementation complete, verify:
207
208
  - [ ] Every page maps to an archetype (Essay, Dossier, Notebook, Landing, Reference/Docs, Changelog, Gallery)
208
209
  - [ ] If the Essay uses sidenotes/a TOC rail (`remarque-tokens/essay`): `.remarque-sidenote-ref`/`.remarque-sidenote` alternate in strict DOM order, the rail never intrudes into `.remarque-prose`'s own measure, and the page renders correctly with `essay.css`'s `@media` block deleted (the narrow-viewport/no-JS fallback)
209
210
  - [ ] If a Landing/archive page uses the Broadsheet pattern (`remarque-tokens/broadsheet`): entry numerals are generated from `data-entry-number` via `attr()` (not `counter()`), the entry list stays a `<ul>` (not `<ol>` — the numeral is `aria-hidden` and decorative), and every kicker/dateline row uses `font-variant-caps: all-small-caps`, never `text-transform: uppercase`
211
+ - [ ] If a page uses form controls (`remarque-tokens/forms`): checkboxes/radios are styled with `accent-color` only — never `appearance: none` plus a hand-drawn replacement; every control (input, button, checkbox/radio label) is ≥44×44px; validation state lives on `.remarque-field[data-state]` AND a real `aria-invalid`/`aria-describedby` pair on the input, not `data-state` alone; disabled controls use `--color-disabled`, never a state color
210
212
  - [ ] Mobile version is roomy — not a compressed desktop layout
211
213
  - [ ] Mobile nav links have ≥44px touch targets
212
214
  - [ ] No pure white or pure black backgrounds
package/CHANGELOG.md CHANGED
@@ -4,6 +4,66 @@ 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.18.0 — 2026-07-23
8
+
9
+ Form control primitives + reference components (closes #27, closes #30).
10
+
11
+ ### Added
12
+ - **`forms.css` — form control primitives module (issue #27).** New own-subpath
13
+ module (`remarque-tokens/forms`, `remarque-tokens/forms.css`), NOT aggregated
14
+ into `tokens.css` — matches `essay.css`/`broadsheet.css`/`print.css`'s
15
+ opt-in convention. Spec-native (built directly from the issue, not
16
+ graduated from a downstream site — no prior flagship implementation to
17
+ re-express).
18
+ - `.remarque-field` (label + control + help/error message vertical stack),
19
+ `.remarque-field-label` (meta voice, same declarations as `.text-label`),
20
+ `.remarque-field-required`, `.remarque-field-message`.
21
+ - `.remarque-input` — shared class across `<input>`/`<textarea>`/`<select>`:
22
+ body-voice text, `--color-border-bold` boundary (functional, 3:1-checked),
23
+ `--radius-sm` (tighter than the general 8px ceiling — form controls stay
24
+ more precise-reading), ≥44px tall, `::placeholder` at `--color-muted`
25
+ (a conscious AA choice, documented — placeholder is supplementary hint
26
+ copy, never the field's only label).
27
+ - `.remarque-checkbox` / `.remarque-radio` — `accent-color` only, no
28
+ `appearance: none`, no hand-drawn replacement; native controls keep every
29
+ platform accessibility behavior. Sized at `--space-5` (24px), wrapped in
30
+ a `<label>` carrying the 44px touch target.
31
+ - `.remarque-button` (+ `--primary` variant) — quiet by rule: bordered,
32
+ transparent, body-voice text by default; `--primary` is the one
33
+ sanctioned accent placement per viewport, still unfilled (accent
34
+ text/border, hover washes in `--color-accent-subtle`, never a solid
35
+ fill). Disabled state (native `:disabled`) uses `--color-disabled`.
36
+ - Validation state wiring on the 0.17.0 state-color tokens:
37
+ `.remarque-field[data-state="error"|"success"|"warning"]` recolors the
38
+ input border and message text together; always pairs with a real
39
+ `aria-invalid`/`aria-describedby` on the input (`data-state` is paint
40
+ only). `.remarque-input:user-invalid`, guarded with
41
+ `@supports selector(:user-invalid)`, is a zero-JS bonus layer.
42
+ - `.remarque-table` / `.remarque-table-wrap` — a standalone-table
43
+ re-scoping of `prose.css`'s table rules (mono `th`, 2px header rule, 1px
44
+ row rules, `.num` tabular-lining columns) for data tables that shouldn't
45
+ be wrapped in the full `.remarque-prose` container. Lives in `forms.css`
46
+ rather than a fifth subpath — AGENT_RULES.md's build order already
47
+ groups tables with buttons/cards as one supplementary-UI step.
48
+ - REMARQUE.md "Forms" section — full markup contract, state-wiring rules,
49
+ restraint rules restated (radius/touch-targets/no-fake-replacements/
50
+ quiet-buttons/placeholder-contrast), standalone-tables note, and "when
51
+ NOT to use" (Remarque is editorial-first — contact/search/newsletter
52
+ moments, not app UIs). AGENT_RULES.md's File Structure Convention and
53
+ Quality Checklist gain matching entries.
54
+ - `scripts/audit.mjs`'s `--src .` invocation covers `forms.css` for free
55
+ (font-floor + no-hardcoded-color scans — no new pairings needed, this
56
+ module introduces no new color tokens).
57
+ - **Reference components (demo site, issue #30)** — `site/src/components/`:
58
+ `Button.astro`, `Input.astro` (a `.remarque-field` wrapper — label/help/
59
+ error props, wires `for`/`id`/`aria-describedby`/`aria-invalid`),
60
+ `Table.astro` (`.remarque-table`, standalone). New demo page
61
+ `site/pages/components` exercises every input state (default/focus/error/
62
+ success/disabled), both button variants, and a sample table in both
63
+ themes — added to `site/tests/helpers.ts`'s `PAGES` for visual-regression
64
+ coverage (new baselines for this page only; no existing baseline changed)
65
+ and linked from the site nav.
66
+
7
67
  ## 0.17.0 — 2026-07-23
8
68
 
9
69
  Semantic state colors + z-index scale (closes #26, closes #29).
package/REMARQUE.md CHANGED
@@ -978,6 +978,149 @@ the archive itself.
978
978
 
979
979
  ---
980
980
 
981
+ ## Forms
982
+
983
+ Native form-control primitives — field wrapper, text input, checkbox,
984
+ radio, button — shipped as their own subpath, `remarque-tokens/forms`
985
+ (issue #27). Unlike the Essay Module and the Broadsheet pattern, this
986
+ module was not graduated from a downstream site implementation; it is
987
+ spec-native, built directly from the issue rather than re-expressing an
988
+ existing literal.
989
+
990
+ ### Markup contract
991
+
992
+ ```html
993
+ <div class="remarque-field">
994
+ <label class="remarque-field-label" for="email">Email</label>
995
+ <input class="remarque-input" type="email" id="email" name="email"
996
+ aria-describedby="email-help" />
997
+ <p class="remarque-field-message" id="email-help">
998
+ We'll never share your email.
999
+ </p>
1000
+ </div>
1001
+
1002
+ <div class="remarque-field" data-state="error">
1003
+ <label class="remarque-field-label" for="email2">Email</label>
1004
+ <input class="remarque-input" type="email" id="email2" name="email2"
1005
+ aria-invalid="true" aria-describedby="email2-message" required />
1006
+ <p class="remarque-field-message" id="email2-message" role="alert">
1007
+ Enter a valid email address.
1008
+ </p>
1009
+ </div>
1010
+
1011
+ <label class="remarque-checkbox">
1012
+ <input type="checkbox" name="subscribe" />
1013
+ <span>Subscribe to updates</span>
1014
+ </label>
1015
+
1016
+ <label class="remarque-radio">
1017
+ <input type="radio" name="plan" value="a" />
1018
+ <span>Plan A</span>
1019
+ </label>
1020
+
1021
+ <button class="remarque-button" type="button">Cancel</button>
1022
+ <button class="remarque-button remarque-button--primary" type="submit">Submit</button>
1023
+ ```
1024
+
1025
+ | Class | Applies to | Notes |
1026
+ |---|---|---|
1027
+ | `.remarque-field` | wrapping `<div>` | Label + control + message vertical stack. Layout only — no border/background of its own. |
1028
+ | `.remarque-field-label` | `<label>` | Meta voice (mono, small caps, `--color-muted`) — same declarations as `.text-label`, repeated per the non-mixin precedent essay.css/broadsheet.css already set. |
1029
+ | `.remarque-field-required` | `<span aria-hidden="true">` inside the label | Accent-colored asterisk. `aria-hidden` — the input's own `required` attribute is the real signal a screen reader announces. |
1030
+ | `.remarque-field-message` | `<p>` after the control | Help text by default; recolored by the parent's `data-state`. Wire `aria-describedby` on the input to this element's `id` in every case, valid or not. |
1031
+ | `.remarque-input` | `<input>` / `<textarea>` / `<select>` | Body-voice text, `--color-border-bold` boundary, `--radius-sm`, ≥44px tall. |
1032
+ | `.remarque-checkbox` / `.remarque-radio` | wrapping `<label>` | `accent-color` on the native control — no `appearance: none`, no hand-drawn replacement. The wrapping label carries the 44px touch target; the control itself stays at its natural (enlarged) size. |
1033
+ | `.remarque-button` | `<button>` | Quiet default: bordered, transparent, body-voice text. |
1034
+ | `.remarque-button--primary` | `<button>` | The one sanctioned accent placement per viewport (Visual Rules) — accent text + accent border, never a solid accent fill. Hover washes in `--color-accent-subtle`, it does not darken to a filled block. |
1035
+
1036
+ ### State wiring
1037
+
1038
+ `data-state="error"` / `"success"` / `"warning"` on `.remarque-field`
1039
+ recolors both the input's border and the message text from the 0.17.0
1040
+ state-color tokens (`--color-error`/`--color-success`/`--color-warning`).
1041
+ `data-state` is the PAINT layer only — always pair it with the real
1042
+ accessibility signal on the input itself: `aria-invalid="true"` (screen
1043
+ readers announce this; `data-state` alone is invisible to them) and
1044
+ `aria-describedby` pointing at the `.remarque-field-message` id. A
1045
+ disabled control never gets a `data-state` — it uses the native
1046
+ `:disabled`/`disabled` state and `--color-disabled`, which is a muted
1047
+ register, not one of the three feedback hues (REMARQUE.md "State Colors":
1048
+ disabled is deliberately not ANSI-derived).
1049
+
1050
+ As a zero-JS bonus layer, `.remarque-input:user-invalid` (guarded with
1051
+ `@supports selector(:user-invalid)`) picks up the same error border once
1052
+ a browser supports the pseudo-class and the user has actually interacted
1053
+ with an invalid field — it never replaces authoring `data-state="error"`
1054
+ explicitly, since `:user-invalid` alone has no matching text-message
1055
+ recolor and no `aria-invalid` wiring of its own.
1056
+
1057
+ ### Restraint rules restated
1058
+
1059
+ - **Border radius**: form controls cap at `--radius-sm` (4px) — a
1060
+ tighter ceiling than AGENT_RULES.md's general `--radius-md` (8px)
1061
+ maximum for other chrome. A smaller, more precise-reading radius suits
1062
+ a control the user directly manipulates.
1063
+ - **Touch targets**: every interactive control — input, textarea,
1064
+ select, button, and the `<label>` wrapping a checkbox/radio — is
1065
+ ≥44×44px (WCAG 2.5.5 AAA, USWDS floor; see "Touch Targets" above and
1066
+ `.nav-link`'s identical convention in the demo site's `globals.css`).
1067
+ - **No fake replacements**: checkboxes and radios stay native elements
1068
+ recolored with `accent-color` only. No `appearance: none` plus a
1069
+ hand-drawn box/dot — that would trade away native keyboard behavior,
1070
+ forced-colors-mode support, and screen-reader state announcements for
1071
+ a purely cosmetic win this system doesn't need.
1072
+ - **Buttons stay quiet**: text-only or bordered, never filled/solid by
1073
+ default — the primary variant included. Accent appears as text/border
1074
+ color, never as a background fill (Visual Rules' one-accent rule and
1075
+ Components' "never filled/solid as default" both still govern the
1076
+ emphasized variant).
1077
+ - **Placeholder contrast**: `--color-muted` (4.5:1 on `--color-bg`/
1078
+ `--color-surface`), a conscious choice — placeholder text is
1079
+ supplementary hint copy, never the field's only label, so AA is the
1080
+ correct tier rather than `--color-fg-muted`'s AAA.
1081
+
1082
+ ### Standalone tables
1083
+
1084
+ `forms.css` also ships `.remarque-table` / `.remarque-table-wrap`
1085
+ (issue #30) — the same visual language as `.remarque-prose table`
1086
+ (mono `th` voice, 2px header rule, 1px row rules, `.num` for tabular-
1087
+ lining right-aligned numeric columns), re-scoped under a top-level
1088
+ class so a standalone data grid doesn't have to wrap itself in the full
1089
+ `.remarque-prose` container (which also applies oldstyle proportional
1090
+ numerals and prose spacing rhythm a data table doesn't want):
1091
+
1092
+ ```html
1093
+ <div class="remarque-table-wrap">
1094
+ <table class="remarque-table">
1095
+ <caption>Example</caption>
1096
+ <thead>
1097
+ <tr><th>Name</th><th class="num">Count</th></tr>
1098
+ </thead>
1099
+ <tbody>
1100
+ <tr><td>Rows</td><td class="num">42</td></tr>
1101
+ </tbody>
1102
+ </table>
1103
+ </div>
1104
+ ```
1105
+
1106
+ Lives in `forms.css` rather than its own subpath — AGENT_RULES.md's
1107
+ build order already groups tables with buttons/cards/code blocks as one
1108
+ "supplementary UI" step, and this table variant is a small re-scoping,
1109
+ not enough surface area to justify a fourth optional module.
1110
+
1111
+ ### When NOT to use
1112
+
1113
+ Remarque is editorial-first. This module exists for the handful of
1114
+ form moments a written-word site actually needs — a contact form, a
1115
+ site search box, a newsletter signup — not for building application
1116
+ UIs. If a page needs more than a few fields, conditional multi-step
1117
+ logic, inline data tables of editable rows, or anything that starts to
1118
+ resemble a settings/dashboard screen, that is out of scope for
1119
+ Remarque's forms primitives; reach for a dedicated UI/form library
1120
+ instead and keep Remarque to the page's editorial chrome around it.
1121
+
1122
+ ---
1123
+
981
1124
  ## Signature Moves
982
1125
 
983
1126
  These are the repeatable visual tells that make a Remarque site recognizable:
package/forms.css ADDED
@@ -0,0 +1,424 @@
1
+ /*
2
+ * Remarque — Form Control Primitives (issue #27)
3
+ * ────────────────────────────────────────────────
4
+ * Namespaced classes that style native form elements — no fake replacement
5
+ * widgets. A `<select>` is a `<select>`, a checkbox stays a checkbox with
6
+ * `accent-color` doing the recoloring work a browser already supports
7
+ * natively. This module exists because Remarque is editorial-first: forms
8
+ * are for the contact/search/newsletter moments a written-word site
9
+ * occasionally needs, not a component library for building app UIs. See
10
+ * "When NOT to use" in REMARQUE.md's "Forms" section before reaching for
11
+ * this on a page that wants more than a handful of fields. Also carries a
12
+ * small standalone-table section (`.remarque-table`) at the end of this
13
+ * file — see that section's header comment for why it lives here rather
14
+ * than in prose.css (issue #30's reference `Table.astro` needs the same
15
+ * table visual language OUTSIDE `.remarque-prose`).
16
+ *
17
+ * PROVENANCE — spec-native, not graduated from a downstream site (unlike
18
+ * essay.css/broadsheet.css). Built directly from issue #27; there was no
19
+ * prior flagship implementation to re-express in token vocabulary, so
20
+ * every value below is original to this module rather than a mapped-over
21
+ * site literal.
22
+ *
23
+ * Own subpath (remarque-tokens/forms, remarque-tokens/forms.css) — NOT
24
+ * included by the tokens.css aggregator, matching essay.css/broadsheet.css/
25
+ * print.css: most Remarque pages have zero forms. Import explicitly, after
26
+ * prose.css if both are in use:
27
+ * @import 'remarque-tokens/prose.css';
28
+ * @import 'remarque-tokens/forms.css';
29
+ * (string form — see AGENT_RULES.md Pitfall #6.)
30
+ *
31
+ * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────
32
+ *
33
+ * <div class="remarque-field">
34
+ * <label class="remarque-field-label" for="email">Email</label>
35
+ * <input class="remarque-input" type="email" id="email" name="email"
36
+ * aria-describedby="email-help" />
37
+ * <p class="remarque-field-message" id="email-help">
38
+ * We'll never share your email.
39
+ * </p>
40
+ * </div>
41
+ *
42
+ * <div class="remarque-field" data-state="error">
43
+ * <label class="remarque-field-label" for="email2">Email</label>
44
+ * <input class="remarque-input" type="email" id="email2" name="email2"
45
+ * aria-invalid="true" aria-describedby="email2-message" required />
46
+ * <p class="remarque-field-message" id="email2-message" role="alert">
47
+ * Enter a valid email address.
48
+ * </p>
49
+ * </div>
50
+ *
51
+ * <label class="remarque-checkbox">
52
+ * <input type="checkbox" name="subscribe" />
53
+ * <span>Subscribe to updates</span>
54
+ * </label>
55
+ *
56
+ * <label class="remarque-radio">
57
+ * <input type="radio" name="plan" value="a" />
58
+ * <span>Plan A</span>
59
+ * </label>
60
+ *
61
+ * <button class="remarque-button" type="button">Cancel</button>
62
+ * <button class="remarque-button remarque-button--primary" type="submit">
63
+ * Submit
64
+ * </button>
65
+ *
66
+ * `data-state` lives on `.remarque-field` (not the input) — it recolors
67
+ * both the control's border AND the message text together from one
68
+ * attribute, the same "one attribute drives several descendants" shape
69
+ * essay.css's counter-driven numbering uses. `aria-invalid="true"` is the
70
+ * REAL accessibility signal (screen readers announce it); `data-state` is
71
+ * the visual layer on top. Author both together — see "State wiring"
72
+ * below for what happens if a consumer only sets one.
73
+ */
74
+
75
+ /* ─── Field wrapper: label + control + help/error text stack ────────────
76
+ * A vertical rhythm of three parts. `--space-2` between label and control
77
+ * (tight — they read as one unit), `--space-2` again before the message
78
+ * line. No border, no background on the wrapper itself — `.remarque-field`
79
+ * is layout only, never a bordered "card" around the control (Components:
80
+ * "no component should be visually louder than the content it contains").
81
+ */
82
+ .remarque-field {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: var(--space-2);
86
+ max-width: var(--content-reading);
87
+ }
88
+
89
+ .remarque-field + .remarque-field {
90
+ margin-top: var(--space-5);
91
+ }
92
+
93
+ /* Label voice: same declarations as `.text-label` (tokens-core.css),
94
+ repeated rather than depended-on-as-a-mixin — the same non-mixin
95
+ precedent essay.css's TOC summary and broadsheet.css's kicker rows
96
+ establish. True small caps (`font-variant-caps`), never `text-transform:
97
+ uppercase` (REMARQUE.md "Small Caps"). Quiet mono metadata voice for the
98
+ label, not the body face — a form label identifies the field, the VALUE
99
+ typed into it is the content, and that distinction is the same one
100
+ `.text-label` already draws for dates/tags/status elsewhere. */
101
+ .remarque-field-label {
102
+ font-family: var(--font-mono);
103
+ font-size: var(--text-meta);
104
+ line-height: var(--leading-meta);
105
+ font-variant-caps: all-small-caps;
106
+ letter-spacing: var(--tracking-caps);
107
+ color: var(--color-muted);
108
+ }
109
+
110
+ /* Required-field marker: a plain asterisk in the accent color is the one
111
+ sanctioned use here (it is unambiguously "this is the interactive thing
112
+ that needs attention right now" — not decoration). Author it inside the
113
+ label: <label>Email <span class="remarque-field-required"
114
+ aria-hidden="true">*</span></label> — aria-hidden because `required` on
115
+ the input itself is the real signal a screen reader announces. */
116
+ .remarque-field-required {
117
+ color: var(--color-accent);
118
+ }
119
+
120
+ /* Help/error/success message line — meta voice, body-face-adjacent size
121
+ floor (--text-meta, 14px, clears the USWDS small-text minimum). Neutral
122
+ (muted) by default; recolored by `.remarque-field[data-state]` below.
123
+ `text-wrap: pretty` matches `.remarque-prose`'s optical default for
124
+ short justified-feeling message copy. */
125
+ .remarque-field-message {
126
+ font-family: var(--font-mono);
127
+ font-size: var(--text-meta);
128
+ line-height: var(--leading-meta);
129
+ letter-spacing: var(--tracking-meta);
130
+ color: var(--color-muted);
131
+ margin: 0;
132
+ text-wrap: pretty;
133
+ }
134
+
135
+ /* ─── Text inputs: input / textarea / select ─────────────────────────────
136
+ * One shared class across all three native elements — same border,
137
+ * radius, spacing, and body-voice typography (Font Slots: "Body ... UI
138
+ * labels, navigation, buttons" — form values are UI labels' closest
139
+ * relative here, not metadata). `--color-border-bold`, not `-bold`'s
140
+ * quieter sibling: an input's boundary is a FUNCTIONAL edge (WCAG 1.4.11
141
+ * non-text contrast), the same reasoning `.remarque-sidenote`'s narrow-
142
+ * viewport border and `.remarque-toc-rail`'s hairline both already apply
143
+ * decoratively-vs-functionally in essay.css.
144
+ */
145
+ .remarque-input {
146
+ display: block;
147
+ width: 100%;
148
+ min-height: 44px; /* USWDS/WCAG 2.5.5 touch-target floor — see AGENT_RULES.md */
149
+ padding: var(--space-2) var(--space-3);
150
+ font-family: var(--font-body);
151
+ font-size: var(--text-body);
152
+ line-height: var(--leading-title);
153
+ color: var(--color-fg);
154
+ background-color: var(--color-bg);
155
+ border: var(--border-width) var(--border-style) var(--color-border-bold);
156
+ /* Restraint rule (AGENT_RULES.md "Border radius never exceeds
157
+ --radius-md" + this module's own tighter floor): form controls stay
158
+ at --radius-sm, never the 8px ceiling other chrome is allowed. A
159
+ smaller control reads as more precise at a smaller radius. */
160
+ border-radius: var(--radius-sm);
161
+ transition: border-color var(--motion-fast) var(--motion-easing);
162
+ }
163
+
164
+ textarea.remarque-input {
165
+ min-height: 6rem; /* several lines tall by default — still clears the 44px floor */
166
+ resize: vertical;
167
+ }
168
+
169
+ /* Native select's disclosure triangle is left as the platform default —
170
+ restyling it means either `appearance: none` (which then requires a
171
+ hand-drawn replacement icon, exactly the "fake replacement" this module
172
+ avoids) or accepting a single browser's rendering as the reference,
173
+ neither of which is worth it for one triangle. */
174
+ .remarque-input:hover {
175
+ border-color: var(--color-fg-muted);
176
+ }
177
+
178
+ /* Placeholder: --color-muted, the same 4.5:1-on-bg/-surface token used
179
+ everywhere else at this contrast tier — a conscious choice, not the
180
+ browser default (which is often far below AA). Placeholder text is
181
+ supplementary (never the only label — every `.remarque-input` pairs
182
+ with a real `<label>`), so 4.5:1 rather than `--color-fg-muted`'s 7:1
183
+ is the correct tier: AA is sufficient for non-essential hint copy. */
184
+ .remarque-input::placeholder {
185
+ color: var(--color-muted);
186
+ opacity: 1; /* Firefox lowers placeholder opacity by default; pin it so the token value is what actually renders */
187
+ }
188
+
189
+ .remarque-input:disabled {
190
+ color: var(--color-disabled);
191
+ background-color: var(--color-bg-subtle);
192
+ border-color: var(--color-border);
193
+ cursor: not-allowed;
194
+ }
195
+
196
+ /* ─── Checkbox / Radio — accent-color, no fake replacements ──────────────
197
+ * `accent-color` is the ENTIRE recoloring mechanism: no `appearance:
198
+ * none`, no hand-drawn checkmark/dot, no absolutely-positioned pseudo-
199
+ * element standing in for the native control. This is a deliberate
200
+ * ceiling on customization, not an oversight — every browser Remarque
201
+ * targets supports `accent-color` on checkboxes/radios, and a real native
202
+ * control keeps every platform accessibility behavior (keyboard,
203
+ * high-contrast mode, forced-colors mode, screen-reader state
204
+ * announcements) for free, at the cost of not controlling the checkmark
205
+ * glyph's exact shape.
206
+ *
207
+ * Sizing: --space-5 (1.5rem / 24px) — comfortably inside the 44px touch
208
+ * target once the wrapping <label> below adds its own min-height, and
209
+ * large enough to keep the accent-colored fill legible (a 16px default
210
+ * checkbox reads as fussy at this system's generous spacing scale).
211
+ */
212
+ .remarque-checkbox,
213
+ .remarque-radio {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ gap: var(--space-2);
217
+ min-height: 44px; /* touch-target floor on the whole label, same shape as .nav-link */
218
+ font-family: var(--font-body);
219
+ font-size: var(--text-body);
220
+ line-height: var(--leading-title);
221
+ color: var(--color-fg);
222
+ cursor: pointer;
223
+ }
224
+
225
+ .remarque-checkbox input,
226
+ .remarque-radio input {
227
+ accent-color: var(--color-accent);
228
+ width: var(--space-5);
229
+ height: var(--space-5);
230
+ margin: 0;
231
+ flex-shrink: 0;
232
+ }
233
+
234
+ /* `:has()` is a light progressive enhancement here, not a functional
235
+ dependency — a browser without it still gets a fully working, natively
236
+ disabled control; it just skips the muted label-text recolor. */
237
+ .remarque-checkbox:has(input:disabled),
238
+ .remarque-radio:has(input:disabled) {
239
+ color: var(--color-disabled);
240
+ cursor: not-allowed;
241
+ }
242
+
243
+ .remarque-checkbox input:disabled,
244
+ .remarque-radio input:disabled {
245
+ cursor: not-allowed;
246
+ }
247
+
248
+ /* ─── Buttons ─────────────────────────────────────────────────────────────
249
+ * Quiet by rule (AGENT_RULES.md Components: "Buttons are quiet: text-only
250
+ * or subtle bordered. Never filled/solid as default."). The default
251
+ * variant is bordered, transparent background, body-voice text — reads as
252
+ * "a clickable label," not a colored block. `--primary` is the ONE
253
+ * sanctioned accent placement per viewport (Visual Rules: accent for
254
+ * "inline links and one interactive element per viewport") — even there,
255
+ * it stays unfilled: accent text + accent border, never a solid accent
256
+ * fill, so the same restraint the default variant holds carries through
257
+ * to the emphasized one. Hover on `--primary` washes in
258
+ * `--color-accent-subtle` (the same quiet-highlight token
259
+ * `.remarque-prose mark` already uses) rather than darkening to a filled
260
+ * block — a wash, not a fill.
261
+ */
262
+ .remarque-button {
263
+ display: inline-flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ gap: var(--space-2);
267
+ min-height: 44px;
268
+ padding: var(--space-2) var(--space-4);
269
+ font-family: var(--font-body);
270
+ font-size: var(--text-body);
271
+ font-weight: var(--weight-medium);
272
+ line-height: var(--leading-title);
273
+ color: var(--color-fg);
274
+ background-color: transparent;
275
+ border: var(--border-width) var(--border-style) var(--color-border-bold);
276
+ border-radius: var(--radius-sm);
277
+ cursor: pointer;
278
+ transition:
279
+ color var(--motion-fast) var(--motion-easing),
280
+ border-color var(--motion-fast) var(--motion-easing),
281
+ background-color var(--motion-fast) var(--motion-easing);
282
+ }
283
+
284
+ .remarque-button:hover:not(:disabled) {
285
+ border-color: var(--color-fg-muted);
286
+ color: var(--color-fg);
287
+ }
288
+
289
+ .remarque-button--primary {
290
+ color: var(--color-accent);
291
+ border-color: var(--color-accent);
292
+ }
293
+
294
+ .remarque-button--primary:hover:not(:disabled) {
295
+ color: var(--color-accent-hover);
296
+ border-color: var(--color-accent-hover);
297
+ background-color: var(--color-accent-subtle);
298
+ }
299
+
300
+ .remarque-button:disabled {
301
+ color: var(--color-disabled);
302
+ border-color: var(--color-border);
303
+ background-color: transparent;
304
+ cursor: not-allowed;
305
+ }
306
+
307
+ /* ─── Validation state wiring (0.17.0 state-color tokens) ────────────────
308
+ * `data-state` lives on `.remarque-field`, cascading down to the input's
309
+ * border and the message line's color together — author it alongside a
310
+ * real `aria-invalid="true"`/`aria-describedby` pair on the input (the
311
+ * markup contract above); `data-state` alone changes only the paint,
312
+ * `aria-invalid` is what a screen reader actually announces. `--warning`
313
+ * covers a caution/needs-attention message that isn't yet a hard failure
314
+ * (REMARQUE.md "State Colors" — "states needing attention before
315
+ * proceeding"), e.g. a password-strength hint. No `-subtle` background is
316
+ * used here: `-subtle` is sized for banner/callout backgrounds (REMARQUE.md
317
+ * "State Colors"), and painting a per-field background would make the
318
+ * field louder than the content it holds (Components rule) — border +
319
+ * message-text color is enough signal at this scale.
320
+ */
321
+ .remarque-field[data-state="error"] .remarque-input,
322
+ .remarque-input[aria-invalid="true"] {
323
+ border-color: var(--color-error);
324
+ }
325
+
326
+ .remarque-field[data-state="error"] .remarque-field-message {
327
+ color: var(--color-error);
328
+ }
329
+
330
+ .remarque-field[data-state="success"] .remarque-input {
331
+ border-color: var(--color-success);
332
+ }
333
+
334
+ .remarque-field[data-state="success"] .remarque-field-message {
335
+ color: var(--color-success);
336
+ }
337
+
338
+ .remarque-field[data-state="warning"] .remarque-input {
339
+ border-color: var(--color-warning);
340
+ }
341
+
342
+ .remarque-field[data-state="warning"] .remarque-field-message {
343
+ color: var(--color-warning);
344
+ }
345
+
346
+ /* `:user-invalid` — native, zero-JS validity feedback (matches only after
347
+ the user has interacted with the field and left it invalid, unlike
348
+ `:invalid`, which can match a required-but-untouched field on page
349
+ load). Guarded with `@supports selector(...)`: a browser lacking the
350
+ pseudo-class simply never matches this block and falls back to
351
+ whatever `data-state`/`aria-invalid` already provide — the same
352
+ "degrades to the no-JS/no-support baseline" shape essay.css's `@media`
353
+ gate uses for its own progressive enhancement. This is a bonus layer,
354
+ not a replacement for authoring `data-state="error"` explicitly. */
355
+ @supports selector(:user-invalid) {
356
+ .remarque-input:user-invalid {
357
+ border-color: var(--color-error);
358
+ }
359
+ }
360
+
361
+ /* ─── Standalone data tables (issue #30) ──────────────────────────────────
362
+ * `.remarque-table` re-scopes prose.css's table rules (mono `th` voice,
363
+ * 2px `border-bottom` under the header row, 1px row rules only, `.num`
364
+ * for tabular-lining right-aligned numeric columns) under a top-level
365
+ * class instead of `.remarque-prose table`. This is intentionally a
366
+ * near-verbatim copy of those declarations, not a shared mixin (CSS has
367
+ * none) and not a requirement to wrap a standalone data table in the full
368
+ * `.remarque-prose` container — `.remarque-prose` also applies oldstyle
369
+ * proportional numerals, `> * + *` paragraph-spacing rhythm, and prose
370
+ * max-width, none of which a reference data grid (e.g. `Table.astro`)
371
+ * wants. Living in forms.css rather than a new subpath: this module is
372
+ * already "the supplementary-UI module" in AGENT_RULES.md's build order
373
+ * (step 7 groups buttons, tags, cards, code blocks, AND tables together),
374
+ * and a table this small does not justify its own subpath the way the
375
+ * Essay/Broadsheet modules' much larger surface area did.
376
+ *
377
+ * Wrap a wide table exactly like prose.css's convention:
378
+ * <div class="remarque-table-wrap"><table class="remarque-table">...</table></div>
379
+ */
380
+ .remarque-table-wrap {
381
+ overflow-x: auto;
382
+ -webkit-overflow-scrolling: touch;
383
+ }
384
+
385
+ .remarque-table {
386
+ width: 100%;
387
+ border-collapse: collapse;
388
+ font-size: var(--text-body);
389
+ }
390
+
391
+ .remarque-table caption {
392
+ font-family: var(--font-mono);
393
+ font-size: var(--text-meta);
394
+ letter-spacing: var(--tracking-meta);
395
+ color: var(--color-muted);
396
+ text-align: left;
397
+ caption-side: top;
398
+ margin-bottom: var(--space-2);
399
+ }
400
+
401
+ .remarque-table th {
402
+ font-family: var(--font-mono);
403
+ font-size: var(--text-meta);
404
+ letter-spacing: var(--tracking-meta);
405
+ font-weight: var(--weight-semibold);
406
+ text-align: left;
407
+ padding: var(--space-2) var(--space-3);
408
+ border-bottom: 2px solid var(--color-border-bold);
409
+ color: var(--color-fg);
410
+ }
411
+
412
+ .remarque-table td {
413
+ font-family: var(--font-body);
414
+ font-size: var(--text-body);
415
+ padding: var(--space-2) var(--space-3);
416
+ border-bottom: var(--border-width) var(--border-style) var(--color-border);
417
+ color: var(--color-fg);
418
+ }
419
+
420
+ .remarque-table th.num,
421
+ .remarque-table td.num {
422
+ font-variant-numeric: tabular-nums lining-nums;
423
+ text-align: right;
424
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -72,6 +72,8 @@
72
72
  "./essay.css": "./essay.css",
73
73
  "./broadsheet": "./broadsheet.css",
74
74
  "./broadsheet.css": "./broadsheet.css",
75
+ "./forms": "./forms.css",
76
+ "./forms.css": "./forms.css",
75
77
  "./agent-rules": "./AGENT_RULES.md",
76
78
  "./spec": "./REMARQUE.md",
77
79
  "./package.json": "./package.json"
@@ -98,6 +100,7 @@
98
100
  "print.css",
99
101
  "essay.css",
100
102
  "broadsheet.css",
103
+ "forms.css",
101
104
  "scripts/lib/css-tokens.mjs"
102
105
  ]
103
106
  }
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.17.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.18.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
@@ -2,7 +2,7 @@
2
2
  "$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
3
3
  "$extensions": {
4
4
  "remarque": {
5
- "version": "0.17.0",
5
+ "version": "0.18.0",
6
6
  "tiers": {
7
7
  "core": "immutable identity — overriding forks the system",
8
8
  "palette": "sanctioned personalization surface — override freely, then run remarque-audit"