@issuegraph/viewer 0.3.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/clusters.d.ts +116 -0
  2. package/dist/clusters.d.ts.map +1 -1
  3. package/dist/clusters.js +85 -2
  4. package/dist/clusters.js.map +1 -1
  5. package/dist/document.d.ts +228 -5
  6. package/dist/document.d.ts.map +1 -1
  7. package/dist/document.js +204 -3
  8. package/dist/document.js.map +1 -1
  9. package/dist/element.d.ts +11 -0
  10. package/dist/element.d.ts.map +1 -1
  11. package/dist/element.js.map +1 -1
  12. package/dist/index.d.ts +17 -6
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +31 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/layout.d.ts +79 -50
  17. package/dist/layout.d.ts.map +1 -1
  18. package/dist/layout.js +371 -170
  19. package/dist/layout.js.map +1 -1
  20. package/dist/marks.d.ts +105 -0
  21. package/dist/marks.d.ts.map +1 -0
  22. package/dist/marks.js +430 -0
  23. package/dist/marks.js.map +1 -0
  24. package/dist/mount.d.ts +13 -0
  25. package/dist/mount.d.ts.map +1 -1
  26. package/dist/mount.js +69 -1
  27. package/dist/mount.js.map +1 -1
  28. package/dist/navigation.d.ts +14 -0
  29. package/dist/navigation.d.ts.map +1 -1
  30. package/dist/navigation.js +51 -0
  31. package/dist/navigation.js.map +1 -1
  32. package/dist/parts.d.ts +308 -2
  33. package/dist/parts.d.ts.map +1 -1
  34. package/dist/parts.js +845 -65
  35. package/dist/parts.js.map +1 -1
  36. package/dist/projections/graph.d.ts +25 -0
  37. package/dist/projections/graph.d.ts.map +1 -1
  38. package/dist/projections/graph.js +476 -335
  39. package/dist/projections/graph.js.map +1 -1
  40. package/dist/projections/linear.d.ts +135 -0
  41. package/dist/projections/linear.d.ts.map +1 -1
  42. package/dist/projections/linear.js +266 -31
  43. package/dist/projections/linear.js.map +1 -1
  44. package/dist/projections/tree.d.ts.map +1 -1
  45. package/dist/projections/tree.js +29 -3
  46. package/dist/projections/tree.js.map +1 -1
  47. package/dist/styles.d.ts +10 -1
  48. package/dist/styles.d.ts.map +1 -1
  49. package/dist/styles.js +1275 -132
  50. package/dist/styles.js.map +1 -1
  51. package/dist/theme.d.ts +5 -6
  52. package/dist/theme.d.ts.map +1 -1
  53. package/dist/theme.js +119 -7
  54. package/dist/theme.js.map +1 -1
  55. package/dist/vocabulary.d.ts +39 -0
  56. package/dist/vocabulary.d.ts.map +1 -1
  57. package/dist/vocabulary.js +27 -0
  58. package/dist/vocabulary.js.map +1 -1
  59. package/package.json +2 -2
package/dist/styles.js CHANGED
@@ -12,14 +12,305 @@
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
  */
