@issuegraph/viewer 0.3.0 → 0.5.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 (51) hide show
  1. package/dist/document.d.ts +152 -0
  2. package/dist/document.d.ts.map +1 -1
  3. package/dist/document.js +185 -2
  4. package/dist/document.js.map +1 -1
  5. package/dist/element.d.ts +11 -0
  6. package/dist/element.d.ts.map +1 -1
  7. package/dist/element.js.map +1 -1
  8. package/dist/index.d.ts +3 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +28 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/layout.d.ts +51 -50
  13. package/dist/layout.d.ts.map +1 -1
  14. package/dist/layout.js +364 -168
  15. package/dist/layout.js.map +1 -1
  16. package/dist/mount.d.ts +13 -0
  17. package/dist/mount.d.ts.map +1 -1
  18. package/dist/mount.js +69 -1
  19. package/dist/mount.js.map +1 -1
  20. package/dist/navigation.d.ts +14 -0
  21. package/dist/navigation.d.ts.map +1 -1
  22. package/dist/navigation.js +20 -0
  23. package/dist/navigation.js.map +1 -1
  24. package/dist/parts.d.ts +285 -2
  25. package/dist/parts.d.ts.map +1 -1
  26. package/dist/parts.js +781 -63
  27. package/dist/parts.js.map +1 -1
  28. package/dist/projections/graph.d.ts +10 -0
  29. package/dist/projections/graph.d.ts.map +1 -1
  30. package/dist/projections/graph.js +413 -326
  31. package/dist/projections/graph.js.map +1 -1
  32. package/dist/projections/linear.d.ts +90 -0
  33. package/dist/projections/linear.d.ts.map +1 -1
  34. package/dist/projections/linear.js +213 -31
  35. package/dist/projections/linear.js.map +1 -1
  36. package/dist/projections/tree.d.ts.map +1 -1
  37. package/dist/projections/tree.js +26 -3
  38. package/dist/projections/tree.js.map +1 -1
  39. package/dist/styles.d.ts +10 -1
  40. package/dist/styles.d.ts.map +1 -1
  41. package/dist/styles.js +803 -132
  42. package/dist/styles.js.map +1 -1
  43. package/dist/theme.d.ts +5 -6
  44. package/dist/theme.d.ts.map +1 -1
  45. package/dist/theme.js +72 -7
  46. package/dist/theme.js.map +1 -1
  47. package/dist/vocabulary.d.ts +39 -0
  48. package/dist/vocabulary.d.ts.map +1 -1
  49. package/dist/vocabulary.js +27 -0
  50. package/dist/vocabulary.js.map +1 -1
  51. package/package.json +2 -2
package/dist/styles.js CHANGED
@@ -12,14 +12,26 @@
12
12
  * loaded by a bare Node runtime (`ERR_UNKNOWN_FILE_EXTENSION`), and this
13
13
  * package's floor is checked by a smoke test that imports the built entry. A
14
14
  * string also means no consumer needs a bundler to use it.
15
+ *
16
+ * THE VIEWER DRAWS THE PANEL, and that is a decision this file records rather
17
+ * than a detail. §16a is one bordered surface with a header bar, hairline-
18
+ * separated rows and a footer group on a second ground; §9097's done-when read
19
+ * "§16 IS that test — the viewer with no editor attached", so the thing a host
20
+ * mounts with no chrome of its own has to BE the frame. Rows therefore carry no
21
+ * border and no radius of their own — they are separated by the panel's own
22
+ * hairlines — and a host that wants the panel suppressed overrides the four
23
+ * properties on `.ig-viewer`.
15
24
  */
16
25
  export const viewerStylesheet = `
17
26
  .ig-viewer {
18
27
  background: var(--ig-bg);
28
+ border: var(--ig-stroke) solid var(--ig-line);
29
+ border-radius: var(--ig-radius-large);
19
30
  color: var(--ig-text-body);
20
31
  font-family: var(--ig-font-ui);
21
32
  font-size: var(--ig-font-size);
22
33
  line-height: var(--ig-line-height);
34
+ overflow: hidden;
23
35
  position: relative;
24
36
  }
25
37
 
@@ -31,7 +43,7 @@ export const viewerStylesheet = `
31
43
 
32
44
  .ig-viewer :focus-visible {
33
45
  outline: var(--ig-focus-ring) solid var(--ig-focus);
34
- outline-offset: var(--ig-space-tight);
46
+ outline-offset: calc(var(--ig-focus-ring) * -1);
35
47
  }
36
48
 
