css-is-awesome 1.19.2 → 1.21.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.
Files changed (41) hide show
  1. package/AGENTS.md +11 -5
  2. package/CHANGELOG.md +20 -0
  3. package/CONTRACT.md +10 -0
  4. package/MIGRATION.md +58 -0
  5. package/README.md +19 -11
  6. package/bin/analyze.cjs +47 -2
  7. package/bin/cia.cjs +10 -0
  8. package/bin/fix-theme.cjs +151 -0
  9. package/llm.txt +5 -5
  10. package/mcp/server.cjs +64 -3
  11. package/package.json +6 -3
  12. package/public/theme.css +138 -0
  13. package/public/themes/boilerplate/theme.css +6 -0
  14. package/public/themes/boilerplate-dark/theme.css +6 -0
  15. package/public/themes/boilerplate-light/theme.css +6 -0
  16. package/public/themes/cupertino/theme.css +6 -0
  17. package/public/themes/cupertino-dark/theme.css +6 -0
  18. package/public/themes/cupertino-light/theme.css +6 -0
  19. package/public/themes/glass/theme.css +6 -0
  20. package/public/themes/glass-dark/theme.css +6 -0
  21. package/public/themes/glass-light/theme.css +6 -0
  22. package/public/themes/graphite/theme.css +6 -0
  23. package/public/themes/graphite-dark/theme.css +6 -0
  24. package/public/themes/graphite-light/theme.css +6 -0
  25. package/public/themes/press/theme.css +6 -0
  26. package/public/themes/press-dark/theme.css +6 -0
  27. package/public/themes/press-light/theme.css +6 -0
  28. package/public/themes/prism/theme.css +6 -0
  29. package/public/themes/prism-dark/theme.css +6 -0
  30. package/public/themes/prism-light/theme.css +6 -0
  31. package/public/themes/sketchbook/theme.css +6 -0
  32. package/public/themes/sketchbook-dark/theme.css +6 -0
  33. package/public/themes/sketchbook-light/theme.css +6 -0
  34. package/public/themes/terminal/theme.css +6 -0
  35. package/public/themes/terminal-dark/theme.css +6 -0
  36. package/public/themes/terminal-light/theme.css +6 -0
  37. package/scripts/audit-pairs.json +171 -23
  38. package/scripts/fix-theme.cjs +184 -0
  39. package/scripts/theme-contract.json +33 -4
  40. package/scripts/theme-validator.js +40 -1
  41. package/scss/_layout.scss +129 -0
@@ -202,4 +202,10 @@
202
202
  --z-modal: 1050;
203
203
  --z-popover: 1060;
204
204
  --z-tooltip: 1070;
