@changfenhuang/dsh-genui 0.9.9 → 0.10.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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/SKILL.md +128 -6
  5. package/lib/assets/echarts-core.js +43 -0
  6. package/lib/assets/{echarts.js → echarts-full.js} +1 -1
  7. package/lib/assets/mermaid.js +1 -1
  8. package/lib/client.js +5 -3
  9. package/lib/index.js +230 -69
  10. package/lib/types/client/EChartNode.d.ts +9 -0
  11. package/lib/types/client/GenuiBlock.d.ts +3 -4
  12. package/lib/types/client/asset-echarts-core.d.ts +1 -0
  13. package/lib/types/client/asset-echarts.d.ts +3 -2
  14. package/lib/types/client/asset-loader.d.ts +2 -2
  15. package/lib/types/client/blocks/advanced.d.ts +0 -3
  16. package/lib/types/client/blocks/charts.d.ts +23 -12
  17. package/lib/types/client/blocks/state.d.ts +5 -3
  18. package/lib/types/client/echarts-lazy.d.ts +4 -1
  19. package/lib/types/client/genui-runtime/schema.d.ts +11 -1
  20. package/lib/types/client/inline.d.ts +29 -0
  21. package/lib/types/client/spec.d.ts +76 -6
  22. package/lib/types/plugin/index.d.ts +1 -1
  23. package/package.json +4 -3
  24. package/src/client/EChartNode.tsx +233 -19
  25. package/src/client/GenuiBlock.module.css +895 -145
  26. package/src/client/GenuiBlock.tsx +31 -23
  27. package/src/client/PlotBlock.module.css +17 -1
  28. package/src/client/PlotBlock.tsx +33 -1
  29. package/src/client/asset-echarts-core.ts +39 -0
  30. package/src/client/asset-echarts.ts +4 -3
  31. package/src/client/asset-loader.ts +5 -3
  32. package/src/client/blocks/advanced.tsx +60 -16
  33. package/src/client/blocks/charts.tsx +761 -144
  34. package/src/client/blocks/render-node.tsx +252 -11
  35. package/src/client/blocks/state.ts +5 -3
  36. package/src/client/dom-fence.tsx +60 -6
  37. package/src/client/echarts-lazy.ts +11 -2
  38. package/src/client/fence-render.tsx +6 -5
  39. package/src/client/gallery.ts +78 -2
  40. package/src/client/genui-runtime/schema.ts +45 -11
  41. package/src/client/guard.ts +161 -23
  42. package/src/client/index.tsx +14 -3
  43. package/src/client/inline.ts +73 -0
  44. package/src/client/mermaid-core.ts +42 -5
  45. package/src/client/spec.ts +79 -6
  46. package/src/plugin/index.ts +33 -45
  47. package/src/plugin/tool.ts +5 -1
  48. package/tsdown.config.ts +2 -1
@@ -16,6 +16,16 @@
16
16
  literals here are only last-resort fallbacks for hosts without the token
17
17
  sheets. */
18
18
 
19
+ /* Dark theme: the layer step already lifts the card off the canvas, so the
20
+ extra label tint stays a small overlay (4%) instead of the 10% that light
21
+ mode needs to clear the 4% lightness-step minimum. */
22
+ body[data-ds-dark-theme] .block,
23
+ body[data-ds-dark-theme] .panel {
24
+ --dsl-g-surface: color-mix(in srgb, var(--dsw-alias-label-primary) 4%,
25
+ var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block, transparent))));
26
+ --dsl-g-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.24), 0 8px 24px rgba(0, 0, 0, 0.18);
27
+ }
28
+
19
29
  .block,
20
30
  .panel {
21
31
  /* ── design tokens (design system v2) ──
@@ -25,16 +35,22 @@
25
35
  gap/padding collapsed to zero). */
26
36
  --dsl-g-radius-surface: 12px;
27
37
  --dsl-g-radius-control: 8px;
38
+ --dsl-g-radius-sm: 6px;
28
39
  --dsl-g-radius-pill: 999px;
29
40
 
30
- --dsl-g-font-display: 24px;
31
- --dsl-g-font-h1: 20px;
32
- --dsl-g-font-h2: 16px;
33
- --dsl-g-font-h3: 14px;
34
- --dsl-g-font-body: 14px;
35
- --dsl-g-font-title: 13px;
36
- --dsl-g-font-meta: 12px;
37
- --dsl-g-font-data: 11px;
41
+ /* Design system v4: the v3 scale was too compressed — every level sat
42
+ within 26..12px, so nothing dominated. v4 restores a display contrast
43
+ (hero 34 / display 28 / h1 24 / h2 19 / h3 16) against a 14.5px body, and
44
+ the reading floor stays 12px. */
45
+ --dsl-g-font-hero: 34px;
46
+ --dsl-g-font-display: 28px;
47
+ --dsl-g-font-h1: 24px;
48
+ --dsl-g-font-h2: 19px;
49
+ --dsl-g-font-h3: 16px;
50
+ --dsl-g-font-body: 14.5px;
51
+ --dsl-g-font-title: 13.5px;
52
+ --dsl-g-font-meta: 12.5px;
53
+ --dsl-g-font-data: 12px;
38
54
 
39
55
  --dsl-g-gap-lg: 16px;
40
56
  --dsl-g-gap-md: 12px;
@@ -48,9 +64,28 @@
48
64
  /* Row/surface hover: a 4% wash of the theme's own label color works on
49
65
  * dark AND light hosts without any off-brand literal. */
50
66
  --dsl-g-hover: color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent);
67
+ /* Zebra rows / table header wash: half the hover strength, so a striped
68
+ table still reads as one surface rather than a stack of bands. */
69
+ --dsl-g-zebra: color-mix(in srgb, var(--dsw-alias-label-primary) 2.5%, transparent);
51
70
  --dsl-g-tint: 8%;
52
71
  --dsl-g-tint-strong: 14%;
53
72
  --dsl-g-font-mono: var(--ds-font-family-code, ui-monospace, 'SF Mono', monospace);
73
+ /* Elevation, theme-agnostic: the light theme maps EVERY bg layer to white,
74
+ so a card there is white-on-white and a border alone cannot carry the
75
+ hierarchy. Both a tint of the theme's own label colour and the surface
76
+ border are derived from tokens that exist in both themes:
77
+ light: label ≈ near-black → tint darkens the white card to ~#f5f5f5
78
+ dark : label ≈ near-white → tint lifts the card off the page */
79
+ /* 4% / 14% measured as 255→246 and read as "no difference" in review: the
80
+ tint now sits clearly above the perception floor. Light: page 255 → card
81
+ ~232, border ~190. Dark: page 21 → card ~62 (an unmistakable elevation). */
82
+ --dsl-g-surface: color-mix(in srgb, var(--dsw-alias-label-primary) 10%,
83
+ var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block, transparent))));
84
+ /* Borders separate, shadows lift (design standard: "shadows over borders"):
85
+ the outline stays quiet and the layered shadow carries the depth, with the
86
+ first layer at the documented minimum `0 1px 3px rgba(0,0,0,0.10)`. */
87
+ --dsl-g-border-surface: color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent);
88
+ --dsl-g-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.10), 0 8px 24px rgba(0, 0, 0, 0.06);
54
89
  }
55
90
 
56
91
  .block {
@@ -60,15 +95,23 @@
60
95
  does not need a card wrapper. */
61
96
  margin: 8px 0;
62
97
  color: var(--dsw-alias-label-primary);
98
+ /* Numbers line up wherever they appear (stats, tables, progress, legends). */
99
+ font-variant-numeric: tabular-nums;
63
100
  }
101
+ /* Content-box padding made a <select> and an <input> with the same
102
+ width:100% measure 365 vs 391px. One reset inside our subtree. */
103
+ .block *, .block *::before, .block *::after,
104
+ .panel *, .panel *::before, .panel *::after { box-sizing: border-box; }
64
105
 
65
106
  .banner {
66
- font-size: var(--dsl-g-font-title);
67
- font-weight: 600;
68
- letter-spacing: 0.01em;
107
+ /* Hierarchy through type, not decoration: 22px/800 with generous space
108
+ below. The accent bars v4 added read as PPT chrome and are gone. */
109
+ font-size: 22px;
110
+ font-weight: 800;
111
+ letter-spacing: -0.02em;
69
112
  color: var(--dsw-alias-label-primary);
70
- padding-bottom: 10px;
71
- margin-bottom: var(--dsl-g-gap-lg);
113
+ padding-bottom: 14px;
114
+ margin-bottom: 18px;
72
115
  border-bottom: 1px solid var(--dsl-g-border);
73
116
  }
74
117
 
@@ -78,6 +121,17 @@
78
121
  .row { display: flex; align-items: center; gap: var(--dsl-g-gap-md); }
79
122
  .row.wrap { flex-wrap: wrap; }
80
123
  .grid { display: grid; gap: var(--dsl-g-gap-md); }
124
+ /* Bento child: occupies N grid columns (the wrapper exists only so the child
125
+ keeps its own layout). */
126
+ .gridSpan { display: flex; flex-direction: column; min-width: 0; }
127
+ .gridSpan > * { flex: 1; }
128
+ /* Narrow conversation column (phone / split pane): a fixed 3-4 column grid
129
+ would squeeze every cell, so the grid collapses to one column. The inline
130
+ gridTemplateColumns from the spec needs !important to be overridden. */
131
+ @media (max-width: 560px) {
132
+ .grid { grid-template-columns: 1fr !important; }
133
+ .row { flex-wrap: wrap; }
134
+ }
81
135
  .spacer { flex: 1; }
