@intentius/behold 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/AGENTS.md +94 -2
  2. package/README.md +133 -2
  3. package/demos.json +25 -1
  4. package/dist/cli.js +3176 -330
  5. package/example-argo-estate/README.md +71 -0
  6. package/example-argo-estate/app-a/chant.config.ts +18 -0
  7. package/example-argo-estate/app-a/manifests/app.yaml +54 -0
  8. package/example-argo-estate/app-a/package.json +13 -0
  9. package/example-argo-estate/app-a/src/app.ts +35 -0
  10. package/example-argo-estate/app-a/tsconfig.json +1 -0
  11. package/example-argo-estate/app-b/chant.config.ts +18 -0
  12. package/example-argo-estate/app-b/manifests/app.yaml +54 -0
  13. package/example-argo-estate/app-b/package.json +13 -0
  14. package/example-argo-estate/app-b/src/app.ts +34 -0
  15. package/example-argo-estate/app-b/tsconfig.json +1 -0
  16. package/example-argo-estate/control-plane/chant.config.ts +29 -0
  17. package/example-argo-estate/control-plane/package.json +13 -0
  18. package/example-argo-estate/control-plane/src/argo.ts +69 -0
  19. package/example-argo-estate/control-plane/tsconfig.json +1 -0
  20. package/example-argo-estate/package-lock.json +1949 -0
  21. package/example-argo-estate/package.json +10 -0
  22. package/example-carve/README.md +194 -0
  23. package/example-carve/app/chant.config.ts +6 -0
  24. package/example-carve/app/package-lock.json +1075 -0
  25. package/example-carve/app/package.json +13 -0
  26. package/example-carve/app/src/carved.ts +30 -0
  27. package/example-carve/app/tsconfig.json +1 -0
  28. package/example-carve/carve-report.json +872 -0
  29. package/example-carve/legacy-tf/cdn.tf +23 -0
  30. package/example-carve/legacy-tf/compute.tf +63 -0
  31. package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
  32. package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
  33. package/example-carve/legacy-tf/naming.tf +10 -0
  34. package/example-carve/legacy-tf/network.tf +119 -0
  35. package/example-carve/legacy-tf/observability.tf +18 -0
  36. package/example-carve/legacy-tf/outputs.tf +16 -0
  37. package/example-carve/legacy-tf/storage.tf +33 -0
  38. package/example-carve/legacy-tf/terraform.tfstate +602 -0
  39. package/example-carve/legacy-tf/versions.tf +40 -0
  40. package/example-flux-estate/README.md +22 -12
  41. package/example-flux-estate/app-a/package.json +2 -2
  42. package/example-flux-estate/app-a/src/app.ts +2 -1
  43. package/example-flux-estate/app-b/chant.config.ts +4 -3
  44. package/example-flux-estate/app-b/package.json +2 -2
  45. package/example-flux-estate/app-b/src/app.ts +5 -3
  46. package/example-flux-estate/control-plane/package.json +2 -2
  47. package/example-flux-estate/control-plane/src/flux.ts +10 -2
  48. package/example-flux-estate/package-lock.json +17 -17
  49. package/example-k8s/README.md +16 -2
  50. package/example-k8s/base/deployment.yaml +21 -0
  51. package/example-k8s/base/kustomization.yaml +3 -0
  52. package/example-k8s/base/service.yaml +12 -0
  53. package/example-k8s/chant.config.ts +10 -0
  54. package/example-k8s/overlays/dev/kustomization.yaml +6 -0
  55. package/example-k8s/package-lock.json +18 -18
  56. package/example-k8s/package.json +3 -3
  57. package/example-writes/package-lock.json +14 -14
  58. package/example-writes/package.json +3 -3
  59. package/package.json +9 -6
  60. package/web/app.js +1027 -42
  61. package/web/carve-steps.js +610 -0
  62. package/web/carve-steps.test.js +233 -0
  63. package/web/demos.js +71 -0
  64. package/web/demos.test.js +83 -0
  65. package/web/icons/cncf/argo.svg +1 -0
  66. package/web/icons/cncf/flux.svg +1 -0
  67. package/web/icons/cncf/helm.svg +1 -0
  68. package/web/icons/k8s/c-role.svg +1 -0
  69. package/web/icons/k8s/cm.svg +1 -0
  70. package/web/icons/k8s/crb.svg +1 -0
  71. package/web/icons/k8s/crd.svg +1 -0
  72. package/web/icons/k8s/cronjob.svg +1 -0
  73. package/web/icons/k8s/deploy.svg +1 -0
  74. package/web/icons/k8s/ds.svg +1 -0
  75. package/web/icons/k8s/ep.svg +1 -0
  76. package/web/icons/k8s/group.svg +1 -0
  77. package/web/icons/k8s/hpa.svg +1 -0
  78. package/web/icons/k8s/ing.svg +1 -0
  79. package/web/icons/k8s/job.svg +1 -0
  80. package/web/icons/k8s/limits.svg +1 -0
  81. package/web/icons/k8s/netpol.svg +1 -0
  82. package/web/icons/k8s/ns.svg +1 -0
  83. package/web/icons/k8s/pod.svg +1 -0
  84. package/web/icons/k8s/psp.svg +1 -0
  85. package/web/icons/k8s/pv.svg +1 -0
  86. package/web/icons/k8s/pvc.svg +1 -0
  87. package/web/icons/k8s/quota.svg +1 -0
  88. package/web/icons/k8s/rb.svg +1 -0
  89. package/web/icons/k8s/role.svg +1 -0
  90. package/web/icons/k8s/rs.svg +1 -0
  91. package/web/icons/k8s/sa.svg +1 -0
  92. package/web/icons/k8s/sc.svg +1 -0
  93. package/web/icons/k8s/secret.svg +1 -0
  94. package/web/icons/k8s/sts.svg +1 -0
  95. package/web/icons/k8s/svc.svg +1 -0
  96. package/web/icons/k8s/user.svg +1 -0
  97. package/web/icons/k8s/vol.svg +1 -0
  98. package/web/index.html +285 -85
  99. package/web/json-view.js +334 -0
  100. package/web/json-view.test.js +218 -0
  101. package/web/layout-store.js +278 -0
  102. package/web/layout-store.test.js +388 -0
  103. package/web/panel.js +28 -0
  104. package/web/theme.js +114 -6