37
49
  .ig-list {
@@ -40,28 +52,366 @@ export const viewerStylesheet = `
40
52
  padding: 0;
41
53
  }
42
54
 
43
- /* ── the spine ─────────────────────────────────────────────────────────── */
55
+ /* ── the panel header ──────────────────────────────────────────────────── */
44
56
 
45
- .ig-slot {
57
+ /* Present only when the host stated a fact; a document with none draws no
58
+ header at all, which is what keeps the pure-graph markup unchanged. */
59
+ .ig-header {
60
+ background: var(--ig-surface-2);
61
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--ig-space);
65
+ margin: 0;
66
+ padding: var(--ig-space-loose) var(--ig-space-wide);
67
+ }
68
+
69
+ .ig-header-lead {
46
70
  align-items: center;
47
- background: var(--ig-surface);
71
+ display: inline-flex;
72
+ gap: var(--ig-space-snug);
73
+ }
74
+
75
+ /* The segmented projection toggle §16a and §16b both draw. One rounded outline
76
+ around two buttons, the pressed one filled with the accent. */
77
+ .ig-toggle {
78
+ border: var(--ig-stroke) solid var(--ig-line);
79
+ border-radius: var(--ig-radius);
80
+ display: inline-flex;
81
+ overflow: hidden;
82
+ }
83
+
84
+ .ig-toggle-option {
85
+ align-items: center;
86
+ background: none;
87
+ border: 0;
88
+ color: var(--ig-text-body);
89
+ cursor: pointer;
90
+ display: inline-flex;
91
+ font: inherit;
92
+ font-size: var(--ig-font-size-small);
93
+ gap: var(--ig-space-tight);
94
+ padding: var(--ig-space-micro) var(--ig-space-snug);
95
+ }
96
+
97
+ .ig-toggle-option[aria-pressed='true'] {
98
+ background: var(--ig-accent);
99
+ color: var(--ig-bg);
100
+ font-weight: var(--ig-weight-strong);
101
+ }
102
+
103
+ .ig-header-top {
104
+ align-items: center;
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: var(--ig-space);
108
+ justify-content: space-between;
109
+ }
110
+
111
+ /* The panel's own name, at the tracking the frame gives a frame-level label. */
112
+ .ig-header-label {
113
+ color: var(--ig-accent);
114
+ font-family: var(--ig-font-mono);
115
+ font-size: var(--ig-font-size-small);
116
+ letter-spacing: var(--ig-tracking-label);
117
+ text-transform: uppercase;
118
+ }
119
+
120
+ /* THE COUNTS ARE CHIPS, NOT A SENTENCE. The frame gives each tally its own
121
+ outline and tints only the one a reader acts on — "how many could run right
122
+ now, against the cap" — so the accent is spent once instead of on a whole
123
+ line of muted prose. */
124
+ .ig-counts {
125
+ align-items: center;
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ gap: var(--ig-space-snug);
129
+ margin: 0;
130
+ }
131
+
132
+ .ig-count-chip {
133
+ background: var(--ig-bg);
134
+ border: var(--ig-stroke) solid var(--ig-line);
135
+ border-radius: var(--ig-radius);
136
+ color: var(--ig-text-body);
137
+ font-family: var(--ig-font-mono);
138
+ font-size: var(--ig-font-size-small);
139
+ font-variant-numeric: tabular-nums;
140
+ padding: var(--ig-space-micro) var(--ig-space-snug);
141
+ }
142
+
143
+ .ig-count-chip[data-count='ready'] {
144
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-fill), transparent);
145
+ border-color: color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
146
+ color: var(--ig-text);
147
+ }
148
+
149
+ .ig-freshness {
150
+ align-items: center;
151
+ color: var(--ig-text-muted);
152
+ display: inline-flex;
153
+ font-family: var(--ig-font-mono);
154
+ font-size: var(--ig-font-size-small);
155
+ font-variant-numeric: tabular-nums;
156
+ gap: var(--ig-space-tight);
157
+ margin: 0;
158
+ }
159
+
160
+ /* STALE GOES GOLD, as the design draws it. The gold this palette already holds
161
+ is the conflict state's — "the document moved upstream" — and a stale mirror
162
+ is that fact about the whole document, so the state hue is reused rather
163
+ than a token added for one word. No new token: the theme's token list is
164
+ the contract every host theme has to fill. */
165
+ .ig-freshness[data-stale='true'] {
166
+ color: var(--ig-state-conflict);
167
+ }
168
+
169
+ /* A control the viewer publishes and does not wire: the UA button reset, so it
170
+ reads as the inline control the frame draws beside the stamp. */
171
+ .ig-refresh,
172
+ .ig-size {
173
+ background: none;
174
+ border: var(--ig-stroke) solid var(--ig-line);
175
+ border-radius: var(--ig-radius);
176
+ color: var(--ig-text);
177
+ cursor: pointer;
178
+ font: inherit;
179
+ padding: var(--ig-space-micro) var(--ig-space-snug);
180
+ }
181
+
182
+ .ig-refresh:hover,
183
+ .ig-size:hover {
184
+ border-color: var(--ig-accent);
185
+ color: var(--ig-accent);
186
+ }
187
+
188
+ /* ── the panel-level notice ────────────────────────────────────────────── */
189
+
190
+ /* WHAT THE HOST SAYS IS TRUE OF THE WHOLE PANEL, drawn between the NOW row and
191
+ the order it qualifies. Its own surface, so it reads as a statement about the
192
+ panel rather than as a row that lost its rank. */
193
+ .ig-notice {
194
+ align-items: flex-start;
195
+ background: var(--ig-surface-2);
48
196
  border: var(--ig-stroke) solid var(--ig-line);
49
197
  border-radius: var(--ig-radius);
198
+ display: flex;
199
+ gap: var(--ig-space-snug);
200
+ justify-content: space-between;
201
+ padding: var(--ig-space-snug) var(--ig-space-loose);
202
+ }
203
+
204
+ .ig-notice-text {
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: var(--ig-space-micro);
208
+ }
209
+
210
+ .ig-notice-head {
211
+ align-items: center;
212
+ color: var(--ig-text);
213
+ display: flex;
214
+ gap: var(--ig-space-tight);
215
+ margin: 0;
216
+ }
217
+
218
+ .ig-notice-body {
219
+ color: var(--ig-text-body);
220
+ margin: 0;
221
+ }
222
+
223
+ /* A COUNT, SO IT SITS ON THE FIGURE FACE. A progress figure is read as a number,
224
+ and the proportional face makes its digits wander the way a rank column's do. */
225
+ .ig-notice-progress {
226
+ color: var(--ig-text-muted);
227
+ font-family: var(--ig-font-mono);
228
+ font-size: var(--ig-font-size-small);
229
+ font-variant-numeric: tabular-nums;
230
+ margin: 0;
231
+ }
232
+
233
+ /* THE SAME CONTROL RESET THE REFRESH BUTTON TAKES, and for the same reason: it
234
+ is published and not wired, so it must look like the inline control the frame
235
+ draws rather than like a UA button. The anchor arm gets it too — a host that
236
+ routes instead of acting should not get a different-looking affordance. */
237
+ .ig-notice-action {
238
+ background: none;
239
+ border: var(--ig-stroke) solid var(--ig-line);
240
+ border-radius: var(--ig-radius);
241
+ color: var(--ig-text);
242
+ cursor: pointer;
243
+ font: inherit;
244
+ padding: var(--ig-space-micro) var(--ig-space-snug);
245
+ text-decoration: none;
246
+ white-space: nowrap;
247
+ }
248
+
249
+ .ig-notice-action:hover {
250
+ border-color: var(--ig-accent);
251
+ color: var(--ig-accent);
252
+ }
253
+
254
+ /* GOLD ON FIRST IMPORT, and gold is the conflict state's hue — the same reuse
255
+ the stale stamp already makes, for the same reason: the token list is the
256
+ contract every host theme has to fill, so a token added for one state is a
257
+ theme every host has to revisit.
258
+
259
+ AND NOTHING FOR THE ERROR ARM. The design makes exactly one panel-level outline
260
+ claim, and it is this one; an error is drawn calm on purpose, because its own
261
+ sentence is that settings are safe and the pipeline is still running. A
262
+ failure-hued outline around the whole panel would say the opposite. */
263
+ .ig-viewer[data-ig-condition='importing'] {
264
+ outline: var(--ig-stroke) solid var(--ig-state-conflict);
265
+ outline-offset: var(--ig-space-micro);
266
+ }
267
+
268
+ .ig-notice[data-ig-condition='importing'] .ig-notice-head {
269
+ color: var(--ig-state-conflict);
270
+ }
271
+
272
+ .ig-notice[data-ig-condition='error'] .ig-notice-head {
273
+ color: var(--ig-state-failed);
274
+ }
275
+
276
+ /* ── the adoption line ─────────────────────────────────────────────────── */
277
+
278
+ /* ONE QUIET LINE, which is the whole of what the design asks for here: the
279
+ day-one panel has to read complete and calm, and a block explaining what the
280
+ reader is missing is the opposite of calm. */
281
+ .ig-adoption {
282
+ align-items: center;
283
+ color: var(--ig-text-muted);
284
+ display: flex;
285
+ font-size: var(--ig-font-size-small);
286
+ gap: var(--ig-space-tight);
287
+ margin: 0;
288
+ }
289
+
290
+ /* THE SENTENCE IS NOT THE LINK. Underlining the whole line made the quietest
291
+ element on the panel its loudest; the link is a short label beside it. */
292
+ .ig-adoption-text {
293
+ color: var(--ig-text-muted);
294
+ }
295
+
296
+ .ig-adoption-link {
297
+ color: var(--ig-text-muted);
298
+ margin-left: auto;
299
+ text-decoration: underline;
300
+ white-space: nowrap;
301
+ }
302
+
303
+ .ig-adoption-link:hover {
304
+ color: var(--ig-accent);
305
+ }
306
+
307
+ /* PUBLISHED, NOT PERFORMED. The viewer never removes its own line; the host
308
+ re-renders without the note. */
309
+ .ig-adoption-dismiss {
310
+ background: none;
311
+ border: 0;
312
+ color: var(--ig-text-muted);
313
+ cursor: pointer;
314
+ font: inherit;
315
+ padding: 0;
316
+ text-decoration: underline;
317
+ }
318
+
319
+ .ig-adoption-dismiss:hover {
320
+ color: var(--ig-accent);
321
+ }
322
+
323
+ /* ── working now ───────────────────────────────────────────────────────── */
324
+
325
+ .ig-now {
326
+ list-style: none;
327
+ margin: 0;
328
+ padding: 0;
329
+ }
330
+
331
+ /* A BAND, NOT A CARD. The frame runs it full-bleed across the panel with a
332
+ wash and an accent rail down its leading edge — the same treatment the shell
333
+ gives anything in flight — so it reads as a state of the panel rather than
334
+ as the first row of the order. */
335
+ .ig-now-row {
336
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-wash), transparent);
337
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
338
+ box-shadow: inset var(--ig-band-rail) 0 0 var(--ig-accent);
50
339
  column-gap: var(--ig-space);
51
340
  display: grid;
52
- grid-template-columns: auto auto 1fr auto;
53
- margin-bottom: var(--ig-space-tight);
54
- min-height: var(--ig-row-height);
55
- padding: var(--ig-space-tight) var(--ig-space);
341
+ grid-template-columns: auto 1fr auto;
342
+ align-items: center;
343
+ padding: var(--ig-space) var(--ig-space-wide);
344
+ }
345
+
346
+ .ig-now-mark {
347
+ background: var(--ig-accent);
348
+ border-radius: var(--ig-radius-small);
349
+ color: var(--ig-bg);
350
+ font-family: var(--ig-font-mono);
351
+ font-size: var(--ig-font-size-pill);
352
+ font-weight: var(--ig-weight-strong);
353
+ letter-spacing: var(--ig-tracking-pill);
354
+ padding: var(--ig-space-micro) var(--ig-space-tight);
355
+ text-transform: uppercase;
356
+ }
357
+
358
+ .ig-now-phase {
359
+ align-items: center;
360
+ color: var(--ig-accent);
361
+ display: inline-flex;
362
+ font-family: var(--ig-font-mono);
363
+ font-size: var(--ig-font-size-small);
364
+ gap: var(--ig-space-tight);
365
+ white-space: nowrap;
56
366
  }
57
367
 
368
+ /* The pulse is the frame's, and it is the one moving thing in the panel. */
369
+ .ig-now-pulse {
370
+ background: var(--ig-accent);
371
+ block-size: var(--ig-space-tight);
372
+ border-radius: 50%;
373
+ display: inline-block;
374
+ inline-size: var(--ig-space-tight);
375
+ }
376
+
377
+ /* ── the order ─────────────────────────────────────────────────────────── */
378
+
379
+ /* TWO COLUMNS, WHICH IS THE FRAME'S GRID EXACTLY. The rank track holds the
380
+ figure and the readiness dot stacked, and everything else is one body
381
+ column, so a row has exactly two children and nothing can auto-place into
382
+ the rank track and set its width. That was a real defect: a single
383
+ relationship badge left unplaced widened the rank column for the whole list
384
+ and squeezed every title to pay for it. */
385
+ .ig-slot {
386
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
387
+ column-gap: var(--ig-space);
388
+ display: grid;
389
+ grid-template-columns: var(--ig-rank-column) 1fr;
390
+ min-height: var(--ig-row-min-height);
391
+ padding: var(--ig-row-padding-block) var(--ig-space-wide);
392
+ }
393
+
394
+ /* THE ONE ROW GROUND THE FRAME DRAWS, and it is not a stripe. A together unit
395
+ is the compound station, and the tint says so; the other ranked rows carry
396
+ no background at all. */
397
+ .ig-slot[data-unit='true'] {
398
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-unit), transparent);
399
+ }
400
+
401
+ /* A HELD ROW IS HATCHED, NOT DASHED. It keeps its position in the sequence —
402
+ "why isn't my P1 running" is answerable in place — so it must read as
403
+ unavailable without leaving the grid the rows above and below hang off. A
404
+ dashed border around one row breaks that alignment; a ground does not. */
58
405
  .ig-slot[data-held='true'] {
59
- background: var(--ig-surface-2);
60
- border-style: dashed;
406
+ background-image: repeating-linear-gradient(
407
+ 135deg,
408
+ color-mix(in srgb, var(--ig-text) var(--ig-tint-unit), transparent) 0 var(--ig-space-tight),
409
+ transparent var(--ig-space-tight) calc(var(--ig-space-tight) * 2)
410
+ );
61
411
  }
62
412
 
63
413
  .ig-slot[aria-current='true'] {
64
- border-color: var(--ig-accent);
414
+ box-shadow: inset var(--ig-band-rail) 0 0 var(--ig-accent);
65
415
  }
66
416
 
67
417
  /* A CANVAS-OWNED NODE IS SELECTABLE TOO, and only the rail rows had a selected
@@ -75,16 +425,43 @@ export const viewerStylesheet = `
75
425
  stroke-width: calc(var(--ig-stroke) * 2);
76
426
  }