25
+ /**
26
+ * §17j's rail density, written once and emitted under two keys.
27
+ *
28
+ * ## Why this is a function and not a block
29
+ *
30
+ * The density has to be reachable two ways — by the container's width, which is
31
+ * §17j's own rule, and by a consumer who disagrees with the crossover this
32
+ * package picked. CSS cannot express "this condition OR that selector" across
33
+ * an `@container` boundary: a rule inside the query cannot also be a rule
34
+ * outside it, so the declarations would have to be written twice. The
35
+ * stylesheet is already a template literal, so they are written once here and
36
+ * interpolated twice instead.
37
+ *
38
+ * `scope` is a selector the whole block hangs off, and every rule below is
39
+ * RELATIVE to it. The original block carried its own `.ig-viewer` prefixes on
40
+ * about half its rules; those are gone, because the scope supplies one and two
41
+ * would never match.
42
+ *
43
+ * ## What it does to specificity, which is a gain rather than a risk
44
+ *
45
+ * Every selector here gains the scope's two compound units, so each dense rule
46
+ * beats the base rule it overrides by SPECIFICITY rather than by source order.
47
+ * One of these already had to be hand-scoped for exactly that reason — the
48
+ * together-unit row, where a container query adds no specificity and the base
49
+ * rule further down the file won, leaving the unit row drawn with nothing in
50
+ * it. That hazard is now closed for the whole block rather than at one site.
51
+ */
52
+ function railDensity(scope) {
53
+ return `
54
+ ${scope} .ig-slot {
55
+ align-items: center;
56
+ grid-template-columns: var(--ig-rank-column-dense) 1fr auto;
57
+ min-height: var(--ig-row-height-dense);
58
+ padding-block: var(--ig-row-padding-block-dense);
59
+ }
60
+
61
+ /* TWO LINES, AND NEITHER WRAPS. A wrapping title is what turns a 53px row
62
+ into a 106px one, so the truncation is not cosmetic — it is what makes the
63
+ height a property of the row rather than of its longest title. */
64
+ ${scope} .ig-slot .ig-row-head {
65
+ gap: 0;
66
+ }
67
+
68
+ ${scope} .ig-slot .ig-title {
69
+ line-height: var(--ig-row-title-line);
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ white-space: nowrap;
73
+ }
74
+
75
+ /* STEP 5 OF THE DROP ORDER, AND IT IS LAST FOR A REASON '17j' STATES:
76
+ *"A row without a title is not a row."* Everything above leaves first. */
77
+ ${scope} .ig-slot .ig-row-body {
78
+ gap: var(--ig-row-line-gap);
79
+ min-width: 0;
80
+ }
81
+
82
+ /* LINE 2, AND IT DOES NOT WRAP. '17j' draws it as one mono run —
83
+ '#512 . P0 . blocked-by #488' — so the chips sit ON the identity's line
84
+ rather than under it. 'nowrap' is what makes the 53px a property of the
85
+ ROW: one long title or one extra badge would otherwise buy a second line
86
+ and double the height, which is exactly how this rail reached 408px. */
87
+ ${scope} .ig-slot .ig-row-meta {
88
+ align-items: center;
89
+ display: flex;
90
+ flex-wrap: nowrap;
91
+ gap: var(--ig-space-tight);
92
+ line-height: var(--ig-row-meta-line);
93
+ min-width: 0;
94
+ overflow: hidden;
95
+ }
96
+
97
+ /* The chips keep their own row semantics and stop wrapping, so what does not
98
+ fit is clipped rather than stacked. The drop order above decides WHICH
99
+ chips are there to clip; this only decides they stay on the line. */
100
+ ${scope} .ig-slot .ig-row-meta .ig-badges {
101
+ flex-wrap: nowrap;
102
+ min-width: 0;
103
+ }
104
+
105
+ /* The issue number never shrinks away — '17j' lists it among the five things
106
+ never dropped at any width. The title truncates first and the chips clip;
107
+ the number holds its ground. */
108
+ ${scope} .ig-slot .ig-row-meta .ig-id {
109
+ flex: none;
110
+ }
111
+
112
+ /* AT RAIL DENSITY LINE 2 IS TEXT, NOT PILLS — and this is read off the frame
113
+ rather than inferred from the height. '17j' draws the meta line as a single
114
+ mono run, '#512 . P0 . blocked-by #488': no outlines, no fills, just the
115
+ glyph and the words. §16a's wider panel is where they are chips.
116
+
117
+ IT IS ALSO WHAT MAKES 53px REACHABLE. A bordered chip is its padding plus
118
+ its stroke plus its line box, so a line of them cannot be 15px however the
119
+ line-height is set — the flex row grows to its tallest item and the row
120
+ followed it to 74px. Removing the chrome removes the reason.
121
+
122
+ THE FOUR REDUNDANT CHANNELS SURVIVE THIS, which is the thing worth
123
+ checking before doing it: dash, terminal and glyph are untouched, and hue
124
+ moves from the border to the text it was outlining. '17j' names the hold
125
+ GLYPH among the five things never dropped, and it is still drawn. */
126
+ ${scope} .ig-slot .ig-row-meta .ig-badge {
127
+ background: none;
128
+ border: 0;
129
+ border-radius: 0;
130
+ padding: 0;
131
+ /* AND THE TEXT INSIDE A CHIP DOES NOT WRAP EITHER. Stopping the ROW from
132
+ wrapping is not enough: a chip squeezed by its neighbours wraps its own
133
+ label instead, and 'signals disagree' at 84px became two 15px lines and
134
+ took the row to 67px while every sibling sat at 53. One row in seven,
135
+ which is exactly the kind of thing that survives a review and does not
136
+ survive a measurement. What does not fit is clipped, per the drop
137
+ order. */
138
+ white-space: nowrap;
139
+ }
140
+
141
+ /* AND THE SWAP, AT RAIL DENSITY. '17j' draws a together unit here as a
142
+ marker inline with an ordinary title at the ordinary 53px, so the head
143
+ comes back and the enclosure stands down. One row, one rank, in both
144
+ densities - RULINGS.md section 1 - drawn the way each width can afford. */
145
+ /* SCOPED WITH '.ig-viewer' SO ORDER CANNOT DECIDE THIS. A container query
146
+ adds no specificity, so this rule and the wide-density one it overrides
147
+ were an even 0,3,0 and the LATER of the two won — which put the base rule,
148
+ written further down the file, in charge of both densities and left the
149
+ unit row showing nothing but its em dash. Measured, not spotted: every row
150
+ was 53px and one of them was empty. The extra class makes the override win
151
+ on specificity, where it does not depend on where anyone adds a rule
152
+ later. */
153
+ ${scope} .ig-slot[data-unit='true'] > .ig-row-body > .ig-row-head {
154
+ display: flex;
155
+ }
156
+
157
+ ${scope} .ig-slot[data-unit='true'] .ig-unit-mark,
158
+ ${scope} .ig-slot[data-unit='true'] .ig-unit {
159
+ display: none;
160
+ }
161
+
162
+ /* The marker itself: the glyph and the count, on the title's line. It is a
163
+ COUNT and not a pill here because 53px has no room for one, and because
164
+ the pill's words are already the accessible name's. */
165
+ ${scope} .ig-unit-count {
166
+ color: var(--ig-edge-together-with);
167
+ display: inline;
168
+ font-family: var(--ig-font-mono);
169
+ margin-right: var(--ig-space-tight);
170
+ }
171
+
172
+ /* THE FOOTER ROWS TOO, AND THIS IS THE POPULATION THE ROW FIX MISSED.
173
+ A footer entry is deliberately ONE LINE and shorter than a ranked row -
174
+ 16a gives it no rank, no station and no explanation block, because it is
175
+ not a fact about the work. That part was right. What was not governed is
176
+ its TITLE: it wrapped, and the excluded row carrying a canonical reference
177
+ ran to 73px while its siblings sat at 36. A footer row taller than a
178
+ ranked row inverts the whole point of the group.
179
+
180
+ THE DEFECT IS THE ONE ALREADY FIXED ON .ig-slot, ONE ROW KIND OVER: a
181
+ title with nothing stopping it wrapping takes the row with it. Fixed at
182
+ the class this time rather than at the site - the rail renders three row
183
+ kinds and the first pass governed two. */
184
+ ${scope} .ig-footer-row .ig-title {
185
+ line-height: var(--ig-row-title-line);
186
+ min-width: 0;
187
+ overflow: hidden;
188
+ text-overflow: ellipsis;
189
+ white-space: nowrap;
190
+ }
191
+
192
+ ${scope} .ig-footer-row .ig-badges {
193
+ flex-wrap: nowrap;
194
+ min-width: 0;
195
+ overflow: hidden;
196
+ }
197
+
198
+ /* AND THE CHIPS LOSE THEIR CHROME HERE TOO, which is what actually makes a
199
+ footer entry ONE LINE. 16a draws it as "a label chip, a title and an
200
+ identity, on one line"; a bordered chip is padding plus stroke plus line
201
+ box, so a row carrying any relationship badge stood at 36px against 22px
202
+ for one carrying none - and the badge block, not the title, was the last
203
+ thing driving it.
204
+
205
+ MEASURED IN TWO PASSES, WHICH IS THE POINT. Stopping after the title fix
206
+ left the footer group at two heights and looked finished: the row that had
207
+ been 73px was down to 36 and matched its siblings, so the obvious check
208
+ passed. It was the SECOND measurement - why 22 and why 36 - that found the
209
+ badges. A class is not fixed until nothing in it varies for a reason you
210
+ have not named. */
211
+ ${scope} .ig-footer-row .ig-badge {
212
+ background: none;
213
+ border: 0;
214
+ border-radius: 0;
215
+ padding: 0;
216
+ white-space: nowrap;
217
+ }
218
+
219
+ /* THE THIRD AND LAST MEMBER OF THE CLASS: the identity. An excluded row
220
+ carries TWO - its own, and the canonical it defers to ('455 -> 512') - and
221
+ the second wrapped, which is why that row alone stood at 30px when the
222
+ others reached 17. Same defect as the title and the chips, third element,
223
+ found by asking the same question a third time rather than by stopping at
224
+ the first uniform-looking answer. */
225
+ ${scope} .ig-footer-row .ig-id {
226
+ white-space: nowrap;
227
+ }
228
+
229
+ /* THE NOW STRIP AT RAIL DENSITY. '17j' draws it as the first row of the rail
230
+ at all three backlog sizes, in the same rhythm as the rows beneath it -
231
+ RULINGS.md section 4: "a single now strip as the first row of the rail,
232
+ above rank 1, on a raised surface, carrying no rank number".
233
+
234
+ THE RANK SLOT IS ALREADY RIGHT AND IS NOT TOUCHED HERE: the strip carries a
235
+ 'now' mark where a rank would be, never a number, which is the half of
236
+ section 4 the build already had. What was wrong was only its height. */
237
+ ${scope} .ig-now-row {
238
+ min-height: var(--ig-row-height-dense);
239
+ padding-block: var(--ig-row-padding-block-dense);
240
+ }
241
+
242
+ ${scope} .ig-now-row .ig-row-head {
243
+ gap: 0;
244
+ min-width: 0;
245
+ }
246
+
247
+ ${scope} .ig-now-row .ig-title {
248
+ line-height: var(--ig-row-title-line);
249
+ overflow: hidden;
250
+ text-overflow: ellipsis;
251
+ white-space: nowrap;
252
+ }
253
+
254
+ ${scope} .ig-now-row .ig-id {
255
+ line-height: var(--ig-row-meta-line);
256
+ overflow: hidden;
257
+ text-overflow: ellipsis;
258
+ white-space: nowrap;
259
+ }
260
+
261
+ /* HIDDEN HERE, AND NOT YET EXPANDABLE — WHICH IS HALF OF WHAT 17j ASKS FOR.
262
+ The tile says: "The provenance line never renders inline here. In the rail
263
+ it is expand-on-demand; in 16a's wider panel it is inline. Same row, two
264
+ densities."
265
+
266
+ THE INLINE HALF IS DONE AND THE ON-DEMAND HALF IS NOT. There is no
267
+ row-level expand affordance at this density — 16f's right-arrow "expand
268
+ provenance" does not exist — so a rail reader reaches the explanation
269
+ through the inspector's WHY RANK block rather than through the row. The
270
+ information is one selection away, not lost, but this comment previously
271
+ claimed "expand-on-demand" and that was an overclaim about a control
272
+ nothing draws.
273
+
274
+ The markup stays whole so the wider container still renders it inline and
275
+ so the accessible name is unchanged; what the rail declines is spending
276
+ 53px of every row on it. The missing affordance is filed, not forgotten. */
277
+ ${scope} .ig-slot .ig-provenance,
278
+ ${scope} .ig-slot .ig-hold,
279
+ ${scope} .ig-slot .ig-caveat {
280
+ display: none;
281
+ }
282
+
283
+ /* AND THE ON-DEMAND HALF, WHICH IS WHAT 17j ACTUALLY ASKED FOR. A row the
284
+ reader opened with 16f's right-arrow draws its provenance again.
285
+
286
+ THE 53px IS NOT BROKEN BY THIS, it is spent deliberately: an open row is
287
+ taller because the reader asked it to be, and closing it returns the row to
288
+ the rhythm. That is the difference between a row that grows because nothing
289
+ governs it - the defect this sweep removed - and one that grows because
290
+ somebody pressed a key.
291
+
292
+ ONLY THE PROVENANCE COMES BACK. Holds and caveats stay down: 17j names the
293
+ provenance line and nothing else, and the holds are already in the
294
+ inspector's WHY RANK block as controls rather than prose. */
295
+ ${scope} .ig-slot[aria-expanded='true'] {
296
+ min-height: var(--ig-row-height-dense);
297
+ }
298
+
299
+ ${scope} .ig-slot[aria-expanded='true'] .ig-provenance {
300
+ display: block;
301
+ }
302
+ `;
303
+ }
16
304
  export const viewerStylesheet = `
17
305
  .ig-viewer {
18
306
  background: var(--ig-bg);
307
+ border: var(--ig-stroke) solid var(--ig-line);
308
+ border-radius: var(--ig-radius-large);
19
309
  color: var(--ig-text-body);
20
310
  font-family: var(--ig-font-ui);
21
311
  font-size: var(--ig-font-size);
22
312
  line-height: var(--ig-line-height);
313
+ overflow: hidden;
23
314
  position: relative;
24
315
  }
25
316
 
@@ -31,37 +322,515 @@ export const viewerStylesheet = `
31
322
 
32
323
  .ig-viewer :focus-visible {
33
324
  outline: var(--ig-focus-ring) solid var(--ig-focus);
34
- outline-offset: var(--ig-space-tight);
325
+ outline-offset: calc(var(--ig-focus-ring) * -1);
35
326
  }
36
327
 