package/web/index.html CHANGED
@@ -5,14 +5,42 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>behold</title>
7
7
  <style>
8
+ /* Pre-paint fallback (#229). These are the DEFAULT theme's tokens —
9
+ Catppuccin Mocha run through theme.js's tokensFor() — not a palette of
10
+ their own, so the first frame is already the frame initTheme() lands
11
+ on and there is no flash of a different app. (It used to be GitHub
12
+ Primer dark, which is why behold's first paint read as every other
13
+ generated tool.) The ONLY hardcoded colours in the whole stylesheet
14
+ live in this block; smoke/ui-smoke.mjs recomputes them from theme.js
15
+ and fails if the two drift apart. */
8
16
  :root {
9
- --bg: #0d1117; --panel: #161b22; --line: #30363d; --fg: #e6edf3; --muted: #8b949e;
10
- --managed: #3fb950; --foreign: #d29922; --pending: #58a6ff; --edge: #6e7681;
11
- --degraded: #f85149;
17
+ --bg: #1e1e2e; --panel: #2e2f40; --line: #3b3c4a; --fg: #cdd6f4; --muted: #777a8c;
18
+ --edge: #56596d; --managed: #a6e3a1; --foreign: #f9e2af; --pending: #89b4fa;
19
+ --degraded: #f38ba8; --neutral: #6d7187; --runtime: #94e2d5;
20
+ --rule: #4e4f64; --focus: #a3b4d0; --active: #3d4660; --well: #11101f; --shadow: #050507;
21
+ }
22
+ /* The non-colour half of the identity: type and radii. These can't derive
23
+ from a colour theme, so they're the one static token block — but they
24
+ ARE tokens, and nothing below hardcodes a size or a radius.
25
+ Type: a terminal-adjacent control plane, so mono does what mono is for —
26
+ ids, addresses, statuses, counts, the diagnosis lines — and the sans
27
+ carries labels only. Three sizes, not five near-identical ones.
28
+ (No vendored typeface: behold ships no binaries. A licensed .woff2 via
29
+ a release asset is the follow-up if the system stacks aren't enough.)
30
+ Radii: sharp on controls, soft on the shells they sit in. */
31
+ :root {
32
+ --font-ui: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
33
+ --font-mono: ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Code", "JetBrains Mono", "IBM Plex Mono", Menlo, Consolas, monospace;
34
+ --t-display: 15px;
35
+ --t-body: 12.5px;
36
+ --t-caption: 10.5px;
37
+ --t-caps: .07em;
38
+ --r-ctl: 3px;
39
+ --r-shell: 10px;
12
40
  }
13
41
  * { box-sizing: border-box; }
14
42
  html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
15
- font: 14px/1.4 ui-sans-serif, system-ui, -apple-system, sans-serif; }
43
+ font: var(--t-body)/1.45 var(--font-ui); }
16
44
  /* #186: no header row at all — the graph runs full-bleed; every piece
17
45
  of state the header used to carry (meta line, statusbar) lives in the
18
46
  floating panel's footer instead. */
@@ -21,31 +49,45 @@
21
49
  #app.inspect-collapsed { grid-template-columns: 1fr 0; }
22
50
  #app.inspect-collapsed #inspect { display: none; }
23
51
  #graph { grid-row: 1; grid-column: 1; overflow: auto; position: relative; }
24
- #inspect { grid-row: 1; grid-column: 2; border-left: 1px solid var(--line); background: var(--panel);
52
+ #inspect { grid-row: 1; grid-column: 2; border-left: 1px solid var(--rule); background: var(--panel);
25
53
  padding: 14px; overflow: auto; position: relative; }
26
- #inspect h2 { font-size: 13px; margin: 0 0 8px; color: var(--muted); font-weight: 600;
27
- text-transform: uppercase; letter-spacing: .5px; }
54
+ /* Section labels are the sans voice: small caps, tracked, muted. Every
55
+ heading in this pane (identity, live, drift, field ownership — app.js
56
+ appends them) is styled here rather than inline, so the scale stays in
57
+ one place. */
58
+ #inspect h2, #inspect h3 { color: var(--muted); font-size: var(--t-caption); font-weight: 600;
59
+ text-transform: uppercase; letter-spacing: var(--t-caps); }
60
+ #inspect h2 { margin: 0 0 10px; }
61
+ #inspect h3 { margin: 16px 0 6px; }
28
62
  /* Declared/identity: key ABOVE value, value full-width — long ARNs wrap
29
- readably instead of being crushed into a squeezed right column. */
63
+ readably instead of being crushed into a squeezed right column. The
64
+ VALUE is data — an id, an ARN, a physical id, a status — so it's mono;
65
+ the key stays sans. */
30
66
  #inspect dl { margin: 0; }
31
- #inspect dt { color: var(--muted); font-size: 11px; margin-top: 9px; }
32
- #inspect dd { margin: 1px 0 0; font-size: 12px; overflow-wrap: anywhere; }
67
+ #inspect dt { color: var(--muted); font-size: var(--t-caption); margin-top: 9px; }
68
+ #inspect dd { margin: 2px 0 0; font: var(--t-body)/1.45 var(--font-mono); overflow-wrap: anywhere; }
69
+ #inspect p { font-size: var(--t-body); }
70
+ /* #192: the queried-address diagnosis line — a request path, so mono. */
71
+ #inspect p.queried { color: var(--muted); font: var(--t-caption)/1.45 var(--font-mono);
72
+ overflow-wrap: anywhere; margin-top: 2px; }
33
73
  /* Drag handle on the pane's left edge → resize; chevron → collapse. */