77
427
 
428
+ /* The rank track: figure over dot, centred, so the title column starts at one
429
+ x on every row whether or not the row has a number to print. */
430
+ .ig-rank-cell {
431
+ align-items: center;
432
+ display: flex;
433
+ flex-direction: column;
434
+ gap: var(--ig-space-tight);
435
+ }
436
+
78
437
  .ig-rank {
79
438
  color: var(--ig-text);
80
439
  font-family: var(--ig-font-mono);
440
+ font-size: var(--ig-font-size-rank);
81
441
  font-variant-numeric: tabular-nums;
82
- min-width: calc(var(--ig-char-width) * 4);
83
- text-align: right;
442
+ font-weight: var(--ig-weight-strong);
443
+ line-height: 1;
84
444
  }
85
445
 
86
446
  .ig-rank[data-held='true'] {
87
447
  color: var(--ig-text-muted);
448
+ font-weight: var(--ig-weight-regular);
449
+ }
450
+
451
+ .ig-row-body {
452
+ display: flex;
453
+ flex-direction: column;
454
+ gap: var(--ig-space-snug);
455
+ min-width: 0;
456
+ }
457
+
458
+ /* The title, the identity and the two-line pair they form: 2px apart, which is
459
+ what makes them read as one object rather than as two rows of metadata. */
460
+ .ig-row-head {
461
+ display: flex;
462
+ flex-direction: column;
463
+ gap: var(--ig-space-micro);
464
+ min-width: 0;
88
465
  }
