jig-ui 0.17.1 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.3
4
+
5
+ Three defects found building jig-site's header with the full loop.
6
+
7
+ ### Fixed
8
+
9
+ - **The gate no longer asks `make` for a critique's verdicts.** A critique
10
+ counted as touched when any file in its folder changed, so a make round that
11
+ saved its probe there (`jig probe --save`) was held to that critique's
12
+ completeness. A critique written before a release that added rules has no
13
+ verdicts for them, and only `critique` may write verdicts, so the round was
14
+ blocked three times on something it was forbidden to do. A critique is now
15
+ touched only when its verdict files change.
16
+ - **`jig verdicts` accepts, and asks for, verdicts on hybrid rules with a
17
+ pass.** 0.17 gave ten code-pass rules (A-135 to A-140, A-143, A-144, A-146,
18
+ G-145) detectors and made them hybrid. Each kept its `pass: code`, because a
19
+ detector catches only some shapes of the failure, and the code arm judged
20
+ them as that field says. `verdicts` knew only judgment and mechanical ids,
21
+ so every verdict on them came back "not a rule", and a critique that did
22
+ what the rules said could not pass. A hybrid rule with a pass is now judged
23
+ in that pass, like a judgment rule: the code arm has 84 rules, not 74.
24
+ - **`jig update` refreshes a project's mode files where `init` wrote them.**
25
+ `init` has written `mode.<mode>.css` beside the brand file (for example
26
+ `src/styles/jig/`) since the token layer moved into the project's styles,
27
+ but `update` still looked for them under the pre-0.6 `.jig/tokens/`. It
28
+ found none, so every release left the old tokens in place, and nothing
29
+ said so; only `jig init --yes` refreshed them. `update` now refreshes each
30
+ untouched copy wherever `init` recorded writing it.
31
+
32
+ ## 0.17.2
33
+
34
+ Four defects found by building one page with the full loop.
35
+
36
+ ### Fixed
37
+
38
+ - **Every mode declares every token name.** `editorial` had no row heights or
39
+ `--font-numeric`, and `product` and `operator` had no ambient durations. A
40
+ project with two modes shares one alias block, so those aliases resolved to
41
+ nothing on the routes of the mode that lacked them; a real site's editorial
42
+ pages had three undefined utilities, and the probe failed them. Each mode now
43
+ states a value for each name (ambient motion is `0s` outside `editorial`),
44
+ and `check-tokens` Rule 14 fails a mode file that declares a name another
45
+ does not.
46
+ - **The gate no longer marks a critique as touched by writing its own lock.**
47
+ 0.17.1 judged only critiques touched this session, but a critique session
48
+ wrote a lock into every critique folder, and the next stop read that write as
49
+ a touch. Locks are now written only for the critiques in play, and the lock
50
+ file is ignored when dating a folder.
51
+ - **The probe does not take an in-page section list for the site menu.** A
52
+ `<summary>` in a `<nav>` inside `<main>`, `<article>` or `<aside>` is page
53
+ navigation; judging it as a menu failed a rule page's "More in this section"
54
+ disclosure for not behaving like one.
55
+ - **Every example fits 320 by 260.** The examples README promised about
56
+ 320 by 180; one figure was 363px wide and four were taller. The bound is now
57
+ stated as measured, the two outliers are trimmed, and a test renders all 286
58
+ figures in Chrome and fails any wider or taller.
59
+
3
60
  ## 0.17.1
4
61
 
5
62
  ### Fixed
