@issuegraph/editor 0.10.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 (106) hide show
  1. package/README.md +38 -7
  2. package/dist/a11y/baseline.d.ts.map +1 -1
  3. package/dist/a11y/baseline.js +4 -0
  4. package/dist/a11y/baseline.js.map +1 -1
  5. package/dist/audit/findings.d.ts +107 -1
  6. package/dist/audit/findings.d.ts.map +1 -1
  7. package/dist/audit/findings.js +113 -6
  8. package/dist/audit/findings.js.map +1 -1
  9. package/dist/audit/panel.d.ts +277 -0
  10. package/dist/audit/panel.d.ts.map +1 -0
  11. package/dist/audit/panel.js +462 -0
  12. package/dist/audit/panel.js.map +1 -0
  13. package/dist/audit/refused.d.ts +99 -0
  14. package/dist/audit/refused.d.ts.map +1 -0
  15. package/dist/audit/refused.js +276 -0
  16. package/dist/audit/refused.js.map +1 -0
  17. package/dist/audit/styles.d.ts +1 -1
  18. package/dist/audit/styles.d.ts.map +1 -1
  19. package/dist/audit/styles.js +428 -0
  20. package/dist/audit/styles.js.map +1 -1
  21. package/dist/audit/surface.d.ts +54 -12
  22. package/dist/audit/surface.d.ts.map +1 -1
  23. package/dist/audit/surface.js +53 -13
  24. package/dist/audit/surface.js.map +1 -1
  25. package/dist/create/draft.d.ts +19 -0
  26. package/dist/create/draft.d.ts.map +1 -1
  27. package/dist/create/draft.js +21 -0
  28. package/dist/create/draft.js.map +1 -1
  29. package/dist/create/keys.d.ts +68 -8
  30. package/dist/create/keys.d.ts.map +1 -1
  31. package/dist/create/keys.js +43 -5
  32. package/dist/create/keys.js.map +1 -1
  33. package/dist/index.d.ts +38 -15
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +34 -13
  36. package/dist/index.js.map +1 -1
  37. package/dist/overlay/render.d.ts +11 -3
  38. package/dist/overlay/render.d.ts.map +1 -1
  39. package/dist/overlay/render.js +22 -6
  40. package/dist/overlay/render.js.map +1 -1
  41. package/dist/overlay/request.d.ts +71 -0
  42. package/dist/overlay/request.d.ts.map +1 -0
  43. package/dist/overlay/request.js +129 -0
  44. package/dist/overlay/request.js.map +1 -0
  45. package/dist/overlay/styles.d.ts +1 -1
  46. package/dist/overlay/styles.d.ts.map +1 -1
  47. package/dist/overlay/styles.js +38 -7
  48. package/dist/overlay/styles.js.map +1 -1
  49. package/dist/reevaluate/parts.d.ts +130 -0
  50. package/dist/reevaluate/parts.d.ts.map +1 -0
  51. package/dist/reevaluate/parts.js +260 -0
  52. package/dist/reevaluate/parts.js.map +1 -0
  53. package/dist/reevaluate/render.d.ts +18 -11
  54. package/dist/reevaluate/render.d.ts.map +1 -1
  55. package/dist/reevaluate/render.js +21 -135
  56. package/dist/reevaluate/render.js.map +1 -1
  57. package/dist/reevaluate/styles.d.ts +1 -1
  58. package/dist/reevaluate/styles.d.ts.map +1 -1
  59. package/dist/reevaluate/styles.js +26 -0
  60. package/dist/reevaluate/styles.js.map +1 -1
  61. package/dist/reevaluate/view.d.ts +16 -1
  62. package/dist/reevaluate/view.d.ts.map +1 -1
  63. package/dist/reevaluate/view.js +21 -1
  64. package/dist/reevaluate/view.js.map +1 -1
  65. package/dist/reevaluate/words.d.ts +7 -0
  66. package/dist/reevaluate/words.d.ts.map +1 -1
  67. package/dist/scale/ladder.d.ts +62 -6
  68. package/dist/scale/ladder.d.ts.map +1 -1
  69. package/dist/scale/ladder.js +119 -8
  70. package/dist/scale/ladder.js.map +1 -1
  71. package/dist/scale/render.d.ts +44 -0
  72. package/dist/scale/render.d.ts.map +1 -1
  73. package/dist/scale/render.js +197 -27
  74. package/dist/scale/render.js.map +1 -1
  75. package/dist/scale/styles.d.ts +11 -5
  76. package/dist/scale/styles.d.ts.map +1 -1
  77. package/dist/scale/styles.js +133 -4
  78. package/dist/scale/styles.js.map +1 -1
  79. package/dist/workspace/host.d.ts +83 -1
  80. package/dist/workspace/host.d.ts.map +1 -1
  81. package/dist/workspace/host.js +197 -4
  82. package/dist/workspace/host.js.map +1 -1
  83. package/dist/workspace/inspector.d.ts +33 -7
  84. package/dist/workspace/inspector.d.ts.map +1 -1
  85. package/dist/workspace/inspector.js +15 -0
  86. package/dist/workspace/inspector.js.map +1 -1
  87. package/dist/workspace/mount.d.ts +23 -1
  88. package/dist/workspace/mount.d.ts.map +1 -1
  89. package/dist/workspace/mount.js +500 -17
  90. package/dist/workspace/mount.js.map +1 -1
  91. package/dist/workspace/rail.d.ts +5 -2
  92. package/dist/workspace/rail.d.ts.map +1 -1
  93. package/dist/workspace/rail.js.map +1 -1
  94. package/dist/workspace/render.d.ts +379 -3
  95. package/dist/workspace/render.d.ts.map +1 -1
  96. package/dist/workspace/render.js +893 -133
  97. package/dist/workspace/render.js.map +1 -1
  98. package/dist/workspace/selection.d.ts +11 -0
  99. package/dist/workspace/selection.d.ts.map +1 -1
  100. package/dist/workspace/selection.js +10 -0
  101. package/dist/workspace/selection.js.map +1 -1
  102. package/dist/workspace/styles.d.ts +1 -1
  103. package/dist/workspace/styles.d.ts.map +1 -1
  104. package/dist/workspace/styles.js +868 -18
  105. package/dist/workspace/styles.js.map +1 -1
  106. package/package.json +3 -3
