jig-ui 0.17.0 → 0.17.2

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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.2
4
+
5
+ Four defects found by building one page with the full loop.
6
+
7
+ ### Fixed
8
+
9
+ - **Every mode declares every token name.** `editorial` had no row heights or
10
+ `--font-numeric`, and `product` and `operator` had no ambient durations. A
11
+ project with two modes shares one alias block, so those aliases resolved to
12
+ nothing on the routes of the mode that lacked them; a real site's editorial
13
+ pages had three undefined utilities, and the probe failed them. Each mode now
14
+ states a value for each name (ambient motion is `0s` outside `editorial`),
15
+ and `check-tokens` Rule 14 fails a mode file that declares a name another
16
+ does not.
17
+ - **The gate no longer marks a critique as touched by writing its own lock.**
18
+ 0.17.1 judged only critiques touched this session, but a critique session
19
+ wrote a lock into every critique folder, and the next stop read that write as
20
+ a touch. Locks are now written only for the critiques in play, and the lock
21
+ file is ignored when dating a folder.
22
+ - **The probe does not take an in-page section list for the site menu.** A
23
+ `<summary>` in a `<nav>` inside `<main>`, `<article>` or `<aside>` is page
24
+ navigation; judging it as a menu failed a rule page's "More in this section"
25
+ disclosure for not behaving like one.
26
+ - **Every example fits 320 by 260.** The examples README promised about
27
+ 320 by 180; one figure was 363px wide and four were taller. The bound is now
28
+ stated as measured, the two outliers are trimmed, and a test renders all 286
29
+ figures in Chrome and fails any wider or taller.
30
+
31
+ ## 0.17.1
32
+
33
+ ### Fixed
34
+
35
+ - **The gate judges the critiques a session touched, not every critique.**
36
+ A spec for one page could not finish because another page's critique
37
+ predated 0.17.0, whose new rules that critique had never judged, and a
38
+ record the project had set aside still failed for screenshots it no longer
39
+ had. The gate now checks a critique with a file changed since the session
40
+ began, and the current spec's surface after `critique`. A folder whose name
41
+ starts with `_` is set aside and never judged. With no transcript, as when
42
+ the gate is run by hand, it checks every critique as before.
43
+
3
44
  ## 0.17.0
4
45
 
5
46
  Thirteen more tells of a generated page, ten of them caught by `check`, and a