34
74
  #inspect-resize { position: absolute; left: -3px; top: 0; bottom: 0; width: 7px; cursor: col-resize; z-index: 6; }
35
- #inspect-resize:hover { background: var(--pending); opacity: .35; }
75
+ #inspect-resize:hover { background: var(--focus); opacity: .5; }
36
76
  #inspect-collapse { position: absolute; top: 12px; right: 12px; z-index: 6; background: none;
37
- border: none; color: var(--muted); font-size: 15px; cursor: pointer; line-height: 1; padding: 2px; }
77
+ border: none; color: var(--muted); font-size: var(--t-display); cursor: pointer; line-height: 1; padding: 2px; }
38
78
  #inspect-collapse:hover { color: var(--fg); }
39
79
  #inspect-reopen { position: fixed; right: 0; top: 50%; transform: translateY(-50%); z-index: 30; display: none;
40
- background: var(--panel); color: var(--muted); border: 1px solid var(--line); border-right: none;
41
- border-radius: 6px 0 0 6px; padding: 10px 5px; cursor: pointer; writing-mode: vertical-rl; font-size: 11px; }
42
- #inspect-reopen:hover { color: var(--fg); border-color: var(--pending); }
80
+ background: var(--panel); color: var(--muted); border: 1px solid var(--rule); border-right: none;
81
+ border-radius: var(--r-ctl) 0 0 var(--r-ctl); padding: 10px 5px; cursor: pointer;
82
+ writing-mode: vertical-rl; font-size: var(--t-caption); letter-spacing: var(--t-caps); }
83
+ #inspect-reopen:hover { color: var(--fg); border-color: var(--focus); }
43
84
  #app.inspect-collapsed #inspect-reopen { display: block; }
44
85
  /* The graph SVG is pinhole's, self-styled. It fills the pane; behold drives
45
86
  zoom/pan by mutating its viewBox (see setupPanZoom in app.js) — pinch or
46
87
  ⌘/Ctrl+scroll to zoom at the cursor, drag to pan. It starts fit-to-pane;
47
88
  the "⤢ fit" button resets. behold also adds a selection highlight. */
48
- #graph svg { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
89
+ #graph svg { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none;
90
+ user-select: none; -webkit-user-select: none; }
49
91
  #graph svg.grabbing { cursor: grabbing; }
50
92
  /* Edge hover-highlight — trace a single edge through an overlapping bundle.
51
93
  pinhole already paints a fat transparent hit-path per edge (pointer-events
@@ -53,6 +95,28 @@
53
95
  visible line. The hovered edge is also raised to the top in JS so it isn't
54
96
  buried under the bundle. */
55
97
  #graph g[data-edge-from] .pin-edge-line { transition: stroke .08s ease, stroke-width .08s ease; }
98
+ /* ---- Motion (#229) ---------------------------------------------------
99
+ behold spends motion exactly twice, and never for decoration:
100
+ 1. the graph fades through on every re-render, so a refresh reads as a
101
+ new frame instead of a flicker (the SVG element is replaced whole,
102
+ so the animation fires once per render by construction);
103
+ 2. a node whose drift `_status` CHANGED since the last render glows
104
+ once in its new status colour — the diff is a module-level
105
+ id → _status map in app.js (markStatusChanges), and `--pulse` is
106
+ set per node to the status hue it just became.
107
+ Both are off entirely under prefers-reduced-motion. */
108
+ #graph svg { animation: graph-fade-through .18s ease-out; }
109
+ @keyframes graph-fade-through { from { opacity: 0; } to { opacity: 1; } }
110
+ #graph [data-node-id].status-changed { animation: status-pulse .85s ease-out 2; }
111
+ @keyframes status-pulse {
112
+ 0%, 100% { filter: drop-shadow(0 0 0 var(--pulse, var(--pending))); }
113
+ 45% { filter: drop-shadow(0 0 7px var(--pulse, var(--pending))); }
114
+ }
115
+ @media (prefers-reduced-motion: reduce) {
116
+ #graph svg, #graph [data-node-id].status-changed { animation: none; }
117
+ #graph g[data-edge-from] .pin-edge-line { transition: none; }
118
+ #graph [data-layout-resize] { transition: none; }
119
+ }
56
120
  #graph g[data-edge-from]:hover .pin-edge-line,
57
121
  #graph g[data-edge-from].edge-hi .pin-edge-line { stroke: var(--pending); stroke-width: 3.2; }
58
122
  /* Edge kind by reason (logical view): a data dependency reads as a dashed
@@ -62,15 +126,27 @@
62
126
  #graph g[data-edge-from][data-edge-via] { cursor: help; }
63
127
  /* Fit reset + a zoom hint, floating top-right of the graph pane. */
64
128
  #zoom-toggle { position: absolute; top: 8px; right: 8px; z-index: 2; background: var(--panel);
