@w6w/ui 0.0.0-stage → 0.1.1

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 (142) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -2
  4. package/dist/ActionTestForm.d.ts +76 -0
  5. package/dist/AddConnectionModal.d.ts +24 -0
  6. package/dist/AppPicker.d.ts +83 -0
  7. package/dist/CodeBlock-CZu4TFSi.cjs +15 -0
  8. package/dist/CodeBlock-CZu4TFSi.cjs.map +1 -0
  9. package/dist/CodeBlock-Dot3Pd7P.js +2889 -0
  10. package/dist/CodeBlock-Dot3Pd7P.js.map +1 -0
  11. package/dist/CodeBlock.d.ts +65 -0
  12. package/dist/CodeEditor.d.ts +42 -0
  13. package/dist/ConfirmModal-8avl9Nv8.cjs +11 -0
  14. package/dist/ConfirmModal-8avl9Nv8.cjs.map +1 -0
  15. package/dist/ConfirmModal-CD-P8mhN.js +6335 -0
  16. package/dist/ConfirmModal-CD-P8mhN.js.map +1 -0
  17. package/dist/Copyable-1niV9gUa.js +771 -0
  18. package/dist/Copyable-1niV9gUa.js.map +1 -0
  19. package/dist/Copyable-CYZU1o4X.cjs +2 -0
  20. package/dist/Copyable-CYZU1o4X.cjs.map +1 -0
  21. package/dist/JsonEditor.d.ts +60 -0
  22. package/dist/NodeConfigForm.d.ts +70 -0
  23. package/dist/ParamsForm.d.ts +56 -0
  24. package/dist/PropertyEntryForm.d.ts +56 -0
  25. package/dist/RetryPolicyFields.d.ts +18 -0
  26. package/dist/StepBuilderModal.d.ts +375 -0
  27. package/dist/TriggerFillForm.d.ts +52 -0
  28. package/dist/WaitEditor.d.ts +6 -0
  29. package/dist/WorkflowFlowEditor.d.ts +119 -0
  30. package/dist/YamlEditor.d.ts +46 -0
  31. package/dist/app-categories.d.ts +38 -0
  32. package/dist/app-pages.d.ts +111 -0
  33. package/dist/code.cjs +2 -0
  34. package/dist/code.cjs.map +1 -0
  35. package/dist/code.d.ts +29 -0
  36. package/dist/code.mjs +8 -0
  37. package/dist/code.mjs.map +1 -0
  38. package/dist/components/ApiCallsPanel.d.ts +41 -0
  39. package/dist/components/AppIcon.d.ts +25 -0
  40. package/dist/components/AuthFieldsForm.d.ts +22 -0
  41. package/dist/components/Combobox.d.ts +24 -0
  42. package/dist/components/ConfirmModal.d.ts +33 -0
  43. package/dist/components/Copyable.d.ts +51 -0
  44. package/dist/components/CopyableText.d.ts +40 -0
  45. package/dist/components/DeleteButton.d.ts +16 -0
  46. package/dist/components/EditButton.d.ts +10 -0
  47. package/dist/components/ExecutionDetail.d.ts +70 -0
  48. package/dist/components/ExecutionFilters.d.ts +59 -0
  49. package/dist/components/ExecutionList.d.ts +60 -0
  50. package/dist/components/ExecutionLogPanel.d.ts +98 -0
  51. package/dist/components/ExecutionStats.d.ts +47 -0
  52. package/dist/components/ExpressionEditorModal.d.ts +42 -0
  53. package/dist/components/ExpressionInput.d.ts +36 -0
  54. package/dist/components/ExpressionOptions.d.ts +185 -0
  55. package/dist/components/HealthStatusPill.d.ts +25 -0
  56. package/dist/components/HistoryTimeline.d.ts +56 -0
  57. package/dist/components/Icon.d.ts +34 -0
  58. package/dist/components/IconButton.d.ts +60 -0
  59. package/dist/components/InfoTip.d.ts +15 -0
  60. package/dist/components/InternalIcon.d.ts +19 -0
  61. package/dist/components/ListItem.d.ts +25 -0
  62. package/dist/components/Modal.d.ts +29 -0
  63. package/dist/components/RepoSyncIndicator.d.ts +29 -0
  64. package/dist/components/ResolvedParams.d.ts +31 -0
  65. package/dist/components/ServerResources.d.ts +47 -0
  66. package/dist/components/StepStatusPill.d.ts +51 -0
  67. package/dist/components/UptimeStrip.d.ts +56 -0
  68. package/dist/components/execution-format.d.ts +24 -0
  69. package/dist/components/expression-dom.d.ts +154 -0
  70. package/dist/components/expression-template.d.ts +86 -0
  71. package/dist/components/history-scale.d.ts +33 -0
  72. package/dist/components/icons.d.ts +41 -0
  73. package/dist/components/resolved-clip.d.ts +42 -0
  74. package/dist/components/server-resources-format.d.ts +68 -0
  75. package/dist/components/server-resources.fixture.d.ts +9 -0
  76. package/dist/components/use-copy.d.ts +14 -0
  77. package/dist/createW6WApi.d.ts +31 -0
  78. package/dist/flow-connect.d.ts +138 -0
  79. package/dist/flow-fanout.d.ts +15 -0
  80. package/dist/flow-types.d.ts +356 -0
  81. package/dist/flow-utils.d.ts +284 -0
  82. package/dist/flow.cjs +3 -0
  83. package/dist/flow.cjs.map +1 -0
  84. package/dist/flow.d.ts +24 -0
  85. package/dist/flow.mjs +1978 -0
  86. package/dist/flow.mjs.map +1 -0
  87. package/dist/index.cjs +2 -0
  88. package/dist/index.cjs.map +1 -0
  89. package/dist/index.d.ts +94 -0
  90. package/dist/index.mjs +1812 -0
  91. package/dist/index.mjs.map +1 -0
  92. package/dist/oauth-popup.d.ts +17 -0
  93. package/dist/output-shape.d.ts +58 -0
  94. package/dist/provider.d.ts +421 -0
  95. package/dist/resolve-params.d.ts +112 -0
  96. package/dist/resolved-json.d.ts +21 -0
  97. package/dist/run-visuals.d.ts +90 -0
  98. package/dist/step-preview-state.d.ts +143 -0
  99. package/dist/theme.d.ts +32 -0
  100. package/dist/trigger-fields.d.ts +56 -0
  101. package/dist/types.d.ts +413 -0
  102. package/dist/use-app-action-defs.d.ts +7 -0
  103. package/dist/use-enter-submit.d.ts +23 -0
  104. package/dist/use-seed-sources.d.ts +16 -0
  105. package/package.json +112 -3
  106. package/src/code.css +561 -0
  107. package/src/styles/_buttons.scss +38 -0
  108. package/src/styles/_chips.scss +210 -0
  109. package/src/styles/_code.scss +139 -0
  110. package/src/styles/_copyable.scss +147 -0
  111. package/src/styles/_data-vars.scss +38 -0
  112. package/src/styles/_execution-history.scss +282 -0
  113. package/src/styles/_execution-log.scss +178 -0
  114. package/src/styles/_expression-field.scss +65 -0
  115. package/src/styles/_expression-inline.scss +63 -0
  116. package/src/styles/_expression-modal.scss +292 -0
  117. package/src/styles/_fields.scss +161 -0
  118. package/src/styles/_flow.scss +217 -0
  119. package/src/styles/_fx.scss +158 -0
  120. package/src/styles/_health.scss +46 -0
  121. package/src/styles/_history-timeline.scss +156 -0
  122. package/src/styles/_icon-button.scss +22 -0
  123. package/src/styles/_icon.scss +15 -0
  124. package/src/styles/_icons.scss +31 -0
  125. package/src/styles/_incoming-state.scss +46 -0
  126. package/src/styles/_info-tip.scss +9 -0
  127. package/src/styles/_layout.scss +6 -0
  128. package/src/styles/_lists.scss +64 -0
  129. package/src/styles/_modal.scss +125 -0
  130. package/src/styles/_multiselect.scss +69 -0
  131. package/src/styles/_repo-sync-indicator.scss +99 -0
  132. package/src/styles/_scale.scss +70 -0
  133. package/src/styles/_sections.scss +36 -0
  134. package/src/styles/_server-resources.scss +163 -0
  135. package/src/styles/_step-builder.scss +376 -0
  136. package/src/styles/_step-config.scss +195 -0
  137. package/src/styles/_tabs.scss +54 -0
  138. package/src/styles/_tester.scss +111 -0
  139. package/src/styles/_tokens.scss +160 -0
  140. package/src/styles/_uptime.scss +61 -0
  141. package/src/styles.css +4894 -0
  142. package/src/styles.scss +61 -0