328
+ /* THE ROW'S DENSITY IS READ FROM THIS BOX, AND THAT IS §17j's CLOSING RULE
329
+ RATHER THAN A PREFERENCE. The same row renders at 390px in the workspace
330
+ rail and at 330px in §18's settings rail, so it *"must take its width from
331
+ its container and pick its drop step from that — not from a viewport media
332
+ query, and not from a prop the host sets. A package that reads the window
333
+ cannot be dropped into someone else's settings page, which is the whole BYO
334
+ premise."*
335
+
336
+ A viewport query would have been the obvious reach and it is precisely
337
+ wrong: it answers a question about the WINDOW when the row's question is
338
+ about the COLUMN it was given. Two rails of different widths on one screen
339
+ is not an edge case here — §17a's rail and §18's settings rail are that
340
+ screen.
341
+
342
+ "NOT FROM A PROP THE HOST SETS" IS ABOUT THE DEFAULT, AND data-density DOES
343
+ NOT BREAK IT. What §17j refuses is a package that CANNOT work out its own
344
+ density — one that draws a wrong row until a host wires something up, which
345
+ is how a drop-in stops being a drop-in. This box still answers that question
346
+ on its own and nothing has to be passed for it to be right. What the
347
+ attribute adds is the ability to DISAGREE, which is a different thing: the
348
+ crossover this package picked is a default, the host is the only party that
349
+ knows about print, a user preference or its own breakpoints, and an override
350
+ nobody uses costs the drop-in case nothing. See the crossover comment below
351
+ for why the number needs no ruling once it is a default. */
37
352
  .ig-list {
353
+ container-name: ig-rail;
354
+ container-type: inline-size;
38
355
  list-style: none;
39
356
  margin: 0;
40
357
  padding: 0;
41
358
  }
42
359
 
43
- /* ── the spine ─────────────────────────────────────────────────────────── */
360
+ /* ── the panel header ──────────────────────────────────────────────────── */
44
361
 
45
- .ig-slot {
362
+ /* Present only when the host stated a fact; a document with none draws no
363
+ header at all, which is what keeps the pure-graph markup unchanged. */
364
+ .ig-header {
365
+ background: var(--ig-surface-2);
366
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
367
+ display: flex;
368
+ flex-direction: column;
369
+ gap: var(--ig-space);
370
+ margin: 0;
371
+ padding: var(--ig-space-loose) var(--ig-space-wide);
372
+ }
373
+
374
+ .ig-header-lead {
46
375
  align-items: center;
47
- background: var(--ig-surface);
376
+ display: inline-flex;
377
+ gap: var(--ig-space-snug);
378
+ }
379
+
380
+ /* The segmented projection toggle §16a and §16b both draw. One rounded outline
381
+ around two buttons, the pressed one filled with the accent. */
382
+ .ig-toggle {
48
383
  border: var(--ig-stroke) solid var(--ig-line);
49
384
  border-radius: var(--ig-radius);
385
+ display: inline-flex;
386
+ overflow: hidden;
387
+ }
388
+
389
+ .ig-toggle-option {
390
+ align-items: center;
391
+ background: none;
392
+ border: 0;
393
+ color: var(--ig-text-body);
394
+ cursor: pointer;
395
+ display: inline-flex;
396
+ font: inherit;
397
+ font-size: var(--ig-font-size-small);
398
+ gap: var(--ig-space-tight);
399
+ padding: var(--ig-space-micro) var(--ig-space-snug);
400
+ }
401
+
402
+ .ig-toggle-option[aria-pressed='true'] {
403
+ background: var(--ig-accent);
404
+ color: var(--ig-bg);
405
+ font-weight: var(--ig-weight-strong);
406
+ }
407
+
408
+ .ig-header-top {
409
+ align-items: center;
410
+ display: flex;
411
+ flex-wrap: wrap;
412
+ gap: var(--ig-space);
413
+ justify-content: space-between;
414
+ }
415
+
416
+ /* The panel's own name, at the tracking the frame gives a frame-level label. */
417
+ .ig-header-label {
418
+ color: var(--ig-accent);
419
+ font-family: var(--ig-font-mono);
420
+ font-size: var(--ig-font-size-small);
421
+ letter-spacing: var(--ig-tracking-label);
422
+ text-transform: uppercase;
423
+ }
424
+
425
+ /* THE COUNTS ARE CHIPS, NOT A SENTENCE. The frame gives each tally its own
426
+ outline and tints only the one a reader acts on — "how many could run right
427
+ now, against the cap" — so the accent is spent once instead of on a whole
428
+ line of muted prose. */
429
+ .ig-counts {
430
+ align-items: center;
431
+ display: flex;
432
+ flex-wrap: wrap;
433
+ gap: var(--ig-space-snug);
434
+ margin: 0;
435
+ }
436
+
437
+ .ig-count-chip {
438
+ background: var(--ig-bg);
439
+ border: var(--ig-stroke) solid var(--ig-line);
440
+ border-radius: var(--ig-radius);
441
+ color: var(--ig-text-body);
442
+ font-family: var(--ig-font-mono);
443
+ font-size: var(--ig-font-size-small);
444
+ font-variant-numeric: tabular-nums;
445
+ padding: var(--ig-space-micro) var(--ig-space-snug);
446
+ }
447
+
448
+ .ig-count-chip[data-count='ready'] {
449
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-fill), transparent);
450
+ border-color: color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
451
+ color: var(--ig-text);
452
+ }
453
+
454
+ .ig-freshness {
455
+ align-items: center;
456
+ color: var(--ig-text-muted);
457
+ display: inline-flex;
458
+ font-family: var(--ig-font-mono);
459
+ font-size: var(--ig-font-size-small);
460
+ font-variant-numeric: tabular-nums;
461
+ gap: var(--ig-space-tight);
462
+ margin: 0;
463
+ }
464
+
465
+ /* STALE GOES GOLD, as the design draws it. The gold this palette already holds
466
+ is the conflict state's — "the document moved upstream" — and a stale mirror
467
+ is that fact about the whole document, so the state hue is reused rather
468
+ than a token added for one word. No new token: the theme's token list is
469
+ the contract every host theme has to fill. */
470
+ .ig-freshness[data-stale='true'] {
471
+ color: var(--ig-state-conflict);
472
+ }
473
+
474
+ /* A control the viewer publishes and does not wire: the UA button reset, so it
475
+ reads as the inline control the frame draws beside the stamp. */
476
+ .ig-refresh,
477
+ .ig-size {
478
+ background: none;
479
+ border: var(--ig-stroke) solid var(--ig-line);
480
+ border-radius: var(--ig-radius);
481
+ color: var(--ig-text);
482
+ cursor: pointer;
483
+ font: inherit;
484
+ padding: var(--ig-space-micro) var(--ig-space-snug);
485
+ }
486
+
487
+ .ig-refresh:hover,
488
+ .ig-size:hover {
489
+ border-color: var(--ig-accent);
490
+ color: var(--ig-accent);
491
+ }
492
+
493
+ /* ── the panel-level notice ────────────────────────────────────────────── */
494
+
495
+ /* WHAT THE HOST SAYS IS TRUE OF THE WHOLE PANEL, drawn between the NOW row and
496
+ the order it qualifies. Its own surface, so it reads as a statement about the
497
+ panel rather than as a row that lost its rank. */
498
+ .ig-notice {
499
+ align-items: flex-start;
500
+ background: var(--ig-surface-2);
501
+ border: var(--ig-stroke) solid var(--ig-line);
502
+ border-radius: var(--ig-radius);
503
+ display: flex;
504
+ gap: var(--ig-space-snug);
505
+ justify-content: space-between;
506
+ padding: var(--ig-space-snug) var(--ig-space-loose);
507
+ }
508
+
509
+ .ig-notice-text {
510
+ display: flex;
511
+ flex-direction: column;
512
+ gap: var(--ig-space-micro);
513
+ }
514
+
515
+ .ig-notice-head {
516
+ align-items: center;
517
+ color: var(--ig-text);
518
+ display: flex;
519
+ gap: var(--ig-space-tight);
520
+ margin: 0;
521
+ }
522
+
523
+ .ig-notice-body {
524
+ color: var(--ig-text-body);
525
+ margin: 0;
526
+ }
527
+
528
+ /* A COUNT, SO IT SITS ON THE FIGURE FACE. A progress figure is read as a number,
529
+ and the proportional face makes its digits wander the way a rank column's do. */
530
+ .ig-notice-progress {
531
+ color: var(--ig-text-muted);
532
+ font-family: var(--ig-font-mono);
533
+ font-size: var(--ig-font-size-small);
534
+ font-variant-numeric: tabular-nums;
535
+ margin: 0;
536
+ }
537
+
538
+ /* THE SAME CONTROL RESET THE REFRESH BUTTON TAKES, and for the same reason: it
539
+ is published and not wired, so it must look like the inline control the frame
540
+ draws rather than like a UA button. The anchor arm gets it too — a host that
541
+ routes instead of acting should not get a different-looking affordance. */
542
+ .ig-notice-action {
543
+ background: none;
544
+ border: var(--ig-stroke) solid var(--ig-line);
545
+ border-radius: var(--ig-radius);
546
+ color: var(--ig-text);
547
+ cursor: pointer;
548
+ font: inherit;
549
+ padding: var(--ig-space-micro) var(--ig-space-snug);
550
+ text-decoration: none;
551
+ white-space: nowrap;
552
+ }
553
+
554
+ .ig-notice-action:hover {
555
+ border-color: var(--ig-accent);
556
+ color: var(--ig-accent);
557
+ }
558
+
559
+ /* GOLD ON FIRST IMPORT, and gold is the conflict state's hue — the same reuse
560
+ the stale stamp already makes, for the same reason: the token list is the
561
+ contract every host theme has to fill, so a token added for one state is a
562
+ theme every host has to revisit.
563
+
564
+ AND NOTHING FOR THE ERROR ARM. The design makes exactly one panel-level outline
565
+ claim, and it is this one; an error is drawn calm on purpose, because its own
566
+ sentence is that settings are safe and the pipeline is still running. A
567
+ failure-hued outline around the whole panel would say the opposite. */
568
+ .ig-viewer[data-ig-condition='importing'] {
569
+ outline: var(--ig-stroke) solid var(--ig-state-conflict);
570
+ outline-offset: var(--ig-space-micro);
571
+ }
572
+
573
+ .ig-notice[data-ig-condition='importing'] .ig-notice-head {
574
+ color: var(--ig-state-conflict);
575
+ }
576
+
577
+ .ig-notice[data-ig-condition='error'] .ig-notice-head {
578
+ color: var(--ig-state-failed);
579
+ }
580
+
581
+ /* ── the adoption line ─────────────────────────────────────────────────── */
582
+
583
+ /* ONE QUIET LINE, which is the whole of what the design asks for here: the
584
+ day-one panel has to read complete and calm, and a block explaining what the
585
+ reader is missing is the opposite of calm. */
586
+ .ig-adoption {
587
+ align-items: center;
588
+ color: var(--ig-text-muted);
589
+ display: flex;
590
+ font-size: var(--ig-font-size-small);
591
+ gap: var(--ig-space-tight);
592
+ margin: 0;
593
+ }
594
+
595
+ /* THE SENTENCE IS NOT THE LINK. Underlining the whole line made the quietest
596
+ element on the panel its loudest; the link is a short label beside it. */
597
+ .ig-adoption-text {
598
+ color: var(--ig-text-muted);
599
+ }
600
+
601
+ .ig-adoption-link {
602
+ color: var(--ig-text-muted);
603
+ margin-left: auto;
604
+ text-decoration: underline;
605
+ white-space: nowrap;
606
+ }
607
+
608
+ .ig-adoption-link:hover {
609
+ color: var(--ig-accent);
610
+ }
611
+
612
+ /* PUBLISHED, NOT PERFORMED. The viewer never removes its own line; the host
613
+ re-renders without the note. */
614
+ .ig-adoption-dismiss {
615
+ background: none;
616
+ border: 0;
617
+ color: var(--ig-text-muted);
618
+ cursor: pointer;
619
+ font: inherit;
620
+ padding: 0;
621
+ text-decoration: underline;
622
+ }
623
+
624
+ .ig-adoption-dismiss:hover {
625
+ color: var(--ig-accent);
626
+ }
627
+
628
+ /* ── working now ───────────────────────────────────────────────────────── */
629
+
630
+ .ig-now {
631
+ /* THE NOW STRIP IS PART OF THE RAIL'S RHYTHM, so it reads the same container
632
+ the rows do. It is its own list element, so without this the density rules
633
+ scoped to 'ig-rail' simply did not reach it and the strip stood 83px tall
634
+ beside 53px rows - 57% taller than everything under it, in the one slot
635
+ 17j gives the top of the rail. */
636
+ container-name: ig-rail;
637
+ container-type: inline-size;
638
+ list-style: none;
639
+ margin: 0;
640
+ padding: 0;
641
+ }
642
+
643
+ /* A BAND, NOT A CARD. The frame runs it full-bleed across the panel with a
644
+ wash and an accent rail down its leading edge — the same treatment the shell
645
+ gives anything in flight — so it reads as a state of the panel rather than
646
+ as the first row of the order. */
647
+ .ig-now-row {
648
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-wash), transparent);
649
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
650
+ box-shadow: inset var(--ig-band-rail) 0 0 var(--ig-accent);
50
651
  column-gap: var(--ig-space);