package/dist/index.js CHANGED
@@ -1014,15 +1014,17 @@ function updateInitFiles(opts) {
1014
1014
  let initChanged = false;
1015
1015
  const writer = createWriter(opts.projectRoot, initFiles);
1016
1016
  for (const file of bundleFiles(opts.packageRoot, "tokens")) {
1017
- const key = relKey(".jig", "tokens", file);
1018
- if (!(key in initManifest.files)) continue;
1019
- if (isInitFileModified(opts.projectRoot, key, initManifest)) {
1020
- skipped.push(key);
1021
- continue;
1017
+ if (!file.startsWith("mode.")) continue;
1018
+ const keys = Object.keys(initManifest.files).filter((k) => k === file || k.endsWith(`/${file}`));
1019
+ for (const key of keys) {
1020
+ if (isInitFileModified(opts.projectRoot, key, initManifest)) {
1021
+ skipped.push(key);
1022
+ continue;
1023
+ }
1024
+ const content = vendorHeader(file, opts.version, "css", null) + readFileSync7(join10(tokensDir, file), "utf8");
1025
+ updated.push(writer.write(key, content));
1026
+ initChanged = true;
1022
1027
  }
1023
- const content = vendorHeader(file, opts.version, "css", null) + readFileSync7(join10(tokensDir, file), "utf8");
1024
- updated.push(writer.write(key, content));
1025
- initChanged = true;
1026
1028
  }
1027
1029
  Object.assign(initFiles, writer.files);
1028
1030
  if (initChanged) writeInitManifest(opts.projectRoot, { ...initManifest, version: opts.version, files: initFiles });
@@ -5763,7 +5765,12 @@ var PROBE_SCRIPT = `(async () => {
5763
5765
  const navAtRest = navLinks();
5764
5766
  // The menu control: inside the header or navigation, or named as the menu.
5765
5767
  // Not any disclosure \u2014 an FAQ <summary> is not a menu.
5766
- const inChrome = (b) => !!b.closest('header, nav, [role=navigation], [role=banner]');
5768
+ // The site's navigation, not a page's own: a <nav> inside <main>, an
5769
+ // <article> or an <aside> is in-page navigation (a section list, a table of
5770
+ // contents), and its <summary> is a disclosure the reader opens, not the menu.
5771
+ // A rule page's "More in this section" list was judged a broken menu this way.
5772
+ const inChrome = (b) => !!b.closest('header, [role=banner]') ||
5773
+ (!!b.closest('nav, [role=navigation]') && !b.closest('main, article, aside, [role=main]'));
5767
5774
  const named = (b) => /\b(menu|navigation)\b/i.test((b.getAttribute('aria-label') || '') + ' ' + b.textContent);
5768
5775
  const toggle = [...document.querySelectorAll('button, summary, [role=button]')].find((b) => vis(b) &&
5769
5776
  (named(b) || (inChrome(b) && (b.tagName === 'SUMMARY' || b.hasAttribute('aria-expanded') || b.hasAttribute('aria-controls')))));
@@ -5928,7 +5935,7 @@ async function runProbe(opts) {
5928
5935
  fail(new Error(`${chrome} exited with code ${code} before it was ready.`));
5929
5936
  });
5930
5937
  });
5931
- return await evaluate(endpoint, opts.url, opts.width, opts.height ?? 900, timeoutMs);
5938
+ return await evaluate(endpoint, opts.url, opts.width, opts.height ?? 900, timeoutMs, opts.expression ?? PROBE_SCRIPT);
5932
5939
  } finally {
5933
5940
  const exited = child.exitCode !== null ? Promise.resolve() : new Promise((done) => child.once("exit", () => done()));
5934
5941
  child.kill();
@@ -5939,7 +5946,7 @@ async function runProbe(opts) {
5939
5946
  }
5940
5947
  }
5941
5948
  }
5942
- async function evaluate(endpoint, url, width, height, timeoutMs) {
5949
+ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
5943
5950
  const socket = new WebSocket(endpoint);
5944
5951
  let nextId = 1;
5945
5952
  const pending = /* @__PURE__ */ new Map();
@@ -5983,7 +5990,7 @@ async function evaluate(endpoint, url, width, height, timeoutMs) {
5983
5990
  ]);
5984
5991
  }
5985
5992
  await new Promise((done) => setTimeout(done, 250));
5986
- const result = await send("Runtime.evaluate", { expression: PROBE_SCRIPT, awaitPromise: true, returnByValue: true }, session);
5993
+ const result = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }, session);
5987
5994
  const value = result.result?.value;
5988
5995
  if (typeof value !== "string") {
5989
5996
  const description = result.exceptionDetails?.text;
@@ -6521,7 +6528,7 @@ function verifyVerdicts(opts) {
6521
6528
  const root = opts.packageRoot ?? assetRoot();
6522
6529
  const errors = [];
6523
6530
  const index = JSON.parse(readFileSync29(join36(root, "rules.index.json"), "utf8"));
6524
- const judgment = index.filter((r) => r.bucket === "judgment");
6531
+ const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
6525
6532
  const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
6526
6533
  const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
6527
6534
  const passOf = new Map([
@@ -6638,11 +6645,13 @@ function sessionStart(transcriptPath) {
6638
6645
  }
6639
6646
  return void 0;
6640
6647
  }
6641
- function newestMtime(dir) {
6648
+ function verdictsMtime(dir) {
6642
6649
  let newest = 0;
6643
- for (const name of readdirSync15(dir, { withFileTypes: true })) {
6644
- const path = join37(dir, name.name);
6645
- newest = Math.max(newest, name.isDirectory() ? newestMtime(path) : statSync4(path).mtimeMs);
6650
+ for (const name of VERDICT_FILES) {
6651
+ try {
6652
+ newest = Math.max(newest, statSync4(join37(dir, name)).mtimeMs);
6653
+ } catch {
6654
+ }
6646
6655
  }
6647
6656
  return newest;
6648
6657
  }
@@ -6653,7 +6662,7 @@ function surfacesInPlay(root, command, transcriptPath) {
6653
6662
  const start = sessionStart(transcriptPath);
6654
6663
  if (start === void 0) return all;
6655
6664
  const current = command === "critique" ? /^\s*surface\s*:\s*(.+)$/im.exec(newestSpec(root)?.body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim().replace(/^["']|["']$/g, "") : void 0;
6656
- return all.filter((s) => s === current || newestMtime(join37(critiqueDir, s)) >= start - 1e3);
6665
+ return all.filter((s) => s === current || verdictsMtime(join37(critiqueDir, s)) >= start - 1e3);
6657
6666
  }
6658
6667
  function commandProblems(root, command) {
6659
6668
  const problems = [];
@@ -6743,7 +6752,7 @@ function gate(opts) {
6743
6752
  );
6744
6753
  }
6745
6754
  }
6746
- problems.push(...verdictGuard(root, command));
6755
+ problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path)));
6747
6756
  const critiqueDir = join37(root, ".jig", "critique");
6748
6757
  if (existsSync24(critiqueDir)) {
6749
6758
  for (const surface of surfacesInPlay(root, command, opts.input.transcript_path)) {
@@ -6806,7 +6815,7 @@ ${readFileSync30(join37(dir, f), "utf8")}`;
6806
6815
  });