89
466
 
90
467
  /* ── readiness stations ────────────────────────────────────────────────── */
@@ -93,7 +470,6 @@ export const viewerStylesheet = `
93
470
  block-size: var(--ig-station-size);
94
471
  border: var(--ig-stroke) solid var(--ig-station-held);
95
472
  border-radius: 50%;
96
- box-shadow: 0 0 0 var(--ig-station-halo) var(--ig-bg);
97
473
  display: inline-block;
98
474
  inline-size: var(--ig-station-size);
99
475
  }
@@ -116,35 +492,84 @@ export const viewerStylesheet = `
116
492
 
117
493
  /* ── titles, identity, provenance ──────────────────────────────────────── */
118
494
 
495
+ /* NOTHING TRUNCATES. The ellipsis this rule used to carry, against a FIXED row
496
+ height, is the direct cause of the frame's titles arriving on screen as
497
+ "Retype the ca…" — a panel whose job is to say what the work is, declining
498
+ to say what the work is. A row grows instead; --ig-row-min-height is a
499
+ floor, not a height. anywhere covers the one case wrapping cannot: an
500
+ unbroken token wider than the column, which would otherwise overflow the
501
+ panel rather than the line. */
119
502
  .ig-title {
120
503
  color: var(--ig-text);
121
- overflow: hidden;
122
- text-overflow: ellipsis;
123
- white-space: nowrap;
504
+ font-size: var(--ig-font-size-row);
505
+ overflow-wrap: anywhere;
506
+ }
507
+
508
+ .ig-slot[data-held='true'] .ig-title,
509
+ .ig-footer .ig-title {
510
+ color: var(--ig-text-body);
511
+ }
512
+
513
+ /* A held row's CONTENT recedes, not its position. The frame dims the body and
514
+ leaves the rank track at full strength, so the row still reads as occupying
515
+ the slot it would have taken. */
516
+ .ig-slot[data-held='true'] .ig-row-body {
517
+ opacity: 0.72;
518
+ }
519
+
520
+ /* The isolated-issue tally sits directly on the panel, outside the padded
521
+ groups, so it carries the panel's own inset rather than none. */
522
+ .ig-count {
523
+ margin: 0;
524
+ padding: var(--ig-space) var(--ig-space-wide);
124
525
  }
125
526
 
126
527
  .ig-id,
127
528
  .ig-count {
128
529
  color: var(--ig-text-muted);
129
530
  font-family: var(--ig-font-mono);
531
+ font-size: var(--ig-font-size-small);
130
532
  font-variant-numeric: tabular-nums;
131
533
  }
132
534
 
133
535
  .ig-link {
134
- color: var(--ig-accent);
536
+ color: inherit;
135
537
  text-decoration: none;
136
538
  }
137
539
 
138
540
  .ig-link:hover {
541
+ color: var(--ig-accent);
139
542
  text-decoration: underline;
140
543
  }
141
544
 
545
+ /* THE PROVENANCE LINE IS NOT MUTED. It is the answer to "why is this here",
546
+ which is the panel's whole reason to exist, so the frame gives it body text
547
+ and lifts the named parts to the brightest ink on the page. Muting the whole
548
+ sentence — which is what shipped — files the explanation under decoration. */
142
549
  .ig-provenance,
143
550
  .ig-hold,
144
551
  .ig-caveat {
145
- color: var(--ig-text-muted);
552
+ align-items: baseline;
553
+ color: var(--ig-text-body);
554
+ display: flex;
555
+ font-family: var(--ig-font-mono);
146
556
  font-size: var(--ig-font-size-small);
147
- grid-column: 3 / -1;
557
+ gap: var(--ig-space-tight);
558
+ margin: 0;
559
+ }
560
+
561
+ .ig-provenance .ig-id,
562
+ .ig-hold .ig-id,
563
+ .ig-caveat .ig-id {
564
+ color: var(--ig-text);
565
+ }
566
+
567
+ /* The turnstile the frame puts in front of every explanation, so the line reads
568
+ as subordinate to the row above it without being dimmed. */
569
+ .ig-turn {
570
+ color: var(--ig-text-muted);
571
+ font-size: var(--ig-font-size-micro);
572
+ flex: 0 0 auto;
148
573
  }
149
574
 