82
136
  .divider {
83
137
  height: 1px;
@@ -87,31 +141,66 @@
87
141
  }
88
142
 
89
143
  .card {
90
- background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block));
91
- border: 1px solid var(--dsl-g-border);
144
+ /* Elevated surface: on the dark theme layer-1 (35,35,36) sits only ~14 units
145
+ above the page (21,21,23) and read as "a black box". layer-2 (44,44,46) is
146
+ the host's own elevated surface; light theme maps all layers to one colour,
147
+ so this changes nothing there. */
148
+ background: var(--dsl-g-surface);
149
+ border: 1px solid var(--dsl-g-border-surface);
92
150
  border-radius: var(--dsl-g-radius-surface);
93
151
  padding: var(--dsl-g-gap-lg);
94
152
  display: flex;
95
153
  flex-direction: column;
96
154
  gap: var(--dsl-g-gap-md);
97
- }
155
+ min-width: 0;
156
+ box-shadow: var(--dsl-g-shadow-card);
157
+ }
158
+ /* Bento rows size every card to the tallest one. Without this, a short card
159
+ (a single donut next to a tall table) kept its content pinned to the top and
160
+ left a large dead block underneath. The last child absorbs the extra height,
161
+ and the graphic shapes centre themselves inside it. */
162
+ .card > :last-child { flex: 1 1 auto; min-height: 0; }
163
+ .card > .chart,
164
+ .card > .lineChart,
165
+ .card > .donut,
166
+ .card > .hbars { justify-content: center; }
167
+ .card > .col { justify-content: center; }
98
168
 
99
169
  .cardTitle {
100
- font-size: var(--dsl-g-font-meta);
101
- font-weight: 600;
102
- letter-spacing: 0.02em;
170
+ /* Was 12.5px uppercase with tracking while the card BODY is 14.5px: the
171
+ title read as a small grey label, which is why nothing scanned. A card
172
+ title is a section heading, so it sits above body size, sentence case,
173
+ no tracking. The 12.5px uppercase treatment belongs to eyebrows only. */
174
+ font-size: var(--dsl-g-font-h3);
175
+ font-weight: 650;
176
+ letter-spacing: -0.01em;
177
+ text-transform: none;
103
178
  color: var(--dsw-alias-label-secondary);
104
179
  }
105
180
 
106
181
  /* ---------- text ---------- */
107
182
 
108
183
  .text { color: var(--dsw-alias-label-primary); }
109
- .text.h1 { font-size: var(--dsl-g-font-h1); font-weight: 700; line-height: 1.3; letter-spacing: -0.02em; }
110
- .text.h2 { font-size: var(--dsl-g-font-h2); font-weight: 600; line-height: 1.35; letter-spacing: -0.01em; }
111
- .text.h3 { font-size: var(--dsl-g-font-h3); font-weight: 600; line-height: 1.5; }
112
- .text.body { font-size: var(--dsl-g-font-body); line-height: 1.6; color: var(--dsw-alias-label-primary); }
113
- .text.muted { font-size: var(--dsl-g-font-title); line-height: 1.5; color: var(--dsw-alias-label-secondary); }
114
- .text.caption { font-size: var(--dsl-g-font-meta); line-height: 1.5; color: var(--dsw-alias-label-tertiary); letter-spacing: 0.02em; }
184
+ .text.h1 {
185
+ font-size: var(--dsl-g-font-h1);
186
+ font-weight: 800;
187
+ line-height: 1.25;
188
+ letter-spacing: -0.02em;
189
+ }
190
+ .text.h2 {
191
+ font-size: var(--dsl-g-font-h2);
192
+ font-weight: 700;
193
+ line-height: 1.3;
194
+ letter-spacing: -0.015em;
195
+ }
196
+ .text.h3 { font-size: var(--dsl-g-font-h3); font-weight: 650; line-height: 1.45; }
197
+ /* CJK reads looser than Latin at the same size: 1.7 for body copy, 1.65 for
198
+ secondary lines, and no letter-spacing on Chinese runs (0.02em inserts
199
+ visible gaps between Han characters). */
200
+ /* ~65ch keeps a comfortable reading measure when the block is wide. */
201
+ .text.body { font-size: var(--dsl-g-font-body); line-height: 1.7; color: var(--dsw-alias-label-primary); text-wrap: pretty; max-width: 68ch; }
202
+ .text.muted { font-size: var(--dsl-g-font-title); line-height: 1.65; color: var(--dsw-alias-label-secondary); }
203
+ .text.caption { font-size: var(--dsl-g-font-meta); line-height: 1.6; color: var(--dsw-alias-label-secondary); }
115
204
  .text.center { text-align: center; }
116
205
 
117
206
  /* ---------- controls ---------- */
@@ -129,6 +218,10 @@
129
218
  cursor: pointer;
130
219
  transition: border-color 0.15s, filter 0.15s, background 0.15s, transform 0.1s;
131
220
  text-align: center;
221
+ /* A button in a `col` must hug its label; only `full: true` stretches.
222
+ (Default flex `align-items: stretch` used to make every stacked button a
223
+ full-width slab.) */
224
+ align-self: flex-start;
132
225
  }
133
226
 
134
227
  .button:hover:not(:disabled) { border-color: var(--dsl-g-accent); filter: brightness(1.06); }
@@ -168,8 +261,8 @@
168
261
  .field > span { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-tertiary); }
169
262
 
170
263
  .input, .select {
171
- background: var(--dsw-alias-bg-base, #17171a);
172
- border: 1px solid var(--dsl-g-border);
264
+ background: var(--dsl-g-surface);
265
+ border: 1px solid var(--dsl-g-border-surface);
173
266
  border-radius: var(--dsl-g-radius-control);
174
267
  color: var(--dsw-alias-label-primary);
175
268
  padding: 8px 12px;
@@ -221,15 +314,102 @@
221
314
  display: flex;
222
315
  flex-direction: column;
223
316
  gap: var(--dsl-g-gap-sm);
224
- background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block));
225
- border: 1px solid var(--dsl-g-border);
317
+ background: var(--dsl-g-surface);
318
+ border: 1px solid var(--dsl-g-border-surface);
226
319
  border-radius: var(--dsl-g-radius-surface);
227
320
  }
228
321
  .mediaLabel { font-size: var(--dsl-g-font-title); font-weight: 600; color: var(--dsw-alias-label-primary); }