65
- color: var(--muted); border: 1px solid var(--line); border-radius: 6px; padding: 3px 10px;
66
- font-size: 12px; cursor: pointer; }
67
- #zoom-toggle:hover { border-color: var(--pending); color: var(--fg); }
129
+ color: var(--muted); border: 1px solid var(--rule); border-radius: var(--r-ctl); padding: 4px 10px;
130
+ font: var(--t-caption)/1.3 var(--font-mono); cursor: pointer; }
131
+ #zoom-toggle:hover { border-color: var(--focus); color: var(--fg); }
132
+ /* Hand layout (#228): the escape hatch, beside ⤢ fit. Hidden by app.js
133
+ until something on this lens is actually hand-placed — it is both the
134
+ reset and the only indication that dagre isn't the whole story here. */
135
+ #layout-reset { position: absolute; top: 8px; right: 66px; z-index: 2; background: var(--panel);
136
+ color: var(--muted); border: 1px solid var(--rule); border-radius: var(--r-ctl); padding: 4px 10px;
137
+ font: var(--t-caption)/1.3 var(--font-mono); cursor: pointer; }
138
+ #layout-reset:hover { border-color: var(--focus); color: var(--fg); }
139
+ /* A containment box's title moves it and its corner handle resizes it.
140
+ The handle carries opacity="0" as a presentation attribute so a static
141
+ export never shows it — this rule outranks the attribute. */
142
+ #graph [data-layout-resize] { opacity: 0; transition: opacity .1s ease; }
143
+ #graph [data-layout-box]:hover [data-layout-resize] { opacity: .85; }
68
144
  /* The way out of waves view, floating top-LEFT so it sits where the eye
69
145
  already is when observe drops you into the wave-laned graph. */
70
146
  #back-to-infra { position: absolute; top: 8px; left: 8px; z-index: 2; background: var(--panel);
71
- color: var(--muted); border: 1px solid var(--line); border-radius: 6px; padding: 3px 10px;
72
- font-size: 12px; cursor: pointer; }
73
- #back-to-infra:hover { border-color: var(--pending); color: var(--fg); }
147
+ color: var(--muted); border: 1px solid var(--rule); border-radius: var(--r-ctl); padding: 4px 10px;
148
+ font: var(--t-caption)/1.3 var(--font-mono); cursor: pointer; }
149
+ #back-to-infra:hover { border-color: var(--focus); color: var(--fg); }
74
150
  /* Blocking load scrim: covers everything (clicks can't pass through) while
75
151
  a live view change is in flight. */
76
152
  #loading-overlay { position: fixed; inset: 0; z-index: 100; display: flex;
@@ -78,11 +154,14 @@
78
154
  background: color-mix(in srgb, var(--bg) 72%, transparent); backdrop-filter: blur(1.5px);
79
155
  cursor: progress; }
80
156
  #loading-overlay[hidden] { display: none; }
81
- #loading-overlay #loading-msg { font-size: 13px; color: var(--muted); }
157
+ #loading-overlay #loading-msg { font: var(--t-body)/1.4 var(--font-mono); color: var(--muted); }
82
158
  #loading-overlay .spinner { width: 34px; height: 34px; border-radius: 50%;
83
- border: 3px solid var(--line); border-top-color: var(--pending);
159
+ border: 3px solid var(--line); border-top-color: var(--focus);
84
160
  animation: spin 0.7s linear infinite; }
85
161
  @keyframes spin { to { transform: rotate(360deg); } }
162
+ /* Selection is one of the few places the FULL accent still shows — the
163
+ drift hues own saturation now, and "which node am I looking at" earns
164
+ one of them. */
86
165
  .sel rect, .sel path, .sel > rect { stroke: var(--pending) !important; stroke-width: 2.5 !important; }
87
166
  .err { padding: 24px; color: var(--foreign); }
88
167
  /* #72: a classified precondition failure (chant's lint gate, a project
@@ -91,16 +170,65 @@
91
170
  are usually expected states on a real (imperfect) project, not bugs.
92
171
  Generalizes the old M2 (#54) tier-only `.tier-note`. */
93
172
  .precondition-error { padding: 24px; max-width: 640px; }
94
- .precondition-error-title { color: var(--fg); font-size: 15px; font-weight: 600; margin-bottom: 10px; }
95
- .precondition-error-message { color: var(--muted); white-space: pre-wrap; font: 12px/1.5 ui-monospace, monospace; }
96
- .precondition-error-remedy { color: var(--pending); margin-top: 14px; }
173
+ .precondition-error-title { color: var(--fg); font-size: var(--t-display); font-weight: 600; margin-bottom: 10px; }
174
+ .precondition-error-message { color: var(--muted); white-space: pre-wrap; font: var(--t-body)/1.55 var(--font-mono); }
175
+ .precondition-error-remedy { color: var(--pending); margin-top: 14px; font: var(--t-body)/1.5 var(--font-mono); }
97
176
  .precondition-error-remedy::before { content: "→ "; }
98
- #actions button, #inspect button { background: var(--panel); color: var(--fg); border: 1px solid var(--line);
99
- border-radius: 6px; padding: 4px 12px; font-size: 12px; cursor: pointer; }
100
- #actions button:hover, #inspect button:hover { border-color: var(--pending); }
177
+ /* #259: the raw /api payload behind the card, collapsed. */
178
+ .precondition-error-raw { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 10px; }
179
+ .precondition-error-rawlabel { color: var(--muted); font-size: var(--t-caption); text-transform: uppercase;
180
+ letter-spacing: var(--t-caps); margin-bottom: 5px; }
181
+ #actions button, #inspect button { background: var(--panel); color: var(--fg); border: 1px solid var(--rule);
182
+ border-radius: var(--r-ctl); padding: 4px 12px; font-size: var(--t-body); cursor: pointer; }
183
+ #actions button:hover, #inspect button:hover { border-color: var(--focus); }
101
184
  #actions button.approve { border-color: var(--managed); color: var(--managed); }
102
185
  #inspect button { border-color: var(--foreign); color: var(--foreign); }
103
186
 