150
575
  .ig-strike {
@@ -153,155 +578,224 @@ export const viewerStylesheet = `
153
578
 
154
579
  /* ── edge badges ───────────────────────────────────────────────────────── */
155
580
 
156
- /* SPANNED FROM THE TITLE COLUMN, LIKE EVERY OTHER FULL-WIDTH ROW MEMBER. Left
157
- unplaced this was the fifth child of a four-column grid, so it auto-placed
158
- into column ONE of a second row — the rank track, an auto-sized column that
159
- takes the width of its widest item. One relationship badge then set the width
160
- of the rank column for the whole row, pushing the station and title across
161
- and squeezing the title to pay for it. Badges belong with the row's other
162
- metadata, which is what 3 / -1 already means here for provenance and holds.
163
- (No backticks in this file: the stylesheet is a template literal.) */
164
581
  .ig-badges {
165
582
  display: flex;
166
583
  flex-wrap: wrap;
167
584
  gap: var(--ig-space-tight);
168
- grid-column: 3 / -1;
169
585
  }
170
586
 
587
+ /* A TINTED CHIP, NOT A BARE OUTLINE. The frame fills every relationship chip
588
+ with its own hue at a low alpha inside a heavier border of the same hue;
589
+ drawn as an outline alone the badge row reads as a row of empty boxes, and
590
+ the hue channel carries a third of the weight the design gives it. The two
591
+ alphas are the theme's, so a host retinting a relationship retints its chip. */
171
592
  .ig-badge {
172
593
  align-items: center;
173
- /* Not currentColor any more: the label and the border now carry DIFFERENT
174
- colours deliberately — see below. Left as currentColor the border would
175
- follow the text to --ig-text and the hue channel would vanish from the
176
- badge entirely, which is the opposite of the fix. */
594
+ background: var(--ig-surface-2);
177
595
  border: var(--ig-stroke) solid var(--ig-line);
178
- border-radius: var(--ig-radius);
596
+ border-radius: var(--ig-radius-small);
179
597
  color: var(--ig-text);
180
598
  display: inline-flex;
181
- font-size: var(--ig-font-size-small);
599
+ font-family: var(--ig-font-mono);
600
+ font-size: var(--ig-font-size-micro);
182
601
  gap: var(--ig-space-tight);
183
- padding: 0 var(--ig-space-tight);
602
+ padding: var(--ig-space-micro) var(--ig-space-tight);
184
603
  }
185
604
 
186
- /* THE HUE IS A BORDER COLOUR HERE, NOT A TEXT COLOUR — the theme holds the
605
+ /* THE HUE IS A BORDER AND A FILL HERE, NOT A TEXT COLOUR — the theme holds the
187
606
  edge hues to the 3:1 NON-TEXT bar, and says so where it defines them, so
188
- painting badge LABELS with them contradicted the palette's own claim. It was
607
+ painting badge LABELS with them contradicts the palette's own claim. It was
189
608
  not merely theoretical: at this size duplicate-of measured 3.98:1 on
190
609
  --ig-surface and decomposed-from 4.37:1, both under the 4.5:1 the text test
191
- asserts for every text colour. The label now takes --ig-text, which that
192
- test already proves on all three surfaces, and the hue keeps the non-text
193
- use it was measured for.
194
- NO CHANNEL IS LOST. Hue is one of four redundant channels and it is still
195
- carried by the border; the dash pattern and the glyph are untouched; and the
610
+ asserts for every text colour. The label takes --ig-text, which that test
611
+ already proves on all three surfaces, and the hue keeps the non-text use it
612
+ was measured for. This is a DELIBERATE DEPARTURE from the frame, which tints
613
+ its chip labels; the frame's own colour-blind-safety clause says hue is one
614
+ of four redundant channels, and here it is carried by two of them.
615
+ NO CHANNEL IS LOST. The dash pattern and the glyph are untouched, and the
196
616
  vocabulary test independently proves all five stay distinguishable with hue
197
617
  removed ENTIRELY, which is the stronger claim. */
198
- .ig-badge[data-edge='blocked-by'] { border-color: var(--ig-edge-blocked-by); border-style: solid; }
199
- .ig-badge[data-edge='serialize-with'] { border-color: var(--ig-edge-serialize-with); border-style: double; }
200
- .ig-badge[data-edge='together-with'] { border-color: var(--ig-edge-together-with); border-style: solid; }
201
- .ig-badge[data-edge='duplicate-of'] { border-color: var(--ig-edge-duplicate-of); border-style: dotted; }
202
- .ig-badge[data-edge='decomposed-from'] { border-color: var(--ig-edge-decomposed-from); border-style: dashed; }
618
+ /* WRITTEN OUT PER EDGE RATHER THAN THROUGH ONE INTERMEDIATE PROPERTY. A
619
+ an intermediate --ig-badge-hue set here and read here would be a property no
620
+ theme and no layout declares, and styles.test.ts refuses those on purpose:
621
+ the whole point of that guard is that every var() in this file names
622
+ something a host or the layout actually sets. */
623
+ .ig-badge[data-edge='blocked-by'] {
624
+ background: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-fill), transparent);
625
+ border-color: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-border), transparent);
626
+ border-style: solid;
627
+ }
203
628
 
204
- .ig-glyph {
205
- font-family: var(--ig-font-mono);
629
+ .ig-badge[data-edge='serialize-with'] {
630
+ background: color-mix(in srgb, var(--ig-edge-serialize-with) var(--ig-tint-fill), transparent);
631
+ border-color: color-mix(in srgb, var(--ig-edge-serialize-with) var(--ig-tint-border), transparent);
632
+ border-style: double;
206
633
  }
207
634
 
208
- /* ── the footer group: holds that earn no rank slot ────────────────────── */
635
+ .ig-badge[data-edge='together-with'] {
636
+ background: color-mix(in srgb, var(--ig-edge-together-with) var(--ig-tint-fill), transparent);
637
+ border-color: color-mix(in srgb, var(--ig-edge-together-with) var(--ig-tint-border), transparent);
638
+ border-style: solid;
639
+ }
209
640
 
210
- .ig-footer {
211
- border-top: var(--ig-stroke) solid var(--ig-line);
212
- margin-top: var(--ig-space);
213
- padding-top: var(--ig-space);
641
+ .ig-badge[data-edge='duplicate-of'] {
642
+ background: color-mix(in srgb, var(--ig-edge-duplicate-of) var(--ig-tint-fill), transparent);
643
+ border-color: color-mix(in srgb, var(--ig-edge-duplicate-of) var(--ig-tint-border), transparent);
644
+ border-style: dotted;
214
645
  }
215
646
 
216
- .ig-footer-title {
217
- color: var(--ig-text-muted);
218
- font-size: var(--ig-font-size-small);
219
- margin: 0 0 var(--ig-space-tight);
647
+ .ig-badge[data-edge='decomposed-from'] {
648
+ background: color-mix(in srgb, var(--ig-edge-decomposed-from) var(--ig-tint-fill), transparent);
649
+ border-color: color-mix(in srgb, var(--ig-edge-decomposed-from) var(--ig-tint-border), transparent);
650
+ border-style: dashed;
220
651
  }
221
652
 
222
- /* ── the host facts: what the graph cannot say ─────────────────────────── */
653
+ /* A PROMOTION IS THE ONE CHIP THE FRAME ACCENTS. It is the most interesting
654
+ event the panel can show — a low tier pulled to the top because it blocks a
655
+ high one — and the design spends its accent on exactly that. */
656
+ .ig-badge[data-priority='promoted'] {
657
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-fill), transparent);
658
+ border-color: color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
659
+ }
223
660
 
224
- /* Present only when the host stated a fact; a document with none draws no
225
- header at all, which is what keeps the pure-graph markup unchanged. */
226
- .ig-header {
227
- align-items: baseline;
661
+ .ig-badge[data-evidence='verified'] {
662
+ background: color-mix(in srgb, var(--ig-station-ready) var(--ig-tint-fill), transparent);
663
+ border-color: color-mix(in srgb, var(--ig-station-ready) var(--ig-tint-border), transparent);
664
+ }
665
+
666
+ /* A caveat or hold-label chip carries no relationship hue: it is a word, not
667
+ an edge, so it keeps the line colour the plain badge has. */
668
+ .ig-badge[data-caveat],
669
+ .ig-badge[data-hold] {
670
+ color: var(--ig-text-body);
671
+ }
672
+
673
+ .ig-badge[data-caveat='preview-only'] {
674
+ border-color: color-mix(in srgb, var(--ig-state-conflict) var(--ig-tint-border), transparent);
675
+ }
676
+
677
+ .ig-badge[data-not-ready='true'] {
678
+ background: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-fill), transparent);
679
+ border-color: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-border), transparent);
680
+ }
681
+
682
+ .ig-glyph {
683
+ font-family: var(--ig-font-mono);
684
+ }
685
+
686
+ /* ── the together unit: one rank, two issues ───────────────────────────── */
687
+
688
+ .ig-unit-mark {
689
+ align-items: center;
228
690
  display: flex;
229
- flex-wrap: wrap;
230
- gap: var(--ig-space);
231
- margin: 0 0 var(--ig-space);
691
+ gap: var(--ig-space-snug);
232
692
  }
233
693
 
234
- .ig-summary,
235
- .ig-freshness {
694
+ .ig-unit-pill {
695
+ background: var(--ig-accent);
696
+ border-radius: var(--ig-radius-small);
697
+ color: var(--ig-bg);
698
+ font-family: var(--ig-font-mono);
699
+ font-size: var(--ig-font-size-pill);
700
+ font-weight: var(--ig-weight-strong);
701
+ letter-spacing: var(--ig-tracking-badge);
702
+ padding: var(--ig-space-micro) var(--ig-space-tight);
703
+ text-transform: uppercase;
704
+ }
705
+
706
+ .ig-unit-note {
236
707
  color: var(--ig-text-muted);
237
- font-size: var(--ig-font-size-small);
238
- font-variant-numeric: tabular-nums;
708
+ font-family: var(--ig-font-mono);
709
+ font-size: var(--ig-font-size-micro);
710
+ }
711
+
712
+ /* THE MEMBERS GET ONE LINE EACH. Joining them with a separator — which is what
713
+ shipped — makes a two-issue unit read as one issue with a long title, and
714
+ the unit is the one place the design draws an enclosure to say the opposite. */
715
+ /* THE UNIT'S ENCLOSURE IS THE together-with MARK. It is drawn at the
716
+ connector's hairline width, which is what that token has always meant: finer
717
+ than an ordinary edge, so the enclosure stays the primary read. */
718
+ .ig-unit {
719
+ border: var(--ig-stroke-connector) solid color-mix(in srgb, var(--ig-edge-together-with) var(--ig-tint-border), transparent);
720
+ border-radius: var(--ig-radius);
721
+ list-style: none;
239
722
  margin: 0;
723
+ overflow: hidden;
724
+ padding: 0;
240
725
  }
241
726
 
242
- /* STALE GOES GOLD, as the design draws it. The gold this palette already holds
243
- is the conflict state's — "the document moved upstream" — and a stale mirror
244
- is that fact about the whole document, so the state hue is reused rather
245
- than a token added for one word. No new token: the theme's token list is
246
- the contract every host theme has to fill. */
247
- .ig-freshness[data-stale='true'] {
248
- color: var(--ig-state-conflict);
727
+ .ig-unit-member {
728
+ display: flex;
729
+ flex-direction: column;
730
+ gap: var(--ig-space-micro);
731
+ padding: var(--ig-space-snug) var(--ig-space);
249
732
  }
250
733
 
251
- /* A control the viewer publishes and does not wire: the UA button reset, so it
252
- reads as the inline link it sits beside. */
253
- .ig-refresh {
254
- background: none;
255
- border: 0;
256
- color: var(--ig-accent);
257
- cursor: pointer;
258
- font: inherit;
259
- margin-left: var(--ig-space-tight);
260
- padding: 0;
734
+ .ig-unit-member + .ig-unit-member {
735
+ border-top: var(--ig-stroke) solid var(--ig-line);
261
736
  }
262
737
 
263
- .ig-refresh:hover {
264
- text-decoration: underline;
738
+ .ig-unit-member .ig-title {
739
+ font-size: var(--ig-font-size-compact);
265
740
  }
266
741
 
267
- .ig-now {
268
- list-style: none;
269
- margin: 0 0 var(--ig-space);
270
- padding: 0;
742
+ .ig-unit-member .ig-id {
743
+ font-size: var(--ig-font-size-meta);
271
744
  }
272
745
 
273
- /* The same row grammar as a slot, minus the rank track: the marker takes its
274
- place, so the title column lines up with the order beneath it. */
275
- .ig-now-row {
746
+ /* ── the footer group: holds that earn no rank slot ────────────────────── */
747
+
748
+ .ig-footer {
749
+ background: var(--ig-surface-2);
750
+ padding: var(--ig-space) var(--ig-space-wide);
751
+ }
752
+
753
+ .ig-footer-head {
276
754
  align-items: center;
277
- background: var(--ig-surface);
278
- border: var(--ig-stroke) solid var(--ig-accent);
279
- border-radius: var(--ig-radius);
280
- column-gap: var(--ig-space);
281
- display: grid;
282
- grid-template-columns: auto 1fr auto auto;
283
- min-height: var(--ig-row-height);
284
- padding: var(--ig-space-tight) var(--ig-space);
755
+ display: flex;
756
+ gap: var(--ig-space);
757
+ justify-content: space-between;
285
758
  }
286
759
 
287
- .ig-now-mark {
288
- color: var(--ig-accent);
289
- font-family: var(--ig-font-mono);
290
- font-size: var(--ig-font-size-small);
291
- text-transform: uppercase;
760
+ .ig-footer-title {
761
+ color: var(--ig-text-body);
762
+ font-size: var(--ig-font-size-compact);
763
+ margin: 0;
292
764
  }
293
765
 
294
- .ig-now-phase {
766
+ .ig-footer-labels {
295
767
  color: var(--ig-text-muted);
296
- font-size: var(--ig-font-size-small);
297
- white-space: nowrap;
768
+ font-family: var(--ig-font-mono);
769
+ font-size: var(--ig-font-size-meta);
298
770
  }
299
771
 
300
- /* A caveat or hold-label chip carries no relationship hue: it is a word, not
301
- an edge, so it keeps the line colour the plain badge has. */
302
- .ig-badge[data-caveat],
303
- .ig-badge[data-hold] {
304
- color: var(--ig-text-muted);
772
+ /* ONE LINE EACH, and the frame is explicit about why: these are not facts
773
+ about the work, so they earn no rank slot and no explanation block. A full
774
+ row here — station, badges, provenance — was claiming the opposite. */
775
+ .ig-footer .ig-list {
776
+ display: flex;
777
+ flex-direction: column;
778
+ gap: var(--ig-space-snug);
779
+ margin-top: var(--ig-space);
780
+ }
781
+
782
+ .ig-footer-row {
783
+ align-items: center;
784
+ display: flex;
785
+ gap: var(--ig-space-snug);
786
+ opacity: 0.72;
787
+ }
788
+
789
+ .ig-footer-row .ig-title {
790
+ font-size: var(--ig-font-size-compact);
791
+ }
792
+
793
+ .ig-footer-row .ig-id {
794
+ font-size: var(--ig-font-size-meta);
795
+ }
796
+
797
+ .ig-footer-row[aria-current='true'] {
798
+ opacity: 1;
305
799
  }
306
800
 
307
801
  /* ── the graph canvas ──────────────────────────────────────────────────── */
@@ -325,7 +819,26 @@ export const viewerStylesheet = `
325
819
  inline-size: var(--ig-stage-w);
326
820
  }