229
- .mediaPlayer { display: block; width: 100%; min-width: 0; }
322
+ .mediaPlayer { display: block; width: 100%; min-width: 0; border-radius: var(--dsl-g-radius-control); }
323
+ /* Reserve the box before the bytes arrive: without an intrinsic ratio an
324
+ unloaded <img> collapses to 0px and the card looks broken. */
325
+ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl-g-surface); }
230
326
  .videoPlayer { max-height: min(70vh, 720px); background: #000; object-fit: contain; }
231
327
  .mediaError { font-size: var(--dsl-g-font-title); color: var(--dsw-alias-state-error-primary, #f25a5a); }
232
328
 
329
+ /* ---------- hero (cover band) ----------
330
+ The answer's focal point: an eyebrow, an oversized metric, a title and a
331
+ subtitle on a soft tinted surface. Tone changes the wash, never the layout. */
332
+
333
+ .hero {
334
+ position: relative;
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 6px;
338
+ padding: 22px 24px 24px;
339
+ border-radius: 14px;
340
+ border: 1px solid var(--dsl-g-border-surface);
341
+ background:
342
+ radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--dsl-g-accent) 22%, transparent) 0%, transparent 58%),
343
+ linear-gradient(180deg, color-mix(in srgb, var(--dsl-g-accent) 7%, transparent), transparent 70%),
344
+ var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, transparent));
345
+ overflow: hidden;
346
+ box-shadow: var(--dsl-g-shadow-card);
347
+ }
348
+ .heroSuccess {
349
+ background:
350
+ radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) 20%, transparent) 0%, transparent 58%),
351
+ linear-gradient(180deg, color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) 6%, transparent), transparent 70%),
352
+ var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, transparent));
353
+ }
354
+ .heroWarning {
355
+ background:
356
+ radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5b83d) 20%, transparent) 0%, transparent 58%),
357
+ linear-gradient(180deg, color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5b83d) 6%, transparent), transparent 70%),
358
+ var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, transparent));
359
+ }
360
+ .heroDanger {
361
+ background:
362
+ radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--dsw-alias-state-error-primary, #ff6b6b) 18%, transparent) 0%, transparent 58%),
363
+ linear-gradient(180deg, color-mix(in srgb, var(--dsw-alias-state-error-primary, #ff6b6b) 6%, transparent), transparent 70%),
364
+ var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, transparent));
365
+ }
366
+ .heroLabel {
367
+ font-size: var(--dsl-g-font-meta);
368
+ font-weight: 700;
369
+ letter-spacing: 0.1em;
370
+ text-transform: uppercase;
371
+ color: var(--dsw-alias-label-secondary);
372
+ }
373
+ .heroTop { display: flex; align-items: baseline; gap: var(--dsl-g-gap-md); flex-wrap: wrap; }
374
+ .heroValue {
375
+ display: inline-flex;
376
+ align-items: baseline;
377
+ gap: 4px;
378
+ font-family: var(--dsl-g-font-mono);
379
+ font-size: 52px;
380
+ font-weight: 750;
381
+ letter-spacing: -0.028em;
382
+ line-height: 1;
383
+ color: var(--dsw-alias-label-primary);
384
+ font-variant-numeric: tabular-nums;
385
+ }
386
+ .heroUnit { font-size: 0.44em; font-weight: 650; letter-spacing: 0; color: var(--dsw-alias-label-secondary); }
387
+ .heroSpark { margin-left: auto; width: 168px; align-self: center; }
388
+ .heroTitle { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; color: var(--dsw-alias-label-primary); line-height: 1.3; }
389
+ .heroSubtitle { font-size: var(--dsl-g-font-title); color: var(--dsw-alias-label-secondary); line-height: 1.6; text-wrap: pretty; }
390
+
391
+ /* ---------- inline markup (inline.ts) ----------
392
+ Emphasis that stays INSIDE a sentence: a paragraph should not have to be
393
+ split into several nodes to bold one phrase. */
394
+ .inlineCode {
395
+ font-family: var(--dsl-g-font-mono);
396
+ font-size: 0.92em;
397
+ padding: 1px 5px;
398
+ border-radius: 5px;
399
+ background: var(--dsl-g-surface);
400
+ border: 1px solid var(--dsl-g-border-surface);
401
+ white-space: nowrap;
402
+ }
403
+ .inlineStrong { font-weight: 680; color: var(--dsw-alias-label-primary); }
404
+ .inlineMark {
405
+ background: color-mix(in srgb, var(--dsl-g-accent) 20%, transparent);
406
+ color: inherit;
407
+ padding: 0 3px;
408
+ border-radius: 4px;
409
+ }
410
+ .inlineLink { color: var(--dsl-g-accent); text-decoration: underline; text-underline-offset: 2px; }
411
+ .inlineLink:hover { text-decoration-thickness: 2px; }
412
+
233
413
  /* ---------- badges / stat / progress ---------- */
234
414
 
235
415
  .badge {
@@ -271,37 +451,86 @@
271
451
  .stat {
272
452
  display: flex;
273
453
  flex-direction: column;
274
- gap: 2px;
275
- padding: 6px var(--dsl-g-gap-lg);
454
+ gap: 4px;
455
+ /* Metric card: the number IS the design. No accent stripe, no bar — the
456
+ 40px figure against a 12px uppercase label carries the hierarchy. */
457
+ padding: 14px 16px 15px;
458
+ border: 1px solid var(--dsl-g-border-surface);
459
+ border-radius: var(--dsl-g-radius-surface);
460
+ background: var(--dsl-g-surface);
461
+ min-width: 0;
462
+ overflow: hidden;
463
+ box-shadow: var(--dsl-g-shadow-card);
276
464
  }
465
+ /* Hero stat: one number is the answer's anchor (use it once per fence). */
466
+ .statHero { padding: 18px 20px 20px; }
467
+ .statHero .statValue { font-size: 44px; line-height: 1.02; letter-spacing: -0.035em; }
468
+ .statHero .statLabel { font-size: 13px; }
277
469
  .grid > .stat, .row > .stat { border-left: 1px solid var(--dsl-g-border); }
278
- .grid > .stat:first-child, .row > .stat:first-child { border-left: none; padding-left: var(--dsl-g-gap-xs); }
279
- .statLabel { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-tertiary); letter-spacing: 0.02em; }
470
+ .statLabel {
471
+ font-size: var(--dsl-g-font-meta);
472
+ color: var(--dsw-alias-label-secondary);
473
+ letter-spacing: 0.05em;
474
+ text-transform: uppercase;
475
+ line-height: 1.4;
476
+ }
280
477
  .statValue {
478
+ display: flex;
479
+ align-items: baseline;
480
+ gap: 3px;
281
481
  font-family: var(--dsl-g-font-mono);
282
- font-size: var(--dsl-g-font-display);
283
- font-weight: 600;
284
- letter-spacing: -0.02em;
482
+ font-size: var(--dsl-g-font-hero);
483
+ font-weight: 700;
484
+ letter-spacing: -0.03em;
285
485
  font-variant-numeric: tabular-nums;
286
486
  color: var(--dsw-alias-label-primary);
287
- line-height: 1.2;
487
+ line-height: 1.05;
488
+ overflow-wrap: anywhere;
489
+ }
490
+ /* Unit suffix (%, GB, k, ms): same baseline, half the size, muted — the
491
+ number keeps the weight. This is the real typography win in v5. */
492
+ .statUnit {
493
+ font-size: 0.5em;
494
+ font-weight: 600;
495
+ letter-spacing: 0;
496
+ color: var(--dsw-alias-label-secondary);
497
+ }
498
+ .statHero .statUnit { font-size: 0.46em; }
499
+ .statDelta {
500
+ font-size: var(--dsl-g-font-meta);
501
+ font-weight: 600;
502
+ width: fit-content;
503
+ padding: 0 6px;
504
+ border-radius: var(--dsl-g-radius-pill);
505
+ color: var(--dsw-alias-label-secondary);
506
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent);
507
+ }
508
+ .statDelta.up { color: var(--dsw-alias-state-success-secondary, #1f9d63); background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 12%, transparent); }
509
+ .statSpark {
510
+ display: block;
511
+ width: calc(100% + 32px);
512
+ max-width: none;
513
+ height: 30px;
514
+ margin: 6px -16px -15px;
515
+ overflow: visible;
288
516
  }
289
- .statDelta { font-size: var(--dsl-g-font-meta); font-weight: 500; }
290
- .statDelta.up { color: var(--dsw-alias-state-success-primary, #22c55e); }
291
- .statDelta.down { color: var(--dsw-alias-state-error-primary, #f25a5a); }
517
+ .statHero .statSpark { width: calc(100% + 40px); margin: 8px -20px -20px; }
518
+
519
+ .statDelta.down { color: var(--dsw-alias-state-error-primary, #d94848); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 12%, transparent); }
292
520
 
293
521
  .progress { display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
294
522
  .progressRow {
295
523
  display: flex;
296
524
  justify-content: space-between;
525
+ gap: var(--dsl-g-gap-md);
297
526
  font-size: var(--dsl-g-font-meta);
298
- color: var(--dsw-alias-label-tertiary);
527
+ color: var(--dsw-alias-label-secondary);
299
528
  }
300
- .progressRow span:last-child { font-family: var(--dsl-g-font-mono); }
529
+ .progressRow span:last-child { font-family: var(--dsl-g-font-mono); color: var(--dsw-alias-label-primary); font-weight: 600; }
301
530
  .track {
302
- height: 6px;
531
+ height: 8px;
303
532
  border-radius: var(--dsl-g-radius-pill);
304
- background: var(--dsw-alias-bg-layer-1);
533
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent);
305
534
  overflow: hidden;
306
535
  box-shadow: inset 0 0 0 1px var(--dsl-g-border);
307
536
  }
@@ -316,14 +545,34 @@
316
545
  /* ---------- list / table / chart / tabs / avatar ---------- */
317
546
 
318
547
  .list { display: flex; flex-direction: column; }
319
- .li { padding: var(--dsl-g-gap-sm) var(--dsl-g-gap-xs); display: flex; flex-direction: column; gap: 2px; }
548
+ .li {
549
+ position: relative;
550
+ padding: 9px var(--dsl-g-gap-xs) 9px 16px;
551
+ display: flex;
552
+ flex-direction: column;
553
+ gap: 3px;
554
+ }
555
+ /* Accent dot instead of a bare indented line: 202 of ~3.7k real nodes are
556
+ list items, and a marker makes a multi-line list scannable at a glance. */
557
+ .li::before {
558
+ content: "";
559
+ position: absolute;
560
+ left: 2px;
561
+ top: 15px;
562
+ width: 5px;
563
+ height: 5px;
564
+ border-radius: 50%;
565
+ background: var(--dsl-g-accent);
566
+ opacity: 0.6;
567
+ }
320
568
  .li + .li { border-top: 1px solid var(--dsl-g-border); }
321
569
  .li:last-child { border-bottom: none; }
322
- .liTitle { font-size: var(--dsl-g-font-title); color: var(--dsw-alias-label-primary); font-weight: 600; line-height: 1.5; }
323
- .liDesc { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-tertiary); line-height: 1.5; }
570
+ .liTitle { font-size: var(--dsl-g-font-body); color: var(--dsw-alias-label-primary); font-weight: 650; line-height: 1.5; }
571
+ .liDesc { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-secondary); line-height: 1.55; }
324
572
 
325
573
  .tableWrap {
326
574
  border-radius: var(--dsl-g-radius-control);
575
+ border: 1px solid var(--dsl-g-border-surface);
327
576
  /* A table whose nowrap cells exceed the column width must scroll, not clip
328
577
  (mirrors the shell markdown table's .tableScroll pattern). */
329
578
  overflow-x: auto;
@@ -334,26 +583,40 @@
334
583
  .table { width: 100%; border-collapse: collapse; font-size: var(--dsl-g-font-title); }
335
584
  .table th {
336
585
  text-align: left;
337
- padding: var(--dsl-g-gap-sm) var(--dsl-g-gap-md);
338
- color: var(--dsw-alias-label-tertiary);
339
- font-weight: 600;
586
+ padding: 9px var(--dsl-g-gap-md);
587
+ color: var(--dsw-alias-label-secondary);
588
+ font-weight: 700;
340
589
  font-size: var(--dsl-g-font-meta);
341
- border-bottom: 1px solid var(--dsl-g-border);
590
+ letter-spacing: 0.06em;
591
+ text-transform: uppercase;
592
+ border-bottom: 1px solid var(--dsl-g-border-strong);
593
+ background: var(--dsl-g-zebra);
342
594
  white-space: nowrap;
343
595
  }
344
596
  .table td {
345
- padding: var(--dsl-g-gap-sm) var(--dsl-g-gap-md);
597
+ padding: 10px var(--dsl-g-gap-md);
346
598
  border-bottom: 1px solid var(--dsl-g-border);
347
- color: var(--dsw-alias-label-secondary);
599
+ color: var(--dsw-alias-label-primary);
348
600
  white-space: nowrap;
349
601
  font-variant-numeric: tabular-nums;
350
602
  line-height: 1.5;
603
+ vertical-align: middle;
351
604
  }
352
- .table tr:last-child td { border-bottom: none; }
605
+ /* The first column is the row's name: give it the weight of a label so a
606
+ table reads as "these rows, these values" instead of a uniform grid. */
607
+ .table td:first-child { font-weight: 600; }
608
+ .table th:first-child { color: var(--dsw-alias-label-primary); }
609
+ .table tbody tr:nth-child(even) { background: var(--dsl-g-zebra); }
353
610
  .table tbody tr:hover { background: var(--dsl-g-hover); }
611
+ .table tr:last-child td { border-bottom: none; }
354
612
  /* Numeric columns (auto-detected) right-align with tabular numerals — the
355
613
  table's data voice. */
356
614
  .table th.thNum, .table td.tdNum { text-align: right; }
615
+ /* Signed values read as a delta chip (+12.4% / -3%) without any new spec
616
+ field: the renderer classifies the cell text. */
617
+ .tdDelta { font-weight: 600; }
618
+ .tdDeltaUp { color: var(--dsw-alias-state-success-secondary, #1f9d63); }
619
+ .tdDeltaDown { color: var(--dsw-alias-state-error-primary, #d94848); }
357
620
 
358
621
  /* sortable header button (v2.9) */
359
622
  .thSort {
@@ -378,13 +641,28 @@
378
641
 
379
642
  /* Chart skeleton: plot area with baseline + 25/50/75% gridlines, category
380
643
  labels in their own row below the axis (they must never cross the lines). */
381
- .chart { display: flex; flex-direction: column; gap: var(--dsl-g-gap-xs); }
644
+ .chart { position: relative; display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
645
+ /* Y-axis gutter + plot share one measured box so tick labels line up with the
646
+ gridlines instead of being guessed from the container height. */
647
+ .chartBody { display: flex; gap: 6px; align-items: stretch; }
648
+ .chartYAxis { position: relative; width: 42px; flex-shrink: 0; }
649
+ .chartYTick {
650
+ position: absolute;
651
+ right: 0;
652
+ transform: translateY(50%);
653
+ font-family: var(--dsl-g-font-mono);
654
+ font-size: var(--dsl-g-font-data);
655
+ color: var(--dsw-alias-label-tertiary);
656
+ white-space: nowrap;
657
+ }
382
658
  .chartPlot {
383
659
  position: relative;
384
660
  display: flex;
385
661
  align-items: flex-end;
386
662
  gap: var(--dsl-g-gap-sm);
387
- height: 132px;
663
+ height: 160px;
664
+ flex: 1;
665
+ min-width: 0;
388
666
  }
389
667
  .baseline {
390
668
  position: absolute;
@@ -402,33 +680,99 @@
402
680
  }
403
681
  .chartLabels { display: flex; gap: var(--dsl-g-gap-sm); }
404
682
  .chartLabels > span { flex: 1; min-width: 0; }
405
- .barCol {
406
- flex: 1;
683
+ .chartLegend {
684
+ display: flex;
685
+ flex-wrap: wrap;
686
+ gap: var(--dsl-g-gap-md);
687
+ font-size: var(--dsl-g-font-meta);
688
+ color: var(--dsw-alias-label-secondary);
689
+ line-height: 1.5;
690
+ }
691
+ .legendItem { display: inline-flex; align-items: center; gap: 6px; }
692
+ .legendSwatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
693
+ .lineArea { fill: var(--dsl-g-accent); opacity: 0.1; }
694
+
695
+ /* Instant hover readout (charts). Native `title` is slow and cannot show a
696
+ stacked breakdown; this is drawn by the chart itself. */
697
+ .chartTip {
698
+ position: absolute;
699
+ transform: translate(-50%, calc(-100% - 10px));
407
700
  display: flex;
408
701
  flex-direction: column;
409
- justify-content: flex-end;
410
- gap: var(--dsl-g-gap-xs);
702
+ gap: 2px;
703
+ padding: 6px 10px;
704
+ border-radius: var(--dsl-g-radius-control);
705
+ border: 1px solid var(--dsl-g-border-strong);
706
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, #1c1c1e)));
707
+ box-shadow: 0 8px 22px rgba(0, 0, 0, 0.3);
708
+ color: var(--dsw-alias-label-primary);
709
+ font-size: var(--dsl-g-font-meta);
710
+ line-height: 1.4;
711
+ white-space: nowrap;
712
+ pointer-events: none;
713
+ z-index: 6;
714
+ }
715
+ .chartTipRow { display: flex; justify-content: space-between; gap: 14px; }
716
+ .chartTipValue { font-family: var(--dsl-g-font-mono); font-weight: 600; font-variant-numeric: tabular-nums; }
717
+ /* Value printed inside a stacked segment when the segment is tall enough. */
718
+ .stackValue {
719
+ display: block;
720
+ text-align: center;
721
+ font-family: var(--dsl-g-font-mono);
722
+ font-size: var(--dsl-g-font-data);
723
+ font-weight: 700;
724
+ line-height: 1.5;
725
+ color: #fff;
726
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
727
+ font-variant-numeric: tabular-nums;
728
+ }
729
+ .barCol {
730
+ position: relative;
731
+ flex: 1;
411
732
  min-width: 0;
412
733
  height: 100%;
413
734
  }
414
735
  .barValue {
736
+ position: absolute;
737
+ left: 0;
738
+ right: 0;
415
739
  font-family: var(--dsl-g-font-mono);
416
740
  font-size: var(--dsl-g-font-data);
417
741
  font-variant-numeric: tabular-nums;
418
- color: var(--dsw-alias-label-secondary);
742
+ color: var(--dsw-alias-label-primary);
743
+ font-weight: 600;
419
744
  text-align: center;
420
745
  line-height: 1.2;
421
746
  }
747
+ .stack {
748
+ position: absolute;
749
+ left: 0;
750
+ right: 0;
751
+ bottom: 0;
752
+ display: flex;
753
+ flex-direction: column-reverse;
754
+ /* A hairline gap between segments: without it a two-colour stack reads as
755
+ one block and the boundary is impossible to find. */
756
+ gap: 2px;
757
+ border-radius: 5px 5px 0 0;
758
+ overflow: hidden;
759
+ transition: height 0.6s ease;
760
+ }
761
+ .stackSeg { width: 100%; transition: height 0.6s ease, filter 0.15s; }
762
+ .stackSeg:hover { filter: brightness(1.15); }
422
763
  .barFill {
423
- border-radius: 4px 4px 2px 2px;
424
- min-height: 4px;
425
- background: var(--dsl-g-accent);
764
+ position: absolute;
765
+ left: 0;
766
+ right: 0;
767
+ border-radius: 5px 5px 0 0;
768
+ min-height: 2px;
769
+ background: linear-gradient(180deg, color-mix(in srgb, var(--dsl-g-accent) 78%, white), var(--dsl-g-accent));
426
770
  transition: height 0.6s ease, filter 0.15s;
427
771
  }
428
772
  .barFill:hover { filter: brightness(1.18); }
429
773
  .barLabel {
430
774
  font-size: var(--dsl-g-font-meta);
431
- color: var(--dsw-alias-label-tertiary);
775
+ color: var(--dsw-alias-label-secondary);
432
776
  text-align: center;
433
777
  white-space: nowrap;
434
778
  overflow: hidden;
@@ -437,23 +781,27 @@
437
781
  }
438
782
 
439
783
  .tabs { display: flex; flex-direction: column; gap: var(--dsl-g-gap-md); }
440
- .tabBar { display: flex; gap: var(--dsl-g-gap-xs); border-bottom: 1px solid var(--dsl-g-border); }
784
+ .tabBar { display: flex; gap: 2px; border-bottom: 1px solid var(--dsl-g-border); }
441
785
  .tab {
442
786
  background: transparent;
443
787
  border: none;
444
- color: var(--dsw-alias-label-tertiary);
788
+ /* Inactive tabs were tertiary (barely readable); secondary + a hover wash
789
+ keeps the hierarchy while staying legible. */
790
+ color: var(--dsw-alias-label-secondary);
445
791
  font-size: var(--dsl-g-font-title);
446
792
  font-weight: 600;
447
793
  font-family: inherit;
448
- padding: 6px 12px;
794
+ padding: 8px 14px;
449
795
  border-bottom: 2px solid transparent;
796
+ border-radius: var(--dsl-g-radius-sm) var(--dsl-g-radius-sm) 0 0;
450
797
  margin-bottom: -1px;
451
798
  cursor: pointer;
452
799
  line-height: 1.5;
453
- transition: color 0.15s, border-bottom-color 0.15s;
800
+ transition: color 0.15s, background 0.15s, border-bottom-color 0.15s;
454
801
  }
455
- .tab:hover { color: var(--dsw-alias-label-secondary); }
456
- .tabActive { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsl-g-accent); }
802
+ .tab:hover { color: var(--dsw-alias-label-primary); background: var(--dsl-g-hover); }
803
+ .tabActive { color: var(--dsw-alias-label-primary); font-weight: 650; border-bottom-color: var(--dsl-g-accent); }
804
+ .tab:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: -2px; }
457
805
 
458
806
  .avatar {
459
807
  width: 32px;
@@ -470,45 +818,38 @@
470
818
  }
471
819
 
472
820
  /* ---------- callout ----------
473
- 克制的信息条:无左竖条、无彩色块底。极淡的底色 + 细边框 + 小圆点标记,
474
- 文字层级轻(像 Linear / Vercel 的 notice),而不是"彩色便签"。 */
821
+ 语义由「底色 + 标题色」表达,不用左侧竖条:竖条是 PPT 式装饰,底色与
822
+ 文字色才是信息层级。 */
475
823
 
476
824
  .callout {
477
825
  display: flex;
478
826
  flex-direction: column;
479
- gap: 2px;
480
- padding: 10px 14px;
481
- border-radius: var(--dsl-g-radius-control);
482
- border: 1px solid var(--dsl-g-border);
827
+ gap: 3px;
828
+ padding: 12px 15px;
829
+ border-radius: var(--dsl-g-radius-surface);
830
+ border: 1px solid var(--dsl-g-border-surface);
483
831
  font-size: var(--dsl-g-font-title);
484
- line-height: 1.6;
485
- background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block));
832
+ line-height: 1.65;
833
+ background: var(--dsl-g-surface);
834
+ box-shadow: var(--dsl-g-shadow-card);
486
835
  }
487
836
  .calloutInfo { background: color-mix(in srgb, var(--dsl-g-accent) var(--dsl-g-tint), transparent); }
488
837
  .calloutSuccess { background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) var(--dsl-g-tint), transparent); }
489
838
  .calloutWarning { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5b83d) var(--dsl-g-tint), transparent); }
490
839
  .calloutError { background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ff6b6b) var(--dsl-g-tint), transparent); }
840
+ .calloutInfo .calloutTitle { color: var(--dsw-alias-state-business-primary, #4f8ef7); }
841
+ .calloutSuccess .calloutTitle { color: var(--dsw-alias-state-success-primary, #2f9e6b); }
842
+ .calloutWarning .calloutTitle { color: var(--dsw-alias-state-warn-primary, #b8860b); }
843
+ .calloutError .calloutTitle { color: var(--dsw-alias-state-error-primary, #d94848); }
491
844
  .calloutTitle {
492
845
  display: flex;
493
846
  align-items: center;
494
847
  gap: 6px;
495
- font-weight: 600;
496
- font-size: var(--dsl-g-font-meta);
497
- letter-spacing: 0.02em;
498
- color: var(--dsw-alias-label-secondary);
499
- }
500
- /* 小圆点:用色点替代左侧竖条表达语义,克制且可读 */
501
- .calloutTitle::before {
502
- content: "";
503
- width: 6px;
504
- height: 6px;
505
- border-radius: 50%;
506
- background: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary));
848
+ font-weight: 700;
849
+ font-size: var(--dsl-g-font-h3);
850
+ letter-spacing: -0.005em;
851
+ color: var(--dsw-alias-label-primary);
507
852
  }
508
- .calloutInfo .calloutTitle::before { background: var(--dsl-g-accent); }
509
- .calloutSuccess .calloutTitle::before { background: var(--dsw-alias-state-success-primary, #3ecf8e); }
510
- .calloutWarning .calloutTitle::before { background: var(--dsw-alias-state-warn-primary, #f5b83d); }
511
- .calloutError .calloutTitle::before { background: var(--dsw-alias-state-error-primary, #ff6b6b); }
512
853
  .calloutBody { color: var(--dsw-alias-label-secondary); }
513
854
 
514
855
  /* ---------- steps ---------- */
@@ -519,9 +860,24 @@
519
860
  padding: 0;
520
861
  display: flex;
521
862
  flex-direction: column;
522
- gap: var(--dsl-g-gap-sm);
863
+ gap: var(--dsl-g-gap-md);
864
+ }
865
+ .step { position: relative; display: flex; gap: var(--dsl-g-gap-md); align-items: flex-start; }
866
+ /* Connector between markers: turns a stack of badges into a visible sequence. */
867
+ .step:not(:last-child)::before {
868
+ content: "";
869
+ position: absolute;
870
+ left: 11px;
871
+ top: 27px;
872
+ bottom: calc(-1 * var(--dsl-g-gap-md));
873
+ width: 1px;
874
+ background: var(--dsl-g-border);
523
875
  }
524
- .step { display: flex; gap: var(--dsl-g-gap-md); align-items: flex-start; }
876
+ /* The rail behind a finished step picks up the success tint, so the sequence
877
+ reads as progress instead of a row of badges. */
878
+ .stepDone:not(:last-child)::before { background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) 45%, transparent); }
879
+ .stepTitle { transition: color 0.15s; }
880
+ .stepActive .stepTitle { color: var(--dsl-g-accent); }
525
881
  .stepMarker {
526
882
  width: 24px;
527
883
  height: 24px;
@@ -531,19 +887,25 @@
531
887
  font-size: var(--dsl-g-font-meta);
532
888
  font-weight: 700;
533
889
  flex-shrink: 0;
534
- background: var(--dsw-alias-bg-layer-1);
535
- border: 1px solid var(--dsl-g-border);
536
- color: var(--dsw-alias-label-tertiary);
890
+ background: var(--dsl-g-surface);
891
+ border: 1px solid var(--dsl-g-border-surface);
892
+ color: var(--dsw-alias-label-secondary);
893
+ z-index: 1;
894
+ }
895
+ .stepDone .stepMarker {
896
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) 18%, transparent);
897
+ border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) 55%, transparent);
898
+ color: var(--dsw-alias-state-success-primary, #2f9e6b);
899
+ font-size: 13px;
537
900
  }
538
- .stepDone .stepMarker { background: var(--dsw-alias-state-success-primary, #3ecf8e); border-color: transparent; color: #fff; font-size: 13px; }
539
901
  .stepActive .stepMarker {
540
902
  background: color-mix(in srgb, var(--dsl-g-accent) var(--dsl-g-tint-strong), transparent);
541
903
  border-color: var(--dsl-g-accent);
542
904
  color: var(--dsl-g-accent);
543
905
  }
544
- .stepContent { display: flex; flex-direction: column; gap: 2px; padding-top: 3px; }
545
- .stepTitle { font-size: var(--dsl-g-font-title); font-weight: 600; color: var(--dsw-alias-label-primary); line-height: 1.5; }
546
- .stepDesc { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-tertiary); line-height: 1.5; }
906
+ .stepContent { display: flex; flex-direction: column; gap: 2px; padding-top: 2px; }
907
+ .stepTitle { font-size: var(--dsl-g-font-body); font-weight: 650; color: var(--dsw-alias-label-primary); line-height: 1.45; }
908
+ .stepDesc { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-secondary); line-height: 1.55; }
547
909
 
548
910
  /* ---------- keyvalue ---------- */
549
911
 
@@ -551,13 +913,14 @@
551
913
  .kvRow {
552
914
  display: flex;
553
915
  gap: var(--dsl-g-gap-md);
554
- padding: 6px 0;
916
+ padding: 7px 0;
555
917
  font-size: var(--dsl-g-font-title);
918
+ align-items: baseline;
556
919
  }
557
920
  .kvRow + .kvRow { border-top: 1px solid var(--dsl-g-border); }
558
921
  .kvRow:last-child { border-bottom: none; }
559
- .kvKey { width: 96px; flex-shrink: 0; color: var(--dsw-alias-label-tertiary); font-weight: 600; }
560
- .kvValue { margin: 0; color: var(--dsw-alias-label-primary); word-break: break-word; line-height: 1.5; }
922
+ .kvKey { width: 120px; flex-shrink: 0; color: var(--dsw-alias-label-secondary); font-weight: 600; }
923
+ .kvValue { margin: 0; color: var(--dsw-alias-label-primary); font-weight: 600; word-break: break-word; line-height: 1.5; font-variant-numeric: tabular-nums; }
561
924
 
562
925
  /* ---------- json scalar ---------- */
563
926
 
@@ -565,7 +928,7 @@
565
928
 
566
929
  /* ---------- chart variants ---------- */
567
930
 
568
- .lineChart { display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
931
+ .lineChart { position: relative; display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
569
932
  .linePath { fill: none; stroke: var(--dsl-g-accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
570
933
  .lineDot { fill: var(--dsl-g-accent); transition: filter 0.15s; }
571
934
  .lineDot:hover { filter: brightness(1.3); }
@@ -576,6 +939,10 @@
576
939
  font-family: var(--dsl-g-font-mono);
577
940
  font-size: var(--dsl-g-font-data);
578
941
  }
942
+ .lineLabel {
943
+ fill: var(--dsw-alias-label-secondary);
944
+ font-size: var(--dsl-g-font-meta);
945
+ }
579
946
  .lineLabels { display: flex; justify-content: space-between; }
580
947
 
581
948
  .groupedBars { display: flex; gap: var(--dsl-g-gap-xs); align-items: flex-end; height: 100%; width: 100%; }
@@ -602,13 +969,295 @@
602
969
  .groupedFill { flex: 0 0 auto; width: 100%; border-radius: 4px 4px 2px 2px; min-height: 3px; background: var(--dsl-g-accent); transition: filter 0.15s; }
603
970
  .groupedFill:hover { filter: brightness(1.18); }
604
971
 
605
- .donut { display: flex; align-items: center; gap: var(--dsl-g-gap-lg); }
972
+ .donut { position: relative; display: flex; align-items: center; gap: var(--dsl-g-gap-lg); }
606
973
  .donutTrack { stroke: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08)); }
607
974
  .donutSeg { transition: filter 0.15s; }
608
975
  .donutSeg:hover { filter: brightness(1.18); }
609
976
  .donutTotal { fill: var(--dsw-alias-label-primary); font-size: var(--dsl-g-font-h2); font-weight: 700; }
610
977
  .donutTotalLabel { fill: var(--dsw-alias-label-tertiary); font-size: 10px; }
611
- .donutLegend { display: flex; flex-direction: column; gap: var(--dsl-g-gap-xs); font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-secondary); line-height: 1.5; }
978
+ .donutLegend { display: flex; flex-direction: column; justify-content: center; gap: var(--dsl-g-gap-sm); font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-secondary); line-height: 1.5; }
979
+ .donutPct { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
980
+
981
+ /* ---------- progress ring + target marker ---------- */
982
+
983
+ .ring { display: flex; align-items: center; gap: var(--dsl-g-gap-md); }
984
+ .ringTrack { stroke: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent); }
985
+ .ringFill { stroke: var(--dsl-g-accent); transition: stroke-dasharray 0.6s ease; }
986
+ .ringValue {
987
+ fill: var(--dsw-alias-label-primary);
988
+ font-family: var(--dsl-g-font-mono);
989
+ font-size: 17px;
990
+ font-weight: 700;
991
+ font-variant-numeric: tabular-nums;
992
+ }
993
+ .ringMeta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
994
+ .ringLabel { font-size: var(--dsl-g-font-body); font-weight: 650; color: var(--dsw-alias-label-primary); }
995
+ .ringSub { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-secondary); }
996
+
997
+ .track { position: relative; overflow: visible; }
998
+ .targetMark {
999
+ position: absolute;
1000
+ top: -3px;
1001
+ bottom: -3px;
1002
+ width: 2px;
1003
+ margin-left: -1px;
1004
+ border-radius: 1px;
1005
+ background: var(--dsw-alias-label-primary);
1006
+ opacity: 0.6;
1007
+ }
1008
+
1009
+ /* Live-filter feedback: how many rows survived the bound control. */
1010
+ .filterRow td {
1011
+ padding: 7px var(--dsl-g-gap-md);
1012
+ border-top: 1px solid var(--dsl-g-border);
1013
+ color: var(--dsw-alias-label-secondary);
1014
+ font-size: var(--dsl-g-font-meta);
1015
+ font-variant-numeric: tabular-nums;
1016
+ background: transparent;
1017
+ }
1018
+ .filterHint {
1019
+ display: block;
1020
+ padding-top: 6px;
1021
+ color: var(--dsw-alias-label-secondary);
1022
+ font-size: var(--dsl-g-font-meta);
1023
+ }
1024
+
1025
+ /* Export chips (`table.export`): a quiet toolbar above the table. */
1026
+ .tableTools {
1027
+ display: flex;
1028
+ justify-content: flex-end;
1029
+ gap: 6px;
1030
+ padding: 6px 8px 4px;
1031
+ }
1032
+ .tableTool {
1033
+ padding: 2px 9px;
1034
+ border: 1px solid var(--dsl-g-border);
1035
+ border-radius: var(--dsl-g-radius-pill);
1036
+ background: transparent;
1037
+ color: var(--dsw-alias-label-secondary);
1038
+ font-family: inherit;
1039
+ font-size: var(--dsl-g-font-meta);
1040
+ cursor: pointer;
1041
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
1042
+ }
1043
+ .tableTool:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsl-g-border-strong); background: var(--dsl-g-hover); }
1044
+ .tableTool:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: 1px; }
1045
+ /* Card accent (`card.accent`): the border comes in inline; the title lifts the
1046
+ hue toward the theme's own text colour so it stays legible on dark AND light
1047
+ surfaces (a raw accent colour on a dark card reads muddy). */
1048
+ .card[style*="--dsl-card-accent"] > .cardTitle {
1049
+ color: color-mix(in srgb, var(--dsl-card-accent) 38%, var(--dsw-alias-label-primary));
1050
+ }
1051
+
1052
+ /* Master-detail rows (table.details): a chevron in the first cell opens a
1053
+ full-width panel underneath, so a table can drill down without leaving the
1054
+ page or opening a modal. */
1055
+ .detailCell { padding-left: var(--dsl-g-gap-sm); }
1056
+ .detailToggle {
1057
+ display: inline-flex;
1058
+ align-items: center;
1059
+ gap: 6px;
1060
+ padding: 0;
1061
+ border: none;
1062
+ background: none;
1063
+ font: inherit;
1064
+ font-weight: 600;
1065
+ color: inherit;
1066
+ cursor: pointer;
1067
+ text-align: left;
1068
+ }
1069
+ .detailToggle:hover { color: var(--dsl-g-accent); }
1070
+ .detailToggle:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: 2px; border-radius: 4px; }
1071
+ .detailChevron {
1072
+ flex-shrink: 0;
1073
+ font-size: 10px;
1074
+ color: var(--dsw-alias-label-tertiary);
1075
+ transition: transform 0.18s ease;
1076
+ }
1077
+ .detailChevron[data-open="true"] { transform: rotate(90deg); }
1078
+ .table tbody tr.detailRow > td {
1079
+ padding: 0 var(--dsl-g-gap-md) 12px;
1080
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent);
1081
+ border-bottom: 1px solid var(--dsl-g-border);
1082
+ }
1083
+ .table tbody tr.detailRow:hover > td { background: color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent); }
1084
+ .detailBody {
1085
+ display: flex;
1086
+ flex-direction: column;
1087
+ gap: var(--dsl-g-gap-sm);
1088
+ padding: 12px 14px;
1089
+ border-radius: var(--dsl-g-radius-control);
1090
+ background: var(--dsw-alias-bg-layer-1, transparent);
1091
+ box-shadow: inset 2px 0 0 color-mix(in srgb, var(--dsl-g-accent) 45%, transparent);
1092
+ }
1093
+
1094
+ /* Section header rows (types[0] === 'group').
1095
+ v7 looked like a data row; v8 was a heavy band; v9 was quiet but still read
1096
+ as "same level" — a hairline and air do not say "these rows belong to me".
1097
+ v10 makes the relationship structural: the header is a TOGGLE (chevron +
1098
+ label + child count) and its rows are indented children under a guide rail,
1099
+ so the tree is unmistakable and each section can be folded away. */
1100
+ .groupRow td {
1101
+ background: none;
1102
+ padding: 22px var(--dsl-g-gap-md) 6px;
1103
+ border-top: none;
1104
+ border-bottom: none;
1105
+ box-shadow: inset 0 1px 0 var(--dsl-g-border);
1106
+ }
1107
+ .table tbody tr.groupRow:first-child td { box-shadow: none; padding-top: 10px; }
1108
+ .table tbody tr.groupRow:hover { background: none; }
1109
+ .table tbody tr.groupRow:nth-child(even) { background: none; }
1110
+ .groupToggle {
1111
+ display: inline-flex;
1112
+ align-items: center;
1113
+ gap: 7px;
1114
+ padding: 0;
1115
+ border: none;
1116
+ background: none;
1117
+ font: inherit;
1118
+ font-size: var(--dsl-g-font-title);
1119
+ font-weight: 650;
1120
+ letter-spacing: 0.02em;
1121
+ color: var(--dsw-alias-label-primary);
1122
+ cursor: pointer;
1123
+ }
1124
+ .groupToggle:hover { color: var(--dsl-g-accent); }
1125
+ .groupToggle:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: 2px; border-radius: 4px; }
1126
+ .groupChevron { color: var(--dsw-alias-label-tertiary); font-size: 11px; width: 10px; }
1127
+ .groupCount {
1128
+ padding: 0 7px;
1129
+ border-radius: var(--dsl-g-radius-pill);
1130
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent);
1131
+ color: var(--dsw-alias-label-secondary);
1132
+ font-size: var(--dsl-g-font-meta);
1133
+ font-weight: 600;
1134
+ font-variant-numeric: tabular-nums;
1135
+ }
1136
+ /* Children: indented one level and tied to the section by a guide rail. */
1137
+ .table tbody tr.groupChild td:first-child {
1138
+ padding-left: calc(var(--dsl-g-gap-md) + 22px);
1139
+ position: relative;
1140
+ }
1141
+ .table tbody tr.groupChild td:first-child::before {
1142
+ content: "";
1143
+ position: absolute;
1144
+ left: calc(var(--dsl-g-gap-md) + 8px);
1145
+ top: 0;
1146
+ bottom: 0;
1147
+ width: 1px;
1148
+ background: var(--dsl-g-border);
1149
+ }
1150
+ .table tbody tr.groupChild:last-child td:first-child::before { bottom: 50%; }
1151
+ .table tfoot td {
1152
+ border-top: 1px solid var(--dsl-g-border-strong);
1153
+ border-bottom: none;
1154
+ font-weight: 700;
1155
+ color: var(--dsw-alias-label-primary);
1156
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent);
1157
+ }
1158
+
1159
+ /* Card surface tones: tint + colored title, no bars. */
1160
+ .cardInfo { background: color-mix(in srgb, var(--dsl-g-accent) var(--dsl-g-tint), transparent); }
1161
+ .cardSuccess { background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #3ecf8e) var(--dsl-g-tint), transparent); }
1162
+ .cardWarning { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5b83d) var(--dsl-g-tint), transparent); }
1163
+ .cardDanger { background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ff6b6b) var(--dsl-g-tint), transparent); }
1164
+ .cardInfo > .cardTitle { color: var(--dsw-alias-state-business-primary, #4f8ef7); }
1165
+ .cardSuccess > .cardTitle { color: var(--dsw-alias-state-success-primary, #2f9e6b); }
1166
+ .cardWarning > .cardTitle { color: var(--dsw-alias-state-warn-primary, #b8860b); }
1167
+ .cardDanger > .cardTitle { color: var(--dsw-alias-state-error-primary, #d94848); }
1168
+
1169
+ /* ---------- horizontal bars (chart.horizontal) ---------- */
1170
+
1171
+ .hbars { display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
1172
+ .hbarRow { display: flex; align-items: center; gap: var(--dsl-g-gap-md); }
1173
+ .hbarLabel {
1174
+ width: 96px;
1175
+ flex-shrink: 0;
1176
+ text-align: right;
1177
+ font-size: var(--dsl-g-font-meta);
1178
+ color: var(--dsw-alias-label-secondary);
1179
+ white-space: nowrap;
1180
+ overflow: hidden;
1181
+ text-overflow: ellipsis;
1182
+ }
1183
+ .hbarTracks { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
1184
+ .hbarSeg { height: 100%; }
1185
+ .hbarTrack {
1186
+ display: flex;
1187
+ height: 14px;
1188
+ border-radius: var(--dsl-g-radius-sm);
1189
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent);
1190
+ overflow: hidden;
1191
+ }
1192
+ .hbarFill {
1193
+ height: 100%;
1194
+ min-width: 2px;
1195
+ border-radius: var(--dsl-g-radius-sm);
1196
+ background: var(--dsl-g-accent);
1197
+ transition: width 0.6s ease;
1198
+ }
1199
+ .hbarValue {
1200
+ width: 64px;
1201
+ flex-shrink: 0;
1202
+ text-align: right;
1203
+ font-family: var(--dsl-g-font-mono);
1204
+ font-size: var(--dsl-g-font-data);
1205
+ color: var(--dsw-alias-label-primary);
1206
+ font-variant-numeric: tabular-nums;
1207
+ }
1208
+
1209
+ /* ---------- table cell types (types: ["bar"|"badge"|...]) ---------- */
1210
+
1211
+ .cellBadge {
1212
+ display: inline-block;
1213
+ padding: 1px 8px;
1214
+ border-radius: var(--dsl-g-radius-pill);
1215
+ background: color-mix(in srgb, var(--dsl-g-accent) var(--dsl-g-tint), transparent);
1216
+ color: var(--dsw-alias-label-primary);
1217
+ font-size: var(--dsl-g-font-meta);
1218
+ font-weight: 600;
1219
+ }
1220
+ .cellSpark { display: block; width: 88px; height: 22px; overflow: visible; }
1221
+ .cellRing { display: inline-flex; align-items: center; gap: 6px; }
1222
+ .cellRingText {
1223
+ font-family: var(--dsl-g-font-mono);
1224
+ font-size: var(--dsl-g-font-data);
1225
+ color: var(--dsw-alias-label-primary);
1226
+ font-variant-numeric: tabular-nums;
1227
+ }
1228
+ .cellIndex {
1229
+ display: inline-block;
1230
+ min-width: 18px;
1231
+ font-family: var(--dsl-g-font-mono);
1232
+ font-size: var(--dsl-g-font-data);
1233
+ color: var(--dsw-alias-label-tertiary);
1234
+ font-variant-numeric: tabular-nums;
1235
+ }
1236
+ .cellBar {
1237
+ position: relative;
1238
+ display: block;
1239
+ min-width: 104px;
1240
+ height: 18px;
1241
+ border-radius: var(--dsl-g-radius-sm);
1242
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent);
1243
+ overflow: hidden;
1244
+ }
1245
+ .cellBarFill {
1246
+ position: absolute;
1247
+ inset: 0 auto 0 0;
1248
+ border-radius: var(--dsl-g-radius-sm);
1249
+ background: color-mix(in srgb, var(--dsl-g-accent) 72%, transparent);
1250
+ }
1251
+ .cellBarText {
1252
+ position: relative;
1253
+ display: block;
1254
+ text-align: right;
1255
+ padding: 0 6px;
1256
+ font-family: var(--dsl-g-font-mono);
1257
+ font-size: var(--dsl-g-font-data);
1258
+ line-height: 18px;
1259
+ color: var(--dsw-alias-label-primary);
1260
+ }
612
1261
 
613
1262
  /* ---------- radio / switch / textarea ---------- */
614
1263
 
@@ -716,8 +1365,8 @@
716
1365
 
717
1366
  .textarea {
718
1367
  width: 100%;
719
- background: var(--dsw-alias-bg-base, #17171a);
720
- border: 1px solid var(--dsl-g-border);
1368
+ background: var(--dsl-g-surface);
1369
+ border: 1px solid var(--dsl-g-border-surface);
721
1370
  border-radius: var(--dsl-g-radius-control);
722
1371
  color: var(--dsw-alias-label-primary);
723
1372
  padding: var(--dsl-g-gap-sm) 12px;
@@ -737,9 +1386,11 @@
737
1386
  .accordion {
738
1387
  display: flex;
739
1388
  flex-direction: column;
740
- border: 1px solid var(--dsl-g-border);
1389
+ border: 1px solid var(--dsl-g-border-surface);
741
1390
  border-radius: var(--dsl-g-radius-surface);
742
1391
  overflow: hidden;
1392
+ background: var(--dsl-g-surface);
1393
+ box-shadow: var(--dsl-g-shadow-card);
743
1394
  }
744
1395
  .accItem { border-bottom: 1px solid var(--dsl-g-border); }
745
1396
  .accItem:last-child { border-bottom: none; }
@@ -749,26 +1400,35 @@
749
1400
  align-items: center;
750
1401
  justify-content: space-between;
751
1402
  gap: var(--dsl-g-gap-sm);
752
- background: var(--dsw-alias-bg-layer-1);
1403
+ background: transparent;
753
1404
  border: none;
754
1405
  color: var(--dsw-alias-label-primary);
755
- padding: 10px 14px;
756
- font-size: var(--dsl-g-font-title);
1406
+ padding: 12px 15px;
1407
+ font-size: var(--dsl-g-font-body);
757
1408
  font-weight: 600;
758
1409
  cursor: pointer;
759
1410
  font-family: inherit;
760
1411
  line-height: 1.5;
1412
+ text-align: left;
761
1413
  transition: background 0.15s, color 0.15s;
762
1414
  }
763
- .accHead:hover { background: color-mix(in srgb, var(--dsw-alias-label-primary) 5%, var(--dsw-alias-bg-layer-1)); }
764
- .accHead:active { color: var(--dsl-g-accent); }
765
- .accChevron { color: var(--dsw-alias-label-tertiary); transition: transform 0.2s; }
1415
+ .accHead:hover { background: var(--dsl-g-hover); }
1416
+ .accHead[aria-expanded="true"] { border-bottom: 1px solid var(--dsl-g-border); }
1417
+ /* One chevron that rotates, instead of swapping glyphs: the state change reads
1418
+ as motion, and the closed/open shapes stay consistent. */
1419
+ .accChevron {
1420
+ flex-shrink: 0;
1421
+ color: var(--dsw-alias-label-tertiary);
1422
+ font-size: 11px;
1423
+ transition: transform 0.2s ease;
1424
+ }
1425
+ .accChevron[data-open="true"] { transform: rotate(90deg); }
766
1426
  .accBody {
767
- padding: var(--dsl-g-gap-md) 14px;
1427
+ padding: 12px 15px 14px;
768
1428
  display: flex;
769
1429
  flex-direction: column;
770
1430
  gap: var(--dsl-g-gap-sm);
771
- background: var(--dsw-alias-bg-base, #17171a);
1431
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 2%, transparent);
772
1432
  }
773
1433
 
774
1434
  /* ---------- copy chip ---------- */
@@ -864,22 +1524,34 @@
864
1524
 
865
1525
  .timeline { display: flex; flex-direction: column; }
866
1526
  .tlItem { display: flex; gap: var(--dsl-g-gap-md); }
867
- .tlRail { display: flex; flex-direction: column; align-items: center; width: 12px; flex-shrink: 0; }
1527
+ .tlRail { display: flex; flex-direction: column; align-items: center; width: 10px; flex-shrink: 0; }
868
1528
  .tlDot {
869
- width: 10px;
870
- height: 10px;
1529
+ width: 9px;
1530
+ height: 9px;
871
1531
  border-radius: 50%;
872
- margin-top: 4px;
1532
+ margin-top: 5px;
873
1533
  flex-shrink: 0;
874
1534
  background: var(--dsl-g-accent);
875
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsl-g-accent) var(--dsl-g-tint-strong), transparent);
876
- }
877
- .tlLine { flex: 1; width: 2px; margin: 2px 0; background: var(--dsl-g-border); }
878
- .tlBody { padding-bottom: var(--dsl-g-gap-lg); flex: 1; min-width: 0; }
1535
+ /* A soft halo instead of a hard ring: the dot reads as a marker on the rail
1536
+ rather than a bullet stuck to the text. */
1537
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsl-g-accent) 16%, transparent);
1538
+ transition: transform 0.15s ease;
1539
+ }
1540
+ .tlItem:hover .tlDot { transform: scale(1.15); }
1541
+ .tlLine { flex: 1; width: 1px; margin: 4px 0 0; background: var(--dsl-g-border); }
1542
+ .tlBody { padding-bottom: 18px; flex: 1; min-width: 0; }
1543
+ .tlItem:last-child .tlBody { padding-bottom: 0; }
879
1544
  .tlHead { display: flex; align-items: baseline; gap: var(--dsl-g-gap-md); }
880
- .tlTitle { font-size: var(--dsl-g-font-title); font-weight: 600; color: var(--dsw-alias-label-primary); line-height: 1.5; }
881
- .tlTime { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-tertiary); margin-left: auto; white-space: nowrap; }
882
- .tlDesc { font-size: var(--dsl-g-font-title); color: var(--dsw-alias-label-secondary); margin-top: 2px; line-height: 1.5; }
1545
+ .tlTitle { font-size: var(--dsl-g-font-body); font-weight: 650; color: var(--dsw-alias-label-primary); line-height: 1.45; }
1546
+ .tlTime {
1547
+ margin-left: auto;
1548
+ white-space: nowrap;
1549
+ font-family: var(--dsl-g-font-mono);
1550
+ font-size: var(--dsl-g-font-data);
1551
+ color: var(--dsw-alias-label-tertiary);
1552
+ font-variant-numeric: tabular-nums;
1553
+ }
1554
+ .tlDesc { font-size: var(--dsl-g-font-title); color: var(--dsw-alias-label-secondary); margin-top: 3px; line-height: 1.55; }
883
1555
 
884
1556
  /* ---------- file tree ---------- */
885
1557
 
@@ -896,28 +1568,51 @@
896
1568
  max-width: 100%;
897
1569
  min-width: 0;
898
1570
  }
899
- .ftRow { display: flex; align-items: center; gap: var(--dsl-g-gap-sm); padding: 3px var(--dsl-g-gap-xs); border-radius: var(--dsl-g-radius-control); }
900
- .ftRow:hover { background: var(--dsw-alias-bg-layer-1); }
901
- .ftIcon {
902
- font-size: 10px;
1571
+ .ftChildren { display: block; }
1572
+ .ftRow {
1573
+ position: relative;
1574
+ display: flex;
1575
+ align-items: center;
1576
+ gap: 6px;
1577
+ min-height: 26px;
1578
+ padding: 2px var(--dsl-g-gap-xs);
1579
+ border-radius: var(--dsl-g-radius-sm);
1580
+ transition: background 0.12s;
1581
+ }
1582
+ .ftRow:hover { background: color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent); }
1583
+ /* One hairline per depth level: the rail that makes a tree read as a tree. */
1584
+ /* Depth rails are absolutely positioned so the indentation itself stays the
1585
+ inline padding-left on the row (it must survive a stylesheet failure). */
1586
+ .ftGuide {
1587
+ position: absolute;
1588
+ top: 0;
1589
+ bottom: 0;
1590
+ width: 1px;
1591
+ background: var(--dsl-g-border);
1592
+ }
1593
+ .ftChevron {
1594
+ width: 10px;
903
1595
  flex-shrink: 0;
904
- width: 13px;
905
- text-align: center;
906
- font-family: var(--dsl-g-font-mono);
907
- color: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary));
1596
+ font-size: 10px;
908
1597
  line-height: 1;
1598
+ color: var(--dsw-alias-label-tertiary);
1599
+ transition: transform 0.15s ease;
1600
+ transform-origin: 50% 50%;
909
1601
  }