187
+ /* ---- Collapsible JSON (#259) -----------------------------------------
188
+ web/json-view.js builds this; nothing else styles it. Two colours and
189
+ one font: keys and scalars are --fg (the data), every bracket, comma,
190
+ chevron, count and affordance is --muted (the punctuation around it).
191
+ The status hues are deliberately NOT used — they carry meaning in this
192
+ pane, and unlike --fg/--muted they clear no contrast floor across the
193
+ 552 palettes (#240), so a "strings are green" scheme would be
194
+ unreadable on some of them.
195
+ Indent is 2ch — the same two spaces the copied text carries — with a
196
+ --line rule down it so a deep tree still tracks by eye. */
197
+ .jsonv { font: var(--t-body)/1.5 var(--font-mono); color: var(--fg); overflow-wrap: anywhere; }
198
+ .jsonv-children { padding-left: 2ch; border-left: 1px solid var(--line); }
199
+ .jsonv-node[data-open="0"] > .jsonv-children,
200
+ .jsonv-node[data-open="0"] > .jsonv-tail { display: none; }
201
+ .jsonv-node[data-open="1"] > .jsonv-line .jsonv-summary,
202
+ .jsonv-node[data-open="1"] > .jsonv-line .jsonv-close-inline { display: none; }
203
+ .jsonv-key { color: var(--fg); }
204
+ .jsonv-punct, .jsonv-summary, .jsonv-toggle, .jsonv-null { color: var(--muted); }
205
+ .jsonv-head { cursor: pointer; border-radius: var(--r-ctl); }
206
+ .jsonv-head:hover .jsonv-toggle { color: var(--fg); }
207
+ .jsonv-toggle { display: inline-block; width: 1.2ch; user-select: none; }
208
+ /* The two affordances stay quiet until the row is under the pointer or a
209
+ keyboard focus is inside it — a tree of visible "copy" links reads as
210
+ chrome, not as data. */
211
+ .jsonv-copy, .jsonv-more { color: var(--muted); font-size: var(--t-caption); cursor: pointer;
212
+ margin-left: 8px; user-select: none; }
213
+ .jsonv-copy { opacity: 0; }
214
+ .jsonv-line:hover > .jsonv-copy, .jsonv-copy:focus-visible, .jsonv-copy[data-copied] { opacity: 1; }
215
+ .jsonv-copy:hover, .jsonv-more:hover { color: var(--fg); }
216
+ .jsonv-copy[data-copied="1"] { color: var(--managed); }
217
+ .jsonv-copy[data-copied="0"] { color: var(--degraded); }
218
+ .jsonv-head:focus-visible, .jsonv-copy:focus-visible, .jsonv-more:focus-visible {
219
+ outline: 1px solid var(--focus); outline-offset: 1px; }
220
+ /* In the inspect pane the tree replaces a <dd>'s text, so it inherits the
221
+ pane's own mono scale and needs no extra gap. */
222
+ #inspect dd > .jsonv { margin-top: 1px; }
223
+ /* A value PAIR (was/now, declared/live, baseline/live) whose sides are
224
+ trees rather than scalars — see pairCell() in app.js. */
225
+ #inspect .pair-row { display: flex; align-items: baseline; gap: 6px; margin-top: 3px; }
226
+ #inspect .pair-row > .jsonv { flex: 1; min-width: 0; }
227
+ #inspect .pair-label { color: var(--muted); font-size: var(--t-caption); flex: none; }
228
+ /* The op log's JSON lines (#259): a report chant emitted as an object
229
+ reads as a tree in the now-line, not as one 900-column line. */
230
+ #nowline .jsonv { white-space: normal; margin: 2px 0; }
231
+
104
232
  /* ---- Floating control panel ------------------------------------------
105
233
  An Adobe-style palette holding the controls that used to line the
106
234
  header and its strips: zoom, scope lenses, substrate pills, the model
@@ -113,64 +241,85 @@
113
241
  is the discoverable surface, the palette the fast one. Sits above the
114
242
  graph's own floating buttons (z 2) and below toasts (60) / the load
115
243
  scrim (100) / ⌘K (110). */
244
+ /* #229: the shell is the soft thing (10px, a real drop shadow derived
245
+ from the theme, a palette-tinted --rule border); everything INSIDE it
246
+ is sharp (3px). That contrast is what makes it read as a tool palette
247
+ rather than a card of stacked pills. */
116
248
  #panel { position: fixed; width: 272px; z-index: 40; background: var(--panel);
117
- border: 1px solid var(--line); border-radius: 10px; display: flex; flex-direction: column;
118
- box-shadow: 0 10px 34px rgba(0,0,0,.45); user-select: none; }
249
+ border: 1px solid var(--rule); border-radius: var(--r-shell); display: flex; flex-direction: column;
250
+ box-shadow: 0 10px 30px color-mix(in srgb, var(--shadow) 55%, transparent); user-select: none; }
119
251
  #panel.dragging { opacity: .95; }
120
252
  /* #184: tabs-only — the strip doubles as the drag handle and carries the
121
253
  ⌘K pill + collapse chevron. Collapsing hides only the body, so the
122
- panel collapses TO the strip. */
254
+ panel collapses TO the strip.
255
+ #229: the strip is slightly recessed off the body, and the active tab
256
+ is a FILL, not an underline — an underlined tab is the single most
257
+ generated-looking control on the web. */
123
258
  #panel-tabs { display: flex; align-items: stretch; border-bottom: 1px solid var(--line);
124
- cursor: grab; padding-right: 4px; }
259
+ background: color-mix(in srgb, var(--well) 45%, var(--panel));
260
+ border-radius: calc(var(--r-shell) - 1px) calc(var(--r-shell) - 1px) 0 0;
261
+ cursor: grab; padding: 3px 4px 3px 3px; gap: 2px; }
125
262
  #panel.dragging #panel-tabs { cursor: grabbing; }
126
- #panel.collapsed #panel-tabs { border-bottom: none; }
263
+ #panel.collapsed #panel-tabs { border-bottom: none; border-radius: calc(var(--r-shell) - 1px); }
127
264
  #panel.collapsed #panel-body { display: none; }