327
821
 
328
- /* The ranks and stations FOR the spine nodes, sitting on them. */
822
+ /* The three column headers, which say what each column is FOR. Without them
823
+ the gutters read as two more piles of issues rather than as the two answers
824
+ the spine deliberately keeps off itself. */
825
+ .ig-column-head {
826
+ color: var(--ig-text-muted);
827
+ font-family: var(--ig-font-mono);
828
+ font-size: var(--ig-font-size-pill);
829
+ letter-spacing: var(--ig-tracking-group);
830
+ position: absolute;
831
+ text-transform: uppercase;
832
+ inset-block-start: var(--ig-space-snug);
833
+ inset-inline-start: var(--ig-col-x);
834
+ }
835
+
836
+ .ig-column-head[data-column='spine'] {
837
+ color: var(--ig-accent);
838
+ }
839
+
840
+ /* The node cards and the station column, sitting on the coordinates the layout
841
+ computed. */
329
842
  .ig-rail {
330
843
  inset: 0;
331
844
  pointer-events: none;
@@ -333,16 +846,147 @@ export const viewerStylesheet = `
333
846
  }
334
847
 
335
848
  .ig-rail-row {
336
- block-size: var(--ig-row-h);
849
+ block-size: auto;
337
850
  inline-size: var(--ig-row-w);
338
851
  inset-block-start: var(--ig-row-y);
339
852
  inset-inline-start: var(--ig-row-x);
340
- margin-bottom: 0;
341
- min-block-size: 0;
853
+ min-block-size: var(--ig-row-h);
342
854
  pointer-events: auto;
343
855
  position: absolute;
344
856
  }
345
857
 
858
+ /* A CARD, NOT A LABELLED RECTANGLE. Drawing the node's text in SVG is what
859
+ forced every title through a width fit and out the other side truncated; an
860
+ HTML card wraps, so the acceptance criterion "nothing truncates" is met by
861
+ construction rather than by choosing a wider box. */
862
+ .ig-card {
863
+ background: var(--ig-surface);
864
+ border: var(--ig-stroke) solid var(--ig-line);
865
+ border-radius: var(--ig-radius-large);
866
+ display: flex;
867
+ flex-direction: column;
868
+ gap: var(--ig-space-tight);
869
+ padding: var(--ig-space) var(--ig-space);
870
+ }
871
+
872
+ .ig-card .ig-title {
873
+ font-size: var(--ig-font-size-compact);
874
+ }
875
+
876
+ .ig-card .ig-id {
877
+ font-size: var(--ig-font-size-meta);
878
+ }
879
+
880
+ .ig-card[data-column='left'] {
881
+ background: var(--ig-surface-2);
882
+ border-color: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-border), transparent);
883
+ }
884
+
885
+ /* NEVER WORKED READS AS NEVER DRAWN IN. The right gutter's two node states are
886
+ the design's own: a dashed provenance origin and a dotted duplicate, both on
887
+ no ground at all, so the eye files them as outside the order before reading
888
+ a word. */
889
+ .ig-card[data-column='right'] {
890
+ background: transparent;
891
+ border-style: dashed;
892
+ opacity: 0.8;
893
+ }
894
+
895
+ .ig-card[data-exclusion='true'] {
896
+ border-style: dotted;
897
+ border-color: color-mix(in srgb, var(--ig-edge-duplicate-of) var(--ig-tint-border), transparent);
898
+ }
899
+
900
+ .ig-card[data-held='true'] {
901
+ background: transparent;
902
+ border-style: dashed;
903
+ opacity: 0.75;
904
+ }
905
+
906
+ .ig-card[data-now='true'] {
907
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-wash), transparent);
908
+ border-color: color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
909
+ }
910
+
911
+ .ig-card[data-unit='true'] {
912
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-unit), transparent);
913
+ border-color: var(--ig-accent);
914
+ }
915
+
916
+ .ig-rail-row[aria-current='true'] .ig-card {
917
+ border-color: var(--ig-accent);
918
+ box-shadow: 0 0 0 var(--ig-band-rail) color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
919
+ }
920
+
921
+ /* The station column: a disc on the spine, carrying the rank the row would
922
+ otherwise have to be read for. */
923
+ .ig-spine-station {
924
+ align-items: center;
925
+ background: var(--ig-bg);
926
+ block-size: var(--ig-station-box);
927
+ border: calc(var(--ig-stroke) * 2) solid var(--ig-station-ready);
928
+ border-radius: 50%;
929
+ box-shadow: 0 0 0 var(--ig-station-halo) var(--ig-bg);
930
+ color: var(--ig-station-pending);
931
+ display: flex;
932
+ font-family: var(--ig-font-mono);
933
+ font-size: var(--ig-font-size-small);
934
+ font-weight: var(--ig-weight-heavy);
935
+ inline-size: var(--ig-station-box);
936
+ inset-block-start: var(--ig-station-y);
937
+ inset-inline-start: var(--ig-station-x);
938
+ justify-content: center;
939
+ pointer-events: none;
940
+ position: absolute;
941
+ }
942
+
943
+ .ig-spine-station[data-fill='filled'] {
944
+ background: var(--ig-station-ready);
945
+ color: var(--ig-bg);
946
+ }
947
+
948
+ .ig-spine-station[data-fill='hollow'] {
949
+ border-color: var(--ig-station-ready);
950
+ color: var(--ig-station-ready);
951
+ }
952
+
953
+ .ig-spine-station[data-fill='dashed'] {
954
+ border-style: dashed;
955
+ border-width: var(--ig-stroke);
956
+ border-color: var(--ig-station-held);
957
+ color: var(--ig-text-muted);
958
+ }
959
+
960
+ /* The serialize hold is gold in the frame, on the station as well as the arc:
961
+ a station that waits for a PEER is a different fact from one that waits for
962
+ a rank above it. */
963
+ /* THE RING TAKES THE HUE, THE DIGIT DOES NOT. §16b draws both in gold, and the
964
+ theme holds every edge hue to the 3:1 NON-TEXT bar — so painting the rank
965
+ figure with it makes a claim the palette does not support, on the one glyph
966
+ in the station a reader has to actually read. The hue keeps the ring, which
967
+ is the channel it was measured for; the number keeps the ink every text
968
+ contrast test already proves. */
969
+ .ig-spine-station[data-wait='serialize'] {
970
+ border-color: var(--ig-edge-serialize-with);
971
+ color: var(--ig-text);
972
+ }
973
+
974
+ .ig-spine {
975
+ stroke: var(--ig-line);
976
+ stroke-width: calc(var(--ig-stroke) * 2);
977
+ }
978
+
979
+ .ig-more {
980
+ color: var(--ig-text-muted);
981
+ font-family: var(--ig-font-mono);
982
+ font-size: var(--ig-font-size-micro);
983
+ inline-size: var(--ig-row-w);
984
+ inset-block-start: var(--ig-row-y);
985
+ inset-inline-start: var(--ig-row-x);
986
+ position: absolute;
987
+ text-align: center;
988
+ }
989
+
346
990
  .ig-node {
347
991
  fill: var(--ig-surface);
348
992
  stroke: var(--ig-line);
@@ -372,18 +1016,15 @@ export const viewerStylesheet = `
372
1016
  .ig-edge[data-edge='decomposed-from'] { stroke: var(--ig-edge-decomposed-from); }
373
1017
 
374
1018
  /* The dash pattern is set per element from the edge vocabulary, never here —
375
- one source for the channel the colour-blind-safety claim rests on. */
1019
+ one source for the channel the colour-blind-safety claim rests on. Drawn only
1020
+ in the legend now: a together unit is ONE card with its members listed inside
1021
+ it, so the canvas has no pair of boxes to surround. */
376
1022
  .ig-enclosure {
377
1023
  fill: none;
378
1024
  stroke: var(--ig-edge-together-with);
379
1025
  stroke-width: var(--ig-stroke);
380
1026
  }
381
1027
 
382
- .ig-connector {
383
- stroke: var(--ig-edge-together-with);
384
- stroke-width: var(--ig-stroke-connector);
385
- }
386
-
387
1028
  /* currentColor on the marker resolves to the inherited text colour, not to the
388
1029
  edge's stroke — so a terminal has to be given the hue explicitly or it renders
389
1030
  in body text and the fourth channel silently collapses. */
@@ -406,6 +1047,7 @@ export const viewerStylesheet = `
406
1047
  border: var(--ig-stroke) solid var(--ig-line);
407
1048
  border-radius: var(--ig-radius);
408
1049
  color: var(--ig-text-body);
1050
+ margin: var(--ig-space-wide);
409
1051
  padding: var(--ig-space);
410
1052
  }
411
1053
 
@@ -445,6 +1087,10 @@ export const viewerStylesheet = `
445
1087
  padding: 0;
446
1088
  }