package/src/styles.css ADDED
@@ -0,0 +1,4894 @@
1
+ /* GENERATED FILE — DO NOT EDIT.
2
+ *
3
+ * Compiled from src/styles.build.scss by scripts/build-css.mjs (`pnpm build:css`).
4
+ * Edit the .scss partials under src/styles/ instead; `pnpm check:css` fails
5
+ * if this file has drifted from them.
6
+ */
7
+ @charset "UTF-8";
8
+ /* this gets exported as style.css and can be used for the default theming */
9
+ /* these are the necessary styles for React/Svelte Flow, they get used by base.css and style.css */
10
+ .react-flow {
11
+ direction: ltr;
12
+ --xy-edge-stroke-default: #b1b1b7;
13
+ --xy-edge-stroke-width-default: 1;
14
+ --xy-edge-stroke-selected-default: #555;
15
+ --xy-connectionline-stroke-default: #b1b1b7;
16
+ --xy-connectionline-stroke-width-default: 1;
17
+ --xy-attribution-background-color-default: rgba(255, 255, 255, 0.5);
18
+ --xy-minimap-background-color-default: #fff;
19
+ --xy-minimap-mask-background-color-default: rgba(240, 240, 240, 0.6);
20
+ --xy-minimap-mask-stroke-color-default: transparent;
21
+ --xy-minimap-mask-stroke-width-default: 1;
22
+ --xy-minimap-node-background-color-default: #e2e2e2;
23
+ --xy-minimap-node-stroke-color-default: transparent;
24
+ --xy-minimap-node-stroke-width-default: 2;
25
+ --xy-background-color-default: transparent;
26
+ --xy-background-pattern-dots-color-default: #91919a;
27
+ --xy-background-pattern-lines-color-default: #eee;
28
+ --xy-background-pattern-cross-color-default: #e2e2e2;
29
+ background-color: var(--xy-background-color, var(--xy-background-color-default));
30
+ --xy-node-color-default: inherit;
31
+ --xy-node-border-default: 1px solid #1a192b;
32
+ --xy-node-background-color-default: #fff;
33
+ --xy-node-group-background-color-default: rgba(240, 240, 240, 0.25);
34
+ --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
35
+ --xy-node-boxshadow-selected-default: 0 0 0 0.5px #1a192b;
36
+ --xy-node-border-radius-default: 3px;
37
+ --xy-handle-background-color-default: #1a192b;
38
+ --xy-handle-border-color-default: #fff;
39
+ --xy-selection-background-color-default: rgba(0, 89, 220, 0.08);
40
+ --xy-selection-border-default: 1px dotted rgba(0, 89, 220, 0.8);
41
+ --xy-controls-button-background-color-default: #fefefe;
42
+ --xy-controls-button-background-color-hover-default: #f4f4f4;
43
+ --xy-controls-button-color-default: inherit;
44
+ --xy-controls-button-color-hover-default: inherit;
45
+ --xy-controls-button-border-color-default: #eee;
46
+ --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
47
+ --xy-edge-label-background-color-default: #ffffff;
48
+ --xy-edge-label-color-default: inherit;
49
+ --xy-resize-background-color-default: #3367d9;
50
+ }
51
+
52
+ .react-flow.dark {
53
+ --xy-edge-stroke-default: #3e3e3e;
54
+ --xy-edge-stroke-width-default: 1;
55
+ --xy-edge-stroke-selected-default: #727272;
56
+ --xy-connectionline-stroke-default: #b1b1b7;
57
+ --xy-connectionline-stroke-width-default: 1;
58
+ --xy-attribution-background-color-default: rgba(150, 150, 150, 0.25);
59
+ --xy-minimap-background-color-default: #141414;
60
+ --xy-minimap-mask-background-color-default: rgba(60, 60, 60, 0.6);
61
+ --xy-minimap-mask-stroke-color-default: transparent;
62
+ --xy-minimap-mask-stroke-width-default: 1;
63
+ --xy-minimap-node-background-color-default: #2b2b2b;
64
+ --xy-minimap-node-stroke-color-default: transparent;
65
+ --xy-minimap-node-stroke-width-default: 2;
66
+ --xy-background-color-default: #141414;
67
+ --xy-background-pattern-dots-color-default: #777;
68
+ --xy-background-pattern-lines-color-default: #777;
69
+ --xy-background-pattern-cross-color-default: #777;
70
+ --xy-node-color-default: #f8f8f8;
71
+ --xy-node-border-default: 1px solid #3c3c3c;
72
+ --xy-node-background-color-default: #1e1e1e;
73
+ --xy-node-group-background-color-default: rgba(240, 240, 240, 0.25);
74
+ --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, 0.08);
75
+ --xy-node-boxshadow-selected-default: 0 0 0 0.5px #999;
76
+ --xy-handle-background-color-default: #bebebe;
77
+ --xy-handle-border-color-default: #1e1e1e;
78
+ --xy-selection-background-color-default: rgba(200, 200, 220, 0.08);
79
+ --xy-selection-border-default: 1px dotted rgba(200, 200, 220, 0.8);
80
+ --xy-controls-button-background-color-default: #2b2b2b;
81
+ --xy-controls-button-background-color-hover-default: #3e3e3e;
82
+ --xy-controls-button-color-default: #f8f8f8;
83
+ --xy-controls-button-color-hover-default: #fff;
84
+ --xy-controls-button-border-color-default: #5b5b5b;
85
+ --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
86
+ --xy-edge-label-background-color-default: #141414;
87
+ --xy-edge-label-color-default: #f8f8f8;
88
+ }
89
+
90
+ .react-flow__background {
91
+ background-color: var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));
92
+ pointer-events: none;
93
+ z-index: -1;
94
+ }
95
+
96
+ .react-flow__container {
97
+ position: absolute;
98
+ width: 100%;
99
+ height: 100%;
100
+ top: 0;
101
+ left: 0;
102
+ }
103
+
104
+ .react-flow__pane {
105
+ z-index: 1;
106
+ touch-action: none;
107
+ }
108
+
109
+ .react-flow__pane.draggable {
110
+ cursor: grab;
111
+ }
112
+
113
+ .react-flow__pane.dragging {
114
+ cursor: grabbing;
115
+ }
116
+
117
+ .react-flow__pane.selection {
118
+ cursor: pointer;
119
+ }
120
+
121
+ .react-flow__viewport {
122
+ transform-origin: 0 0;
123
+ z-index: 2;
124
+ pointer-events: none;
125
+ }
126
+
127
+ .react-flow__renderer {
128
+ z-index: 4;
129
+ }
130
+
131
+ .react-flow__selection {
132
+ z-index: 6;
133
+ }
134
+
135
+ .react-flow__nodesselection-rect:focus,
136
+ .react-flow__nodesselection-rect:focus-visible {
137
+ outline: none;
138
+ }
139
+
140
+ .react-flow__edge-path {
141
+ stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
142
+ stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));
143
+ fill: none;
144
+ }
145
+
146
+ .react-flow__connection-path {
147
+ stroke: var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));
148
+ stroke-width: var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));
149
+ fill: none;
150
+ }
151
+
152
+ .react-flow .react-flow__edges {
153
+ position: absolute;
154
+ }
155
+
156
+ .react-flow .react-flow__edges svg {
157
+ overflow: visible;
158
+ position: absolute;
159
+ pointer-events: none;
160
+ }
161
+
162
+ .react-flow__edge {
163
+ pointer-events: visibleStroke;
164
+ }
165
+
166
+ .react-flow__edge.selectable {
167
+ cursor: pointer;
168
+ }
169
+
170
+ .react-flow__edge.animated path {
171
+ stroke-dasharray: 5;
172
+ animation: dashdraw 0.5s linear infinite;
173
+ }
174
+
175
+ .react-flow__edge.animated path.react-flow__edge-interaction {
176
+ stroke-dasharray: none;
177
+ animation: none;
178
+ }
179
+
180
+ .react-flow__edge.inactive {
181
+ pointer-events: none;
182
+ }
183
+
184
+ .react-flow__edge.selected,
185
+ .react-flow__edge:focus,
186
+ .react-flow__edge:focus-visible {
187
+ outline: none;
188
+ }
189
+
190
+ .react-flow__edge.selected .react-flow__edge-path,
191
+ .react-flow__edge.selectable:focus .react-flow__edge-path,
192
+ .react-flow__edge.selectable:focus-visible .react-flow__edge-path {
193
+ stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default));
194
+ }
195
+
196
+ .react-flow__edge-textwrapper {
197
+ pointer-events: all;
198
+ }
199
+
200
+ .react-flow__edge .react-flow__edge-text {
201
+ pointer-events: none;
202
+ -webkit-user-select: none;
203
+ -moz-user-select: none;
204
+ user-select: none;
205
+ }
206
+
207
+ /* Arrowhead marker styles - use CSS custom properties as default */
208
+ .react-flow__arrowhead polyline {
209
+ stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
210
+ }
211
+
212
+ .react-flow__arrowhead polyline.arrowclosed {
213
+ fill: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
214
+ }
215
+
216
+ .react-flow__connection {
217
+ pointer-events: none;
218
+ }
219
+
220
+ .react-flow__connection .animated {
221
+ stroke-dasharray: 5;
222
+ animation: dashdraw 0.5s linear infinite;
223
+ }
224
+
225
+ svg.react-flow__connectionline {
226
+ z-index: 1001;
227
+ overflow: visible;
228
+ position: absolute;
229
+ }
230
+
231
+ .react-flow__nodes {
232
+ pointer-events: none;
233
+ transform-origin: 0 0;
234
+ }
235
+
236
+ .react-flow__node {
237
+ position: absolute;
238
+ -webkit-user-select: none;
239
+ -moz-user-select: none;
240
+ user-select: none;
241
+ pointer-events: all;
242
+ transform-origin: 0 0;
243
+ box-sizing: border-box;
244
+ cursor: default;
245
+ }
246
+
247
+ .react-flow__node.selectable {
248
+ cursor: pointer;
249
+ }
250
+
251
+ .react-flow__node.draggable {
252
+ cursor: grab;
253
+ pointer-events: all;
254
+ }
255
+
256
+ .react-flow__node.draggable.dragging {
257
+ cursor: grabbing;
258
+ }
259
+
260
+ .react-flow__nodesselection {
261
+ z-index: 3;
262
+ transform-origin: left top;
263
+ pointer-events: none;
264
+ }
265
+
266
+ .react-flow__nodesselection-rect {
267
+ position: absolute;
268
+ pointer-events: all;
269
+ cursor: grab;
270
+ }
271
+
272
+ .react-flow__handle {
273
+ position: absolute;
274
+ pointer-events: none;
275
+ min-width: 5px;
276
+ min-height: 5px;
277
+ width: 6px;
278
+ height: 6px;
279
+ background-color: var(--xy-handle-background-color, var(--xy-handle-background-color-default));
280
+ border: 1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));
281
+ border-radius: 100%;
282
+ }
283
+
284
+ .react-flow__handle.connectingfrom {
285
+ pointer-events: all;
286
+ }
287
+
288
+ .react-flow__handle.connectionindicator {
289
+ pointer-events: all;
290
+ cursor: crosshair;
291
+ }
292
+
293
+ .react-flow__handle-bottom {
294
+ top: auto;
295
+ left: 50%;
296
+ bottom: 0;
297
+ transform: translate(-50%, 50%);
298
+ }
299
+
300
+ .react-flow__handle-top {
301
+ top: 0;
302
+ left: 50%;
303
+ transform: translate(-50%, -50%);
304
+ }
305
+
306
+ .react-flow__handle-left {
307
+ top: 50%;
308
+ left: 0;
309
+ transform: translate(-50%, -50%);
310
+ }
311
+
312
+ .react-flow__handle-right {
313
+ top: 50%;
314
+ right: 0;
315
+ transform: translate(50%, -50%);
316
+ }
317
+
318
+ .react-flow__edgeupdater {
319
+ cursor: move;
320
+ pointer-events: all;
321
+ }
322
+
323
+ .react-flow__pane.selection .react-flow__panel {
324
+ pointer-events: none;
325
+ }
326
+
327
+ .react-flow__panel {
328
+ position: absolute;
329
+ z-index: 5;
330
+ margin: 15px;
331
+ }
332
+
333
+ .react-flow__panel.top {
334
+ top: 0;
335
+ }
336
+
337
+ .react-flow__panel.bottom {
338
+ bottom: 0;
339
+ }
340
+
341
+ .react-flow__panel.top.center, .react-flow__panel.bottom.center {
342
+ left: 50%;
343
+ transform: translateX(-15px) translateX(-50%);
344
+ }
345
+
346
+ .react-flow__panel.left {
347
+ left: 0;
348
+ }
349
+
350
+ .react-flow__panel.right {
351
+ right: 0;
352
+ }
353
+
354
+ .react-flow__panel.left.center, .react-flow__panel.right.center {
355
+ top: 50%;
356
+ transform: translateY(-15px) translateY(-50%);
357
+ }
358
+
359
+ .react-flow__attribution {
360
+ font-size: 10px;
361
+ background: var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));
362
+ padding: 2px 3px;
363
+ margin: 0;
364
+ }
365
+
366
+ .react-flow__attribution a {
367
+ text-decoration: none;
368
+ color: #999;
369
+ }
370
+
371
+ @keyframes dashdraw {
372
+ from {
373
+ stroke-dashoffset: 10;
374
+ }
375
+ }
376
+ .react-flow__edgelabel-renderer {
377
+ position: absolute;
378
+ width: 100%;
379
+ height: 100%;
380
+ pointer-events: none;
381
+ -webkit-user-select: none;
382
+ -moz-user-select: none;
383
+ user-select: none;
384
+ left: 0;
385
+ top: 0;
386
+ }
387
+
388
+ .react-flow__viewport-portal {
389
+ position: absolute;
390
+ width: 100%;
391
+ height: 100%;
392
+ left: 0;
393
+ top: 0;
394
+ -webkit-user-select: none;
395
+ -moz-user-select: none;
396
+ user-select: none;
397
+ }
398
+
399
+ .react-flow__minimap {
400
+ background: var(--xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)));
401
+ }
402
+
403
+ .react-flow__minimap-svg {
404
+ display: block;
405
+ }
406
+
407
+ .react-flow__minimap-mask {
408
+ fill: var(--xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)));
409
+ stroke: var(--xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)));
410
+ stroke-width: var(--xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)));
411
+ }
412
+
413
+ .react-flow__minimap-node {
414
+ fill: var(--xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)));
415
+ stroke: var(--xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)));
416
+ stroke-width: var(--xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)));
417
+ }
418
+
419
+ .react-flow__background-pattern.dots {
420
+ fill: var(--xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)));
421
+ }
422
+
423
+ .react-flow__background-pattern.lines {
424
+ stroke: var(--xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)));
425
+ }
426
+
427
+ .react-flow__background-pattern.cross {
428
+ stroke: var(--xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)));
429
+ }
430
+
431
+ .react-flow__controls {
432
+ display: flex;
433
+ flex-direction: column;
434
+ box-shadow: var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default));
435
+ }
436
+
437
+ .react-flow__controls.horizontal {
438
+ flex-direction: row;
439
+ }
440
+
441
+ .react-flow__controls-button {
442
+ display: flex;
443
+ justify-content: center;
444
+ align-items: center;
445
+ height: 26px;
446
+ width: 26px;
447
+ padding: 4px;
448
+ border: none;
449
+ background: var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));
450
+ border-bottom: 1px solid var(--xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)));
451
+ color: var(--xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)));
452
+ cursor: pointer;
453
+ -webkit-user-select: none;
454
+ -moz-user-select: none;
455
+ user-select: none;
456
+ }
457
+
458
+ .react-flow__controls-button svg {
459
+ width: 100%;
460
+ max-width: 12px;
461
+ max-height: 12px;
462
+ fill: currentColor;
463
+ }
464
+
465
+ .react-flow__edge.updating .react-flow__edge-path {
466
+ stroke: #777;
467
+ }
468
+
469
+ .react-flow__edge-text {
470
+ font-size: 10px;
471
+ }
472
+
473
+ .react-flow__node.selectable:focus,
474
+ .react-flow__node.selectable:focus-visible {
475
+ outline: none;
476
+ }
477
+
478
+ .react-flow__node-input,
479
+ .react-flow__node-default,
480
+ .react-flow__node-output,
481
+ .react-flow__node-group {
482
+ padding: 10px;
483
+ border-radius: var(--xy-node-border-radius, var(--xy-node-border-radius-default));
484
+ width: 150px;
485
+ font-size: 12px;
486
+ color: var(--xy-node-color, var(--xy-node-color-default));
487
+ text-align: center;
488
+ border: var(--xy-node-border, var(--xy-node-border-default));
489
+ background-color: var(--xy-node-background-color, var(--xy-node-background-color-default));
490
+ }
491
+
492
+ .react-flow__node-input.selectable:hover, .react-flow__node-default.selectable:hover, .react-flow__node-output.selectable:hover, .react-flow__node-group.selectable:hover {
493
+ box-shadow: var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default));
494
+ }
495
+
496
+ .react-flow__node-input.selectable.selected,
497
+ .react-flow__node-input.selectable:focus,
498
+ .react-flow__node-input.selectable:focus-visible,
499
+ .react-flow__node-default.selectable.selected,
500
+ .react-flow__node-default.selectable:focus,
501
+ .react-flow__node-default.selectable:focus-visible,
502
+ .react-flow__node-output.selectable.selected,
503
+ .react-flow__node-output.selectable:focus,
504
+ .react-flow__node-output.selectable:focus-visible,
505
+ .react-flow__node-group.selectable.selected,
506
+ .react-flow__node-group.selectable:focus,
507
+ .react-flow__node-group.selectable:focus-visible {
508
+ box-shadow: var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default));
509
+ }
510
+
511
+ .react-flow__node-group {
512
+ background-color: var(--xy-node-group-background-color, var(--xy-node-group-background-color-default));
513
+ }
514
+
515
+ .react-flow__nodesselection-rect,
516
+ .react-flow__selection {
517
+ background: var(--xy-selection-background-color, var(--xy-selection-background-color-default));
518
+ border: var(--xy-selection-border, var(--xy-selection-border-default));
519
+ }
520
+
521
+ .react-flow__nodesselection-rect:focus,
522
+ .react-flow__nodesselection-rect:focus-visible,
523
+ .react-flow__selection:focus,
524
+ .react-flow__selection:focus-visible {
525
+ outline: none;
526
+ }
527
+
528
+ .react-flow__controls-button:hover {
529
+ background: var(--xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)));
530
+ color: var(--xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)));
531
+ }
532
+
533
+ .react-flow__controls-button:disabled {
534
+ pointer-events: none;
535
+ }
536
+
537
+ .react-flow__controls-button:disabled svg {
538
+ fill-opacity: 0.4;
539
+ }
540
+
541
+ .react-flow__controls-button:last-child {
542
+ border-bottom: none;
543
+ }
544
+
545
+ .react-flow__controls.horizontal .react-flow__controls-button {
546
+ border-bottom: none;
547
+ border-right: 1px solid var(--xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)));
548
+ }
549
+
550
+ .react-flow__controls.horizontal .react-flow__controls-button:last-child {
551
+ border-right: none;
552
+ }
553
+
554
+ .react-flow__resize-control {
555
+ position: absolute;
556
+ }
557
+
558
+ .react-flow__resize-control.left,
559
+ .react-flow__resize-control.right {
560
+ cursor: ew-resize;
561
+ }
562
+
563
+ .react-flow__resize-control.top,
564
+ .react-flow__resize-control.bottom {
565
+ cursor: ns-resize;
566
+ }
567
+
568
+ .react-flow__resize-control.top.left,
569
+ .react-flow__resize-control.bottom.right {
570
+ cursor: nwse-resize;
571
+ }
572
+
573
+ .react-flow__resize-control.bottom.left,
574
+ .react-flow__resize-control.top.right {
575
+ cursor: nesw-resize;
576
+ }
577
+
578
+ /* handle styles */
579
+ .react-flow__resize-control.handle {
580
+ width: 5px;
581
+ height: 5px;
582
+ border: 1px solid #fff;
583
+ border-radius: 1px;
584
+ background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
585
+ translate: -50% -50%;
586
+ }
587
+
588
+ .react-flow__resize-control.handle.left {
589
+ left: 0;
590
+ top: 50%;
591
+ }
592
+
593
+ .react-flow__resize-control.handle.right {
594
+ left: 100%;
595
+ top: 50%;
596
+ }
597
+
598
+ .react-flow__resize-control.handle.top {
599
+ left: 50%;
600
+ top: 0;
601
+ }
602
+
603
+ .react-flow__resize-control.handle.bottom {
604
+ left: 50%;
605
+ top: 100%;
606
+ }
607
+
608
+ .react-flow__resize-control.handle.top.left {
609
+ left: 0;
610
+ }
611
+
612
+ .react-flow__resize-control.handle.bottom.left {
613
+ left: 0;
614
+ }
615
+
616
+ .react-flow__resize-control.handle.top.right {
617
+ left: 100%;
618
+ }
619
+
620
+ .react-flow__resize-control.handle.bottom.right {
621
+ left: 100%;
622
+ }
623
+
624
+ /* line styles */
625
+ .react-flow__resize-control.line {
626
+ border-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
627
+ border-width: 0;
628
+ border-style: solid;
629
+ }
630
+
631
+ .react-flow__resize-control.line.left,
632
+ .react-flow__resize-control.line.right {
633
+ width: 1px;
634
+ transform: translate(-50%, 0);
635
+ top: 0;
636
+ height: 100%;
637
+ }
638
+
639
+ .react-flow__resize-control.line.left {
640
+ left: 0;
641
+ border-left-width: 1px;
642
+ }
643
+
644
+ .react-flow__resize-control.line.right {
645
+ left: 100%;
646
+ border-right-width: 1px;
647
+ }
648
+
649
+ .react-flow__resize-control.line.top,
650
+ .react-flow__resize-control.line.bottom {
651
+ height: 1px;
652
+ transform: translate(0, -50%);
653
+ left: 0;
654
+ width: 100%;
655
+ }
656
+
657
+ .react-flow__resize-control.line.top {
658
+ top: 0;
659
+ border-top-width: 1px;
660
+ }
661
+
662
+ .react-flow__resize-control.line.bottom {
663
+ border-bottom-width: 1px;
664
+ top: 100%;
665
+ }
666
+
667
+ .react-flow__edge-textbg {
668
+ fill: var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default));
669
+ }
670
+
671
+ .react-flow__edge-text {
672
+ fill: var(--xy-edge-label-color, var(--xy-edge-label-color-default));
673
+ }
674
+
675
+ /*
676
+ * @w6w/ui — styles for the pure-presentation components.
677
+ *
678
+ * Everything is namespaced under `.w6w-*` class names so this stylesheet
679
+ * won't collide with your app's CSS.
680
+ *
681
+ * THEMING
682
+ * -------
683
+ * The library ships a complete DEFAULT theme in BOTH light and dark:
684
+ * - Light is the base.
685
+ * - Dark applies automatically when the OS prefers dark
686
+ * (`prefers-color-scheme: dark`).
687
+ * - Either mode can be forced with `data-theme="light" | "dark"` on any
688
+ * ancestor (usually <html>) — the same signal the JS components read
689
+ * (AppIcon / CodeEditor / JsonEditor: explicit prop > data-theme > OS pref),
690
+ * so CSS and components always agree.
691
+ *
692
+ * A HOST can override any subset of the tokens — set the ones you care about and
693
+ * the rest keep their themed defaults:
694
+ *
695
+ * :root { --w6w-accent: #3355e6; } // one token, both modes
696
+ * html[data-theme="dark"] { --w6w-panel: #000 } // mode-specific
697
+ *
698
+ * Every default below is declared through `:where(...)` (specificity 0), so ANY
699
+ * host rule — even a plain `:root { ... }` — wins without !important. Tokens are
700
+ * defined on :root so every `.w6w-*` component inherits them out of the box.
701
+ */
702
+ /* Light — the base defaults. */
703
+ :where(:root) {
704
+ --w6w-bg: #f7f8fa;
705
+ --w6w-panel: #ffffff;
706
+ --w6w-panel-2: #f0f2f6;
707
+ --w6w-border: #d9dee5;
708
+ --w6w-text: #1a1d24;
709
+ --w6w-muted: #5f6875;
710
+ --w6w-accent: #3355e6;
711
+ --w6w-danger: #c1362f;
712
+ --w6w-success: #2e9e5b;
713
+ --w6w-health-ok: var(--w6w-success);
714
+ --w6w-health-degraded: #c77700;
715
+ --w6w-health-down: var(--w6w-danger);
716
+ --w6w-health-unknown: #5f6875;
717
+ --w6w-icon-swatch: #f0f2f6;
718
+ --w6w-code-comment: #6b7280;
719
+ --w6w-code-keyword: #8250df;
720
+ --w6w-code-string: #0a7b34;
721
+ --w6w-code-number: #b3541e;
722
+ --w6w-code-function: #1f6feb;
723
+ --w6w-code-punctuation: #5f6875;
724
+ --w6w-code-variable: #953800;
725
+ --w6w-radius: 10px;
726
+ --w6w-scheme: light;
727
+ }
728
+
729
+ /* Dark — followed automatically from the OS preference (unless a host has
730
+ forced a mode via data-theme, which is handled by the blocks below). */
731
+ @media (prefers-color-scheme: dark) {
732
+ :where(:root) {
733
+ --w6w-bg: #0f1115;
734
+ --w6w-panel: #181b22;
735
+ --w6w-panel-2: #1f232c;
736
+ --w6w-border: #2a2f3a;
737
+ --w6w-text: #e6e8ee;
738
+ --w6w-muted: #9aa3b2;
739
+ --w6w-accent: #5b8cff;
740
+ --w6w-danger: #ff6b6b;
741
+ --w6w-success: #3fbf77;
742
+ --w6w-health-ok: var(--w6w-success);
743
+ --w6w-health-degraded: #f0a020;
744
+ --w6w-health-down: var(--w6w-danger);
745
+ --w6w-health-unknown: #9aa3b2;
746
+ --w6w-icon-swatch: #1f232c;
747
+ --w6w-scheme: dark;
748
+ --w6w-code-comment: #7d8797;
749
+ --w6w-code-keyword: #d2a8ff;
750
+ --w6w-code-string: #7ee787;
751
+ --w6w-code-number: #ffab70;
752
+ --w6w-code-function: #79c0ff;
753
+ --w6w-code-punctuation: #9aa3b2;
754
+ --w6w-code-variable: #ffa657;
755
+ }
756
+ }
757
+ /* Explicit mode. Declared AFTER the media query at equal (zero) specificity, so
758
+ a forced data-theme always beats the OS preference. */
759
+ :where([data-theme=light]) {
760
+ --w6w-scheme: light;
761
+ --w6w-bg: #f7f8fa;
762
+ --w6w-panel: #ffffff;
763
+ --w6w-panel-2: #f0f2f6;
764
+ --w6w-border: #d9dee5;
765
+ --w6w-text: #1a1d24;
766
+ --w6w-muted: #5f6875;
767
+ --w6w-accent: #3355e6;
768
+ --w6w-danger: #c1362f;
769
+ --w6w-success: #2e9e5b;
770
+ --w6w-health-ok: var(--w6w-success);
771
+ --w6w-health-degraded: #c77700;
772
+ --w6w-health-down: var(--w6w-danger);
773
+ --w6w-health-unknown: #5f6875;
774
+ --w6w-icon-swatch: #f0f2f6;
775
+ --w6w-code-comment: #6b7280;
776
+ --w6w-code-keyword: #8250df;
777
+ --w6w-code-string: #0a7b34;
778
+ --w6w-code-number: #b3541e;
779
+ --w6w-code-function: #1f6feb;
780
+ --w6w-code-punctuation: #5f6875;
781
+ --w6w-code-variable: #953800;
782
+ }
783
+
784
+ :where([data-theme=dark]) {
785
+ --w6w-scheme: dark;
786
+ --w6w-bg: #0f1115;
787
+ --w6w-panel: #181b22;
788
+ --w6w-panel-2: #1f232c;
789
+ --w6w-border: #2a2f3a;
790
+ --w6w-text: #e6e8ee;
791
+ --w6w-muted: #9aa3b2;
792
+ --w6w-accent: #5b8cff;
793
+ --w6w-danger: #ff6b6b;
794
+ --w6w-success: #3fbf77;
795
+ --w6w-health-ok: var(--w6w-success);
796
+ --w6w-health-degraded: #f0a020;
797
+ --w6w-health-down: var(--w6w-danger);
798
+ --w6w-health-unknown: #9aa3b2;
799
+ --w6w-icon-swatch: #1f232c;
800
+ --w6w-code-comment: #7d8797;
801
+ --w6w-code-keyword: #d2a8ff;
802
+ --w6w-code-string: #7ee787;
803
+ --w6w-code-number: #ffab70;
804
+ --w6w-code-function: #79c0ff;
805
+ --w6w-code-punctuation: #9aa3b2;
806
+ --w6w-code-variable: #ffa657;
807
+ }
808
+
809
+ .w6w-muted {
810
+ color: var(--w6w-muted);
811
+ }
812
+
813
+ .w6w-small {
814
+ font-size: 0.85em;
815
+ }
816
+
817
+ /* color-scheme — declared on ui COMPONENT ROOTS, never on the host's <html>.
818
+ `color-scheme` inherits and also themes the page canvas, scrollbars and form
819
+ controls, so setting it on :root / [data-theme] let importing this file flip a
820
+ host's whole document. Instead each token block above also sets the
821
+ `--w6w-scheme` custom property (light / dark), which INHERITS, so the nearest
822
+ themed ancestor wins however themes nest (a light panel inside a dark page and
823
+ the reverse). Elements ui renders (any `w6w-` class, which covers the native
824
+ <dialog> modals) and the native controls inside them read it with
825
+ `color-scheme: var(--w6w-scheme)`. Not a hand-picked root list: every ui
826
+ component carries a `w6w-` class. Only an element whose class LIST has a token
827
+ starting `w6w-` matches (`class^=` / `class*=" w6w-"`), so host elements are never
828
+ touched: not a host `data-theme` wrapper, not a host class that merely contains
829
+ `w6w-` (`my-w6w-card`), and not a host element's own color-scheme on inputs nested
830
+ in a ui root (color-scheme inherits, so no descendant rule is needed). Nothing here
831
+ ever matches <html>/:root. All zero specificity. */
832
+ :where([class^=w6w-]:not(:root), [class*=" w6w-"]:not(:root)) {
833
+ color-scheme: var(--w6w-scheme);
834
+ }
835
+
836
+ /*
837
+ * @w6w/ui — the spacing / typography scale.
838
+ *
839
+ * Values are MIRRORED, not invented, from `packages/frontend/packages/web`'s
840
+ * `src/styles/global.css:83-130` — that file is the canonical reference (it
841
+ * arrived at the same 4px base independently); this block copies its
842
+ * `--sp-*`/`--fs-*`/`--fw-*`/`--lh-*`/`--font-*` values byte-for-byte under the
843
+ * `--w6w-*` namespace this package already uses for every other token.
844
+ * `--fs-hero`'s `clamp()` stays `web`-only (a marketing-page concern; `ui` has
845
+ * no hero). Colour tokens, `--radius*`, `--shadow-*` and `--transition-fast`
846
+ * are NOT part of this scale — see `_tokens.scss`.
847
+ *
848
+ * HALF-STEPS (`--w6w-sp-0-5` / `--w6w-sp-1-5` / `--w6w-sp-2-5`) are `ui`-only
849
+ * additions on top of `web`'s set — `web` has none. The rule that keeps this a
850
+ * system rather than a free-for-all: whole steps for new code; a half-step is
851
+ * only for preserving an existing measured value that does not sit on a 4px
852
+ * step. Parity with `web` is therefore DIRECTIONAL — `ui` ⊇ `web` on the
853
+ * shared key set (every `web` token here has a byte-identical `ui` counterpart;
854
+ * `ui` additionally declares the three half-steps `web` does not).
855
+ *
856
+ * One `:where(:root)` block, not colour's three-way light / prefers-color-scheme /
857
+ * [data-theme] split — the scale is not theme-dependent, so a single specificity-0
858
+ * block covers every mode. Positioning this partial's `@use` line immediately
859
+ * after `styles/tokens` (see `styles.scss`) is a readability convention, not a
860
+ * cascade constraint: CSS custom properties resolve at computed-value time, so
861
+ * definition order relative to consumption order does not matter here, unlike
862
+ * `_tokens.scss`'s internal prefers-color-scheme / [data-theme] ordering, which
863
+ * IS load-bearing.
864
+ */
865
+ :where(:root) {
866
+ /* Spacing — 4px ramp. Values mirror frontend/web's --sp-* verbatim. */
867
+ --w6w-sp-1: 4px;
868
+ --w6w-sp-2: 8px;
869
+ --w6w-sp-3: 12px;
870
+ --w6w-sp-4: 16px;
871
+ --w6w-sp-5: 20px;
872
+ --w6w-sp-6: 24px;
873
+ --w6w-sp-8: 32px;
874
+ --w6w-sp-10: 40px;
875
+ --w6w-sp-12: 48px;
876
+ --w6w-sp-16: 64px;
877
+ --w6w-sp-20: 80px;
878
+ /* Half-steps — ui-only, see the decision above. */
879
+ --w6w-sp-0-5: 2px;
880
+ --w6w-sp-1-5: 6px;
881
+ --w6w-sp-2-5: 10px;
882
+ /* Type ramp — rem, role names mirror frontend/web's --fs-*. */
883
+ --w6w-fs-xs: 0.75rem;
884
+ --w6w-fs-sm: 0.875rem;
885
+ --w6w-fs-base: 1rem;
886
+ --w6w-fs-lg: 1.125rem;
887
+ --w6w-fs-xl: 1.25rem;
888
+ --w6w-fs-2xl: 1.5rem;
889
+ --w6w-fs-3xl: 2rem;
890
+ --w6w-fw-regular: 400;
891
+ --w6w-fw-medium: 500;
892
+ --w6w-fw-semibold: 600;
893
+ --w6w-fw-bold: 700;
894
+ --w6w-lh-tight: 1.2;
895
+ --w6w-lh-normal: 1.5;
896
+ --w6w-lh-relaxed: 1.7;
897
+ --w6w-font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
898
+ --w6w-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
899
+ }
900
+
901
+ /*
902
+ * Breakpoints — Sass variables, NOT CSS custom properties.
903
+ *
904
+ * `@media (min-width: var(--w6w-bp-md))` is invalid in every browser: a media
905
+ * query condition cannot read a custom property. So these live as Sass
906
+ * variables instead, resolved before the browser ever sees the stylesheet.
907
+ * `!default` lets a Sass consumer override one:
908
+ *
909
+ * @use "@w6w/ui/styles/scale" with ($w6w-bp-md: 960px);
910
+ *
911
+ * `ui` itself consumes none of these — it deliberately ships no layout media
912
+ * queries (the host owns page layout; `ui` owns component internals). They
913
+ * exist purely so a consumer stops inventing its own breakpoint values (seven
914
+ * uncoordinated ones exist across the four repos today).
915
+ */
916
+ /* Layout helpers */
917
+ .w6w-stack {
918
+ display: flex;
919
+ flex-direction: column;
920
+ gap: var(--w6w-sp-2-5);
921
+ }
922
+
923
+ /* Form fields */
924
+ .w6w-field {
925
+ display: flex;
926
+ flex-direction: column;
927
+ gap: var(--w6w-sp-1);
928
+ color: var(--w6w-muted);
929
+ font-size: 0.9em;
930
+ }
931
+
932
+ .w6w-field > span {
933
+ color: var(--w6w-muted);
934
+ }
935
+
936
+ .w6w-field input[type=text],
937
+ .w6w-field input[type=password],
938
+ .w6w-field input[type=number],
939
+ .w6w-field select,
940
+ .w6w-field textarea {
941
+ background: var(--w6w-panel-2);
942
+ border: 1px solid var(--w6w-border);
943
+ border-radius: 8px;
944
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
945
+ color: var(--w6w-text);
946
+ font-size: var(--w6w-fs-sm);
947
+ font-family: inherit;
948
+ }
949
+
950
+ /* Native popups: the list background follows `color-scheme` (declared with the
951
+ tokens), so give the options token colours too — never one theme's text on
952
+ the other theme's background. */
953
+ .w6w-field option,
954
+ .w6w-field optgroup,
955
+ select option,
956
+ select optgroup {
957
+ color: var(--w6w-text);
958
+ background-color: var(--w6w-panel);
959
+ }
960
+
961
+ .w6w-field input:focus,
962
+ .w6w-field select:focus,
963
+ .w6w-field textarea:focus {
964
+ outline: none;
965
+ border-color: var(--w6w-accent);
966
+ }
967
+
968
+ /* Credential inputs are plain text fields (never type="password", which would
969
+ trigger the browser's save-password prompt + suggestions). Mask them visually
970
+ so a secret still reads as dots. WebKit/Blink support text-security; other
971
+ engines fall back to plain text, which is acceptable for a credential. */
972
+ .w6w-secret-input {
973
+ -webkit-text-security: disc;
974
+ text-security: disc;
975
+ }
976
+
977
+ .w6w-hint {
978
+ color: var(--w6w-muted);
979
+ font-size: 0.8em;
980
+ }
981
+
982
+ /* Side-by-side fields: params sharing a `row` id lay out horizontally. */
983
+ .w6w-field-row {
984
+ display: flex;
985
+ flex-wrap: wrap;
986
+ gap: var(--w6w-sp-2-5);
987
+ }
988
+
989
+ .w6w-field-row > .w6w-field {
990
+ flex: 1 1 140px;
991
+ min-width: 0;
992
+ }
993
+
994
+ /* Array control: a row per item + an add button. */
995
+ .w6w-array {
996
+ display: flex;
997
+ flex-direction: column;
998
+ gap: var(--w6w-sp-1-5);
999
+ align-items: flex-start;
1000
+ }
1001
+
1002
+ .w6w-array-row {
1003
+ display: flex;
1004
+ align-items: center;
1005
+ gap: var(--w6w-sp-1-5);
1006
+ width: 100%;
1007
+ }
1008
+
1009
+ .w6w-array-cells {
1010
+ display: flex;
1011
+ flex-wrap: wrap;
1012
+ gap: var(--w6w-sp-1-5);
1013
+ flex: 1;
1014
+ min-width: 0;
1015
+ }
1016
+
1017
+ .w6w-array-cells > .w6w-array-input {
1018
+ flex: 1 1 120px;
1019
+ }
1020
+
1021
+ /* An object-item array row's boolean cell: `<label><checkbox><span>`. It is a
1022
+ `<label>`, so `margin: 0` is load-bearing for the same reason it is on
1023
+ `.w6w-fx-inline-label` further down — a host page layers an unscoped
1024
+ `label { margin-bottom: 10px }` after this file (studio's does, at
1025
+ `studio/src/styles.css:499`) and owns any property this block leaves
1026
+ undeclared. `.w6w-array-cells` is a flex row at the default `align-items:
1027
+ stretch`, so a bottom margin does not translate this cell, it SHORTENS its
1028
+ stretched box — and because this block centres its own contents, the checkbox
1029
+ and its caption then ride upward off the row's centre line. Measured before
1030
+ this declaration existed, in [ui+studio]: checkbox cy 792 against the sibling
1031
+ text cell's 799 — 7.0px out, while a ui-only render showed 0.0px. */
1032
+ .w6w-array-check {
1033
+ display: inline-flex;
1034
+ align-items: center;
1035
+ gap: var(--w6w-sp-1-5);
1036
+ flex: 0 0 auto;
1037
+ margin: 0;
1038
+ font-size: 13px;
1039
+ color: var(--w6w-text);
1040
+ white-space: nowrap;
1041
+ cursor: pointer;
1042
+ }
1043
+
1044
+ /* `> input[type="checkbox"]`, not `input`: a bare `.w6w-array-check input` is
1045
+ (0,1,1), which TIES with a host `input[type="checkbox"] { margin-right: 6px }`
1046
+ (studio/src/styles.css:785) and then loses on source order — measured
1047
+ `margin-right: 6px` reaching this checkbox with the `margin: 0` below already
1048
+ present. The compound form is (0,2,1) and wins outright. Same shape as
1049
+ `.w6w-fx-inline > input[type="checkbox"]`, which is why that one was never
1050
+ affected. */
1051
+ .w6w-array-check > input[type=checkbox] {
1052
+ margin: 0;
1053
+ }
1054
+
1055
+ .w6w-field .w6w-array-input {
1056
+ flex: 1;
1057
+ min-width: 0;
1058
+ /* Load-bearing, same class as `.w6w-array-check` above: a host
1059
+ `input[type="text"] { margin-top: 4px }` otherwise lands here and drops this
1060
+ cell 2px below its own row's checkbox cell. Measured in [ui+studio]:
1061
+ checkbox cy 797 against this input's 799. This selector is (0,2,0) and beats
1062
+ the host's (0,1,1). */
1063
+ margin: 0;
1064
+ background: var(--w6w-panel-2);
1065
+ border: 1px solid var(--w6w-border);
1066
+ border-radius: 8px;
1067
+ padding: var(--w6w-sp-1-5) var(--w6w-sp-2);
1068
+ color: var(--w6w-text);
1069
+ font-size: var(--w6w-fs-sm);
1070
+ font-family: inherit;
1071
+ }
1072
+
1073
+ .w6w-field .w6w-array-input:focus {
1074
+ outline: none;
1075
+ border-color: var(--w6w-accent);
1076
+ }
1077
+
1078
+ .w6w-array-x {
1079
+ display: inline-flex;
1080
+ align-items: center;
1081
+ justify-content: center;
1082
+ width: 26px;
1083
+ height: 26px;
1084
+ flex-shrink: 0;
1085
+ border: 1px solid var(--w6w-border);
1086
+ border-radius: 8px;
1087
+ background: transparent;
1088
+ color: var(--w6w-muted);
1089
+ font-size: var(--w6w-fs-base);
1090
+ line-height: 1;
1091
+ cursor: pointer;
1092
+ }
1093
+
1094
+ .w6w-array-x:hover {
1095
+ background: var(--w6w-panel-2);
1096
+ color: var(--w6w-text);
1097
+ }
1098
+
1099
+ .w6w-array-add {
1100
+ margin-top: var(--w6w-sp-0-5);
1101
+ }
1102
+
1103
+ /* Material-style multi-select: chips live INSIDE a single input-like box,
1104
+ followed by the dropdown which reads as the trailing placeholder. */
1105
+ .w6w-multiselect {
1106
+ display: flex;
1107
+ flex-wrap: wrap;
1108
+ align-items: center;
1109
+ gap: var(--w6w-sp-1-5);
1110
+ background: var(--w6w-panel-2);
1111
+ border: 1px solid var(--w6w-border);
1112
+ border-radius: 8px;
1113
+ padding: 5px var(--w6w-sp-2);
1114
+ min-height: 38px;
1115
+ }
1116
+
1117
+ .w6w-multiselect:focus-within {
1118
+ border-color: var(--w6w-accent);
1119
+ }
1120
+
1121
+ .w6w-multiselect.is-readonly {
1122
+ opacity: 0.7;
1123
+ }
1124
+
1125
+ /* The add-dropdown blends into the box (no border/background) — compound
1126
+ selector so it beats the global `.w6w-field select` styling. */
1127
+ .w6w-field .w6w-multiselect-add,
1128
+ .w6w-multiselect .w6w-multiselect-add {
1129
+ flex: 1 1 90px;
1130
+ min-width: 90px;
1131
+ border: none;
1132
+ background: transparent;
1133
+ color: var(--w6w-muted);
1134
+ font-size: var(--w6w-fs-sm);
1135
+ font-family: inherit;
1136
+ padding: var(--w6w-sp-0-5) 0;
1137
+ outline: none;
1138
+ cursor: pointer;
1139
+ }
1140
+
1141
+ .w6w-multiselect .w6w-multiselect-add:disabled {
1142
+ cursor: default;
1143
+ opacity: 0.6;
1144
+ }
1145
+
1146
+ /* A selected value, rendered as a filled chip inside the box. */
1147
+ .w6w-chip {
1148
+ display: inline-flex;
1149
+ align-items: center;
1150
+ gap: var(--w6w-sp-1);
1151
+ background: var(--w6w-panel);
1152
+ border: 1px solid var(--w6w-border);
1153
+ border-radius: 6px;
1154
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-1) var(--w6w-sp-0-5) var(--w6w-sp-2);
1155
+ font-size: 13px;
1156
+ line-height: 1.4;
1157
+ color: var(--w6w-text);
1158
+ }
1159
+
1160
+ .w6w-chip-x {
1161
+ display: inline-flex;
1162
+ align-items: center;
1163
+ justify-content: center;
1164
+ width: 16px;
1165
+ height: 16px;
1166
+ border: none;
1167
+ border-radius: 4px;
1168
+ background: transparent;
1169
+ color: var(--w6w-muted);
1170
+ font-size: var(--w6w-fs-sm);
1171
+ line-height: 1;
1172
+ cursor: pointer;
1173
+ }
1174
+
1175
+ .w6w-chip-x:hover {
1176
+ background: var(--w6w-border);
1177
+ color: var(--w6w-text);
1178
+ }
1179
+
1180
+ /* Incoming state (WorkflowFlowEditor · `IncomingStateField`, mounted by BOTH the
1181
+ step editor's Test tab and the canvas ▶ Run collect form). The raw JSON box is
1182
+ an OVERRIDE, so it sits behind a disclosure instead of being the first thing
1183
+ the tab shows — the seed chips above it stay in plain sight. */
1184
+ .w6w-state-override {
1185
+ border: 1px solid var(--w6w-border);
1186
+ border-radius: 6px;
1187
+ padding: var(--w6w-sp-1-5) var(--w6w-sp-2);
1188
+ }
1189
+
1190
+ .w6w-state-override > summary {
1191
+ cursor: pointer;
1192
+ color: var(--w6w-muted);
1193
+ font-size: 0.85em;
1194
+ }
1195
+
1196
+ /* Breathing room only once it is open: closed, the summary IS the whole box. */
1197
+ .w6w-state-override[open] > summary {
1198
+ margin-bottom: var(--w6w-sp-1-5);
1199
+ }
1200
+
1201
+ .w6w-state-override > .w6w-hint {
1202
+ display: block;
1203
+ margin-top: var(--w6w-sp-1);
1204
+ }
1205
+
1206
+ /* Incoming-state seed picker: upstream steps' saved tests offered as one-click
1207
+ seeds for the step's incoming state (WorkflowFlowEditor · StepEditModal). */
1208
+ .w6w-seed-picker {
1209
+ display: flex;
1210
+ flex-direction: column;
1211
+ gap: var(--w6w-sp-1);
1212
+ margin-bottom: var(--w6w-sp-1-5);
1213
+ }
1214
+
1215
+ .w6w-seed-chips {
1216
+ display: flex;
1217
+ flex-wrap: wrap;
1218
+ gap: var(--w6w-sp-1-5);
1219
+ }
1220
+
1221
+ .w6w-seed-chip {
1222
+ cursor: pointer;
1223
+ }
1224
+
1225
+ .w6w-seed-chip:hover:not(:disabled) {
1226
+ border-color: var(--w6w-text);
1227
+ }
1228
+
1229
+ .w6w-seed-chip:disabled {
1230
+ cursor: default;
1231
+ opacity: 0.6;
1232
+ }
1233
+
1234
+ /* Expression input: a field whose contents are inline SEGMENTS — plain text
1235
+ plus var/secret/expr chips, edited in place (contentEditable, like a Notion /
1236
+ Zapier block field). A small toolbar carries the Insert menu + the {{ }} (fx)
1237
+ mode toggle. Theme-aware via the shared tokens. */
1238
+ .w6w-expr {
1239
+ display: flex;
1240
+ flex-direction: column;
1241
+ gap: var(--w6w-sp-1-5);
1242
+ }
1243
+
1244
+ /* Inline expression field: ONE input-like box holding the editable surface plus
1245
+ a ƒx button that reads as a trailing decoration INSIDE it — the same idiom as
1246
+ `.w6w-multiselect` above and `.w6w-expr-sealed` below. The box lives here so
1247
+ the standalone uses still render as a field (the `secret` param path and the
1248
+ public ExpressionInput export). When this rides inside a `.w6w-fx-wrap`, that
1249
+ wrapper is the box and this is flattened — see `.w6w-fx-wrap` below. */
1250
+ .w6w-expr-field {
1251
+ display: flex;
1252
+ align-items: center;
1253
+ gap: var(--w6w-sp-1);
1254
+ background: var(--w6w-panel-2);
1255
+ border: 1px solid var(--w6w-border);
1256
+ border-radius: 8px;
1257
+ padding: var(--w6w-sp-1) var(--w6w-sp-2);
1258
+ min-height: 38px;
1259
+ }
1260
+
1261
+ .w6w-expr-field:focus-within {
1262
+ border-color: var(--w6w-accent);
1263
+ }
1264
+
1265
+ .w6w-expr-field .w6w-expr-editor {
1266
+ flex: 1;
1267
+ min-width: 0;
1268
+ }
1269
+
1270
+ /* The ƒx rides inside the box, so it draws no chrome of its own — only its
1271
+ colour states remain. */
1272
+ .w6w-expr-field .w6w-expr-edit {
1273
+ border: none;
1274
+ background: transparent;
1275
+ }
1276
+
1277
+ .w6w-expr-edit {
1278
+ flex-shrink: 0;
1279
+ color: var(--w6w-muted);
1280
+ font-family: var(--w6w-font-mono);
1281
+ font-size: 13px;
1282
+ line-height: 1;
1283
+ padding: var(--w6w-sp-1) var(--w6w-sp-1-5);
1284
+ cursor: pointer;
1285
+ }
1286
+
1287
+ .w6w-expr-edit:hover {
1288
+ color: var(--w6w-accent);
1289
+ }
1290
+
1291
+ /* Sealed at-rest secret (masked chip + Replace). */
1292
+ .w6w-expr-sealed {
1293
+ display: flex;
1294
+ align-items: center;
1295
+ gap: var(--w6w-sp-1-5);
1296
+ background: var(--w6w-panel-2);
1297
+ border: 1px solid var(--w6w-border);
1298
+ border-radius: 8px;
1299
+ padding: 5px var(--w6w-sp-2);
1300
+ min-height: 38px;
1301
+ }
1302
+
1303
+ .w6w-expr-sealed.is-readonly {
1304
+ opacity: 0.7;
1305
+ }
1306
+
1307
+ /* Expression editor modal: left data sources | right (editor over preview). */
1308
+ .w6w-exprmodal {
1309
+ flex: 1;
1310
+ min-height: 0;
1311
+ display: grid;
1312
+ grid-template-columns: 260px 1fr;
1313
+ gap: 14px;
1314
+ }
1315
+
1316
+ .w6w-exprmodal-sources {
1317
+ overflow: auto;
1318
+ border: 1px solid var(--w6w-border);
1319
+ border-radius: 8px;
1320
+ background: var(--w6w-panel-2);
1321
+ padding: var(--w6w-sp-2-5);
1322
+ display: flex;
1323
+ flex-direction: column;
1324
+ gap: 14px;
1325
+ }
1326
+
1327
+ .w6w-exprmodal-group {
1328
+ display: flex;
1329
+ flex-direction: column;
1330
+ gap: 3px;
1331
+ }
1332
+
1333
+ .w6w-exprmodal-group-label {
1334
+ font-size: 0.72em;
1335
+ font-weight: var(--w6w-fw-semibold);
1336
+ letter-spacing: 0.04em;
1337
+ text-transform: uppercase;
1338
+ color: var(--w6w-muted);
1339
+ margin-bottom: var(--w6w-sp-0-5);
1340
+ }
1341
+
1342
+ .w6w-exprmodal-source {
1343
+ display: flex;
1344
+ align-items: center;
1345
+ gap: var(--w6w-sp-1-5);
1346
+ text-align: left;
1347
+ background: transparent;
1348
+ border: 1px solid transparent;
1349
+ border-radius: 6px;
1350
+ padding: 5px var(--w6w-sp-2);
1351
+ color: var(--w6w-text);
1352
+ font-size: 13px;
1353
+ font-family: inherit;
1354
+ cursor: pointer;
1355
+ }
1356
+
1357
+ .w6w-exprmodal-source:hover {
1358
+ background: var(--w6w-panel);
1359
+ border-color: var(--w6w-border);
1360
+ }
1361
+
1362
+ .w6w-exprmodal-source.w6w-expr-chip-var {
1363
+ color: var(--w6w-accent);
1364
+ }
1365
+
1366
+ .w6w-exprmodal-source.w6w-expr-chip-secret {
1367
+ color: var(--w6w-danger);
1368
+ }
1369
+
1370
+ .w6w-exprmodal-source-label {
1371
+ overflow: hidden;
1372
+ text-overflow: ellipsis;
1373
+ white-space: nowrap;
1374
+ }
1375
+
1376
+ /* A step's per-field sources, nested under its whole-output source, so three
1377
+ upstream steps x four fields reads as a tree rather than a flat wall. */
1378
+ .w6w-exprmodal-subsources {
1379
+ display: flex;
1380
+ flex-direction: column;
1381
+ gap: 3px;
1382
+ margin-left: 9px;
1383
+ padding-left: var(--w6w-sp-2);
1384
+ border-left: 1px solid var(--w6w-border);
1385
+ }
1386
+
1387
+ /* One document-field row: the primary insert source (flex: 1, so it still
1388
+ truncates a long key the same way a bare `.w6w-exprmodal-source` does) plus
1389
+ the second, visible action (D-P1) that inserts the SAME ref as a `render`
1390
+ part — so the two read as one entry, not two unrelated controls. */
1391
+ .w6w-exprmodal-subsource-row {
1392
+ display: flex;
1393
+ align-items: center;
1394
+ gap: var(--w6w-sp-1);
1395
+ }
1396
+
1397
+ .w6w-exprmodal-subsource-row .w6w-exprmodal-source {
1398
+ flex: 1;
1399
+ min-width: 0;
1400
+ }
1401
+
1402
+ .w6w-exprmodal-render-btn {
1403
+ display: inline-flex;
1404
+ flex: none;
1405
+ align-items: center;
1406
+ justify-content: center;
1407
+ width: 20px;
1408
+ height: 20px;
1409
+ border: 1px solid var(--w6w-border);
1410
+ border-radius: 6px;
1411
+ background: transparent;
1412
+ color: var(--w6w-success);
1413
+ opacity: 0.75;
1414
+ font-size: var(--w6w-fs-xs);
1415
+ cursor: pointer;
1416
+ }
1417
+
1418
+ .w6w-exprmodal-render-btn:hover {
1419
+ opacity: 1;
1420
+ background: var(--w6w-panel);
1421
+ border-color: var(--w6w-success);
1422
+ }
1423
+
1424
+ /* A rail source with surviving child rows (a multi-field document, a step
1425
+ with declared outputs) — the "whole source" button plus the expand/collapse
1426
+ toggle, as siblings on one row. Mirrors `.w6w-exprmodal-subsource-row`'s
1427
+ flex row + child-fills-width shape (`:74-82` above), kept as its own class
1428
+ because the two are counted independently by the test suite. */
1429
+ .w6w-exprmodal-source-row {
1430
+ display: flex;
1431
+ align-items: center;
1432
+ gap: var(--w6w-sp-1);
1433
+ }
1434
+
1435
+ .w6w-exprmodal-source-row .w6w-exprmodal-source {
1436
+ flex: 1;
1437
+ min-width: 0;
1438
+ }
1439
+
1440
+ /* A step row's disclosure caret, sitting where the chip's `▸` sigil was —
1441
+ that arrow reads as "expand", so on a row with fields it does exactly that
1442
+ (the chip beside it still inserts the whole output). */
1443
+ .w6w-exprmodal-caret-btn {
1444
+ display: inline-flex;
1445
+ flex: none;
1446
+ align-items: center;
1447
+ justify-content: center;
1448
+ width: 20px;
1449
+ height: 20px;
1450
+ padding: 0;
1451
+ border: 0;
1452
+ border-radius: 6px;
1453
+ background: transparent;
1454
+ color: var(--w6w-accent);
1455
+ font-size: var(--w6w-fs-xs);
1456
+ cursor: pointer;
1457
+ }
1458
+
1459
+ .w6w-exprmodal-caret-btn:hover {
1460
+ background: var(--w6w-panel);
1461
+ }
1462
+
1463
+ /* The "N fields" toggle with the Metadata toggle stacked under it, so both
1464
+ disclosures share one left edge on a step row. */
1465
+ .w6w-exprmodal-toggle-stack {
1466
+ display: flex;
1467
+ flex: none;
1468
+ flex-direction: column;
1469
+ align-items: flex-start;
1470
+ gap: 3px; /* lint-tokens-allow: 3px sits between no two steps of the spacing scale; tight stack of the fields and Metadata toggles */
1471
+ }
1472
+
1473
+ /* A step with no fields: its Metadata toggle sits at the leaf indentation of
1474
+ `.w6w-exprmodal-subsources` (margin 9px + 1px border + sp-2 padding). */
1475
+ .w6w-exprmodal-meta-row {
1476
+ margin-left: calc(10px + var(--w6w-sp-2)); /* lint-tokens-allow: 9px margin + 1px border of .w6w-exprmodal-subsources */
1477
+ }
1478
+
1479
+ /* Same 20px-high chrome as `.w6w-exprmodal-render-btn` above — the sizing
1480
+ precedent for a dense rail control — but a separate class: this toggles
1481
+ the `.w6w-exprmodal-subsources` block below it, the render-btn inserts a
1482
+ `render` part. It carries a "N fields" label, not a bare chevron, so it is
1483
+ auto-width (a lone 20px chevron went unnoticed beside the chip). */
1484
+ .w6w-exprmodal-toggle-btn {
1485
+ display: inline-flex;
1486
+ flex: none;
1487
+ align-items: center;
1488
+ justify-content: center;
1489
+ gap: var(--w6w-sp-1);
1490
+ height: 20px;
1491
+ padding: 0 6px; /* lint-tokens-allow: no 6px step on the spacing scale; between sp-1 and sp-2 keeps the 20px pill's label centred */
1492
+ white-space: nowrap;
1493
+ border: 1px solid var(--w6w-border);
1494
+ border-radius: 6px;
1495
+ background: transparent;
1496
+ color: var(--w6w-muted);
1497
+ font-size: var(--w6w-fs-xs);
1498
+ cursor: pointer;
1499
+ }
1500
+
1501
+ .w6w-exprmodal-toggle-btn:hover {
1502
+ background: var(--w6w-panel);
1503
+ border-color: var(--w6w-accent);
1504
+ color: var(--w6w-text);
1505
+ }
1506
+
1507
+ .w6w-exprmodal-main {
1508
+ min-width: 0;
1509
+ min-height: 0;
1510
+ display: flex;
1511
+ flex-direction: column;
1512
+ gap: var(--w6w-sp-3);
1513
+ }
1514
+
1515
+ .w6w-exprmodal-editor {
1516
+ display: flex;
1517
+ flex-direction: column;
1518
+ gap: var(--w6w-sp-1-5);
1519
+ min-height: 0;
1520
+ flex: 1 1 auto;
1521
+ }
1522
+
1523
+ .w6w-exprmodal-pane-label {
1524
+ font-size: 13px;
1525
+ font-weight: var(--w6w-fw-semibold);
1526
+ color: var(--w6w-text);
1527
+ }
1528
+
1529
+ /* The WYSIWYG chip editor: tags flow inline with text (colored + spaced). */
1530
+ .w6w-exprmodal-chips {
1531
+ flex: 1;
1532
+ min-height: 180px;
1533
+ overflow: auto;
1534
+ background: var(--w6w-panel-2);
1535
+ border: 1px solid var(--w6w-border);
1536
+ border-radius: 8px;
1537
+ padding: var(--w6w-sp-2-5) var(--w6w-sp-3);
1538
+ color: var(--w6w-text);
1539
+ font-size: var(--w6w-fs-sm);
1540
+ line-height: 2.1;
1541
+ outline: none;
1542
+ white-space: pre-wrap;
1543
+ word-break: break-word;
1544
+ }
1545
+
1546
+ .w6w-exprmodal-chips:focus {
1547
+ border-color: var(--w6w-accent);
1548
+ }
1549
+
1550
+ .w6w-exprmodal-chips.is-empty::before {
1551
+ content: attr(data-placeholder);
1552
+ color: var(--w6w-muted);
1553
+ pointer-events: none;
1554
+ }
1555
+
1556
+ .w6w-exprmodal-chips.is-masked {
1557
+ -webkit-text-security: disc;
1558
+ text-security: disc;
1559
+ }
1560
+
1561
+ .w6w-exprmodal-chips.is-masked .w6w-expr-chip {
1562
+ -webkit-text-security: none;
1563
+ text-security: none;
1564
+ }
1565
+
1566
+ .w6w-exprmodal-preview {
1567
+ display: flex;
1568
+ flex-direction: column;
1569
+ gap: var(--w6w-sp-1-5);
1570
+ }
1571
+
1572
+ /* The Result pane's own share of `.w6w-exprmodal-main`'s flex column — the
1573
+ SHARE lives on this wrapper, not on the `<pre>` below: the `<pre>`'s parent
1574
+ is this wrapper (`.w6w-exprmodal-preview`, a flex column), not `-main`
1575
+ itself, so a `flex-basis` on the `<pre>` would be a share of a
1576
+ content-sized box and do nothing. 42% keeps the pane in the 30-50% band and
1577
+ the chips editor above the 25% floor at 1440x900, 1280x720 and 1440x1200 —
1578
+ measured in real Chromium, `test/expr-template/expr-template-guards.test.cjs`
1579
+ T-height; see the project's result for the numbers. */
1580
+ .w6w-exprmodal-result-pane {
1581
+ flex: 0 0 42%;
1582
+ min-height: 0;
1583
+ }
1584
+
1585
+ /* The pane's 42% share above is a flex-basis on `.w6w-exprmodal-result-pane`
1586
+ itself (a column flex container) — it must still reach the `<pre>` now that
1587
+ `Copyable` wraps it in a `.w6w-copyable` div. That wrapper is the actual
1588
+ column child now, so it needs the grow/shrink share instead. */
1589
+ .w6w-exprmodal-result-pane > .w6w-copyable {
1590
+ flex: 1 1 auto;
1591
+ min-height: 0;
1592
+ }
1593
+
1594
+ .w6w-exprmodal-result {
1595
+ flex: 1 1 auto;
1596
+ /* `.w6w-copyable` is a ROW flex container (`_copyable.scss:5`), and its
1597
+ `:has(> pre)` arm (`_copyable.scss:19-21`) sets `align-items: flex-start`
1598
+ for CodeBlock's snippet case, where the box is sized TO the pre's
1599
+ content. Here it's the other way around — the wrapper is grown to the
1600
+ pane's share by the rule above, and the `<pre>` must fill IT. `flex: 1 1
1601
+ auto`'s grow/shrink applies to the row's main axis (width) and does
1602
+ nothing for height there, so the cross-axis stretch has to be pinned
1603
+ directly on this item, overriding the container's `flex-start`. */
1604
+ align-self: stretch;
1605
+ min-height: 0;
1606
+ max-height: none;
1607
+ overflow: auto;
1608
+ margin: 0;
1609
+ background: var(--w6w-panel-2);
1610
+ border: 1px solid var(--w6w-border);
1611
+ border-radius: 8px;
1612
+ padding: var(--w6w-sp-2-5) var(--w6w-sp-3);
1613
+ /* `_copyable.scss:105-107`'s corner-button clearance is scoped to
1614
+ `.w6w-code-block` and does not apply here — same 40px value (icon box +
1615
+ inset + gap) so the overlaid copy button never sits on the first line. */
1616
+ padding-right: var(--w6w-sp-10);
1617
+ color: var(--w6w-muted);
1618
+ font-family: var(--w6w-font-mono);
1619
+ font-size: 12.5px;
1620
+ line-height: 1.6;
1621
+ white-space: pre-wrap;
1622
+ word-break: break-word;
1623
+ }
1624
+
1625
+ .w6w-exprmodal-actions {
1626
+ display: flex;
1627
+ gap: var(--w6w-sp-2);
1628
+ }
1629
+
1630
+ .w6w-expr-toolbar {
1631
+ display: flex;
1632
+ align-items: center;
1633
+ gap: var(--w6w-sp-2);
1634
+ }
1635
+
1636
+ /* fx toggle — an end decoration INSIDE the field, not a button beside it. The
1637
+ WRAPPER owns the input chrome (background/border/radius/min-height and the
1638
+ focus ring) and every widget it holds is flattened below, so the value and its
1639
+ ƒx affordance read as ONE box. Mirrors `.w6w-multiselect` above. */
1640
+ .w6w-fx-wrap {
1641
+ display: flex;
1642
+ align-items: center;
1643
+ gap: var(--w6w-sp-1);
1644
+ background: var(--w6w-panel-2);
1645
+ border: 1px solid var(--w6w-border);
1646
+ border-radius: 8px;
1647
+ padding: var(--w6w-sp-1) var(--w6w-sp-2);
1648
+ min-height: 38px;
1649
+ }
1650
+
1651
+ /* A textarea is taller than the ƒx, so there the decoration rides at the top. */
1652
+ .w6w-fx-wrap:has(> textarea) {
1653
+ align-items: flex-start;
1654
+ }
1655
+
1656
+ /* Focus reads on the box now that the inner widget has no border of its own. */
1657
+ .w6w-fx-wrap:focus-within {
1658
+ border-color: var(--w6w-accent);
1659
+ }
1660
+
1661
+ .w6w-fx-wrap > input,
1662
+ .w6w-fx-wrap > select,
1663
+ .w6w-fx-wrap > textarea,
1664
+ .w6w-fx-wrap > .w6w-multiselect,
1665
+ .w6w-fx-wrap > .w6w-expr {
1666
+ flex: 1 1 auto;
1667
+ min-width: 0;
1668
+ }
1669
+
1670
+ /* The widget inside the wrapper carries NO chrome of its own. COMPOUND
1671
+ selectors, because the global `.w6w-field input|select|textarea` rule is
1672
+ (0,2,1) and a bare `.w6w-fx-wrap > input` at (0,1,1) would lose to it — the
1673
+ same trap documented on `.w6w-multiselect-add` above. `.w6w-datavar-row
1674
+ input|select` is the second rule to beat, for the `bare` vars-row cell. */
1675
+ .w6w-field .w6w-fx-wrap input[type=text],
1676
+ .w6w-field .w6w-fx-wrap input[type=password],
1677
+ .w6w-field .w6w-fx-wrap input[type=number],
1678
+ .w6w-field .w6w-fx-wrap select,
1679
+ .w6w-field .w6w-fx-wrap textarea,
1680
+ .w6w-datavar-row .w6w-fx-wrap input,
1681
+ .w6w-datavar-row .w6w-fx-wrap select,
1682
+ .w6w-fx-wrap input[type=text],
1683
+ .w6w-fx-wrap input[type=password],
1684
+ .w6w-fx-wrap input[type=number],
1685
+ .w6w-fx-wrap select,
1686
+ .w6w-fx-wrap textarea {
1687
+ border: none;
1688
+ background: transparent;
1689
+ border-radius: 0;
1690
+ padding: var(--w6w-sp-1) var(--w6w-sp-0-5);
1691
+ outline: none;
1692
+ /* EVERY property in this block is load-bearing, for the same reason
1693
+ `.w6w-fx-inline-label`'s `margin: 0` is (see below): a host page layers its
1694
+ own unscoped `input[type="text"], textarea { … }` rule AFTER this file
1695
+ (studio's does, at `studio/src/styles.css:513-533`, `:759-771` and `:788`),
1696
+ so every property this block leaves undeclared is the host's — including
1697
+ inside a box this library draws. The two below were measured leaking, in
1698
+ [ui+studio], before they were declared — and they fix DIFFERENT things:
1699
+
1700
+ margin — ALIGNMENT. A host `margin-top: 4px` in an `align-items:
1701
+ center` row drops the control 2px below the ƒx it shares the
1702
+ row with (string, number and the bare vars cell; select and
1703
+ multiselect escaped only because the host rule does not list
1704
+ them). Rendered: control cy 67 vs ƒx cy 65. On the textarea
1705
+ row — `align-items: flex-start` — the same 4px showed up as
1706
+ the ƒx sitting 4px off the control's first line. `margin: 0`
1707
+ is the whole of that fix.
1708
+
1709
+ min-height — SIZE, and nothing else. Do not re-justify this one as
1710
+ alignment: the counterfactual was rendered (this `margin: 0`
1711
+ kept, `min-height: 0` removed) and the row measures a 0.0px
1712
+ top delta with the host's 80px floor fully in effect. What it
1713
+ fixes is that a host `textarea { min-height: 80px }`, written
1714
+ for that host's own bare textareas, was sizing the INSIDE of
1715
+ a box it cannot see — 80px/94px against the 62px/72px the
1716
+ same markup renders elsewhere, i.e. one of five control types
1717
+ 29% taller than the library drew it. The box is the library's
1718
+ to size; how many lines it holds is `rows` on the control.
1719
+ If a taller body field is ever wanted, it belongs HERE, not
1720
+ in a host element selector.
1721
+
1722
+ Declared, not fought for: this stays at the block's existing specificity and
1723
+ uses no `!important`. A component that is only aligned while the host
1724
+ cooperates is broken. */
1725
+ margin: 0;
1726
+ min-height: 0;
1727
+ }
1728
+
1729
+ /* …and the two widgets that own an input-like box themselves are flattened too,
1730
+ so the wrapper never draws a box around a box. */
1731
+ .w6w-fx-wrap .w6w-expr-field,
1732
+ .w6w-fx-wrap .w6w-multiselect {
1733
+ border: none;
1734
+ background: transparent;
1735
+ border-radius: 0;
1736
+ padding: 0;
1737
+ min-height: 0;
1738
+ }
1739
+
1740
+ /* The OTHER wrapper FxField can render (`variant="inline"`). A control that does
1741
+ not FILL a text-field box — today only a `boolean`'s ~13px checkbox — opts out
1742
+ of the box at the component seam and lands here instead, so `.w6w-fx-wrap`'s
1743
+ chrome above is never applied and nothing has to be clawed back with a more
1744
+ specific selector. Layout is the review's sketch: <checkbox> <ƒx> <label>,
1745
+ one row, no border/background/padding/min-height of its own. */
1746
+ .w6w-fx-inline {
1747
+ display: flex;
1748
+ align-items: center;
1749
+ gap: var(--w6w-sp-2);
1750
+ }
1751
+
1752
+ .w6w-fx-inline > input[type=checkbox] {
1753
+ flex: 0 0 auto;
1754
+ margin: 0;
1755
+ }
1756
+
1757
+ /* The label reads as the control's own text, next to it (mirrors
1758
+ `.w6w-array-check` above, the existing inline-checkbox idiom).
1759
+ `margin: 0` is LOAD-BEARING, not tidiness: a host page may carry an unscoped
1760
+ top-level `label { … margin-bottom: 10px }` (studio's own sheet does, at
1761
+ `studio/src/styles.css:499`), and since it is layered AFTER this file any
1762
+ property this block leaves undeclared is the host's. A bottom margin inside an
1763
+ `align-items: center` row lifts the text off the row's centre line — measured
1764
+ 5px above the checkbox and the ƒx before this declaration existed. Every other
1765
+ element of the row is defended the same way: the checkbox's `margin: 0` above
1766
+ out-specifies a host `input[type="checkbox"] { margin-right: … }` (0,2,1 vs
1767
+ 0,1,1), and the ƒx's chrome is pinned by `.w6w-fx-inline .w6w-expr-fx` below.
1768
+ A component that only lines up when the host sheet cooperates is broken. */
1769
+ .w6w-fx-inline-label {
1770
+ margin: 0;
1771
+ color: var(--w6w-text);
1772
+ font-size: 13px;
1773
+ cursor: pointer;
1774
+ }
1775
+
1776
+ /* The ƒx itself is a decoration inside whichever wrapper holds it: no chrome,
1777
+ colour only. This is where the UA button border/background is removed (the
1778
+ `.w6w-expr-fx` block below declares neither), so the inline wrapper has to be
1779
+ listed here too or its ƒx comes back as a grey rounded button. The label row
1780
+ (`json` / childless `group` params, which have no field box) is a third
1781
+ placement and gets the same chrome-stripping. */
1782
+ .w6w-fx-wrap .w6w-expr-fx,
1783
+ .w6w-fx-inline .w6w-expr-fx,
1784
+ .w6w-field-labelrow .w6w-expr-fx {
1785
+ border: none;
1786
+ background: transparent;
1787
+ }
1788
+
1789
+ .w6w-expr-fx {
1790
+ flex-shrink: 0;
1791
+ color: var(--w6w-muted);
1792
+ font-size: 13px;
1793
+ font-family: var(--w6w-font-mono);
1794
+ line-height: 1;
1795
+ padding: var(--w6w-sp-1) var(--w6w-sp-1-5);
1796
+ cursor: pointer;
1797
+ }
1798
+
1799
+ .w6w-expr-fx:hover {
1800
+ color: var(--w6w-text);
1801
+ }
1802
+
1803
+ .w6w-expr-fx.is-active {
1804
+ color: var(--w6w-accent);
1805
+ }
1806
+
1807
+ /* Copyable — an in-box copy affordance decorating a value-displaying control
1808
+ (an <input>, a <textarea>, or CodeBlock's <pre>). Mirrors `.w6w-fx-wrap`
1809
+ (`_fx.scss:1-88`): the WRAPPER owns the box chrome and every child it holds
1810
+ is flattened below, so the value and the copy affordance read as ONE box. */
1811
+ .w6w-copyable {
1812
+ display: flex;
1813
+ align-items: center;
1814
+ gap: var(--w6w-sp-1);
1815
+ background: var(--w6w-panel-2);
1816
+ border: 1px solid var(--w6w-border);
1817
+ border-radius: 8px;
1818
+ padding: var(--w6w-sp-1) var(--w6w-sp-2);
1819
+ /* Box-family invariant: byte-identical to `.w6w-fx-wrap`'s at `_fx.scss:13` —
1820
+ six rules across four partials share this value (`building-blocks.md` §3
1821
+ row 7); a scale token here would drift the moment that family moves. */
1822
+ min-height: 38px;
1823
+ }
1824
+
1825
+ /* A textarea/pre is taller than the button, so the button rides at the top —
1826
+ mirrors `.w6w-fx-wrap:has(> textarea)` (`_fx.scss:16-18`). */
1827
+ .w6w-copyable:has(> textarea),
1828
+ .w6w-copyable:has(> pre) {
1829
+ align-items: flex-start;
1830
+ }
1831
+
1832
+ /* Focus reads on the box now that the inner control has no border of its own. */
1833
+ .w6w-copyable:focus-within {
1834
+ border-color: var(--w6w-accent);
1835
+ }
1836
+
1837
+ .w6w-copyable > input,
1838
+ .w6w-copyable > textarea,
1839
+ .w6w-copyable > pre {
1840
+ flex: 1 1 auto;
1841
+ min-width: 0;
1842
+ }
1843
+
1844
+ /* The control inside the wrapper carries NO chrome of its own. Mirrors
1845
+ `_fx.scss:36-88`'s selector list and property list WHOLESALE
1846
+ (s/w6w-fx-wrap/w6w-copyable/g) — not re-derived, gated for parity by the
1847
+ project's test plan. See that file for the measured leak evidence (7.0px
1848
+ alignment / 2px / 29% height) each property below defends against; a host
1849
+ page's unscoped `input[type="text"], … textarea { … }` rule (studio's own,
1850
+ layered AFTER this stylesheet) is exactly what these declarations beat. */
1851
+ .w6w-field .w6w-copyable input[type=text],
1852
+ .w6w-field .w6w-copyable input[type=password],
1853
+ .w6w-field .w6w-copyable input[type=number],
1854
+ .w6w-field .w6w-copyable select,
1855
+ .w6w-field .w6w-copyable textarea,
1856
+ .w6w-datavar-row .w6w-copyable input,
1857
+ .w6w-datavar-row .w6w-copyable select,
1858
+ .w6w-copyable input[type=text],
1859
+ .w6w-copyable input[type=password],
1860
+ .w6w-copyable input[type=number],
1861
+ .w6w-copyable select,
1862
+ .w6w-copyable textarea {
1863
+ border: none;
1864
+ background: transparent;
1865
+ border-radius: 0;
1866
+ /* lint-tokens-allow: mirrors _fx.scss:36-88 byte-for-byte, required by the parity gate */
1867
+ padding: 4px 2px;
1868
+ outline: none;
1869
+ margin: 0;
1870
+ min-height: 0;
1871
+ }
1872
+
1873
+ /* The wrapper never draws a box around a box (mirrors `_fx.scss:91-98`). */
1874
+ .w6w-copyable .w6w-code-block {
1875
+ border: none;
1876
+ background: transparent;
1877
+ padding: 0;
1878
+ }
1879
+
1880
+ /* Button chrome extends `.w6w-icon-btn` (`_icons.scss:1-11`) in markup — this
1881
+ class only pins the flex behaviour and the copied state, no re-authored
1882
+ hover/active states. */
1883
+ .w6w-copyable-btn {
1884
+ flex: none;
1885
+ }
1886
+
1887
+ /* ── CodeBlock: the button OVERLAYS the box's top-right corner ──────────────
1888
+ For an <input>/<textarea> the button is a flex sibling, which is right: the
1889
+ value is one line and the control shrinks to leave room for it. A <pre> is
1890
+ different — it is a block of many lines, and a flex sibling would carve a
1891
+ permanent empty column out of the full height of it to seat one 28px button.
1892
+ So here the button leaves the flow and sits over the corner instead.
1893
+
1894
+ Absolute against the WRAPPER, not the <pre>, on purpose: the <pre> is the
1895
+ element that scrolls (`overflow-x: auto`, plus `overflow-y` when `maxHeight`
1896
+ is set), so anchoring to it would slide the button out of view on a wide or
1897
+ tall snippet. Anchored to the wrapper it stays pinned to the visible corner. */
1898
+ .w6w-copyable:has(> pre) {
1899
+ position: relative;
1900
+ }
1901
+
1902
+ .w6w-copyable:has(> pre) > .w6w-copyable-btn {
1903
+ position: absolute;
1904
+ top: var(--w6w-sp-1);
1905
+ right: var(--w6w-sp-1);
1906
+ /* An opaque backdrop, which the flex-sibling case does not need: this button
1907
+ sits ON the code, and `.w6w-icon-btn`'s transparent idle state would let
1908
+ the first line's text read through it. It also restores the hover feedback
1909
+ — `.w6w-icon-btn:hover` paints `--w6w-panel-2`, which is the wrapper's own
1910
+ background here and would otherwise be a no-op. */
1911
+ background: var(--w6w-panel);
1912
+ border-color: var(--w6w-border);
1913
+ }
1914
+
1915
+ /* Keep the first line clear of the button. Deliberately NOT folded into the
1916
+ `.w6w-copyable .w6w-code-block` reset above — that rule mirrors
1917
+ `_fx.scss:91-98` and is held byte-for-byte by the parity gate. 40px = the
1918
+ button's ~28px box + its 4px inset + a 8px gap before the text. */
1919
+ .w6w-copyable:has(> pre) > .w6w-code-block {
1920
+ padding-right: var(--w6w-sp-10);
1921
+ }
1922
+
1923
+ .w6w-copyable-btn.is-copied {
1924
+ /* Mirrors `.w6w-expr-chip-render`'s "confirmed/success" convention exactly
1925
+ (`_chips.scss:38-42`) — the one visual for a copied/success state. */
1926
+ border-color: var(--w6w-success);
1927
+ color: var(--w6w-success);
1928
+ background: color-mix(in srgb, var(--w6w-success) 14%, transparent);
1929
+ }
1930
+
1931
+ /* Scoped visually-hidden live region — not a new general `sr-only` utility
1932
+ (none exists in `ui` to extend; see `Copyable.tsx` for why this, not the
1933
+ button's own accessible name, carries the "Copied" announcement). */
1934
+ .w6w-copyable-status {
1935
+ position: absolute;
1936
+ overflow: hidden;
1937
+ width: 1px;
1938
+ height: 1px;
1939
+ white-space: nowrap;
1940
+ clip-path: inset(50%);
1941
+ border: 0;
1942
+ padding: 0;
1943
+ }
1944
+
1945
+ /* ── Bare modifier: CopyableText (D-7) ───────────────────────────────────
1946
+ A self-contained modifier, NOT a widening of the shared `> input, >
1947
+ textarea, > pre` selector above (`:28-33`) — that list stays untouched.
1948
+ Strips the box chrome the base `.w6w-copyable` rule sets (`:5-17`: border,
1949
+ background, padding, min-height), and separately gives its own `> span`
1950
+ child the SAME flex-and-shrink pair the shared selector gives an
1951
+ `<input>`/`<textarea>`/`<pre>` (`:28-33` below) — a bare `<span>` is not in
1952
+ that list, and without this it silently reproduces the overflow bug
1953
+ `studio/src/pages/VarsPage.tsx:62-79` had to hand-patch. */
1954
+ .w6w-copyable--bare {
1955
+ border: none;
1956
+ background: transparent;
1957
+ padding: 0;
1958
+ min-height: 0;
1959
+ }
1960
+
1961
+ .w6w-copyable--bare > span {
1962
+ flex: 1 1 auto;
1963
+ min-width: 0;
1964
+ }
1965
+
1966
+ /* The raw {{ }} input stands alone, so it keeps an input box of its own. */
1967
+ .w6w-expr-raw {
1968
+ width: 100%;
1969
+ background: var(--w6w-panel-2);
1970
+ border: 1px solid var(--w6w-border);
1971
+ border-radius: 8px;
1972
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
1973
+ min-height: 38px;
1974
+ color: var(--w6w-text);
1975
+ font-family: var(--w6w-font-mono);
1976
+ font-size: 13px;
1977
+ line-height: var(--w6w-lh-relaxed);
1978
+ }
1979
+
1980
+ .w6w-expr-raw:focus {
1981
+ outline: none;
1982
+ border-color: var(--w6w-accent);
1983
+ }
1984
+
1985
+ /* The inline editor surface (one continuous field) sits INSIDE
1986
+ `.w6w-expr-field`'s box, so it draws no chrome — the border, the background
1987
+ and the focus ring all belong to that box. */
1988
+ .w6w-expr-editor {
1989
+ width: 100%;
1990
+ padding: var(--w6w-sp-0-5) 0;
1991
+ color: var(--w6w-text);
1992
+ font-size: var(--w6w-fs-sm);
1993
+ font-family: inherit;
1994
+ line-height: var(--w6w-lh-relaxed);
1995
+ outline: none;
1996
+ white-space: pre-wrap;
1997
+ word-break: break-word;
1998
+ }
1999
+
2000
+ .w6w-expr-editor.is-readonly,
2001
+ .w6w-expr-input.is-readonly {
2002
+ opacity: 0.7;
2003
+ }
2004
+
2005
+ /* Placeholder for the empty contentEditable field. */
2006
+ .w6w-expr-editor.is-empty::before {
2007
+ content: attr(data-placeholder);
2008
+ color: var(--w6w-muted);
2009
+ pointer-events: none;
2010
+ }
2011
+
2012
+ /* Masked (secret) fields: mask typed TEXT as dots; chips are exempt — a chip
2013
+ shows a name/label, never a secret value. */
2014
+ .w6w-expr-editor.is-masked {
2015
+ -webkit-text-security: disc;
2016
+ text-security: disc;
2017
+ }
2018
+
2019
+ .w6w-expr-editor.is-masked .w6w-expr-chip {
2020
+ -webkit-text-security: none;
2021
+ text-security: none;
2022
+ }
2023
+
2024
+ /* Sealed at-rest secret uses a simple flex wrapper (chip + Replace). */
2025
+ .w6w-expr-input {
2026
+ display: flex;
2027
+ flex-wrap: wrap;
2028
+ align-items: center;
2029
+ gap: var(--w6w-sp-1-5);
2030
+ background: var(--w6w-panel-2);
2031
+ border: 1px solid var(--w6w-border);
2032
+ border-radius: 8px;
2033
+ padding: 5px var(--w6w-sp-2);
2034
+ min-height: 38px;
2035
+ }
2036
+
2037
+ /* Chips: a var/secret/expr segment. Distinct look per kind; sits inline in the
2038
+ text flow. */
2039
+ .w6w-expr-chip {
2040
+ display: inline-flex;
2041
+ align-items: center;
2042
+ gap: var(--w6w-sp-1);
2043
+ vertical-align: baseline;
2044
+ box-sizing: border-box;
2045
+ /* Never exceed the field's content box; the label truncates before the chip
2046
+ can overrun the adjacent ƒx button. */
2047
+ max-width: 100%;
2048
+ border: 1px solid var(--w6w-border);
2049
+ border-radius: 6px;
2050
+ padding: 1px var(--w6w-sp-1-5);
2051
+ margin: 0 3px;
2052
+ font-size: 12.5px;
2053
+ line-height: 1.4;
2054
+ font-family: var(--w6w-font-mono);
2055
+ color: var(--w6w-text);
2056
+ background: var(--w6w-panel);
2057
+ white-space: nowrap;
2058
+ user-select: none;
2059
+ }
2060
+
2061
+ .w6w-expr-chip-var {
2062
+ border-color: var(--w6w-accent);
2063
+ color: var(--w6w-accent);
2064
+ background: color-mix(in srgb, var(--w6w-accent) 14%, transparent);
2065
+ }
2066
+
2067
+ .w6w-expr-chip-secret {
2068
+ border-color: var(--w6w-danger);
2069
+ color: var(--w6w-danger);
2070
+ background: color-mix(in srgb, var(--w6w-danger) 14%, transparent);
2071
+ }
2072
+
2073
+ .w6w-expr-chip-expr {
2074
+ border-style: dashed;
2075
+ background: color-mix(in srgb, var(--w6w-text) 8%, transparent);
2076
+ }
2077
+
2078
+ .w6w-expr-chip-render {
2079
+ border-color: var(--w6w-success);
2080
+ color: var(--w6w-success);
2081
+ background: color-mix(in srgb, var(--w6w-success) 14%, transparent);
2082
+ }
2083
+
2084
+ .w6w-expr-chip-sigil {
2085
+ flex: none;
2086
+ font-weight: var(--w6w-fw-semibold);
2087
+ opacity: 0.8;
2088
+ }
2089
+
2090
+ .w6w-expr-chip-label {
2091
+ min-width: 0;
2092
+ overflow: hidden;
2093
+ text-overflow: ellipsis;
2094
+ white-space: nowrap;
2095
+ font-weight: var(--w6w-fw-medium);
2096
+ }
2097
+
2098
+ .w6w-expr-chip-x {
2099
+ display: inline-flex;
2100
+ flex: none;
2101
+ align-items: center;
2102
+ justify-content: center;
2103
+ width: 15px;
2104
+ height: 15px;
2105
+ border-radius: 4px;
2106
+ background: transparent;
2107
+ color: inherit;
2108
+ opacity: 0.6;
2109
+ font-size: var(--w6w-fs-sm);
2110
+ line-height: 1;
2111
+ cursor: pointer;
2112
+ }
2113
+
2114
+ .w6w-expr-chip-x:hover {
2115
+ opacity: 1;
2116
+ background: var(--w6w-border);
2117
+ }
2118
+
2119
+ /* The render affordance: flips a var/render chip in place (see `makeChip`). */
2120
+ .w6w-expr-chip-toggle {
2121
+ display: inline-flex;
2122
+ flex: none;
2123
+ align-items: center;
2124
+ justify-content: center;
2125
+ width: 15px;
2126
+ height: 15px;
2127
+ border-radius: 4px;
2128
+ background: transparent;
2129
+ color: inherit;
2130
+ opacity: 0.6;
2131
+ font-size: var(--w6w-fs-xs);
2132
+ line-height: 1;
2133
+ cursor: pointer;
2134
+ }
2135
+
2136
+ .w6w-expr-chip-toggle:hover {
2137
+ opacity: 1;
2138
+ background: var(--w6w-border);
2139
+ }
2140
+
2141
+ .w6w-expr-replace {
2142
+ border: 1px solid var(--w6w-border);
2143
+ border-radius: 6px;
2144
+ background: transparent;
2145
+ color: var(--w6w-muted);
2146
+ font-size: var(--w6w-fs-xs);
2147
+ font-family: inherit;
2148
+ padding: 3px var(--w6w-sp-2);
2149
+ cursor: pointer;
2150
+ }
2151
+
2152
+ .w6w-expr-replace:hover {
2153
+ color: var(--w6w-text);
2154
+ background: var(--w6w-panel);
2155
+ }
2156
+
2157
+ /* Insert affordance + popover menu (variable / secret / expression). */
2158
+ .w6w-expr-insert {
2159
+ position: relative;
2160
+ display: inline-flex;
2161
+ }
2162
+
2163
+ .w6w-expr-insert-btn {
2164
+ border: 1px dashed var(--w6w-border);
2165
+ border-radius: 6px;
2166
+ background: transparent;
2167
+ color: var(--w6w-muted);
2168
+ font-size: var(--w6w-fs-xs);
2169
+ font-family: inherit;
2170
+ padding: 3px var(--w6w-sp-2);
2171
+ cursor: pointer;
2172
+ }
2173
+
2174
+ .w6w-expr-insert-btn:hover {
2175
+ color: var(--w6w-text);
2176
+ border-color: var(--w6w-accent);
2177
+ }
2178
+
2179
+ .w6w-expr-menu {
2180
+ position: absolute;
2181
+ top: calc(100% + var(--w6w-sp-1));
2182
+ left: 0;
2183
+ z-index: 60;
2184
+ display: flex;
2185
+ flex-direction: column;
2186
+ gap: var(--w6w-sp-2);
2187
+ min-width: 200px;
2188
+ background: var(--w6w-panel);
2189
+ border: 1px solid var(--w6w-border);
2190
+ border-radius: 8px;
2191
+ padding: var(--w6w-sp-2);
2192
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
2193
+ }
2194
+
2195
+ .w6w-expr-menu-group {
2196
+ display: flex;
2197
+ flex-direction: column;
2198
+ gap: var(--w6w-sp-1);
2199
+ }
2200
+
2201
+ .w6w-expr-menu-group + .w6w-expr-menu-group {
2202
+ border-top: 1px solid var(--w6w-border);
2203
+ padding-top: var(--w6w-sp-2);
2204
+ }
2205
+
2206
+ .w6w-expr-menu-label {
2207
+ font-size: 0.72em;
2208
+ font-weight: var(--w6w-fw-semibold);
2209
+ letter-spacing: 0.04em;
2210
+ text-transform: uppercase;
2211
+ color: var(--w6w-muted);
2212
+ }
2213
+
2214
+ /* A secret-typed field surfaces the Secrets group first + highlighted, so
2215
+ picking a secret is the obvious one-click path (task 3.2). */
2216
+ .w6w-expr-menu-group.is-primary {
2217
+ background: var(--w6w-panel-2);
2218
+ border-radius: 6px;
2219
+ padding: var(--w6w-sp-1-5);
2220
+ margin: -2px;
2221
+ }
2222
+
2223
+ .w6w-expr-menu-group.is-primary .w6w-expr-menu-label {
2224
+ color: var(--w6w-accent);
2225
+ }
2226
+
2227
+ .w6w-expr-menu-empty {
2228
+ font-size: var(--w6w-fs-xs);
2229
+ color: var(--w6w-muted);
2230
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2);
2231
+ font-style: italic;
2232
+ }
2233
+
2234
+ .w6w-expr-menu-item {
2235
+ text-align: left;
2236
+ background: transparent;
2237
+ border: none;
2238
+ border-radius: 6px;
2239
+ padding: 5px var(--w6w-sp-2);
2240
+ color: var(--w6w-text);
2241
+ font-size: 13px;
2242
+ font-family: inherit;
2243
+ cursor: pointer;
2244
+ }
2245
+
2246
+ .w6w-expr-menu-item:hover {
2247
+ background: var(--w6w-panel-2);
2248
+ }
2249
+
2250
+ .w6w-expr-menu-add {
2251
+ display: flex;
2252
+ gap: var(--w6w-sp-1-5);
2253
+ align-items: center;
2254
+ }
2255
+
2256
+ .w6w-expr-menu-add input {
2257
+ flex: 1;
2258
+ min-width: 0;
2259
+ background: var(--w6w-panel-2);
2260
+ border: 1px solid var(--w6w-border);
2261
+ border-radius: 6px;
2262
+ padding: 5px var(--w6w-sp-2);
2263
+ color: var(--w6w-text);
2264
+ font-size: 13px;
2265
+ font-family: inherit;
2266
+ }
2267
+
2268
+ .w6w-expr-menu-add input:focus {
2269
+ outline: none;
2270
+ border-color: var(--w6w-accent);
2271
+ }
2272
+
2273
+ /* Typed key/value rows (the `vars` form element / `data` control step). */
2274
+ .w6w-datavar-row {
2275
+ display: grid;
2276
+ grid-template-columns: 1fr 110px 1fr auto;
2277
+ gap: var(--w6w-sp-1-5);
2278
+ /* Align to the end so that on an `expression` row — where the value cell is a
2279
+ taller ExpressionInput (toolbar above its editor) — the key/type/remove
2280
+ cells line up with the editor at the bottom, not the toolbar. Single-line
2281
+ rows are unaffected (every cell is the same height). */
2282
+ align-items: end;
2283
+ }
2284
+
2285
+ .w6w-datavar-row input,
2286
+ .w6w-datavar-row select {
2287
+ background: var(--w6w-panel-2);
2288
+ border: 1px solid var(--w6w-border);
2289
+ border-radius: 8px;
2290
+ padding: var(--w6w-sp-1-5) var(--w6w-sp-2);
2291
+ color: var(--w6w-text);
2292
+ font-size: 13px;
2293
+ font-family: inherit;
2294
+ min-width: 0;
2295
+ }
2296
+
2297
+ /* The block above is `.w6w-datavar-row input` at (0,1,1), which TIES with a host
2298
+ `input[type="text"] { margin-top: 4px }` and loses on source order — so the
2299
+ margin has to be re-declared at a specificity that wins, rather than added to
2300
+ that block where it would have no effect. `.w6w-field` is always an ancestor
2301
+ here (`VarsField` renders one), giving (0,2,1). */
2302
+ .w6w-field .w6w-datavar-row input,
2303
+ .w6w-field .w6w-datavar-row select {
2304
+ margin: 0;
2305
+ }
2306
+
2307
+ .w6w-datavar-row input:focus,
2308
+ .w6w-datavar-row select:focus {
2309
+ outline: none;
2310
+ border-color: var(--w6w-accent);
2311
+ }
2312
+
2313
+ .w6w-datavar-row .w6w-btn {
2314
+ padding: var(--w6w-sp-1-5) var(--w6w-sp-2-5);
2315
+ }
2316
+
2317
+ /* Buttons */
2318
+ .w6w-btn {
2319
+ background: var(--w6w-accent);
2320
+ color: white;
2321
+ border: none;
2322
+ border-radius: 8px;
2323
+ padding: var(--w6w-sp-2) 14px;
2324
+ font-size: var(--w6w-fs-sm);
2325
+ cursor: pointer;
2326
+ }
2327
+
2328
+ .w6w-btn:hover {
2329
+ filter: brightness(1.08);
2330
+ }
2331
+
2332
+ .w6w-btn:disabled {
2333
+ opacity: 0.5;
2334
+ cursor: default;
2335
+ }
2336
+
2337
+ .w6w-btn-ghost {
2338
+ background: transparent;
2339
+ border: 1px solid var(--w6w-border);
2340
+ color: var(--w6w-text);
2341
+ }
2342
+
2343
+ .w6w-btn-ghost:hover {
2344
+ background: var(--w6w-panel-2);
2345
+ }
2346
+
2347
+ .w6w-btn-ghost.active {
2348
+ border-color: var(--w6w-accent);
2349
+ background: var(--w6w-panel-2);
2350
+ }
2351
+
2352
+ /* Destructive confirm button (ConfirmModal's default). Styled from the same
2353
+ --w6w-danger token the error notices and health pills already use. */
2354
+ .w6w-btn-danger {
2355
+ background: var(--w6w-danger);
2356
+ color: white;
2357
+ }
2358
+
2359
+ /* List row — a clean, reusable list item (leading icon · title/subtitle · trailing).
2360
+ Mirrors the ghost saved-test button rows it replaces. */
2361
+ .w6w-list-item {
2362
+ display: flex;
2363
+ align-items: center;
2364
+ gap: var(--w6w-sp-2-5);
2365
+ width: 100%;
2366
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
2367
+ border: 1px solid transparent;
2368
+ border-radius: 8px;
2369
+ background: transparent;
2370
+ color: var(--w6w-text);
2371
+ text-align: left;
2372
+ font-size: var(--w6w-fs-sm);
2373
+ cursor: pointer;
2374
+ }
2375
+
2376
+ button.w6w-list-item {
2377
+ font: inherit;
2378
+ font-size: var(--w6w-fs-sm);
2379
+ }
2380
+
2381
+ .w6w-list-item:hover {
2382
+ background: var(--w6w-panel-2);
2383
+ }
2384
+
2385
+ .w6w-list-item:focus-visible {
2386
+ outline: none;
2387
+ border-color: var(--w6w-accent);
2388
+ box-shadow: 0 0 0 2px var(--w6w-accent);
2389
+ }
2390
+
2391
+ .w6w-list-item.active {
2392
+ border-color: var(--w6w-accent);
2393
+ background: var(--w6w-panel-2);
2394
+ }
2395
+
2396
+ .w6w-list-item-icon {
2397
+ display: inline-flex;
2398
+ align-items: center;
2399
+ flex-shrink: 0;
2400
+ }
2401
+
2402
+ .w6w-list-item-body {
2403
+ display: flex;
2404
+ flex-direction: column;
2405
+ min-width: 0;
2406
+ gap: var(--w6w-sp-0-5);
2407
+ }
2408
+
2409
+ .w6w-list-item-title {
2410
+ overflow: hidden;
2411
+ text-overflow: ellipsis;
2412
+ white-space: nowrap;
2413
+ }
2414
+
2415
+ .w6w-list-item-subtitle {
2416
+ color: var(--w6w-muted);
2417
+ font-size: var(--w6w-fs-xs);
2418
+ overflow: hidden;
2419
+ text-overflow: ellipsis;
2420
+ white-space: nowrap;
2421
+ }
2422
+
2423
+ .w6w-list-item-trailing {
2424
+ margin-left: auto;
2425
+ display: inline-flex;
2426
+ align-items: center;
2427
+ flex-shrink: 0;
2428
+ }
2429
+
2430
+ /* Health status pill — a small, theme-aware badge for one health state
2431
+ (ok · degraded · down · unknown). Colour comes from the `--w6w-health-*`
2432
+ tokens so a host can restyle every state by overriding those; the dot tints
2433
+ with the state while the pill keeps a soft tinted background + border. */
2434
+ .w6w-health-pill {
2435
+ display: inline-flex;
2436
+ align-items: center;
2437
+ gap: var(--w6w-sp-1-5);
2438
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2) var(--w6w-sp-0-5) 7px;
2439
+ border: 1px solid color-mix(in srgb, var(--w6w-health-color) 40%, var(--w6w-border));
2440
+ border-radius: 999px;
2441
+ background: color-mix(in srgb, var(--w6w-health-color) 12%, transparent);
2442
+ color: var(--w6w-text);
2443
+ font-size: var(--w6w-fs-xs);
2444
+ line-height: var(--w6w-lh-normal);
2445
+ white-space: nowrap;
2446
+ --w6w-health-color: var(--w6w-health-unknown);
2447
+ }
2448
+
2449
+ .w6w-health-pill-ok {
2450
+ --w6w-health-color: var(--w6w-health-ok);
2451
+ }
2452
+
2453
+ .w6w-health-pill-degraded {
2454
+ --w6w-health-color: var(--w6w-health-degraded);
2455
+ }
2456
+
2457
+ .w6w-health-pill-down {
2458
+ --w6w-health-color: var(--w6w-health-down);
2459
+ }
2460
+
2461
+ .w6w-health-pill-unknown {
2462
+ --w6w-health-color: var(--w6w-health-unknown);
2463
+ }
2464
+
2465
+ .w6w-health-pill-dot {
2466
+ flex-shrink: 0;
2467
+ width: 8px;
2468
+ height: 8px;
2469
+ border-radius: 50%;
2470
+ background: var(--w6w-health-color);
2471
+ }
2472
+
2473
+ .w6w-health-pill-label {
2474
+ overflow: hidden;
2475
+ text-overflow: ellipsis;
2476
+ }
2477
+
2478
+ /* App summary block */
2479
+ .w6w-app-summary {
2480
+ display: flex;
2481
+ align-items: center;
2482
+ gap: var(--w6w-sp-2-5);
2483
+ }
2484
+
2485
+ /* Modal */
2486
+ .w6w-modal-backdrop {
2487
+ position: fixed;
2488
+ inset: 0;
2489
+ background: rgba(0, 0, 0, 0.55);
2490
+ display: grid;
2491
+ place-items: center;
2492
+ padding: var(--w6w-sp-5);
2493
+ z-index: 50;
2494
+ }
2495
+
2496
+ dialog.w6w-modal {
2497
+ position: fixed;
2498
+ inset: 0;
2499
+ margin: auto;
2500
+ background: var(--w6w-panel);
2501
+ color: var(--w6w-text);
2502
+ border: 1px solid var(--w6w-border);
2503
+ border-radius: var(--w6w-radius);
2504
+ width: 100%;
2505
+ max-width: 800px;
2506
+ max-height: 90vh;
2507
+ overflow: auto;
2508
+ padding: var(--w6w-sp-5);
2509
+ display: flex;
2510
+ flex-direction: column;
2511
+ gap: var(--w6w-sp-3);
2512
+ }
2513
+
2514
+ dialog.w6w-modal::backdrop {
2515
+ background: rgba(0, 0, 0, 0.55);
2516
+ }
2517
+
2518
+ dialog.w6w-modal h3,
2519
+ dialog.w6w-modal h4,
2520
+ dialog.w6w-modal p {
2521
+ color: var(--w6w-text);
2522
+ margin: 0;
2523
+ }
2524
+
2525
+ dialog.w6w-modal.w6w-modal-wide {
2526
+ max-width: min(850px, 80vw);
2527
+ min-height: min(70vh, 560px);
2528
+ }
2529
+
2530
+ dialog.w6w-modal.w6w-modal-xl {
2531
+ max-width: 1040px;
2532
+ min-height: min(70vh, 620px);
2533
+ }
2534
+
2535
+ dialog.w6w-modal.w6w-modal-full {
2536
+ max-width: min(1200px, 96vw);
2537
+ width: 96vw;
2538
+ height: 92vh;
2539
+ max-height: 92vh;
2540
+ overflow: hidden;
2541
+ }
2542
+
2543
+ /* Edge-to-edge surface: the whole viewport, no rounded corners and no visible
2544
+ backdrop margin. Distinct from `.w6w-modal-full` (96vw x 92vh), which is
2545
+ deliberately inset so the page behind stays visible as context — the
2546
+ expression editor and the action tester both rely on that.
2547
+ `position: fixed; inset: 0` already sizes to the viewport, so width/height
2548
+ here are belt-and-braces against a UA stylesheet's `dialog` sizing.
2549
+ The header is pinned; the caller's own child scrolls (it must set
2550
+ `flex: 1; min-height: 0; overflow: auto` on itself). */
2551
+ dialog.w6w-modal.w6w-modal-fullscreen {
2552
+ max-width: none;
2553
+ width: 100vw;
2554
+ height: 100vh;
2555
+ max-height: 100vh;
2556
+ border: none;
2557
+ border-radius: 0;
2558
+ overflow: hidden;
2559
+ padding: var(--w6w-sp-5) var(--w6w-sp-6);
2560
+ }
2561
+
2562
+ dialog.w6w-modal.w6w-modal-fullscreen .w6w-modal-header {
2563
+ flex-shrink: 0;
2564
+ padding-bottom: var(--w6w-sp-4);
2565
+ border-bottom: 1px solid var(--w6w-border);
2566
+ }
2567
+
2568
+ .w6w-modal-header {
2569
+ display: flex;
2570
+ align-items: center;
2571
+ justify-content: space-between;
2572
+ gap: var(--w6w-sp-3);
2573
+ }
2574
+
2575
+ .w6w-modal-title {
2576
+ display: flex;
2577
+ align-items: center;
2578
+ gap: var(--w6w-sp-2-5);
2579
+ min-width: 0;
2580
+ }
2581
+
2582
+ .w6w-modal-subtitle {
2583
+ color: var(--w6w-muted);
2584
+ font-size: 0.7em;
2585
+ font-weight: var(--w6w-fw-regular);
2586
+ white-space: nowrap;
2587
+ overflow: hidden;
2588
+ text-overflow: ellipsis;
2589
+ }
2590
+
2591
+ .w6w-modal-subtitle code {
2592
+ font-size: inherit;
2593
+ }
2594
+
2595
+ /* The one scrolling region of a dialog (D-3): the body takes the leftover height
2596
+ and scrolls, so the header/footer around it stay put. The dialog's own
2597
+ `gap: var(--w6w-sp-3)` column is reproduced here so content-spacing is
2598
+ unchanged now that body children are flex items of THIS box, not the dialog's. */
2599
+ .w6w-modal-body {
2600
+ display: flex;
2601
+ flex-direction: column;
2602
+ gap: var(--w6w-sp-3);
2603
+ flex: 1;
2604
+ min-height: 0;
2605
+ overflow-y: auto;
2606
+ }
2607
+
2608
+ /* Pinned below the scrolling `.w6w-modal-body`, never shrunk away by it (D-3).
2609
+ Sits at the dialog's bottom for free: the body above it already absorbs the
2610
+ free space, so no `margin-top: auto` is needed (or wanted — it would shrink
2611
+ this row instead). */
2612
+ .w6w-modal-actions {
2613
+ display: flex;
2614
+ gap: var(--w6w-sp-2);
2615
+ justify-content: flex-end;
2616
+ flex-shrink: 0;
2617
+ }
2618
+
2619
+ /* Icon-button view toggle (e.g. Form ⇄ JSON), inline with a modal header. */
2620
+ .w6w-view-toggle {
2621
+ display: flex;
2622
+ gap: var(--w6w-sp-1);
2623
+ flex-shrink: 0;
2624
+ }
2625
+
2626
+ .w6w-icon-btn {
2627
+ display: inline-flex;
2628
+ align-items: center;
2629
+ justify-content: center;
2630
+ background: transparent;
2631
+ border: 1px solid transparent;
2632
+ border-radius: 8px;
2633
+ padding: var(--w6w-sp-1-5);
2634
+ color: var(--w6w-muted);
2635
+ cursor: pointer;
2636
+ }
2637
+
2638
+ .w6w-icon-btn:hover {
2639
+ background: var(--w6w-panel-2);
2640
+ color: var(--w6w-text);
2641
+ }
2642
+
2643
+ .w6w-icon-btn.active {
2644
+ background: var(--w6w-panel-2);
2645
+ border-color: var(--w6w-accent);
2646
+ color: var(--w6w-text);
2647
+ }
2648
+
2649
+ /* Segmented sub-tabs inside the edit modal's form view (Parameters | Config). */
2650
+ .w6w-subtabs {
2651
+ display: inline-flex;
2652
+ gap: var(--w6w-sp-0-5);
2653
+ padding: var(--w6w-sp-0-5);
2654
+ background: var(--w6w-panel-2);
2655
+ border: 1px solid var(--w6w-border);
2656
+ border-radius: 8px;
2657
+ align-self: flex-start;
2658
+ }
2659
+
2660
+ .w6w-subtab {
2661
+ background: transparent;
2662
+ border: none;
2663
+ border-radius: 6px;
2664
+ padding: 5px var(--w6w-sp-3);
2665
+ color: var(--w6w-muted);
2666
+ font-size: 13px;
2667
+ cursor: pointer;
2668
+ }
2669
+
2670
+ .w6w-subtab:hover {
2671
+ color: var(--w6w-text);
2672
+ }
2673
+
2674
+ .w6w-subtab.active {
2675
+ background: var(--w6w-panel);
2676
+ color: var(--w6w-text);
2677
+ font-weight: var(--w6w-fw-medium);
2678
+ }
2679
+
2680
+ .w6w-subtab:disabled {
2681
+ opacity: 0.4;
2682
+ cursor: default;
2683
+ }
2684
+
2685
+ .w6w-subtab:disabled:hover {
2686
+ color: var(--w6w-muted);
2687
+ }
2688
+
2689
+ /* Full-width tabs bar: segmented tabs on the left, view icons on the right. */
2690
+ .w6w-tabsbar {
2691
+ display: flex;
2692
+ align-items: center;
2693
+ justify-content: space-between;
2694
+ gap: var(--w6w-sp-3);
2695
+ border-bottom: 1px solid var(--w6w-border);
2696
+ padding-bottom: var(--w6w-sp-2);
2697
+ flex-shrink: 0;
2698
+ }
2699
+
2700
+ .w6w-icon-btn:disabled {
2701
+ opacity: 0.35;
2702
+ cursor: default;
2703
+ }
2704
+
2705
+ .w6w-icon-btn:disabled:hover {
2706
+ background: transparent;
2707
+ color: var(--w6w-muted);
2708
+ }
2709
+
2710
+ /* Config modal body (a picked app/node) fills the dialog so the footer pins to
2711
+ the modal's bottom rather than a fixed-height content area. */
2712
+ .w6w-stepbuilder-config {
2713
+ flex: 1;
2714
+ min-height: 0;
2715
+ display: flex;
2716
+ flex-direction: column;
2717
+ }
2718
+
2719
+ /* Compact one-line step identity (id · app · action) in the edit modal. */
2720
+ .w6w-step-meta {
2721
+ display: flex;
2722
+ flex-wrap: wrap;
2723
+ align-items: center;
2724
+ gap: var(--w6w-sp-1-5);
2725
+ /* lint-tokens-allow: relative em, not absolute rem like the scale's --w6w-fs- type tokens — mirrors the accepted _modal.scss:88 .w6w-modal-subtitle precedent; .w6w-step-meta currently has zero .tsx call sites (possible dead CSS, not this task's to delete) */
2726
+ font-size: 0.8em;
2727
+ color: var(--w6w-muted);
2728
+ }
2729
+
2730
+ .w6w-step-meta code {
2731
+ color: var(--w6w-text);
2732
+ }
2733
+
2734
+ /* Step builder (sidebar + content) */
2735
+ .w6w-stepbuilder {
2736
+ display: flex;
2737
+ gap: var(--w6w-sp-4);
2738
+ min-height: 340px;
2739
+ }
2740
+
2741
+ .w6w-stepbuilder-sidebar {
2742
+ display: flex;
2743
+ flex-direction: column;
2744
+ gap: var(--w6w-sp-1);
2745
+ width: 140px;
2746
+ flex-shrink: 0;
2747
+ border-right: 1px solid var(--w6w-border);
2748
+ padding-right: var(--w6w-sp-3);
2749
+ }
2750
+
2751
+ .w6w-stepbuilder-tab {
2752
+ text-align: left;
2753
+ background: transparent;
2754
+ border: 1px solid transparent;
2755
+ border-radius: 8px;
2756
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
2757
+ color: var(--w6w-muted);
2758
+ font-size: var(--w6w-fs-sm);
2759
+ cursor: pointer;
2760
+ }
2761
+
2762
+ .w6w-stepbuilder-tab:hover {
2763
+ background: var(--w6w-panel-2);
2764
+ color: var(--w6w-text);
2765
+ }
2766
+
2767
+ .w6w-stepbuilder-tab.active {
2768
+ background: var(--w6w-panel-2);
2769
+ color: var(--w6w-text);
2770
+ font-weight: var(--w6w-fw-medium);
2771
+ }
2772
+
2773
+ .w6w-stepbuilder-content {
2774
+ flex: 1;
2775
+ min-width: 0;
2776
+ /* Fixed height (not content-driven) so the modal never resizes when toggling
2777
+ tabs, and the sidebar divider — which stretches to this row — stays a
2778
+ constant length regardless of the right pane's content. */
2779
+ height: min(70vh, 520px);
2780
+ overflow: auto;
2781
+ }
2782
+
2783
+ /* Apps flow: a fixed search header + a separately scrolling list, so the
2784
+ scrollbar is contained to the list and never runs alongside the search. */
2785
+ .w6w-stepbuilder-apps {
2786
+ display: flex;
2787
+ flex-direction: column;
2788
+ gap: var(--w6w-sp-2-5);
2789
+ height: 100%;
2790
+ min-height: 0;
2791
+ }
2792
+
2793
+ .w6w-stepbuilder-scroll {
2794
+ overflow-y: auto;
2795
+ /* Fill the remaining height below the search header; the list — not the
2796
+ outer modal content — owns the scrollbar. */
2797
+ flex: 1;
2798
+ min-height: 0;
2799
+ padding-right: var(--w6w-sp-1);
2800
+ }
2801
+
2802
+ .w6w-stepbuilder-search {
2803
+ background: var(--w6w-panel-2);
2804
+ border: 1px solid var(--w6w-border);
2805
+ border-radius: 8px;
2806
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
2807
+ color: var(--w6w-text);
2808
+ font-size: var(--w6w-fs-sm);
2809
+ font-family: inherit;
2810
+ width: 100%;
2811
+ }
2812
+
2813
+ .w6w-stepbuilder-search:focus {
2814
+ outline: none;
2815
+ border-color: var(--w6w-accent);
2816
+ }
2817
+
2818
+ .w6w-stepbuilder-list {
2819
+ display: flex;
2820
+ flex-direction: column;
2821
+ gap: var(--w6w-sp-1-5);
2822
+ }
2823
+
2824
+ .w6w-stepbuilder-item {
2825
+ display: flex;
2826
+ align-items: center;
2827
+ gap: var(--w6w-sp-2-5);
2828
+ text-align: left;
2829
+ background: var(--w6w-panel-2);
2830
+ border: 1px solid var(--w6w-border);
2831
+ border-radius: 8px;
2832
+ padding: var(--w6w-sp-2-5) var(--w6w-sp-3);
2833
+ color: var(--w6w-text);
2834
+ cursor: pointer;
2835
+ font-size: var(--w6w-fs-sm);
2836
+ }
2837
+
2838
+ .w6w-stepbuilder-item:hover {
2839
+ border-color: var(--w6w-accent);
2840
+ }
2841
+
2842
+ .w6w-stepbuilder-item-main {
2843
+ display: flex;
2844
+ flex-direction: column;
2845
+ gap: var(--w6w-sp-0-5);
2846
+ min-width: 0;
2847
+ }
2848
+
2849
+ /* AppPicker's own multi-column card grid (T1.1.1). ADDITIVE on top of the
2850
+ shared `.w6w-stepbuilder-list`/`-item` above, which stay byte-identical to
2851
+ `main` (NodeList/CallableRow/ReadyToUseFlow still consume them unmodified)
2852
+ — this rule wins the cascade purely by being declared later at equal
2853
+ (single-class) specificity. `minmax(190px, 1fr)` is re-derived for this
2854
+ card's much shorter content (icon + name + id + version, not a full
2855
+ description card) from the scale `packages/frontend`'s generic `.grid`
2856
+ utility uses (`minmax(260px, 1fr)`) — `grid-template-columns` is box
2857
+ geometry, outside `lint:tokens`' scanned property set, so the literal is
2858
+ deliberate, not an oversight.
2859
+
2860
+ `auto-fill`, NOT `auto-fit`: the two differ in exactly one case, and it is
2861
+ the case a search box guarantees will happen. `auto-fit` COLLAPSES the
2862
+ tracks no card landed in, and the `1fr` then hands their width to the
2863
+ survivors — so typing a query that matches one app blew that single card up
2864
+ to the full width of the panel, a tile that changes size as you type. With
2865
+ `auto-fill` the empty tracks are kept at their `minmax()` width, so a card
2866
+ is the same size whether the grid holds 60 of them or 1.
2867
+
2868
+ `align-content: start` is the vertical half of the same invariant: an auto
2869
+ row track under the default `stretch` absorbs the free space of a grid with
2870
+ a definite height (this one's, from `.w6w-stepbuilder-scroll`'s `flex: 1`),
2871
+ so a single result would also have grown DOWN to fill the panel. */
2872
+ .w6w-apppicker-grid {
2873
+ display: grid;
2874
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
2875
+ gap: var(--w6w-sp-2);
2876
+ align-content: start;
2877
+ }
2878
+
2879
+ /* The quieter resting card (D-3): `--w6w-panel` at rest instead of
2880
+ `--w6w-panel-2`, shifting TO `--w6w-panel-2` on hover/focus — the inverse
2881
+ of the shared item's flat `--w6w-panel-2` always-on tint. No new token;
2882
+ `.w6w-stepbuilder-item:hover`'s `border-color: var(--w6w-accent)` above
2883
+ still applies unchanged, so hover keeps both effects together. Slightly
2884
+ tighter padding than the shared item, sized for a narrower multi-column
2885
+ card rather than a single full-width row.
2886
+
2887
+ `min-height` pins every tile to one height regardless of what it holds:
2888
+ `version` is optional on `AppSummary`, so a catalog that mixes versioned and
2889
+ unversioned apps would otherwise render two card heights down the same grid
2890
+ (grid items stretch to their ROW, not to the whole grid). The floor is the
2891
+ three-line card's own content height rounded up, so it never clips.
2892
+
2893
+ `align-items: flex-start` overrides the shared item's `center`: against a
2894
+ three-line block a centred 24px icon floats beside the id rather than
2895
+ reading as the name's own mark. */
2896
+ .w6w-apppicker-card {
2897
+ background: var(--w6w-panel);
2898
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
2899
+ align-items: flex-start;
2900
+ min-height: 84px;
2901
+ }
2902
+
2903
+ .w6w-apppicker-card:hover,
2904
+ .w6w-apppicker-card:focus-visible {
2905
+ background: var(--w6w-panel-2);
2906
+ }
2907
+
2908
+ /* The card's two metadata lines. One `font-size` declaration for both, because
2909
+ there is only one deliberate literal here to justify: the type ramp's
2910
+ smallest role is `--w6w-fs-xs` (0.75rem), the id and version want to sit a
2911
+ step UNDER it, and the ramp has no `2xs` — its 30-name key set is pinned
2912
+ name-for-name by `scale-tokens.test.ts` and mirrored by `packages/frontend`'s
2913
+ `check:scale-parity`, so adding one is a cross-repo contract change, not a
2914
+ card tweak. `code`'s own UA `font-size` also has to be overridden here
2915
+ whatever value is chosen. */
2916
+ .w6w-apppicker-card-id,
2917
+ .w6w-apppicker-card-version {
2918
+ /* lint-tokens-allow: one step under --w6w-fs-xs; the ramp has no 2xs role and its keys are pinned */
2919
+ font-size: 0.6875rem;
2920
+ color: var(--w6w-muted);
2921
+ font-family: var(--w6w-font-mono);
2922
+ }
2923
+
2924
+ /* Truncation, not wrapping: `min-width: 0` on the parent
2925
+ `.w6w-stepbuilder-item-main` is what lets this shrink below its own
2926
+ min-content width in the first place — without it the ellipsis never
2927
+ engages and a long id widens the flex line instead. */
2928
+ .w6w-apppicker-card-id {
2929
+ display: block;
2930
+ max-width: 100%;
2931
+ overflow: hidden;
2932
+ text-overflow: ellipsis;
2933
+ white-space: nowrap;
2934
+ opacity: 0.6;
2935
+ }
2936
+
2937
+ /* Quieter than the name, but a step louder than the id it sits under — it is
2938
+ the shorter, more scannable of the two. */
2939
+ .w6w-apppicker-card-version {
2940
+ opacity: 0.75;
2941
+ }
2942
+
2943
+ /* The opt-in category-filter chip row (D-1a; AddConnectionModal only —
2944
+ `AppPickerProps.categoryFilter`). Extends `.w6w-chip`'s look
2945
+ (`_multiselect.scss:40-53`) for the general shape rather than importing
2946
+ it — that partial belongs to `ParamsForm`'s array-editor chips, a
2947
+ different feature with the opposite interaction (only SELECTED values are
2948
+ chips there; every available category is always a chip here). */
2949
+ .w6w-apppicker-cats {
2950
+ display: flex;
2951
+ flex-wrap: wrap;
2952
+ align-items: center;
2953
+ gap: var(--w6w-sp-1-5);
2954
+ }
2955
+
2956
+ .w6w-apppicker-chip {
2957
+ display: inline-flex;
2958
+ align-items: center;
2959
+ gap: var(--w6w-sp-0-5);
2960
+ background: var(--w6w-panel);
2961
+ border: 1px solid var(--w6w-border);
2962
+ border-radius: 999px;
2963
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2);
2964
+ color: var(--w6w-muted);
2965
+ font-size: var(--w6w-fs-xs);
2966
+ cursor: pointer;
2967
+ }
2968
+
2969
+ .w6w-apppicker-chip:hover {
2970
+ border-color: var(--w6w-accent);
2971
+ color: var(--w6w-text);
2972
+ }
2973
+
2974
+ /* The pressed/selected state is driven by the same `aria-pressed` the
2975
+ button already carries for assistive tech (P10) — no separate class to
2976
+ keep in sync with it. */
2977
+ .w6w-apppicker-chip[aria-pressed=true] {
2978
+ background: var(--w6w-accent);
2979
+ border-color: var(--w6w-accent);
2980
+ color: #fff;
2981
+ /* The count's `.w6w-muted` sets its OWN color, so it doesn't inherit this
2982
+ `color: #fff` — left alone it stays `var(--w6w-muted)`, a gray tuned for
2983
+ the unpressed `--w6w-panel` background, which is nearly invisible against
2984
+ the accent fill. Not full white: the count is still secondary to the
2985
+ label. */
2986
+ }
2987
+ .w6w-apppicker-chip[aria-pressed=true] .w6w-muted {
2988
+ color: rgba(255, 255, 255, 0.75);
2989
+ }
2990
+
2991
+ /* AppPicker's own layout owner, nested inside `.w6w-stepbuilder-content` (never
2992
+ applied in its place — see the call sites). A flex item's automatic minimum
2993
+ size is its min-content size unless `overflow` is non-`visible` or
2994
+ `min-height:0` is set; `.w6w-stepbuilder-content` has neither, so its
2995
+ min-content (every app row, unclipped) pushes `dialog.w6w-modal` up against
2996
+ `max-height:90vh`. This host's explicit `height` + `overflow:hidden` zeroes
2997
+ that contribution, so the dialog falls back to `.w6w-modal-xl`'s
2998
+ `min-height: min(70vh, 620px)` — the step builder's own height. */
2999
+ .w6w-apppicker-host {
3000
+ flex: 0 0 auto;
3001
+ min-width: 0;
3002
+ height: min(70vh, 520px);
3003
+ display: flex;
3004
+ flex-direction: column;
3005
+ overflow: hidden;
3006
+ }
3007
+
3008
+ .w6w-apppicker-host > .w6w-stepbuilder-apps {
3009
+ height: auto;
3010
+ flex: 1 1 auto;
3011
+ min-height: 0;
3012
+ }
3013
+
3014
+ /* The picker's home tab — the intake's two-column layout:
3015
+
3016
+ Connnected apps | functions
3017
+ | workflows
3018
+
3019
+ Connected apps down the left; Functions above Workflows down the right.
3020
+ Nested inside `.w6w-apppicker-host`, which owns the height, so each column's
3021
+ own list is what scrolls — never the modal. `min-height: 0` on the grid and
3022
+ on every column is load-bearing: a grid/flex item's automatic minimum size
3023
+ is its min-content size, so without it a long list pushes the dialog past
3024
+ `.w6w-apppicker-host`'s fixed height instead of scrolling inside it. */
3025
+ .w6w-readytouse {
3026
+ display: grid;
3027
+ grid-template-columns: 1fr 1fr;
3028
+ gap: var(--w6w-sp-4);
3029
+ flex: 1 1 auto;
3030
+ min-height: 0;
3031
+ }
3032
+
3033
+ /* A half with nothing in it is not rendered at all (an empty "Workflows"
3034
+ heading would claim you have workflows ready to use), so the grid collapses
3035
+ to the one column that survived — and the divider below, which only makes
3036
+ sense between two halves, goes with it. */
3037
+ .w6w-readytouse[data-cols="1"] {
3038
+ grid-template-columns: 1fr;
3039
+ }
3040
+
3041
+ /* The `|` in the intake's sketch. On the right half's leading edge rather than
3042
+ as a third grid track, so it spans the full column height however tall the
3043
+ two halves end up, and disappears with the half itself. */
3044
+ .w6w-readytouse[data-cols="2"] > .w6w-readytouse-stack {
3045
+ border-left: 1px solid var(--w6w-border);
3046
+ padding-left: var(--w6w-sp-4);
3047
+ }
3048
+
3049
+ /* The right half stacks its two sections; they share the height evenly. */
3050
+ .w6w-readytouse-stack {
3051
+ display: grid;
3052
+ grid-template-rows: 1fr 1fr;
3053
+ gap: var(--w6w-sp-3);
3054
+ min-height: 0;
3055
+ }
3056
+
3057
+ /* A single column: heading pinned, list scrolling under it. */
3058
+ .w6w-readytouse-col {
3059
+ display: flex;
3060
+ flex-direction: column;
3061
+ gap: var(--w6w-sp-2);
3062
+ min-height: 0;
3063
+ }
3064
+
3065
+ /* One section only in the stack — a picker offering a single callable family,
3066
+ or a workspace with Functions but no Workflows. It takes the whole right
3067
+ half rather than leaving a gap under it. */
3068
+ .w6w-readytouse-stack > .w6w-readytouse-col:only-child {
3069
+ grid-row: 1/-1;
3070
+ }
3071
+
3072
+ .w6w-readytouse-heading {
3073
+ margin: 0;
3074
+ font-size: var(--w6w-fs-sm);
3075
+ font-weight: var(--w6w-fw-semibold);
3076
+ color: var(--w6w-muted-text, var(--w6w-text));
3077
+ }
3078
+
3079
+ /* A Function/Workflow row, distinct from an app row at a glance: an app is
3080
+ somebody else's service, a Function/Workflow is something YOU built here.
3081
+ Applied by `CallableRow`, which both the home tab and the Workflows/
3082
+ Functions tabs render — so the tint and the glyph travel together and a
3083
+ Function looks the same wherever it is offered. */
3084
+ .w6w-stepbuilder-item--callable {
3085
+ background: var(--w6w-panel);
3086
+ border-style: dashed;
3087
+ }
3088
+
3089
+ /* Step config (app action / control node): a fixed header (connection + action)
3090
+ that never scrolls, a scrolling parameters body, and a pinned footer — mirrors
3091
+ the fixed-header/scroll-body split used by `.w6w-stepbuilder-apps`. */
3092
+ .w6w-stepconfig {
3093
+ display: flex;
3094
+ flex-direction: column;
3095
+ flex: 1;
3096
+ height: 100%;
3097
+ min-height: 0;
3098
+ gap: var(--w6w-sp-3);
3099
+ /* Horizontal breathing room so header/footer align with the padded body. */
3100
+ padding-inline: var(--w6w-sp-1);
3101
+ }
3102
+
3103
+ /* Inline step rename (pencil) shown next to the modal title. */
3104
+ .w6w-step-rename {
3105
+ display: inline-flex;
3106
+ align-items: center;
3107
+ gap: var(--w6w-sp-1);
3108
+ }
3109
+
3110
+ .w6w-step-rename input {
3111
+ background: var(--w6w-panel-2);
3112
+ border: 1px solid var(--w6w-accent);
3113
+ border-radius: 6px;
3114
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-1-5);
3115
+ color: var(--w6w-text);
3116
+ /* lint-tokens-allow: relative em, not absolute rem like the scale's --w6w-fs- type tokens — mirrors the accepted _modal.scss:88 .w6w-modal-subtitle precedent */
3117
+ font-size: 0.9em;
3118
+ font-family: inherit;
3119
+ }
3120
+
3121
+ .w6w-stepconfig-header {
3122
+ display: flex;
3123
+ flex-direction: column;
3124
+ gap: var(--w6w-sp-2-5);
3125
+ flex-shrink: 0;
3126
+ }
3127
+
3128
+ /* Connection + action share a row and wrap on narrow widths. */
3129
+ .w6w-stepconfig-row {
3130
+ display: flex;
3131
+ flex-wrap: wrap;
3132
+ gap: var(--w6w-sp-3);
3133
+ align-items: flex-start;
3134
+ }
3135
+
3136
+ .w6w-stepconfig-conn,
3137
+ .w6w-stepconfig-action {
3138
+ flex: 1 1 240px;
3139
+ min-width: 0;
3140
+ }
3141
+
3142
+ .w6w-stepconfig-conn-empty {
3143
+ flex: 1 1 100%;
3144
+ }
3145
+
3146
+ /* Selected connection rendered as a static label (not a dropdown). */
3147
+ .w6w-conn-label {
3148
+ display: flex;
3149
+ align-items: center;
3150
+ gap: var(--w6w-sp-2);
3151
+ background: var(--w6w-panel-2);
3152
+ border: 1px solid var(--w6w-border);
3153
+ border-radius: 8px;
3154
+ padding: var(--w6w-sp-1-5) var(--w6w-sp-2) var(--w6w-sp-1-5) var(--w6w-sp-2-5);
3155
+ min-height: 36px;
3156
+ }
3157
+
3158
+ .w6w-conn-label-name {
3159
+ flex: 1;
3160
+ min-width: 0;
3161
+ color: var(--w6w-text);
3162
+ font-size: var(--w6w-fs-sm);
3163
+ overflow: hidden;
3164
+ text-overflow: ellipsis;
3165
+ white-space: nowrap;
3166
+ }
3167
+
3168
+ /* A pseudo-field wrapper (border/background, like every other field in this
3169
+ form) CONTAINING two real buttons, spaced apart — `[App] <--> [Connection]`
3170
+ (2026-08-30, fifth pass: "still missing border around SendGrid + prod...
3171
+ look like before, where both were inside a pseudo field, only change was
3172
+ to make the buttons look like buttons"). The outer border is back; each
3173
+ button ALSO keeps its own border/background (one shade lighter,
3174
+ `--w6w-panel` against the field's `--w6w-panel-2`) so it still reads as a
3175
+ clickable button, not merely text, without the two touching directly —
3176
+ the field's own padding is the gap between its edge and each button. */
3177
+ .w6w-app-conn-row {
3178
+ display: flex;
3179
+ align-items: center;
3180
+ justify-content: space-between;
3181
+ gap: var(--w6w-sp-2);
3182
+ background: var(--w6w-panel-2);
3183
+ border: 1px solid var(--w6w-border);
3184
+ border-radius: 8px;
3185
+ min-height: 36px;
3186
+ padding: var(--w6w-sp-1);
3187
+ }
3188
+
3189
+ .w6w-app-conn-btn {
3190
+ flex: 0 1 auto;
3191
+ min-width: 0;
3192
+ display: flex;
3193
+ align-items: center;
3194
+ gap: var(--w6w-sp-2);
3195
+ background: var(--w6w-panel);
3196
+ border: 1px solid var(--w6w-border);
3197
+ border-radius: 6px;
3198
+ padding: var(--w6w-sp-1) var(--w6w-sp-2-5);
3199
+ cursor: pointer;
3200
+ text-align: left;
3201
+ font: inherit;
3202
+ color: var(--w6w-text);
3203
+ }
3204
+
3205
+ .w6w-app-conn-btn:disabled {
3206
+ cursor: default;
3207
+ opacity: 0.6;
3208
+ }
3209
+
3210
+ .w6w-app-conn-btn:hover:not(:disabled),
3211
+ .w6w-app-conn-btn[aria-expanded=true] {
3212
+ border-color: var(--w6w-accent);
3213
+ }
3214
+
3215
+ /* A flyout popover (App/Connection pickers) — portaled to the enclosing
3216
+ `<dialog>` so it floats OVER the modal's own scrollable body instead of
3217
+ being clipped/squeezed inside it (`Flyout` component doc has the why).
3218
+ Plain positioned div, not a nested native dialog. No max-height/overflow
3219
+ of its own: `AppPicker` already caps and scrolls itself
3220
+ (`.w6w-apppicker-host`, `min(70vh, 520px)`), and the plain connection
3221
+ list is short enough in practice to size to content — a SECOND scroll
3222
+ container nested inside this one would double-scroll `AppPicker`. */
3223
+ .w6w-flyout {
3224
+ position: fixed;
3225
+ z-index: 40;
3226
+ /* No `box-sizing: border-box` reset elsewhere in this package (checked) —
3227
+ the inline `width` set by `Flyout` is meant to match the anchor row
3228
+ EXACTLY (2026-08-30: "should extend to the end of the prod button and
3229
+ div"), so padding/border must come out of that width, not add to it. */
3230
+ box-sizing: border-box;
3231
+ background: var(--w6w-panel);
3232
+ border: 1px solid var(--w6w-border);
3233
+ border-radius: 8px;
3234
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
3235
+ padding: var(--w6w-sp-2);
3236
+ }
3237
+
3238
+ .w6w-btn-sm {
3239
+ padding: var(--w6w-sp-1) var(--w6w-sp-2);
3240
+ font-size: var(--w6w-fs-xs);
3241
+ flex-shrink: 0;
3242
+ }
3243
+
3244
+ /* Only the parameters form scrolls; x-padding keeps fields off the edge + scrollbar. */
3245
+ .w6w-stepconfig-body {
3246
+ flex: 1;
3247
+ min-height: 0;
3248
+ overflow-y: auto;
3249
+ padding-inline: var(--w6w-sp-1);
3250
+ padding-right: var(--w6w-sp-2);
3251
+ }
3252
+
3253
+ /* Footer pinned to the modal bottom, outside the scroll area. */
3254
+ .w6w-stepconfig-footer {
3255
+ flex-shrink: 0;
3256
+ border-top: 1px solid var(--w6w-border);
3257
+ padding-top: var(--w6w-sp-3);
3258
+ margin-top: 0;
3259
+ }
3260
+
3261
+ /* Test-required note — always rendered (empty when there is nothing to say),
3262
+ so its reserved height keeps the footer's buttons at the same x/y whether
3263
+ the note has text or not; one line, ellipsized rather than wrapped. */
3264
+ .w6w-stepconfig-testnote {
3265
+ flex-shrink: 0;
3266
+ min-height: 1.4em;
3267
+ white-space: nowrap;
3268
+ overflow: hidden;
3269
+ text-overflow: ellipsis;
3270
+ }
3271
+
3272
+ /* The whole-config "code"/JSON view is a DIRECT child of the body — stretch it
3273
+ to fill the modal height instead of leaving the dialog half-empty (it passes
3274
+ height="100%"). Inline json/code PARAM fields live inside a `.w6w-field` and
3275
+ size to their content, capped by the editor's own maxHeight (grow, then
3276
+ scroll inside), so they are intentionally NOT stretched here — that was the
3277
+ cause of the empty space + overlap under short group/json fields. */
3278
+ .w6w-stepconfig-body:has(> .w6w-code-editor, > .w6w-json-editor) {
3279
+ display: flex;
3280
+ flex-direction: column;
3281
+ gap: var(--w6w-sp-2-5);
3282
+ }
3283
+
3284
+ .w6w-stepconfig-body > .w6w-code-editor,
3285
+ .w6w-stepconfig-body > .w6w-json-editor {
3286
+ flex: 1 1 auto;
3287
+ min-height: 0;
3288
+ }
3289
+
3290
+ /* The `copyable` prop's button overlays `.w6w-json-editor`'s own top-right
3291
+ corner — mirrors `_copyable.scss:86-100`'s CodeBlock geometry, but there is
3292
+ no `<pre>` here for a `:has()` selector to reach, so it is scoped directly
3293
+ to the class instead. Anchored to `.w6w-json-editor` itself (not the
3294
+ CodeMirror scroller), so it stays pinned to the visible corner rather than
3295
+ scrolling with the content. */
3296
+ .w6w-json-editor {
3297
+ position: relative;
3298
+ }
3299
+
3300
+ .w6w-json-editor > .w6w-copyable-btn {
3301
+ position: absolute;
3302
+ top: var(--w6w-sp-1);
3303
+ right: var(--w6w-sp-1);
3304
+ /* Opaque backdrop: this button sits ON the JSON's first line, and
3305
+ `.w6w-icon-btn`'s transparent idle state would let the text read through
3306
+ it (mirrors `_copyable.scss:93-99`'s rationale exactly). */
3307
+ background: var(--w6w-panel);
3308
+ border-color: var(--w6w-border);
3309
+ }
3310
+
3311
+ .w6w-stepconfig-body .w6w-field > textarea {
3312
+ resize: vertical;
3313
+ }
3314
+
3315
+ /* Field label row with a trailing action (e.g. the JSON "open full view" btn). */
3316
+ .w6w-field-labelrow {
3317
+ display: flex;
3318
+ align-items: center;
3319
+ justify-content: space-between;
3320
+ gap: var(--w6w-sp-2);
3321
+ }
3322
+
3323
+ /* Full-view JSON modal body — fill the dialog so the editor is roomy. */
3324
+ .w6w-json-fullview {
3325
+ display: flex;
3326
+ flex-direction: column;
3327
+ min-height: 0;
3328
+ flex: 1 1 auto;
3329
+ height: 60vh;
3330
+ }
3331
+
3332
+ .w6w-json-fullview > .w6w-json-editor {
3333
+ flex: 1 1 auto;
3334
+ min-height: 0;
3335
+ }
3336
+
3337
+ /* React Flow chrome, driven from the app's own tokens.
3338
+
3339
+ React Flow themes every surface through a three-link chain,
3340
+ `var(--xy-X-props, var(--xy-X, var(--xy-X-default)))`. The library defines
3341
+ only the `-default` link (light/dark, its own palette); the MIDDLE name is
3342
+ never declared by the library and exists purely as a host hook. Setting those
3343
+ middle names here re-points the zoom/fit control stack at `--w6w-*`, so it
3344
+ matches whatever surface the host theme actually uses — and any ControlButton
3345
+ added to that stack later inherits it with no styling of its own.
3346
+
3347
+ Custom properties, deliberately, rather than a `.react-flow__controls-button`
3348
+ override: React Flow's own stylesheet is compiled into the same
3349
+ styles.css (`@use` in the build-only styles.build.scss), but cascade
3350
+ order is not something to lean on: a bare class override (0,1,0) would tie
3351
+ with the library's own rule and lose if the order ever changed. Custom properties inherit and are
3352
+ order-independent, and scoping them on `.w6w-flow` keeps them off the rest of
3353
+ the page.
3354
+
3355
+ Pairs with `colorMode={…}` on <ReactFlow>, which fixes the `-default` link for
3356
+ the chrome this block doesn't name (minimap mask, handles, background dots). */
3357
+ .w6w-flow {
3358
+ --xy-controls-button-background-color: var(--w6w-panel);
3359
+ --xy-controls-button-background-color-hover: var(--w6w-panel-2);
3360
+ --xy-controls-button-color: var(--w6w-text);
3361
+ --xy-controls-button-color-hover: var(--w6w-accent);
3362
+ --xy-controls-button-border-color: var(--w6w-border);
3363
+ }
3364
+
3365
+ /* An error edge — `Edge.when: "error"` (core rfcs/workflow.md · "Amendment —
3366
+ 2026-07-29: failure-conditioned edges"). Dashed and drawn in the shared
3367
+ --w6w-danger token (never a hard-coded hex) so the failure lane reads
3368
+ differently from the success lane, and correctly, in BOTH themes.
3369
+
3370
+ ORDER MATTERS — this must stay ABOVE the selected/focused block below.
3371
+ Both selectors are (0,4,0), i.e. exactly equal specificity, so source order
3372
+ decides the winner. Selection has to be able to repaint the edge in the
3373
+ accent colour; put this block after that one and a selected error edge stops
3374
+ highlighting, which reads as a bug. */
3375
+ .w6w-flow .react-flow__edge.w6w-edge-error .react-flow__edge-path {
3376
+ stroke: var(--w6w-danger);
3377
+ stroke-dasharray: 6 4;
3378
+ }
3379
+
3380
+ .w6w-flow .react-flow__edge.w6w-edge-error .react-flow__edge-text {
3381
+ fill: var(--w6w-danger);
3382
+ }
3383
+
3384
+ .w6w-flow .react-flow__edge.w6w-edge-error .react-flow__edge-textbg {
3385
+ fill: var(--w6w-panel);
3386
+ }
3387
+
3388
+ /* Run-state edge painting (T1.1.1) — whether a main-graph edge was TAKEN or
3389
+ SKIPPED by the run currently showing on the canvas (see `run-visuals.ts`'s
3390
+ `edgeRunState`). Both classes are ADDITIVE to `.w6w-edge-error` above (an
3391
+ edge can be `w6w-edge-error w6w-edge-taken` at once), so ORDER decides how
3392
+ they compose — same equal-(0,4,0)-specificity discipline `.w6w-edge-error`
3393
+ already documents, kept below it in source:
3394
+
3395
+ - `.w6w-edge-taken`'s STROKE COLOUR is guarded to the non-error case via
3396
+ `:where(:not(.w6w-edge-error))` — `:where()` contributes ZERO
3397
+ specificity, so this stays at the SAME (0,4,0) weight as every other
3398
+ block here instead of out-ranking `.selected` below. A taken ERROR edge
3399
+ (the failure path a run actually followed) keeps the danger colour — the
3400
+ fact of having failed does not stop being true — and only gets the
3401
+ thicker stroke-width (the second, unconditional rule).
3402
+ - `.w6w-edge-skipped` is intentionally UNGUARDED: a skipped error edge
3403
+ (the step succeeded, so its error lane never fired) dims to muted same
3404
+ as a skipped success edge does — the failure never happened, so danger
3405
+ is the wrong colour regardless of which lane it is. */
3406
+ .w6w-flow .react-flow__edge.w6w-edge-taken:where(:not(.w6w-edge-error)) .react-flow__edge-path {
3407
+ stroke: var(--w6w-success);
3408
+ }
3409
+
3410
+ .w6w-flow .react-flow__edge.w6w-edge-taken .react-flow__edge-path {
3411
+ stroke-width: 2.5;
3412
+ }
3413
+
3414
+ .w6w-flow .react-flow__edge.w6w-edge-skipped .react-flow__edge-path {
3415
+ stroke: var(--w6w-muted);
3416
+ stroke-dasharray: 2 3;
3417
+ opacity: 0.55;
3418
+ }
3419
+
3420
+ /* A step's own Notes, shown under its canvas card — human override,
3421
+ 2026-08-30, so a description written once (settings → gear icon) is
3422
+ visible without opening the step. One line, clipped with an ellipsis (the
3423
+ full text is still on the node's `title=` for a hover); `max-width`
3424
+ matches the card's own `minWidth` so a short note never forces the row
3425
+ wider than the card sitting above it. Font size matches the sibling
3426
+ meta-line's own inline fontSize:10 (not scanned by this ratchet) directly
3427
+ above it — the smallest declared token, --w6w-fs-xs, is 12px and would
3428
+ visibly mismatch the two lines. */
3429
+ .w6w-flow .w6w-node-notes {
3430
+ margin-top: var(--w6w-sp-0-5);
3431
+ padding-left: var(--w6w-sp-0-5);
3432
+ /* lint-tokens-allow: matches the sibling meta-line's inline fontSize:10 above it */
3433
+ font-size: 10px;
3434
+ font-style: italic;
3435
+ opacity: 0.75;
3436
+ max-width: 180px;
3437
+ overflow: hidden;
3438
+ text-overflow: ellipsis;
3439
+ white-space: nowrap;
3440
+ }
3441
+
3442
+ /* The ordinary (success) ports — bumped from the library's own 6px default
3443
+ (2026-08-30: "ports must have consistent size") to match the error port
3444
+ below exactly. `:not(.w6w-handle-multi)`: the multi-input handle is its
3445
+ own taller pill shape (`PortHandle`'s inline style), not a dot — this
3446
+ rule must not touch it. */
3447
+ .w6w-flow .react-flow__handle:not(.w6w-handle-multi) {
3448
+ width: 8px;
3449
+ height: 8px;
3450
+ }
3451
+
3452
+ /* The error exit port — a step/control node's second SOURCE handle (T1.1.1),
3453
+ rendered below the ordinary exit via the shared PortHandle component
3454
+ (`id={ERROR_SOURCE_HANDLE}`, `variant="error"`). Danger-tinted, same
3455
+ --w6w-danger token as .w6w-edge-error above (never a hard-coded hex).
3456
+ DELIBERATELY 1px SMALLER, in actual box size, than the ordinary ports
3457
+ above (2026-08-30, second pass on "consistent size"): setting both to the
3458
+ literal same 8px×8px box (the first pass) still read as bigger — measured
3459
+ directly, computed styles for both were byte-identical (same width,
3460
+ height, border-width, box-sizing, no box-shadow/outline difference) — a
3461
+ saturated warm fill on a light canvas background perceptually "expands"
3462
+ past a same-sized dark/cool one (the classic irradiation illusion), so
3463
+ matching PIXELS does not produce matching APPEARANCE. Compensating with a
3464
+ smaller literal box is what actually reads as equal. */
3465
+ .w6w-flow .react-flow__handle.w6w-handle-error {
3466
+ width: 7px;
3467
+ height: 7px;
3468
+ background: var(--w6w-danger);
3469
+ border-color: var(--w6w-danger);
3470
+ opacity: 0.85;
3471
+ }
3472
+
3473
+ .w6w-flow .react-flow__handle.w6w-handle-error:hover {
3474
+ opacity: 1;
3475
+ }
3476
+
3477
+ /* Workflow edges: highlight the selected/focused edge in the accent colour and
3478
+ thicken it, so it's obvious which one Backspace/Delete will remove. The wide,
3479
+ invisible interaction path keeps the click target generous. */
3480
+ .w6w-flow .react-flow__edge.selected .react-flow__edge-path,
3481
+ .w6w-flow .react-flow__edge:focus .react-flow__edge-path,
3482
+ .w6w-flow .react-flow__edge:focus-visible .react-flow__edge-path {
3483
+ stroke: var(--w6w-accent);
3484
+ stroke-width: 2.5;
3485
+ }
3486
+
3487
+ /* The edge-lane control — `Run on: Success / Error`, revealed in a React Flow
3488
+ <Panel position="top-right"> when exactly one edge is selected. Unlike the
3489
+ `+ Step` button (a self-contained .w6w-btn), this row mixes a label, two ghost
3490
+ buttons and an inline refusal, so it needs its own surface to stay legible over
3491
+ the canvas background. Same shape as .w6w-node-toolbar below; tokens only. */
3492
+ .w6w-edge-lane {
3493
+ display: flex;
3494
+ align-items: center;
3495
+ flex-wrap: wrap;
3496
+ gap: var(--w6w-sp-1-5);
3497
+ max-width: 280px;
3498
+ padding: var(--w6w-sp-1) var(--w6w-sp-2);
3499
+ background: var(--w6w-panel);
3500
+ border: 1px solid var(--w6w-border);
3501
+ border-radius: 8px;
3502
+ }
3503
+
3504
+ /* An inline refusal (re-laning would mint an id another edge already holds).
3505
+ Rendered here rather than through a browser dialog — see .ai/conventions.md ·
3506
+ "No browser dialogs" — and on its own row so the buttons don't reflow. */
3507
+ .w6w-edge-lane-err {
3508
+ flex: 1 1 100%;
3509
+ color: var(--w6w-danger);
3510
+ }
3511
+
3512
+ /* Per-node control toolbar (React Flow NodeToolbar) */
3513
+ .w6w-node-toolbar {
3514
+ display: flex;
3515
+ gap: var(--w6w-sp-1);
3516
+ background: var(--w6w-panel);
3517
+ border: 1px solid var(--w6w-border);
3518
+ border-radius: 8px;
3519
+ padding: var(--w6w-sp-1);
3520
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
3521
+ }
3522
+
3523
+ .w6w-node-toolbar-btn {
3524
+ display: inline-flex;
3525
+ align-items: center;
3526
+ justify-content: center;
3527
+ background: transparent;
3528
+ border: none;
3529
+ border-radius: 6px;
3530
+ padding: 5px;
3531
+ color: var(--w6w-text);
3532
+ font-size: var(--w6w-fs-xs);
3533
+ cursor: pointer;
3534
+ }
3535
+
3536
+ .w6w-node-toolbar-btn:hover {
3537
+ background: var(--w6w-panel-2);
3538
+ }
3539
+
3540
+ .w6w-node-toolbar-btn.danger:hover {
3541
+ background: var(--w6w-danger);
3542
+ color: white;
3543
+ }
3544
+
3545
+ .w6w-params-optional > summary {
3546
+ cursor: pointer;
3547
+ }
3548
+
3549
+ /* `section` param — a layout-only container. `section: "collapsible"` renders as
3550
+ a titled, bordered disclosure; `section: "group"` reuses `.w6w-field-row` /
3551
+ `.w6w-stack` for its layout (no rules of its own needed). */
3552
+ /* Fan-out badge under a card: the glyph (⇉ parallel / → sequence), not colour, carries the mode. */
3553
+ .w6w-flow .w6w-fanout-badge {
3554
+ display: inline-block;
3555
+ margin-top: var(--w6w-sp-0-5);
3556
+ padding: 0 var(--w6w-sp-1);
3557
+ border: 1px solid var(--w6w-border);
3558
+ border-radius: var(--w6w-radius-sm, 3px);
3559
+ font-size: var(--w6w-fs-xs);
3560
+ color: var(--w6w-muted, inherit);
3561
+ }
3562
+
3563
+ .w6w-section {
3564
+ border: 1px solid var(--w6w-border);
3565
+ border-radius: 8px;
3566
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
3567
+ }
3568
+
3569
+ .w6w-section > summary {
3570
+ cursor: pointer;
3571
+ display: flex;
3572
+ flex-direction: column;
3573
+ gap: var(--w6w-sp-0-5);
3574
+ }
3575
+
3576
+ .w6w-section-title {
3577
+ color: var(--w6w-text);
3578
+ font-weight: var(--w6w-fw-semibold);
3579
+ }
3580
+
3581
+ .w6w-section-subtitle {
3582
+ color: var(--w6w-muted);
3583
+ font-size: 0.8em;
3584
+ }
3585
+
3586
+ .w6w-section-body {
3587
+ margin-top: var(--w6w-sp-2);
3588
+ }
3589
+
3590
+ /* Inline "Test run" block in the step builder (sits above the footer). */
3591
+ .w6w-steptest {
3592
+ display: flex;
3593
+ flex-direction: column;
3594
+ gap: var(--w6w-sp-2);
3595
+ flex-shrink: 0;
3596
+ }
3597
+
3598
+ .w6w-steptest-bar {
3599
+ display: flex;
3600
+ align-items: center;
3601
+ gap: var(--w6w-sp-2-5);
3602
+ }
3603
+
3604
+ /* A labeled test-run output block (Result vs Console). */
3605
+ .w6w-testout {
3606
+ display: flex;
3607
+ flex-direction: column;
3608
+ gap: var(--w6w-sp-1);
3609
+ }
3610
+
3611
+ .w6w-testout-label {
3612
+ font-size: 0.72em;
3613
+ font-weight: var(--w6w-fw-semibold);
3614
+ letter-spacing: 0.04em;
3615
+ text-transform: uppercase;
3616
+ color: var(--w6w-muted);
3617
+ }
3618
+
3619
+ /* Console output reads as a terminal — monospace + muted, distinct from Result. */
3620
+ .w6w-testout-console {
3621
+ font-family: var(--w6w-font-mono);
3622
+ color: var(--w6w-muted);
3623
+ border-style: dashed;
3624
+ }
3625
+
3626
+ /* Result / error box */
3627
+ .w6w-result {
3628
+ background: var(--w6w-panel-2);
3629
+ border: 1px solid var(--w6w-border);
3630
+ border-radius: 8px;
3631
+ padding: var(--w6w-sp-2-5) var(--w6w-sp-3);
3632
+ font-family: var(--w6w-font-mono);
3633
+ font-size: 0.85em;
3634
+ white-space: pre-wrap;
3635
+ word-break: break-word;
3636
+ color: var(--w6w-text);
3637
+ }
3638
+
3639
+ .w6w-result.w6w-error {
3640
+ border-color: var(--w6w-danger);
3641
+ color: var(--w6w-danger);
3642
+ }
3643
+
3644
+ /* Host-embedded ActionTestForm (studio's ConnectionTesterModal wraps it in its
3645
+ OWN full modal). The widget then fills that modal as a full-height flex column:
3646
+ the params + saved-tests region scrolls, and the action bar is a bottom-pinned,
3647
+ non-scrolling footer. These classes are only applied in `embedded` mode, so the
3648
+ inline / own-pop-out layout is untouched. */
3649
+ .w6w-tester-embedded-root {
3650
+ flex: 1;
3651
+ min-height: 0;
3652
+ }
3653
+
3654
+ .w6w-tester-embedded {
3655
+ display: flex;
3656
+ flex-direction: column;
3657
+ flex: 1;
3658
+ min-height: 0;
3659
+ }
3660
+
3661
+ .w6w-tester-embedded-scroll {
3662
+ flex: 1;
3663
+ min-height: 0;
3664
+ overflow: auto;
3665
+ }
3666
+
3667
+ .w6w-tester-embedded-cols {
3668
+ display: flex;
3669
+ gap: var(--w6w-sp-4);
3670
+ align-items: flex-start;
3671
+ }
3672
+
3673
+ .w6w-tester-embedded-main {
3674
+ flex: 1;
3675
+ min-width: 0;
3676
+ }
3677
+
3678
+ .w6w-tester-embedded-rail {
3679
+ width: 260px;
3680
+ flex-shrink: 0;
3681
+ }
3682
+
3683
+ /* The action bar as a pinned footer: kept out of the scroll region and separated
3684
+ with a top border so Run / Save / Delete stay visible however long the params. */
3685
+ .w6w-tester-actions-footer {
3686
+ margin-top: auto;
3687
+ border-top: 1px solid var(--w6w-border);
3688
+ padding-top: var(--w6w-sp-3);
3689
+ }
3690
+
3691
+ /* ActionTestForm's OWN pop-out (`Modal size="full"`, the "Edit params" ⤢
3692
+ toggle — not the host-embedded case above). T1.4.1 round 1 made `-full`'s
3693
+ `overflow: hidden` live for the first time; the dialog itself no longer
3694
+ scrolls, so at a realistic param count (15-23 is typical for a first-party
3695
+ action, e.g. followupboss/search-people, youtube/search) content below the
3696
+ fold became unreachable with zero scrollbar. This mirrors the
3697
+ `.w6w-tester-embedded-scroll` mechanism above — flex:1/min-height:0 to fill
3698
+ the dialog's remaining flex-column height, `overflow: auto` to own the
3699
+ scrollbar — so the params + result (+ saved-tests rail) region scrolls
3700
+ internally while the Run/Save/Delete action bar stays a pinned,
3701
+ non-scrolling footer via the SAME `.w6w-tester-actions-footer` class. */
3702
+ .w6w-tester-popout {
3703
+ display: flex;
3704
+ flex-direction: column;
3705
+ flex: 1;
3706
+ min-height: 0;
3707
+ }
3708
+
3709
+ .w6w-tester-popout-scroll {
3710
+ flex: 1;
3711
+ min-height: 0;
3712
+ overflow: auto;
3713
+ }
3714
+
3715
+ /* Uptime strip — one cell per day, "how has this service behaved lately" at a
3716
+ glance. Colour comes from the same `--w6w-health-*` tokens as the health pill,
3717
+ through the same single `--w6w-health-color` indirection (set once on the cell,
3718
+ remapped by the per-state class below), so overriding a token restyles the pill
3719
+ and the strip together.
3720
+
3721
+ Colour is never the only signal: the FILL HEIGHT encodes severity, so the row
3722
+ stays readable in greyscale and for a colour-blind viewer — `ok` a short
3723
+ baseline bar, `degraded` taller, `down` the full cell, and `unknown` a hatched
3724
+ outline with no solid fill (nothing was measured: not a good day, not a bad
3725
+ one). Each cell also carries a `title` with its day and state. */
3726
+ .w6w-uptime-strip {
3727
+ display: grid;
3728
+ grid-auto-flow: column;
3729
+ grid-auto-columns: minmax(0, 1fr);
3730
+ gap: var(--w6w-sp-0-5);
3731
+ height: 22px;
3732
+ min-width: 0;
3733
+ align-items: stretch;
3734
+ }
3735
+
3736
+ .w6w-uptime-cell {
3737
+ --w6w-health-color: var(--w6w-health-unknown);
3738
+ --w6w-uptime-fill: 100%;
3739
+ position: relative;
3740
+ min-width: 2px;
3741
+ border-radius: 2px;
3742
+ /* The unfilled remainder of the day, so the strip reads as a continuous row
3743
+ rather than as floating bars. */
3744
+ background: color-mix(in srgb, var(--w6w-border) 55%, transparent);
3745
+ overflow: hidden;
3746
+ }
3747
+
3748
+ .w6w-uptime-cell::after {
3749
+ content: "";
3750
+ position: absolute;
3751
+ left: 0;
3752
+ right: 0;
3753
+ bottom: 0;
3754
+ height: var(--w6w-uptime-fill);
3755
+ border-radius: inherit;
3756
+ background: var(--w6w-health-color);
3757
+ }
3758
+
3759
+ .w6w-uptime-cell-ok {
3760
+ --w6w-health-color: var(--w6w-health-ok);
3761
+ --w6w-uptime-fill: 35%;
3762
+ }
3763
+
3764
+ .w6w-uptime-cell-degraded {
3765
+ --w6w-health-color: var(--w6w-health-degraded);
3766
+ --w6w-uptime-fill: 65%;
3767
+ }
3768
+
3769
+ .w6w-uptime-cell-down {
3770
+ --w6w-health-color: var(--w6w-health-down);
3771
+ --w6w-uptime-fill: 100%;
3772
+ }
3773
+
3774
+ .w6w-uptime-cell-unknown {
3775
+ --w6w-health-color: var(--w6w-health-unknown);
3776
+ --w6w-uptime-fill: 100%;
3777
+ }
3778
+
3779
+ /* No solid fill for `unknown` — a hatch, so an unmeasured day can never be
3780
+ mistaken for a healthy or a failing one, with or without colour. */
3781
+ .w6w-uptime-cell-unknown::after {
3782
+ background: repeating-linear-gradient(45deg, color-mix(in srgb, var(--w6w-health-color) 45%, transparent) 0 2px, transparent 2px 5px);
3783
+ border: 1px solid color-mix(in srgb, var(--w6w-health-color) 35%, var(--w6w-border));
3784
+ }
3785
+
3786
+ /* --- CodeBlock ------------------------------------------------------------ */
3787
+ /* Read-only syntax-highlighted display (`<CodeBlock>`). The colours come from
3788
+ the `--w6w-code-*` tokens via inline styles Prism sets, so this rule only
3789
+ owns box, rhythm and overflow. */
3790
+ .w6w-history-timeline {
3791
+ display: flex;
3792
+ flex-direction: column;
3793
+ gap: var(--w6w-sp-2);
3794
+ width: 100%;
3795
+ }
3796
+
3797
+ /* One positioned container shared by both lanes AND the axis, so every
3798
+ `left`/`width` percentage in any of the three is on the same coordinate
3799
+ system — this is what keeps the two series from drifting apart. */
3800
+ .w6w-history-plot {
3801
+ position: relative;
3802
+ display: flex;
3803
+ flex-direction: column;
3804
+ gap: var(--w6w-sp-1);
3805
+ }
3806
+
3807
+ .w6w-history-lane {
3808
+ position: relative;
3809
+ height: 22px;
3810
+ min-width: 0;
3811
+ }
3812
+
3813
+ /* --- execution lane -------------------------------------------------- */
3814
+ .w6w-history-cell {
3815
+ --w6w-health-color: var(--w6w-health-unknown);
3816
+ --w6w-history-fill: 100%;
3817
+ position: absolute;
3818
+ top: 0;
3819
+ bottom: 0;
3820
+ min-width: 1px;
3821
+ border-radius: 2px;
3822
+ overflow: hidden;
3823
+ /* The unfilled remainder of the cell, so the lane reads as a continuous
3824
+ row rather than as floating marks — same treatment as `UptimeStrip`. */
3825
+ background: color-mix(in srgb, var(--w6w-border) 55%, transparent);
3826
+ }
3827
+
3828
+ .w6w-history-cell::after {
3829
+ content: "";
3830
+ position: absolute;
3831
+ left: 0;
3832
+ right: 0;
3833
+ bottom: 0;
3834
+ height: var(--w6w-history-fill);
3835
+ border-radius: inherit;
3836
+ background: var(--w6w-health-color);
3837
+ }
3838
+
3839
+ .w6w-history-cell-ok {
3840
+ --w6w-health-color: var(--w6w-health-ok);
3841
+ --w6w-history-fill: 35%;
3842
+ }
3843
+
3844
+ .w6w-history-cell-degraded {
3845
+ --w6w-health-color: var(--w6w-health-degraded);
3846
+ --w6w-history-fill: 65%;
3847
+ }
3848
+
3849
+ .w6w-history-cell-down {
3850
+ --w6w-health-color: var(--w6w-health-down);
3851
+ --w6w-history-fill: 100%;
3852
+ }
3853
+
3854
+ .w6w-history-cell-unknown {
3855
+ --w6w-health-color: var(--w6w-health-unknown);
3856
+ --w6w-history-fill: 100%;
3857
+ }
3858
+
3859
+ /* No solid fill for `unknown` — a hatch, same reasoning as `UptimeStrip`'s:
3860
+ an unmeasured day must never be mistaken for a healthy or a failing one. */
3861
+ .w6w-history-cell-unknown::after {
3862
+ background: repeating-linear-gradient(45deg, color-mix(in srgb, var(--w6w-health-color) 45%, transparent) 0 2px, transparent 2px 5px);
3863
+ border: 1px solid color-mix(in srgb, var(--w6w-health-color) 35%, var(--w6w-border));
3864
+ }
3865
+
3866
+ /* --- vendor lane ------------------------------------------------------- */
3867
+ .w6w-history-bar {
3868
+ --w6w-health-color: var(--w6w-health-unknown);
3869
+ position: absolute;
3870
+ /* Thickness is the geometry half of severity — thin for `ok`, thicker for
3871
+ `degraded`, full lane height for `down`/`unknown` — using the spacing
3872
+ scale rather than a one-off literal so it never drifts from it. */
3873
+ top: var(--w6w-sp-1);
3874
+ bottom: var(--w6w-sp-1);
3875
+ min-width: 2px;
3876
+ border-radius: 2px;
3877
+ background: var(--w6w-health-color);
3878
+ }
3879
+
3880
+ .w6w-history-bar-ok {
3881
+ --w6w-health-color: var(--w6w-health-ok);
3882
+ }
3883
+
3884
+ .w6w-history-bar-degraded {
3885
+ --w6w-health-color: var(--w6w-health-degraded);
3886
+ top: var(--w6w-sp-0-5);
3887
+ bottom: var(--w6w-sp-0-5);
3888
+ }
3889
+
3890
+ .w6w-history-bar-down {
3891
+ --w6w-health-color: var(--w6w-health-down);
3892
+ top: 0;
3893
+ bottom: 0;
3894
+ }
3895
+
3896
+ .w6w-history-bar-unknown {
3897
+ --w6w-health-color: var(--w6w-health-unknown);
3898
+ top: 0;
3899
+ bottom: 0;
3900
+ background: repeating-linear-gradient(45deg, color-mix(in srgb, var(--w6w-health-color) 45%, transparent) 0 2px, transparent 2px 5px);
3901
+ border: 1px solid color-mix(in srgb, var(--w6w-health-color) 35%, var(--w6w-border));
3902
+ }
3903
+
3904
+ /* A clipped edge is squared off rather than rounded, so a bar that runs off
3905
+ the window reads as continuing beyond it rather than as ending there. */
3906
+ .w6w-history-bar-clip-start {
3907
+ border-top-left-radius: 0;
3908
+ border-bottom-left-radius: 0;
3909
+ }
3910
+
3911
+ .w6w-history-bar-clip-end {
3912
+ border-top-right-radius: 0;
3913
+ border-bottom-right-radius: 0;
3914
+ }
3915
+
3916
+ .w6w-history-empty {
3917
+ position: absolute;
3918
+ inset: 0;
3919
+ display: flex;
3920
+ align-items: center;
3921
+ color: var(--w6w-muted);
3922
+ font-size: var(--w6w-fs-xs);
3923
+ white-space: nowrap;
3924
+ overflow: hidden;
3925
+ text-overflow: ellipsis;
3926
+ }
3927
+
3928
+ /* --- axis --------------------------------------------------------------- */
3929
+ .w6w-history-axis {
3930
+ position: relative;
3931
+ height: 16px;
3932
+ }
3933
+
3934
+ .w6w-history-tick {
3935
+ position: absolute;
3936
+ top: 0;
3937
+ color: var(--w6w-muted);
3938
+ font-size: var(--w6w-fs-xs);
3939
+ line-height: var(--w6w-lh-normal);
3940
+ white-space: nowrap;
3941
+ }
3942
+
3943
+ .w6w-history-tick-start {
3944
+ transform: translateX(0);
3945
+ }
3946
+
3947
+ .w6w-history-tick-end {
3948
+ transform: translateX(-100%);
3949
+ }
3950
+
3951
+ .w6w-code-block {
3952
+ background: var(--w6w-panel-2);
3953
+ border: 1px solid var(--w6w-border);
3954
+ border-radius: var(--w6w-radius);
3955
+ margin: 0;
3956
+ overflow-x: auto;
3957
+ padding: var(--w6w-sp-3) 14px;
3958
+ tab-size: 2;
3959
+ }
3960
+
3961
+ .w6w-code-block code {
3962
+ background: none;
3963
+ display: block;
3964
+ font-family: var(--w6w-font-mono);
3965
+ font-size: 13px;
3966
+ line-height: 1.55;
3967
+ padding: 0;
3968
+ }
3969
+
3970
+ /* Each line is its own row so a line number can sit beside the text rather
3971
+ than inside it — a number inside the flow would be caught by a copy. */
3972
+ .w6w-code-line {
3973
+ display: flex;
3974
+ gap: var(--w6w-sp-3);
3975
+ white-space: pre;
3976
+ }
3977
+
3978
+ .w6w-code-line-text {
3979
+ white-space: inherit;
3980
+ }
3981
+
3982
+ /* `user-select: none` keeps the gutter out of a mouse selection; `aria-hidden`
3983
+ on the element keeps it out of the accessibility tree. Both are needed —
3984
+ neither one alone stops a copy from picking up the digits. */
3985
+ .w6w-code-line-no {
3986
+ color: var(--w6w-muted);
3987
+ flex: 0 0 auto;
3988
+ min-width: 2ch;
3989
+ opacity: 0.65;
3990
+ text-align: right;
3991
+ user-select: none;
3992
+ }
3993
+
3994
+ /* Wrapped mode indents continuation rows to the text column so a wrapped line
3995
+ still reads as one line. */
3996
+ .w6w-code-block--wrap .w6w-code-line,
3997
+ .w6w-code-block--wrap .w6w-code-line-text {
3998
+ white-space: pre-wrap;
3999
+ word-break: break-word;
4000
+ }
4001
+
4002
+ .w6w-code-block--wrap {
4003
+ overflow-x: hidden;
4004
+ }
4005
+
4006
+ /* Resolved params (Test tab, read-only): inline badge weight — like
4007
+ JsonParamField's "Invalid JSON" hint, not a full `.w6w-result.w6w-error`
4008
+ block per param, since either can appear once per configured param. */
4009
+ .w6w-param-unresolved {
4010
+ color: var(--w6w-danger);
4011
+ font-size: 0.85em;
4012
+ font-style: italic;
4013
+ }
4014
+
4015
+ .w6w-param-unevaluated {
4016
+ color: var(--w6w-muted);
4017
+ font-size: 0.85em;
4018
+ font-style: italic;
4019
+ }
4020
+
4021
+ /* One resolved param: a table row, not a stacked form field — this list is
4022
+ read-only display, never an editable `.w6w-field`, so it earns its own
4023
+ compact key-left/value-right layout instead of a label-above-control block
4024
+ (human report, 2026-09-01: key and value were indistinguishable and every
4025
+ row burned a full stacked block of vertical space). ONE grid across the
4026
+ whole list (`.w6w-resolved-table`), not one grid per row, so every row's
4027
+ key column lines up with the one above and below it. */
4028
+ .w6w-resolved-table {
4029
+ display: grid;
4030
+ grid-template-columns: auto 1fr;
4031
+ align-items: baseline;
4032
+ }
4033
+
4034
+ /* Each row is `display: contents` — it renders no box of its own, so its two
4035
+ children (label, value) become direct items of the parent grid above,
4036
+ sharing its column tracks instead of sizing their own. */
4037
+ .w6w-resolved-row {
4038
+ display: contents;
4039
+ font-size: calc(var(--w6w-fs-sm) * 0.9);
4040
+ }
4041
+
4042
+ /* No `column-gap` on the grid: a gap leaves a hole no cell's own border
4043
+ crosses, breaking the row divider into two segments. The same visual
4044
+ space between key and value comes from padding instead, which is part of
4045
+ each cell's own box — the border-bottom stays one unbroken line. */
4046
+ .w6w-resolved-row > * {
4047
+ padding: var(--w6w-sp-1-5) 0;
4048
+ border-bottom: 1px solid var(--w6w-border);
4049
+ }
4050
+
4051
+ .w6w-resolved-row:last-child > * {
4052
+ border-bottom: none;
4053
+ }
4054
+
4055
+ .w6w-resolved-label {
4056
+ padding-right: var(--w6w-sp-3);
4057
+ color: var(--w6w-muted);
4058
+ }
4059
+
4060
+ /* One resolved row's value. `anywhere` (not `break-word`) because these values
4061
+ are markup and URLs as often as prose: a single unbroken 40kB attribute
4062
+ string would otherwise force the whole modal to scroll sideways. Full-
4063
+ strength text color (not `--w6w-muted`) so it reads as the actual content,
4064
+ distinct from the label beside it. */
4065
+ .w6w-resolved-value {
4066
+ color: var(--w6w-text);
4067
+ overflow-wrap: anywhere;
4068
+ min-width: 0;
4069
+ }
4070
+
4071
+ /* The expanded form of a clipped value (`resolved-clip.ts`). A block with its
4072
+ own scroll, so "Show all" can never restore the unbounded inline paste that
4073
+ buried the other params and the run's result. */
4074
+ .w6w-resolved-text-full {
4075
+ display: block;
4076
+ margin: var(--w6w-sp-1) 0;
4077
+ max-height: 14em;
4078
+ overflow: auto;
4079
+ padding: var(--w6w-sp-2);
4080
+ background: var(--w6w-panel-2);
4081
+ border: 1px solid var(--w6w-border);
4082
+ border-radius: var(--w6w-radius);
4083
+ font-family: var(--w6w-font-mono);
4084
+ font-size: 0.85em; /* lint-tokens-allow: matches the sibling code-typography rules above in this same file */
4085
+ line-height: var(--w6w-lh-normal);
4086
+ white-space: pre-wrap;
4087
+ }
4088
+
4089
+ /* Link-weight toggle — this sits inside a value, so it must not read as a
4090
+ second action competing with the modal's own buttons. */
4091
+ .w6w-resolved-more {
4092
+ background: none;
4093
+ border: none;
4094
+ padding: 0;
4095
+ margin-left: var(--w6w-sp-2);
4096
+ color: var(--w6w-accent);
4097
+ font-size: 0.85em; /* lint-tokens-allow: matches the sibling code-typography rules above in this same file */
4098
+ cursor: pointer;
4099
+ white-space: nowrap;
4100
+ }
4101
+
4102
+ .w6w-resolved-more:hover {
4103
+ text-decoration: underline;
4104
+ }
4105
+
4106
+ .w6w-icon-button {
4107
+ display: inline-flex;
4108
+ align-items: center;
4109
+ justify-content: center;
4110
+ background: transparent;
4111
+ border: none;
4112
+ border-radius: 8px;
4113
+ padding: var(--w6w-sp-1-5);
4114
+ color: var(--w6w-muted);
4115
+ cursor: pointer;
4116
+ }
4117
+
4118
+ .w6w-icon-button:hover:not(:disabled) {
4119
+ background: var(--w6w-panel-2);
4120
+ color: var(--w6w-text);
4121
+ }
4122
+
4123
+ .w6w-icon-button-danger:hover:not(:disabled) {
4124
+ color: var(--w6w-danger);
4125
+ }
4126
+
4127
+ .w6w-icon-button:disabled {
4128
+ opacity: 0.4;
4129
+ cursor: default;
4130
+ }
4131
+
4132
+ .w6w-info-tip {
4133
+ display: inline-flex;
4134
+ flex-wrap: wrap;
4135
+ align-items: center;
4136
+ gap: var(--w6w-sp-1);
4137
+ }
4138
+
4139
+ .w6w-info-tip-body {
4140
+ flex-basis: 100%;
4141
+ }
4142
+
4143
+ /* The shared `Icon` (`src/components/Icon.tsx`).
4144
+ *
4145
+ * Only geometry invariants live here. The PAINT is per-entry in
4146
+ * `src/components/icons.tsx` (the set mixes filled Material-style glyphs with
4147
+ * stroked Feather ones, and the language marks paint their own elements), and
4148
+ * the SIZE comes from the `size` prop — so there is nothing about colour or
4149
+ * scale for this class to say. */
4150
+ .w6w-icon {
4151
+ /* Never squashed by a flex parent: a rail glyph squeezed to 9px is unreadable,
4152
+ and the sources draw at 14–20px inside rows that can be tight. */
4153
+ flex-shrink: 0;
4154
+ /* A glyph dropped into a line of text sits on the baseline by default, which
4155
+ reads as a foot below the words. */
4156
+ vertical-align: middle;
4157
+ }
4158
+
4159
+ .w6w-repo-sync {
4160
+ position: relative;
4161
+ display: inline-flex;
4162
+ }
4163
+
4164
+ .w6w-repo-sync-trigger {
4165
+ display: inline-flex;
4166
+ align-items: center;
4167
+ gap: var(--w6w-sp-2);
4168
+ padding: var(--w6w-sp-1-5) var(--w6w-sp-3);
4169
+ border: 1px solid var(--w6w-border);
4170
+ border-radius: 999px;
4171
+ background: var(--w6w-panel-2);
4172
+ color: var(--w6w-text);
4173
+ font-size: var(--w6w-fs-sm);
4174
+ cursor: pointer;
4175
+ }
4176
+
4177
+ .w6w-repo-sync-trigger:hover {
4178
+ border-color: var(--w6w-accent);
4179
+ }
4180
+
4181
+ .w6w-repo-sync-icon {
4182
+ position: relative;
4183
+ display: inline-flex;
4184
+ }
4185
+
4186
+ .w6w-repo-sync-spin {
4187
+ position: absolute;
4188
+ bottom: calc(-1 * var(--w6w-sp-0-5));
4189
+ right: calc(-1 * var(--w6w-sp-0-5));
4190
+ width: 10px;
4191
+ height: 10px;
4192
+ background: var(--w6w-panel-2);
4193
+ border-radius: 50%;
4194
+ animation: w6w-spin 0.8s linear infinite;
4195
+ }
4196
+
4197
+ @keyframes w6w-spin {
4198
+ to {
4199
+ transform: rotate(360deg);
4200
+ }
4201
+ }
4202
+ @media (prefers-reduced-motion: reduce) {
4203
+ .w6w-repo-sync-spin {
4204
+ animation: none;
4205
+ }
4206
+ }
4207
+ .w6w-repo-sync-branch {
4208
+ font-weight: var(--w6w-fw-medium);
4209
+ }
4210
+
4211
+ .w6w-repo-sync-sha {
4212
+ color: var(--w6w-muted);
4213
+ font-size: var(--w6w-fs-xs);
4214
+ font-family: var(--w6w-font-mono);
4215
+ }
4216
+
4217
+ .w6w-repo-sync-menu {
4218
+ position: absolute;
4219
+ top: calc(100% + var(--w6w-sp-1-5));
4220
+ right: 0;
4221
+ min-width: 220px;
4222
+ background: var(--w6w-panel);
4223
+ border: 1px solid var(--w6w-border);
4224
+ border-radius: var(--w6w-radius);
4225
+ padding: var(--w6w-sp-1-5);
4226
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
4227
+ z-index: 30;
4228
+ display: flex;
4229
+ flex-direction: column;
4230
+ gap: var(--w6w-sp-0-5);
4231
+ }
4232
+
4233
+ .w6w-repo-sync-detail {
4234
+ display: flex;
4235
+ flex-direction: column;
4236
+ gap: var(--w6w-sp-0-5);
4237
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
4238
+ color: var(--w6w-muted);
4239
+ font-size: var(--w6w-fs-xs);
4240
+ }
4241
+
4242
+ .w6w-repo-sync-detail > :first-child {
4243
+ color: var(--w6w-text);
4244
+ font-size: var(--w6w-fs-sm);
4245
+ font-weight: var(--w6w-fw-medium);
4246
+ }
4247
+
4248
+ .w6w-repo-sync-item {
4249
+ display: flex;
4250
+ align-items: center;
4251
+ width: 100%;
4252
+ text-align: left;
4253
+ background: transparent;
4254
+ border: none;
4255
+ border-radius: 8px;
4256
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
4257
+ color: var(--w6w-text);
4258
+ font-size: var(--w6w-fs-sm);
4259
+ cursor: pointer;
4260
+ }
4261
+
4262
+ .w6w-repo-sync-item:hover:not(:disabled) {
4263
+ background: var(--w6w-panel-2);
4264
+ }
4265
+
4266
+ .w6w-repo-sync-item:disabled {
4267
+ cursor: default;
4268
+ opacity: 0.6;
4269
+ }
4270
+
4271
+ /* --- StepStatusPill ---------------------------------------------------- */
4272
+ /* Mirrors `.w6w-health-pill` shape for shape (see `_health.scss`), with its
4273
+ own five-state colour set behind `--w6w-step-color` — never a new global
4274
+ `--w6w-*` token, only the base palette (`--w6w-muted`/`--w6w-accent`/
4275
+ `--w6w-success`/`--w6w-danger`) this file is scoped to. */
4276
+ .w6w-step-pill {
4277
+ display: inline-flex;
4278
+ align-items: center;
4279
+ gap: var(--w6w-sp-1-5);
4280
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2) var(--w6w-sp-0-5) var(--w6w-sp-1-5);
4281
+ border: 1px solid color-mix(in srgb, var(--w6w-step-color) 40%, var(--w6w-border));
4282
+ border-radius: 999px;
4283
+ background: color-mix(in srgb, var(--w6w-step-color) 12%, transparent);
4284
+ color: var(--w6w-text);
4285
+ font-size: var(--w6w-fs-xs);
4286
+ line-height: var(--w6w-lh-normal);
4287
+ white-space: nowrap;
4288
+ --w6w-step-color: var(--w6w-muted);
4289
+ }
4290
+
4291
+ .w6w-step-pill-pending {
4292
+ --w6w-step-color: var(--w6w-muted);
4293
+ }
4294
+
4295
+ .w6w-step-pill-running {
4296
+ --w6w-step-color: var(--w6w-accent);
4297
+ }
4298
+
4299
+ .w6w-step-pill-succeeded {
4300
+ --w6w-step-color: var(--w6w-success);
4301
+ }
4302
+
4303
+ .w6w-step-pill-failed {
4304
+ --w6w-step-color: var(--w6w-danger);
4305
+ }
4306
+
4307
+ /* Same base colour as `pending` — a dashed border, not a second colour, is
4308
+ what keeps this distinct: colour alone is never the only signal, and this
4309
+ pill already carries a different text label too. */
4310
+ .w6w-step-pill-skipped {
4311
+ --w6w-step-color: var(--w6w-muted);
4312
+ border-style: dashed;
4313
+ }
4314
+
4315
+ /* The two RUN states (`ExecutionStatus`), both deliberately in the same muted
4316
+ family as `pending`/`skipped` above: neither is a verdict on the work —
4317
+ `queued` hasn't started and `canceled` stopped before reaching one — so
4318
+ neither earns `--w6w-success`/`--w6w-danger`/`--w6w-accent`, and this file
4319
+ keeps its stated scope (the base palette only, never a new global token).
4320
+ `canceled` takes `skipped`'s dashed border for the same reason that one
4321
+ does; the two never co-occur in one list (one is a step state, the other a
4322
+ run state), and the label carries the meaning regardless. */
4323
+ .w6w-step-pill-queued {
4324
+ --w6w-step-color: var(--w6w-muted);
4325
+ }
4326
+
4327
+ .w6w-step-pill-canceled {
4328
+ --w6w-step-color: var(--w6w-muted);
4329
+ border-style: dashed;
4330
+ }
4331
+
4332
+ .w6w-step-pill-dot {
4333
+ flex-shrink: 0;
4334
+ width: 8px;
4335
+ height: 8px;
4336
+ border-radius: 50%;
4337
+ background: var(--w6w-step-color);
4338
+ }
4339
+
4340
+ .w6w-step-pill-label {
4341
+ overflow: hidden;
4342
+ text-overflow: ellipsis;
4343
+ }
4344
+
4345
+ /* --- ExecutionLogPanel --------------------------------------------------- */
4346
+ /* A vertical ordered list, not `_history-timeline.scss`'s idiom: that
4347
+ partial lays out a fixed WINDOW of time-proportional lanes (`left`/`width`
4348
+ percentages against a shared axis) for a health-history strip, which has
4349
+ no notion of "one row per step" or of steps that haven't happened yet.
4350
+ This panel is a plain in-order list of a priori unbounded rows, closer in
4351
+ shape to `.w6w-resolved-table`'s row list — reused here as `.w6w-stack`
4352
+ (`_layout.scss`) for the two-block (input/output) stack inside a row,
4353
+ rather than inventing a second vertical-list treatment.
4354
+
4355
+ Each row's own box (border/background/border-radius/padding) comes from
4356
+ `.w6w-section` (`_sections.scss`), reused rather than duplicated here —
4357
+ `ApiCallsPanel.tsx`'s collapsed-by-default `<details>` idiom, applied to
4358
+ this panel's rows (REVIEW.md R-1). This file only adds the row-specific
4359
+ layout `.w6w-section` doesn't know about: the flex line for the summary's
4360
+ pill/label/timing. */
4361
+ /* The optional header row (T1.1.1 A4) — rendered only when the host supplies
4362
+ `onDismiss`, and in BOTH branches below, since the empty state is exactly
4363
+ where a dismiss control is needed most. Right-aligned, mirroring where
4364
+ studio's own `.notice-close` dismiss sits on a panel header; that class is
4365
+ studio-scoped CSS and does not exist in this package, so only the placement
4366
+ transfers, not the rule. */
4367
+ .w6w-execution-log-head {
4368
+ display: flex;
4369
+ justify-content: flex-end;
4370
+ align-items: center;
4371
+ gap: var(--w6w-sp-2);
4372
+ width: 100%;
4373
+ margin-bottom: var(--w6w-sp-2);
4374
+ }
4375
+
4376
+ .w6w-execution-log {
4377
+ display: flex;
4378
+ flex-direction: column;
4379
+ gap: var(--w6w-sp-3);
4380
+ list-style: none;
4381
+ margin: 0;
4382
+ padding: 0;
4383
+ width: 100%;
4384
+ }
4385
+
4386
+ /* `width: 100%` plus padding and a border overflows the parent under the
4387
+ default content-box sizing; this package has no global box-sizing reset, so
4388
+ it opts in per element (as `_chips.scss` and `_step-config.scss` do). */
4389
+ .w6w-execution-log-empty {
4390
+ box-sizing: border-box;
4391
+ width: 100%;
4392
+ padding: var(--w6w-sp-4);
4393
+ border: 1px dashed var(--w6w-border);
4394
+ border-radius: var(--w6w-radius);
4395
+ text-align: center;
4396
+ }
4397
+
4398
+ .w6w-execution-log-row {
4399
+ min-width: 0;
4400
+ }
4401
+
4402
+ .w6w-execution-log-row-header {
4403
+ display: flex;
4404
+ align-items: center;
4405
+ gap: var(--w6w-sp-2);
4406
+ flex-wrap: wrap;
4407
+ }
4408
+
4409
+ .w6w-execution-log-row-label {
4410
+ flex: 1 1 auto;
4411
+ min-width: 0;
4412
+ font-weight: var(--w6w-fw-medium);
4413
+ overflow: hidden;
4414
+ text-overflow: ellipsis;
4415
+ white-space: nowrap;
4416
+ }
4417
+
4418
+ /* Shrinkable, so a narrow row (the 360px docked panel) wraps the
4419
+ "<ts> → <ts> (elapsed)" text inside the row's box. With `flex-shrink: 0` it
4420
+ kept its max-content width, wrapped onto its own flex line, and ran past the
4421
+ row's border. */
4422
+ .w6w-execution-log-timing {
4423
+ flex: 0 1 auto;
4424
+ min-width: 0;
4425
+ color: var(--w6w-muted);
4426
+ font-size: var(--w6w-fs-xs);
4427
+ }
4428
+
4429
+ .w6w-execution-log-json {
4430
+ min-width: 0;
4431
+ }
4432
+
4433
+ /* Error blocks (step `error`, run `runError`/`stepErrors`). The box itself is
4434
+ `.w6w-result.w6w-error` (`_tester.scss`) — TriggerFillForm's StepError
4435
+ visual, reused, not restyled here; these rules only place it. */
4436
+ .w6w-execution-log-errors {
4437
+ width: 100%;
4438
+ margin-bottom: var(--w6w-sp-2);
4439
+ }
4440
+
4441
+ /* A step's error sits after its `.w6w-section` box, inside the same `<li>` —
4442
+ closer to its own row than the list's row gap, so it reads as that row's. */
4443
+ .w6w-execution-log-row > .w6w-error {
4444
+ margin-top: var(--w6w-sp-1-5);
4445
+ }
4446
+
4447
+ /* TriggerFillForm's inline `opacity: 0.75; marginBottom: 4` code line, as tokens. */
4448
+ .w6w-execution-log-error-code {
4449
+ opacity: 0.75;
4450
+ margin-bottom: var(--w6w-sp-1);
4451
+ }
4452
+
4453
+ .w6w-execution-log-error-label {
4454
+ display: block;
4455
+ margin-bottom: var(--w6w-sp-1);
4456
+ }
4457
+
4458
+ /* --- ExecutionList ------------------------------------------------------- */
4459
+ /* Rows of past executions: a click-to-select list plus a prev/next pager.
4460
+ Closer in shape to `.w6w-execution-log` (`_execution-log.scss`) than to
4461
+ `.w6w-history-*`: that family lays out a time-proportional strip against a
4462
+ shared axis, while this is a plain in-order list of one row per run. The row
4463
+ itself is `ListItem` (`.w6w-list-item`, `_lists.scss`) — reused whole, box,
4464
+ hover, focus ring, `active` highlight and all — so this partial only owns
4465
+ the LIST around the rows: the `ul` reset and the pager strip.
4466
+
4467
+ Spacing/type come from the scale tokens only (the `lint:tokens` ratchet has
4468
+ no baseline entry for this file, so anything literal here would be a new
4469
+ violation); colour stays on `_execution-log.scss`'s pill and the shared
4470
+ `.w6w-btn` classes. */
4471
+ .w6w-execution-list {
4472
+ display: flex;
4473
+ flex-direction: column;
4474
+ gap: var(--w6w-sp-3);
4475
+ width: 100%;
4476
+ min-width: 0;
4477
+ }
4478
+
4479
+ /* A `<ul>`, reset to a bare vertical stack — the marker/indent are the only
4480
+ things `_layout.scss`'s `.w6w-stack` doesn't already strip, and rows are
4481
+ `<li>` so the list is announced as a list, not a run of cards. */
4482
+ .w6w-execution-list-rows {
4483
+ display: flex;
4484
+ flex-direction: column;
4485
+ gap: var(--w6w-sp-1);
4486
+ list-style: none;
4487
+ margin: 0;
4488
+ padding: 0;
4489
+ width: 100%;
4490
+ min-width: 0;
4491
+ }
4492
+
4493
+ .w6w-execution-list-rows > li {
4494
+ min-width: 0;
4495
+ }
4496
+
4497
+ /* Both non-list branches (the loading and the empty `<p>`) hang straight off
4498
+ the root, so one direct-child rule covers them and neither needs a class the
4499
+ pinned markup doesn't have. Same dashed box `.w6w-execution-log-empty`
4500
+ uses — one treatment for "nothing here yet" across the run-log family. */
4501
+ .w6w-execution-list > p {
4502
+ width: 100%;
4503
+ margin: 0;
4504
+ padding: var(--w6w-sp-4);
4505
+ border: 1px dashed var(--w6w-border);
4506
+ border-radius: var(--w6w-radius);
4507
+ color: var(--w6w-muted);
4508
+ font-size: var(--w6w-fs-sm);
4509
+ text-align: center;
4510
+ }
4511
+
4512
+ .w6w-execution-list-pager {
4513
+ display: flex;
4514
+ align-items: center;
4515
+ justify-content: flex-end;
4516
+ gap: var(--w6w-sp-2);
4517
+ }
4518
+
4519
+ /* --- ExecutionFilters --------------------------------------------------- */
4520
+ /* One dense row of heterogeneous controls. The control chrome cannot come from
4521
+ `_fields.scss`: those rules are anchored on `.w6w-field` (a labelled,
4522
+ stacked field with a visible `<span>`), and these controls are labelled
4523
+ accessibly (`aria-label`) instead — a visible label per control would triple
4524
+ the bar's height. Tokens only, for the same reason as the rules above. */
4525
+ /* The row is width-driven: a wrapped line is formed from each control's
4526
+ *hypothetical* size, so six controls plus their gaps sit on one line for as
4527
+ long as they fit, and the row breaks by itself once they don't. That is the
4528
+ behaviour this bar needs — it renders full width in the studio History page
4529
+ and ~500px wide inside the detail modal — so the break follows THIS box
4530
+ rather than the viewport, and nothing here may key off a media query. */
4531
+ .w6w-execution-filters {
4532
+ display: flex;
4533
+ flex-wrap: wrap;
4534
+ align-items: center;
4535
+ gap: var(--w6w-sp-2);
4536
+ width: 100%;
4537
+ min-width: 0;
4538
+ }
4539
+
4540
+ /* These are bare form elements, so a host's own global `input`/`select` chrome
4541
+ lands on them too — and in the studio it is decisive: `studio/src/styles.css`
4542
+ gives `input[type="search"]`/`input[type="date"]` a `width: 100%` and a
4543
+ `margin-top`. A `width: 100%` on a flex item whose `flex-basis` is `auto` IS
4544
+ that item's basis, so each date picker asked for the whole bar and the row
4545
+ broke into one control per line, while the margin lifted the two inputs off
4546
+ the selects' line. Both are pinned back here; at (0,2,1) this selector beats
4547
+ a host's bare `input[type=…]` (0,1,1) and `input` (0,0,1) rules without
4548
+ needing `!important`, and a host that leaves the controls alone is unaffected
4549
+ because `width: auto`/`margin: 0` are what these controls want anyway. */
4550
+ .w6w-execution-filters input[type=search],
4551
+ .w6w-execution-filters input[type=date],
4552
+ .w6w-execution-filters select {
4553
+ background: var(--w6w-panel-2);
4554
+ border: 1px solid var(--w6w-border);
4555
+ border-radius: var(--w6w-radius);
4556
+ padding: var(--w6w-sp-2) var(--w6w-sp-2-5);
4557
+ margin: 0;
4558
+ width: auto;
4559
+ color: var(--w6w-text);
4560
+ font-size: var(--w6w-fs-sm);
4561
+ font-family: inherit;
4562
+ }
4563
+
4564
+ .w6w-execution-filters input[type=search]:focus-visible,
4565
+ .w6w-execution-filters input[type=date]:focus-visible,
4566
+ .w6w-execution-filters select:focus-visible {
4567
+ outline: 2px solid var(--w6w-accent);
4568
+ outline-offset: 1px;
4569
+ }
4570
+
4571
+ /* The dropdowns and the two date pickers show one short, fixed label, so they
4572
+ keep their intrinsic width and neither grow nor shrink: the row's spare width
4573
+ is the search box's alone. */
4574
+ .w6w-execution-filters select,
4575
+ .w6w-execution-filters input[type=date] {
4576
+ flex: 0 0 auto;
4577
+ }
4578
+
4579
+ /* The search box is the only control that flexes — it takes the spare width and
4580
+ gives it back down to a 160px floor, and that floor is what sets the row's
4581
+ breakpoint: 160px of search + the five intrinsic controls + five 8px gaps
4582
+ ≈ 757px, so the bar is one row from the 760px the layout promises, and wraps
4583
+ below it. */
4584
+ .w6w-execution-filters-search {
4585
+ flex: 1 1 160px;
4586
+ min-width: 160px;
4587
+ }
4588
+
4589
+ /* Clear is a fixed-size control and stays at the end of the row. */
4590
+ .w6w-execution-filters-clear {
4591
+ flex: 0 0 auto;
4592
+ }
4593
+
4594
+ /* --- ExecutionStats ----------------------------------------------------- */
4595
+ /* The `DashboardPage.tsx` `StatCard` shape (big value, muted label) promoted
4596
+ out of the page and onto tokens — that page had the value's type ramp and
4597
+ weight inline, which is exactly what a shared component must not leave to
4598
+ its host. */
4599
+ .w6w-execution-stats {
4600
+ display: flex;
4601
+ flex-direction: column;
4602
+ gap: var(--w6w-sp-2);
4603
+ width: 100%;
4604
+ min-width: 0;
4605
+ }
4606
+
4607
+ .w6w-execution-stats-label {
4608
+ margin: 0;
4609
+ color: var(--w6w-muted);
4610
+ font-size: var(--w6w-fs-sm);
4611
+ }
4612
+
4613
+ .w6w-execution-stats-cards {
4614
+ display: flex;
4615
+ flex-wrap: wrap;
4616
+ gap: var(--w6w-sp-3);
4617
+ }
4618
+
4619
+ .w6w-execution-stat {
4620
+ display: flex;
4621
+ flex-direction: column;
4622
+ gap: var(--w6w-sp-0-5);
4623
+ flex: 1 1 140px;
4624
+ min-width: 0;
4625
+ padding: var(--w6w-sp-3);
4626
+ border: 1px solid var(--w6w-border);
4627
+ border-radius: var(--w6w-radius);
4628
+ background: var(--w6w-panel-2);
4629
+ }
4630
+
4631
+ .w6w-execution-stat-value {
4632
+ color: var(--w6w-text);
4633
+ font-size: var(--w6w-fs-3xl);
4634
+ font-weight: var(--w6w-fw-semibold);
4635
+ line-height: var(--w6w-lh-tight);
4636
+ }
4637
+
4638
+ .w6w-execution-stat-label {
4639
+ color: var(--w6w-muted);
4640
+ font-size: var(--w6w-fs-sm);
4641
+ }
4642
+
4643
+ /* --- ExecutionDetail ---------------------------------------------------- */
4644
+ .w6w-execution-detail {
4645
+ width: 100%;
4646
+ min-width: 0;
4647
+ }
4648
+
4649
+ /* Title block left, controls right; the actions keep their width, the name
4650
+ wraps or ellipses into whatever is left. */
4651
+ .w6w-execution-detail-head {
4652
+ display: flex;
4653
+ align-items: flex-start;
4654
+ justify-content: space-between;
4655
+ gap: var(--w6w-sp-3);
4656
+ }
4657
+
4658
+ .w6w-execution-detail-title {
4659
+ display: flex;
4660
+ flex-direction: column;
4661
+ gap: var(--w6w-sp-0-5);
4662
+ min-width: 0;
4663
+ }
4664
+
4665
+ .w6w-execution-detail-name {
4666
+ margin: 0;
4667
+ color: var(--w6w-text);
4668
+ font-size: var(--w6w-fs-lg);
4669
+ font-weight: var(--w6w-fw-semibold);
4670
+ overflow-wrap: anywhere;
4671
+ }
4672
+
4673
+ .w6w-execution-detail-meta {
4674
+ display: flex;
4675
+ align-items: center;
4676
+ gap: var(--w6w-sp-2);
4677
+ }
4678
+
4679
+ .w6w-execution-detail-actions {
4680
+ display: flex;
4681
+ align-items: center;
4682
+ gap: var(--w6w-sp-2);
4683
+ flex-shrink: 0;
4684
+ }
4685
+
4686
+ .w6w-execution-detail-id-line {
4687
+ margin: 0;
4688
+ }
4689
+
4690
+ .w6w-execution-detail-id {
4691
+ font-family: var(--w6w-font-mono);
4692
+ font-size: var(--w6w-fs-sm);
4693
+ color: var(--w6w-muted);
4694
+ overflow-wrap: anywhere;
4695
+ }
4696
+
4697
+ /* A `<dl>` whose pairs are `<div>`s, so the three facts row up side by side
4698
+ instead of stacking as three label/value paragraphs. */
4699
+ .w6w-execution-detail-timing {
4700
+ display: flex;
4701
+ flex-wrap: wrap;
4702
+ gap: var(--w6w-sp-5);
4703
+ margin: 0;
4704
+ }
4705
+
4706
+ .w6w-execution-detail-timing-item {
4707
+ display: flex;
4708
+ flex-direction: column;
4709
+ gap: var(--w6w-sp-0-5);
4710
+ min-width: 0;
4711
+ }
4712
+
4713
+ .w6w-execution-detail-timing-item dt {
4714
+ color: var(--w6w-muted);
4715
+ font-size: var(--w6w-fs-xs);
4716
+ }
4717
+
4718
+ .w6w-execution-detail-timing-item dd {
4719
+ margin: 0;
4720
+ color: var(--w6w-text);
4721
+ font-size: var(--w6w-fs-sm);
4722
+ }
4723
+
4724
+ /* The view-level failure notice: the same danger token `.w6w-btn-danger` and
4725
+ the health pills use, tinted rather than filled so it reads as a notice and
4726
+ not as a button. */
4727
+ .w6w-execution-detail-error-message {
4728
+ margin: 0;
4729
+ padding: var(--w6w-sp-2) var(--w6w-sp-3);
4730
+ border: 1px solid var(--w6w-danger);
4731
+ border-radius: var(--w6w-radius);
4732
+ background: color-mix(in srgb, var(--w6w-danger) 10%, transparent);
4733
+ color: var(--w6w-danger);
4734
+ font-size: var(--w6w-fs-sm);
4735
+ }
4736
+
4737
+ /*
4738
+ * Server resources — the four presentational variants from
4739
+ * `src/components/ServerResources.tsx` (card, small card, rail, badge).
4740
+ *
4741
+ * Spacing and type come from the `--w6w-sp-*` / `--w6w-fs-*` ramps only, so a
4742
+ * host that re-tunes the scale re-tunes these too; box geometry (heights,
4743
+ * track widths, radii) is deliberately literal, as everywhere else in this
4744
+ * package. The only colour that is not a themed token reference is the
4745
+ * sparkline stroke, which is `--w6w-accent` in both modes.
4746
+ *
4747
+ * The rail variant is the size-sensitive one: it lands in a ~200 px sidebar
4748
+ * column, so every part of it is `min-width: 0`-safe and the sparkline is
4749
+ * scaled by `preserveAspectRatio="none"` rather than given an intrinsic width.
4750
+ */
4751
+ .w6w-server-resources {
4752
+ display: flex;
4753
+ flex-direction: column;
4754
+ gap: var(--w6w-sp-2);
4755
+ min-width: 0;
4756
+ color: var(--w6w-text);
4757
+ font-size: var(--w6w-fs-sm);
4758
+ line-height: var(--w6w-lh-normal);
4759
+ }
4760
+
4761
+ .w6w-server-resources__title {
4762
+ margin: 0;
4763
+ font-size: var(--w6w-fs-base);
4764
+ font-weight: var(--w6w-fw-semibold);
4765
+ line-height: var(--w6w-lh-tight);
4766
+ }
4767
+
4768
+ .w6w-server-resources__rows {
4769
+ display: flex;
4770
+ flex-direction: column;
4771
+ gap: var(--w6w-sp-2);
4772
+ margin: 0;
4773
+ padding: 0;
4774
+ list-style: none;
4775
+ }
4776
+
4777
+ .w6w-server-resources__row {
4778
+ display: grid;
4779
+ grid-template-columns: minmax(0, 1fr) auto;
4780
+ align-items: baseline;
4781
+ gap: var(--w6w-sp-1) var(--w6w-sp-2);
4782
+ }
4783
+
4784
+ .w6w-server-resources__label {
4785
+ color: var(--w6w-muted);
4786
+ font-size: var(--w6w-fs-xs);
4787
+ letter-spacing: 0.04em;
4788
+ text-transform: uppercase;
4789
+ }
4790
+
4791
+ .w6w-server-resources__value {
4792
+ font-family: var(--w6w-font-mono);
4793
+ font-size: var(--w6w-fs-sm);
4794
+ font-variant-numeric: tabular-nums;
4795
+ }
4796
+
4797
+ /* The line itself. `stroke` is repeated as a property (not only as the
4798
+ polyline's presentation attribute) so a host stylesheet can re-colour it
4799
+ without reaching into the SVG. */
4800
+ .w6w-server-resources__spark {
4801
+ grid-column: 1/-1;
4802
+ display: block;
4803
+ width: 100%;
4804
+ height: 28px;
4805
+ stroke: var(--w6w-accent);
4806
+ stroke-width: 2;
4807
+ stroke-linejoin: round;
4808
+ stroke-linecap: round;
4809
+ overflow: visible;
4810
+ }
4811
+
4812
+ .w6w-server-resources__meta {
4813
+ margin: 0;
4814
+ color: var(--w6w-muted);
4815
+ font-family: var(--w6w-font-mono);
4816
+ font-size: var(--w6w-fs-xs);
4817
+ }
4818
+
4819
+ /* --- card ---------------------------------------------------------------- */
4820
+ .w6w-server-resources--card {
4821
+ gap: var(--w6w-sp-3);
4822
+ padding: var(--w6w-sp-4);
4823
+ border: 1px solid var(--w6w-border);
4824
+ border-radius: var(--w6w-radius);
4825
+ background: var(--w6w-panel);
4826
+ }
4827
+
4828
+ /* --- small card ---------------------------------------------------------- */
4829
+ .w6w-server-resources--small {
4830
+ gap: var(--w6w-sp-1-5);
4831
+ padding: var(--w6w-sp-3);
4832
+ border: 1px solid var(--w6w-border);
4833
+ border-radius: var(--w6w-radius);
4834
+ background: var(--w6w-panel);
4835
+ }
4836
+
4837
+ .w6w-server-resources--small .w6w-server-resources__title {
4838
+ font-size: var(--w6w-fs-sm);
4839
+ }
4840
+
4841
+ /* --- rail ---------------------------------------------------------------- */
4842
+ .w6w-server-resources--rail {
4843
+ gap: var(--w6w-sp-1);
4844
+ padding: var(--w6w-sp-2);
4845
+ border: 1px solid var(--w6w-border);
4846
+ border-radius: var(--w6w-radius);
4847
+ background: var(--w6w-panel);
4848
+ }
4849
+
4850
+ .w6w-server-resources__rail-row {
4851
+ display: grid;
4852
+ grid-template-columns: auto minmax(0, 1fr);
4853
+ align-items: baseline;
4854
+ gap: 0 var(--w6w-sp-2);
4855
+ }
4856
+
4857
+ .w6w-server-resources__rail-row .w6w-server-resources__value {
4858
+ text-align: right;
4859
+ }
4860
+
4861
+ .w6w-server-resources__rail-row .w6w-server-resources__spark {
4862
+ height: 20px;
4863
+ }
4864
+
4865
+ .w6w-server-resources__expand {
4866
+ width: 100%;
4867
+ padding: var(--w6w-sp-1) var(--w6w-sp-2);
4868
+ border: 1px solid var(--w6w-border);
4869
+ border-radius: var(--w6w-radius);
4870
+ background: var(--w6w-panel-2);
4871
+ color: var(--w6w-muted);
4872
+ font-family: inherit;
4873
+ font-size: var(--w6w-fs-xs);
4874
+ cursor: pointer;
4875
+ }
4876
+
4877
+ .w6w-server-resources__expand:hover {
4878
+ color: var(--w6w-text);
4879
+ border-color: var(--w6w-accent);
4880
+ }
4881
+
4882
+ /* --- badge --------------------------------------------------------------- */
4883
+ .w6w-server-resources--badge {
4884
+ flex-direction: row;
4885
+ gap: var(--w6w-sp-2);
4886
+ align-items: center;
4887
+ padding: 0 var(--w6w-sp-2);
4888
+ border: 1px solid var(--w6w-border);
4889
+ border-radius: 999px;
4890
+ background: var(--w6w-panel-2);
4891
+ font-family: var(--w6w-font-mono);
4892
+ font-size: var(--w6w-fs-xs);
4893
+ white-space: nowrap;
4894
+ }