@w6w/ui 0.0.0-stage → 0.1.0

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