205
+
206
+ /* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
207
+ --page-hero-bg: #1f1f1f;
208
+ --page-hero-ink: #f4efe2;
209
+ --page-band-bg: #1d1c1b;
210
+ --page-band-ink: #f4efe2;
205
211
  }
@@ -189,4 +189,10 @@
189
189
  --space-7: calc(var(--space-unit) * 24);
190
190
  --space-8: calc(var(--space-unit) * 32);
191
191
  --space-9: calc(var(--space-unit) * 48);
192
+
193
+ /* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
194
+ --page-hero-bg: #eae8e2;
195
+ --page-hero-ink: #2a241e;
196
+ --page-band-bg: #efede5;
197
+ --page-band-ink: #2a241e;
192
198
  }
@@ -167,4 +167,10 @@
167
167
  --input-radius: 0;
168
168
  --card-shadow: var(--glow-sm);
169
169
  --card-border: 1px solid var(--ink-faint);
170
+
171
+ /* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
172
+ --page-hero-bg: #111a19;
173
+ --page-hero-ink: #4fe078;
174
+ --page-band-bg: #0e1513;
175
+ --page-band-ink: #4fe078;
170
176
  }
@@ -237,4 +237,10 @@
237
237
  --input-radius: 0;
238
238
  --card-shadow: var(--glow-sm);
239
239
  --card-border: 1px solid var(--ink-faint);
240
+
241
+ /* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
242
+ --page-hero-bg: #111a19;
243
+ --page-hero-ink: #4fe078;
244
+ --page-band-bg: #0e1513;
245
+ --page-band-ink: #4fe078;
240
246
  }
@@ -160,4 +160,10 @@
160
160
  --z-modal: 1050;
161
161
  --z-popover: 1060;
162
162
  --z-tooltip: 1070;
163
+
164
+ /* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
165
+ --page-hero-bg: #dadfe4;
166
+ --page-hero-ink: #1a1a1a;
167
+ --page-band-bg: #e0e3e6;
168
+ --page-band-ink: #1a1a1a;
163
169
  }
@@ -1,25 +1,173 @@
1
1
  [
2
- { "fg": "--text-primary", "bg": "--paper", "required": 4.5, "kind": "text", "note": "primary body text on the page background" },
3
- { "fg": "--text-secondary", "bg": "--paper", "required": 4.5, "kind": "text", "note": "secondary body text on the page background" },
4
- { "fg": "--text-tertiary", "bg": "--paper", "required": 3.0, "kind": "large-text", "warnIfBelow": 4.5, "note": "tertiary text - legal at large sizes; warn if used as body" },
5
- { "fg": "--text-muted", "bg": "--paper", "required": 4.5, "kind": "text", "note": "muted UI text on the page background" },
6
- { "fg": "--ink", "bg": "--paper", "required": 4.5, "kind": "text", "note": "native ink on native paper" },
7
- { "fg": "--ink-soft", "bg": "--paper", "required": 4.5, "kind": "text", "note": "soft ink on native paper" },
8
- { "fg": "--ink-faint", "bg": "--paper", "required": 3.0, "kind": "large-text", "warnIfBelow": 4.5, "note": "faint ink - large/UI only; warn if used as body" },
9
- { "fg": "--text-link", "bg": "--paper", "required": 4.5, "kind": "text", "note": "inline link color on the page background" },
10
- { "fg": "--text-inverse", "bg": "--action-primary-default", "required": 4.5, "kind": "text", "note": "button label on the primary action surface" },
11
- { "fg": "--text-inverse", "bg": "--ai", "required": 4.5, "kind": "text", "note": "inverse text on the AI/brand accent surface" },
12
- { "fg": "--success-text", "bg": "--success-subtle", "required": 4.5, "kind": "text", "note": "success message text on its subtle background" },
13
- { "fg": "--warning-text", "bg": "--warning-subtle", "required": 4.5, "kind": "text", "note": "warning message text on its subtle background" },
14
- { "fg": "--error-text", "bg": "--error-subtle", "required": 4.5, "kind": "text", "note": "error message text on its subtle background" },
15
- { "fg": "--info-text", "bg": "--info-subtle", "required": 4.5, "kind": "text", "note": "info message text on its subtle background" },
16
- { "fg": "--border-default", "bg": "--paper", "required": 3.0, "kind": "non-text", "decorative": true, "note": "default border against the page background - decorative; WCAG 2.2 SC 1.4.11 applies only to graphical objects essential for understanding content" },
17
- { "fg": "--border-focus", "bg": "--paper", "required": 3.0, "kind": "non-text", "note": "focus ring against the page background" },
18
- { "fg": "--shu", "bg": "--paper", "required": 3.0, "kind": "non-text", "note": "seal accent - must read as a UI mark on paper" },
19
-
20
- { "fg": "--code-ink", "bg": "--code-bg", "required": 4.5, "kind": "text", "note": "default code text on the code surface" },
21
- { "fg": "--code-muted", "bg": "--code-bg", "required": 4.5, "kind": "text", "note": "code comments on the code surface - read as prose, so full text contrast" },
22
- { "fg": "--code-accent", "bg": "--code-bg", "required": 4.5, "kind": "text", "note": "keyword / accent token on the code surface" },
23
- { "fg": "--code-blue", "bg": "--code-bg", "required": 4.5, "kind": "text", "note": "blue syntax token on the code surface" },
24
- { "fg": "--code-green", "bg": "--code-bg", "required": 4.5, "kind": "text", "note": "green syntax token (strings) on the code surface" }
2
+ {
3
+ "fg": "--text-primary",
4
+ "bg": "--paper",
5
+ "required": 4.5,
6
+ "kind": "text",
7
+ "note": "primary body text on the page background"
8
+ },
9
+ {
10
+ "fg": "--text-secondary",
11
+ "bg": "--paper",
12
+ "required": 4.5,
13
+ "kind": "text",
14
+ "note": "secondary body text on the page background"
15
+ },
16
+ {
17
+ "fg": "--text-tertiary",
18
+ "bg": "--paper",
19
+ "required": 3,
20
+ "kind": "large-text",
21
+ "warnIfBelow": 4.5,
22
+ "note": "tertiary text - legal at large sizes; warn if used as body"
23
+ },
24
+ {
25
+ "fg": "--text-muted",
26
+ "bg": "--paper",
27
+ "required": 4.5,
28
+ "kind": "text",
29
+ "note": "muted UI text on the page background"
30
+ },
31
+ {
32
+ "fg": "--ink",
33
+ "bg": "--paper",
34
+ "required": 4.5,
35
+ "kind": "text",
36
+ "note": "native ink on native paper"
37
+ },
38
+ {
39
+ "fg": "--ink-soft",
40
+ "bg": "--paper",
41
+ "required": 4.5,
42
+ "kind": "text",
43
+ "note": "soft ink on native paper"
44
+ },
45
+ {
46
+ "fg": "--ink-faint",
47
+ "bg": "--paper",
48
+ "required": 3,
49
+ "kind": "large-text",
50
+ "warnIfBelow": 4.5,
51
+ "note": "faint ink - large/UI only; warn if used as body"
52
+ },
53
+ {
54
+ "fg": "--text-link",
55
+ "bg": "--paper",
56
+ "required": 4.5,
57
+ "kind": "text",
58
+ "note": "inline link color on the page background"
59
+ },
60
+ {
61
+ "fg": "--text-inverse",
62
+ "bg": "--action-primary-default",
63
+ "required": 4.5,
64
+ "kind": "text",
65
+ "note": "button label on the primary action surface"
66
+ },
67
+ {
68
+ "fg": "--text-inverse",
69
+ "bg": "--ai",
70
+ "required": 4.5,
71
+ "kind": "text",
72
+ "note": "inverse text on the AI/brand accent surface"
73
+ },
74
+ {
75
+ "fg": "--success-text",
76
+ "bg": "--success-subtle",
77
+ "required": 4.5,
78
+ "kind": "text",
79
+ "note": "success message text on its subtle background"
80
+ },
81
+ {
82
+ "fg": "--warning-text",
83
+ "bg": "--warning-subtle",
84
+ "required": 4.5,
85
+ "kind": "text",
86
+ "note": "warning message text on its subtle background"
87
+ },
88
+ {
89
+ "fg": "--error-text",
90
+ "bg": "--error-subtle",
91
+ "required": 4.5,
92
+ "kind": "text",
93
+ "note": "error message text on its subtle background"
94
+ },
95
+ {
96
+ "fg": "--info-text",
97
+ "bg": "--info-subtle",
98
+ "required": 4.5,
99
+ "kind": "text",
100
+ "note": "info message text on its subtle background"
101
+ },
102
+ {
103
+ "fg": "--border-default",
104
+ "bg": "--paper",
105
+ "required": 3,
106
+ "kind": "non-text",
107
+ "decorative": true,
108
+ "note": "default border against the page background - decorative; WCAG 2.2 SC 1.4.11 applies only to graphical objects essential for understanding content"
109
+ },
110
+ {
111
+ "fg": "--border-focus",
112
+ "bg": "--paper",
113
+ "required": 3,
114
+ "kind": "non-text",
115
+ "note": "focus ring against the page background"
116
+ },
117
+ {
118
+ "fg": "--shu",
119
+ "bg": "--paper",
120
+ "required": 3,
121
+ "kind": "non-text",
122
+ "note": "seal accent - must read as a UI mark on paper"
123
+ },
124
+ {
125
+ "fg": "--code-ink",
126
+ "bg": "--code-bg",
127
+ "required": 4.5,
128
+ "kind": "text",
129
+ "note": "default code text on the code surface"
130
+ },
131
+ {
132
+ "fg": "--code-muted",
133
+ "bg": "--code-bg",
134
+ "required": 4.5,
135
+ "kind": "text",
136
+ "note": "code comments on the code surface - read as prose, so full text contrast"
137
+ },
138
+ {
139
+ "fg": "--code-accent",
140
+ "bg": "--code-bg",
141
+ "required": 4.5,
142
+ "kind": "text",
143
+ "note": "keyword / accent token on the code surface"
144
+ },
145
+ {
146
+ "fg": "--code-blue",
147
+ "bg": "--code-bg",
148
+ "required": 4.5,
149
+ "kind": "text",
150
+ "note": "blue syntax token on the code surface"
151
+ },
152
+ {
153
+ "fg": "--code-green",
154
+ "bg": "--code-bg",
155
+ "required": 4.5,
156
+ "kind": "text",
157
+ "note": "green syntax token (strings) on the code surface"
158
+ },
159
+ {
160
+ "fg": "--page-hero-ink",
161
+ "bg": "--page-hero-bg",
162
+ "required": 4.5,
163
+ "kind": "text",
164
+ "note": "text on the hero page surface - graded only when the theme declares the slot"
165
+ },
166
+ {
167
+ "fg": "--page-band-ink",
168
+ "bg": "--page-band-bg",
169
+ "required": 4.5,
170
+ "kind": "text",
171
+ "note": "text on the band page surface - graded only when the theme declares the slot"
172
+ }
25
173
  ]
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================================
3
+ // fix-theme.cjs
4
+ // ============================================================================
5
+ // Rewrite a theme's DEPRECATED token declarations to their replacements.
6
+ //
7
+ // WHY THIS EXISTS
8
+ // Until now the tooling could only *say* a token was deprecated. A consumer
9
+ // then had to find it, look up what replaced it, and edit by hand — for a
10
+ // rename that a machine can do exactly. `scripts/theme-contract.json` already
11
+ // carries the `deprecated` map (old token → replacedBy / since / removeIn /
12
+ // note); this turns that map into an offer: here is what changed, here is the
13
+ // corrected file, say the word and it is yours.
14
+ //
15
+ // WHAT IT WILL NOT DO
16
+ // * It never writes to disk. It returns text. The caller decides — same
17
+ // contract as themeFromTokens(), for the same reason: a tool that edits
18
+ // a consumer's file as a side effect of being asked a question is a tool
19
+ // nobody can trust in a pipeline.
20
+ // * It never changes a VALUE. Only the property name on the left of the
21
+ // colon moves. A rename cannot alter how the theme looks.
22
+ // * It never duplicates. If a block already declares the replacement, the
23
+ // old line is left exactly where it is and the collision is reported,
24
+ // because merging two values is a judgement call, not a rewrite.
25
+ //
26
+ // FORMATTING
27
+ // Line-based on purpose. A CSS parser would round-trip the file through an
28
+ // AST and quietly restyle whitespace, comment placement and declaration
29
+ // order — a diff full of noise around the one line that mattered. Here the
30
+ // bytes either side of the property name survive untouched.
31
+ //
32
+ // Usage (library):
33
+ // const { fixTheme } = require('css-is-awesome/scripts/fix-theme.cjs');
34
+ // const { css, changes, unchanged } = fixTheme({ css: themeText });
35
+ // ============================================================================
36
+ 'use strict';
37
+
38
+ const fs = require('fs');
39
+ const path = require('path');
40
+
41
+ const CONTRACT_PATH = path.join(__dirname, 'theme-contract.json');
42
+
43
+ /** Load the token contract, or throw something a human can act on. */
44
+ function loadContract(ciaRoot) {
45
+ const p = ciaRoot ? path.join(ciaRoot, 'scripts', 'theme-contract.json') : CONTRACT_PATH;
46
+ let raw;
47
+ try {
48
+ raw = fs.readFileSync(p, 'utf8');
49
+ } catch (err) {
50
+ throw new Error(`fix-theme: could not read the token contract at ${p} — ${err.message}`);
51
+ }
52
+ try {
53
+ return JSON.parse(raw);
54
+ } catch (err) {
55
+ throw new Error(`fix-theme: the token contract at ${p} is not valid JSON — ${err.message}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The deprecations as a flat, serialisable list — the same data `get_token`
61
+ * and the validator report, so all three can never disagree.
62
+ */
63
+ function deprecations(opts) {
64
+ const contract = (opts && opts.contract) || loadContract(opts && opts.ciaRoot);
65
+ const map = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
66
+ return Object.entries(map).map(([token, d]) => ({
67
+ token,
68
+ replacedBy: (d && d.replacedBy) || null,
69
+ since: (d && d.since) || null,
70
+ removeIn: (d && d.removeIn) || null,
71
+ note: (d && d.note) || null,
72
+ }));
73
+ }
74
+
75
+ // A custom-property declaration: leading whitespace, the name, optional
76
+ // whitespace, the colon. Everything from the colon rightwards is untouched.
77
+ const DECL = /^(\s*)(--[A-Za-z0-9_-]+)(\s*):/;
78
+
79
+ // Strip /* … */ so a commented-out declaration is never counted as declared
80
+ // nor rewritten. Length is preserved so line numbers stay honest.
81
+ function blankComments(text) {
82
+ return text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
83
+ }
84
+
85
+ /**
86
+ * Rewrite deprecated declarations.
87
+ *
88
+ * @param {object} o
89
+ * @param {string} o.css theme CSS (compiled, not .scss source)
90
+ * @param {object} [o.contract] pre-loaded contract (tests / callers with one)
91
+ * @param {string} [o.ciaRoot] resolve the contract from another install
92
+ * @returns {{css:string, changes:Array, unchanged:boolean, deprecatedFound:string[]}}
93
+ * changes[] entries are `{ line, from, to, reason, kind }` where kind is
94
+ * 'rewrite' (applied) or 'conflict' (left alone, and why).
95
+ */
96
+ function fixTheme(o) {
97
+ const opts = o || {};
98
+ if (typeof opts.css !== 'string') {
99
+ throw new Error('fix-theme: css is required and must be a string (compiled CSS, not .scss source)');
100
+ }
101
+ const contract = opts.contract || loadContract(opts.ciaRoot);
102
+ const map = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
103
+
104
+ const lines = opts.css.split('\n');
105
+ const scan = blankComments(opts.css).split('\n');
106
+
107
+ // ── Pass 1: which tokens does each brace-scope already declare? ──────────
108
+ // Scope id changes on every `{`, so two theme blocks in one bundled file
109
+ // are judged independently — a collision in `[data-theme="a"]` says nothing
110
+ // about `[data-theme="b"]`.
111
+ const scopeOf = new Array(lines.length).fill(0);
112
+ const declaredIn = new Map(); // scopeId -> Set<token>
113
+ {
114
+ let depth = 0;
115
+ const stack = [0];
116
+ let next = 1;
117
+ for (let i = 0; i < scan.length; i++) {
118
+ const line = scan[i];
119
+ scopeOf[i] = stack[stack.length - 1];
120
+ const m = DECL.exec(line);
121
+ if (m) {
122
+ const id = stack[stack.length - 1];
123
+ if (!declaredIn.has(id)) declaredIn.set(id, new Set());
124
+ declaredIn.get(id).add(m[2]);
125
+ }
126
+ for (const ch of line) {
127
+ if (ch === '{') { stack.push(next++); depth++; }
128
+ else if (ch === '}') { if (stack.length > 1) stack.pop(); depth--; }
129
+ }
130
+ }
131
+ }
132
+
133
+ // ── Pass 2: rewrite, or explain why not ─────────────────────────────────
134
+ const changes = [];
135
+ const found = new Set();
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const m = DECL.exec(scan[i]);
138
+ if (!m) continue;
139
+ const token = m[2];
140
+ const dep = map[token];
141
+ if (!dep || !dep.replacedBy) continue;
142
+
143
+ found.add(token);
144
+ const replacement = dep.replacedBy;
145
+ const already = declaredIn.get(scopeOf[i]);
146
+
147
+ if (already && already.has(replacement)) {
148
+ changes.push({
149
+ line: i + 1,
150
+ from: token,
151
+ to: replacement,
152
+ kind: 'conflict',
153
+ reason:
154
+ `${replacement} is already declared in this block, so renaming ${token} would ` +
155
+ `produce two declarations of the same property and silently pick the last one. ` +
156
+ `Left untouched — decide which value you want and delete the other.`,
157
+ });
158
+ continue;
159
+ }
160
+
161
+ // Rename only the property. Indentation, spacing before the colon, the
162
+ // value, the semicolon and any trailing comment all survive byte-exact.
163
+ lines[i] = lines[i].replace(DECL, (_full, indent, _name, gap) => `${indent}${replacement}${gap}:`);
164
+ changes.push({
165
+ line: i + 1,
166
+ from: token,
167
+ to: replacement,
168
+ kind: 'rewrite',
169
+ reason:
170
+ `${token} is deprecated since contract ${dep.since || '?'} and is removed in ` +
171
+ `${dep.removeIn || 'a future major'}. ${dep.note || ''}`.trim(),
172
+ });
173
+ }
174
+
175
+ const rewrites = changes.filter((c) => c.kind === 'rewrite');
176
+ return {
177
+ css: rewrites.length ? lines.join('\n') : opts.css,
178
+ changes,
179
+ unchanged: rewrites.length === 0,
180
+ deprecatedFound: [...found].sort(),
181
+ };
182
+ }
183
+
184
+ module.exports = { fixTheme, deprecations, loadContract };
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.2",
3
- "description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures \u2014 the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR. Contract 1.2 (2026-09-18): `features` groups every optional token by the capability it enables, so a validator or installer can say \"missing the tokens for print\" instead of listing all optional tokens.",
2
+ "version": "1.3",
3
+ "description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures — the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.3 (2026-09-24): the page-surfaces feature adds eight optional tokens for the hero and band surfaces, and --background-hero is deprecated in favour of --page-hero-bg (kept valid, removed no earlier than contract 2).",
4
4
  "required": [
5
5
  "--action-primary-active",
6
6
  "--action-primary-default",
@@ -151,6 +151,14 @@
151
151
  "--logo-monochrome",
152
152
  "--logo-wordmark",
153
153
  "--modal-radius",
154
+ "--page-band-bg",
155
+ "--page-band-image",
156
+ "--page-band-ink",
157
+ "--page-band-scrim",
158
+ "--page-hero-bg",
159
+ "--page-hero-image",
160
+ "--page-hero-ink",
161
+ "--page-hero-scrim",
154
162
  "--print-ink",
155
163
  "--print-line",
156
164
  "--print-muted",
@@ -181,7 +189,7 @@
181
189
  ]
182
190
  },
183
191
  "spacing-aliases": {
184
- "enables": "T-shirt spacing names (2xs\u2013xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases.",
192
+ "enables": "T-shirt spacing names (2xs–xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases.",
185
193
  "tokens": [
186
194
  "--space-2xs",
187
195
  "--space-xs",
@@ -236,7 +244,6 @@
236
244
  "enables": "Extra background layers (elevated, hero, overlay, scrim) for themes that want more than the required surface set; each falls back to a required surface.",
237
245
  "tokens": [
238
246
  "--background-elevated",
239
- "--background-hero",
240
247
  "--background-overlay",
241
248
  "--background-scrim"
242
249
  ]
@@ -264,6 +271,28 @@
264
271
  "tokens": [
265
272
  "--touch-target-min"
266
273
  ]
274
+ },
275
+ "page-surfaces": {
276
+ "enables": "Two page-level surfaces a theme owns outright: a hero for the front page and a band for section stripes and footers. Each carries a background colour, an optional gradient or url() image, an ink colour for text on it, and a scrim that keeps that text legible over an image. Applied only via cia.surface() or [data-surface] - declaring them changes nothing on its own.",
277
+ "tokens": [
278
+ "--page-hero-bg",
279
+ "--page-hero-image",
280
+ "--page-hero-ink",
281
+ "--page-hero-scrim",
282
+ "--page-band-bg",
283
+ "--page-band-image",
284
+ "--page-band-ink",
285
+ "--page-band-scrim",
286
+ "--background-hero"
287
+ ]
288
+ }
289
+ },
290
+ "deprecated": {
291
+ "--background-hero": {
292
+ "replacedBy": "--page-hero-bg",
293
+ "since": "1.3",
294
+ "removeIn": "2",
295
+ "note": "Was never read by the library. --page-hero-bg falls back through it, so an existing declaration keeps working untouched."
267
296
  }
268
297
  }
269
298
  }
@@ -354,7 +354,24 @@ function validateTokenSet(declared, contract) {
354
354
  const f = featureOf[t] || 'other';
355
355
  (optionalMissingByFeature[f] = optionalMissingByFeature[f] || []).push(t);
356
356
  }
357
- return { ok: missing.length === 0, missing, optionalMissing, optionalMissingByFeature, declaredCount: declared.size };
357
+ // Contract 1.3+: a token this theme declares that has since been superseded.
358
+ // Reported separately from the optional-token info line — "you declared
359
+ // something that still works but has a better name now" is a different
360
+ // message from "you left a default to the library", and it comes with an
361
+ // action: `cia fix-theme`.
362
+ const deprecatedMap = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
363
+ const deprecatedUsed = [];
364
+ for (const [token, def] of Object.entries(deprecatedMap)) {
365
+ if (declared.has(token)) {
366
+ deprecatedUsed.push({
367
+ token,
368
+ replacedBy: (def && def.replacedBy) || null,
369
+ since: (def && def.since) || null,
370
+ removeIn: (def && def.removeIn) || null,
371
+ });
372
+ }
373
+ }
374
+ return { ok: missing.length === 0, missing, optionalMissing, optionalMissingByFeature, deprecatedUsed, declaredCount: declared.size };
358
375
  }
359
376
 
360
377
  // -----------------------------------------------------------
@@ -380,6 +397,7 @@ function validateText(text, contract, options) {
380
397
  missing: [],
381
398
  optionalMissing: [],
382
399
  optionalMissingByFeature: {},
400
+ deprecatedUsed: [],
383
401
  themes: null,
384
402
  a11y: null,
385
403
  error: null,
@@ -403,6 +421,7 @@ function validateText(text, contract, options) {
403
421
  missing: v.missing,
404
422
  optionalMissing: v.optionalMissing,
405
423
  optionalMissingByFeature: v.optionalMissingByFeature,
424
+ deprecatedUsed: v.deprecatedUsed,
406
425
  a11y: null,
407
426
  };
408
427
  if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
@@ -432,6 +451,7 @@ function validateText(text, contract, options) {
432
451
  result.missing = v.missing;
433
452
  result.optionalMissing = v.optionalMissing;
434
453
  result.optionalMissingByFeature = v.optionalMissingByFeature;
454
+ result.deprecatedUsed = v.deprecatedUsed;
435
455
  result.ok = v.ok;
436
456
  if (wantA11y) {
437
457
  const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
@@ -471,6 +491,21 @@ function relForDisplay(p) {
471
491
 
472
492
  // Optional tokens a theme leaves to the library default. Info only — shown as
473
493
  // a count, or listed with --show-optional. Never affects the exit code.
494
+ // A token the theme declares that has been superseded. Never a failure — the
495
+ // old value still works — but it names the replacement and the way to apply it.
496
+ function deprecationInfo(deprecatedUsed, indent) {
497
+ const list = Array.isArray(deprecatedUsed) ? deprecatedUsed : [];
498
+ if (!list.length) return;
499
+ for (const d of list) {
500
+ console.log(
501
+ `${indent}${yellow('~')} ${d.token} is deprecated` +
502
+ (d.since ? ` (since contract ${d.since}` + (d.removeIn ? `, removed in ${d.removeIn}` : '') + ')' : '') +
503
+ (d.replacedBy ? ` — use ${d.replacedBy}` : '')
504
+ );
505
+ console.log(`${indent} ${dim('your value still works; `npx cia fix-theme <file>` renames it for you')}`);
506
+ }
507
+ }
508
+
474
509
  function optionalInfo(optionalMissing, byFeature, indent) {
475
510
  const list = Array.isArray(optionalMissing) ? optionalMissing : [];
476
511
  if (!list.length) return;
@@ -506,6 +541,7 @@ function reportResult(result) {
506
541
  ` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
507
542
  );
508
543
  optionalInfo(t.optionalMissing, t.optionalMissingByFeature, ' ');
544
+ deprecationInfo(t.deprecatedUsed, ' ');
509
545
  } else {
510
546
  const n = t.missing.length;
511
547
  console.log(
@@ -514,6 +550,7 @@ function reportResult(result) {
514
550
  for (const token of t.missing) {
515
551
  console.log(` ${red(token)}`);
516
552
  }
553
+ deprecationInfo(t.deprecatedUsed, ' ');
517
554
  }
518
555
  }
519
556
  return;
@@ -525,6 +562,7 @@ function reportResult(result) {
525
562
  `${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
526
563
  );
527
564
  optionalInfo(result.optionalMissing, result.optionalMissingByFeature, ' ');
565
+ deprecationInfo(result.deprecatedUsed, ' ');
528
566
  return;
529
567
  }
530
568
 
@@ -535,6 +573,7 @@ function reportResult(result) {
535
573
  for (const token of result.missing) {
536
574
  console.log(` ${red(token)}`);
537
575
  }
576
+ deprecationInfo(result.deprecatedUsed, ' ');
538
577
  }
539
578
 
540
579
  // -----------------------------------------------------------