@caddis/cli 0.4.46 → 0.4.47

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.132",
3
+ "version": "1.3.133",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -70,6 +70,7 @@ In either route, derive node and link IDs from immutable business keys. Preserve
70
70
  8. **Short titles**: titles fit one line at column width. Long descriptions belong in subtitles or member lists.
71
71
  9. **Fixed node dimensions**: node kinds use fixed box sizes so font rendering does not shift layout.
72
72
  10. **Reduced motion**: removes all animations while preserving static borders and status glows.
73
+ 11. **Thin links, dotted canvas**: links are hairlines (about 1.1 px) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
73
74
 
74
75
  ## Model summary
75
76
 
@@ -24,7 +24,15 @@ Tokens are defined on `:root` and adapted for dark mode via `@media (prefers-col
24
24
  | `--down` | `#8250df` | `#d0a0ee` | Downstream relation count and notes marker |
25
25
  | `--glow` | `rgba(9,105,218,.22)` | `rgba(68,147,248,.35)` | Accent halo for selection, hover, and canvas focus |
26
26
  | `--shadow` | `0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08)` | `0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45)` | Elevated card and modal elevation shadow |
27
- | `--grid` | `rgba(31,35,40,.035)` | `rgba(255,255,255,.04)` | Viewport grid pattern dots and lines |
27
+ | `--grid` | `rgba(31,35,40,.035)` | `rgba(255,255,255,.04)` | Legacy: no longer drawn. The canvas background is dots now (see `--dot`) |
28
+ | `--dot` | `rgba(31,35,40,.18)` | `rgba(255,255,255,.16)` | Colour of the canvas background dots |
29
+ | `--dot-size` | `1.2px` | Same | Radius of a background dot |
30
+ | `--dot-gap` | `24px` | Same | Spacing of the background dots |
31
+ | `--edge-w` | `1.1px` | Same | Width of a normal link (thin, like a hairline; motion carries the state) |
32
+ | `--edge-w-hot` | `1.5px` | Same | Width of a traced or selected link |
33
+ | `--edge-w-play` | `1.6px` | Same | Width of a link while playback runs along it |
34
+ | `--trail-w` | `1.8px` | Same | Width of the draw-in trail on a selected path (divided by the zoom) |
35
+ | `--ripple-w` | `2px` | Same | Width of the what-if ripple (divided by the zoom) |
28
36
  | `--ok-bg` | `rgba(26,127,55,.10)` | `rgba(63,185,80,.12)` | Subtle tinted background for healthy states |
29
37
  | `--warn-bg` | `rgba(154,103,0,.10)` | `rgba(210,153,34,.14)` | Subtle tinted background for warning states |
30
38
  | `--crit-bg` | `rgba(207,34,46,.08)` | `rgba(248,81,73,.12)` | Subtle tinted background for critical states |
@@ -65,7 +73,7 @@ Layout elements follow a strict 4px grid rhythm:
65
73
  | xs | 4px to 7px | Button vertical padding (`4px 11px`), card vertical padding (`5px 10px 7px`), playbar gaps (`7px`) |
66
74
  | sm | 8px to 10px | Card horizontal padding (`10px`), toolbar item gaps, summary grid gaps |
67
75
  | md | 12px to 16px | Page padding (`12px 14px 20px`), header gaps, inspector body padding |
68
- | lg | 20px to 28px | Section margins, top header padding, grid line pitch (28px canvas grid) |
76
+ | lg | 20px to 28px | Section margins, top header padding, dot pitch (24px canvas dots) |
69
77
  | xl | 32px to 44px | Column header heights (44px), tool button dimensions (34px x 32px) |
70
78
 
71
79
  ---