447
1089
 
1090
+ .ig-tree {
1091
+ padding: var(--ig-space) var(--ig-space-wide);
1092
+ }
1093
+
448
1094
  .ig-tree .ig-list {
449
1095
  border-left: var(--ig-stroke) dashed var(--ig-edge-decomposed-from);
450
1096
  margin-left: var(--ig-space);
@@ -457,19 +1103,44 @@ export const viewerStylesheet = `
457
1103
 
458
1104
  /* ── legend ────────────────────────────────────────────────────────────── */
459
1105
 
1106
+ /* A FOOTER BAR, WHERE §16b PUTS IT, and worded rather than glyph-only. It sits
1107
+ under the drawing it explains, carries a drawn sample of each line so the
1108
+ dash and terminal channels are legible cold, and ends with the readiness key
1109
+ the station fills need. */
460
1110
  .ig-legend {
1111
+ align-items: center;
1112
+ background: var(--ig-surface-2);
461
1113
  border: 0;
1114
+ border-top: var(--ig-stroke) solid var(--ig-line);
462
1115
  display: flex;
463
1116
  flex-wrap: wrap;
464
- gap: var(--ig-space);
465
- margin: 0 0 var(--ig-space);
466
- padding: 0;
1117
+ gap: var(--ig-space-wide);
1118
+ margin: 0;
1119
+ padding: var(--ig-space) var(--ig-space-wide);
467
1120
  }
468
1121
 
469
1122
  .ig-legend-caption {
470
1123
  color: var(--ig-text-muted);
471
- font-size: var(--ig-font-size-small);
1124
+ font-family: var(--ig-font-mono);
1125
+ font-size: var(--ig-font-size-pill);
1126
+ letter-spacing: var(--ig-tracking-group);
472
1127
  padding: 0;
1128
+ text-transform: uppercase;
1129
+ }
1130
+
1131
+ .ig-legend-item {
1132
+ align-items: center;
1133
+ color: var(--ig-text-body);
1134
+ display: inline-flex;
1135
+ font-family: var(--ig-font-mono);
1136
+ font-size: var(--ig-font-size-meta);
1137
+ gap: var(--ig-space-tight);
1138
+ }
1139
+
1140
+ .ig-legend-keys {
1141
+ display: inline-flex;
1142
+ gap: var(--ig-space-loose);
1143
+ margin-inline-start: auto;
473
1144
  }
474
1145
  `;
475
1146
  //# sourceMappingURL=styles.js.map