package/dist/index.js CHANGED
@@ -5763,7 +5763,12 @@ var PROBE_SCRIPT = `(async () => {
5763
5763
  const navAtRest = navLinks();
5764
5764
  // The menu control: inside the header or navigation, or named as the menu.
5765
5765
  // Not any disclosure \u2014 an FAQ <summary> is not a menu.
5766
- const inChrome = (b) => !!b.closest('header, nav, [role=navigation], [role=banner]');
5766
+ // The site's navigation, not a page's own: a <nav> inside <main>, an
5767
+ // <article> or an <aside> is in-page navigation (a section list, a table of
5768
+ // contents), and its <summary> is a disclosure the reader opens, not the menu.
5769
+ // A rule page's "More in this section" list was judged a broken menu this way.
5770
+ const inChrome = (b) => !!b.closest('header, [role=banner]') ||
5771
+ (!!b.closest('nav, [role=navigation]') && !b.closest('main, article, aside, [role=main]'));
5767
5772
  const named = (b) => /\b(menu|navigation)\b/i.test((b.getAttribute('aria-label') || '') + ' ' + b.textContent);
5768
5773
  const toggle = [...document.querySelectorAll('button, summary, [role=button]')].find((b) => vis(b) &&
5769
5774
  (named(b) || (inChrome(b) && (b.tagName === 'SUMMARY' || b.hasAttribute('aria-expanded') || b.hasAttribute('aria-controls')))));
@@ -5928,7 +5933,7 @@ async function runProbe(opts) {
5928
5933
  fail(new Error(`${chrome} exited with code ${code} before it was ready.`));
5929
5934
  });
5930
5935
  });
5931
- return await evaluate(endpoint, opts.url, opts.width, opts.height ?? 900, timeoutMs);
5936
+ return await evaluate(endpoint, opts.url, opts.width, opts.height ?? 900, timeoutMs, opts.expression ?? PROBE_SCRIPT);
5932
5937
  } finally {
5933
5938
  const exited = child.exitCode !== null ? Promise.resolve() : new Promise((done) => child.once("exit", () => done()));
5934
5939
  child.kill();
@@ -5939,7 +5944,7 @@ async function runProbe(opts) {
5939
5944
  }
5940
5945
  }
5941
5946
  }
5942
- async function evaluate(endpoint, url, width, height, timeoutMs) {
5947
+ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
5943
5948
  const socket = new WebSocket(endpoint);
5944
5949
  let nextId = 1;
5945
5950
  const pending = /* @__PURE__ */ new Map();
@@ -5983,7 +5988,7 @@ async function evaluate(endpoint, url, width, height, timeoutMs) {
5983
5988
  ]);
5984
5989
  }
5985
5990
  await new Promise((done) => setTimeout(done, 250));
5986
- const result = await send("Runtime.evaluate", { expression: PROBE_SCRIPT, awaitPromise: true, returnByValue: true }, session);
5991
+ const result = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }, session);
5987
5992
  const value = result.result?.value;
5988
5993
  if (typeof value !== "string") {
5989
5994
  const description = result.exceptionDetails?.text;
@@ -6159,7 +6164,7 @@ async function ensureProbes(opts) {
6159
6164
  function critiquedSurfaces(projectRoot) {
6160
6165
  const root = join32(projectRoot, ".jig", "critique");
6161
6166
  if (!existsSync19(root)) return [];
6162
- return readdirSync12(root).filter((surface) => existsSync19(join32(root, surface, "screen.json")) || existsSync19(join32(root, surface, "code.json")));
6167
+ return readdirSync12(root).filter((surface) => !surface.startsWith("_") && (existsSync19(join32(root, surface, "screen.json")) || existsSync19(join32(root, surface, "code.json"))));
6163
6168
  }
6164
6169
 
6165
6170
  // src/probe/check.ts
@@ -6575,7 +6580,7 @@ function verifyVerdicts(opts) {
6575
6580
  }
6576
6581
 
6577
6582
  // src/commands/gate.ts
6578
- import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync30, writeFileSync as writeFileSync7 } from "fs";
6583
+ import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync30, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
6579
6584
  import { createHash as createHash2 } from "crypto";
6580
6585
  import { join as join37 } from "path";
6581
6586
  var MAX_BLOCKS = 3;
@@ -6624,6 +6629,38 @@ function asksOwner(text) {
6624
6629
  return /\?(?=[\s)\]"'*_]|$)/.test(prose);
6625
6630
  }
6626
6631
  var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup"]);
6632
+ function sessionStart(transcriptPath) {
6633
+ if (!transcriptPath || !existsSync24(transcriptPath)) return void 0;
6634
+ try {
6635
+ for (const line of readFileSync30(transcriptPath, "utf8").split("\n")) {
6636
+ const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
6637
+ if (m) {
6638
+ const t = Date.parse(m[1]);
6639
+ if (!Number.isNaN(t)) return t;
6640
+ }
6641
+ }
6642
+ } catch {
6643
+ }
6644
+ return void 0;
6645
+ }
6646
+ function newestMtime(dir) {
6647
+ let newest = 0;
6648
+ for (const name of readdirSync15(dir, { withFileTypes: true })) {
6649
+ if (name.name === LOCK) continue;
6650
+ const path = join37(dir, name.name);
6651
+ newest = Math.max(newest, name.isDirectory() ? newestMtime(path) : statSync4(path).mtimeMs);
6652
+ }
6653
+ return newest;
6654
+ }
6655
+ function surfacesInPlay(root, command, transcriptPath) {
6656
+ const critiqueDir = join37(root, ".jig", "critique");
6657
+ if (!existsSync24(critiqueDir)) return [];
6658
+ const all = readdirSync15(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
6659
+ const start = sessionStart(transcriptPath);
6660
+ if (start === void 0) return all;
6661
+ const current = command === "critique" ? /^\s*surface\s*:\s*(.+)$/im.exec(newestSpec(root)?.body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim().replace(/^["']|["']$/g, "") : void 0;
6662
+ return all.filter((s) => s === current || newestMtime(join37(critiqueDir, s)) >= start - 1e3);
6663
+ }
6627
6664
  function commandProblems(root, command) {
6628
6665
  const problems = [];
6629
6666
  const spec = newestSpec(root);
@@ -6712,10 +6749,10 @@ function gate(opts) {
6712
6749
  );
6713
6750
  }
6714
6751
  }
6715
- problems.push(...verdictGuard(root, command));
6752
+ problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path)));
6716
6753
  const critiqueDir = join37(root, ".jig", "critique");
6717
6754
  if (existsSync24(critiqueDir)) {
6718
- for (const surface of readdirSync15(critiqueDir)) {
6755
+ for (const surface of surfacesInPlay(root, command, opts.input.transcript_path)) {
6719
6756
  const dir = join37(critiqueDir, surface);
6720
6757
  if (!existsSync24(join37(dir, "screen.json")) && !existsSync24(join37(dir, "code.json"))) continue;
6721
6758
  const v = verifyVerdicts({ projectRoot: root, surface });
@@ -6775,16 +6812,17 @@ ${readFileSync30(join37(dir, f), "utf8")}`;
6775
6812
  });
6776
6813
  return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
6777
6814
  }
6778
- function verdictGuard(root, command) {
6815
+ function verdictGuard(root, command, inPlay) {
6779
6816
  const critiqueDir = join37(root, ".jig", "critique");
6780
6817
  if (!existsSync24(critiqueDir)) return [];
6781
6818
  const problems = [];
6782
6819
  for (const surface of readdirSync15(critiqueDir)) {
6820
+ if (surface.startsWith("_")) continue;
6783
6821
  const dir = join37(critiqueDir, surface);
6784
6822
  const now = verdictChecksum(dir);
6785
6823
  if (!now) continue;
6786
6824
  const lockPath = join37(dir, LOCK);
6787
- if (command === "critique") {
6825
+ if (command === "critique" && (!inPlay || inPlay.includes(surface))) {
6788
6826
  try {
6789
6827
  writeFileSync7(lockPath, JSON.stringify({ checksum: now }) + "\n", "utf8");
6790
6828
  } 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.0",
3
+ "version": "0.17.2",
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
@@ -1191,6 +1191,12 @@ blocking. It also refuses when a critique's verdict files changed after `critiqu
1191
1191
  wrote them, in a session that did not run `critique`: a fix is judged by the next
1192
1192
  review, never by the builder editing the last one.
1193
1193
 
1194
+ It judges the critiques this session touched: any with a file changed since the
1195
+ session began, and the current spec's surface when you ran `critique`. Another
1196
+ page's older critique does not hold you back. To set a critique aside for good,
1197
+ rename its folder with a leading underscore (`.jig/critique/_old-catalog`); the
1198
+ gate never judges it.
1199
+
1194
1200
  A warning you are sure is right as it stands, because the detector guessed and the
1195
1201
  guess is wrong here, is waived on its own line or the line above, in the file's
1196
1202
  comment syntax: `jig-allow <ID>: <why>`. The reason is what the user reads, so write
@@ -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) {