@issuegraph/editor 0.4.0 → 0.22.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 (137) hide show
  1. package/README.md +65 -9
  2. package/dist/a11y/baseline.d.ts +182 -0
  3. package/dist/a11y/baseline.d.ts.map +1 -0
  4. package/dist/a11y/baseline.js +434 -0
  5. package/dist/a11y/baseline.js.map +1 -0
  6. package/dist/audit/findings.d.ts +107 -1
  7. package/dist/audit/findings.d.ts.map +1 -1
  8. package/dist/audit/findings.js +113 -6
  9. package/dist/audit/findings.js.map +1 -1
  10. package/dist/audit/panel.d.ts +277 -0
  11. package/dist/audit/panel.d.ts.map +1 -0
  12. package/dist/audit/panel.js +462 -0
  13. package/dist/audit/panel.js.map +1 -0
  14. package/dist/audit/refused.d.ts +99 -0
  15. package/dist/audit/refused.d.ts.map +1 -0
  16. package/dist/audit/refused.js +276 -0
  17. package/dist/audit/refused.js.map +1 -0
  18. package/dist/audit/styles.d.ts +1 -1
  19. package/dist/audit/styles.d.ts.map +1 -1
  20. package/dist/audit/styles.js +429 -1
  21. package/dist/audit/styles.js.map +1 -1
  22. package/dist/audit/surface.d.ts +54 -12
  23. package/dist/audit/surface.d.ts.map +1 -1
  24. package/dist/audit/surface.js +53 -13
  25. package/dist/audit/surface.js.map +1 -1
  26. package/dist/create/draft.d.ts +36 -0
  27. package/dist/create/draft.d.ts.map +1 -1
  28. package/dist/create/draft.js +40 -0
  29. package/dist/create/draft.js.map +1 -1
  30. package/dist/create/keys.d.ts +134 -9
  31. package/dist/create/keys.d.ts.map +1 -1
  32. package/dist/create/keys.js +119 -20
  33. package/dist/create/keys.js.map +1 -1
  34. package/dist/firstpass/candidates.d.ts +17 -1
  35. package/dist/firstpass/candidates.d.ts.map +1 -1
  36. package/dist/index.d.ts +42 -17
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +45 -14
  39. package/dist/index.js.map +1 -1
  40. package/dist/overlay/render.d.ts +11 -3
  41. package/dist/overlay/render.d.ts.map +1 -1
  42. package/dist/overlay/render.js +76 -20
  43. package/dist/overlay/render.js.map +1 -1
  44. package/dist/overlay/request.d.ts +71 -0
  45. package/dist/overlay/request.d.ts.map +1 -0
  46. package/dist/overlay/request.js +129 -0
  47. package/dist/overlay/request.js.map +1 -0
  48. package/dist/overlay/styles.d.ts +1 -1
  49. package/dist/overlay/styles.d.ts.map +1 -1
  50. package/dist/overlay/styles.js +78 -7
  51. package/dist/overlay/styles.js.map +1 -1
  52. package/dist/picker/render.d.ts +26 -16
  53. package/dist/picker/render.d.ts.map +1 -1
  54. package/dist/picker/render.js +25 -36
  55. package/dist/picker/render.js.map +1 -1
  56. package/dist/picker/styles.d.ts +7 -7
  57. package/dist/picker/styles.d.ts.map +1 -1
  58. package/dist/picker/styles.js +7 -36
  59. package/dist/picker/styles.js.map +1 -1
  60. package/dist/picker/view.d.ts +8 -4
  61. package/dist/picker/view.d.ts.map +1 -1
  62. package/dist/picker/view.js +4 -3
  63. package/dist/picker/view.js.map +1 -1
  64. package/dist/picker/words.d.ts +12 -10
  65. package/dist/picker/words.d.ts.map +1 -1
  66. package/dist/picker/words.js +12 -8
  67. package/dist/picker/words.js.map +1 -1
  68. package/dist/reevaluate/parts.d.ts +130 -0
  69. package/dist/reevaluate/parts.d.ts.map +1 -0
  70. package/dist/reevaluate/parts.js +260 -0
  71. package/dist/reevaluate/parts.js.map +1 -0
  72. package/dist/reevaluate/render.d.ts +18 -11
  73. package/dist/reevaluate/render.d.ts.map +1 -1
  74. package/dist/reevaluate/render.js +21 -135
  75. package/dist/reevaluate/render.js.map +1 -1
  76. package/dist/reevaluate/styles.d.ts +1 -1
  77. package/dist/reevaluate/styles.d.ts.map +1 -1
  78. package/dist/reevaluate/styles.js +26 -0
  79. package/dist/reevaluate/styles.js.map +1 -1
  80. package/dist/reevaluate/view.d.ts +16 -1
  81. package/dist/reevaluate/view.d.ts.map +1 -1
  82. package/dist/reevaluate/view.js +21 -1
  83. package/dist/reevaluate/view.js.map +1 -1
  84. package/dist/reevaluate/words.d.ts +7 -0
  85. package/dist/reevaluate/words.d.ts.map +1 -1
  86. package/dist/scale/ladder.d.ts +62 -6
  87. package/dist/scale/ladder.d.ts.map +1 -1
  88. package/dist/scale/ladder.js +129 -11
  89. package/dist/scale/ladder.js.map +1 -1
  90. package/dist/scale/render.d.ts +44 -0
  91. package/dist/scale/render.d.ts.map +1 -1
  92. package/dist/scale/render.js +206 -28
  93. package/dist/scale/render.js.map +1 -1
  94. package/dist/scale/styles.d.ts +11 -5
  95. package/dist/scale/styles.d.ts.map +1 -1
  96. package/dist/scale/styles.js +133 -4
  97. package/dist/scale/styles.js.map +1 -1
  98. package/dist/workspace/chrome.d.ts +1 -1
  99. package/dist/workspace/chrome.d.ts.map +1 -1
  100. package/dist/workspace/chrome.js +48 -0
  101. package/dist/workspace/chrome.js.map +1 -1
  102. package/dist/workspace/firstpass.d.ts +166 -0
  103. package/dist/workspace/firstpass.d.ts.map +1 -0
  104. package/dist/workspace/firstpass.js +151 -0
  105. package/dist/workspace/firstpass.js.map +1 -0
  106. package/dist/workspace/host.d.ts +283 -11
  107. package/dist/workspace/host.d.ts.map +1 -1
  108. package/dist/workspace/host.js +614 -43
  109. package/dist/workspace/host.js.map +1 -1
  110. package/dist/workspace/inspector.d.ts +72 -2
  111. package/dist/workspace/inspector.d.ts.map +1 -1
  112. package/dist/workspace/inspector.js +26 -0
  113. package/dist/workspace/inspector.js.map +1 -1
  114. package/dist/workspace/mount.d.ts +75 -6
  115. package/dist/workspace/mount.d.ts.map +1 -1
  116. package/dist/workspace/mount.js +1851 -60
  117. package/dist/workspace/mount.js.map +1 -1
  118. package/dist/workspace/rail.d.ts +5 -2
  119. package/dist/workspace/rail.d.ts.map +1 -1
  120. package/dist/workspace/rail.js.map +1 -1
  121. package/dist/workspace/recovery.d.ts +155 -0
  122. package/dist/workspace/recovery.d.ts.map +1 -0
  123. package/dist/workspace/recovery.js +156 -0
  124. package/dist/workspace/recovery.js.map +1 -0
  125. package/dist/workspace/render.d.ts +779 -2
  126. package/dist/workspace/render.d.ts.map +1 -1
  127. package/dist/workspace/render.js +1727 -61
  128. package/dist/workspace/render.js.map +1 -1
  129. package/dist/workspace/selection.d.ts +11 -0
  130. package/dist/workspace/selection.d.ts.map +1 -1
  131. package/dist/workspace/selection.js +10 -0
  132. package/dist/workspace/selection.js.map +1 -1
  133. package/dist/workspace/styles.d.ts +1 -1
  134. package/dist/workspace/styles.d.ts.map +1 -1
  135. package/dist/workspace/styles.js +1405 -42
  136. package/dist/workspace/styles.js.map +1 -1
  137. package/package.json +4 -4
@@ -58,7 +58,17 @@ export const workspaceStylesheet = `
58
58
  squeeze the canvas, which is the one thing fixed positions exist to stop.
59
59
  Nothing failed loudly, because an invalid declaration is simply absent.
60
60
 
61
- 40 is a reading measure for a title plus its id, in characters. */
61
+ 40 is a reading measure for a title plus its id, in characters.
62
+
63
+ THIS IS THE PRE-17k FALLBACK, and it is deliberately the OLD equal measure. 17k
64
+ gives 390 / 1fr / 330 for widths at or above 1360 and something else
65
+ entirely below it: the inspector lifts to an overlay from 1120, and under
66
+ that the canvas collapses to a strip. Neither of those exists yet, so
67
+ applying the wide proportions at every width squeezes the canvas instead
68
+ of collapsing it — measured at 990px, where the canvas fell to 248px. That
69
+ is the one rule 17k states outright inverted: "the rail never yields, the
70
+ canvas yields first." Below the breakpoint the old equal tracks are the
71
+ honest placeholder. */
62
72
  grid-template-columns:
63
73
  calc(var(--ig-char-width) * 40)
64
74
  1fr
@@ -72,6 +82,53 @@ export const workspaceStylesheet = `
72
82
  line-height: var(--ig-line-height);
73
83
  }
74
84
 