@@ -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
@@ -106,6 +163,46 @@ export const workspaceStylesheet = `
106
163
  font-family: var(--ig-font-mono);
107
164
  }
108
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
+
109
206
  /* The one place the accent is spent on this surface, on the only action that
110
207
  starts something. The frame draws it as the header's primary control. */
111
208
  .ig-workspace-firstpass {
@@ -127,17 +224,315 @@ export const workspaceStylesheet = `
127
224
  border-right: var(--ig-stroke) solid var(--ig-line);
128
225
  }
129
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
+
130
289
  .ig-zone[data-zone='canvas'] {
131
290
  grid-area: canvas;
132
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;
133
329
  }
134
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;
361
+ }
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. */
135
438
  .ig-zone[data-zone='inspector'] {
136
439
  grid-area: inspector;
137
440
  overflow-y: auto;
138
441
  border-left: var(--ig-stroke) solid var(--ig-line);
139
442
  }
140
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
+
141
536
  /* THE AMBIENT LEFT-BAR. A 2px gold rule on the affected row and nothing else —
142
537
  no fill, no icon, no badge. §17a's whole ask is that encoding errors stay
143
538
  visible while you work rather than pulling you out of it.
@@ -162,8 +557,12 @@ export const workspaceStylesheet = `
162
557
 
163
558
  ONE TREATMENT FOR ALL FOUR SEVERITIES, which is also a correction. §17d
164
559
  names exactly one ambient mark — "a 2px gold left-bar on the affected rail
165
- row" — and puts the severity distinction in the findings list, which is
166
- host-side. The misleading variant this replaces gave the LEAST urgent
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
167
566
  severity ("clearing is bookkeeping, not urgency") the alarm colour while
168
567
  every other finding got cyan, inverting the design it came from. */