128
- #panel-tabs button[data-tab] { flex: 1; background: none; border: none; border-bottom: 2px solid transparent;
129
- color: var(--muted); font-size: 10.5px; padding: 7px 0; cursor: pointer; }
265
+ #panel-tabs button[data-tab] { flex: 1; background: none; border: none; border-radius: var(--r-ctl);
266
+ color: var(--muted); font: var(--t-caption)/1 var(--font-ui); padding: 6px 0; cursor: pointer; }
130
267
  #panel-tabs button[data-tab]:hover { color: var(--fg); }
131
- #panel-tabs button[data-tab].active { color: var(--fg); border-bottom-color: var(--pending); }
132
- #panel-tabs #hintk { flex: none; align-self: center; font-size: 10px; padding: 1px 6px; margin-left: 2px; }
268
+ #panel-tabs button[data-tab].active { color: var(--fg); background: var(--active); font-weight: 600; }
269
+ #panel-tabs #hintk { flex: none; align-self: center; padding: 2px 6px; margin-left: 2px; }
133
270
  #panel-collapse { flex: none; align-self: center; background: none; border: none; color: var(--muted);
134
- font-size: 12px; cursor: pointer; line-height: 1; padding: 2px 4px; }
271
+ font-size: var(--t-body); cursor: pointer; line-height: 1; padding: 2px 4px; }
135
272
  #panel-collapse:hover { color: var(--fg); }
136
273
  #panel-body { overflow-y: auto; max-height: min(62vh, 540px); padding: 10px 12px;
137
- font-size: 12px; user-select: text; }
274
+ font-size: var(--t-body); user-select: text; }
138
275
  #panel-body section { display: none; }
139
276
  #panel-body section.active { display: block; }
140
- #panel h3 { margin: 14px 0 6px; font-size: 10px; text-transform: uppercase; letter-spacing: .5px;
141
- color: var(--muted); font-weight: 600; }
277
+ #panel h3 { margin: 14px 0 6px; font-size: var(--t-caption); text-transform: uppercase;
278
+ letter-spacing: var(--t-caps); color: var(--muted); font-weight: 600; }
142
279
  #panel h3:first-child { margin-top: 0; }
143
- #panel .panel-muted { color: var(--muted); font-size: 11px; margin: 4px 0; }
280
+ #panel .panel-muted { color: var(--muted); font: var(--t-caption)/1.5 var(--font-mono);
281
+ margin: 4px 0; overflow-wrap: anywhere; }
144
282
  #panel .prow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 2px 0; }
145
- /* A radio-like option row: the current value carries the accent border. */
283
+ /* A radio-like option row: the current value is FILLED, with a hairline of
284
+ the full accent down its leading edge — the one saturated mark in the
285
+ chrome, so the eye finds the current lens without the whole row
286
+ shouting in accent-coloured text. */
146
287
  #panel .opt { display: block; width: 100%; text-align: left; background: none;
147
- border: 1px solid transparent; border-radius: 6px; color: var(--fg); font-size: 12px;
148
- padding: 4px 8px; cursor: pointer; }
149
- #panel .opt:hover { border-color: var(--line); }
150
- #panel .opt.active { border-color: var(--pending); color: var(--pending); }
151
- #panel select { width: 100%; background: var(--bg); color: var(--fg); border: 1px solid var(--line);
152
- border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer; }
153
- #panel select:hover { border-color: var(--pending); }
154
- #panel button.act { background: var(--bg); color: var(--fg); border: 1px solid var(--line);
155
- border-radius: 6px; padding: 3px 10px; font-size: 11px; cursor: pointer; }
156
- #panel button.act:hover { border-color: var(--pending); }
157
- #panel input.panel-input { flex: 1; min-width: 0; background: var(--bg); color: var(--fg);
158
- border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px; font-size: 11px; }
159
- #panel input.panel-input:focus { border-color: var(--pending); outline: none; }
160
- /* Model tab rows: legend counts (static) and clickable node rows. */
288
+ border: 1px solid transparent; border-radius: var(--r-ctl); color: var(--fg);
289
+ font-size: var(--t-body); padding: 4px 8px; cursor: pointer; }
290
+ #panel .opt:hover:not(:disabled) { border-color: var(--line); }
291
+ /* #268: a demo whose prerequisites are missing stays visible and says why
292
+ dimmed and unclickable, not removed from the catalog. */
293
+ #panel .opt:disabled { opacity: .5; cursor: not-allowed; }
294
+ #panel .opt.active { background: var(--active); border-color: var(--rule); color: var(--fg);
295
+ box-shadow: inset 2px 0 0 var(--pending); }
296
+ #panel select { width: 100%; background: var(--well); color: var(--fg); border: 1px solid var(--line);
297
+ border-radius: var(--r-ctl); padding: 4px 8px; font-size: var(--t-body); cursor: pointer; }
298
+ #panel select:hover { border-color: var(--focus); }
299
+ #panel button.act { background: var(--well); color: var(--fg); border: 1px solid var(--rule);
300
+ border-radius: var(--r-ctl); padding: 3px 10px; font-size: var(--t-caption); cursor: pointer; }
301
+ #panel button.act:hover { border-color: var(--focus); }
302
+ #panel input.panel-input { flex: 1; min-width: 0; background: var(--well); color: var(--fg);
303
+ border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 4px 8px;
304
+ font: var(--t-caption)/1.4 var(--font-mono); }
305
+ #panel input.panel-input:focus { border-color: var(--focus); outline: none; }
306
+ /* Model tab rows: legend counts (static) and clickable node rows. Node
307
+ ids are data — mono, so they align and scan like a terminal listing. */
161
308
  #panel .count-row, #panel .node-row { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