85
+ /* THE CONTAINER IS THE MOUNT, NOT THE WORKSPACE, AND THAT IS NOT A DETAIL.
86
+ A container query CANNOT style the element that establishes the container:
87
+ put container-type on .ig-workspace and a rule inside @container matching
88
+ .ig-workspace never fires. It looked right, built clean, and did nothing —
89
+ measured at 1500px, where the rail and inspector sat at 312/312 while only
90
+ the canvas 1fr grew. A rule that can never fire is the same class as a guard
91
+ that can never fail, and the only reason this was caught is that the check
92
+ compared the TRACKS either side of the breakpoint rather than asking whether
93
+ the layout "changed".
94
+
95
+ .ig-mount is this package's own wrapper around the workspace, so the query
96
+ has a box to measure that is not the box it restyles. */
97
+ .ig-mount {
98
+ container-name: ig-workspace;
99
+ container-type: inline-size;
100
+ }
101
+
102
+ /* 17k's WIDE LAYOUT, AT THE WIDTH 17k GIVES IT FOR. A container query, not a
103
+ viewport one, for the reason layer 1's row already answers to: the workspace
104
+ is a box a host places, and a package that reads the window cannot be
105
+ dropped into someone else's page.
106
+
107
+ 1360 IS 17k's OWN NUMBER, not a chosen one — the table's first row reads
108
+ "at or above 1360: 390 / 1fr / 330, three zones, as 17a".
109
+
110
+ THE TWO ZONES ARE NOT THE SAME WIDTH, AND THAT IS THE POINT OF THIS RULE.
111
+ Both tracks were 40 characters, which is one reading measure applied twice;
112
+ 17a draws 1292 = 390 + 572 + 330, a rail WIDER than the inspector. The rail
113
+ carries a rank, a title, a meta line and a delta chip on one row while the
114
+ inspector carries a column of stacked blocks — not the same problem, and
115
+ sizing them alike shortchanged the surface 17k says must never yield.
116
+
117
+ 50 AND 42 CHARACTERS, NOT 390px AND 330px, because this file sizes zones in
118
+ TYPE on purpose: a host that scales its type scales the zones with it, and a
119
+ fixed track would not. At the shipped --ig-char-width of 7.8 that is 390 and
120
+ 327.6 — the rail exact, the inspector 2.4px under 17k's figure. Stated
121
+ rather than rounded away; the type-relative property is worth more than the
122
+ 2.4px, and it is the same BYO argument layer 1's row query rests on. */
123
+ @container ig-workspace (width >= 1360px) {
124
+ .ig-workspace {
125
+ grid-template-columns:
126
+ calc(var(--ig-char-width) * 50)
127
+ 1fr
128
+ calc(var(--ig-char-width) * 42);
129
+ }
130
+ }
131
+
75
132
  .ig-zone {
76
133
  min-width: 0;
77
134
  /* Without this a long title in any zone sets that track's minimum width and
@@ -83,6 +140,83 @@ export const workspaceStylesheet = `
83
140
  grid-area: header;
84
141
  }
85
142
 
143
+ /* SECTION 17a's HEADER: what backlog this is, how much of it is encoded, what
144
+ is wrong with it, how fresh the read is, and the way into a first pass. One
145
+ row, baseline-aligned, with the first-pass entry pushed to the far end --
146
+ the frame's only primary action on this surface.
147
+
148
+ NOTHING HERE MOVES, for the reason the whole sheet holds: the audit count
149
+ lives in this row, and section 17d asks for a count that never does. The
150
+ guard on that is a literal scan over these bytes, comments included, so the
151
+ words it looks for must not appear even in prose explaining their absence. */
152
+ .ig-workspace-header {
153
+ display: flex;
154
+ align-items: baseline;
155
+ flex-wrap: wrap;
156
+ gap: var(--ig-space);
157
+ padding: var(--ig-space-tight) var(--ig-space);
158
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
159
+ }
160
+
161
+ .ig-workspace-identity {
162
+ color: var(--ig-text);
163
+ font-family: var(--ig-font-mono);
164
+ }
165
+
166
+ /* The adoption pair and the freshness stamp, hoisted out of layer 1's panel
167
+ header by #135. Quiet, because the row's only loud element is the entry at
168
+ its far end -- these are orientation, read once and then ignored. */
169
+ .ig-workspace-counts,
170
+ .ig-workspace-freshness {
171
+ color: var(--ig-text-muted);
172
+ font-family: var(--ig-font-ui);
173
+ font-size: var(--ig-font-size-small);
174
+ }
175
+
176
+ /* THE ONLY TREATMENT STALENESS GETS, and it is why the stamp carries no word
177
+ for it: the state is on the element, so the sheet says what a stale read
178
+ looks like and no host has to translate an adjective. */
179
+ .ig-workspace-freshness[data-stale='true'] {
180
+ color: var(--ig-state-invalid);
181
+ }
182
+
183
+ /* A CONTROL, NOT A CHIP. It sits inside the stamp because that is what it
184
+ refreshes, and it takes the button reset the sheet gives every other quiet
185
+ control on this surface rather than the accent the first-pass entry spends. */
186
+ .ig-workspace-refresh {
187
+ margin-left: var(--ig-space-tight);
188
+ font: inherit;
189
+ color: var(--ig-text);
190
+ background: var(--ig-surface-2);
191
+ border: var(--ig-stroke) solid var(--ig-line);
192
+ border-radius: var(--ig-radius);
193
+ padding: 0 var(--ig-space-tight);
194
+ cursor: pointer;
195
+ }
196
+
197
+ .ig-workspace-refresh:hover {
198
+ border-color: var(--ig-accent);
199
+ }
200
+
201
+ .ig-workspace-refresh:focus-visible {
202
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
203
+ outline-offset: var(--ig-space-tight);
204
+ }
205
+
206
+ /* The one place the accent is spent on this surface, on the only action that
207
+ starts something. The frame draws it as the header's primary control. */
208
+ .ig-workspace-firstpass {
209
+ margin-left: auto;
210
+ background: var(--ig-accent);
211
+ color: var(--ig-bg);
212
+ border: none;
213
+ border-radius: var(--ig-radius);
214
+ padding: var(--ig-space-tight) var(--ig-space);
215
+ font-family: var(--ig-font-ui);
216
+ font-size: var(--ig-font-size-small);
217
+ cursor: pointer;
218
+ }
219
+
86
220
  .ig-zone[data-zone='rail'] {
87
221
  grid-area: rail;
88
222
  /* The rail scrolls; the window slides underneath. */
@@ -90,31 +224,349 @@ export const workspaceStylesheet = `
90
224
  border-right: var(--ig-stroke) solid var(--ig-line);
91
225
  }
92
226
 
227
+ /* §17a'S RAIL FOOTER, AND IT IS THE ZONE'S LAST ROW. The frame ends the rail
228
+ with it; that it is PINNED is this sheet's decision, on the reasoning §17c
229
+ already settled one zone over — the rail is the scroller, mount.ts restores
230
+ its scrollTop on every redraw, and a fact placed in the scrolled content is a
231
+ fact you have to go back for. .ig-canvas-toolbar below pins itself against
232
+ the other edge for exactly this reason and is the shape copied here.
233
+
234
+ IT ANCHORS ON A SHORT RAIL TOO, WITH NO FLEX LAYOUT, and this is recorded
235
+ because it looks as though it should not. A sticky box is offset within its
236
+ CONTAINING BLOCK to stay in the scrollport, and this zone's containing block
237
+ is the grid area — the full track height — not the height of the rows inside
238
+ it. So content shorter than the track still leaves the footer against the
239
+ bottom edge. Measured: rows 416px in a 445px track, footer flush, no flex
240
+ rule present. A review round proposed a column wrapper with an expanding row
241
+ to "push the footer down"; it was built, measured against this, changed
242
+ nothing, and was removed.
243
+
244
+ OPAQUE, BECAUSE IT OCCLUDES. Rows slide under it, so a transparent ground
245
+ would show the order through the count. That is also why the OPEN LIST IS NOT
246
+ IN HERE: an element taller than its scrollport cannot stick — the browser
247
+ clamps it and it scrolls like any other block — so a footer holding 248
248
+ entries loses the pin exactly when it grows, and covers the whole rail on the
249
+ way. The deeper reason is the rail's VIRTUALIZATION: railRowAt maps a scroll
250
+ offset to a row by floor((scrollTop - chrome) / pitch), so content of
251
+ arbitrary height anywhere in this track makes every offset below it name the
252
+ wrong row. The list is drawn in the canvas zone, which has no such
253
+ arithmetic. */
254
+ .ig-rail-footer {
255
+ position: sticky;
256
+ bottom: 0;
257
+ z-index: 1;
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: space-between;
261
+ gap: var(--ig-space-tight);
262
+ padding: var(--ig-space-tight) var(--ig-space);
263
+ border-top: var(--ig-stroke) solid var(--ig-line);
264
+ background: var(--ig-bg);
265
+ font-family: var(--ig-font-mono);
266
+ font-size: var(--ig-font-size-small);
267
+ line-height: var(--ig-line-height);
268
+ color: var(--ig-text-body);
269
+ }
270
+
271
+ .ig-rail-count {
272
+ color: var(--ig-text);
273
+ }
274
+
275
+ .ig-rail-isolated-toggle {
276
+ font: inherit;
277
+ color: var(--ig-accent);
278
+ background: none;
279
+ border: none;
280
+ padding: 0;
281
+ cursor: pointer;
282
+ }
283
+
284
+ .ig-rail-isolated-toggle:focus-visible {
285
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
286
+ outline-offset: var(--ig-space-tight);
287
+ }
288
+
93
289
  .ig-zone[data-zone='canvas'] {
94
290
  grid-area: canvas;
95
291
  overflow: auto;
292
+ /* THE OVERLAY'S CONTAINING BLOCK. §17d's panel is positioned against THIS
293
+ zone rather than the workspace, and that is what keeps it off the rail
294
+ without a magic offset: the rail is a different grid area, so an absolutely
295
+ positioned child of the canvas cannot reach it however wide it gets.
296
+ RULINGS.md §3 states the constraint and gives the reason — "judging a
297
+ finding is largely a question of what it would do to the order, so the
298
+ order has to stay readable while you judge." */
299
+ position: relative;
300
+ }
301
+
302
+ /* §17f'S CAPTION, AND IT IS THE ZONE'S FIRST ROW. Frame 17a puts it above the
303
+ graph with a rule under it, which is the same shape the header row beside it
304
+ already has — so it takes that row's padding tokens rather than a bespoke
305
+ pair, and the two read as one band across the surface.
306
+
307
+ THE GROUND, NOT THE CARD COLOUR. --ig-surface is what a RAISED element takes
308
+ here: a bordered card, a button. This row is a full-bleed structural band, and
309
+ painting it the panel colour made it read as a card floating on the canvas —
310
+ which is also what the frame does not draw, and what the comment above it
311
+ claimed it was not. --ig-bg is the ground .ig-workspace itself paints and the
312
+ header row inherits, so the two really do match. It has to be OPAQUE either
313
+ way: the row is sticky over a scrolling graph and must occlude it.
314
+
315
+ IT DOES NOT SCROLL WITH THE CANVAS, deliberately. The zone scrolls (above),
316
+ and a caption saying "6 of 312 shown" that scrolls away is a caption you have
317
+ to go back for. A sticky position keeps it against the zone's top edge
318
+ without taking the graph out of the zone's own scroll. */
319
+ .ig-canvas-toolbar {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: var(--ig-space-tight);
323
+ padding: var(--ig-space-tight) var(--ig-space);
324
+ border-bottom: var(--ig-stroke) solid var(--ig-line);
325
+ position: sticky;
326
+ top: 0;
327
+ background: var(--ig-bg);
328
+ z-index: 1;
329
+ }
330
+
331
+ /* MONO AND MUTED, because it is a read-out rather than a heading: the frame
332
+ sets it in the same mono face at the same muted hue as the rail's own footer
333
+ count, and the two are the same kind of fact. */
334
+ .ig-canvas-caption {
335
+ margin: 0;
336
+ font-family: var(--ig-font-mono);
337
+ font-size: var(--ig-font-size-micro);
338
+ color: var(--ig-text-muted);
339
+ min-width: 0;
340
+ overflow: hidden;
341
+ text-overflow: ellipsis;
342
+ white-space: nowrap;
343
+ }
344
+
345
+ /* ITS OWN CLASS, NOT LAYER 1'S .ig-count. They look like the same thing and are
346
+ not: layer 1's is the isolated-issue tally that "sits directly on the panel,
347
+ outside the padded groups, so it carries the panel's own inset" — 12px 20px of
348
+ it. Borrowing the name put that inset around two numbers in the middle of a
349
+ sentence, which is what the frame comparison caught. */
350
+ .ig-canvas-count {
351
+ color: var(--ig-text-body);
352
+ font-variant-numeric: tabular-nums;
353
+ }
354
+
355
+ /* THE CLAUSES DO NOT BREAK INTERNALLY. The caption is one line that ellipsizes
356
+ as a whole; what must not happen is a label parting from the key it names, or
357
+ a count parting from the word that says what it counts. */
358
+ .ig-canvas-focus,
359
+ .ig-canvas-shown {
360
+ white-space: nowrap;
96
361
  }
97
362
 
363
+ /* PUSHED RIGHT BY ITS OWN MARGIN, NOT BY THE ROW'S JUSTIFICATION. The row's
364
+ arity changes: off the direct tier the caption is gone and the pill is the
365
+ only child, and justify-content: space-between puts a lone item at main
366
+ START — so the pill jumped to the left edge on exactly the tiers a large
367
+ backlog is always in, which is the state this surface exists to handle well.
368
+ margin-left: auto is one declaration that is correct at either arity, and it
369
+ is also what leaves #181's projection toggle a real slot: a control added
370
+ ahead of the caption lands in the left group without touching this rule.
371
+
372
+ THE TINT PAIR, NOT A BARE OUTLINE. The tint fill and tint border tokens
373
+ exist for exactly this — "a fill of its own hue inside a heavier border of
374
+ the same hue" — and the frame draws the pill that way. Reaching for the
375
+ accent directly would give a solid chip that reads as loud as the selection.
376
+
377
+ IT NEVER SHRINKS. The caption beside it is the part that may ellipsize; the
378
+ pill is three or four characters and losing them would leave a coloured stub
379
+ saying nothing. */
380
+ .ig-edit-mode {
381
+ flex: 0 0 auto;
382
+ margin-left: auto;
383
+ font-family: var(--ig-font-mono);
384
+ font-size: var(--ig-font-size-pill);
385
+ letter-spacing: var(--ig-tracking-pill);
386
+ color: var(--ig-accent);
387
+ /* THE TINT TOKENS ARE PERCENTAGES, NOT COLOURS — 8% and 30% — so they are only
388
+ meaningful inside color-mix, which is how every other tinted badge in the
389
+ package consumes them. Handed straight to background and border-color they
390
+ are invalid, and an invalid declaration is DROPPED SILENTLY: the pill
391
+ rendered as bare cyan text with no fill and no border, and no test noticed
392
+ because nothing here was a hex literal. */
393
+ background: color-mix(in srgb, var(--ig-accent) var(--ig-tint-fill), transparent);
394
+ border: var(--ig-stroke) solid
395
+ color-mix(in srgb, var(--ig-accent) var(--ig-tint-border), transparent);
396
+ border-radius: var(--ig-radius-small);
397
+ padding: var(--ig-space-micro) var(--ig-space-snug);
398
+ }
399
+
400
+ /* ONE SCROLL TRACK, AND EXACTLY ONE SIBLING IS BOUNDED INSIDE IT. This zone
401
+ holds three: §17d's AUDIT REGION, this column's selection detail, and the
402
+ .ig-chrome the mount appends. #177 settled how they share it, and the whole
403
+ of the answer is that the zone stays ONE track while the region alone takes a
404
+ declared share of it (max-height: 50%, scrolling itself past that).
405
+ .ig-inspector and .ig-chrome keep this track's own scrolling and declare no
406
+ share, so neither can be clipped by any height.
407
+
408
+ THE BOUNDED SIBLING IS A REGION NOW, AND THAT IS #177'S RULE KEPT RATHER THAN
409
+ RESTATED. §17d has two surfaces — the findings panel and, since the fourth
410
+ class moved out of it, the refused block (audit/refused.ts) — and bounding
411
+ each at half the column lets the two of them take ALL of it, which is worse
412
+ than the state #177 fixed rather than a smaller version of it. So
413
+ workspace/render.ts wraps both in .ig-audit-overlay and the share is declared
414
+ once, here, on the thing the zone actually holds. Neither leaf declares a
415
+ size of its own; audit/styles.ts says so at both.
416
+
417
+ AN EARLIER REVISION OF THIS COMMENT CREDITED audit/styles.ts WITH THE SHARE.
418
+ It never carried it — that file states in as many words that the panel
419
+ "DECLARES NO SIZE OF ITS OWN" — and the misattribution is why the fourth
420
+ sibling nearly arrived unbounded.
421
+
422
+ THAT IS A PROPERTY NOW, NOT A LUCKY DEFAULT, and it is what makes the chrome
423
+ safe. A TWO-PANE VERSION WAS TRIED AND REVERTED: making the zone a flex
424
+ column with overflow: hidden did give the audit and the selection independent
425
+ scroll — and it CLIPPED the mount's chrome, a third sibling holding the
426
+ target search's lower matches, the cancel button and the key legend, all of
427
+ which lost the scrolling the zone used to give them. The lesson was read as
428
+ "treat the chrome as a third pane"; it is better read as "do not make the
429
+ zone a multi-pane layout at all". Bounding one sibling needs no panes.
430
+
431
+ WHY THE PANEL IS THE ONE THAT IS BOUNDED. It is the only GLOBAL member of a
432
+ selection-scoped zone, and renderWorkspace draws it FIRST — so unbounded, it
433
+ is the one sibling whose length pushes a detail the reader just asked for
434
+ below the fold. The selection detail and the chrome are both about what the
435
+ reader is doing; the audit is about the document. §17a fixes the workspace at
436
+ three zones plus a header, so there is nowhere else ambient for it to be, and
437
+ a share is what makes it affordable where it has to live. */
98
438
  .ig-zone[data-zone='inspector'] {
99
439
  grid-area: inspector;
100
440
  overflow-y: auto;
101
441
  border-left: var(--ig-stroke) solid var(--ig-line);
102
442
  }
103
443
 
444
+
445
+
446
+ /* THE REGION SCROLLS; ITS MEMBERS DO NOT SHRINK. Both leaves declare
447
+ min-height: 0 — correct for each of them, because each was once the scroll
448
+ container itself — and in a bounded flex column that is exactly the licence
449
+ to squeeze them below their content. Measured in a browser rather than
450
+ reasoned about: with a refusal and five findings the block was laid out 54px
451
+ tall around 131px of content, and since a leaf's own overflow is visible it
452
+ PAINTED OVER the panel underneath it. Nothing threw, no test failed, and the
453
+ markup was correct — the two surfaces were simply drawn on top of each other.
454
+
455
+ flex-shrink RATHER THAN A HEIGHT ON EITHER LEAF, which is the same layering
456
+ the share above follows: how the region's members divide it is a fact about
457
+ the region, and a leaf that sized itself here would be wrong in every other
458
+ composition. The region's own max-height is what bounds the pair; this is
459
+ what makes the overflow it declares actually reachable. */
460
+ .ig-zone[data-zone='canvas'] .ig-audit-overlay > * {
461
+ flex-shrink: 0;
462
+ }
463
+
464
+
465
+ /* §17d'S PANEL, AS AN OVERLAY OVER THE CANVAS.
466
+
467
+ RULINGS.md §3: "a transient overlay anchored to the header count, floating
468
+ over the canvas, 520px wide, dismissed on Escape." It was a child of the
469
+ INSPECTOR zone until now, sharing that column's height with the selection —
470
+ §17a fixes three zones and none of them is the audit's, so it had nowhere to
471
+ go and took a share of the one next to it. At six findings the inspector was
472
+ pushed below the fold.
473
+
474
+ 520px IS DESIGN'S NUMBER, not this package's, which is why it is written as
475
+ one rather than derived from a token. It is expressed in ch units against the
476
+ type scale so it survives a theme that changes the font size, and clamped to
477
+ the zone so a narrow canvas cannot push it over the rail — the constraint §3
478
+ states outright.
479
+
480
+ ANCHORED TOP-RIGHT, under the header count it belongs to. The count sits at
481
+ the right end of the header band, so the panel hangs from that corner and the
482
+ eye travels the shortest distance from the control to what it opened.
483
+
484
+ IT PAINTS OVER THE GRAPH, which is what an overlay is: z-index above the
485
+ canvas content, and its own raised surface and shadow so the graph beneath
486
+ reads as behind rather than as a background it is part of.
487
+
488
+ --ig-elevation-overlay IS THE TOKEN FOR EXACTLY THIS, and the first draft
489
+ wrote a literal rgb() instead. The package's own guard caught it, correctly:
490
+ the theme's two shadows are deliberately the only values carrying a colour,
491
+ so a host retheming owns them and a literal here would be invisible to that.
492
+
493
+ SCOPED TO THE ZONE, and every declaration is in this one rule. Two reasons,
494
+ and the second is the sheet's own standing rule: an absolutely positioned
495
+ .ig-audit-overlay outside this zone has no containing block to hang from and
496
+ would anchor to the page, and the panel is a public export a consumer can
497
+ render standalone. The same argument the inspector's old share rule made. */
498
+ /* BORDER-BOX, AND IT IS LOAD-BEARING. There IS a universal reset in this
499
+ codebase and it does not reach here: viewer/src/styles.ts scopes it to
500
+ .ig-viewer descendants, and this panel is a sibling of the rail's viewer
501
+ rather than inside it. Without it the cap is measured on the CONTENT box and
502
+ the panel's own padding and border push the drawn box past it. Nothing looks
503
+ broken.
504
+
505
+ THE PANEL SCROLLS, NOT THE LIST INSIDE IT. Scrolling the list would pin the
506
+ panel's head, which is the wrong trade twice over. The ambient count 17d
507
+ fixes is the WORKSPACE HEADER's, drawn in the header grid area and outside
508
+ this zone entirely - it already never moves, so a second pinned count buys
509
+ nothing. And the list has zero horizontal padding, so making it the scroll
510
+ container computes its overflow-x to auto and CLIPS the focus ring on every
511
+ card's control: an outline is ink overflow, so it is cut rather than
512
+ scrolled to. Each leaf's own padding gives those rings room. */
513
+ .ig-zone[data-zone='canvas'] .ig-audit-overlay {
514
+ box-sizing: border-box;
515
+ display: flex;
516
+ flex-direction: column;
517
+ min-height: 0;
518
+ overflow-y: auto;
519
+ position: absolute;
520
+ z-index: 3;
521
+ top: var(--ig-space-tight);
522
+ right: var(--ig-space-tight);
523
+ width: min(calc(var(--ig-char-width) * 66.67), calc(100% - var(--ig-space-tight) * 2));
524
+ max-height: calc(100% - var(--ig-space-tight) * 2);
525
+ /* --ig-surface IS THE RAISED ONE. The first draft wrote --ig-raised, which
526
+ does not exist; the sheet's own guard listed it as a property nothing
527
+ resolves, and an unresolved custom property is not an error in CSS — it
528
+ computes to nothing and the panel would have been transparent over the
529
+ graph. */
530
+ background: var(--ig-surface);
531
+ border: var(--ig-stroke) solid var(--ig-line);
532
+ border-radius: var(--ig-radius);
533
+ box-shadow: var(--ig-elevation-overlay);
534
+ }
535
+
104
536
  /* THE AMBIENT LEFT-BAR. A 2px gold rule on the affected row and nothing else —
105
537
  no fill, no icon, no badge. §17a's whole ask is that encoding errors stay
106
538
  visible while you work rather than pulling you out of it.
107
539
 
108
540
  An inset box-shadow rather than a border: a border changes the row's box and
109
541
  would shift every marked row against its neighbours, which is a layout jump
110
- the eye reads as movement. The width is the theme's spine, so the bar scales
111
- with the rest of the rail. */
112
- .ig-zone[data-zone='rail'] [data-ig-audit] {
113
- box-shadow: inset var(--ig-spine-width) 0 0 0 var(--ig-accent);
114
- }
542
+ the eye reads as movement.
543
+
544
+ IT DREW A 330px CYAN BLOCK UNTIL #122, and the comment above it — unchanged
545
+ here because it was right — is how the defect survived. --ig-spine-width is
546
+ §16b's SPINE CARD WIDTH (330), not a stroke; the sentence "the width is the
547
+ theme's spine, so the bar scales with the rest of the rail" reads as a
548
+ scaling argument and is a token mix-up. --ig-accent is the cyan the panel
549
+ spends once on the number an operator acts on, and it is not the attention
550
+ colour. This rule is more specific than audit/styles.ts's correct one, so
551
+ it silently won.
115
552
 
116
- .ig-zone[data-zone='rail'] [data-ig-audit='misleading'] {
117
- box-shadow: inset var(--ig-spine-width) 0 0 0 var(--ig-state-invalid);
553
+ AND ITS TEST PASSED THROUGHOUT. styles.test.ts matched the rule and
554
+ asserted it EXISTED; it never asserted what the declaration resolves to, so
555
+ it was green about a string rather than about a property. The test below it
556
+ now reads the values.
557
+
558
+ ONE TREATMENT FOR ALL FOUR SEVERITIES, which is also a correction. §17d
559
+ names exactly one ambient mark — "a 2px gold left-bar on the affected rail
560
+ row" — and puts the severity distinction in the findings list, which
561
+ audit/panel.ts now draws. (It read "which is host-side" until that panel
562
+ existed; the reasoning is unaffected — the distinction still belongs to the
563
+ list rather than to this bar — but the list is the package's now.)
564
+
565
+ The misleading variant this replaces gave the LEAST urgent
566
+ severity ("clearing is bookkeeping, not urgency") the alarm colour while
567
+ every other finding got cyan, inverting the design it came from. */
568
+ .ig-zone[data-zone='rail'] [data-ig-audit] {
569
+ box-shadow: inset var(--ig-stroke-audit) 0 0 0 var(--ig-edge-serialize-with);
118
570
  }
119
571
 
120
572
  /* THE ROWS OUTSIDE THE WINDOW, AS HEIGHT. The rail scrolls, so without a spacer
@@ -142,9 +594,16 @@ export const workspaceStylesheet = `
142
594
  --ig-rail-rows is the count, set inline by the renderer. What remains
143
595
  approximate is only VARIABLE row height — a row carrying holds is taller than
144
596
  a bare one — so the scrollbar stays proportional rather than exact. Measuring
145
- that needs a mount, which this package does not have. */
597
+ that needs a mount, which this package does not have.
598
+
599
+ THE PITCH IS THE FLOOR ALONE NOW. Layer 1's section 16 pass separates its rows
600
+ with the panel's own hairlines instead of a margin — a row is
601
+ --ig-row-min-height of content and a hairline, with no gap to add — so the sum
602
+ this used to take double-counted a gap that is no longer there. The row is
603
+ content-sized above that floor, which is the same approximation the note above
604
+ already describes, at a slightly larger typical size. */
146
605
  .ig-rail-spacer {
147
- height: calc((var(--ig-row-height) + var(--ig-space-tight)) * var(--ig-rail-rows, 0));
606
+ height: calc(var(--ig-row-min-height) * var(--ig-rail-rows, 0));
148
607
  }
149
608
 
150
609
  .ig-inspector {
@@ -154,12 +613,28 @@ export const workspaceStylesheet = `
154
613
  padding: var(--ig-space);
155
614
  }
156
615
 
157
- .ig-inspector-empty {
616
+ /* TWO EMPTIES, ONE TREATMENT. "Nothing is selected" and "this issue is related
617
+ to nothing" are different facts and the panel states each in its own place —
618
+ but they are the same KIND of statement, a quiet line where content would be,
619
+ and giving them separate looks would suggest a distinction that is not
620
+ there. */
621
+ .ig-inspector-empty,
622
+ .ig-inspector-none {
158
623
  margin: 0;
159
624
  color: var(--ig-text-muted);
160
625
  font-size: var(--ig-font-size-small);
161
626
  }
162
627
 
628
+ /* The panel's heading row: the name at one end, the way out at the other.
629
+ Baseline-aligned rather than centred, so a heading in caps and a button in
630
+ sentence case sit on one line rather than on two optical ones. */
631
+ .ig-inspector-head {
632
+ display: flex;
633
+ align-items: baseline;
634
+ justify-content: space-between;
635
+ gap: var(--ig-space-tight);
636
+ }
637
+
163
638
  .ig-inspector-issue {
164
639
  display: flex;
165
640
  flex-direction: column;
@@ -172,21 +647,6 @@ export const workspaceStylesheet = `
172
647
  color: var(--ig-text);
173
648
  }
174
649
 
175
- .ig-inspector-key {
176
- font-family: var(--ig-font-mono);
177
- color: var(--ig-text-muted);
178
- }
179
-
180
- .ig-inspector-position {
181
- margin: 0;
182
- font-family: var(--ig-font-mono);
183
- color: var(--ig-text-muted);
184
- }
185
-
186
- .ig-inspector-position[data-ready='true'] {
187
- color: var(--ig-station-ready);
188
- }
189
-
190
650
  .ig-inspector-holds {
191
651
  list-style: none;
192
652
  margin: 0;
@@ -232,14 +692,75 @@ export const workspaceStylesheet = `
232
692
  gap: var(--ig-space-tight);
233
693
  }
234
694
 
235
- .ig-inspector-heading {
695
+ /* §17a PUTS '+ add' ON THE HEADING'S ROW, opposite the heading. The list
696
+ between them is unbounded, so a control after it drifts down the panel as a
697
+ subject gains relationships; here its position does not depend on content. */
698
+ .ig-inspector-relationships-head {
699
+ display: flex;
700
+ align-items: baseline;
701
+ justify-content: space-between;
702
+ gap: var(--ig-space-tight);
703
+ }
704
+
705
+ /* SECTION 17a DRAWS EVERY INSPECTOR HEADING IN CAPS -- WHY RANK 2,
706
+ RELATIONSHIPS, ADD RELATIONSHIP -- so the treatment is shared rather than
707
+ spelled per heading. Tracking widens with the caps because letterforms at a
708
+ small size need it; that is what --ig-tracking-label is for. */
709
+ .ig-inspector-heading,
710
+ .ig-inspector-name,
711
+ .ig-why-rank-heading {
236
712
  margin: 0;
237
713
  font-size: var(--ig-font-size-small);
238
714
  color: var(--ig-text-muted);
715
+ text-transform: uppercase;
716
+ letter-spacing: var(--ig-tracking-label);
239
717
  }
240
718
 
241
- .ig-inspector-clear {
242
- align-self: flex-start;
719
+ /* THE EXPLANATION, SECTION 17a's WHOLE POINT FOR THIS ZONE. The clauses are
720
+ inline spans so the block reads as one sentence rather than a list of
721
+ findings: the reader asked one question. */
722
+ .ig-why-rank {
723
+ display: flex;
724
+ flex-direction: column;
725
+ gap: var(--ig-space-tight);
726
+ }
727
+
728
+ .ig-why-rank-sentence {
729
+ margin: 0;
730
+ color: var(--ig-text-body);
731
+ }
732
+
733
+ /* Each clause keeps a space after it so the sentence does not run together.
734
+ A margin rather than a literal space in the markup: the text is host-authored
735
+ and the package does not add characters to it. */
736
+ .ig-why-rank-sentence > * + * {
737
+ margin-left: var(--ig-space-micro);
738
+ }
739
+
740
+
741
+ /* THE PANEL'S TWO QUIET BUTTONS SHARE ONE TREATMENT — clear and cancel. They
742
+ are the same affordance at two moments (leave this selection, abandon the
743
+ draft) and drawing them two ways would invent a hierarchy the design does
744
+ not state.
745
+
746
+ '+ add' WAS THE THIRD AND HAS LEFT THE SET, because frame 17a moved it and
747
+ redrew it: on the relationships header row it is unbordered accent text with
748
+ the key beside it, not a filled box. That is a hierarchy the design DOES
749
+ state — a header affordance reads differently from a control in the panel
750
+ body — so matching it here is following the frame rather than inventing a
751
+ difference. Its focus ring is still shared, below. The destructive controls are
752
+ deliberately NOT in this set: a row's '✕' is a glyph in the row's own slot,
753
+ and the mount's delete button carries its own danger treatment.
754
+
755
+ ALIGNMENT IS NOT PART OF THE TREATMENT, and putting it here was a silent
756
+ contradiction. 'align-self: flex-start' belongs to the two buttons in
757
+ '.ig-inspector-add', a COLUMN, where without it they stretch to the panel's
758
+ full width. The clear button sits in '.ig-inspector-head', a baseline row —
759
+ and 'align-self' beats the container's 'align-items', so it overrode the
760
+ baseline that rule's own comment says it is there to get. Declared where each
761
+ is true instead. */
762
+ .ig-inspector-clear,
763
+ .ig-inspector-cancel {
243
764
  background: var(--ig-surface-2);
244
765
  color: var(--ig-text-body);
245
766
  border: var(--ig-stroke) solid var(--ig-line);
@@ -250,11 +771,45 @@ export const workspaceStylesheet = `
250
771
  cursor: pointer;
251
772
  }
252
773
 
253
- .ig-inspector-clear:focus-visible {
774
+ .ig-inspector-clear:focus-visible,
775
+ .ig-inspector-addbutton:focus-visible,
776
+ .ig-inspector-cancel:focus-visible {
777
+ /* '+ add' keeps the shared focus ring though it left the shared fill: a
778
+ focus indicator is an accessibility guarantee, not a visual register. */
254
779
  outline: var(--ig-focus-ring) solid var(--ig-focus);
255
780
  outline-offset: var(--ig-space-tight);
256
781
  }
257
782
 
783
+ /* §17a's '+ add', AS THE FRAME DRAWS IT: accent text on the header row, no box.
784
+ It is a header affordance rather than a control in the panel body, and the
785
+ frame states that difference by removing the border and the fill rather than
786
+ by moving it alone. */
787
+ .ig-inspector-addbutton {
788
+ display: inline-flex;
789
+ align-items: baseline;
790
+ gap: var(--ig-space-micro);
791
+ background: none;
792
+ border: none;
793
+ border-radius: var(--ig-radius);
794
+ padding: 0;
795
+ color: var(--ig-accent);
796
+ font-family: var(--ig-font-ui);
797
+ font-size: var(--ig-font-size-small);
798
+ cursor: pointer;
799
+ }
800
+
801
+ /* THE KEY, QUIETER THAN THE ACT IT NAMES. It is a hint about how to reach the
802
+ control, not part of the control's label, so it takes the muted tone the
803
+ panel gives every line that states a fact rather than offering an act.
804
+ UPPERCASED HERE, NEVER IN THE MARKUP: the binding table stores the lowercase
805
+ key a press normalizes to, and drawing 'R' is presentation. A second,
806
+ uppercase copy in the package would be the drift RELATE_KEY exists to end. */
807
+ .ig-inspector-addkey {
808
+ color: var(--ig-text-muted);
809
+ font-family: var(--ig-font-mono);
810
+ text-transform: uppercase;
811
+ }
812
+
258
813
  .ig-relationship-list {
259
814
  list-style: none;
260
815
  margin: 0;
@@ -264,8 +819,21 @@ export const workspaceStylesheet = `
264
819
  gap: var(--ig-space-tight);
265
820
  }
266
821
 
822
+ /* IT WRAPS RATHER THAN TRUNCATES, which is '.ig-picker-direction''s rule
823
+ inherited along with §17b's statement. A qualified reference is long —
824
+ 'owner/repo#602' — the inspector is a fixed 40-character track with no
825
+ horizontal scroll, and a row that cannot wrap pushes its trailing controls
826
+ out of the zone entirely. A statement whose object is cut off says something
827
+ other than what the format holds, and a flip a reader cannot reach is the
828
+ affordance this row exists to offer.
829
+
830
+ ON THE ROW AND ON THE STATEMENT BOTH. The row wraps so the slot and the flip
831
+ drop to a second line rather than overflow; '.ig-relationship-select' below
832
+ wraps so the reference pair does the same inside it. Wrapping one without the
833
+ other leaves the other overflowing. */
267
834
  .ig-relationship {
268
835
  display: flex;
836
+ flex-wrap: wrap;
269
837
  gap: var(--ig-space-tight);
270
838
  align-items: baseline;
271
839
  padding: var(--ig-space-tight);
@@ -274,37 +842,70 @@ export const workspaceStylesheet = `
274
842
  background: var(--ig-surface);
275
843
  }
276
844
 
277
- /* Hue by field, from the theme's own edge tokens — the same channel layer 1
278
- uses, so a relationship reads the same colour in the list as on the canvas. */
279
- .ig-relationship[data-edge='blocked-by'] {
845
+ /* HUE BY FIELD, ONCE, FOR EVERY ELEMENT THAT CARRIES 'data-edge'. From the
846
+ theme's own edge tokens — the same channel layer 1 uses — so a relationship
847
+ reads the same colour in the row and in the kind-list entry it was chosen
848
+ from as it does on the canvas. The refusal capsule is deliberately NOT in
849
+ this set: its whole border is the invalid state's dotted stroke, and a hue on
850
+ its left edge would overwrite the half of that a reader looks for first.
851
+
852
+ WRITTEN PER FIELD RATHER THAN FROM A 'data-edge'-KEYED TOKEN, because CSS has
853
+ no way to build 'var(--ig-edge-' + attr() + ')': 'attr()' is usable in
854
+ 'content' and nowhere a custom property name is read. Five rules is the cost
855
+ of that, and the alternative the package rejected is a token per hue set
856
+ inline by the renderer, which moves a theme value into markup.
857
+
858
+ ONE SELECTOR LIST PER FIELD, AND THAT IS THE POINT OF THIS BLOCK. The kind
859
+ list arrived with a verbatim second copy of these five rules under
860
+ '.ig-kind-option' — same five fields, same five tokens — under a comment
861
+ claiming the file should not hold two ways of reading the theme while it
862
+ held the same way twice. A sixth field is now one line here, not two. */
863
+ .ig-relationship[data-edge='blocked-by'],
864
+ .ig-kind-option[data-edge='blocked-by'] {
280
865
  border-left-color: var(--ig-edge-blocked-by);
281
866
  }
282
867
 
283
- .ig-relationship[data-edge='duplicate-of'] {
868
+ .ig-relationship[data-edge='duplicate-of'],
869
+ .ig-kind-option[data-edge='duplicate-of'] {
284
870
  border-left-color: var(--ig-edge-duplicate-of);
285
871
  }
286
872
 
287
- .ig-relationship[data-edge='serialize-with'] {
873
+ .ig-relationship[data-edge='serialize-with'],
874
+ .ig-kind-option[data-edge='serialize-with'] {
288
875
  border-left-color: var(--ig-edge-serialize-with);
289
876
  }
290
877
 
291
- .ig-relationship[data-edge='together-with'] {
878
+ .ig-relationship[data-edge='together-with'],
879
+ .ig-kind-option[data-edge='together-with'] {
292
880
  border-left-color: var(--ig-edge-together-with);
293
881
  }
294
882
 
295
- .ig-relationship[data-edge='decomposed-from'] {
883
+ .ig-relationship[data-edge='decomposed-from'],
884
+ .ig-kind-option[data-edge='decomposed-from'] {
296
885
  border-left-color: var(--ig-edge-decomposed-from);
297
886
  }
298
887
 
888
+ /* THE DESCRIPTION'S BOX, WHETHER OR NOT A CONTROL IS WRAPPING IT. A row's head
889
+ is a button and a phantom capsule's is a plain span -- 'render.ts' withholds
890
+ the selector for an edge the landed document does not carry -- and the two
891
+ occupy the row identically: they fill it, so the reason wraps under them
892
+ rather than squeezing the reference out, and their parts sit on one baseline.
893
+ Declared once, so the capsule cannot come to sit differently from the row it
894
+ stands in for. */
895
+ .ig-relationship-select,
896
+ .ig-relationship-name {
897
+ flex: 1;
898
+ display: flex;
899
+ flex-wrap: wrap;
900
+ gap: var(--ig-space-tight);
901
+ align-items: baseline;
902
+ }
903
+
299
904
  /* The button fills its row, so the whole relationship is the hit target rather
300
905
  than the words inside it. Transparent and borderless: the li already carries
301
906
  the border and the hue, and a second box around it would read as two
302
907
  controls. */
303
908
  .ig-relationship-select {
304
- flex: 1;
305
- display: flex;
306
- gap: var(--ig-space-tight);
307
- align-items: baseline;
308
909
  background: none;
309
910
  border: none;
310
911
  padding: 0;
@@ -320,8 +921,15 @@ export const workspaceStylesheet = `
320
921
  outline-offset: var(--ig-space-tight);
321
922
  }
322
923
 
924
+ /* The glyph and its word are one phrase, so they get their own gap rather than
925
+ inheriting the row's — which spaces the phrase from the reference beside it,
926
+ a wider relationship. NOT a literal space character in the markup: the word
927
+ is the vocabulary's and this package does not add characters to it, which is
928
+ the same rule the why-rank sentence's clauses are spaced by. */
323
929
  .ig-relationship-kind {
324
- font-family: var(--ig-font-mono);
930
+ display: inline-flex;
931
+ gap: var(--ig-space-micro);
932
+ align-items: baseline;
325
933
  color: var(--ig-text-body);
326
934
  }
327
935
 
@@ -329,5 +937,760 @@ export const workspaceStylesheet = `
329
937
  font-family: var(--ig-font-mono);
330
938
  color: var(--ig-text-muted);
331
939
  }
940
+
941
+ /* THE RIGHT-HAND SLOT, WHOSE THREE OCCUPANTS ARE EXCLUSIVE. Two of them are
942
+ statements and one is a control, and they are drawn as such: the state and
943
+ the inbound marker are quiet label text, the remove is a button.
944
+ '.ig-relationship-flip' below is NOT one of the three: it sits beside the
945
+ slot on the selected row, because it is neither a statement about the row nor
946
+ a destructive act on it. */
947
+ .ig-relationship-state,
948
+ .ig-relationship-inbound {
949
+ align-self: center;
950
+ color: var(--ig-text-muted);
951
+ font-size: var(--ig-font-size-small);
952
+ text-transform: uppercase;
953
+ letter-spacing: var(--ig-tracking-label);
954
+ }
955
+
956
+ /* The selected row names the state the canvas is drawing a halo for, so it
957
+ takes the focus hue rather than the muted one — one selection, one colour,
958
+ across the zones. */
959
+ .ig-relationship-state[data-ig-state='selected'] {
960
+ color: var(--ig-focus);
961
+ }
962
+
963
+ /* A GLYPH BUTTON, SIZED BY ITS GLYPH. No border and no fill: the row already
964
+ carries a border, and a second box inside it would read as two controls —
965
+ the same reasoning '.ig-relationship-select' records. */
966
+ .ig-relationship-remove {
967
+ align-self: center;
968
+ background: none;
969
+ border: none;
970
+ padding: 0 var(--ig-space-tight);
971
+ color: var(--ig-text-muted);
972
+ font-family: inherit;
973
+ font-size: inherit;
974
+ line-height: 1;
975
+ cursor: pointer;
976
+ }
977
+
978
+ .ig-relationship-remove:focus-visible {
979
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
980
+ outline-offset: var(--ig-space-tight);
981
+ }
982
+
983
+ /* §17b's FLIP, AT THE ROW'S END. It is drawn last and needs no margin of its
984
+ own to get there: '.ig-relationship-select' above carries 'flex: 1', so the
985
+ statement takes the free space and everything after it is already pushed to
986
+ the end — which is where frame 17b draws this control. A 'margin-left: auto'
987
+ here would be inert, and a comment crediting it would be an explanation that
988
+ outlived the thing it explained.
989
+
990
+ A BORDERED CONTROL RATHER THAN THE REMOVE'S BARE GLYPH, which is the frame's
991
+ own distinction and not decoration: '✕' is a mark a reader recognises without
992
+ a box, and 'flip' is a word that would otherwise read as part of the sentence
993
+ it sits beside. The border is what says it is pressable.
994
+
995
+ ITS FOCUS RING IS NOT OPTIONAL, and it takes the offset the row's OTHER two
996
+ controls already use rather than the one it was born with. This rule came
997
+ from the picker's sheet, where the ring sat a hairline out;
998
+ '.ig-relationship-select' and '.ig-relationship-remove' share this row and
999
+ both ring at '--ig-space-tight', and one control ringing tighter than its
1000
+ neighbours reads as a rendering fault rather than as a distinction. This is
1001
+ the control §17b calls the guard against the most common encoding mistake; a
1002
+ keyboard reader who cannot see where they are has it and not its affordance. */
1003
+ .ig-relationship-flip {
1004
+ align-self: center;
1005
+ background: var(--ig-surface);
1006
+ color: var(--ig-text-body);
1007
+ border: var(--ig-stroke) solid var(--ig-line);
1008
+ border-radius: var(--ig-radius);
1009
+ padding: 0 var(--ig-space-tight);
1010
+ font-family: inherit;
1011
+ font-size: var(--ig-font-size-small);
1012
+ line-height: inherit;
1013
+ cursor: pointer;
1014
+ }
1015
+
1016
+ .ig-relationship-flip:focus-visible {
1017
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
1018
+ outline-offset: var(--ig-space-tight);
1019
+ }
1020
+
1021
+ /* A REFUSED EDIT, IN THE ROW IT WOULD HAVE BEEN. It borrows the ghost the
1022
+ canvas draws for the same state — a dotted edge in the invalid hue — so the
1023
+ line and the row a reader looks between are recognisably one fact.
1024
+ NO OPACITY HERE. 'overlay/grammar.ts' holds the opacity every write state is
1025
+ drawn at, on the record that a second copy would drift; what this sheet
1026
+ contributes is the structure. */
1027
+ .ig-relationship-refused {
1028
+ display: flex;
1029
+ flex-wrap: wrap;
1030
+ gap: var(--ig-space-tight);
1031
+ align-items: baseline;
1032
+ padding: var(--ig-space-tight);
1033
+ border: var(--ig-stroke) dotted var(--ig-state-invalid);
1034
+ border-radius: var(--ig-radius);
1035
+ background: var(--ig-surface);
1036
+ }
1037
+
1038
+ .ig-relationship-reason {
1039
+ color: var(--ig-state-invalid);
1040
+ font-size: var(--ig-font-size-small);
1041
+ }
1042
+
1043
+ /* A REFUSAL ON A RELATIONSHIP THAT IS STILL THERE. The row stays — it names a
1044
+ real edge and its remove control still works — so this marks it rather than
1045
+ replacing it: the capsule's dotted stroke, and room for the reason to wrap
1046
+ under the row rather than squeezing the reference out of it.
1047
+ BORDER-STYLE ONLY, NOT BORDER-COLOR: the field's hue is on the left edge from
1048
+ the block above, and a shorthand here would overwrite it and take the row's
1049
+ one colour channel away on exactly the rows carrying the most information. */
1050
+ .ig-relationship[data-ig-code] {
1051
+ flex-wrap: wrap;
1052
+ border-style: dotted;
1053
+ }
1054
+
1055
+ /* THE KIND STEP'S OWN BOX — the heading, the numbered list and the cancel that
1056
+ withdraws from it.
1057
+
1058
+ IT NO LONGER HOLDS '+ add'. This box was once "one place the reader returns
1059
+ to", holding whichever of the two create steps was live; frame 17a splits
1060
+ them by zone, putting '+ add' on the relationships header row and leaving the
1061
+ kinds here under a heading of their own. The two steps are still exclusive —
1062
+ 'createStep' answers one of them — but they are no longer drawn in one
1063
+ place, so this rule is about the kind step alone. */
1064
+ .ig-inspector-add {
1065
+ display: flex;
1066
+ flex-direction: column;
1067
+ gap: var(--ig-space-tight);
1068
+ }
1069
+
1070
+ /* Sized by its own word rather than stretched across the column — see the
1071
+ quiet-button rule above, which deliberately does not carry this.
1072
+
1073
+ '+ add' IS NO LONGER LISTED HERE, and dropping it was required rather than
1074
+ tidy. This declaration is scoped to '.ig-inspector-add', a COLUMN; '+ add'
1075
+ now sits in the relationships header, a ROW, where 'align-self' beats the
1076
+ container's 'align-items' and would top-align the control against the
1077
+ heading — the exact bug the '.ig-inspector-clear' comment above records
1078
+ having already been fixed once. */
1079
+ .ig-inspector-cancel {
1080
+ align-self: flex-start;
1081
+ }
1082
+
1083
+ /* WHOSE DRAFT THIS IS, drawn only when it is not the panel's subject. Quiet and
1084
+ small, like every other line that states a fact rather than offering an act:
1085
+ it is a caption on the step below it, not a heading over it. The reference
1086
+ inside keeps layer 1's '.ig-id' treatment, so the issue reads the same here
1087
+ as it does in the why-rank sentence. */
1088
+ .ig-inspector-source {
1089
+ margin: 0;
1090
+ color: var(--ig-text-muted);
1091
+ font-size: var(--ig-font-size-small);
1092
+ }
1093
+
1094
+ .ig-kind-list {
1095
+ list-style: none;
1096
+ margin: 0;
1097
+ padding: 0;
1098
+ display: flex;
1099
+ flex-direction: column;
1100
+ gap: var(--ig-space-micro);
1101
+ }
1102
+
1103
+ /* Full width, so the whole entry is the hit target.
1104
+
1105
+ THE DIGIT IS A CHIP AT THE ROW'S END, which is frame 17a's device and
1106
+ REVERSES the placement #144 shipped. That decision was made in the same §17a
1107
+ pass and with this frame available, so it is overturned on the merits rather
1108
+ than as an oversight: what made the old left-hand column read as a set of
1109
+ KEYS was vertical alignment, which a label long enough to wrap breaks, while
1110
+ a bordered chip carries the key-cap signal on each row by itself and cannot
1111
+ come apart. Reversing a same-campaign decision is called out on the pull
1112
+ request so autnmy/issuegraph#147 rules rather than the implementer.
1113
+
1114
+ CENTRED, NOT BASELINE-ALIGNED. A chip has its own border and padding, so on a
1115
+ baseline it sits low against the glyph and the label; the frame's row centres
1116
+ all three. */
1117
+ .ig-kind-option {
1118
+ width: 100%;
1119
+ display: flex;
1120
+ gap: var(--ig-space-tight);
1121
+ align-items: center;
1122
+ background: var(--ig-surface);
1123
+ color: var(--ig-text-body);
1124
+ border: var(--ig-stroke) solid var(--ig-line);
1125
+ border-radius: var(--ig-radius);
1126
+ padding: var(--ig-space-tight);
1127
+ font-family: inherit;
1128
+ font-size: var(--ig-font-size-small);
1129
+ text-align: left;
1130
+ cursor: pointer;
1131
+ }
1132
+
1133
+ .ig-kind-option:focus-visible {
1134
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
1135
+ outline-offset: var(--ig-space-tight);
1136
+ }
1137
+
1138
+ /* THE CHIP TAKES THE SLACK BEFORE IT, rather than the label being told to grow.
1139
+ The label is drawn by layer 1's shared 'glyphAndLabel' and carries no class of
1140
+ its own, so selecting it from here means selecting by exclusion — a rule that
1141
+ silently stops matching the day that helper adds an element. An auto margin
1142
+ is stated on the element this rule already owns, needs nothing of the label,
1143
+ and puts the chip at the row's end whatever precedes it. */
1144
+ .ig-kind-digit {
1145
+ flex: 0 0 auto;
1146
+ margin-left: auto;
1147
+ border: var(--ig-stroke) solid var(--ig-line);
1148
+ border-radius: var(--ig-radius);
1149
+ padding: 0 var(--ig-space-micro);
1150
+ font-family: var(--ig-font-mono);
1151
+ color: var(--ig-text-muted);
1152
+ }
1153
+
1154
+ /* §17b's RECOVERY CARDS. An unsettled write the reader can still act on, drawn
1155
+ where they made it.
1156
+ THE HUE COMES FROM THE STATE, through the same custom properties the canvas
1157
+ draws the line with — so a card and the edge it is about are recognisably one
1158
+ fact, and a host retheming the palette moves both at once. The tokens are
1159
+ named per state below rather than set once, because '--ig-state-failed' and
1160
+ '--ig-state-conflict' are different channels and a shared variable here would
1161
+ be a third place the pairing could drift.
1162
+ NO OPACITY, for the reason the refusal capsule states: 'overlay/grammar.ts'
1163
+ holds what each write state is drawn at, and this sheet contributes structure. */
1164
+ .ig-recovery-region {
1165
+ display: flex;
1166
+ flex-direction: column;
1167
+ gap: var(--ig-space-tight);
1168
+ margin-top: var(--ig-space);
1169
+ }
1170
+
1171
+ .ig-recovery-list {
1172
+ list-style: none;
1173
+ margin: 0;
1174
+ padding: 0;
1175
+ display: flex;
1176
+ flex-direction: column;
1177
+ gap: var(--ig-space-tight);
1178
+ }
1179
+
1180
+ .ig-recovery {
1181
+ display: flex;
1182
+ flex-direction: column;
1183
+ gap: var(--ig-space-tight);
1184
+ padding: var(--ig-space-tight);
1185
+ border-radius: var(--ig-radius);
1186
+ background: var(--ig-surface);
1187
+ border: var(--ig-stroke) solid var(--ig-line);
1188
+ }
1189
+
1190
+ /* GHOST + THE ✕ THE CANVAS DRAWS. §17b gives a failed write a ghosted line with
1191
+ a cross terminal; the card carries the same hue so the two read together. */
1192
+ .ig-recovery[data-ig-state='failed'] {
1193
+ border-color: var(--ig-state-failed);
1194
+ border-style: dashed;
1195
+ }
1196
+
1197
+ /* THE GOLD DOUBLE RULE. §17b draws a conflict as a doubled line holding two
1198
+ versions. 'border-style: double' is the card's half of that grammar — the
1199
+ LINE's half is the 'second-version' mark, which needs a position this layer
1200
+ does not have and is issue #102's. */
1201
+ .ig-recovery[data-ig-state='conflict'] {
1202
+ border-color: var(--ig-state-conflict);
1203
+ border-style: double;
1204
+ border-width: calc(var(--ig-stroke) * 3);
1205
+ }
1206
+
1207
+ .ig-recovery-name {
1208
+ margin: 0;
1209
+ font-size: var(--ig-font-size-small);
1210
+ font-weight: 600;
1211
+ }
1212
+
1213
+ .ig-recovery[data-ig-state='failed'] .ig-recovery-name {
1214
+ color: var(--ig-state-failed);
1215
+ }
1216
+
1217
+ .ig-recovery[data-ig-state='conflict'] .ig-recovery-name {
1218
+ color: var(--ig-state-conflict);
1219
+ }
1220
+
1221
+ .ig-recovery-reason,
1222
+ .ig-recovery-refresh-error {
1223
+ margin: 0;
1224
+ font-size: var(--ig-font-size-small);
1225
+ color: var(--ig-text-muted);
1226
+ }
1227
+
1228
+ .ig-recovery-refresh-detail {
1229
+ font-family: var(--ig-font-mono);
1230
+ }
1231
+
1232
+ /* THE THREE RESOLUTIONS, IN A ROW AND NEVER IN A MENU. §17b lists them as
1233
+ peers; nesting one behind a disclosure would rank them, and the one that
1234
+ would end up hidden is 'view diff' — the one that lets a reader decide
1235
+ between the other two. */
1236
+ .ig-recovery-actions {
1237
+ display: flex;
1238
+ flex-wrap: wrap;
1239
+ gap: var(--ig-space-tight);
1240
+ }
1241
+
1242
+ .ig-recovery-action {
1243
+ background: var(--ig-surface);
1244
+ color: var(--ig-text-body);
1245
+ border: var(--ig-stroke) solid var(--ig-line);
1246
+ border-radius: var(--ig-radius);
1247
+ padding: var(--ig-space-tight);
1248
+ font-family: inherit;
1249
+ font-size: var(--ig-font-size-small);
1250
+ cursor: pointer;
1251
+ }
1252
+
1253
+ .ig-recovery-action:focus-visible {
1254
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
1255
+ outline-offset: var(--ig-space-tight);
1256
+ }
1257
+
1258
+ .ig-recovery-action[aria-pressed='true'] {
1259
+ border-color: var(--ig-focus);
1260
+ }
1261
+
1262
+ /* TWO SIDES, SIDE BY SIDE, AND NOTHING BETWEEN THEM THAT COMBINES THEM. The
1263
+ layout is the design constraint: there is no column for a merged result,
1264
+ because there is no merged result to put in one. */
1265
+ .ig-recovery-diff {
1266
+ display: flex;
1267
+ flex-direction: column;
1268
+ gap: var(--ig-space-tight);
1269
+ padding-top: var(--ig-space-tight);
1270
+ border-top: var(--ig-stroke) solid var(--ig-line);
1271
+ }
1272
+
1273
+ /* ONE SIDE OF THE DIFFERENCE. Stacked rather than columned: at a panel's width
1274
+ two columns give each side about half of an already narrow zone, and a
1275
+ relationship row is a sentence rather than a cell. */
1276
+ .ig-recovery-side {
1277
+ display: flex;
1278
+ flex-direction: column;
1279
+ gap: var(--ig-space-tight);
1280
+ }
1281
+
1282
+ .ig-recovery-side-name {
1283
+ margin: 0;
1284
+ font-size: var(--ig-font-size-small);
1285
+ color: var(--ig-text-muted);
1286
+ font-weight: 400;
1287
+ }
1288
+
1289
+ .ig-recovery-edges,
1290
+ .ig-recovery-issues {
1291
+ list-style: none;
1292
+ margin: 0;
1293
+ padding: 0;
1294
+ display: flex;
1295
+ flex-direction: column;
1296
+ gap: var(--ig-space-tight);
1297
+ }
1298
+
1299
+ .ig-recovery-edge,
1300
+ .ig-recovery-issue {
1301
+ display: flex;
1302
+ flex-wrap: wrap;
1303
+ gap: var(--ig-space-tight);
1304
+ align-items: baseline;
1305
+ font-size: var(--ig-font-size-small);
1306
+ }
1307
+
1308
+ /* BOTH TITLES, NEITHER STRUCK THROUGH. A strike-out would say the old one is
1309
+ gone, which is exactly the judgement this card must not make for the reader. */
1310
+ .ig-recovery-was,
1311
+ .ig-recovery-now {
1312
+ color: var(--ig-text-muted);
1313
+ }
1314
+
1315
+ .ig-recovery-now {
1316
+ color: var(--ig-state-conflict);
1317
+ }
1318
+
1319
+ .ig-recovery-diff-empty {
1320
+ margin: 0;
1321
+ padding-top: var(--ig-space-tight);
1322
+ border-top: var(--ig-stroke) solid var(--ig-line);
1323
+ font-size: var(--ig-font-size-small);
1324
+ color: var(--ig-text-muted);
1325
+ }
1326
+
1327
+ /* §17c ON THE ROW: the tint, the third column, and the chip in place.
1328
+
1329
+ THE TINT IS DRAWN FROM AN ATTRIBUTE rather than from a class the row is
1330
+ given, which is audit/surface.ts's precedent and its reason: layer 1 owns
1331
+ the row's markup, so layer 2 says WHICH KIND and the stylesheet says what
1332
+ that kind looks like. A row the edit did not touch carries no attribute at
1333
+ all, so none of these rules can reach it, and "unaffected rows are left
1334
+ completely alone" is structural rather than asserted.
1335
+
1336
+ THESE REACH LAYER 1'S OWN CLASSES, and that is a deliberate exception of
1337
+ exactly the shape reevaluate/styles.ts already declares for the greyed rail:
1338
+ only from INSIDE this surface's own root, and only for a state layer 1 does
1339
+ not model. The viewer draws a row; whether the last edit moved that row is a
1340
+ fact only the store knows, so the mark belongs to whoever knows it.
1341
+
1342
+ BOTH ROW SHAPES, because a delta can land on either. A ranked row is an
1343
+ .ig-slot; an excluded one — the shape an issue takes when an edit turns it
1344
+ into a duplicate — is an .ig-footer-row, and it is still navigable, so
1345
+ reevaluateView still places its chip. An earlier revision listed .ig-slot
1346
+ only, so a left row got its chip and its extended name and no ground.
1347
+
1348
+ EVERY VALUE deltaKind CAN EMIT, and that list is readiness, then a
1349
+ movement's direction, then a presence: promoted, newly-held, up, down,
1350
+ entered, left. An earlier revision keyed a rule on 'absent', which is not a
1351
+ value this code has ever produced — presence is 'entered' | 'left' — so that
1352
+ rule matched nothing while looking like the neutral case, and a left row
1353
+ took the READY tint from the bare fallback below. Named exhaustively now
1354
+ rather than defaulted, so a seventh value renders untinted rather than
1355
+ wrong.
1356
+
1357
+ BACKGROUND-COLOR, NEVER THE SHORTHAND. the background shorthand resets background-image
1358
+ to none, and layer 1 draws the HELD row's hatch as a repeating-linear-
1359
+ gradient on exactly that property. These rules are more specific and load
1360
+ after it, so the shorthand silently took the hatch off any row that was both
1361
+ held and changed — which is not a corner: newly-held is the delta that
1362
+ co-occurs with data-held='true' by definition, so the most common overlap
1363
+ lost the held channel entirely. The longhand leaves the gradient alone.
1364
+
1365
+ COLOUR-MIX ON THE TOKEN THE KIND ALREADY NAMES, at the strength layer 1
1366
+ tints its own rows with. Inventing --ig-row-promoted would put a hue in this
1367
+ package that a host retheming the station colours could not move in step,
1368
+ and inventing a percentage would make this tint drift from the unit tint
1369
+ layer 1 draws with --ig-tint-unit. */
1370
+ .ig-workspace .ig-slot[data-ig-delta='promoted'],
1371
+ .ig-workspace .ig-slot[data-ig-delta='up'],
1372
+ .ig-workspace .ig-slot[data-ig-delta='entered'],
1373
+ .ig-workspace .ig-footer-row[data-ig-delta='promoted'],
1374
+ .ig-workspace .ig-footer-row[data-ig-delta='up'],
1375
+ .ig-workspace .ig-footer-row[data-ig-delta='entered'] {
1376
+ background-color: color-mix(in srgb, var(--ig-station-ready) var(--ig-tint-unit), transparent);
1377
+ }
1378
+
1379
+ .ig-workspace .ig-slot[data-ig-delta='newly-held'],
1380
+ .ig-workspace .ig-slot[data-ig-delta='down'],
1381
+ .ig-workspace .ig-footer-row[data-ig-delta='newly-held'],
1382
+ .ig-workspace .ig-footer-row[data-ig-delta='down'] {
1383
+ background-color: color-mix(in srgb, var(--ig-station-held) var(--ig-tint-unit), transparent);
1384
+ }
1385
+
1386
+ /* A row the edit took OUT of the order. Neutral rather than held: it is not
1387
+ waiting on anything, it is simply no longer ranked. */
1388
+ .ig-workspace .ig-slot[data-ig-delta='left'],
1389
+ .ig-workspace .ig-footer-row[data-ig-delta='left'] {
1390
+ background-color: color-mix(in srgb, var(--ig-text-muted) var(--ig-tint-unit), transparent);
1391
+ }
1392
+
1393
+ /* THE THIRD COLUMN IS WHAT PUTS THE CHIP AT THE ROW'S TRAILING EDGE.
1394
+
1395
+ The frame draws the row as rank, body, chip — three columns, the last
1396
+ sized to its content. Layer 1's own rule is a TWO-column grid, so a chip
1397
+ appended as a third child wrapped onto an implicit second row and sat under
1398
+ the rank. That was visible in the first capture of this pair, and it is why
1399
+ this rule is a column template rather than a margin: in a grid, an auto
1400
+ inline margin cannot move an item that is in the wrong row to begin with.
1401
+
1402
+ ONLY ON A ROW THAT HAS A CHIP, so a row with no delta keeps layer 1's own
1403
+ template untouched. AND ONLY ON .ig-slot: a footer row is not that grid, so
1404
+ naming it here would set a column template on a box that has none. */
1405
+ .ig-workspace .ig-slot[data-ig-delta] {
1406
+ grid-template-columns: var(--ig-rank-column) 1fr auto;
1407
+ }
1408
+
1409
+ /* A FOOTER ROW IS A FLEX BOX, NOT THE THREE-COLUMN GRID.
1410
+
1411
+ A ranked row reaches the trailing edge by the column template above. An
1412
+ excluded or tracker-held row is layer 1's .ig-footer-row — display:flex —
1413
+ so a chip appended to it simply follows the content it was appended after,
1414
+ and the frame puts it at the row's end. An auto inline margin is the flex
1415
+ idiom for that, and it is the right tool HERE for the same reason it was the
1416
+ wrong one on the grid: it moves an item within its line, and on the grid the
1417
+ item was in the wrong line to begin with. */
1418
+ .ig-workspace .ig-footer-row > .ig-delta-chip[data-placed] {
1419
+ margin-inline-start: auto;
1420
+ }
1421
+
1422
+ /* §17c's "write landed · order computing": the PREVIOUS order, held still and
1423
+ greyed one step, with the label saying why.
1424
+
1425
+ THE SAME FILTER, for the reason reevaluate/styles.ts gives where it greys
1426
+ .ig-reevaluate[data-order='held'] .ig-viewer: layer 1 sets color directly on
1427
+ its own descendants, so an inherited colour greys almost nothing, and a list
1428
+ of descendants to override goes stale the first time layer 1 colours
1429
+ something new.
1430
+
1431
+ SCOPED TO THE RAIL ZONE, WHICH THE STANDALONE SURFACE DID NOT HAVE TO BE.
1432
+ There, .ig-viewer is the rail and nothing else. The workspace draws a SECOND
1433
+ viewer in the canvas zone, and greying that one is not a smaller version of
1434
+ the same idea — it is the opposite of what the held state means. What is
1435
+ stale is the ORDER; the canvas is drawing the write states #164 put there,
1436
+ and pending, failed and conflict are told partly in hue. So an in-flight edit
1437
+ would have stripped the colour channel off the very marks that say an edit is
1438
+ in flight. The rail is the only surface whose ranks the store cannot vouch
1439
+ for, so it is the only one greyed. */
1440
+ .ig-workspace[data-order='held'] .ig-zone[data-zone='rail'] .ig-viewer {
1441
+ filter: grayscale(1);
1442
+ }
1443
+
1444
+ /* ── 17k's OTHER TWO LAYOUTS ───────────────────────────────────────────────
1445
+
1446
+ ONE RULE GENERATES ALL THREE, and it is the sentence 17k states outright:
1447
+ THE RAIL NEVER YIELDS. THE CANVAS YIELDS FIRST. THE INSPECTOR YIELDS IN
1448
+ BETWEEN. Because the canvas is already the zone designed to refuse (17f) and
1449
+ the inspector is the edit path that survives without it (17b).
1450
+
1451
+ at or above 1360 390 / 1fr / 330, three zones, as 17a
1452
+ 1120 - 1359 rail unchanged; the inspector lifts to a 330 overlay
1453
+ over the canvas, opened by selection, Escape to dismiss
1454
+ below 1120 rail unchanged; the canvas collapses to a strip that
1455
+ opens full-width on demand; the inspector returns inline
1456
+
1457
+ Below roughly 860 is section 9 territory and out of scope for the package.
1458
+
1459
+ THE RAIL IS THE SAME 50 CHARACTERS IN ALL THREE. That is the whole rule, and
1460
+ it is the one thing to check when touching this block: measured on the demo
1461
+ at 1440, 1280 and 1000 the rail track reads 390 at every one of them. The
1462
+ layout before this shipped read 390 / 698 / 328 at 1440 and 312 / 632 / 312
1463
+ at 1280 — the rail giving up 78 of its 390 to keep a three-column template
1464
+ alive, which is the rule upside down.
1465
+
1466
+ WHY THESE ARE RANGE CONDITIONS AND NOT max-width/min-width PAIRS. A
1467
+ container's inline size is NOT AN INTEGER. Written as (min-width: 1360px)
1468
+ and (max-width: 1359px), a box measured at 1359.5 satisfies NEITHER — so the
1469
+ base fallback comes back, the rail drops to its 40-character measure, and
1470
+ the one rule 17k states outright is broken at exactly the widths a resize
1471
+ drags through. The same hole sits at 1119.5. 'width < 1360' and
1472
+ 'width >= 1360' are complements with nothing between them, which is the
1473
+ property the layouts need and the only reason the syntax changed.
1474
+
1475
+ THE BASE STAYS THE THREE-ZONE TEMPLATE, because a surface rendered WITHOUT a
1476
+ mount has no container at all and no query fires: base-is-narrow would hand
1477
+ a static page a collapsed canvas with a control nothing is listening to. So
1478
+ the wide layout is the floor and the narrow ones are carved off it, widest
1479
+ first.
1480
+
1481
+ THESE SIT AT THE FOOT OF THE SHEET, WHICH IS LOAD-BEARING TWICE. The narrow
1482
+ block must come after the middle one to win where both match; and the zones'
1483
+ own rules have to come first, or a regex looking for the rule that defines
1484
+ .ig-zone[data-zone='inspector'] finds an override instead. That hazard is
1485
+ recorded further up this sheet and has already bitten once in layer 1.
1486
+
1487
+ NEITHER LAYOUT IS THE STYLESHEET'S ALONE. The lifted inspector opens on
1488
+ selection and dismisses on Escape, and the strip opens on a press — so
1489
+ host.ts holds two fields and render.ts publishes them on the surface
1490
+ root as data-canvas and data-inspector. What is NOT in the host is the
1491
+ width: nothing in the package asks how wide it is, per 17k's own note that
1492
+ a package which reads the window cannot be dropped into someone else's
1493
+ settings page. Each attribute is read only by the block that has a use for
1494
+ it, and means nothing at the other widths. */
1495
+
1496
+ /* 17k's STRIP CONTROL, RENDERED ALWAYS AND DRAWN ONLY WHEN NARROW.
1497
+ render.ts cannot know its own width, so the button is always in the
1498
+ markup and this is what keeps it off the other two layouts — display:none
1499
+ rather than visibility, so it is not a tab stop where it has nothing to do.
1500
+ The two narrow states size it themselves, below. */
1501
+ .ig-workspace .ig-canvas-strip {
1502
+ display: none;
1503
+ box-sizing: border-box;
1504
+ border: var(--ig-stroke) solid var(--ig-line);
1505
+ background: var(--ig-surface);
1506
+ color: var(--ig-text-body);
1507
+ font-family: var(--ig-font-ui);
1508
+ font-size: var(--ig-font-size-small);
1509
+ padding: var(--ig-space-tight);
1510
+ cursor: pointer;
1511
+ }
1512
+
1513
+ .ig-workspace .ig-canvas-strip:focus-visible {
1514
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
1515
+ outline-offset: var(--ig-space-tight);
1516
+ }
1517
+
1518
+ /* THE MIDDLE LAYOUT: the inspector lifts, and the rail does not move.
1519
+
1520
+ IT IS PLACED IN THE CANVAS'S GRID AREA, NOT POSITIONED OVER IT. An absolute
1521
+ overlay would need an offset chosen so it clears the rail, and an offset is
1522
+ a number that is right until someone changes a track. A grid area is not:
1523
+ the rail is a DIFFERENT area, so an item placed in the canvas's cannot reach
1524
+ it however wide it gets. Section 17d's audit overlay is positioned on the
1525
+ same argument one surface over, and this is that argument without the
1526
+ position.
1527
+
1528
+ 42 CHARACTERS, THE SAME MEASURE THE WIDE LAYOUT GIVES THE INSPECTOR — 17k
1529
+ lifts the panel, it does not resize it. At the shipped --ig-char-width that
1530
+ is 327.6 against 17k's 330, the 2.4 the wide block already accounts for.
1531
+
1532
+ OPAQUE AND RAISED, because it now has the canvas underneath it rather than a
1533
+ border between them. --ig-elevation-overlay is the token for exactly this
1534
+ and --ig-surface is the raised ground; the zone keeps its own border-left
1535
+ from the base rule, which reads as the panel's leading edge. */
1536
+ @container ig-workspace (width < 1360px) {
1537
+ .ig-workspace {
1538
+ grid-template-areas:
1539
+ 'header header'
1540
+ 'rail canvas';
1541
+ grid-template-columns:
1542
+ calc(var(--ig-char-width) * 50)
1543
+ 1fr;
1544
+ }
1545
+
1546
+ .ig-workspace .ig-zone[data-zone='inspector'] {
1547
+ grid-area: canvas;
1548
+ justify-self: end;
1549
+ inline-size: calc(var(--ig-char-width) * 42);
1550
+ z-index: 1;
1551
+ background: var(--ig-surface);
1552
+ box-shadow: var(--ig-elevation-overlay);
1553
+ }
1554
+
1555
+ /* OPENED BY SELECTION, DISMISSED ON ESCAPE — 17k's own words, and both
1556
+ halves arrive as this one attribute. render.ts writes 'lifted' only when
1557
+ something is SELECTED and the reader has not dismissed it since, so an
1558
+ empty "nothing selected" card never floats over the canvas. */
1559
+ .ig-workspace[data-inspector='dismissed'] .ig-zone[data-zone='inspector'] {
1560
+ display: none;
1561
+ }
1562
+ }
1563
+
1564
+ /* THE NARROW LAYOUT: the canvas collapses, the inspector comes back inline,
1565
+ and the rail still does not move.
1566
+
1567
+ THE INSPECTOR IS RESTORED DECLARATION BY DECLARATION rather than by scoping
1568
+ the block above to a band, because the sheet's guard has no band to scope to.
1569
+ Each line here answers one line there, on the same selector so source order
1570
+ settles it — that is the price of single-sided conditions, paid in full and
1571
+ in one place.
1572
+
1573
+ THE STRIP IS 4 CHARACTERS. The canvas is the zone designed to refuse (17f),
1574
+ so what it yields is everything except the handle that brings it back; the
1575
+ inspector takes 1fr, which at 1000 is 550 and at 860 is 418 — both above the
1576
+ 330 the wide layout gives it, so the zone that yields in between is not
1577
+ yielding here at all. That is the order the rule names. */
1578
+ @container ig-workspace (width < 1120px) {
1579
+ .ig-workspace {
1580
+ grid-template-areas:
1581
+ 'header header header'
1582
+ 'rail canvas inspector';
1583
+ grid-template-columns:
1584
+ calc(var(--ig-char-width) * 50)
1585
+ calc(var(--ig-char-width) * 4)
1586
+ 1fr;
1587
+ }
1588
+
1589
+ .ig-workspace .ig-zone[data-zone='inspector'] {
1590
+ grid-area: inspector;
1591
+ justify-self: stretch;
1592
+ inline-size: auto;
1593
+ z-index: auto;
1594
+ background: none;
1595
+ box-shadow: none;
1596
+ }
1597
+
1598
+ /* THE DISMISSAL MEANS NOTHING HERE, and saying so is the undo of the middle
1599
+ block's only conditional rule. The inspector is a zone again, so there is
1600
+ no overlay to have dismissed. */
1601
+ .ig-workspace[data-inspector='dismissed'] .ig-zone[data-zone='inspector'] {
1602
+ display: block;
1603
+ }
1604
+
1605
+ .ig-workspace[data-canvas='strip'] .ig-canvas-strip {
1606
+ display: block;
1607
+ writing-mode: vertical-rl;
1608
+ inline-size: 100%;
1609
+ block-size: 100%;
1610
+ overflow: hidden;
1611
+ white-space: nowrap;
1612
+ }
1613
+
1614
+ /* COLLAPSED MEANS GONE, NOT CLIPPED. The zone is overflow:auto, so leaving
1615
+ the graph in a 31-wide track would keep every node in the tab order behind
1616
+ a scrollbar the reader cannot use — and a keyboard reader would walk a
1617
+ canvas nobody can see. display:none is what takes them out of the tree,
1618
+ and it is reversible in one attribute.
1619
+
1620
+ TWO EXCEPTIONS, AND NEITHER IS THE CANVAS. The strip is the handle that
1621
+ brings the zone back. 17d's audit overlay is a sibling of the graph rather
1622
+ than part of it — it is anchored to the HEADER COUNT, which is drawn at
1623
+ every width, so a collapsed canvas taking it down would remove a surface
1624
+ the reader opened from a control that is still there. */
1625
+ .ig-workspace[data-canvas='strip']
1626
+ .ig-zone[data-zone='canvas']
1627
+ > :not(.ig-canvas-strip):not(.ig-audit-overlay) {
1628
+ display: none;
1629
+ }
1630
+
1631
+ /* AND THE OVERLAY HAS TO ESCAPE THE STRIP IT HANGS OFF.
1632
+
1633
+ 17d positions it against the canvas ZONE, which is what keeps it off the
1634
+ rail without a magic offset. That argument still holds here and the
1635
+ ARITHMETIC does not: the zone is four characters wide, so the panel's own
1636
+ 'width: min(66.67ch, 100% - gap)' resolves to the strip, and 'right' would
1637
+ anchor a wider one leftwards — across the rail, which is the one thing 17k
1638
+ says an overlay must never do.
1639
+
1640
+ SO THE ANCHOR FLIPS AND THE CLAMP GOES. Anchored at the zone's leading
1641
+ edge it opens away from the rail, into the room the inspector is using —
1642
+ over it, which is what an overlay is for. The zone stops clipping while it
1643
+ is a strip, which costs nothing: every other child is display:none above,
1644
+ so there is nothing else left to spill.
1645
+
1646
+ MEASURED at a 1000 mount: the zone's leading edge is 419, the rail ends at
1647
+ 407, and 520 of panel fits in the 598 that remains. Below about 900 it
1648
+ runs past the trailing edge, which is the band 17k already hands to
1649
+ section 9. */
1650
+ .ig-workspace[data-canvas='strip'] .ig-zone[data-zone='canvas'] {
1651
+ overflow: visible;
1652
+ }
1653
+
1654
+ .ig-workspace[data-canvas='strip'] .ig-zone[data-zone='canvas'] .ig-audit-overlay {
1655
+ left: 0;
1656
+ right: auto;
1657
+ width: calc(var(--ig-char-width) * 66.67);
1658
+ max-height: 100%;
1659
+ /* THE SHADOW GOES WITH THE CLIPPING, and that pairing is the whole point.
1660
+ Section 3 forbids this panel reaching the rail ABSOLUTELY, and it is a
1661
+ rule about what a reader SEES — a soft edge falling across the order
1662
+ breaks it exactly as a border would. Everywhere else the zone's own
1663
+ overflow is what guarantees that; here the zone is deliberately not
1664
+ clipping, so the one thing that could paint outside the border box is
1665
+ removed instead. The panel still reads as raised: it keeps its ground
1666
+ and its border, over a canvas that is four characters wide and drawing
1667
+ nothing. */
1668
+ box-shadow: none;
1669
+ }
1670
+
1671
+ /* OPENED FULL-WIDTH ON DEMAND — 17k's words — and full-width is measured
1672
+ from the rail, which still does not yield. The inspector stands down while
1673
+ it is open, which is the one place this layout borrows from the middle
1674
+ one: the canvas is transient here in the way the panel is there. */
1675
+ .ig-workspace[data-canvas='open'] {
1676
+ grid-template-areas:
1677
+ 'header header'
1678
+ 'rail canvas';
1679
+ grid-template-columns:
1680
+ calc(var(--ig-char-width) * 50)
1681
+ 1fr;
1682
+ }
1683
+
1684
+ .ig-workspace[data-canvas='open'] .ig-zone[data-zone='inspector'] {
1685
+ display: none;
1686
+ }
1687
+
1688
+ /* The same control closes it, so it stops being a strip and reads as a
1689
+ button above the canvas it opened. */
1690
+ .ig-workspace[data-canvas='open'] .ig-canvas-strip {
1691
+ display: block;
1692
+ justify-self: start;
1693
+ }
1694
+ }
332
1695
  `;
333
1696
  //# sourceMappingURL=styles.js.map