51
652
  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);
653
+ grid-template-columns: auto 1fr auto;
654
+ align-items: center;
655
+ padding: var(--ig-space) var(--ig-space-wide);
656
+ }
657
+
658
+ .ig-now-mark {
659
+ background: var(--ig-accent);
660
+ border-radius: var(--ig-radius-small);
661
+ color: var(--ig-bg);
662
+ font-family: var(--ig-font-mono);
663
+ font-size: var(--ig-font-size-pill);
664
+ font-weight: var(--ig-weight-strong);
665
+ letter-spacing: var(--ig-tracking-pill);
666
+ padding: var(--ig-space-micro) var(--ig-space-tight);
667
+ text-transform: uppercase;
668
+ }
669
+
670
+ .ig-now-phase {
671
+ align-items: center;
672
+ color: var(--ig-accent);
673
+ display: inline-flex;
674
+ font-family: var(--ig-font-mono);
675
+ font-size: var(--ig-font-size-small);
676
+ gap: var(--ig-space-tight);
677
+ white-space: nowrap;
678
+ }
679
+
680
+ /* The pulse is the frame's, and it is the one moving thing in the panel. */
681
+ .ig-now-pulse {
682
+ background: var(--ig-accent);
683
+ block-size: var(--ig-space-tight);
684
+ border-radius: 50%;
685
+ display: inline-block;
686
+ inline-size: var(--ig-space-tight);
687
+ }
688
+
689
+ /* ── the order ─────────────────────────────────────────────────────────── */
690
+
691
+ /* TWO COLUMNS, WHICH IS THE FRAME'S GRID EXACTLY. The rank track holds the
692
+ figure and the readiness dot stacked, and everything else is one body
693
+ column, so a row has exactly two children and nothing can auto-place into
694
+ the rank track and set its width. That was a real defect: a single
695
+ relationship badge left unplaced widened the rank column for the whole list
696
+ and squeezed every title to pay for it. */
697
+ .ig-slot {
698
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
699
+ column-gap: var(--ig-space);
700
+ display: grid;
701
+ grid-template-columns: var(--ig-rank-column) 1fr;
702
+ min-height: var(--ig-row-min-height);
703
+ padding: var(--ig-row-padding-block) var(--ig-space-wide);
704
+ }
705
+
706
+ /* ── §17j's RAIL DENSITY ───────────────────────────────────────────────────
707
+
708
+ THE 53px ROW, AND THE ORDER THINGS LEAVE IT. '17j' fixes both, and the
709
+ reason it fixes the second is that the build *"showed everything because
710
+ nothing told it what to give up"*. This is that instruction.
711
+
712
+ 9 pad + 17 title + 3 gap + 15 meta + 9 pad = 53px, divider inclusive
713
+ tracks: 30px rank · 1fr issue · auto delta
714
+
715
+ THE FINDING THIS ENCODES, which is the one to keep: at 6, 60 and 312 issues
716
+ the rail is IDENTICAL. What changes is around it — virtualisation appears,
717
+ the runner-hold footer grows, filter chips stop being optional, the canvas
718
+ starts refusing. *"If you are making the rail denser as the backlog grows,
719
+ you are compressing the surface that was never the problem."* So nothing
720
+ here keys off the backlog size, and nothing ever should.
721
+
722
+ 430 IS THIS PACKAGE'S DEFAULT, NOT A CLAIM ABOUT THE DESIGN, AND A CONSUMER
723
+ CAN TAKE IT OVER. That is the whole posture of this package — sensible
724
+ defaults, everything overridable — and it is why this number needed no
725
+ ruling in the end. '17j' gives two anchors and no crossover: the workspace
726
+ rail is 390, §18's settings rail is 330 where steps 1-3 apply "by default",
727
+ and §16a's panel, which keeps provenance inline, is wider than either and
728
+ carries no published figure anywhere. 430 sits above the rail and below that
729
+ panel. A host that does nothing gets it and gets correct behaviour; a host
730
+ that disagrees sets data-density on the viewer root and this condition stops
731
+ deciding for them.
732
+
733
+ SO THE OPEN QUESTION IS CLOSED RATHER THAN WAITING. An earlier revision of
734
+ this comment filed the distance between 390 and 430 as needing a width for
735
+ §16a's panel. It does not: a DEFAULT only has to be reasonable, and the
736
+ figure would only have mattered if this were the single crossover every
737
+ consumer had to live with. It is not, so it is not blocking.
738
+
739
+ IT CANNOT BE A TOKEN, AND THE NEXT PERSON WILL TRY. Making the threshold a
740
+ theme property is the obvious reach, and it does not work: a container
741
+ condition takes a length and only a length. Measured in a browser — a
742
+ max-width condition written as a literal applies, and the same condition
743
+ naming a custom property PARSES INTO A RULE THAT MATCHES AT NO WIDTH AT ALL,
744
+ while CSS.supports() reports that same condition as supported. It fails
745
+ silently in both directions: the rule is not dropped, it is simply never
746
+ satisfied, and the one API that could have warned agrees it is fine. That is
747
+ a CSS limitation rather than a decision, and it is why the escape hatch below
748
+ is a selector rather than a property.
749
+
750
+ '17k' MAKES THE LOWER ANCHOR EXACT, which is worth keeping. It fixes the
751
+ three workspace layouts under one rule — "the rail never yields" — so the
752
+ workspace draws its rail at 390 at every width it supports and no amount of
753
+ resizing moves it across this threshold. Before that it fell to 312 below
754
+ 1360, so 430 had to clear two numbers and only one was '17j's. The editor's
755
+ sheet pins that relationship rather than restating it: see its test "keeps
756
+ the workspace rail on the dense side of layer 1's crossover". */
757
+ @container ig-rail (max-width: 430px) {
758
+ ${railDensity('.ig-viewer:not([data-density])')}
759
+ }
760
+
761
+ /* THE ESCAPE HATCH ITSELF, AND IT IS THE SAME BLOCK WITH A DIFFERENT KEY.
762
+ Emitted a second time under an attribute that carries no width at all, so
763
+ a consumer who sets it gets the dense row wherever they want it. The
764
+ scopes above and below are mutually exclusive by construction — one is
765
+ :not([data-density]) and the other names a value — so nothing here
766
+ depends on which of them a browser reads first.
767
+
768
+ THE OTHER DIRECTION NEEDS NO RULES. data-density='wide' matches neither
769
+ scope, so the row falls back to the base density this sheet declares
770
+ above, at any container width. */
771
+ ${railDensity(".ig-viewer[data-density='dense']")}
772
+
773
+ /* STEPS 1-3 OF THE DROP ORDER, AT §18's 330px SETTINGS RAIL, which '17j'
774
+ names as the width where they apply by default.
775
+
776
+ THE SELECTORS ARE THE MARKUP'S OWN ATTRIBUTES, not class guesses: evidence
777
+ is 'data-evidence', a relationship is 'data-edge', a priority is
778
+ 'data-priority' with 'query' / 'tier' / 'promoted'. Three of these were
779
+ written from memory first and two were wrong — the parts publish
780
+ 'data-evidence="verified"', never a 'data-ig-badge'. */
781
+
782
+ /* STEP 1 — evidence and verification chips. '17j': *"Reassurance, not an
783
+ answer to any question the rail is asked."* */
784
+ @container ig-rail (max-width: 360px) {
785
+ .ig-slot .ig-badge[data-evidence] {
786
+ display: none;
787
+ }
56
788
  }