162
- #panel .node-row { cursor: pointer; border-radius: 6px; padding: 3px 6px; }
163
- #panel .node-row:hover { background: var(--bg); }
309
+ #panel .node-row { cursor: pointer; border-radius: var(--r-ctl); padding: 3px 6px;
310
+ font: var(--t-caption)/1.5 var(--font-mono); }
311
+ #panel .node-row:hover { background: var(--well); }
164
312
  #panel .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; display: inline-block; }
165
313
  #panel .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
166
- #panel .tag { color: var(--muted); font-size: 11px; white-space: nowrap; }
314
+ #panel .tag { color: var(--muted); font: var(--t-caption)/1.5 var(--font-mono); white-space: nowrap; }
167
315
 
168
316
  /* Substrate readiness (M5, #54) — one row per substrate on the panel's
169
317
  Substrates tab: status dot + name + state, with its actions (bring up /
170
318
  reset / run pipeline) visible inline again. #73 moved those into ⌘K;
171
319
  they remain there too — same actions, two surfaces. */
172
320
  #substrates { display: flex; flex-direction: column; gap: 2px; }
173
- .sub { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
321
+ .sub { display: flex; align-items: center; gap: 8px; padding: 3px 0;
322
+ font: var(--t-caption)/1.6 var(--font-mono); }
174
323
  .sub .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
175
324
  .sub.up .dot { background: var(--managed); }
176
325
  .sub.down .dot { background: var(--degraded); }
@@ -184,34 +333,81 @@
184
333
  tab now, stacked vertically under the deploy actions. */
185
334
  #actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
186
335
  #dial { display: none; flex-direction: column; align-items: stretch; gap: 8px;
187
- font-size: 12px; margin-top: 10px; }
336
+ font-size: var(--t-body); margin-top: 10px; }
188
337
  .dial-track { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
189
338
  .dial-arrow { color: var(--muted); }
190
- .dial-step { background: var(--panel); color: var(--fg); border: 1px solid var(--line);
191
- border-radius: 999px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
192
- .dial-step:hover:not(:disabled) { border-color: var(--pending); }
193
- .dial-step.active { border-color: var(--pending); color: var(--pending); }
339
+ /* The lifecycle steps are controls, so they take the control radius too —
340
+ the pill shape was the last soft edge left inside the shell. */
341
+ .dial-step { background: var(--well); color: var(--fg); border: 1px solid var(--rule);
342
+ border-radius: var(--r-ctl); padding: 3px 12px; font-size: var(--t-body); cursor: pointer; }
343
+ .dial-step:hover:not(:disabled) { border-color: var(--focus); }
344
+ .dial-step.active { background: var(--active); border-color: var(--rule); color: var(--fg);
345
+ box-shadow: inset 2px 0 0 var(--pending); }
194
346
  .dial-step:disabled { opacity: .5; cursor: not-allowed; }
195
- .dial-detail { display: flex; flex-wrap: wrap; gap: 4px 12px; color: var(--muted); }
347
+ .dial-detail { display: flex; flex-wrap: wrap; gap: 4px 12px; color: var(--muted);
348
+ font: var(--t-caption)/1.6 var(--font-mono); }
349
+
350
+ /* The carve walkthrough (#254) — the panel's Carve tab. Six steps on the
351
+ same track the deploy dial uses, so "where am I in this" reads the same
352
+ way in both places; below it, the step's own panel. Every colour here is
353
+ a shared token, so all 552 palettes get it for free. */
354
+ #tab-carve { display: flex; flex-direction: column; gap: 10px; }
355
+ #tab-carve .dial-track { gap: 4px; }
356
+ .carve-step { padding: 3px 8px; font-size: var(--t-caption); }
357
+ .carve-step.done { color: var(--managed); border-color: color-mix(in srgb, var(--managed) 45%, var(--rule)); }
358
+ .carve-step.blocked { opacity: .55; }
359
+ .carve-body { display: flex; flex-direction: column; gap: 6px; }
360
+ /* Data — chant's output, a file, a command — is mono and boxed; prose
361
+ about it stays in the panel's own voice. */
362
+ .carve-pre { margin: 0; background: var(--well); border: 1px solid var(--line);
363
+ border-radius: var(--r-ctl); padding: 6px 8px; max-height: 240px; overflow: auto;
364
+ font: var(--t-caption)/1.5 var(--font-mono); color: var(--fg); white-space: pre-wrap;
365
+ overflow-wrap: anywhere; }
366
+ .carve-pre.carve-cmd { color: var(--muted); }
367
+ .carve-artifact { display: flex; flex-direction: column; gap: 3px; }
368
+ .carve-path { font: var(--t-caption)/1.5 var(--font-mono); overflow-wrap: anywhere; }
369
+ .carve-cut, .carve-honesty { margin: 2px 0; }
370
+ /* The one thing the walkthrough will not do for you, and the refusals it
371
+ hands back — same card, because they are the same kind of statement. */
372
+ .carve-refusal { border: 1px solid var(--degraded); border-left: 3px solid var(--degraded);
373
+ border-radius: var(--r-ctl); padding: 6px 8px; background: var(--well); }
374
+ .carve-refusal.carve-human { border-color: var(--pending); border-left-color: var(--pending); }
375
+ .carve-refusal-title { font-size: var(--t-body); margin-bottom: 2px; }
376
+ .carve-cmd-row { align-items: center; gap: 6px; }
377
+ .carve-cmd-text { font: var(--t-caption)/1.6 var(--font-mono); background: var(--well);
378
+ border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 3px 6px;
379
+ overflow-wrap: anywhere; }
380
+ .carve-cmd-note, .carve-section { margin: 2px 0; }
381
+ .carve-endcard { border: 1px solid var(--managed); border-left: 3px solid var(--managed);
382
+ border-radius: var(--r-ctl); padding: 6px 8px; background: var(--well); }
383
+ .carve-endcard-title { font-size: var(--t-body); margin-bottom: 2px; }
384
+ .carve-caveat > summary { cursor: pointer; color: var(--muted);
385
+ font: var(--t-caption)/1.6 var(--font-mono); }
386
+ /* The still frame of the morph the follow-up animates: a carved card
387
+ wears the managed edge its new owner paints everything with. */
388
+ #graph [data-node-id].carved rect:first-of-type { stroke: var(--managed); stroke-width: 2; }
196
389
 
197
390
  #nowline { grid-column: 1 / 3; grid-row: 2; margin: 0; max-height: 160px; overflow: auto; display: none;
198
- background: #010409; border-top: 1px solid var(--line); padding: 8px 12px; font: 11px/1.5 ui-monospace, monospace;
199
- color: var(--fg); white-space: pre-wrap; }
391
+ background: var(--well); border-top: 1px solid var(--rule); padding: 8px 12px;
392
+ font: var(--t-caption)/1.6 var(--font-mono); color: var(--fg); white-space: pre-wrap; }
200
393
  /* #186: the panel's footer — the state the removed header carried. The
201
394
  statusbar (#73 "hide controls, never state": current zoom + env +
202
395
  stack + tier, see renderStatusbar() in app.js) and the meta line.
203
396
  Hidden when the panel collapses, like the body. */