169
568
  .ig-zone[data-zone='rail'] [data-ig-audit] {
@@ -293,6 +692,16 @@ export const workspaceStylesheet = `
293
692
  gap: var(--ig-space-tight);
294
693
  }
295
694
 
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
+
296
705
  /* SECTION 17a DRAWS EVERY INSPECTOR HEADING IN CAPS -- WHY RANK 2,
297
706
  RELATIONSHIPS, ADD RELATIONSHIP -- so the treatment is shared rather than
298
707
  spelled per heading. Tracking widens with the caps because letterforms at a
@@ -329,10 +738,17 @@ export const workspaceStylesheet = `
329
738
  }
330
739
 
331
740
 
332
- /* THE PANEL'S THREE QUIET BUTTONS SHARE ONE TREATMENT — clear, '+ add' and
333
- cancel. They are the same affordance at three moments (leave this selection,
334
- begin a relationship, abandon the draft) and drawing them three ways would
335
- invent a hierarchy the design does not state. The destructive controls are
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
336
752
  deliberately NOT in this set: a row's '✕' is a glyph in the row's own slot,
337
753
  and the mount's delete button carries its own danger treatment.
338
754
 
@@ -344,7 +760,6 @@ export const workspaceStylesheet = `
344
760
  baseline that rule's own comment says it is there to get. Declared where each
345
761
  is true instead. */
346
762
  .ig-inspector-clear,
347
- .ig-inspector-addbutton,
348
763
  .ig-inspector-cancel {
349
764
  background: var(--ig-surface-2);
350
765
  color: var(--ig-text-body);
@@ -359,10 +774,42 @@ export const workspaceStylesheet = `
359
774
  .ig-inspector-clear:focus-visible,
360
775
  .ig-inspector-addbutton:focus-visible,
361
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. */
362
779
  outline: var(--ig-focus-ring) solid var(--ig-focus);
363
780
  outline-offset: var(--ig-space-tight);
364
781
  }
365
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
+
366
813
  .ig-relationship-list {
367
814
  list-style: none;
368
815
  margin: 0;
@@ -605,18 +1052,30 @@ export const workspaceStylesheet = `
605
1052
  border-style: dotted;
606
1053
  }
607
1054
 
608
- /* THE CREATE STEP, WHICHEVER STEP IT IS. One box holds '+ add' on its own and
609
- the numbered list with its cancel, because they are one place in the panel
610
- the reader returns to rather than two that happen to alternate. */
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. */
611
1064
  .ig-inspector-add {
612
1065
  display: flex;
613
1066
  flex-direction: column;
614
1067
  gap: var(--ig-space-tight);
615
1068
  }
616
1069
 
617
- /* Sized by their own words rather than stretched across the column — see the
618
- quiet-button rule above, which deliberately does not carry this. */
619
- .ig-inspector-addbutton,
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. */
620
1079
  .ig-inspector-cancel {
621
1080
  align-self: flex-start;
622
1081
  }
@@ -641,14 +1100,25 @@ export const workspaceStylesheet = `
641
1100
  gap: var(--ig-space-micro);
642
1101
  }
643
1102
 
644
- /* Full width, so the whole entry is the hit target and the digits form a
645
- readable column down the left — which is what makes the list legible as a
646
- set of KEYS rather than as five buttons that happen to start with a number. */
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. */
647
1117
  .ig-kind-option {
648
1118
  width: 100%;
649
1119
  display: flex;
650
1120
  gap: var(--ig-space-tight);
651
- align-items: baseline;
1121
+ align-items: center;
652
1122
  background: var(--ig-surface);
653
1123
  color: var(--ig-text-body);
654
1124
  border: var(--ig-stroke) solid var(--ig-line);
@@ -665,7 +1135,18 @@ export const workspaceStylesheet = `
665
1135
  outline-offset: var(--ig-space-tight);
666
1136
  }
667
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. */
668
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);
669
1150
  font-family: var(--ig-font-mono);
670
1151
  color: var(--ig-text-muted);
671
1152
  }
@@ -842,5 +1323,374 @@ export const workspaceStylesheet = `
842
1323
  font-size: var(--ig-font-size-small);
843
1324
  color: var(--ig-text-muted);
844
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
+ }
845
1695
  `;
846
1696
  //# sourceMappingURL=styles.js.map