57
789
 
790
+ /* STEP 2 — relationship badges past the FIRST, which is kept because it is
791
+ the one causing the hold. Scoped to 'data-edge' so it counts relationships
792
+ and not the priority, evidence and readiness chips that share '.ig-badge';
793
+ a positional 'nth-of-type' would have hidden whichever chip happened to sit
794
+ second, which is not what the rule says at all. The '+n more' chip carries
795
+ what left, and it already exists — 'data-omitted'. */
796
+ @container ig-rail (max-width: 345px) {
797
+ .ig-slot .ig-badge[data-edge] ~ .ig-badge[data-edge] {
798
+ display: none;
799
+ }
800
+ }
801
+
802
+ /* STEP 3 — the priority token. A DECLARED TIER goes, because '17j' says it is
803
+ *"recoverable from the rank itself"*. A PROMOTION stays: 'P3 → 0' is the
804
+ one form the rank cannot give back, and it is rank provenance rather than a
805
+ restatement. A matched query stays for the same reason — it names WHICH
806
+ query, which no rank encodes. */
807
+ @container ig-rail (max-width: 335px) {
808
+ .ig-slot .ig-badge[data-priority='tier'] {
809
+ display: none;
810
+ }
811
+ }
812
+
813
+ /* THE ONE ROW GROUND THE FRAME DRAWS, and it is not a stripe. A together unit
814
+ is the compound station, and the tint says so; the other ranked rows carry
815
+ no background at all. */
816
+ .ig-slot[data-unit='true'] {
817
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-unit), transparent);
818
+ }
819
+
820
+ /* A HELD ROW IS HATCHED, NOT DASHED. It keeps its position in the sequence —
821
+ "why isn't my P1 running" is answerable in place — so it must read as
822
+ unavailable without leaving the grid the rows above and below hang off. A
823
+ dashed border around one row breaks that alignment; a ground does not. */
58
824
  .ig-slot[data-held='true'] {
59
- background: var(--ig-surface-2);
60
- border-style: dashed;
825
+ background-image: repeating-linear-gradient(
826
+ 135deg,
827
+ color-mix(in srgb, var(--ig-text) var(--ig-tint-unit), transparent) 0 var(--ig-space-tight),
828
+ transparent var(--ig-space-tight) calc(var(--ig-space-tight) * 2)
829
+ );
61
830
  }
62
831
 
63
832
  .ig-slot[aria-current='true'] {
64
- border-color: var(--ig-accent);
833
+ box-shadow: inset var(--ig-band-rail) 0 0 var(--ig-accent);
65
834
  }
66
835
 
67
836
  /* A CANVAS-OWNED NODE IS SELECTABLE TOO, and only the rail rows had a selected
@@ -75,16 +844,67 @@ export const viewerStylesheet = `
75
844
  stroke-width: calc(var(--ig-stroke) * 2);
76
845
  }
77
846
 
847
+ /* The rank track: figure over dot, centred, so the title column starts at one
848
+ x on every row whether or not the row has a number to print. */
849
+ .ig-rank-cell {
850
+ align-items: center;
851
+ display: flex;
852
+ flex-direction: column;
853
+ gap: var(--ig-space-tight);
854
+ }
855
+
78
856
  .ig-rank {
79
857
  color: var(--ig-text);
80
858
  font-family: var(--ig-font-mono);
859
+ font-size: var(--ig-font-size-rank);
81
860
  font-variant-numeric: tabular-nums;
82
- min-width: calc(var(--ig-char-width) * 4);
83
- text-align: right;
861
+ font-weight: var(--ig-weight-strong);
862
+ line-height: 1;
84
863
  }
85
864
 
86
865
  .ig-rank[data-held='true'] {
87
866
  color: var(--ig-text-muted);
867
+ font-weight: var(--ig-weight-regular);
868
+ }
869
+
870
+ .ig-row-body {
871
+ display: flex;
872
+ flex-direction: column;
873
+ gap: var(--ig-space-snug);
874
+ min-width: 0;
875
+ }
876
+
877
+ /* The title, the identity and the two-line pair they form: 2px apart, which is
878
+ what makes them read as one object rather than as two rows of metadata. */
879
+ .ig-row-head {
880
+ display: flex;
881
+ flex-direction: column;
882
+ gap: var(--ig-space-micro);
883
+ min-width: 0;
884
+ }
885
+
886
+ /* A UNIT AT THE WIDE DENSITY IS ITS ENCLOSURE, which is what §16a draws: the
887
+ pill, the note, and a member list. The ordinary head the row also carries is
888
+ the RAIL's way of drawing the same unit, so it stands down here rather than
889
+ printing the lead's title above a box that already opens with it. */
890
+ .ig-slot[data-unit='true'] > .ig-row-body > .ig-row-head {
891
+ display: none;
892
+ }
893
+
894
+ .ig-unit-count {
895
+ display: none;
896
+ }
897
+
898
+ /* LINE 2 AT THE WIDE DENSITY. §16a's panel draws the same sequence — identity
899
+ then chips — and simply has room, so here it WRAPS rather than clipping and
900
+ the provenance turnstile stays inline beneath it. Same markup, two
901
+ densities; the rail's rules are in the container query above. */
902
+ .ig-row-meta {
903
+ align-items: center;
904
+ display: flex;
905
+ flex-wrap: wrap;
906
+ gap: var(--ig-space-tight);
907
+ min-width: 0;
88
908
  }
89
909
 
90
910
  /* ── readiness stations ────────────────────────────────────────────────── */
@@ -93,7 +913,6 @@ export const viewerStylesheet = `
93
913
  block-size: var(--ig-station-size);
94
914
  border: var(--ig-stroke) solid var(--ig-station-held);
95
915
  border-radius: 50%;
96
- box-shadow: 0 0 0 var(--ig-station-halo) var(--ig-bg);
97
916
  display: inline-block;
98
917
  inline-size: var(--ig-station-size);
99
918
  }
@@ -116,35 +935,84 @@ export const viewerStylesheet = `
116
935
 
117
936
  /* ── titles, identity, provenance ──────────────────────────────────────── */
118
937
 
938
+ /* NOTHING TRUNCATES. The ellipsis this rule used to carry, against a FIXED row
939
+ height, is the direct cause of the frame's titles arriving on screen as
940
+ "Retype the ca…" — a panel whose job is to say what the work is, declining
941
+ to say what the work is. A row grows instead; --ig-row-min-height is a
942
+ floor, not a height. anywhere covers the one case wrapping cannot: an
943
+ unbroken token wider than the column, which would otherwise overflow the
944
+ panel rather than the line. */
119
945
  .ig-title {
120
946
  color: var(--ig-text);
121
- overflow: hidden;
122
- text-overflow: ellipsis;
123
- white-space: nowrap;
947
+ font-size: var(--ig-font-size-row);
948
+ overflow-wrap: anywhere;
949
+ }
950
+
951
+ .ig-slot[data-held='true'] .ig-title,
952
+ .ig-footer .ig-title {
953
+ color: var(--ig-text-body);
954
+ }
955
+
956
+ /* A held row's CONTENT recedes, not its position. The frame dims the body and
957
+ leaves the rank track at full strength, so the row still reads as occupying
958
+ the slot it would have taken. */
959
+ .ig-slot[data-held='true'] .ig-row-body {
960
+ opacity: 0.72;
961
+ }
962
+
963
+ /* The isolated-issue tally sits directly on the panel, outside the padded
964
+ groups, so it carries the panel's own inset rather than none. */
965
+ .ig-count {
966
+ margin: 0;
967
+ padding: var(--ig-space) var(--ig-space-wide);
124
968
  }