204
- #panel-foot { border-top: 1px solid var(--line); padding: 6px 12px;
205
- display: flex; flex-direction: column; gap: 2px; }
397
+ /* Counts, env, zoom, tier all of it is state read at a glance, so the
398
+ whole footer is mono. */
399
+ #panel-foot { border-top: 1px solid var(--line); padding: 7px 12px;
400
+ display: flex; flex-direction: column; gap: 3px;
401
+ font: var(--t-caption)/1.5 var(--font-mono); }
206
402
  #panel.collapsed #panel-foot { display: none; }
207
- #panel-foot .meta { color: var(--muted); font-size: 10px; overflow-wrap: anywhere; }
208
- #statusbar { color: var(--muted); font-size: 10.5px; cursor: pointer; }
403
+ #panel-foot .meta { color: var(--muted); overflow-wrap: anywhere; }
404
+ #statusbar { color: var(--muted); cursor: pointer; }
209
405
  /* #131: why the current zoom rendered what it did. Same muted strip,
210
406
  italic so it reads as a caption on the state rather than more state. */
211
407
  .statusbar-note { font-style: italic; opacity: 0.85; }
212
- #hintk { color: var(--muted); border: 1px solid var(--line); border-radius: 6px;
213
- padding: 2px 8px; font-size: 11px; cursor: pointer; white-space: nowrap; }
214
- #hintk:hover { border-color: var(--pending); color: var(--fg); }
408
+ #hintk { color: var(--muted); border: 1px solid var(--rule); border-radius: var(--r-ctl);
409
+ padding: 2px 8px; font: var(--t-caption)/1.4 var(--font-mono); cursor: pointer; white-space: nowrap; }
410
+ #hintk:hover { border-color: var(--focus); color: var(--fg); }
215
411
  /* Command palette (⌘K / Ctrl+K, #73) — ported from spicypath's FG-036
216
412
  pattern (../spicypath/src/index.html), theme vars mapped onto behold's
217
413
  own: --panel/--line/--fg/--bg already exist; --pending stands in for
@@ -221,18 +417,22 @@
221
417
  applyPaletteContrast() in app.js) so the selected row stays legible
222
418
  across all 552 themes, light or dark. */
223
419
  #palette { position: fixed; inset: 0; z-index: 110; display: none; align-items: flex-start;
224
- justify-content: center; background: rgba(0,0,0,.45); }
420
+ justify-content: center; background: color-mix(in srgb, var(--shadow) 62%, transparent); }
225
421
  #palette.on { display: flex; }
226
422
  #palette .box { margin-top: 12vh; width: min(560px, 92vw); background: var(--panel);
227
- border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 16px 50px rgba(0,0,0,.55);
228
- overflow: hidden; }
229
- #pal-input { width: 100%; box-sizing: border-box; background: var(--bg); color: var(--fg);
230
- border: 0; border-bottom: 1px solid var(--line); padding: 11px 14px; font: inherit; outline: none; }
423
+ border: 1px solid var(--rule); border-radius: var(--r-shell);
424
+ box-shadow: 0 16px 44px color-mix(in srgb, var(--shadow) 62%, transparent); overflow: hidden; }
425
+ /* You type commands here mono, at the one display size, is the whole
426
+ point of the surface. */
427
+ #pal-input { width: 100%; box-sizing: border-box; background: var(--well); color: var(--fg);
428
+ border: 0; border-bottom: 1px solid var(--rule); padding: 12px 14px;
429
+ font: var(--t-display)/1.3 var(--font-mono); outline: none; }
231
430
  #pal-list { max-height: 50vh; overflow: auto; padding: 5px; }
232
- #pal-list .row { padding: 7px 11px; border-radius: 6px; color: var(--fg); font-size: 13px;
431
+ #pal-list .row { padding: 7px 11px; border-radius: var(--r-ctl); color: var(--fg);
432
+ font: var(--t-body)/1.5 var(--font-mono);
233
433
  cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
234
434
  #pal-list .row.sel { background: var(--pending); color: var(--pal-sel-fg, var(--fg)); }
235
- #pal-list .empty { padding: 8px 11px; color: var(--muted); font-size: 12px; }
435
+ #pal-list .empty { padding: 8px 11px; color: var(--muted); font-size: var(--t-body); }
236
436
  </style>
237
437
  </head>
238
438
  <body>