910
- .ftIconDir { color: var(--dsl-g-accent); font-size: 11px; }
1602
+ .ftChevron[data-open="true"] { transform: rotate(90deg); }
1603
+ .ftGlyph { display: inline-flex; flex-shrink: 0; color: var(--dsw-alias-label-tertiary); }
1604
+ .ftGlyphDir { color: var(--dsl-g-accent); }
911
1605
  /* Long paths must stay on ONE line (issue #28: per-character vertical
912
1606
  wrapping when the host message column gets narrow). */
913
1607
  .ftName {
914
1608
  color: var(--dsw-alias-label-secondary);
1609
+ font-size: var(--dsl-g-font-title);
915
1610
  white-space: nowrap;
916
1611
  overflow: hidden;
917
1612
  text-overflow: ellipsis;
918
1613
  min-width: 0;
919
1614
  }
920
- .ftDir { color: var(--dsw-alias-label-primary); font-weight: 600; }
1615
+ .ftDir { color: var(--dsw-alias-label-primary); font-weight: 650; }
921
1616
  .ftNameBtn {
922
1617
  display: inline-flex;
923
1618
  align-items: center;
@@ -927,7 +1622,7 @@
927
1622
  background: none;
928
1623
  border: none;
929
1624
  font-family: inherit;
930
- font-size: var(--dsl-g-font-body);
1625
+ font-size: var(--dsl-g-font-title);
931
1626
  padding: 0;
932
1627
  cursor: default;
933
1628
  text-align: left;
@@ -945,6 +1640,52 @@ button.ftNameBtn[aria-expanded] { cursor: pointer; }
945
1640
  .bcCurrent { color: var(--dsw-alias-label-primary); font-weight: 600; cursor: default; }
946
1641
  .bcSep { color: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary)); }