125
969
 
126
970
  .ig-id,
127
971
  .ig-count {
128
972
  color: var(--ig-text-muted);
129
973
  font-family: var(--ig-font-mono);
974
+ font-size: var(--ig-font-size-small);
130
975
  font-variant-numeric: tabular-nums;
131
976
  }
132
977
 
133
978
  .ig-link {
134
- color: var(--ig-accent);
979
+ color: inherit;
135
980
  text-decoration: none;
136
981
  }
137
982
 
138
983
  .ig-link:hover {
984
+ color: var(--ig-accent);
139
985
  text-decoration: underline;
140
986
  }
141
987
 
988
+ /* THE PROVENANCE LINE IS NOT MUTED. It is the answer to "why is this here",
989
+ which is the panel's whole reason to exist, so the frame gives it body text
990
+ and lifts the named parts to the brightest ink on the page. Muting the whole
991
+ sentence — which is what shipped — files the explanation under decoration. */
142
992
  .ig-provenance,
143
993
  .ig-hold,
144
994
  .ig-caveat {
145
- color: var(--ig-text-muted);
995
+ align-items: baseline;
996
+ color: var(--ig-text-body);
997
+ display: flex;
998
+ font-family: var(--ig-font-mono);
146
999
  font-size: var(--ig-font-size-small);
147
- grid-column: 3 / -1;
1000
+ gap: var(--ig-space-tight);
1001
+ margin: 0;
1002
+ }
1003
+
1004
+ .ig-provenance .ig-id,
1005
+ .ig-hold .ig-id,
1006
+ .ig-caveat .ig-id {
1007
+ color: var(--ig-text);
1008
+ }
1009
+
1010
+ /* The turnstile the frame puts in front of every explanation, so the line reads
1011
+ as subordinate to the row above it without being dimmed. */
1012
+ .ig-turn {
1013
+ color: var(--ig-text-muted);
1014
+ font-size: var(--ig-font-size-micro);
1015
+ flex: 0 0 auto;
148
1016
  }
149
1017
 
150
1018
  .ig-strike {
@@ -153,155 +1021,224 @@ export const viewerStylesheet = `
153
1021
 
154
1022
  /* ── edge badges ───────────────────────────────────────────────────────── */
155
1023
 
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
1024
  .ig-badges {
165
1025
  display: flex;
166
1026
  flex-wrap: wrap;
167
1027
  gap: var(--ig-space-tight);
168
- grid-column: 3 / -1;
169
1028
  }
170
1029
 
1030
+ /* A TINTED CHIP, NOT A BARE OUTLINE. The frame fills every relationship chip
1031
+ with its own hue at a low alpha inside a heavier border of the same hue;
1032
+ drawn as an outline alone the badge row reads as a row of empty boxes, and
1033
+ the hue channel carries a third of the weight the design gives it. The two
1034
+ alphas are the theme's, so a host retinting a relationship retints its chip. */
171
1035
  .ig-badge {
172
1036
  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. */
1037
+ background: var(--ig-surface-2);
177
1038
  border: var(--ig-stroke) solid var(--ig-line);
178
- border-radius: var(--ig-radius);
1039
+ border-radius: var(--ig-radius-small);
179
1040
  color: var(--ig-text);
180
1041
  display: inline-flex;
181
- font-size: var(--ig-font-size-small);
1042
+ font-family: var(--ig-font-mono);
1043
+ font-size: var(--ig-font-size-micro);
182
1044
  gap: var(--ig-space-tight);
183
- padding: 0 var(--ig-space-tight);
1045
+ padding: var(--ig-space-micro) var(--ig-space-tight);
184
1046
  }
185
1047
 
186
- /* THE HUE IS A BORDER COLOUR HERE, NOT A TEXT COLOUR — the theme holds the
1048
+ /* THE HUE IS A BORDER AND A FILL HERE, NOT A TEXT COLOUR — the theme holds the
187
1049
  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
1050
+ painting badge LABELS with them contradicts the palette's own claim. It was
189
1051
  not merely theoretical: at this size duplicate-of measured 3.98:1 on
190
1052
  --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
1053
+ asserts for every text colour. The label takes --ig-text, which that test
1054
+ already proves on all three surfaces, and the hue keeps the non-text use it
1055
+ was measured for. This is a DELIBERATE DEPARTURE from the frame, which tints
1056
+ its chip labels; the frame's own colour-blind-safety clause says hue is one
1057
+ of four redundant channels, and here it is carried by two of them.
1058
+ NO CHANNEL IS LOST. The dash pattern and the glyph are untouched, and the
196
1059
  vocabulary test independently proves all five stay distinguishable with hue
197
1060
  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; }
1061
+ /* WRITTEN OUT PER EDGE RATHER THAN THROUGH ONE INTERMEDIATE PROPERTY. A
1062
+ an intermediate --ig-badge-hue set here and read here would be a property no
1063
+ theme and no layout declares, and styles.test.ts refuses those on purpose:
1064
+ the whole point of that guard is that every var() in this file names
1065
+ something a host or the layout actually sets. */
1066
+ .ig-badge[data-edge='blocked-by'] {
1067
+ background: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-fill), transparent);
1068
+ border-color: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-border), transparent);
1069
+ border-style: solid;
1070
+ }
203
1071
 
204
- .ig-glyph {
205
- font-family: var(--ig-font-mono);
1072
+ .ig-badge[data-edge='serialize-with'] {
1073
+ background: color-mix(in srgb, var(--ig-edge-serialize-with) var(--ig-tint-fill), transparent);
1074
+ border-color: color-mix(in srgb, var(--ig-edge-serialize-with) var(--ig-tint-border), transparent);
1075
+ border-style: double;
206
1076
  }
207
1077
 
208
- /* ── the footer group: holds that earn no rank slot ────────────────────── */
1078
+ .ig-badge[data-edge='together-with'] {
1079
+ background: color-mix(in srgb, var(--ig-edge-together-with) var(--ig-tint-fill), transparent);
1080
+ border-color: color-mix(in srgb, var(--ig-edge-together-with) var(--ig-tint-border), transparent);
1081
+ border-style: solid;
1082
+ }
209
1083
 
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);
1084
+ .ig-badge[data-edge='duplicate-of'] {
1085
+ background: color-mix(in srgb, var(--ig-edge-duplicate-of) var(--ig-tint-fill), transparent);
1086
+ border-color: color-mix(in srgb, var(--ig-edge-duplicate-of) var(--ig-tint-border), transparent);
1087
+ border-style: dotted;
214
1088
  }
215
1089
 
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);
1090
+ .ig-badge[data-edge='decomposed-from'] {
1091
+ background: color-mix(in srgb, var(--ig-edge-decomposed-from) var(--ig-tint-fill), transparent);
1092
+ border-color: color-mix(in srgb, var(--ig-edge-decomposed-from) var(--ig-tint-border), transparent);
1093
+ border-style: dashed;
220
1094
  }
221
1095
 
222
- /* ── the host facts: what the graph cannot say ─────────────────────────── */
1096
+ /* A PROMOTION IS THE ONE CHIP THE FRAME ACCENTS. It is the most interesting
1097
+ event the panel can show — a low tier pulled to the top because it blocks a
1098
+ high one — and the design spends its accent on exactly that. */
1099
+ .ig-badge[data-priority='promoted'] {
1100
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-fill), transparent);
1101
+ border-color: color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
1102
+ }
223
1103
 
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;
1104
+ .ig-badge[data-evidence='verified'] {
1105
+ background: color-mix(in srgb, var(--ig-station-ready) var(--ig-tint-fill), transparent);
1106
+ border-color: color-mix(in srgb, var(--ig-station-ready) var(--ig-tint-border), transparent);
1107
+ }
1108
+
1109
+ /* A caveat or hold-label chip carries no relationship hue: it is a word, not
1110
+ an edge, so it keeps the line colour the plain badge has. */
1111
+ .ig-badge[data-caveat],
1112
+ .ig-badge[data-hold] {
1113
+ color: var(--ig-text-body);
1114
+ }
1115
+
1116
+ .ig-badge[data-caveat='preview-only'] {
1117
+ border-color: color-mix(in srgb, var(--ig-state-conflict) var(--ig-tint-border), transparent);
1118
+ }
1119
+
1120
+ .ig-badge[data-not-ready='true'] {
1121
+ background: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-fill), transparent);
1122
+ border-color: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-border), transparent);
1123
+ }
1124
+
1125
+ .ig-glyph {
1126
+ font-family: var(--ig-font-mono);
1127
+ }
1128
+
1129
+ /* ── the together unit: one rank, two issues ───────────────────────────── */
1130
+
1131
+ .ig-unit-mark {
1132
+ align-items: center;
228
1133
  display: flex;
229
- flex-wrap: wrap;
230
- gap: var(--ig-space);
231
- margin: 0 0 var(--ig-space);
1134
+ gap: var(--ig-space-snug);
232
1135
  }
233
1136
 
234
- .ig-summary,
235
- .ig-freshness {
1137
+ .ig-unit-pill {
1138
+ background: var(--ig-accent);
1139
+ border-radius: var(--ig-radius-small);
1140
+ color: var(--ig-bg);
1141
+ font-family: var(--ig-font-mono);
1142
+ font-size: var(--ig-font-size-pill);
1143
+ font-weight: var(--ig-weight-strong);
1144
+ letter-spacing: var(--ig-tracking-badge);
1145
+ padding: var(--ig-space-micro) var(--ig-space-tight);
1146
+ text-transform: uppercase;
1147
+ }
1148
+
1149
+ .ig-unit-note {
236
1150
  color: var(--ig-text-muted);
237
- font-size: var(--ig-font-size-small);
238
- font-variant-numeric: tabular-nums;
1151
+ font-family: var(--ig-font-mono);
1152
+ font-size: var(--ig-font-size-micro);
1153
+ }
1154
+
1155
+ /* THE MEMBERS GET ONE LINE EACH. Joining them with a separator — which is what
1156
+ shipped — makes a two-issue unit read as one issue with a long title, and
1157
+ the unit is the one place the design draws an enclosure to say the opposite. */
1158
+ /* THE UNIT'S ENCLOSURE IS THE together-with MARK. It is drawn at the
1159
+ connector's hairline width, which is what that token has always meant: finer
1160
+ than an ordinary edge, so the enclosure stays the primary read. */
1161
+ .ig-unit {
1162
+ border: var(--ig-stroke-connector) solid color-mix(in srgb, var(--ig-edge-together-with) var(--ig-tint-border), transparent);
1163
+ border-radius: var(--ig-radius);
1164
+ list-style: none;
239
1165
  margin: 0;
1166
+ overflow: hidden;
1167
+ padding: 0;
240
1168
  }
241
1169
 
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);
1170
+ .ig-unit-member {
1171
+ display: flex;
1172
+ flex-direction: column;
1173
+ gap: var(--ig-space-micro);
1174
+ padding: var(--ig-space-snug) var(--ig-space);
249
1175
  }
250
1176
 
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;
1177
+ .ig-unit-member + .ig-unit-member {
1178
+ border-top: var(--ig-stroke) solid var(--ig-line);
261
1179
  }
262
1180
 
263
- .ig-refresh:hover {
264
- text-decoration: underline;
1181
+ .ig-unit-member .ig-title {
1182
+ font-size: var(--ig-font-size-compact);
265
1183
  }
266
1184
 
267
- .ig-now {
268
- list-style: none;
269
- margin: 0 0 var(--ig-space);
270
- padding: 0;
1185
+ .ig-unit-member .ig-id {
1186
+ font-size: var(--ig-font-size-meta);
271
1187
  }
272
1188
 
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 {
1189
+ /* ── the footer group: holds that earn no rank slot ────────────────────── */
1190
+
1191
+ .ig-footer {
1192
+ background: var(--ig-surface-2);
1193
+ padding: var(--ig-space) var(--ig-space-wide);
1194
+ }
1195
+
1196
+ .ig-footer-head {
276
1197
  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);
1198
+ display: flex;
1199
+ gap: var(--ig-space);
1200
+ justify-content: space-between;
285
1201
  }
286
1202
 
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;
1203
+ .ig-footer-title {
1204
+ color: var(--ig-text-body);
1205
+ font-size: var(--ig-font-size-compact);
1206
+ margin: 0;
292
1207
  }
293
1208
 
294
- .ig-now-phase {
1209
+ .ig-footer-labels {
295
1210
  color: var(--ig-text-muted);
296
- font-size: var(--ig-font-size-small);
297
- white-space: nowrap;
1211
+ font-family: var(--ig-font-mono);
1212
+ font-size: var(--ig-font-size-meta);
298
1213
  }
299
1214
 
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);
1215
+ /* ONE LINE EACH, and the frame is explicit about why: these are not facts
1216
+ about the work, so they earn no rank slot and no explanation block. A full
1217
+ row here — station, badges, provenance — was claiming the opposite. */
1218
+ .ig-footer .ig-list {
1219
+ display: flex;
1220
+ flex-direction: column;
1221
+ gap: var(--ig-space-snug);
1222
+ margin-top: var(--ig-space);
1223
+ }
1224
+
1225
+ .ig-footer-row {
1226
+ align-items: center;
1227
+ display: flex;
1228
+ gap: var(--ig-space-snug);
1229
+ opacity: 0.72;
1230
+ }
1231
+
1232
+ .ig-footer-row .ig-title {
1233
+ font-size: var(--ig-font-size-compact);
1234
+ }
1235
+
1236
+ .ig-footer-row .ig-id {
1237
+ font-size: var(--ig-font-size-meta);
1238
+ }
1239
+
1240
+ .ig-footer-row[aria-current='true'] {
1241
+ opacity: 1;
305
1242
  }
306
1243
 
307
1244
  /* ── the graph canvas ──────────────────────────────────────────────────── */
@@ -325,7 +1262,26 @@ export const viewerStylesheet = `
325
1262
  inline-size: var(--ig-stage-w);
326
1263
  }