6807
6816
  return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
6808
6817
  }
6809
- function verdictGuard(root, command) {
6818
+ function verdictGuard(root, command, inPlay) {
6810
6819
  const critiqueDir = join37(root, ".jig", "critique");
6811
6820
  if (!existsSync24(critiqueDir)) return [];
6812
6821
  const problems = [];
@@ -6816,7 +6825,7 @@ function verdictGuard(root, command) {
6816
6825
  const now = verdictChecksum(dir);
6817
6826
  if (!now) continue;
6818
6827
  const lockPath = join37(dir, LOCK);
6819
- if (command === "critique") {
6828
+ if (command === "critique" && (!inPlay || inPlay.includes(surface))) {
6820
6829
  try {
6821
6830
  writeFileSync7(lockPath, JSON.stringify({ checksum: now }) + "\n", "utf8");
6822
6831
  } catch {
@@ -1,6 +1,6 @@
1
1
  <!-- I-82 · Uneven text length across parallel elements -->
2
2
  <figure data-example="dont">
3
- <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-wrap:nowrap;align-items:stretch"><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Search</b><div style="color:#5c5c5c;margin-top:4px;">Words.</div></div><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Alerts</b><div style="color:#5c5c5c;margin-top:4px;">Saved questions that run every five minutes and message you when the answer changes.</div></div><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Sharing</b><div style="color:#5c5c5c;margin-top:4px;">A link.</div></div></div></div>
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-wrap:nowrap;align-items:stretch"><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Search</b><div style="color:#5c5c5c;margin-top:4px;">Words.</div></div><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Alerts</b><div style="color:#5c5c5c;margin-top:4px;">Saved questions that run every five minutes and tell you on change.</div></div><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Sharing</b><div style="color:#5c5c5c;margin-top:4px;">A link.</div></div></div></div>
4
4
  </figure>
5
5
  <figure data-example="do">
6
6
  <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-wrap:nowrap;align-items:stretch"><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Search</b><div style="color:#5c5c5c;margin-top:4px;">Ask in plain English.</div></div><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Alerts</b><div style="color:#5c5c5c;margin-top:4px;">Hear when an answer changes.</div></div><div style="background:#ffffff;border:1px solid #d6d6d6;border-radius:8px;padding:12px;flex:1;min-width:0"><b>Sharing</b><div style="color:#5c5c5c;margin-top:4px;">Send any result by link.</div></div></div></div>
@@ -3,5 +3,5 @@
3
3
  <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:0px;"><div style="font-size:12px;font-weight:600;margin-bottom:4px;">API key</div><div style="background:#ffffff;border:1px solid #a3a3a3;border-radius:6px;padding:7px 9px;font-size:13px;min-height:18px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">tw_live_8f2c91a7d44e0b6c3e</div></div><div style="font-size:11px;color:#5c5c5c;margin-top:8px">On screen, in a screenshot, in a screen share.</div></div>
4
4
  </figure>
5
5
  <figure data-example="do">
6
- <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:0px;"><div style="font-size:12px;font-weight:600;margin-bottom:4px;">API key</div><div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-wrap:nowrap"><div style="background:#ffffff;border:1px solid #a3a3a3;border-radius:6px;padding:7px 9px;font-size:13px;min-height:18px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;flex:1">tw_live_••••••••••0b6c3e</div><span style="display:inline-block;align-self:flex-start;background:#ffffff;color:#1f1f1f;border:1px solid #d6d6d6;border-radius:6px;padding:6px 11px;font-weight:600;font-size:12px;">Copy</span><span style="display:inline-block;align-self:flex-start;background:#ffffff;color:#1f1f1f;border:1px solid #d6d6d6;border-radius:6px;padding:6px 11px;font-weight:600;font-size:12px;">Reveal</span></div></div></div>
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:0px;"><div style="font-size:12px;font-weight:600;margin-bottom:4px;">API key</div><div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-wrap:nowrap"><div style="background:#ffffff;border:1px solid #a3a3a3;border-radius:6px;padding:7px 9px;font-size:13px;min-height:18px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;flex:1;min-width:0">tw_live_••••6c3e</div><span style="display:inline-block;align-self:flex-start;background:#ffffff;color:#1f1f1f;border:1px solid #d6d6d6;border-radius:6px;padding:6px 11px;font-weight:600;font-size:12px;">Copy</span><span style="display:inline-block;align-self:flex-start;background:#ffffff;color:#1f1f1f;border:1px solid #d6d6d6;border-radius:6px;padding:6px 11px;font-weight:600;font-size:12px;">Reveal</span></div></div></div>
7
7
  </figure>
@@ -22,8 +22,11 @@ for them. The example is the picture beside the name.
22
22
  - **Self-contained.** Styles are `style` attributes on the elements. No `<style>`,
23
23
  `<script>`, `<link>`, event handler, or external URL. A consumer renders a figure
24
24
  in a sandboxed frame or an inert box, and it has to look the same in both.
25
- - **Small.** A figure is drawn for a box about 320 by 180 pixels. It exaggerates
26
- just enough to be recognised, and no more.
25
+ - **Small, and bounded.** A figure is drawn 320 pixels wide and is at most 260
26
+ tall; most are 176. Frame each one at 320 by 260, or size the frame to the
27
+ figure's measured height. A figure that shows content running off an edge
28
+ clips it inside itself, so nothing ever spills past 320. It exaggerates just
29
+ enough to be recognised, and no more.
27
30
  - **Raw values are fine here.** An example has to show violet, a glow, a 44px
28
31
  radius. It is a specimen of the failure, not a component, so the token rule
29
32
  that governs a real page does not apply inside it.
@@ -41,4 +44,5 @@ Where even that is impossible, the figure shows the code, set as code.
41
44
 
42
45
  `packages/cli/test/examples.test.ts` fails when a rule has no example, when a file
43
46
  has other than one `dont` and one `do`, or when a figure carries anything that
44
- would not render the same in a sandbox.
47
+ would not render the same in a sandbox. It also renders every figure in a real
48
+ browser and fails any wider than 320 pixels or taller than 260.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
4
4
  "description": "A design system for coding agents. 143 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -194,24 +194,28 @@ mode states its own value, because density is the thing a mode *is*.
194
194
  | --- | --- | --- | --- |
195
195
  | `--size-control` | 48px | 40px | 32px |
196
196
  | `--size-control-sm` | 40px | 32px | 28px |
197
- | `--size-row` | — | 48px | 36px |
198
- | `--size-row-compact` | — | — | 32px |
197
+ | `--size-row` | 56px | 48px | 36px |
198
+ | `--size-row-compact` | 48px | 40px | 32px |
199
199
  | `--size-icon` | 20px | 18px | 16px |
200
200
  | `--duration-fast` | 150ms | 100ms | 75ms |
201
201
  | `--duration-base` | 250ms | 150ms | 100ms |
202
202
  | `--duration-slow` | 300ms | 200ms | 120ms |
203
- | `--duration-ambient-fast` | 3s | — | — |
204
- | `--duration-ambient-base` | 4.7s | — | — |
205
- | `--duration-ambient-slow` | 7.1s | — | — |
203
+ | `--duration-ambient-fast` | 3s | 0s | 0s |
204
+ | `--duration-ambient-base` | 4.7s | 0s | 0s |
205
+ | `--duration-ambient-slow` | 7.1s | 0s | 0s |
206
206
  | `--measure-prose` | 68ch | 60ch | 72ch |
207
207
  | `--ease-out` | cubic-bezier(0.16, 1, 0.3, 1) | cubic-bezier(0.16, 1, 0.3, 1) | cubic-bezier(0.2, 0, 0, 1) |
208
208
  | `--ease-in-out` | cubic-bezier(0.65, 0, 0.35, 1) | cubic-bezier(0.65, 0, 0.35, 1) | cubic-bezier(0.4, 0, 0.2, 1) |
209
209
 
210
- A `—` means the mode does not define that token: `editorial` has no row
211
- heights because it has no dense record views, and `product` selects a single
212
- row height rather than a compact variant. A pattern that needs one in those
213
- modes is using the wrong mode, or the mode file needs the token added
214
- deliberately.
210
+ **Every mode declares every token name.** A project with two modes shares one
211
+ alias block (`utilities.css`) between them, and an alias for a token one mode
212
+ never declares points at nothing on that mode's routes: on a real site, three
213
+ utilities for row height and tabular figures were undefined on every editorial
214
+ page. So each mode states a value for each name, even where the value is only
215
+ a fallback: `editorial` rows are sized to its controls, though it rarely shows
216
+ dense records, and ambient motion is `0s` outside `editorial` (`P-13`), so a
217
+ shared class resolves and nothing loops. `check-tokens` fails a mode file that
218
+ declares a name another does not.
215
219
 
216
220
  **The ambient periods are a chord, not a ladder.** The three interaction
217
221
  durations are a scale — fast, base, slow, pick by weight of change. The three
@@ -75,6 +75,7 @@
75
75
  --grid-margin: var(--spacing-xxl); --grid-margin-sm: var(--spacing-s); /* 80 → 16 */
76
76
 
77
77
  --size-control: 48px; --size-control-sm: 40px;
78
+ --size-row: 56px; --size-row-compact: 48px;
78
79
  --size-touch-target: 48px; --size-icon: 20px;
79
80
 
80
81
  --border-width-control: var(--border-width-hairline);
@@ -102,6 +103,10 @@
102
103
  --duration-ambient-fast: 3s;
103
104
  --duration-ambient-base: 4.7s;
104
105
  --duration-ambient-slow: 7.1s;
106
+
107
+ /* Every mode declares every token name, so one alias block (utilities.css)
108
+ resolves on every surface. Tabular figures are harmless in prose. */
109
+ --font-numeric: var(--font-numeric-features);
105
110
  }
106
111
 
107
112
  @media (prefers-reduced-motion: reduce) {
@@ -83,6 +83,13 @@
83
83
  have anything moving on it that they did not cause. */
84
84
 
85
85
  --font-numeric: var(--font-numeric-features);
86
+
87
+ /* Every mode declares every token name, so one alias block (utilities.css)
88
+ resolves on every surface. Ambient motion is editorial's (P-13); here it
89
+ is declared still, so a shared class resolves and nothing loops. */
90
+ --duration-ambient-fast: 0s;
91
+ --duration-ambient-base: 0s;
92
+ --duration-ambient-slow: 0s;
86
93
  }
87
94
 
88
95
  @media (prefers-reduced-motion: reduce) {
@@ -71,7 +71,7 @@
71
71
  --grid-gutter: var(--spacing-m); --grid-gutter-sm: var(--spacing-s); /* 24 → 16 */
72
72
  --grid-margin: var(--spacing-l); --grid-margin-sm: var(--spacing-s); /* 32 → 16 */
73
73
 
74
- --size-control: 40px; --size-control-sm: 32px; --size-row: 48px;
74
+ --size-control: 40px; --size-control-sm: 32px; --size-row: 48px; --size-row-compact: 40px;
75
75
  --size-touch-target: 48px; --size-icon: 18px;
76
76
 
77
77
  --border-width-control: var(--border-width-hairline);
@@ -85,6 +85,14 @@
85
85
  /* No ambient duration tokens here, deliberately. Ambient motion is
86
86
  editorial only (P-13) — a surface someone works in all day must not
87
87
  have anything moving on it that they did not cause. */
88
+
89
+ /* Every mode declares every token name, so one alias block (utilities.css)
90
+ resolves on every surface. Ambient motion is editorial's (P-13); here it
91
+ is declared still, so a shared class resolves and nothing loops. */
92
+ --font-numeric: var(--font-numeric-features);
93
+ --duration-ambient-fast: 0s;
94
+ --duration-ambient-base: 0s;
95
+ --duration-ambient-slow: 0s;
88
96
  }
89
97
 
90
98
  @media (prefers-reduced-motion: reduce) {