947
1642
 
1643
+ /* ---------- streaming skeleton ----------
1644
+ Shown while a fence body is still arriving and no component has finished.
1645
+ It replaces half-written JSON with a quiet shimmer that mirrors the block
1646
+ layout; the settle sweep restores the raw code block if the body never
1647
+ parses. */
1648
+
1649
+ .skeleton {
1650
+ display: flex;
1651
+ flex-direction: column;
1652
+ gap: var(--dsl-g-gap-md);
1653
+ padding: var(--dsl-g-gap-lg);
1654
+ border: 1px solid var(--dsl-g-border);
1655
+ border-radius: var(--dsl-g-radius-surface);
1656
+ background: var(--dsl-g-surface);
1657
+ }
1658
+ .skeletonTitle {
1659
+ display: block;
1660
+ width: 34%;
1661
+ height: 12px;
1662
+ border-radius: var(--dsl-g-radius-pill);
1663
+ }
1664
+ .skeletonBars { display: flex; gap: var(--dsl-g-gap-sm); }
1665
+ .skeletonBars > span { display: block; height: 44px; border-radius: var(--dsl-g-radius-control); }
1666
+ .skeletonBlock { display: block; height: 76px; border-radius: var(--dsl-g-radius-control); }
1667
+ .skeletonTitle,
1668
+ .skeletonBars > span,
1669
+ .skeletonBlock {
1670
+ background: linear-gradient(
1671
+ 90deg,
1672
+ color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent) 0%,
1673
+ color-mix(in srgb, var(--dsw-alias-label-primary) 13%, transparent) 50%,
1674
+ color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent) 100%
1675
+ );
1676
+ background-size: 200% 100%;
1677
+ animation: dsl-skeleton 1.4s ease-in-out infinite;
1678
+ }
1679
+ @keyframes dsl-skeleton {
1680
+ 0% { background-position: 200% 0; }
1681
+ 100% { background-position: -200% 0; }
1682
+ }
1683
+ @media (prefers-reduced-motion: reduce) {
1684
+ .skeletonTitle,
1685
+ .skeletonBars > span,
1686
+ .skeletonBlock { animation: none; }
1687
+ }
1688
+
948
1689
  /* ---------- staggered reveal ---------- */