327
1264
 
328
- /* The ranks and stations FOR the spine nodes, sitting on them. */
1265
+ /* The three column headers, which say what each column is FOR. Without them
1266
+ the gutters read as two more piles of issues rather than as the two answers
1267
+ the spine deliberately keeps off itself. */
1268
+ .ig-column-head {
1269
+ color: var(--ig-text-muted);
1270
+ font-family: var(--ig-font-mono);
1271
+ font-size: var(--ig-font-size-pill);
1272
+ letter-spacing: var(--ig-tracking-group);
1273
+ position: absolute;
1274
+ text-transform: uppercase;
1275
+ inset-block-start: var(--ig-space-snug);
1276
+ inset-inline-start: var(--ig-col-x);
1277
+ }
1278
+
1279
+ .ig-column-head[data-column='spine'] {
1280
+ color: var(--ig-accent);
1281
+ }
1282
+
1283
+ /* The node cards and the station column, sitting on the coordinates the layout
1284
+ computed. */
329
1285
  .ig-rail {
330
1286
  inset: 0;
331
1287
  pointer-events: none;
@@ -333,16 +1289,147 @@ export const viewerStylesheet = `
333
1289
  }
334
1290
 
335
1291
  .ig-rail-row {
336
- block-size: var(--ig-row-h);
1292
+ block-size: auto;
337
1293
  inline-size: var(--ig-row-w);
338
1294
  inset-block-start: var(--ig-row-y);
339
1295
  inset-inline-start: var(--ig-row-x);
340
- margin-bottom: 0;
341
- min-block-size: 0;
1296
+ min-block-size: var(--ig-row-h);
342
1297
  pointer-events: auto;
343
1298
  position: absolute;
344
1299
  }
345
1300
 
1301
+ /* A CARD, NOT A LABELLED RECTANGLE. Drawing the node's text in SVG is what
1302
+ forced every title through a width fit and out the other side truncated; an
1303
+ HTML card wraps, so the acceptance criterion "nothing truncates" is met by
1304
+ construction rather than by choosing a wider box. */
1305
+ .ig-card {
1306
+ background: var(--ig-surface);
1307
+ border: var(--ig-stroke) solid var(--ig-line);
1308
+ border-radius: var(--ig-radius-large);
1309
+ display: flex;
1310
+ flex-direction: column;
1311
+ gap: var(--ig-space-tight);
1312
+ padding: var(--ig-space) var(--ig-space);
1313
+ }
1314
+
1315
+ .ig-card .ig-title {
1316
+ font-size: var(--ig-font-size-compact);
1317
+ }
1318
+
1319
+ .ig-card .ig-id {
1320
+ font-size: var(--ig-font-size-meta);
1321
+ }
1322
+
1323
+ .ig-card[data-column='left'] {
1324
+ background: var(--ig-surface-2);
1325
+ border-color: color-mix(in srgb, var(--ig-edge-blocked-by) var(--ig-tint-border), transparent);
1326
+ }
1327
+
1328
+ /* NEVER WORKED READS AS NEVER DRAWN IN. The right gutter's two node states are
1329
+ the design's own: a dashed provenance origin and a dotted duplicate, both on
1330
+ no ground at all, so the eye files them as outside the order before reading
1331
+ a word. */
1332
+ .ig-card[data-column='right'] {
1333
+ background: transparent;
1334
+ border-style: dashed;
1335
+ opacity: 0.8;
1336
+ }
1337
+
1338
+ .ig-card[data-exclusion='true'] {
1339
+ border-style: dotted;
1340
+ border-color: color-mix(in srgb, var(--ig-edge-duplicate-of) var(--ig-tint-border), transparent);
1341
+ }
1342
+
1343
+ .ig-card[data-held='true'] {
1344
+ background: transparent;
1345
+ border-style: dashed;
1346
+ opacity: 0.75;
1347
+ }
1348
+
1349
+ .ig-card[data-now='true'] {
1350
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-wash), transparent);
1351
+ border-color: color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
1352
+ }
1353
+
1354
+ .ig-card[data-unit='true'] {
1355
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-unit), transparent);
1356
+ border-color: var(--ig-accent);
1357
+ }
1358
+
1359
+ .ig-rail-row[aria-current='true'] .ig-card {
1360
+ border-color: var(--ig-accent);
1361
+ box-shadow: 0 0 0 var(--ig-band-rail) color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
1362
+ }
1363
+
1364
+ /* The station column: a disc on the spine, carrying the rank the row would
1365
+ otherwise have to be read for. */
1366
+ .ig-spine-station {
1367
+ align-items: center;
1368
+ background: var(--ig-bg);
1369
+ block-size: var(--ig-station-box);
1370
+ border: calc(var(--ig-stroke) * 2) solid var(--ig-station-ready);
1371
+ border-radius: 50%;
1372
+ box-shadow: 0 0 0 var(--ig-station-halo) var(--ig-bg);
1373
+ color: var(--ig-station-pending);
1374
+ display: flex;
1375
+ font-family: var(--ig-font-mono);
1376
+ font-size: var(--ig-font-size-small);
1377
+ font-weight: var(--ig-weight-heavy);
1378
+ inline-size: var(--ig-station-box);
1379
+ inset-block-start: var(--ig-station-y);
1380
+ inset-inline-start: var(--ig-station-x);
1381
+ justify-content: center;
1382
+ pointer-events: none;
1383
+ position: absolute;
1384
+ }
1385
+
1386
+ .ig-spine-station[data-fill='filled'] {
1387
+ background: var(--ig-station-ready);
1388
+ color: var(--ig-bg);
1389
+ }
1390
+
1391
+ .ig-spine-station[data-fill='hollow'] {
1392
+ border-color: var(--ig-station-ready);
1393
+ color: var(--ig-station-ready);
1394
+ }
1395
+
1396
+ .ig-spine-station[data-fill='dashed'] {
1397
+ border-style: dashed;
1398
+ border-width: var(--ig-stroke);
1399
+ border-color: var(--ig-station-held);
1400
+ color: var(--ig-text-muted);
1401
+ }
1402
+
1403
+ /* The serialize hold is gold in the frame, on the station as well as the arc:
1404
+ a station that waits for a PEER is a different fact from one that waits for
1405
+ a rank above it. */
1406
+ /* THE RING TAKES THE HUE, THE DIGIT DOES NOT. §16b draws both in gold, and the
1407
+ theme holds every edge hue to the 3:1 NON-TEXT bar — so painting the rank
1408
+ figure with it makes a claim the palette does not support, on the one glyph
1409
+ in the station a reader has to actually read. The hue keeps the ring, which
1410
+ is the channel it was measured for; the number keeps the ink every text
1411
+ contrast test already proves. */
1412
+ .ig-spine-station[data-wait='serialize'] {
1413
+ border-color: var(--ig-edge-serialize-with);
1414
+ color: var(--ig-text);
1415
+ }
1416
+
1417
+ .ig-spine {
1418
+ stroke: var(--ig-line);
1419
+ stroke-width: calc(var(--ig-stroke) * 2);
1420
+ }
1421
+
1422
+ .ig-more {
1423
+ color: var(--ig-text-muted);
1424
+ font-family: var(--ig-font-mono);
1425
+ font-size: var(--ig-font-size-micro);
1426
+ inline-size: var(--ig-row-w);
1427
+ inset-block-start: var(--ig-row-y);
1428
+ inset-inline-start: var(--ig-row-x);
1429
+ position: absolute;
1430
+ text-align: center;
1431
+ }
1432
+
346
1433
  .ig-node {
347
1434
  fill: var(--ig-surface);
348
1435
  stroke: var(--ig-line);
@@ -372,18 +1459,15 @@ export const viewerStylesheet = `
372
1459
  .ig-edge[data-edge='decomposed-from'] { stroke: var(--ig-edge-decomposed-from); }
373
1460
 
374
1461
  /* 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. */
1462
+ one source for the channel the colour-blind-safety claim rests on. Drawn only
1463
+ in the legend now: a together unit is ONE card with its members listed inside
1464
+ it, so the canvas has no pair of boxes to surround. */
376
1465
  .ig-enclosure {
377
1466
  fill: none;
378
1467
  stroke: var(--ig-edge-together-with);
379
1468
  stroke-width: var(--ig-stroke);
380
1469
  }
381
1470
 
382
- .ig-connector {
383
- stroke: var(--ig-edge-together-with);
384
- stroke-width: var(--ig-stroke-connector);
385
- }
386
-
387
1471
  /* currentColor on the marker resolves to the inherited text colour, not to the
388
1472
  edge's stroke — so a terminal has to be given the hue explicitly or it renders
389
1473
  in body text and the fourth channel silently collapses. */
@@ -392,6 +1476,35 @@ export const viewerStylesheet = `
392
1476
  stroke-width: var(--ig-stroke);
393
1477
  }
394
1478
 
1479
+ /* A MARK IS HIT-TESTABLE, AND THAT IS THE OPPOSITE OF WHAT IT FIRST SAID.
1480
+
1481
+ It carried a pointer-events none rule, reasoning from the halo: a decoration
1482
+ over a line must not swallow the click aimed at the line. That reasoning does
1483
+ not transfer, because a halo IS the line again and a mark is NOT — every one
1484
+ of these is deliberately offset AWAY from the path, so a click on it does not
1485
+ fall through onto the edge. It falls through onto the canvas, and the viewer's
1486
+ walk climbs to the canvas group and reports a click on nothing, clearing the
1487
+ very selection the reader was making. A conflict's companion is the clearest
1488
+ case: a visibly separate line that could not be pointed at.
1489
+
1490
+ That is a defect this package has already paid for once. The overlay module
1491
+ keeps its pointer identity on the dash clone for exactly this reason, and says
1492
+ so: dropping the identity makes a clone a dead zone over its own edge. Every
1493
+ mark here publishes the same identity, so making it unhittable spent the
1494
+ identity it had just been given.
1495
+
1496
+ Colour is set per element, from the token name the caller supplied, for the
1497
+ reason the edge hues are set per element: one source for a channel. Nothing
1498
+ here paints a state, because nothing here knows what a state is. */
1499
+ .ig-edge-mark {
1500
+ font-family: var(--ig-font-ui);
1501
+ font-size: var(--ig-font-size-small);
1502
+ }
1503
+
1504
+ .ig-edge-companion {
1505
+ stroke-width: var(--ig-stroke);
1506
+ }
1507
+
395
1508
  .ig-terminal[data-edge='blocked-by'] { color: var(--ig-edge-blocked-by); }
396
1509
  .ig-terminal[data-edge='serialize-with'] { color: var(--ig-edge-serialize-with); }
397
1510
  .ig-terminal[data-edge='together-with'] { color: var(--ig-edge-together-with); }
@@ -406,6 +1519,7 @@ export const viewerStylesheet = `
406
1519
  border: var(--ig-stroke) solid var(--ig-line);
407
1520
  border-radius: var(--ig-radius);
408
1521
  color: var(--ig-text-body);
1522
+ margin: var(--ig-space-wide);
409
1523
  padding: var(--ig-space);
410
1524
  }