@@ -0,0 +1,37 @@
1
+ // The canvas look is part of the skill's contract: thin lines and a dotted background, both set by tokens
2
+ // an app can override. These tests pin the defaults so a later edit cannot quietly bring back the grid.
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { readFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ import { join, dirname } from "node:path";
8
+
9
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
10
+ const css = readFileSync(join(root, "src/styles.css"), "utf8");
11
+
12
+ test("the canvas background is dots, not grid lines", () => {
13
+ assert.match(css, /#viewport[\s\S]*radial-gradient\(circle, var\(--dot\) var\(--dot-size\)/);
14
+ assert.doesNotMatch(css, /linear-gradient\(var\(--grid\)/, "grid lines must not come back");
15
+ });
16
+
17
+ test("dots are set by tokens: size, spacing and a colour for light and dark", () => {
18
+ assert.match(css, /--dot-size:\s*1\.2px/);
19
+ assert.match(css, /--dot-gap:\s*24px/);
20
+ const colours = css.match(/--dot:\s*rgba\([^)]*\)/g) ?? [];
21
+ assert.ok(colours.length >= 3, "a dot colour for light, dark by media query and dark by attribute");
22
+ });
23
+
24
+ test("links are thin by default and every width comes from a token", () => {
25
+ assert.match(css, /--edge-w:\s*1\.1px/);
26
+ assert.match(css, /path\.edge \{ fill:none; stroke-width:var\(--edge-w\)/);
27
+ assert.match(css, /path\.edge\.hot \{[^}]*stroke-width:var\(--edge-w-hot\)/);
28
+ assert.match(css, /path\.trail \{[^}]*stroke-width:calc\(var\(--trail-w\)/);
29
+ assert.match(css, /path\.ripple \{[^}]*stroke-width:calc\(var\(--ripple-w\)/);
30
+ // Thin means at most 1.5 px for a plain link and 2 px for the highlighted one.
31
+ const w = (name) => Number(css.match(new RegExp(`${name}:\s*([0-9.]+)px`))[1]);
32
+ assert.ok(w("--edge-w") <= 1.5 && w("--edge-w-hot") <= 2 && w("--ripple-w") <= 2.5 && w("--trail-w") <= 2.5);
33
+ });
34
+
35
+ test("forced-colours mode drops the dot pattern", () => {
36
+ assert.match(css, /@media \(forced-colors: active\) \{ #viewport \{ background: Canvas; \} \}/);
37
+ });
@@ -1,7 +1,7 @@
1
1
  :root {
2
2
  --bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
3
3
  --accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
4
- --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
4
+ --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
5
5
  --warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
6
6
  --mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
7
7
  }
@@ -9,14 +9,14 @@
9
9
  :root:not([data-theme="light"]) {
10
10
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
11
11
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
12
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
12
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
13
13
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
14
14
  }
15
15
  }
16
16
  :root[data-theme="dark"] {
17
17
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
18
18
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
19
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
19
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
20
20
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
21
21
  }
22
22
  * { box-sizing:border-box; }
@@ -58,7 +58,7 @@
58
58
  .workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
59
59
  .canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
60
60
  #viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
61
- background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
61
+ background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
62
62
  #viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
63
63
  #world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
64
64
  #viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
@@ -92,8 +92,8 @@
92
92
  .cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
93
93
  .node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
94
94
  #viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
95
- path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
- path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
95
+ path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
+ path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
97
97
  path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
98
98
  .elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
99
99
  #cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
@@ -146,8 +146,8 @@
146
146
  .node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
147
147
  .node.sel { animation:selpulse 2.4s ease-out infinite; }
148
148
  .node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
149
- path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
150
- path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
149
+ path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
150
+ path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
151
151
  .intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
152
152
  .intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
153
153
  #viewport:focus-visible { outline-offset:-2px; }
@@ -171,7 +171,7 @@
171
171
  #pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
172
172
  #pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
173
173
  circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
174
- path.edge.playing { stroke:var(--accent); stroke-width:3; filter:drop-shadow(0 0 5px var(--accent)); opacity:1; }
174
+ path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
175
175
  path.edge.visited { stroke:var(--accent); opacity:.7; }
176
176
  .node.playing { outline:2px solid var(--accent); outline-offset:2px; }
177
177
  /* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
@@ -188,3 +188,4 @@
188
188
  .node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
189
189
  .node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
190
190
  .metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
191
+ @media (forced-colors: active) { #viewport { background: Canvas; } }
@@ -15,7 +15,7 @@
15
15
  <style>:root {
16
16
  --bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
17
17
  --accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
18
- --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
18
+ --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
19
19
  --warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
20
20
  --mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
21
21
  }
@@ -23,14 +23,14 @@
23
23
  :root:not([data-theme="light"]) {
24
24
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
25
25
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
26
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
26
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
27
27
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
28
28
  }
29
29
  }
30
30
  :root[data-theme="dark"] {
31
31
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
32
32
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
33
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
33
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
34
34
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
35
35
  }
36
36
  * { box-sizing:border-box; }
@@ -72,7 +72,7 @@
72
72
  .workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
73
73
  .canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
74
74
  #viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
75
- background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
75
+ background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
76
76
  #viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
77
77
  #world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
78
78
  #viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
@@ -106,8 +106,8 @@
106
106
  .cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
107
107
  .node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
108
108
  #viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
109
- path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
110
- path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
109
+ path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
110
+ path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
111
111
  path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
112
112
  .elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
113
113
  #cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
@@ -160,8 +160,8 @@
160
160
  .node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
161
161
  .node.sel { animation:selpulse 2.4s ease-out infinite; }
162
162
  .node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
163
- path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
164
- path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
163
+ path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
164
+ path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
165
165
  .intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
166
166
  .intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
167
167
  #viewport:focus-visible { outline-offset:-2px; }
@@ -185,7 +185,7 @@
185
185
  #pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
186
186
  #pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
187
187
  circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
188
- path.edge.playing { stroke:var(--accent); stroke-width:3; filter:drop-shadow(0 0 5px var(--accent)); opacity:1; }
188
+ path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
189
189
  path.edge.visited { stroke:var(--accent); opacity:.7; }
190
190
  .node.playing { outline:2px solid var(--accent); outline-offset:2px; }
191
191
  /* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
@@ -202,6 +202,7 @@
202
202
  .node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
203
203
  .node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
204
204
  .metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
205
+ @media (forced-colors: active) { #viewport { background: Canvas; } }
205
206
  </style>
206
207
  </head>
207
208
  <body>
@@ -1,7 +1,7 @@
1
1
  :root {
2
2
  --bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
3
3
  --accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
4
- --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
4
+ --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
5
5
  --warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
6
6
  --mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
7
7
  }
@@ -9,14 +9,14 @@
9
9
  :root:not([data-theme="light"]) {
10
10
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
11
11
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
12
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
12
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
13
13
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
14
14
  }
15
15
  }
16
16
  :root[data-theme="dark"] {
17
17
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
18
18
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
19
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
19
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
20
20
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
21
21
  }
22
22
  * { box-sizing:border-box; }
@@ -58,7 +58,7 @@
58
58
  .workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
59
59
  .canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
60
60
  #viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
61
- background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
61
+ background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
62
62
  #viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
63
63
  #world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
64
64
  #viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
@@ -92,8 +92,8 @@
92
92
  .cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
93
93
  .node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
94
94
  #viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
95
- path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
- path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
95
+ path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
+ path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
97
97
  path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
98
98
  .elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
99
99
  #cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
@@ -146,8 +146,8 @@
146
146
  .node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
147
147
  .node.sel { animation:selpulse 2.4s ease-out infinite; }
148
148
  .node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
149
- path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
150
- path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
149
+ path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
150
+ path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
151
151
  .intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
152
152
  .intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
153
153
  #viewport:focus-visible { outline-offset:-2px; }
@@ -171,7 +171,7 @@
171
171
  #pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
172
172
  #pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
173
173
  circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
174
- path.edge.playing { stroke:var(--accent); stroke-width:3; filter:drop-shadow(0 0 5px var(--accent)); opacity:1; }
174
+ path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
175
175
  path.edge.visited { stroke:var(--accent); opacity:.7; }
176
176
  .node.playing { outline:2px solid var(--accent); outline-offset:2px; }
177
177
  /* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
@@ -188,3 +188,4 @@
188
188
  .node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
189
189
  .node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
190
190
  .metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
191
+ @media (forced-colors: active) { #viewport { background: Canvas; } }
@@ -1764,7 +1764,8 @@ function install6(ctx) {
1764
1764
  if (!a) continue;
1765
1765
  let r = a.getBoundingClientRect();
1766
1766
  if (!r.width && !r.height) {
1767
- const d = a.closest("details");
1767
+ let d = a.closest("details");
1768
+ while (d && d.open) d = d.parentElement?.closest("details") ?? null;
1768
1769
  if (d) r = d.getBoundingClientRect();
1769
1770
  }
1770
1771
  if (!r.width && !r.height) continue;
@@ -70,6 +70,7 @@ In either route, derive node and link IDs from immutable business keys. Preserve
70
70
  8. **Short titles**: titles fit one line at column width. Long descriptions belong in subtitles or member lists.
71
71
  9. **Fixed node dimensions**: node kinds use fixed box sizes so font rendering does not shift layout.
72
72
  10. **Reduced motion**: removes all animations while preserving static borders and status glows.
73
+ 11. **Thin links, dotted canvas**: links are hairlines (about 1.1 px) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
73
74
 
74
75
  ## Model summary
75
76
 
@@ -24,7 +24,15 @@ Tokens are defined on `:root` and adapted for dark mode via `@media (prefers-col
24
24
  | `--down` | `#8250df` | `#d0a0ee` | Downstream relation count and notes marker |
25
25
  | `--glow` | `rgba(9,105,218,.22)` | `rgba(68,147,248,.35)` | Accent halo for selection, hover, and canvas focus |
26
26
  | `--shadow` | `0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08)` | `0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45)` | Elevated card and modal elevation shadow |
27
- | `--grid` | `rgba(31,35,40,.035)` | `rgba(255,255,255,.04)` | Viewport grid pattern dots and lines |
27
+ | `--grid` | `rgba(31,35,40,.035)` | `rgba(255,255,255,.04)` | Legacy: no longer drawn. The canvas background is dots now (see `--dot`) |
28
+ | `--dot` | `rgba(31,35,40,.18)` | `rgba(255,255,255,.16)` | Colour of the canvas background dots |
29
+ | `--dot-size` | `1.2px` | Same | Radius of a background dot |
30
+ | `--dot-gap` | `24px` | Same | Spacing of the background dots |
31
+ | `--edge-w` | `1.1px` | Same | Width of a normal link (thin, like a hairline; motion carries the state) |
32
+ | `--edge-w-hot` | `1.5px` | Same | Width of a traced or selected link |
33
+ | `--edge-w-play` | `1.6px` | Same | Width of a link while playback runs along it |
34
+ | `--trail-w` | `1.8px` | Same | Width of the draw-in trail on a selected path (divided by the zoom) |
35
+ | `--ripple-w` | `2px` | Same | Width of the what-if ripple (divided by the zoom) |
28
36
  | `--ok-bg` | `rgba(26,127,55,.10)` | `rgba(63,185,80,.12)` | Subtle tinted background for healthy states |
29
37
  | `--warn-bg` | `rgba(154,103,0,.10)` | `rgba(210,153,34,.14)` | Subtle tinted background for warning states |
30
38
  | `--crit-bg` | `rgba(207,34,46,.08)` | `rgba(248,81,73,.12)` | Subtle tinted background for critical states |
@@ -65,7 +73,7 @@ Layout elements follow a strict 4px grid rhythm:
65
73
  | xs | 4px to 7px | Button vertical padding (`4px 11px`), card vertical padding (`5px 10px 7px`), playbar gaps (`7px`) |
66
74
  | sm | 8px to 10px | Card horizontal padding (`10px`), toolbar item gaps, summary grid gaps |
67
75
  | md | 12px to 16px | Page padding (`12px 14px 20px`), header gaps, inspector body padding |
68
- | lg | 20px to 28px | Section margins, top header padding, grid line pitch (28px canvas grid) |
76
+ | lg | 20px to 28px | Section margins, top header padding, dot pitch (24px canvas dots) |
69
77
  | xl | 32px to 44px | Column header heights (44px), tool button dimensions (34px x 32px) |
70
78
 
71
79
  ---
@@ -0,0 +1,37 @@
1
+ // The canvas look is part of the skill's contract: thin lines and a dotted background, both set by tokens
2
+ // an app can override. These tests pin the defaults so a later edit cannot quietly bring back the grid.
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { readFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ import { join, dirname } from "node:path";
8
+
9
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
10
+ const css = readFileSync(join(root, "src/styles.css"), "utf8");
11
+
12
+ test("the canvas background is dots, not grid lines", () => {
13
+ assert.match(css, /#viewport[\s\S]*radial-gradient\(circle, var\(--dot\) var\(--dot-size\)/);
14
+ assert.doesNotMatch(css, /linear-gradient\(var\(--grid\)/, "grid lines must not come back");
15
+ });
16
+
17
+ test("dots are set by tokens: size, spacing and a colour for light and dark", () => {
18
+ assert.match(css, /--dot-size:\s*1\.2px/);
19
+ assert.match(css, /--dot-gap:\s*24px/);
20
+ const colours = css.match(/--dot:\s*rgba\([^)]*\)/g) ?? [];
21
+ assert.ok(colours.length >= 3, "a dot colour for light, dark by media query and dark by attribute");
22
+ });
23
+
24
+ test("links are thin by default and every width comes from a token", () => {
25
+ assert.match(css, /--edge-w:\s*1\.1px/);
26
+ assert.match(css, /path\.edge \{ fill:none; stroke-width:var\(--edge-w\)/);
27
+ assert.match(css, /path\.edge\.hot \{[^}]*stroke-width:var\(--edge-w-hot\)/);
28
+ assert.match(css, /path\.trail \{[^}]*stroke-width:calc\(var\(--trail-w\)/);
29
+ assert.match(css, /path\.ripple \{[^}]*stroke-width:calc\(var\(--ripple-w\)/);
30
+ // Thin means at most 1.5 px for a plain link and 2 px for the highlighted one.
31
+ const w = (name) => Number(css.match(new RegExp(`${name}:\s*([0-9.]+)px`))[1]);
32
+ assert.ok(w("--edge-w") <= 1.5 && w("--edge-w-hot") <= 2 && w("--ripple-w") <= 2.5 && w("--trail-w") <= 2.5);
33
+ });
34
+
35
+ test("forced-colours mode drops the dot pattern", () => {
36
+ assert.match(css, /@media \(forced-colors: active\) \{ #viewport \{ background: Canvas; \} \}/);
37
+ });
@@ -1,7 +1,7 @@
1
1
  :root {
2
2
  --bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
3
3
  --accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
4
- --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
4
+ --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
5
5
  --warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
6
6
  --mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
7
7
  }
@@ -9,14 +9,14 @@
9
9
  :root:not([data-theme="light"]) {
10
10
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
11
11
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
12
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
12
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
13
13
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
14
14
  }
15
15
  }
16
16
  :root[data-theme="dark"] {
17
17
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
18
18
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
19
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
19
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
20
20
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
21
21
  }
22
22
  * { box-sizing:border-box; }
@@ -58,7 +58,7 @@
58
58
  .workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
59
59
  .canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
60
60
  #viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
61
- background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
61
+ background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
62
62
  #viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
63
63
  #world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
64
64
  #viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
@@ -92,8 +92,8 @@
92
92
  .cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
93
93
  .node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
94
94
  #viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
95
- path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
- path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
95
+ path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
+ path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
97
97
  path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
98
98
  .elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
99
99
  #cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
@@ -146,8 +146,8 @@
146
146
  .node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
147
147
  .node.sel { animation:selpulse 2.4s ease-out infinite; }
148
148
  .node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
149
- path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
150
- path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
149
+ path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
150
+ path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
151
151
  .intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
152
152
  .intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
153
153
  #viewport:focus-visible { outline-offset:-2px; }
@@ -171,7 +171,7 @@
171
171
  #pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
172
172
  #pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
173
173
  circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
174
- path.edge.playing { stroke:var(--accent); stroke-width:3; filter:drop-shadow(0 0 5px var(--accent)); opacity:1; }
174
+ path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
175
175
  path.edge.visited { stroke:var(--accent); opacity:.7; }
176
176
  .node.playing { outline:2px solid var(--accent); outline-offset:2px; }
177
177
  /* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
@@ -188,3 +188,4 @@
188
188
  .node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
189
189
  .node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
190
190
  .metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
191
+ @media (forced-colors: active) { #viewport { background: Canvas; } }
@@ -15,7 +15,7 @@
15
15
  <style>:root {
16
16
  --bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
17
17
  --accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
18
- --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
18
+ --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
19
19
  --warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
20
20
  --mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
21
21
  }
@@ -23,14 +23,14 @@
23
23
  :root:not([data-theme="light"]) {
24
24
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
25
25
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
26
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
26
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
27
27
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
28
28
  }
29
29
  }
30
30
  :root[data-theme="dark"] {
31
31
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
32
32
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
33
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
33
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
34
34
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
35
35
  }
36
36
  * { box-sizing:border-box; }
@@ -72,7 +72,7 @@
72
72
  .workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
73
73
  .canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
74
74
  #viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
75
- background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
75
+ background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
76
76
  #viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
77
77
  #world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
78
78
  #viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
@@ -106,8 +106,8 @@
106
106
  .cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
107
107
  .node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
108
108
  #viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
109
- path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
110
- path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
109
+ path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
110
+ path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
111
111
  path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
112
112
  .elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
113
113
  #cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
@@ -160,8 +160,8 @@
160
160
  .node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
161
161
  .node.sel { animation:selpulse 2.4s ease-out infinite; }
162
162
  .node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
163
- path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
164
- path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
163
+ path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
164
+ path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
165
165
  .intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
166
166
  .intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
167
167
  #viewport:focus-visible { outline-offset:-2px; }
@@ -185,7 +185,7 @@
185
185
  #pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
186
186
  #pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
187
187
  circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
188
- path.edge.playing { stroke:var(--accent); stroke-width:3; filter:drop-shadow(0 0 5px var(--accent)); opacity:1; }
188
+ path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
189
189
  path.edge.visited { stroke:var(--accent); opacity:.7; }
190
190
  .node.playing { outline:2px solid var(--accent); outline-offset:2px; }
191
191
  /* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
@@ -202,6 +202,7 @@
202
202
  .node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
203
203
  .node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
204
204
  .metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
205
+ @media (forced-colors: active) { #viewport { background: Canvas; } }
205
206
  </style>
206
207
  </head>
207
208
  <body>
@@ -1,7 +1,7 @@
1
1
  :root {
2
2
  --bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
3
3
  --accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
4
- --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
4
+ --up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
5
5
  --warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
6
6
  --mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
7
7
  }
@@ -9,14 +9,14 @@
9
9
  :root:not([data-theme="light"]) {
10
10
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
11
11
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
12
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
12
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
13
13
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
14
14
  }
15
15
  }
16
16
  :root[data-theme="dark"] {
17
17
  --bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
18
18
  --accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
19
- --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
19
+ --up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
20
20
  --warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
21
21
  }
22
22
  * { box-sizing:border-box; }
@@ -58,7 +58,7 @@
58
58
  .workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
59
59
  .canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
60
60
  #viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
61
- background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
61
+ background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
62
62
  #viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
63
63
  #world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
64
64
  #viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
@@ -92,8 +92,8 @@
92
92
  .cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
93
93
  .node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
94
94
  #viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
95
- path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
- path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
95
+ path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
96
+ path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
97
97
  path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
98
98
  .elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
99
99
  #cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
@@ -146,8 +146,8 @@
146
146
  .node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
147
147
  .node.sel { animation:selpulse 2.4s ease-out infinite; }
148
148
  .node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
149
- path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
150
- path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
149
+ path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
150
+ path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
151
151
  .intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
152
152
  .intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
153
153
  #viewport:focus-visible { outline-offset:-2px; }
@@ -171,7 +171,7 @@
171
171
  #pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
172
172
  #pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
173
173
  circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
174
- path.edge.playing { stroke:var(--accent); stroke-width:3; filter:drop-shadow(0 0 5px var(--accent)); opacity:1; }
174
+ path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
175
175
  path.edge.visited { stroke:var(--accent); opacity:.7; }
176
176
  .node.playing { outline:2px solid var(--accent); outline-offset:2px; }
177
177
  /* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
@@ -188,3 +188,4 @@
188
188
  .node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
189
189
  .node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
190
190
  .metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
191
+ @media (forced-colors: active) { #viewport { background: Canvas; } }
@@ -1764,7 +1764,8 @@ function install6(ctx) {
1764
1764
  if (!a) continue;
1765
1765
  let r = a.getBoundingClientRect();
1766
1766
  if (!r.width && !r.height) {
1767
- const d = a.closest("details");
1767
+ let d = a.closest("details");
1768
+ while (d && d.open) d = d.parentElement?.closest("details") ?? null;
1768
1769
  if (d) r = d.getBoundingClientRect();
1769
1770
  }
1770
1771
  if (!r.width && !r.height) continue;
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.132",
2
+ "poolVersion": "1.3.133",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.132",
4
+ "antigravity-plugin": "1.3.133",
5
5
  "antigravity-plugin-extras": "1.3.41",
6
6
  "codex": "unknown"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.46",
3
+ "version": "0.4.47",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",