949
1690
 
950
1691
  @keyframes genuiReveal {
@@ -1096,7 +1837,16 @@ button.ftNameBtn[aria-expanded] { cursor: pointer; }
1096
1837
  color: var(--dsw-alias-state-success-primary, #3ecf8e);
1097
1838
  line-height: 1.5;
1098
1839
  }
1099
- .quizMarker { width: 16px; text-align: center; font-weight: 700; }
1840
+ .quizMarker {
1841
+ display: inline-block;
1842
+ width: 14px;
1843
+ text-align: center;
1844
+ font-weight: 700;
1845
+ color: var(--dsw-alias-label-tertiary);
1846
+ flex-shrink: 0;
1847
+ }
1848
+ .quizOptCorrect .quizMarker { color: var(--dsw-alias-state-success-primary, #3ecf8e); }
1849
+ .quizOptWrong .quizMarker { color: var(--dsw-alias-state-error-primary, #ff6b6b); }
1100
1850
  .quizResult { display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
1101
1851
  .quizCorrectMsg { font-size: var(--dsl-g-font-title); font-weight: 600; color: var(--dsw-alias-state-success-primary, #3ecf8e); }
1102
1852
  .quizWrongMsg { font-size: var(--dsl-g-font-title); font-weight: 600; color: var(--dsw-alias-state-error-primary, #ff6b6b); }