411
1525
 
@@ -445,6 +1559,10 @@ export const viewerStylesheet = `
445
1559
  padding: 0;
446
1560
  }
447
1561
 
1562
+ .ig-tree {
1563
+ padding: var(--ig-space) var(--ig-space-wide);
1564
+ }
1565
+
448
1566
  .ig-tree .ig-list {
449
1567
  border-left: var(--ig-stroke) dashed var(--ig-edge-decomposed-from);
450
1568
  margin-left: var(--ig-space);
@@ -457,19 +1575,44 @@ export const viewerStylesheet = `
457
1575
 
458
1576
  /* ── legend ────────────────────────────────────────────────────────────── */
459
1577
 
1578
+ /* A FOOTER BAR, WHERE §16b PUTS IT, and worded rather than glyph-only. It sits
1579
+ under the drawing it explains, carries a drawn sample of each line so the
1580
+ dash and terminal channels are legible cold, and ends with the readiness key
1581
+ the station fills need. */
460
1582
  .ig-legend {
1583
+ align-items: center;
1584
+ background: var(--ig-surface-2);
461
1585
  border: 0;
1586
+ border-top: var(--ig-stroke) solid var(--ig-line);
462
1587
  display: flex;
463
1588
  flex-wrap: wrap;
464
- gap: var(--ig-space);
465
- margin: 0 0 var(--ig-space);
466
- padding: 0;
1589
+ gap: var(--ig-space-wide);
1590
+ margin: 0;
1591
+ padding: var(--ig-space) var(--ig-space-wide);
467
1592
  }
468
1593
 
469
1594
  .ig-legend-caption {
470
1595
  color: var(--ig-text-muted);
471
- font-size: var(--ig-font-size-small);
1596
+ font-family: var(--ig-font-mono);
1597
+ font-size: var(--ig-font-size-pill);
1598
+ letter-spacing: var(--ig-tracking-group);
472
1599
  padding: 0;
1600
+ text-transform: uppercase;
1601
+ }
1602
+
1603
+ .ig-legend-item {
1604
+ align-items: center;
1605
+ color: var(--ig-text-body);
1606
+ display: inline-flex;
1607
+ font-family: var(--ig-font-mono);
1608
+ font-size: var(--ig-font-size-meta);
1609
+ gap: var(--ig-space-tight);
1610
+ }
1611
+
1612
+ .ig-legend-keys {
1613
+ display: inline-flex;
1614
+ gap: var(--ig-space-loose);
1615
+ margin-inline-start: auto;
473
1616
  }
474
1617
  `;
475
1618
  //# sourceMappingURL=styles.js.map