@hueest/xray 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { a as ROOT_ATTR, c as SPEC_GATE, i as NODE_CLASS, l as SPEC_INLINE, n as BOUNDARY_ATTR, r as LEAF_CLASS, u as SPEC_LEAF } from "./plate-BwJUgHDc.js";
1
+ import { a as ROOT_ATTR, c as SPEC_GATE, i as NODE_CLASS, l as SPEC_INLINE, n as BOUNDARY_ATTR, r as LEAF_CLASS, u as SPEC_LEAF } from "./plate-CXIh5_OB.js";
2
2
  //#region src/ir.ts
3
3
  /** A frozen `LiteStyleDeclaration` of empty strings, for entry-less nodes (root/stitch/synthetic). */
4
4
  const EMPTY_STYLE = Object.freeze({
@@ -45,33 +45,35 @@ const EMPTY_STYLE = Object.freeze({
45
45
  alignItems: "",
46
46
  verticalAlign: ""
47
47
  });
48
- /** The `LeafKind` values, as a runtime list (kept local so `ir.ts` has no runtime edge back to `serialize.ts`). */
48
+ /**
49
+ * The `LeafKind` values, as a runtime list (kept local so `ir.ts` has no runtime edge back to
50
+ * `measure.ts`).
51
+ */
49
52
  const LEAF_KINDS$1 = [
50
- "text",
53
+ "text-line",
51
54
  "text-block",
52
55
  "media",
53
- "box"
56
+ "box",
57
+ "hidden"
54
58
  ];
55
59
  function isLeafKind(kind) {
56
60
  return LEAF_KINDS$1.includes(kind);
57
61
  }
58
62
  /**
59
- * Run a per-View pass over every View of a `PlateIR` (ADR 0022 §3). The pass is
60
- * the annotate-in-place kind — it mutates `view.bundle.root`'s nodes (sets
61
- * `dropped`/`folded`/`count`/… fields) and returns nothing; `eachView` just
62
- * sequences it across the bundle's Views. A cross-View pass reads across
63
- * `plate.views` directly instead (those stay deferred; ADR 0022 §2).
63
+ * Run a per-View pass over every View of a `PlateIR` (ADR 0022 §3). The pass is the
64
+ * annotate-in-place kind — it mutates `view.bundle.root`'s nodes (sets `dropped`/`folded`/`count`/…
65
+ * fields) and returns nothing; `eachView` just sequences it across the bundle's Views. A cross-View
66
+ * pass reads across `plate.views` directly instead (those stay deferred; ADR 0022 §2).
64
67
  */
65
68
  function eachView(plate, pass) {
66
69
  for (const view of plate.views) pass(view);
67
70
  }
68
71
  /**
69
- * Copy the bounded computed-style set off the live `CSSStyleDeclaration` into a
70
- * frozen plain object (ADR 0022 invariant 5). The live view is NEVER stored, so a
71
- * pass cannot re-read the DOM. Each prop coalesces to `''` so a node whose `cs`
72
- * carries only a partial set (a test stub, or a UA-default-only element) still
73
- * reads a safe `string`, never `undefined`. Called by the walk at each node's
74
- * build site (it already holds the live `cs`) — the snapshot folds into Measure.
72
+ * Copy the bounded computed-style set off the live `CSSStyleDeclaration` into a frozen plain object
73
+ * (ADR 0022 invariant 5). The live view is NEVER stored, so a pass cannot re-read the DOM. Each
74
+ * prop coalesces to `''` so a node whose `cs` carries only a partial set (a test stub, or a
75
+ * UA-default-only element) still reads a safe `string`, never `undefined`. Called by the walk at
76
+ * each node's build site (it already holds the live `cs`) the snapshot folds into Measure.
75
77
  */
76
78
  function snapshotStyle(cs) {
77
79
  return Object.freeze({
@@ -120,18 +122,15 @@ function snapshotStyle(cs) {
120
122
  });
121
123
  }
122
124
  /**
123
- * Read the (grouped) IR back into a per-View `ViewCapture`'s `{tree, rules}` — the
124
- * inverse of the walk's IR build. The tree comes from the node structure;
125
- * the flat rule list is REBUILT from the IR: each node's lifted author + inline
126
- * rules (`decls.author`), its carried token defs (`decls.carried`), and its
127
- * synthesized fallback/size rules (`decls.fallback`/`decls.size`), plus the
128
- * bundle's non-node-anchored rules the text-context rule and the `@container`
129
- * rep bands. A rule shared across nodes (its `ids` spanning several) sits on each
130
- * node's `decls.author` BY REFERENCE, so it is de-duplicated by identity and
131
- * emitted once. Fallback/size order follows the walk's PUSH order
132
- * (`bundle.nodes`); author/carried/context/band rules carry their own
133
- * `(spec, order)`, so their array position is immaterial — classify re-sorts by
134
- * `(layered, spec, order)`.
125
+ * Read the (grouped) IR back into a per-View `ViewCapture`'s `{tree, rules}` — the inverse of the
126
+ * walk's IR build. The tree comes from the node structure; the flat rule list is REBUILT from the
127
+ * IR: each node's lifted author + inline rules (`decls.author`), its carried token defs
128
+ * (`decls.carried`), and its synthesized fallback/size rules (`decls.fallback`/`decls.size`), plus
129
+ * the bundle's non-node-anchored rules — the text-context rule and the `@container` rep bands. A
130
+ * rule shared across nodes (its `ids` spanning several) sits on each node's `decls.author` BY
131
+ * REFERENCE, so it is de-duplicated by identity and emitted once. Fallback/size order follows the
132
+ * walk's PUSH order (`bundle.nodes`); author/carried/context/band rules carry their own `(spec,
133
+ * order)`, so their array position is immaterial classify re-sorts by `(layered, spec, order)`.
135
134
  */
136
135
  function emit(bundle) {
137
136
  const tree = irTree(bundle.root);
@@ -198,22 +197,17 @@ function irTree(node) {
198
197
  return out;
199
198
  }
200
199
  /**
201
- * A node's EFFECTIVE kids — the kids `emit` actually projects, after lowering the
202
- * `dropped` and `flattened` annotations. Used by BOTH `irTree` (tree building) and
203
- * `collectNodes` (rule collection) so structure and rules stay in lockstep, and by
204
- * `templateIR`'s structural matching (`serialize.ts`) so a run is templated on the
205
- * shape `emit` will project, not the raw pre-Flatten tree (ADR 0022 §3). A FOLDED
206
- * node yields NO kids it projects as one synthetic text-bar leaf, so its raw
207
- * subtree is suppressed (the glyph-fold survivor model, ADR 0022 §4). For
208
- * each kid, in order:
209
- * - `dropped` omit it AND its whole subtree (a dropped node is never recursed).
210
- * - `flattened` splice it out: contribute NEITHER a tree node NOR its rules,
211
- * and in its slot hoist the kid's OWN effective kids (recurse, so a chain of
212
- * flattened wrappers collapses to a fixpoint). Dropping the flattened node's
213
- * rules matches the old destructive Flatten exactly — the spliced wrapper never
214
- * reached emit, and `isPassthroughWrapper` guarantees it carries no tokens, zero
215
- * margin, and a coincident box, so there is no layout to lose.
216
- * - otherwise → keep it.
200
+ * A node's EFFECTIVE kids — the kids `emit` actually projects, after lowering the `dropped` and
201
+ * `flattened` annotations. Used by BOTH `irTree` (tree building) and `collectNodes` (rule
202
+ * collection) so structure and rules stay in lockstep, and by `templateIR`'s structural matching
203
+ * (`distil.ts`) so a run is templated on the shape `emit` will project, not the raw pre-Flatten
204
+ * tree (ADR 0022 §3). A FOLDED node yields NO kids — it projects as one synthetic text-bar leaf, so
205
+ * its raw subtree is suppressed (the glyph-fold survivor model, ADR 0022 §4). For each kid, in
206
+ * order: - `dropped` → omit it AND its whole subtree (a dropped node is never recursed). -
207
+ * `flattened` → splice it out: contribute NEITHER a tree node NOR its rules, and in its slot hoist
208
+ * the kid's OWN effective kids (recurse, so a chain of flattened wrappers collapses to a fixpoint).
209
+ * Dropping the flattened node's rules is safe: `isPassthroughWrapper` guarantees it carries no
210
+ * tokens, zero margin, and a coincident box, so there is no layout to lose. - otherwise keep it.
217
211
  */
218
212
  function effectiveKids(node) {
219
213
  if (node.folded) return [];
@@ -226,38 +220,35 @@ function effectiveKids(node) {
226
220
  return out;
227
221
  }
228
222
  /**
229
- * A node's EFFECTIVE bone box — the rect downstream geometry passes read (ADR 0022
230
- * invariant 3). A glyph-fold survivor reports its `folded.box` (the union of the
231
- * absorbed icon + the text unit, exactly what the old synthetic bar measured); every
232
- * other node reports its raw measured `box`. The single place "override ?? raw box"
233
- * lives, so Superset / Crop never branch on `folded` themselves.
223
+ * A node's EFFECTIVE bone box — the rect downstream geometry passes read (ADR 0022 invariant 3). A
224
+ * glyph-fold survivor reports its `folded.box` (the union of the absorbed icon + the text unit);
225
+ * every other node reports its raw measured `box`. The single place "override ?? raw box" lives, so
226
+ * Superset / Crop never branch on `folded` themselves.
234
227
  */
235
228
  function boneBox(node) {
236
229
  return node.folded?.box ?? node.box;
237
230
  }
238
231
  /**
239
- * A node's EFFECTIVE kind — the kind downstream shape/leaf passes see. A glyph-fold
240
- * survivor is a `text` leaf bar regardless of its raw `kind` (it was a text unit or
241
- * a thin container around one; the old merge re-stamped it `kind: 'text'`); every
232
+ * A node's EFFECTIVE kind — the kind downstream shape/leaf passes see. A glyph-fold survivor is a
233
+ * `text` leaf bar regardless of its raw `kind` (a text unit or a thin container around one); every
242
234
  * other node reports its raw `kind`.
243
235
  */
244
236
  function effectiveKind(node) {
245
- return node.folded ? "text" : node.kind;
237
+ return node.folded ? "text-line" : node.kind;
246
238
  }
247
239
  /**
248
- * A node's EFFECTIVE declarations — the decls `emit` projects (ADR 0022 §4). A
249
- * glyph-fold survivor emits ONLY its synthesized `folded.decls` (the inline-block +
250
- * width/height the old merged bar carried); its own author/carried/fallback/size are
251
- * SUPPRESSED, since the bar replaces the original text unit. Every other node emits
252
- * its raw `decls`.
240
+ * A node's EFFECTIVE declarations — the decls `emit` projects (ADR 0022 §4). A glyph-fold survivor
241
+ * emits ONLY its synthesized `folded.decls` (inline-block + width/height); its own
242
+ * author/carried/fallback/size are SUPPRESSED, since the bar replaces the original text unit. Every
243
+ * other node emits its raw `decls`.
253
244
  */
254
245
  function effectiveDecls(node) {
255
246
  return node.folded ? node.folded.decls : node.decls;
256
247
  }
257
248
  /**
258
- * Map every node by its plate-local id. Used by the build phase's CSS-extract to
259
- * attach each lifted rule to the node(s) it targets. Ids are unique across the
260
- * tree (`state.nextId++`), so the map is one-to-one.
249
+ * Map every node by its plate-local id. Used by the build phase's CSS-extract to attach each lifted
250
+ * rule to the node(s) it targets. Ids are unique across the tree (`state.nextId++`), so the map is
251
+ * one-to-one.
261
252
  */
262
253
  function indexById(root) {
263
254
  const map = /* @__PURE__ */ new Map();
@@ -271,11 +262,11 @@ function indexById(root) {
271
262
  //#endregion
272
263
  //#region src/css.ts
273
264
  /**
274
- * Pure CSS plumbing shared by both extraction strategies: which properties are
275
- * layout (vs paint), approximate selector specificity for cascade-preserving
276
- * ordering, and rendering scoped rules back to a single plate CSS string.
265
+ * Pure CSS plumbing shared by both extraction strategies: which properties are layout (vs paint),
266
+ * approximate selector specificity for cascade-preserving ordering, and rendering scoped rules back
267
+ * to a single plate CSS string.
277
268
  */
278
- const EXACT_PROPS = new Set([
269
+ const EXACT_PROPS = /* @__PURE__ */ new Set([
279
270
  "display",
280
271
  "position",
281
272
  "float",
@@ -352,17 +343,15 @@ function isLayoutProp(prop) {
352
343
  return PATTERN_PROPS.some((re) => re.test(prop));
353
344
  }
354
345
  /**
355
- * The legacy `-webkit-box` display family is the 2009 flexbox, used almost
356
- * exclusively for the `-webkit-line-clamp` truncation idiom
357
- * (`display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: N;
358
- * overflow: hidden`). Its companions `-webkit-box-orient` / `-webkit-line-clamp`
359
- * are NOT layout props we carry, so copying `display: -webkit-box` alone yields a
360
- * broken box: with no `-webkit-box-orient` it defaults to a HORIZONTAL old-flexbox
361
- * that collapses an empty bone child to zero width (invisible), and `overflow:
362
- * hidden` then reads as a blank placeholder with the next sibling riding up under
363
- * it. The idiom's only visible effect clamping text to N lines is already
364
- * captured in the text bone's measured (clamped) height, so the container just
365
- * needs to be a normal block. Map the legacy values to their modern flow
346
+ * The legacy `-webkit-box` display family is the 2009 flexbox, used almost exclusively for the
347
+ * `-webkit-line-clamp` truncation idiom (`display: -webkit-box; -webkit-box-orient: vertical;
348
+ * -webkit-line-clamp: N; overflow: hidden`). Its companions `-webkit-box-orient` /
349
+ * `-webkit-line-clamp` are NOT carried layout props, so copying `display: -webkit-box` alone yields
350
+ * a broken box: with no `-webkit-box-orient` it defaults to a HORIZONTAL old-flexbox that collapses
351
+ * an empty bone child to zero width (invisible), and `overflow: hidden` then reads as a blank
352
+ * placeholder with the next sibling riding up under it. The idiom's only visible effect — clamping
353
+ * text to N lines is already captured in the text bone's measured (clamped) height, so the
354
+ * container just needs to be a normal block. Map the legacy values to their modern flow
366
355
  * equivalents; every other display value passes through untouched.
367
356
  */
368
357
  const LEGACY_DISPLAY = {
@@ -382,20 +371,18 @@ function resolveVars(value, computed) {
382
371
  return out;
383
372
  }
384
373
  /**
385
- * A non-nesting `var(--name[, fallback])` matcher used by the FREEZE: the
386
- * fallback group `[^()]*` deliberately stops at a nested paren, so a
387
- * `var(--a, var(--b))` is rewritten head-first (replacing `--a` re-exposes the
388
- * inner `var(--b)` for the next iteration). The freeze loops to a fixpoint, so
389
- * one-level-at-a-time is correct and keeps the regex simple.
374
+ * A non-nesting `var(--name[, fallback])` matcher used by the FREEZE: the fallback group `[^()]*`
375
+ * deliberately stops at a nested paren, so a `var(--a, var(--b))` is rewritten head-first
376
+ * (replacing `--a` re-exposes the inner `var(--b)` for the next iteration). The freeze loops to a
377
+ * fixpoint, so one-level-at-a-time is correct and keeps the regex simple.
390
378
  */
391
379
  const VAR_REF = /var\(\s*(--[\w-]+)\s*(?:,([^()]*))?\)/g;
392
380
  /**
393
- * The HEAD token name of EVERY `var(` in a value — both the top-level heads and
394
- * any head nested inside a fallback (`var(--a, var(--b))` → `--a`, `--b`). A
395
- * fallback branch is reached by the engine only when its head is undefined, but
396
- * a carried chain must stay self-contained whichever branch renders, so every
397
- * head is a reachability seed. Matching `var(\s*(--name)` globally catches all of
398
- * them in one pass regardless of nesting depth — simpler and more correct than a
381
+ * The HEAD token name of EVERY `var(` in a value — both the top-level heads and any head nested
382
+ * inside a fallback (`var(--a, var(--b))` → `--a`, `--b`). A fallback branch is reached by the
383
+ * engine only when its head is undefined, but a carried chain must stay self-contained whichever
384
+ * branch renders, so every head is a reachability seed. Matching `var(\s*(--name)` globally catches
385
+ * all of them in one pass regardless of nesting depth — simpler and more correct than a
399
386
  * fallback-recursing regex (whose `[^()]*` cannot see into a nested `var()`).
400
387
  */
401
388
  const VAR_HEAD = /var\(\s*(--[\w-]+)/g;
@@ -409,28 +396,24 @@ function referencedTokens(value) {
409
396
  return out;
410
397
  }
411
398
  /**
412
- * Freeze ONLY the `var()` references whose token was NOT carried (ADR 0019 §1).
413
- * A decl produced in carry mode keeps its raw `var(--x, …)` text; once the caller
414
- * knows which tokens it actually emitted on the plate (`carried`), this resolves
415
- * every leftover reference to its capture-time value the irreducible freeze for
416
- * a constant token, an un-anchorable wrapper token (Q5), or a genuinely
417
- * unreadable one. A reference to a carried token is left INTACT so the plate's own
418
- * definition drives it. The result is self-contained by construction: after this
419
- * runs, every surviving `var()` names a token the plate defines.
399
+ * Freeze ONLY the `var()` references whose token was NOT carried (ADR 0019 §1). A decl produced in
400
+ * carry mode keeps its raw `var(--x, …)` text; once the caller knows which tokens it actually
401
+ * emitted on the plate (`carried`), this resolves every leftover reference to its capture-time
402
+ * value the irreducible freeze for a constant token, an un-anchorable wrapper token, or a
403
+ * genuinely unreadable one. A reference to a carried token is left INTACT so the plate's own
404
+ * definition drives it. The result is self-contained by construction: after this runs, every
405
+ * surviving `var()` names a token the plate defines.
420
406
  *
421
- * A leftover token resolves in this order: the caller's `frozen` map (the token's
422
- * winning author value, read straight from the CSSOM) FIRST, then the matched
423
- * element's `computed` style, then the var()'s own fallback. The author-value
424
- * path is authoritative and independent of computed-style inheritance computed
425
- * style cannot always resolve a deeply-inherited custom property (and happy-dom
426
- * never does), so without it a constant token could dangle and break
427
- * self-containment. The `frozen` map already carries the cascade winner, so it is
428
- * preferred where present.
407
+ * A leftover token resolves in this order: the caller's `frozen` map (the token's winning author
408
+ * value, read straight from the CSSOM) FIRST, then the matched element's `computed` style, then the
409
+ * var()'s own fallback. The author-value path is authoritative and independent of computed-style
410
+ * inheritance computed style cannot always resolve a deeply-inherited custom property (and
411
+ * happy-dom never does), so without it a constant token could dangle and break self-containment.
412
+ * The `frozen` map already carries the cascade winner, so it is preferred where present.
429
413
  *
430
- * Operates on whole `prop: value` decl strings (splitting on the FIRST colon, so
431
- * a `calc()`/`url()` value with colons is untouched) and re-runs `fluidTracks`
432
- * on a frozen grid track list — a token that froze to px-only tracks falls back
433
- * to the heuristic, the documented last resort.
414
+ * Operates on whole `prop: value` decl strings (splitting on the FIRST colon, so a `calc()`/`url()`
415
+ * value with colons is untouched) and re-runs `fluidTracks` on a frozen grid track list — a token
416
+ * that froze to px-only tracks falls back to the heuristic, the documented last resort.
434
417
  */
435
418
  function freezeUncarriedVars(decls, carried, computed, frozen) {
436
419
  return decls.map((decl) => {
@@ -456,26 +439,24 @@ function freezeUncarriedVars(decls, carried, computed, frozen) {
456
439
  });
457
440
  }
458
441
  /**
459
- * A grid track list that is only `px` lengths is a frozen computed value —
460
- * authors write `fr`/`%`/`minmax`/`auto`, not sub-pixel tracks, so this is
461
- * almost always a var()-driven `grid` shorthand the engine dropped, leaving us
462
- * the resolved computed track sizes. Frozen px locks the grid to the capture
463
- * width (off-centre at any other size); we can't recover the author value, so we
464
- * reconstruct a responsive one. Already-fluid lists (fr/%/minmax/repeat/auto/…)
465
- * pass through untouched.
442
+ * A grid track list that is only `px` lengths is a frozen computed value — authors write
443
+ * `fr`/`%`/`minmax`/`auto`, not sub-pixel tracks, so this is almost always a var()-driven `grid`
444
+ * shorthand the engine dropped, leaving us the resolved computed track sizes. Frozen px locks the
445
+ * grid to the capture width (off-centre at any other size); the author value is unrecoverable, so a
446
+ * responsive one is reconstructed. Already-fluid lists (fr/%/minmax/repeat/auto/…) pass through
447
+ * untouched.
466
448
  *
467
- * - Rows follow their content in a skeleton (the bones set the height), so a
468
- * frozen row list becomes `auto` tracks.
469
- * - Columns are almost always a centred layout: a wide content track flanked by
470
- * gutters. Make the gutters flexible and equal (`1fr`) so the grid recentres
471
- * at any widththis also erases the scrollbar-width asymmetry the capture
472
- * picked up and cap the content track(s) at the captured size
473
- * (`minmax(0, px)`) so they shrink on narrow viewports but never overgrow. A
474
- * uniform list (no track ≥2× the others — e.g. an even `repeat(n, 1fr)`) has
475
- * no gutters, so every track becomes `1fr`.
449
+ * - Rows follow their content in a skeleton (the bones set the height), so a frozen row list becomes
450
+ * `auto` tracks.
451
+ * - Columns are almost always a centred layout: a wide content track flanked by gutters. Make the
452
+ * gutters flexible and equal (`1fr`) so the grid recentres at any width — this also erases the
453
+ * scrollbar-width asymmetry the capture picked up and cap the content track(s) at the captured
454
+ * size (`minmax(0, px)`) so they shrink on narrow viewports but never overgrow. A uniform list
455
+ * (no track ≥2× the others — e.g. an even `repeat(n, 1fr)`) has no gutters, so every track
456
+ * becomes `1fr`.
476
457
  *
477
- * A single capture can't tell fixed tracks from flexible ones; the precise fix is
478
- * to diff track sizes across the capture-all widths (a later refinement).
458
+ * A single capture can't tell fixed tracks from flexible ones; the precise fix is to diff track
459
+ * sizes across the capture-all widths (deferred).
479
460
  */
480
461
  const PX_ONLY_TRACKS = /^\s*(?:-?[\d.]+px\s*)+$/;
481
462
  function fluidTracks(value, axis) {
@@ -488,19 +469,16 @@ function fluidTracks(value, axis) {
488
469
  return tracks.map((t, i) => isContent[i] ? `minmax(0, ${t}px)` : "1fr").join(" ");
489
470
  }
490
471
  /**
491
- * Extract the layout-relevant declarations of a style block as `prop: value`
492
- * strings.
472
+ * Extract the layout-relevant declarations of a style block as `prop: value` strings.
493
473
  *
494
- * Without `carry`, custom-property references are resolved against `computed`
495
- * (the matched element's computed style) and FROZEN to their capture-time value
496
- * — the legacy ADR 0005 path, unchanged.
474
+ * Without `carry`, custom-property references are resolved against `computed` (the matched
475
+ * element's computed style) and FROZEN to their capture-time value (the ADR 0005 freeze).
497
476
  *
498
- * With `carry` (ADR 0019 §1), a `var()` is KEPT RAW and its referenced token
499
- * names are recorded into `carry.referenced`; the caller resolves the
500
- * carry-vs-freeze decision globally (it alone sees every cross-sheet definition)
501
- * and runs `freezeUncarriedVars` afterward to freeze only the leftovers. This is
502
- * how a responsive token survives as a live reference the plate's own emitted
503
- * definition drives, instead of being locked to one viewport's px.
477
+ * With `carry` (ADR 0019 §1), a `var()` is KEPT RAW and its referenced token names are recorded
478
+ * into `carry.referenced`; the caller resolves the carry-vs-freeze decision globally (it alone sees
479
+ * every cross-sheet definition) and runs `freezeUncarriedVars` afterward to freeze only the
480
+ * leftovers. A responsive token thus survives as a live reference the plate's own emitted
481
+ * definition drives, instead of locking to one viewport's px.
504
482
  */
505
483
  function filterLayoutDecls(style, computed, carry) {
506
484
  const out = [];
@@ -523,10 +501,9 @@ function filterLayoutDecls(style, computed, carry) {
523
501
  return out;
524
502
  }
525
503
  /**
526
- * Approximate selector specificity packed as a single comparable number
527
- * (`a * 1e6 + b * 1e3 + c`). Good enough to order rewritten rules so the
528
- * original cascade winner still wins; known approximation: `:is()`/`:not()`
529
- * sum their arguments instead of taking the max.
504
+ * Approximate selector specificity packed as a single comparable number (`a * 1e6 + b * 1e3 + c`).
505
+ * Good enough to order rewritten rules so the original cascade winner still wins; known
506
+ * approximation: `:is()`/`:not()` sum their arguments instead of taking the max.
530
507
  */
531
508
  function specificity(selector) {
532
509
  let a = 0;
@@ -624,18 +601,17 @@ const BORDER_WIDTH_SATURATION = 4;
624
601
  const SPACING_SATURATION = 64;
625
602
  /** Type-scale (size×weight ratio over base) at which the axis saturates — ~a hero heading. */
626
603
  const TYPE_SCALE_SATURATION = 2.5;
627
- /** font-weight at/above which weight contributes its full bump to type-scale. */
604
+ /** Font-weight at/above which weight contributes its full bump to type-scale. */
628
605
  const BOLD_WEIGHT = 700;
629
606
  /** The base (`normal`) font weight type-scale measures a bump against. */
630
607
  const NORMAL_WEIGHT = 400;
631
608
  /** The CSS default font-size (px) used when the context base is missing/degenerate. */
632
609
  const DEFAULT_FONT_SIZE = 16;
633
610
  /**
634
- * Axis weights for the convenience `score` blend. Contrast and size lead — a
635
- * skeleton bone exists because something PAINTS at some SIZE — with type-scale
636
- * and spacing as secondary modifiers. They sum to 1 so `score` stays in 0..1.
637
- * A per-axis consumer ignores this entirely; it exists only so a gate that wants
638
- * one number has a sensible default. Tunable.
611
+ * Axis weights for the convenience `score` blend. Contrast and size lead — a skeleton bone exists
612
+ * because something PAINTS at some SIZE — with type-scale and spacing as secondary modifiers. They
613
+ * sum to 1 so `score` stays in 0..1. A per-axis consumer ignores this entirely; it exists only so a
614
+ * gate that wants one number has a sensible default. Tunable.
639
615
  */
640
616
  const WEIGHTS = {
641
617
  contrast: .35,
@@ -656,10 +632,10 @@ function pad$1(value) {
656
632
  return Number.isFinite(n) ? n : 0;
657
633
  }
658
634
  /**
659
- * A ramp from `lo` to `hi`: 0 at/below `lo`, 1 at/above `hi`, linear between.
660
- * Every axis funnels its raw measurement through one of these so "more signal"
661
- * maps monotonically to a higher score and then saturates — past a point, a
662
- * thicker border or a bigger heading does not read as proportionally more salient.
635
+ * A ramp from `lo` to `hi`: 0 at/below `lo`, 1 at/above `hi`, linear between. Every axis funnels
636
+ * its raw measurement through one of these so "more signal" maps monotonically to a higher score
637
+ * and then saturates — past a point, a thicker border or a bigger heading does not read as
638
+ * proportionally more salient.
663
639
  */
664
640
  function ramp(value, lo, hi) {
665
641
  if (hi <= lo) return value > lo ? 1 : 0;
@@ -730,7 +706,7 @@ function spacingAxis(cs) {
730
706
  }
731
707
  /** True when justify/align-content/items declares a non-default distribution. */
732
708
  function hasExplicitAlignment(cs) {
733
- const inert = new Set([
709
+ const inert = /* @__PURE__ */ new Set([
734
710
  "",
735
711
  "normal",
736
712
  "stretch",
@@ -761,14 +737,13 @@ function typeScaleAxis(cs, ctx) {
761
737
  return ramp(sizeRatio * (1 + .3 * clamp01(((Number.isFinite(weightRaw) ? weightRaw : NORMAL_WEIGHT) - NORMAL_WEIGHT) / (BOLD_WEIGHT - NORMAL_WEIGHT))), 1, TYPE_SCALE_SATURATION);
762
738
  }
763
739
  /**
764
- * Score a node's visual relevance from the four measurable-visual signals
765
- * (ADR 0019 §4). Returns the per-axis vector plus a convenience blended scalar,
766
- * each in 0..1.
740
+ * Score a node's visual relevance from the four measurable-visual signals (ADR 0019 §4). Returns
741
+ * the per-axis vector plus a convenience blended scalar, each in 0..1.
767
742
  *
768
- * @param cs the node's computed style (any `getPropertyValue` reader)
769
- * @param rect the node's rendered box (a `getBoundingClientRect`-shaped value)
770
- * @param ctx the measured context: nearest painted ancestor background, base
771
- * font-size, and reference (root/viewport) area
743
+ * @param cs The node's computed style (any `getPropertyValue` reader)
744
+ * @param rect The node's rendered box (a `getBoundingClientRect`-shaped value)
745
+ * @param ctx The measured context: nearest painted ancestor background, base font-size, and
746
+ * reference (root/viewport) area
772
747
  */
773
748
  function salience(cs, rect, ctx) {
774
749
  const contrast = contrastAxis(cs, ctx);
@@ -808,11 +783,10 @@ function createDiagnostics() {
808
783
  };
809
784
  }
810
785
  /**
811
- * Build a single diagnostic record with its canonical message, for callers
812
- * outside the walk that surface a diagnostic directly (e.g. the client mapping
813
- * `CaptureTooLargeError`, or the server reporting an invalid committed plate).
814
- * Uses the same message table as the collector so the text is identical
815
- * everywhere. `detail` must never carry user CSS or captured text.
786
+ * Build a single diagnostic record with its canonical message, for callers outside the walk that
787
+ * surface a diagnostic directly (e.g. the client mapping `CaptureTooLargeError`, or the server
788
+ * reporting an invalid committed plate). Uses the same message table as the collector so the text
789
+ * is identical everywhere. `detail` must never carry user CSS or captured text.
816
790
  */
817
791
  function makeDiagnostic(code, options) {
818
792
  return {
@@ -823,10 +797,9 @@ function makeDiagnostic(code, options) {
823
797
  };
824
798
  }
825
799
  /**
826
- * The human-readable message per code, shared verbatim by browser and server so
827
- * their text cannot drift. Messages describe the omission and its fidelity
828
- * cost; none echoes user CSS or captured text. `formatDiagnostic` appends the
829
- * aggregate count and any short detail.
800
+ * The human-readable message per code, shared verbatim by browser and server so their text cannot
801
+ * drift. Messages describe the omission and its fidelity cost; none echoes user CSS or captured
802
+ * text. `formatDiagnostic` appends the aggregate count and any short detail.
830
803
  */
831
804
  const DIAGNOSTIC_MESSAGES = {
832
805
  "unreadable-stylesheet": "could not read a stylesheet (likely cross-origin); rules from it were not captured",
@@ -841,11 +814,10 @@ const DIAGNOSTIC_MESSAGES = {
841
814
  "invalid-plate-file": "a committed plate file could not be read (corrupt JSON, wrong shape, or a version mismatch) and was ignored"
842
815
  };
843
816
  /**
844
- * The single shared formatter. Renders one diagnostic to a stable one-line
845
- * string used IDENTICALLY by the browser console and the Vite server output, so
846
- * the two can never disagree. Shape: `<message> (<count>)[: <detail>]`. Count is
847
- * omitted when 1 or absent; detail is appended only when present and is never
848
- * user content.
817
+ * The single shared formatter. Renders one diagnostic to a stable one-line string used IDENTICALLY
818
+ * by the browser console and the Vite server output, so the two can never disagree. Shape:
819
+ * `<message> (<count>)[: <detail>]`. Count is omitted when 1 or absent; detail is appended only
820
+ * when present and is never user content.
849
821
  */
850
822
  function formatDiagnostic(diagnostic) {
851
823
  const count = diagnostic.count !== void 0 && diagnostic.count > 1 ? ` (${diagnostic.count})` : "";
@@ -859,14 +831,12 @@ function diagnosticsHeader(name) {
859
831
  //#endregion
860
832
  //#region src/style-cache.ts
861
833
  /**
862
- * A per-capture memo of `getComputedStyle(el)` keyed by element, scoped to ONE
863
- * synchronous capture (capture performance plan, piece 5). The walk reads each
864
- * entry node's own style through here, and the ancestor-walking helpers
865
- * (contrast, line-height, clipping) memoize the parents they climb. A
866
- * `CSSStyleDeclaration` is a live view, so this WeakMap lives only as long as
867
- * the single capture that built it and is then dropped it is NEVER cached
868
- * across captures or time (a stored style would silently go stale; the plan
869
- * forbids it). `get` lazily computes and stores on first lookup.
834
+ * A per-capture memo of `getComputedStyle(el)` keyed by element, scoped to ONE synchronous capture
835
+ * (ADR 0025). The walk reads each entry node's own style through here, and the ancestor-walking
836
+ * helpers (contrast, line-height, clipping) memoize the parents they climb. A `CSSStyleDeclaration`
837
+ * is a live view, so this WeakMap lives only as long as the single capture that built it and is
838
+ * then dropped — it is NEVER cached across captures or time (a stored style would silently go
839
+ * stale; ADR 0025 forbids it). `get` lazily computes and stores on first lookup.
870
840
  */
871
841
  var StyleCache = class {
872
842
  #win;
@@ -889,11 +859,10 @@ function isInlineish(display) {
889
859
  return display === "inline" || display.startsWith("inline-");
890
860
  }
891
861
  /**
892
- * Does this container lay its children out in normal inline/block FLOW — so its
893
- * inline-level children coalesce into one run (ADR 0021 §2) — versus flex / grid
894
- * / table, which blockify every child into its own Bone? Keyed on the formatting
895
- * context, not nesting depth: this is why two flex-column labels stay two Bones
896
- * while inline siblings in a block become one line.
862
+ * Does this container lay its children out in normal inline/block FLOW — so its inline-level
863
+ * children coalesce into one run (ADR 0021 §2) — versus flex / grid / table, which blockify every
864
+ * child into its own Bone? Keyed on the formatting context, not nesting depth: this is why two
865
+ * flex-column labels stay two Bones while inline siblings in a block become one line.
897
866
  */
898
867
  function establishesInlineFlow(display) {
899
868
  return !/\b(flex|grid|table|ruby)\b/.test(display);
@@ -906,28 +875,27 @@ function pxValue(value) {
906
875
  return /(?:^|\s)-?[\d.]+px$/.test(value.trim()) ? Number.parseFloat(value) : null;
907
876
  }
908
877
  //#endregion
909
- //#region src/passes.ts
878
+ //#region src/distil.ts
910
879
  /**
911
- * An element at or below this opacity contributes no visible ink: opacity is not
912
- * inherited, but it composites the element AND its subtree as one group and then
913
- * multiplies that group to (near) nothing, so every descendant is hidden too.
914
- * Dropped like `display:none`/`visibility:hidden`. The small epsilon (not a
915
- * strict `=== 0`) also catches the `opacity: 0.001` visually-hidden idiom while
916
- * sparing genuinely faint-but-readable content. A missing/invalid value parses
917
- * to `NaN`, which fails the `<=` and is correctly kept.
880
+ * An element at or below this opacity contributes no visible ink: opacity is not inherited, but it
881
+ * composites the element AND its subtree as one group and then multiplies that group to (near)
882
+ * nothing, so every descendant is hidden too. Dropped like `display:none`/`visibility:hidden`. The
883
+ * small epsilon (not a strict `=== 0`) also catches the `opacity: 0.001` visually-hidden idiom
884
+ * while sparing genuinely faint-but-readable content. A missing/invalid value parses to `NaN`,
885
+ * which fails the `<=` and is correctly kept.
918
886
  */
919
887
  const INVISIBLE_OPACITY = .01;
920
888
  /**
921
- * Does this IR node read as ONE single-line text bar a glyph icon can fold into?
922
- * A bare `text` leaf, or a thin branch whose sole child is a `text` leaf (a
923
- * `<span>label</span>` blockified in a flex row). A `text-block` (multi-line) or
924
- * any structured branch does NOT — a glyph hugs one line.
889
+ * Does this IR node read as ONE single-line text bar a glyph icon can fold into? A bare `text`
890
+ * leaf, or a thin branch whose sole child is a `text` leaf (a `<span>label</span>` blockified in a
891
+ * flex row). A `text-block` (multi-line) or any structured branch does NOT — a glyph hugs one
892
+ * line.
925
893
  */
926
894
  function textLineUnitIR(kid) {
927
- if (kid.kind === "text" && kid.kids.length === 0) return true;
895
+ if (kid.kind === "text-line" && kid.kids.length === 0) return true;
928
896
  if (kid.kind === "container" && kid.kids.length === 1) {
929
897
  const only = kid.kids[0];
930
- return only !== void 0 && only.kind === "text" && only.kids.length === 0;
898
+ return only !== void 0 && only.kind === "text-line" && only.kids.length === 0;
931
899
  }
932
900
  return false;
933
901
  }
@@ -939,59 +907,52 @@ function boxGlyphGeometry(icon, text, lineHeight) {
939
907
  return (icon.x >= text.x + text.width ? icon.x - (text.x + text.width) : text.x - (icon.x + icon.width)) <= lineHeight * .6;
940
908
  }
941
909
  /**
942
- * Is this text bar a multi-line BLOCK rather than a single LINE? Reads the bar's
943
- * `ink` snapshot — the clamped run box and the parent's resolved line-height the
944
- * walk recorded — and applies the SAME `height >= lineHeight × 1.5` threshold the
945
- * old in-walk `isMultiLineRun` used (ADR 0021 §4). An unresolved line-height (0)
946
- * reads as a single line, exactly as before.
910
+ * Is this text bar a multi-line BLOCK rather than a single LINE? Reads the bar's `ink` snapshot
911
+ * (the clamped run box and the parent's resolved line-height the walk recorded) and applies the
912
+ * `height >= lineHeight × 1.5` threshold (ADR 0021 §4). An unresolved line-height (0) reads as a
913
+ * single line.
947
914
  */
948
915
  function inkIsMultiLine(ink) {
949
916
  const runHeight = ink.lineRects[0]?.height ?? 0;
950
917
  return ink.lineHeight > 0 && runHeight >= ink.lineHeight * 1.5;
951
918
  }
952
919
  /**
953
- * MARK-INK — the classification pass (ADR 0022 invariant 5; capture-pipeline Phase
954
- * 3). It takes over the BONE-KIND assignment the measuring walk used to decide
955
- * inline, reading ONLY the IR snapshot (never the live DOM): `isMedia`, `paint`,
956
- * `ink`, and `style`. The walk now builds STRUCTURE + decls + the synthetic text
957
- * bars and leaves a PROVISIONAL kind; `markInk` finalizes the leaf kinds and folds
958
- * invisibility into a `dropped: { reason: 'invisible' }` annotation. Runs BEFORE
959
- * `foldGlyphRowsIR` (which reads `kind: 'media'` / `'text'`).
920
+ * MARK-INK — the classification pass (ADR 0022 invariant 5). It owns BONE-KIND assignment, reading
921
+ * ONLY the IR snapshot (never the live DOM): `isMedia`, `paint`, `ink`, and `style`. The walk
922
+ * builds STRUCTURE + decls + the synthetic text bars and leaves a PROVISIONAL kind; `markInk`
923
+ * finalizes the leaf kinds and folds invisibility into a `dropped: { reason: 'invisible' }`
924
+ * annotation. Runs BEFORE `foldGlyphRowsIR` (which reads `kind: 'media'` / `'text-line'`).
925
+ *
926
+ * Per-node, the rules — UPGRADE-ONLY for the inferred kinds, so an explicit kind the walk set (a
927
+ * `ctx.bone()` bone, the `@container` text-rep's `text-block` bone) is never downgraded:
960
928
  *
961
- * Per-node, the rules UPGRADE-ONLY for the inferred kinds, so an explicit kind the
962
- * walk set (a `ctx.bone()` bone, the `@container` text-rep's `text-block` bone) is
963
- * never downgraded:
964
- * - a DECLARED bone (`markKind`, from `data-xr-bone-kind`; ADR 0018; ADR 0022 §5)
965
- * → re-stamped a CHILDLESS bone of that kind: the walk descended normally
966
- * (measuring the full subtree into the IR), and here every effective kid is
967
- * marked `dropped: { reason: 'collapsed' }` and the node synthesizes its own
968
- * leaf display+size, so `effectiveKids`/emit lower it to one bone. This pre-empts
969
- * the inferred kinds below (a declared kind is authoritative) and shares the
970
- * Absorb re-stamp's display/size synthesis;
971
- * - a text bar (`ink` present) `text` (one baseline) or `text-block` (≥2),
972
- * the line-vs-block decision MOVED out of `textBar`;
973
- * - an empty/childless ELEMENT (`el !== null`, no raw kids) that is `isMedia`
974
- * → `media`; else a `container` seed whose `paint.surface` paints → `box`
975
- * (the empty-element painted-box-vs-spacer split, off `paint` instead of a
976
- * live `hasVisibleBackground`); an explicit seed (media/box/text) is never
977
- * overwritten, and a non-painted spacer is left as a container.
929
+ * - A DECLARED bone (`markKind`, from `data-xr-bone-kind`; ADR 0018; ADR 0022 §5) re-stamped a
930
+ * CHILDLESS bone of that kind: the walk descended normally (measuring the full subtree into the
931
+ * IR), and here every effective kid is marked `dropped: { reason: 'collapsed' }` and the node
932
+ * synthesizes its own leaf display+size, so `effectiveKids`/emit lower it to one bone. This
933
+ * pre-empts the inferred kinds below (a declared kind is authoritative) and shares the Absorb
934
+ * re-stamp's display/size synthesis;
935
+ * - A text bar (`ink` present) `text` (one baseline) or `text-block` (≥2);
936
+ * - An empty/childless ELEMENT (`el !== null`, no raw kids) that is `isMedia` → `media`; else a
937
+ * `container` seed whose `paint.surface` paints `box` (the empty-element painted-box-vs-spacer
938
+ * split, off `paint` instead of a live `hasVisibleBackground`); an explicit seed (media/box/text)
939
+ * is never overwritten, and a non-painted spacer is left as a container.
978
940
  *
979
- * Invisibility (ADR 0022 §4) is annotated `dropped: { reason: 'invisible' }`. The
980
- * two CSS mechanisms reach differently, and `markInk` models both off the snapshot,
981
- * threading the inherited context DOWN (Measure now DESCENDS both it skips only
982
- * `display: none`):
983
- * - `opacity <= INVISIBLE_OPACITY` composites the element AND its whole subtree as
984
- * one group and multiplies it to nothing — every descendant is hidden too and
985
- * NOTHING below can re-show. The TOPMOST group-hidden node drops; `effectiveKids`
986
- * lowers the whole subtree, so the descendants need no individual drop.
987
- * - `visibility: hidden|collapse` is INHERITED but an ELEMENT descendant can
988
- * override it (`visibility: visible` re-shows). A node is visibility-hidden when
989
- * its own value hides it OR it inherits hidden and does not re-show; such a node
990
- * drops ONLY when it has no surviving effective kid a re-shown descendant keeps
991
- * the bone-free ancestors alive so it survives. This fixes the latent bug where a
992
- * hidden ancestor used to vanish a re-shown subtree. A synthetic text bar carries
993
- * no own visibility (empty snapshot), so it correctly INHERITS its parent's.
994
- * Idempotent — never overwrites a prior drop (a glyph-folded icon keeps `folded`).
941
+ * Invisibility (ADR 0022 §4) is annotated `dropped: { reason: 'invisible' }`. The two CSS
942
+ * mechanisms reach differently, and `markInk` models both off the snapshot, threading the inherited
943
+ * context DOWN (Measure DESCENDS both; it skips only `display: none`):
944
+ *
945
+ * - `opacity <= INVISIBLE_OPACITY` composites the element AND its whole subtree as one group and
946
+ * multiplies it to nothing — every descendant is hidden too and NOTHING below can re-show. The
947
+ * TOPMOST group-hidden node drops; `effectiveKids` lowers the whole subtree, so the descendants
948
+ * need no individual drop.
949
+ * - `visibility: hidden|collapse` is INHERITED but an ELEMENT descendant can override it
950
+ * (`visibility: visible` re-shows). A node is visibility-hidden when its own value hides it OR it
951
+ * inherits hidden and does not re-show; such a node drops ONLY when it has no surviving effective
952
+ * kid a re-shown descendant keeps the bone-free ancestors alive so it survives; a hidden
953
+ * ancestor can never vanish a re-shown subtree. A synthetic text bar carries no own visibility
954
+ * (empty snapshot), so it correctly INHERITS its parent's. Idempotent never overwrites a prior
955
+ * drop (a glyph-folded icon keeps `folded`).
995
956
  *
996
957
  * Pure: reads only the IR. Tested with a hand-built IR (markInk.test.ts).
997
958
  */
@@ -1031,7 +992,7 @@ function markInk(node, inherited = NOT_HIDDEN) {
1031
992
  return;
1032
993
  }
1033
994
  if (node.ink) {
1034
- node.kind = inkIsMultiLine(node.ink) ? "text-block" : "text";
995
+ node.kind = inkIsMultiLine(node.ink) ? "text-block" : "text-line";
1035
996
  return;
1036
997
  }
1037
998
  if (node.el !== null && node.kids.length === 0) {
@@ -1061,49 +1022,46 @@ function markInk(node, inherited = NOT_HIDDEN) {
1061
1022
  }
1062
1023
  }
1063
1024
  /**
1064
- * The Salience contrast axis at/above which a NON-fill surface (a contrasting
1065
- * border or a box-shadow) is strong enough to read as one Bone on its own —
1066
- * Absorb's trigger for the cases the contrasting fill never covered. Set so an
1067
- * INTENTIONAL, visible surface clears it and an incidental hairline does not: a
1068
- * box-shadow scores 1; a contrasting border needs ~2px (the contrast axis ramps
1069
- * border width 0→`BORDER_WIDTH_SATURATION`=4px, so 2px 0.5) AND an opaque,
1070
- * page-contrasting colour. Conservative by design — a 1px same-as-page divider
1071
- * scores ~0 and is left as a container, never over-absorbed. Tunable.
1025
+ * The Salience contrast axis at/above which a NON-fill surface (a contrasting border or a
1026
+ * box-shadow) is strong enough to read as one Bone on its own — Absorb's trigger for the cases the
1027
+ * contrasting fill never covered. Set so an INTENTIONAL, visible surface clears it and an
1028
+ * incidental hairline does not: a box-shadow scores 1; a contrasting border needs ~2px (the
1029
+ * contrast axis ramps border width 0→`BORDER_WIDTH_SATURATION`=4px, so 2px 0.5) AND an opaque,
1030
+ * page-contrasting colour. Conservative by design a 1px same-as-page divider scores ~0 and is
1031
+ * left as a container, never over-absorbed. Tunable.
1072
1032
  */
1073
1033
  const ABSORB_CONTRAST_MIN = .5;
1074
1034
  /**
1075
- * Does this node's SURFACE absorb its label content into one Bone (ADR 0021 §3)?
1076
- * PURE — reads ONLY the snapshot Measure resolved at build time (3a), never the
1077
- * DOM. Two additive paths, neither weakening the other:
1035
+ * Does this node's SURFACE absorb its label content into one Bone (ADR 0021 §3)? PURE — reads ONLY
1036
+ * the snapshot Measure resolved at build time, never the DOM. Two additive paths, neither weakening
1037
+ * the other:
1078
1038
  *
1079
- * - FILL — `paint.contrastingFill`, the resolved `hasContrastingFill` boolean, so
1080
- * every filled control that collapsed before collapses identically.
1081
- * - BORDER / BOX-SHADOW — the resolved Salience contrast axis (graded) at/above
1082
- * `ABSORB_CONTRAST_MIN`. The axis is the MAX of fill/border/shadow, so this path
1083
- * only ever ADDS surfaces the fill boolean missed: a card outlined or lifted off
1084
- * the page whose content is label-only is itself the visual.
1039
+ * - FILL — `paint.contrastingFill`, the resolved `hasContrastingFill` boolean: a filled control
1040
+ * collapses to one Bone.
1041
+ * - BORDER / BOX-SHADOW — the resolved Salience contrast axis (graded) at/above
1042
+ * `ABSORB_CONTRAST_MIN`. The axis is the MAX of fill/border/shadow, so this path only ever ADDS
1043
+ * surfaces the fill boolean missed: a card outlined or lifted off the page whose content is
1044
+ * label-only is itself the visual.
1085
1045
  *
1086
- * Conservative: a node with neither a contrasting fill NOR a strong border/shadow
1087
- * surface returns false and keeps its children. An entry-less node (root/stitch/
1088
- * synthetic bar) has no `paint`/`salience` snapshot, so it never absorbs.
1046
+ * Conservative: a node with neither a contrasting fill NOR a strong border/shadow surface returns
1047
+ * false and keeps its children. An entry-less node (root/stitch/ synthetic bar) has no
1048
+ * `paint`/`salience` snapshot, so it never absorbs.
1089
1049
  */
1090
1050
  function surfaceAbsorbs(node) {
1091
1051
  if (node.paint?.contrastingFill) return true;
1092
1052
  return node.salience !== void 0 && node.salience.contrast >= ABSORB_CONTRAST_MIN;
1093
1053
  }
1094
1054
  /**
1095
- * Does this node's EFFECTIVE content reduce to a single label/run and nothing else
1096
- * (ADR 0021 §3)? PURE IR predicate over `effectiveKids`/`effectiveKind` — never the
1097
- * DOM. Every effective kid must read as a text label drawing one bone with no
1098
- * independent structure:
1099
- * - a bare text leaf (`text` / `text-block`), incl. a glyph-fold survivor (an
1100
- * effective `text` bar);
1101
- * - a thin wrapper whose sole effective kid is itself a single text leaf (a
1102
- * `<span>label</span>` blockified in a flex/grid control, or the `@container`
1103
- * text-rep wrapper around one `text-block` bone).
1104
- * A SECOND distinct shape (a `box`, a non-folded sibling structure), any REPLACED
1105
- * MEDIA (`media` is never a swallowing surface, ADR 0021 §3), or a Stitch makes the
1106
- * node a container/frame — draw the parts, keep the surface as a frame.
1055
+ * Does this node's EFFECTIVE content reduce to a single label/run and nothing else (ADR 0021 §3)?
1056
+ * PURE IR predicate over `effectiveKids`/`effectiveKind` — never the DOM. Every effective kid must
1057
+ * read as a text label drawing one bone with no independent structure:
1058
+ *
1059
+ * - A bare text leaf (`text` / `text-block`), incl. a glyph-fold survivor (an effective `text` bar);
1060
+ * - A thin wrapper whose sole effective kid is itself a single text leaf (a `<span>label</span>`
1061
+ * blockified in a flex/grid control, or the `@container` text-rep wrapper around one `text-block`
1062
+ * bone). A SECOND distinct shape (a `box`, a non-folded sibling structure), any REPLACED MEDIA
1063
+ * (`media` is never a swallowing surface, ADR 0021 §3), or a Stitch makes the node a
1064
+ * container/frame draw the parts, keep the surface as a frame.
1107
1065
  */
1108
1066
  function isLabelContent(node) {
1109
1067
  return effectiveKids(node).every(isLabelUnit);
@@ -1111,43 +1069,37 @@ function isLabelContent(node) {
1111
1069
  /** A single effective kid that reads as one text label (no media, no distinct shape, no stitch). */
1112
1070
  function isLabelUnit(node) {
1113
1071
  const kind = effectiveKind(node);
1114
- if (kind === "text" || kind === "text-block") return true;
1072
+ if (kind === "text-line" || kind === "text-block") return true;
1115
1073
  if (kind !== "container") return false;
1116
1074
  const kids = effectiveKids(node);
1117
1075
  const only = kids[0];
1118
1076
  if (kids.length !== 1 || only === void 0) return false;
1119
1077
  const onlyKind = effectiveKind(only);
1120
- return (onlyKind === "text" || onlyKind === "text-block") && effectiveKids(only).length === 0;
1078
+ return (onlyKind === "text-line" || onlyKind === "text-block") && effectiveKids(only).length === 0;
1121
1079
  }
1122
1080
  const NOT_HIDDEN = {
1123
1081
  groupHidden: false,
1124
1082
  visHidden: false
1125
1083
  };
1126
1084
  /**
1127
- * Fold a glyph-sized icon Bone into an adjacent single-line text Bone — the
1128
- * geometry-driven group pass (ADR 0021 §2, "row anything"), now run over the built
1129
- * IR instead of the live DOM via `kidEls`. For each NON-FLOW parent (flex / grid /
1130
- * table / ruby normal flow already folds its icon into the run during the walk),
1131
- * a `media` kid that hugs a neighbouring element-backed text-line on the same row
1132
- * (`boxGlyphGeometry` over the snapshotted boxes) folds WITH that neighbour into one
1133
- * text-line bar sized to their union. A bigger or gutter-gapped icon fails the
1134
- * geometry and stays its own Bone.
1085
+ * Fold a glyph-sized icon Bone into an adjacent single-line text Bone — the geometry-driven group
1086
+ * pass (ADR 0021 §2, "row anything"), run over the built IR. For each NON-FLOW parent (flex / grid
1087
+ * / table / ruby normal flow already folds its icon into the run during the walk), a `media` kid
1088
+ * that hugs a neighbouring element-backed text-line on the same row (`boxGlyphGeometry` over the
1089
+ * snapshotted boxes) folds WITH that neighbour into one text-line bar sized to their union. A
1090
+ * bigger or gutter-gapped icon fails the geometry and stays its own Bone.
1135
1091
  *
1136
- * ANNOTATE-ONLY (ADR 0022 §4): the pass NEVER restructures `kids`. For each match it
1137
- * SETS `folded` on the SURVIVING text unit — the effective bone box (their union) and
1138
- * the synthesized inline-block + width/height decls the old merged bar carried and
1139
- * marks the absorbed icon `dropped: { reason: 'folded' }`. The surviving node keeps
1140
- * its `id` and its place in `bundle.nodes`, so `emit` projects the same single text-bar leaf (icon gone,
1141
- * unit as a `text` leaf sized to the union) the old destructive merge produced; every
1142
- * downstream pass reads the union via `boneBox(node)` and the leaf shape via the
1143
- * effective-node accessors (`effectiveKind`/`effectiveKids`), so a folded node behaves
1144
- * exactly like the old synthetic bar without the tree surgery.
1092
+ * ANNOTATE-ONLY (ADR 0022 §4): the pass NEVER restructures `kids`. For each match it SETS `folded`
1093
+ * on the SURVIVING text unit — the effective bone box (their union) and the synthesized
1094
+ * inline-block + width/height decls of the bar and marks the absorbed icon `dropped: { reason:
1095
+ * 'folded' }`. The surviving node keeps its `id` and its place in `bundle.nodes`, so `emit`
1096
+ * projects one text-bar leaf (icon gone, unit as a `text` leaf sized to the union); every
1097
+ * downstream pass reads the union via `boneBox(node)` and the leaf shape via the effective-node
1098
+ * accessors (`effectiveKind`/`effectiveKids`).
1145
1099
  *
1146
- * Scan/skip semantics mirror the old destructive pass: an icon prefers its LEFT
1147
- * neighbour (`i - 1` before `i + 1`); a dropped (already-folded) icon never folds
1148
- * again; and a folded unit is never re-used as another icon's neighbour (the old
1149
- * merge replaced the unit with an `el: null` bar, which the `el !== null` guard
1150
- * rejected — `folded` is the explicit mirror, since the node is left in place here).
1100
+ * Scan/skip semantics: an icon prefers its LEFT neighbour (`i - 1` before `i + 1`); a dropped
1101
+ * (already-folded) icon never folds again; and a folded unit is never re-used as another icon's
1102
+ * neighbour.
1151
1103
  */
1152
1104
  function foldGlyphRowsIR(node) {
1153
1105
  for (const kid of node.kids) foldGlyphRowsIR(kid);
@@ -1184,29 +1136,22 @@ function foldGlyphRowsIR(node) {
1184
1136
  }
1185
1137
  }
1186
1138
  /**
1187
- * Flatten passthrough wrapper chains (capture-pipeline Phase 2, per-View). A
1188
- * layout-only container — containers paint no Bone (only leaves carry `xr-leaf`),
1189
- * a single child, its box coincident with that child, no margin or positional
1190
- * offset of its own contributes nothing the child does not already produce.
1191
- * Promoting the child into a normal-flow grandparent drops the wrapper
1192
- * Bone-free and position-faithful, shrinking the plate.
1139
+ * FLATTEN (ADR 0022, per-View): collapse passthrough wrapper chains. A layout-only container —
1140
+ * containers paint no Bone (only leaves carry `xr-leaf`), a single child, its box coincident with
1141
+ * that child, no margin or positional offset of its own contributes nothing the child does not
1142
+ * already produce. Promoting the child into a normal-flow grandparent drops the wrapper Bone-free
1143
+ * and position-faithful, shrinking the plate.
1193
1144
  *
1194
- * Annotate-only (ADR 0022 §4): the pass NEVER restructures the tree. It SETS
1195
- * `flattened = true` on each passthrough wrapper that should be spliced; Serialize
1196
- * (`emit`, ir.ts) is the sole structural mutation — it omits a `flattened` node and
1197
- * hoists its kids into the parent's slot. The wrapper stays in the IR with its
1198
- * geometry/decls, so later passes and diagnostics still see it.
1145
+ * Annotate-only (ADR 0022 §4): the pass NEVER restructures the tree. It SETS `flattened = true` on
1146
+ * each passthrough wrapper that should be spliced; Serialize (`emit`, ir.ts) is the sole structural
1147
+ * mutation — it omits a `flattened` node and hoists its kids into the parent's slot. The wrapper
1148
+ * stays in the IR with its geometry/decls, so later passes and diagnostics still see it.
1199
1149
  *
1200
- * Bottom-up so a chain collapses to a fixpoint
1201
- * (`<div><div><div>x</div></div></div>` → one node: every wrapper in the chain is
1202
- * marked). Only flattens wrappers under a normal-flow parent a flex/grid item's
1203
- * slot needs the wrapper's flex sizing, which we do not transfer and never
1204
- * crosses a Stitch (opaque, ADR 0006). The conditions in `isPassthroughWrapper` are
1205
- * what keep it position-faithful. The marked set is the wrappers the old
1206
- * destructive Flatten removed via `collapseWrapperChain` — same predicate, gate,
1207
- * and chain walk (modulo the per-hop 1px coincidence tolerance, evaluated against
1208
- * the unmutated neighbor rather than the collapsed survivor; transitive within
1209
- * tolerance, so the set agrees in every realistic chain — see `markWrapperChain`).
1150
+ * Bottom-up so a chain collapses to a fixpoint (`<div><div><div>x</div></div></div>` → one node:
1151
+ * every wrapper in the chain is marked). Only flattens wrappers under a normal-flow parent — a
1152
+ * flex/grid item's slot needs the wrapper's flex sizing, which the splice does not transfer — and
1153
+ * never crosses a Stitch (opaque, ADR 0006). The conditions in `isPassthroughWrapper` are what keep
1154
+ * it position-faithful.
1210
1155
  */
1211
1156
  function flattenIR(node) {
1212
1157
  if (node.kind === "stitch") return;
@@ -1214,14 +1159,10 @@ function flattenIR(node) {
1214
1159
  if (establishesInlineFlow(node.style.display)) for (const kid of node.kids) markWrapperChain(kid);
1215
1160
  }
1216
1161
  /**
1217
- * Mark every passthrough wrapper in the chain rooted at `node` `flattened`, down to
1218
- * the first node that survives the set the old `collapseWrapperChain` removed (it
1219
- * replaced the kid with that first survivor, dropping every wrapper it walked
1220
- * past). One difference is immaterial: this walks the unmutated chain, so each
1221
- * coincidence check is wrapper-vs-its-own-child (the old code, walking the already
1222
- * collapsed kids, checked wrapper-vs-survivor). Coincidence is transitive within
1223
- * the 1px tolerance, so the marked set agrees in every realistic chain. Sets fields
1224
- * only; never touches `kids`.
1162
+ * Mark every passthrough wrapper in the chain rooted at `node` `flattened`, down to the first node
1163
+ * that survives. Walks the unmutated chain, so each coincidence check is wrapper-vs-its-own-child;
1164
+ * coincidence is transitive within the 1px tolerance, so a whole chain marks consistently. Sets
1165
+ * fields only; never touches `kids`.
1225
1166
  */
1226
1167
  function markWrapperChain(node) {
1227
1168
  let cursor = node;
@@ -1233,11 +1174,10 @@ function markWrapperChain(node) {
1233
1174
  }
1234
1175
  }
1235
1176
  /**
1236
- * A container the skeleton can drop without moving anything: one child, its box
1237
- * coincident with the child (so no padding/border insetting it and no size of its
1238
- * own), no margin and `position: static` with no `transform` (so no offset the
1239
- * promoted child would lose), and no carried token def anchored to it (a
1240
- * descendant's `var()` reads it). A stitch child has no box, so it is never
1177
+ * A container the skeleton can drop without moving anything: one child, its box coincident with the
1178
+ * child (so no padding/border insetting it and no size of its own), no margin and `position:
1179
+ * static` with no `transform` (so no offset the promoted child would lose), and no carried token
1180
+ * def anchored to it (a descendant's `var()` reads it). A stitch child has no box, so it is never
1241
1181
  * coincident — stitch wrappers are conservatively kept.
1242
1182
  */
1243
1183
  function isPassthroughWrapper(node) {
@@ -1262,28 +1202,25 @@ function marginIsZero(margin) {
1262
1202
  return margin.split(/\s+/).every((v) => v === "0px" || v === "0");
1263
1203
  }
1264
1204
  /**
1265
- * Superset (capture-pipeline Phase 2, per-View). When a Bone's box sits wholly
1266
- * inside a SOLID Bone's box, the enclosing Bone *supersets* it it paints the
1267
- * same area, so the two render as one and the layered "bone-over-bone" look the
1268
- * inner one would add is barely perceptible. Annotate the inner Bone (the
1269
- * `subset`) `dropped` so `emit` lowers it out, then prune any container the drop
1270
- * leaves empty (`orphaned`).
1205
+ * SUPERSET (ADR 0022, per-View). When a Bone's box sits wholly inside a SOLID Bone's box, the
1206
+ * enclosing Bone _supersets_ it it paints the same area, so the two render as one and the layered
1207
+ * "bone-over-bone" look the inner one would add is barely perceptible. Annotate the inner Bone (the
1208
+ * `subset`) `dropped` so `emit` lowers it out, then prune any container the drop leaves empty
1209
+ * (`orphaned`).
1271
1210
  *
1272
- * Order / opacity / stacking are IRRELEVANT — it is a set union, so the superset
1273
- * always wins regardless of which paints on top. The subset can be ANY kind (a
1274
- * text line / block on a solid surface is the same redundant bone-over-bone, so
1275
- * it goes too); only the SUPERSET must be SOLID a gappy `text` Bone or a
1276
- * paint-less branch never supersets, since the union must actually fill the area.
1277
- * Decided by geometry, global within the plate (an overlay nested in one subtree
1278
- * can superset media in another); a stitch has no box, so it neither supersets nor
1279
- * is a subset. Non-destructive — the node stays in the IR (see `IRNode.dropped`).
1280
- * Runs last of the IR passes.
1211
+ * Order / opacity / stacking are IRRELEVANT — it is a set union, so the superset always wins
1212
+ * regardless of which paints on top. The subset can be ANY kind (a text line / block on a solid
1213
+ * surface is the same redundant bone-over-bone, so it goes too); only the SUPERSET must be SOLID —
1214
+ * a gappy `text` Bone or a paint-less branch never supersets, since the union must actually fill
1215
+ * the area. Decided by geometry, global within the plate (an overlay nested in one subtree can
1216
+ * superset media in another); a stitch has no box, so it neither supersets nor is a subset.
1217
+ * Non-destructive the node stays in the IR (see `IRNode.dropped`). Runs last of the IR passes.
1281
1218
  *
1282
- * WATCH IN QA: a large solid Bone supersets ANY bone inside it — intended for a
1283
- * filled card collapsing its on-surface lines/badges, but a full-bleed background
1284
- * `<img>` would also swallow a button/CTA or headline laid on it. A white/outline
1285
- * card stays a paint-less container, so its content is kept; only a CONTRASTING
1286
- * (bone-producing) surface supersets. If meaningful content vanishes, add a guard.
1219
+ * WATCH IN QA: a large solid Bone supersets ANY bone inside it — intended for a filled card
1220
+ * collapsing its on-surface lines/badges, but a full-bleed background `<img>` would also swallow a
1221
+ * button/CTA or headline laid on it. A white/outline card stays a paint-less container, so its
1222
+ * content is kept; only a CONTRASTING (bone-producing) surface supersets. If meaningful content
1223
+ * vanishes, add a guard.
1287
1224
  */
1288
1225
  function supersetIR(root, diagnostics) {
1289
1226
  const leaves = [];
@@ -1306,13 +1243,12 @@ function supersetIR(root, diagnostics) {
1306
1243
  pruneEmptyBranches(root);
1307
1244
  }
1308
1245
  /**
1309
- * Gather every leaf with its edges (any kind — all are cullable). A leaf without
1310
- * its own box falls back to its nearest box-bearing ancestor's box: a multi-line
1311
- * `text-block` reflow bone is created boxless (it sizes via `@container` bands,
1312
- * not a rect), but its rendered extent IS its ancestor element's box, so without
1313
- * this fallback an overlay caption that wrapped to a `text-block` would never be
1314
- * recognized as contained. A SOLID (`box`/`media`) bone never falls back: it can
1315
- * only be a container if it truly paints that rect, so a boxless solid is omitted.
1246
+ * Gather every leaf with its edges (any kind — all are droppable). A leaf without its own box falls
1247
+ * back to its nearest box-bearing ancestor's box: a multi-line `text-block` reflow bone is created
1248
+ * boxless (it sizes via `@container` bands, not a rect), but its rendered extent IS its ancestor
1249
+ * element's box, so without this fallback an overlay caption that wrapped to a `text-block` would
1250
+ * never be recognized as contained. A SOLID (`box`/`media`) bone never falls back: it can only be a
1251
+ * container if it truly paints that rect, so a boxless solid is omitted.
1316
1252
  */
1317
1253
  function collectLeaves(node, out, inherited) {
1318
1254
  const own = edgesOf(node);
@@ -1328,17 +1264,16 @@ function collectLeaves(node, out, inherited) {
1328
1264
  }
1329
1265
  /** A painted leaf (carries a bone): `box`/`media`/`text`/`text-block`. */
1330
1266
  function isLeaf(kind) {
1331
- return kind === "box" || kind === "media" || kind === "text" || kind === "text-block";
1267
+ return kind === "box" || kind === "media" || kind === "text-line" || kind === "text-block";
1332
1268
  }
1333
1269
  /** A filled bone that paints its whole rect — `box`/`media`. Only these may be the container. */
1334
1270
  function isSolidLeaf(kind) {
1335
1271
  return kind === "box" || kind === "media";
1336
1272
  }
1337
1273
  /**
1338
- * A node's EFFECTIVE bone box as edges, or null if missing/degenerate (0-area). Reads
1339
- * `boneBox` (ADR 0022 invariant 3), so a glyph-fold survivor reports its `folded.box`
1340
- * (the icon+text union) instead of its raw text box — exactly the rect the old
1341
- * synthetic merged bar had.
1274
+ * A node's EFFECTIVE bone box as edges, or null if missing/degenerate (0-area). Reads `boneBox`
1275
+ * (ADR 0022 invariant 3), so a glyph-fold survivor reports its `folded.box` (the icon+text union)
1276
+ * instead of its raw text box.
1342
1277
  */
1343
1278
  function edgesOf(node) {
1344
1279
  const b = boneBox(node);
@@ -1355,10 +1290,10 @@ function coversFully(outer, inner) {
1355
1290
  return outer.left <= inner.left + 1 && outer.top <= inner.top + 1 && outer.right >= inner.right - 1 && outer.bottom >= inner.bottom - 1;
1356
1291
  }
1357
1292
  /**
1358
- * After Superset, drop any container it emptied — a container whose every child
1359
- * is now dropped paints nothing and adds only dead structure. Bottom-up, so an
1360
- * emptied parent propagates up its chain. Skips originally-empty containers (no
1361
- * kids — other passes own those) and never drops the root.
1293
+ * After Superset, drop any container it emptied — a container whose every child is now dropped
1294
+ * paints nothing and adds only dead structure. Bottom-up, so an emptied parent propagates up its
1295
+ * chain. Skips originally-empty containers (no kids — other passes own those) and never drops the
1296
+ * root.
1362
1297
  */
1363
1298
  function pruneEmptyBranches(node) {
1364
1299
  for (const kid of node.kids) pruneEmptyBranches(kid);
@@ -1366,70 +1301,61 @@ function pruneEmptyBranches(node) {
1366
1301
  if (node.kids.every((kid) => kid.dropped !== void 0)) node.dropped = { reason: "orphaned" };
1367
1302
  }
1368
1303
  /**
1369
- * Crop's ONLY knob: a multiplier on a clipping context's visible bottom, so the
1370
- * fold sits a half-context BELOW the visible edge. It covers render-time reflow to
1371
- * a taller viewport/container (ADR 0004) — the captured fold is a width-dependent,
1372
- * render-time line, so this buffer keeps it deliberately conservative. The buffer
1373
- * is applied ONLY at the final per-leaf compare, never accumulated into the clip
1374
- * bottom threaded to children, so it never compounds across nesting levels.
1304
+ * Crop's ONLY knob: a multiplier on a clipping context's visible bottom, so the fold sits a
1305
+ * half-context BELOW the visible edge. It covers render-time reflow to a taller viewport/container
1306
+ * (ADR 0004) — the captured fold is a width-dependent, render-time line, so this buffer keeps it
1307
+ * deliberately conservative. The buffer is applied ONLY at the final per-leaf compare, never
1308
+ * accumulated into the clip bottom threaded to children, so it never compounds across nesting
1309
+ * levels.
1375
1310
  */
1376
1311
  const CROP_BUFFER = 1.5;
1377
1312
  /**
1378
- * CROP — Phase-2 past-the-fold drop (capture-pipeline Phase 2, per-View). 2D: drops
1379
- * every Bone whose box sits past the FOLD on EITHER axis — its TOP below the visible
1380
- * BOTTOM, or its LEFT past the visible RIGHT edge, of that Bone's NEAREST CLIPPING
1381
- * CONTEXT (a vertically- OR horizontally-scrolling / overflow-clipping ancestor, or
1382
- * the viewport as the base case) then orphan-prunes the emptied containers. The
1383
- * horizontal axis closes the off-screen-RIGHT carousel-card case Template cannot
1313
+ * CROP — the past-the-fold drop (ADR 0022, per-View). 2D: drops every Bone whose box sits past the
1314
+ * FOLD on EITHER axis — its TOP below the visible BOTTOM, or its LEFT past the visible RIGHT edge,
1315
+ * of that Bone's NEAREST CLIPPING CONTEXT (a vertically- OR horizontally-scrolling /
1316
+ * overflow-clipping ancestor, or the viewport as the base case) — then orphan-prunes the emptied
1317
+ * containers. The horizontal axis closes the off-screen-RIGHT carousel-card case Template cannot
1384
1318
  * dedup (a single long run past its scroller's clip).
1385
1319
  *
1386
- * - **No spacer.** Nothing reserves the dropped extent; the skeleton simply ends
1387
- * at the fold.
1388
- * - **Why no CLS.** A dropped Bone was off-screen (below the viewport fold, or right
1389
- * of it) or inside a scroll/overflow container past its clip never visible at
1390
- * capture. When real content lands it just grows the scroll height/width (or, for
1391
- * `overflow:hidden`/`clip`, is never visible at all); off-screen / clipped shifts
1392
- * do not score CLS, so the drop is layout-shift-free.
1393
- * - **Reflow caveat (ADR 0004).** The fold is a render-time, width-dependent line
1394
- * (text rewraps at the render width, not the capture width), so the capture-time
1395
- * fold is conservative via `CROP_BUFFER`; a viewport / container larger than the
1396
- * one captured briefly shows blank past the fold until content fills transient,
1397
- * no shift.
1398
- * - **LTR-first; RTL a known limit.** The horizontal fold drops content past the
1399
- * RIGHT edge (the off-screen direction in LTR). In an RTL carousel the off-screen
1400
- * cards sit to the LEFT, so the right-edge fold does not catch them — an accepted
1401
- * known limit (the base-case viewport fold still drops anything genuinely right of
1402
- * `innerWidth`).
1403
- * - **Per-View.** Views are independent and breakpoint-gated (ADR 0004); each
1404
- * carries its own folds (`viewportHeight`/`viewportWidth` = that View's
1405
- * `window.innerHeight`/`innerWidth`).
1406
- * - **Coordinate space.** Boxes are raw `getBoundingClientRect` (viewport-relative),
1407
- * so the viewport folds (`viewportHeight`/`viewportWidth`) and the clip-ancestor
1408
- * bottoms/rights (`box.bottom`/`box.right`) live in the SAME space — no rebasing.
1409
- * - **Architecture (ADR 0022 invariants 3 & 5).** Measure only snapshots the raw
1410
- * clip inputs (`overflow-y`/`overflow-x`, `scroll*`/`client*`); THIS pass derives
1411
- * the clip bottom/right. The walk records numbers, the pure pass owns all
1412
- * derivation.
1320
+ * - **No spacer.** Nothing reserves the dropped extent; the skeleton simply ends at the fold.
1321
+ * - **Why no CLS.** A dropped Bone was off-screen (below the viewport fold, or right of it) or inside
1322
+ * a scroll/overflow container past its clip never visible at capture. When real content lands
1323
+ * it just grows the scroll height/width (or, for `overflow:hidden`/`clip`, is never visible at
1324
+ * all); off-screen / clipped shifts do not score CLS, so the drop is layout-shift-free.
1325
+ * - **Reflow caveat (ADR 0004).** The fold is a render-time, width-dependent line (text rewraps at
1326
+ * the render width, not the capture width), so the capture-time fold is conservative via
1327
+ * `CROP_BUFFER`; a viewport / container larger than the one captured briefly shows blank past the
1328
+ * fold until content fills transient, no shift.
1329
+ * - **LTR-first; RTL a known limit.** The horizontal fold drops content past the RIGHT edge (the
1330
+ * off-screen direction in LTR). In an RTL carousel the off-screen cards sit to the LEFT, so the
1331
+ * right-edge fold does not catch them — an accepted known limit (the base-case viewport fold
1332
+ * still drops anything genuinely right of `innerWidth`).
1333
+ * - **Per-View.** Views are independent and breakpoint-gated (ADR 0004); each carries its own folds
1334
+ * (`viewportHeight`/`viewportWidth` = that View's `window.innerHeight`/`innerWidth`).
1335
+ * - **Coordinate space.** Boxes are raw `getBoundingClientRect` (viewport-relative), so the viewport
1336
+ * folds (`viewportHeight`/`viewportWidth`) and the clip-ancestor bottoms/rights
1337
+ * (`box.bottom`/`box.right`) live in the SAME space no rebasing.
1338
+ * - **Architecture (ADR 0022 invariants 3 & 5).** Measure only snapshots the raw clip inputs
1339
+ * (`overflow-y`/`overflow-x`, `scroll*`/`client*`); THIS pass derives the clip bottom/right. The
1340
+ * walk records numbers, the pure pass owns all derivation.
1413
1341
  *
1414
- * A top-down traversal threading the RAW (un-buffered) clip bottom AND right: a node
1415
- * lives inside its PARENT's clip context, so the folds that judge a node are its
1416
- * parent's clip bottom/right; the node's own clipping constrains its CHILDREN. The
1417
- * buffer is applied only at the leaf compare so it never compounds down nesting.
1418
- * Reuses `isLeaf`/`isSolidLeaf`/`edgesOf` and the boxless-fallback rule from
1419
- * `collectLeaves`, and `pruneEmptyBranches` for the orphan sweep. Non-destructive
1420
- * (`IRNode.dropped`); `emit` lowers it. Order vs Superset is immaterial — both
1421
- * only annotate `dropped`.
1342
+ * A top-down traversal threading the RAW (un-buffered) clip bottom AND right: a node lives inside
1343
+ * its PARENT's clip context, so the folds that judge a node are its parent's clip bottom/right; the
1344
+ * node's own clipping constrains its CHILDREN. The buffer is applied only at the leaf compare so it
1345
+ * never compounds down nesting. Reuses `isLeaf`/`isSolidLeaf`/`edgesOf` and the boxless-fallback
1346
+ * rule from `collectLeaves`, and `pruneEmptyBranches` for the orphan sweep. Non-destructive
1347
+ * (`IRNode.dropped`); `emit` lowers it. Order vs Superset is immaterial — both only annotate
1348
+ * `dropped`.
1422
1349
  */
1423
1350
  function cropIR(root, viewportHeight, viewportWidth, diagnostics) {
1424
1351
  cropNode(root, viewportHeight, viewportWidth, null, diagnostics);
1425
1352
  pruneEmptyBranches(root);
1426
1353
  }
1427
1354
  /**
1428
- * Does this node clip its content vertically — a scroll/overflow context whose
1429
- * visible bottom is a fold for its children? `overflow-y` is one of
1430
- * auto|scroll|hidden|clip AND the content actually overflows the client box
1431
- * (`scrollHeight > clientHeight`, +1 absorbing sub-pixel). When the metrics were
1432
- * not snapshotted (overflow-y was not clipping at measure time), this is correctly
1355
+ * Does this node clip its content vertically — a scroll/overflow context whose visible bottom is a
1356
+ * fold for its children? `overflow-y` is one of auto|scroll|hidden|clip AND the content actually
1357
+ * overflows the client box (`scrollHeight > clientHeight`, +1 absorbing sub-pixel). When the
1358
+ * metrics were not snapshotted (overflow-y was not clipping at measure time), this is correctly
1433
1359
  * false.
1434
1360
  */
1435
1361
  function nodeClipsY(node) {
@@ -1437,14 +1363,13 @@ function nodeClipsY(node) {
1437
1363
  return node.scrollHeight !== void 0 && node.clientHeight !== void 0 && node.scrollHeight > node.clientHeight + 1;
1438
1364
  }
1439
1365
  /**
1440
- * Does this node clip its content HORIZONTALLY — a scroll/overflow context whose
1441
- * visible right edge is a fold for its children (a carousel)? The horizontal mirror
1442
- * of `nodeClipsY`: `overflow-x` is one of auto|scroll|hidden|clip AND the content
1443
- * actually overflows the client box (`scrollWidth > clientWidth`, +1 absorbing
1444
- * sub-pixel). When the metrics were not snapshotted (overflow-x was not clipping at
1445
- * measure time), this is correctly false so a `flex-wrap`/grid that merely WRAPS
1446
- * to more rows (`overflow-x: visible`) never tightens the horizontal fold; its
1447
- * wrapped items are judged against the wider ancestor/viewport fold and kept.
1366
+ * Does this node clip its content HORIZONTALLY — a scroll/overflow context whose visible right edge
1367
+ * is a fold for its children (a carousel)? The horizontal mirror of `nodeClipsY`: `overflow-x` is
1368
+ * one of auto|scroll|hidden|clip AND the content actually overflows the client box (`scrollWidth >
1369
+ * clientWidth`, +1 absorbing sub-pixel). When the metrics were not snapshotted (overflow-x was not
1370
+ * clipping at measure time), this is correctly false so a `flex-wrap`/grid that merely WRAPS to
1371
+ * more rows (`overflow-x: visible`) never tightens the horizontal fold; its wrapped items are
1372
+ * judged against the wider ancestor/viewport fold and kept.
1448
1373
  */
1449
1374
  function nodeClipsX(node) {
1450
1375
  if (!/^(auto|scroll|hidden|clip)$/.test(node.style.overflowX)) return false;
@@ -1452,13 +1377,12 @@ function nodeClipsX(node) {
1452
1377
  }
1453
1378
  /**
1454
1379
  * Recurse top-down threading `(inheritedAncestorBox, clipBottom, clipRight)`.
1455
- * `clipBottom`/`clipRight` are the folds that apply to THIS node (its parent
1456
- * context's visible bottom / right edge); `inheritedAncestorBox` is the nearest
1457
- * box-bearing ancestor's edges, for the boxless-leaf fallback (identical to
1458
- * `collectLeaves`). The clip bottom/right passed to children tightens to this node's
1459
- * box bottom/right when the node itself clips that axis and ONLY then: a node that
1460
- * merely wraps (overflow:visible) never tightens, so normal in-viewport wrapped
1461
- * content is never over-cropped.
1380
+ * `clipBottom`/`clipRight` are the folds that apply to THIS node (its parent context's visible
1381
+ * bottom / right edge); `inheritedAncestorBox` is the nearest box-bearing ancestor's edges, for the
1382
+ * boxless-leaf fallback (identical to `collectLeaves`). The clip bottom/right passed to children
1383
+ * tightens to this node's box bottom/right when the node itself clips that axis — and ONLY then: a
1384
+ * node that merely wraps (overflow:visible) never tightens, so normal in-viewport wrapped content
1385
+ * is never over-cropped.
1462
1386
  */
1463
1387
  function cropNode(node, clipBottom, clipRight, inheritedAncestorBox, diagnostics) {
1464
1388
  const own = edgesOf(node);
@@ -1475,33 +1399,30 @@ function cropNode(node, clipBottom, clipRight, inheritedAncestorBox, diagnostics
1475
1399
  for (const kid of effectiveKids(node)) cropNode(kid, childClipBottom, childClipRight, own ?? inheritedAncestorBox, diagnostics);
1476
1400
  }
1477
1401
  /**
1478
- * Phase-2 Template (per-View): collapse a run of >= MIN_TEMPLATE_RUN consecutive,
1479
- * structurally-similar sibling cells (a feed/grid) into ONE representative cell
1480
- * carrying `count = N`. The serializer emits the cell's HTML once plus the count
1481
- * and the renderer expands it back to N (chunk.ts / react.core.tsx), so the plate
1482
- * ships one cell instead of N.
1402
+ * TEMPLATE (per-View, runs last): collapse a run of >= MIN_TEMPLATE_RUN consecutive,
1403
+ * structurally-similar sibling cells (a feed/grid) into ONE representative cell carrying `count =
1404
+ * N`. The serializer emits the cell's HTML once plus the count and the renderer expands it back to
1405
+ * N (chunk.ts / react.core.tsx), so the plate ships one cell instead of N.
1483
1406
  *
1484
- * Template runs LAST of the IR passes (after Superset and Crop), so it compresses ONLY
1485
- * what survives the removal passes — what a human would actually see (the owner's
1486
- * guiding principle). Crop drops a run's off-screen tail first; Template then groups the
1487
- * on-screen prefix and `count = run.length` renders back to exactly that surviving N.
1488
- * This makes Template LOSSLESS compression (one cell + a faithful count), so it warns
1489
- * about nothing — the lossy off-screen removal is Crop's job (`dropped-cropped`).
1407
+ * Template runs LAST of the IR passes (after Superset and Crop), so it compresses ONLY what
1408
+ * survives the removal passes — what a viewer would actually see. Crop drops a run's off-screen
1409
+ * tail first; Template then groups the on-screen prefix and `count = run.length` renders back to
1410
+ * exactly that surviving N. Template is therefore LOSSLESS compression (one cell + a faithful
1411
+ * count), so it warns about nothing; the lossy off-screen removal is Crop's job
1412
+ * (`dropped-cropped`).
1490
1413
  *
1491
- * Cells need not be pixel-identical — text length varies cell to cell — so the
1492
- * match is STRUCTURAL: a multiset of every descendant's `structuralHash`
1493
- * (kind + author-decls signature, dimensions excluded), compared within a loose
1494
- * tolerance (TEMPLATE_SHAPE_TOLERANCE differing nodes, so a minority FEATURED
1495
- * badge still groups). The template takes the run's MODAL structure (a minority
1496
- * badge normalizes away) and, for each fixed-size bone, the rounded median
1497
- * width/height across the run; text-block bones reflow via `@container`, so their
1498
- * dims are left alone. Only stitch-free, count-free, NON-dropped cells are templated
1499
- * (the cell must flatten to one HTML string; nested templates are out of scope; a
1500
- * dropped cell breaks the run). The dropped cells' `@container` rep bands are pruned
1501
- * from `extraRules` so no dead CSS ships.
1414
+ * Cells need not be pixel-identical — text length varies cell to cell — so the match is STRUCTURAL:
1415
+ * a multiset of every descendant's `structuralHash` (kind + author-decls signature, dimensions
1416
+ * excluded), compared within a loose tolerance (TEMPLATE_SHAPE_TOLERANCE differing nodes, so a
1417
+ * minority FEATURED badge still groups). The template takes the run's MODAL structure (a minority
1418
+ * badge normalizes away) and, for each fixed-size bone, the rounded median width/height across the
1419
+ * run; text-block bones reflow via `@container`, so their dims are left alone. Only stitch-free,
1420
+ * count-free, NON-dropped cells are templated (the cell must flatten to one HTML string; nested
1421
+ * templates are out of scope; a dropped cell breaks the run). The dropped cells' `@container` rep
1422
+ * bands are pruned from `extraRules` so no dead CSS ships.
1502
1423
  *
1503
- * Loose by design — the tolerance is a single knob; tighten it toward 0 (exact)
1504
- * if false positives surface.
1424
+ * Loose by design — the tolerance is a single knob; tighten it toward 0 (exact) if false positives
1425
+ * surface.
1505
1426
  */
1506
1427
  const MIN_TEMPLATE_RUN = 3;
1507
1428
  const TEMPLATE_SHAPE_TOLERANCE = 1;
@@ -1512,7 +1433,10 @@ function templateIR(bundle) {
1512
1433
  collapseTemplateRuns(bundle.root, dropped);
1513
1434
  if (dropped.size > 0) bundle.extraRules = bundle.extraRules.filter((rule) => !rule.ids.every((id) => dropped.has(id)));
1514
1435
  }
1515
- /** Recurse bottom-up, then replace each maximal template run among a node's kids with one cell + count. */
1436
+ /**
1437
+ * Recurse bottom-up, then replace each maximal template run among a node's kids with one cell +
1438
+ * count.
1439
+ */
1516
1440
  function collapseTemplateRuns(node, dropped) {
1517
1441
  if (node.folded) return;
1518
1442
  for (const kid of node.kids) collapseTemplateRuns(kid, dropped);
@@ -1537,13 +1461,12 @@ function collapseTemplateRuns(node, dropped) {
1537
1461
  node.kids = out;
1538
1462
  }
1539
1463
  /**
1540
- * Maximal run of consecutive templateable kids from `start` matching a common modal
1541
- * shape within tolerance. A `dropped` cell is non-templatable (`isStaticCell` rejects
1542
- * it), so it breaks the run AND is rejected as a first cell — Template runs LAST, after
1543
- * Crop, so it only ever groups NON-dropped (on-screen surviving) cells. Crop drops a
1544
- * contiguous off-screen tail, so this yields the surviving prefix as the run; the
1545
- * dropped tail flows through `collapseTemplateRuns`'s else-branch unchanged (still
1546
- * `dropped`, `emit` lowers it).
1464
+ * Maximal run of consecutive templateable kids from `start` matching a common modal shape within
1465
+ * tolerance. A `dropped` cell is non-templatable (`isStaticCell` rejects it), so it breaks the run
1466
+ * AND is rejected as a first cell — Template runs LAST, after Crop, so it only ever groups
1467
+ * NON-dropped (on-screen surviving) cells. Crop drops a contiguous off-screen tail, so this yields
1468
+ * the surviving prefix as the run; the dropped tail flows through `collapseTemplateRuns`'s
1469
+ * else-branch unchanged (still `dropped`, `emit` lowers it).
1547
1470
  */
1548
1471
  function templateRun(kids, start) {
1549
1472
  const first = kids[start];
@@ -1560,21 +1483,21 @@ function templateRun(kids, start) {
1560
1483
  }
1561
1484
  return run;
1562
1485
  }
1563
- /** A clear majority of the run matches its modal shape exactly (>= MIN_TEMPLATE_MAJORITY) — else there's no single template. */
1486
+ /**
1487
+ * A clear majority of the run matches its modal shape exactly (>= MIN_TEMPLATE_MAJORITY) — else
1488
+ * there's no single template.
1489
+ */
1564
1490
  function hasModalMajority(run) {
1565
1491
  const shapes = run.map(cellShape);
1566
1492
  const modalKey = modalShape(shapes).join(SHAPE_SEP);
1567
1493
  return shapes.filter((shape) => shape.join(SHAPE_SEP) === modalKey).length >= run.length * MIN_TEMPLATE_MAJORITY;
1568
1494
  }
1569
1495
  /**
1570
- * Replace a run with one template cell: modal structure, median fixed-bone dims, and
1571
- * `count = run.length` — the full SURVIVING count. Template runs LAST (after Crop), so
1572
- * the run holds only on-screen cells (Crop already dropped any off-screen tail, and a
1573
- * dropped cell breaks the run in `templateRun`); the count therefore renders back to
1574
- * exactly the N a human would see. Template is now LOSSLESS compression — one cell + a
1575
- * faithful count — so there is nothing to warn about; the lossy off-screen removal is
1576
- * Crop's job and already emits `dropped-cropped`. The median dims are taken over the
1577
- * modal cells.
1496
+ * Replace a run with one template cell: modal structure, median fixed-bone dims, and `count =
1497
+ * run.length` — the full SURVIVING count. Template runs LAST (after Crop), so the run holds only
1498
+ * on-screen cells (Crop already dropped any off-screen tail, and a dropped cell breaks the run in
1499
+ * `templateRun`); the count renders back to exactly the N a viewer would see. The median dims are
1500
+ * taken over the modal cells.
1578
1501
  */
1579
1502
  function synthesizeTemplate(run, dropped) {
1580
1503
  const modalKey = modalShape(run.map(cellShape)).join(SHAPE_SEP);
@@ -1587,23 +1510,26 @@ function synthesizeTemplate(run, dropped) {
1587
1510
  return template;
1588
1511
  }
1589
1512
  /**
1590
- * A cell that can flatten to one HTML string: no stitch and no (nested) template
1591
- * anywhere within. Reads the EFFECTIVE structure (ADR 0022 §3): a `flattened`
1592
- * wrapper contributes nothing of its own and is judged purely by its hoisted kids,
1593
- * so the verdict matches what `emit` projects — mirroring `effectiveKids`.
1513
+ * A cell that can flatten to one HTML string: no stitch and no (nested) template anywhere within.
1514
+ * Reads the EFFECTIVE structure (ADR 0022 §3): a `flattened` wrapper contributes nothing of its own
1515
+ * and is judged purely by its hoisted kids, so the verdict matches what `emit` projects — mirroring
1516
+ * `effectiveKids`.
1594
1517
  *
1595
- * A `dropped` cell is NON-templatable: Template runs LAST (after Crop), so a dropped
1596
- * (off-screen / superseted / etc.) cell must never be grouped — it breaks the run in
1597
- * `templateRun` and is rejected as a first cell, so Template only ever compresses the
1598
- * on-screen survivors. `effectiveKids` already prunes a dropped descendant, so only the
1599
- * cell's OWN drop needs the explicit guard here.
1518
+ * A `dropped` cell is NON-templatable: Template runs LAST (after Crop), so a dropped (off-screen /
1519
+ * superseted / etc.) cell must never be grouped — it breaks the run in `templateRun` and is
1520
+ * rejected as a first cell, so Template only ever compresses the on-screen survivors.
1521
+ * `effectiveKids` already prunes a dropped descendant, so only the cell's OWN drop needs the
1522
+ * explicit guard here.
1600
1523
  */
1601
1524
  function isStaticCell(node) {
1602
1525
  if (node.dropped !== void 0) return false;
1603
1526
  if (!node.flattened && (node.kind === "stitch" || node.ref !== void 0 || node.count !== void 0)) return false;
1604
1527
  return effectiveKids(node).every(isStaticCell);
1605
1528
  }
1606
- /** This bone's structural identity for templating: kind + a dimension-free author-decls signature. Memoized. */
1529
+ /**
1530
+ * This bone's structural identity for templating: kind + a dimension-free author-decls signature.
1531
+ * Memoized.
1532
+ */
1607
1533
  function structuralHash(node) {
1608
1534
  if (node.structuralHash !== void 0) return node.structuralHash;
1609
1535
  const decls = effectiveDecls(node);
@@ -1614,11 +1540,10 @@ function structuralHash(node) {
1614
1540
  return hash;
1615
1541
  }
1616
1542
  /**
1617
- * A cell's shape: the sorted multiset of every EFFECTIVE descendant's
1618
- * `structuralHash`. Reads through `flattened` (ADR 0022 §3): a flattened wrapper
1619
- * contributes no hash of its own and its kids are hoisted, so a run of cells
1620
- * templates on the shape `emit` projects identical whether or not each cell wraps
1621
- * its content in a coincident passthrough wrapper. Mirrors `effectiveKids`.
1543
+ * A cell's shape: the sorted multiset of every EFFECTIVE descendant's `structuralHash`. Reads
1544
+ * through `flattened` (ADR 0022 §3): a flattened wrapper contributes no hash of its own and its
1545
+ * kids are hoisted, so a run of cells templates on the shape `emit` projects — identical whether or
1546
+ * not each cell wraps its content in a coincident passthrough wrapper. Mirrors `effectiveKids`.
1622
1547
  */
1623
1548
  function cellShape(node) {
1624
1549
  const out = [];
@@ -1671,11 +1596,10 @@ function shapeDistance(a, b) {
1671
1596
  return diff + (a.length - i) + (b.length - j);
1672
1597
  }
1673
1598
  /**
1674
- * Median (rounded) fixed-size bone dims across the modal cells, written onto the
1675
- * template's bones. Bones are paired by pre-order INDEX across cells; a kind guard
1676
- * skips mismatches, so a same-shape cell whose DOM order differs (flex `order`,
1677
- * `row-reverse`) just drops out of the median rather than contaminating it —
1678
- * uncommon and guarded, not corrected here.
1599
+ * Median (rounded) fixed-size bone dims across the modal cells, written onto the template's bones.
1600
+ * Bones are paired by pre-order INDEX across cells; a kind guard skips mismatches, so a same-shape
1601
+ * cell whose DOM order differs (flex `order`, `row-reverse`) just drops out of the median rather
1602
+ * than contaminating it uncommon and guarded, not corrected here.
1679
1603
  */
1680
1604
  function applyMedianDims(template, cells) {
1681
1605
  const lists = cells.map(fixedSizeLeaves);
@@ -1707,16 +1631,16 @@ function applyMedianDims(template, cells) {
1707
1631
  }
1708
1632
  }
1709
1633
  /**
1710
- * A cell's fixed-size bones (text-line / box / media) in EFFECTIVE pre-order;
1711
- * text-block reflows, so it's excluded. Reads through `flattened` (a passthrough
1712
- * wrapper is a bone-free container anyway, so this matches `cellShape`'s ordering
1713
- * and the structure `emit` projects — ADR 0022 §3, mirroring `effectiveKids`).
1634
+ * A cell's fixed-size bones (text-line / box / media) in EFFECTIVE pre-order; text-block reflows,
1635
+ * so it's excluded. Reads through `flattened` (a passthrough wrapper is a bone-free container
1636
+ * anyway, so this matches `cellShape`'s ordering and the structure `emit` projects — ADR 0022 §3,
1637
+ * mirroring `effectiveKids`).
1714
1638
  */
1715
1639
  function fixedSizeLeaves(node) {
1716
1640
  const out = [];
1717
1641
  const visit = (n) => {
1718
1642
  const kind = effectiveKind(n);
1719
- if (!n.flattened && (kind === "text" || kind === "box" || kind === "media") && effectiveDecls(n).size) out.push(n);
1643
+ if (!n.flattened && (kind === "text-line" || kind === "box" || kind === "media") && effectiveDecls(n).size) out.push(n);
1720
1644
  for (const kid of effectiveKids(n)) visit(kid);
1721
1645
  };
1722
1646
  visit(node);
@@ -5763,13 +5687,13 @@ function endsWithLineStartProhibitedText(text) {
5763
5687
  const last = getLastCodePoint(text);
5764
5688
  return last !== null && (kinsokuStart.has(last) || leftStickyPunctuation.has(last));
5765
5689
  }
5766
- const keepAllGlueChars = new Set([
5690
+ const keepAllGlueChars = /* @__PURE__ */ new Set([
5767
5691
  "\xA0",
5768
5692
  " ",
5769
5693
  "⁠",
5770
5694
  ""
5771
5695
  ]);
5772
- const keepAllDashBreakChars = new Set([
5696
+ const keepAllDashBreakChars = /* @__PURE__ */ new Set([
5773
5697
  "-",
5774
5698
  "‐",
5775
5699
  "–",
@@ -5790,7 +5714,7 @@ function canContinueKeepAllTextRun(previousText, breakAfterPunctuation) {
5790
5714
  if (endsWithKeepAllDashBreakText(previousText)) return false;
5791
5715
  return true;
5792
5716
  }
5793
- const kinsokuStart = new Set([
5717
+ const kinsokuStart = /* @__PURE__ */ new Set([
5794
5718
  ",",
5795
5719
  ".",
5796
5720
  "!",
@@ -5818,7 +5742,7 @@ const kinsokuStart = new Set([
5818
5742
  "ヽ",
5819
5743
  "ヾ"
5820
5744
  ]);
5821
- const kinsokuEnd = new Set([
5745
+ const kinsokuEnd = /* @__PURE__ */ new Set([
5822
5746
  "\"",
5823
5747
  "(",
5824
5748
  "[",
@@ -5843,8 +5767,8 @@ const kinsokuEnd = new Set([
5843
5767
  "〘",
5844
5768
  "〚"
5845
5769
  ]);
5846
- const forwardStickyGlue = new Set(["'", "’"]);
5847
- const leftStickyPunctuation = new Set([
5770
+ const forwardStickyGlue = /* @__PURE__ */ new Set(["'", "’"]);
5771
+ const leftStickyPunctuation = /* @__PURE__ */ new Set([
5848
5772
  ".",
5849
5773
  ",",
5850
5774
  "!",
@@ -5872,14 +5796,14 @@ const leftStickyPunctuation = new Set([
5872
5796
  "›",
5873
5797
  "…"
5874
5798
  ]);
5875
- const arabicNoSpaceTrailingPunctuation = new Set([
5799
+ const arabicNoSpaceTrailingPunctuation = /* @__PURE__ */ new Set([
5876
5800
  ":",
5877
5801
  ".",
5878
5802
  "،",
5879
5803
  "؛"
5880
5804
  ]);
5881
- const myanmarMedialGlue = new Set(["၏"]);
5882
- const closingQuoteChars = new Set([
5805
+ const myanmarMedialGlue = /* @__PURE__ */ new Set(["၏"]);
5806
+ const closingQuoteChars = /* @__PURE__ */ new Set([
5883
5807
  "”",
5884
5808
  "’",
5885
5809
  "»",
@@ -6255,7 +6179,7 @@ function mergeUrlQueryRuns(segmentation) {
6255
6179
  starts
6256
6180
  };
6257
6181
  }
6258
- const numericJoinerChars = new Set([
6182
+ const numericJoinerChars = /* @__PURE__ */ new Set([
6259
6183
  ":",
6260
6184
  "-",
6261
6185
  "/",
@@ -6268,7 +6192,7 @@ const numericJoinerChars = new Set([
6268
6192
  ]);
6269
6193
  const wordInternalSymbolRe = /[\p{P}\p{S}\p{Co}]/u;
6270
6194
  const emojiPresentationRe$1 = /\p{Emoji_Presentation}/u;
6271
- const noSpaceWordBreakAfterChars = new Set([
6195
+ const noSpaceWordBreakAfterChars = /* @__PURE__ */ new Set([
6272
6196
  "?",
6273
6197
  "֊",
6274
6198
  "-",
@@ -8037,7 +7961,7 @@ function textBar(run, foldedMedia, state, depth) {
8037
7961
  const node = {
8038
7962
  id: state.nextId++,
8039
7963
  el: null,
8040
- kind: "text",
7964
+ kind: "text-line",
8041
7965
  kids: [],
8042
7966
  depth,
8043
7967
  decls: { fallback: ["display: inline-block", "vertical-align: middle"] },
@@ -8065,13 +7989,12 @@ function textBar(run, foldedMedia, state, depth) {
8065
7989
  return node;
8066
7990
  }
8067
7991
  /**
8068
- * CSS generic font-family keywords — none names a concrete face, so pretext's
8069
- * canvas `measureText` can't agree with what the browser actually resolved
8070
- * (pretext's own README flags `system-ui` as unsafe on macOS). A run whose first
8071
- * family token is one of these has no capture-matched named font, so the rep
8072
- * falls back. The list is the CSS generic set plus the `ui-*` system aliases.
7992
+ * CSS generic font-family keywords — none names a concrete face, so pretext's canvas `measureText`
7993
+ * can't agree with what the browser actually resolved (pretext's own README flags `system-ui` as
7994
+ * unsafe on macOS). A run whose first family token is one of these has no capture-matched named
7995
+ * font, so the rep falls back. The list is the CSS generic set plus the `ui-*` system aliases.
8073
7996
  */
8074
- const GENERIC_FONT_FAMILIES = new Set([
7997
+ const GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set([
8075
7998
  "serif",
8076
7999
  "sans-serif",
8077
8000
  "monospace",
@@ -8092,10 +8015,10 @@ const GENERIC_FONT_FAMILIES = new Set([
8092
8015
  "revert-layer"
8093
8016
  ]);
8094
8017
  /**
8095
- * The first font-family token, unquoted and lowercased, or null when the list is
8096
- * empty. `getComputedStyle().fontFamily` serializes the resolved list (the
8097
- * first entry is the face the browser used for this run), comma-separated with
8098
- * quoted multi-word names — `"Helvetica Neue", Arial, sans-serif` → `helvetica neue`.
8018
+ * The first font-family token, unquoted and lowercased, or null when the list is empty.
8019
+ * `getComputedStyle().fontFamily` serializes the resolved list (the first entry is the face the
8020
+ * browser used for this run), comma-separated with quoted multi-word names — `"Helvetica Neue",
8021
+ * Arial, sans-serif` → `helvetica neue`.
8099
8022
  */
8100
8023
  function firstFontFamily(fontFamily) {
8101
8024
  const first = splitTopLevelComma(fontFamily)[0];
@@ -8122,11 +8045,11 @@ function splitTopLevelComma(value) {
8122
8045
  return out;
8123
8046
  }
8124
8047
  /**
8125
- * Build the canvas `font` shorthand pretext wants — `<style> <weight> <size>px
8126
- * <family-list>` — from the run's resolved computed style, mirroring CSS exactly
8127
- * (the families pass through verbatim, quotes and all, so the canvas resolves the
8128
- * SAME face). Returns null when the size is not a usable px length. Order follows
8129
- * the canvas/CSS `font` shorthand: style, then weight, then size, then family.
8048
+ * Build the canvas `font` shorthand pretext wants — `<style> <weight> <size>px <family-list>` —
8049
+ * from the run's resolved computed style, mirroring CSS exactly (the families pass through
8050
+ * verbatim, quotes and all, so the canvas resolves the SAME face). Returns null when the size is
8051
+ * not a usable px length. Order follows the canvas/CSS `font` shorthand: style, then weight, then
8052
+ * size, then family.
8130
8053
  */
8131
8054
  function canvasFontShorthand(cs) {
8132
8055
  const sizePx = pxValue(cs.fontSize);
@@ -8139,14 +8062,12 @@ function canvasFontShorthand(cs) {
8139
8062
  return parts.join(" ");
8140
8063
  }
8141
8064
  /**
8142
- * Does this environment have real canvas text metrics (the feature gate for the
8143
- * whole rep)? pretext relies on Canvas 2D `measureText` AND `Intl.Segmenter`
8144
- * (its documented runtime requirement); a layout-less env (happy-dom in tests,
8145
- * older runtimes) has neither, so `prepare()`/`layout()` cannot compute there.
8146
- * We probe once per capture: a 2d context whose `measureText` returns a positive
8147
- * width for a known glyph, plus a present `Intl.Segmenter`. When this is false
8148
- * the rep is skipped entirely and the run takes the existing pill-bar fallback,
8149
- * keeping the no-canvas path byte-identical to before this change.
8065
+ * Does this environment have real canvas text metrics (the feature gate for the whole rep)? pretext
8066
+ * relies on Canvas 2D `measureText` AND `Intl.Segmenter` (its documented runtime requirement); a
8067
+ * layout-less env (happy-dom in tests, older runtimes) has neither, so `prepare()`/`layout()`
8068
+ * cannot compute there. Probe once per capture: a 2d context whose `measureText` returns a positive
8069
+ * width for a known glyph, plus a present `Intl.Segmenter`. When this is false the rep is skipped
8070
+ * entirely and the run takes the pill-bar fallback.
8150
8071
  */
8151
8072
  function hasUsableTextMetrics(win) {
8152
8073
  if (!("Intl" in win) || !("Segmenter" in Intl)) return false;
@@ -8160,11 +8081,10 @@ function hasUsableTextMetrics(win) {
8160
8081
  }
8161
8082
  }
8162
8083
  /**
8163
- * The nearest block-level ancestor that establishes the run's wrapping width,
8164
- * starting from the text's parent. Inline wrappers (`<b>`, `<span>`) don't bound
8165
- * wrapping; the first non-inline ancestor's content box is the available width
8166
- * the injected `container-type: inline-size` wrapper will assume (it fills its
8167
- * slot). Returns the parent itself when it is already block-level.
8084
+ * The nearest block-level ancestor that establishes the run's wrapping width, starting from the
8085
+ * text's parent. Inline wrappers (`<b>`, `<span>`) don't bound wrapping; the first non-inline
8086
+ * ancestor's content box is the available width the injected `container-type: inline-size` wrapper
8087
+ * will assume (it fills its slot). Returns the parent itself when it is already block-level.
8168
8088
  */
8169
8089
  function wrappingBlock(parent, styles) {
8170
8090
  for (let host = parent; host; host = host.parentElement) if (!isInlineish(styles.get(host).display)) return host;
@@ -8176,12 +8096,10 @@ function innerContentWidth(el, styles) {
8176
8096
  return el.getBoundingClientRect().width - pad(cs.paddingLeft) - pad(cs.paddingRight) - pad(cs.borderLeftWidth) - pad(cs.borderRightWidth);
8177
8097
  }
8178
8098
  /**
8179
- * Count the run's rendered lines from its `Range` rects: `getClientRects()`
8180
- * yields one rect per line fragment, so distinct `top` values (collapsed within
8181
- * 1px) are the line count. This is the same ground truth the spike validated
8182
- * pretext against; here it is the capture-time AGREEMENT check if pretext's
8183
- * predicted line count at the capture width disagrees with what the browser
8184
- * actually rendered, the rep would be wrong for this run and we fall back.
8099
+ * Count the run's rendered lines from its `Range` rects: `getClientRects()` yields one rect per
8100
+ * line fragment, so distinct `top` values (collapsed within 1px) are the line count. The
8101
+ * capture-time AGREEMENT check: if pretext's predicted line count at the capture width disagrees
8102
+ * with what the browser actually rendered, the rep would be wrong for this run, so it falls back.
8185
8103
  */
8186
8104
  function renderedLineCount(range) {
8187
8105
  const rects = Array.from(range.getClientRects());
@@ -8193,25 +8111,22 @@ function renderedLineCount(range) {
8193
8111
  return Math.max(1, tops.length);
8194
8112
  }
8195
8113
  /**
8196
- * The active `-webkit-line-clamp` line cap governing the run, or null. The clamp
8197
- * idiom (`display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp:
8198
- * N; overflow: hidden`) clips text to N lines, so a clamped run NEVER shows more
8199
- * than N no matter how narrow the container gets. pretext measures the UNCLAMPED
8200
- * wrap, and a `Range`'s rects ignore the clip too (`renderedLineCount`), so the
8201
- * agreement check passes on the full count while the run actually renders at most
8202
- * N without this the rep would emit bands TALLER than the clamp ever shows.
8203
- * Capping the band sweep at N makes a clamped block exact and bounds its stack to
8204
- * ≤N. Walks ancestors (the clamp may sit a few wrappers above the text node) and
8205
- * returns the nearest ACTIVE clamp a positive integer `-webkit-line-clamp` on a
8206
- * vertical `-webkit-box-orient` (the idiom). A clamp value without the vertical
8207
- * orientation has no effect and is ignored, matching the browser.
8114
+ * The active `-webkit-line-clamp` line cap governing the run, or null. The clamp idiom (`display:
8115
+ * -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: N; overflow: hidden`) clips text
8116
+ * to N lines, so a clamped run NEVER shows more than N no matter how narrow the container gets.
8117
+ * pretext measures the UNCLAMPED wrap, and a `Range`'s rects ignore the clip too
8118
+ * (`renderedLineCount`), so the agreement check passes on the full count while the run actually
8119
+ * renders at most N — without this the rep would emit bands TALLER than the clamp ever shows.
8120
+ * Capping the band sweep at N makes a clamped block exact and bounds its stack to ≤N. Walks
8121
+ * ancestors (the clamp may sit a few wrappers above the text node) and returns the nearest ACTIVE
8122
+ * clamp a positive integer `-webkit-line-clamp` on a vertical `-webkit-box-orient` (the idiom). A
8123
+ * clamp value without the vertical orientation has no effect and is ignored, matching the browser.
8208
8124
  *
8209
- * Detection keys on the line-clamp + box-orient PAIR, deliberately NOT on
8210
- * computed `display`: Chrome normalizes an active `display: -webkit-box` clamp to
8211
- * `display: flow-root` in computed style (verified), so a `display === -webkit-box`
8212
- * guard silently never matched in Chrome the bug this comment guards against.
8213
- * `-webkit-box-orient` and `-webkit-line-clamp` ARE exposed in computed style, so
8214
- * the pair is the reliable cross-engine signal.
8125
+ * Detection keys on the line-clamp + box-orient PAIR, deliberately NOT on computed `display`:
8126
+ * Chrome normalizes an active `display: -webkit-box` clamp to `display: flow-root` in computed
8127
+ * style (verified), so a `display === -webkit-box` guard silently never matched in Chrome — the bug
8128
+ * this comment guards against. `-webkit-box-orient` and `-webkit-line-clamp` ARE exposed in
8129
+ * computed style, so the pair is the reliable cross-engine signal.
8215
8130
  */
8216
8131
  function activeLineClamp(parent, styles) {
8217
8132
  for (let el = parent; el; el = el.parentElement) {
@@ -8224,24 +8139,21 @@ function activeLineClamp(parent, styles) {
8224
8139
  return null;
8225
8140
  }
8226
8141
  /**
8227
- * The line-count step function across the width range, swept at 1px so each
8228
- * band threshold lands on the exact width the run changes line count (the spike
8229
- * confirmed these coincide with the browser's real wrap points to the pixel).
8230
- * Walks widths ascending and records every transition; the first entry is the
8231
- * baseline (most lines, narrowest), each later entry a `min-width` step.
8232
- * `maxWidth` caps the sweep at the largest realistic container width (the run may
8233
- * fit on one line before reaching it). `lines` is clamped to `maxLines` (the
8234
- * caller passes `MAX_REP_LINES`, or a smaller `-webkit-line-clamp` cap), so the
8235
- * band stack is bounded no matter how many lines the run wraps to at the narrow
8236
- * end.
8142
+ * The line-count step function across the width range, swept at 1px so each band threshold lands on
8143
+ * the exact width the run changes line count (measured to coincide with the browser's real wrap
8144
+ * points to the pixel). Walks widths ascending and records every transition; the first entry is the
8145
+ * baseline (most lines, narrowest), each later entry a `min-width` step. `maxWidth` caps the sweep
8146
+ * at the largest realistic container width (the run may fit on one line before reaching it).
8147
+ * `lines` is clamped to `maxLines` (the caller passes `MAX_REP_LINES`, or a smaller
8148
+ * `-webkit-line-clamp` cap), so the band stack is bounded no matter how many lines the run wraps to
8149
+ * at the narrow end.
8237
8150
  */
8238
8151
  /**
8239
- * Cap on the line-count bands per run. A run that wraps to more lines than this at
8240
- * the narrow end clamps here (a tall block reads as "tall" — a line or two off at
8241
- * an extreme-narrow width is imperceptible on swap), so the band stack can never
8242
- * explode — an unbounded sweep produced 40–70 bands per run on real content. Kept
8243
- * above realistic mobile paragraph line counts so it does not clip genuine content.
8244
- * Tunable.
8152
+ * Cap on the line-count bands per run. A run that wraps to more lines than this at the narrow end
8153
+ * clamps here (a tall block reads as "tall" — a line or two off at an extreme-narrow width is
8154
+ * imperceptible on swap), so the band stack can never explode — an unbounded sweep produced 40–70
8155
+ * bands per run on real content. Kept above realistic mobile paragraph line counts so it does not
8156
+ * clip genuine content. Tunable.
8245
8157
  */
8246
8158
  const MAX_REP_LINES = 16;
8247
8159
  function lineCountStepFunction(prepared, lh, minWidth, maxWidth, maxLines) {
@@ -8261,45 +8173,41 @@ function lineCountStepFunction(prepared, lh, minWidth, maxWidth, maxLines) {
8261
8173
  return bands;
8262
8174
  }
8263
8175
  /**
8264
- * The minimum container width to sweep from. The bone never renders narrower
8265
- * than one line-height tall, and sweeping below a few px is wasted work; a small
8266
- * floor keeps the sweep bounded for a pathologically long run. Real containers
8267
- * are far wider, so this only caps the bottom of the band stack.
8176
+ * The minimum container width to sweep from. The bone never renders narrower than one line-height
8177
+ * tall, and sweeping below a few px is wasted work; a small floor keeps the sweep bounded for a
8178
+ * pathologically long run. Real containers are far wider, so this only caps the bottom of the band
8179
+ * stack.
8268
8180
  */
8269
8181
  const REP_MIN_WIDTH = 16;
8270
8182
  /**
8271
- * Cap on the sweep's upper width: the largest realistic container width. The rep
8272
- * runs during the walk, before per-plate breakpoints are derived, so it cannot
8273
- * read the actual biggest breakpoint — this fixed bound stands in for it. Beyond
8274
- * it (a text run spanning a full ultra-wide viewport), a height mismatch on swap
8275
- * is imperceptible against that much space, so sweeping out to the run's
8276
- * single-line width would only emit wasted bands. Tunable; ≈ the largest common
8277
- * breakpoint.
8183
+ * Cap on the sweep's upper width: the largest realistic container width. The rep runs during the
8184
+ * walk, before per-plate breakpoints are derived, so it cannot read the actual biggest breakpoint —
8185
+ * this fixed bound stands in for it. Beyond it (a text run spanning a full ultra-wide viewport), a
8186
+ * height mismatch on swap is imperceptible against that much space, so sweeping out to the run's
8187
+ * single-line width would only emit wasted bands. Tunable; the largest common breakpoint.
8278
8188
  */
8279
8189
  const REP_MAX_WIDTH = 1920;
8280
8190
  /**
8281
- * Attempt the `@container` line-count rep for a wrapping text run (ADR 0019 §2).
8282
- * Returns the leaf-scoped wrapper `IRNode` (a branch with one full-width text
8283
- * bone child, both pushed to `state.nodes`) and emits its reflow band rules onto
8284
- * `state`, OR returns null to signal the caller to keep the existing measured
8285
- * pill bar. Null is returned for everything the rep can't safely cover, so a
8286
- * fallback is always graceful and never a regression:
8191
+ * Attempt the `@container` line-count rep for a wrapping text run (ADR 0019 §2). Returns the
8192
+ * leaf-scoped wrapper `IRNode` (a branch with one full-width text bone child, both pushed to
8193
+ * `state.nodes`) and emits its reflow band rules onto `state`, OR returns null to signal the caller
8194
+ * to keep the existing measured pill bar. Null is returned for everything the rep can't safely
8195
+ * cover, so a fallback is always graceful and never a regression:
8287
8196
  *
8288
- * - no usable canvas text metrics (every test env; older runtimes) — feature gate;
8289
- * - no resolved parent / no named capture-matched font / no px line-height;
8290
- * - a single-line or short run (stays the thin pill bar — a fat block misreads);
8291
- * - an out-of-model wrap mode pretext doesn't model: `text-align: justify`,
8292
- * `hyphens: auto`, `word-break: break-all`, `white-space` ≠ normal/pre-wrap;
8293
- * - an unbreakable token wider than the container (the spike's failure: the
8294
- * browser overflows it on one line, pretext wraps it — they diverge);
8295
- * - pretext's predicted line count disagreeing with the browser's actual wrap
8296
- * at the capture width (a self-check that also catches font substitution).
8197
+ * - No usable canvas text metrics (every test env; older runtimes) — feature gate;
8198
+ * - No resolved parent / no named capture-matched font / no px line-height;
8199
+ * - A single-line or short run (stays the thin pill bar — a fat block misreads);
8200
+ * - An out-of-model wrap mode pretext doesn't model: `text-align: justify`, `hyphens: auto`,
8201
+ * `word-break: break-all`, `white-space` ≠ normal/pre-wrap;
8202
+ * - An unbreakable token wider than the container (the browser overflows it on one line, pretext
8203
+ * wraps it — they diverge);
8204
+ * - Pretext's predicted line count disagreeing with the browser's actual wrap at the capture width (a
8205
+ * self-check that also catches font substitution).
8297
8206
  *
8298
- * On success the wrapper carries `container-type: inline-size` + a plate-local
8299
- * unique `container-name`, and the bone gets `@container <name> (min-width: …)
8300
- * { height: calc(N × lh) }` bands — reflowing by CSS alone, no frozen height,
8301
- * no render JS. v1 ALWAYS injects a fresh named container; reusing an immediate
8302
- * author container (ADR 0019 §2) is a deferred optimization.
8207
+ * On success the wrapper carries `container-type: inline-size` + a plate-local unique
8208
+ * `container-name`, and the bone gets `@container <name> (min-width: …) { height: calc(N × lh) }`
8209
+ * bands — reflowing by CSS alone, no frozen height, no render JS. v1 ALWAYS injects a fresh named
8210
+ * container; reusing an immediate author container (ADR 0019 §2) is a deferred optimization.
8303
8211
  */
8304
8212
  function textLineCountRep(run, range, height, parent, state, depth) {
8305
8213
  if (!parent) return null;
@@ -8395,12 +8303,11 @@ function textLineCountRep(run, range, height, parent, state, depth) {
8395
8303
  return wrapperNode;
8396
8304
  }
8397
8305
  /**
8398
- * A `Range` rect is the tight union of the glyph line boxes, so a wrapped run
8399
- * measures lines × glyph-height — short of the lines × line-height the source
8400
- * actually occupied (a 2-line paragraph rendered ~3px short per line). When the
8401
- * parent's line-height resolves to px and the run is clearly multi-line, round
8402
- * to a whole number of line boxes so the bone reserves the real block height.
8403
- * A single line is left untouched — its line box comes from the parent's pinned
8306
+ * A `Range` rect is the tight union of the glyph line boxes, so a wrapped run measures lines ×
8307
+ * glyph-height — short of the lines × line-height the source actually occupied (a 2-line paragraph
8308
+ * rendered ~3px short per line). When the parent's line-height resolves to px and the run is
8309
+ * clearly multi-line, round to a whole number of line boxes so the bone reserves the real block
8310
+ * height. A single line is left untouched its line box comes from the parent's pinned
8404
8311
  * line-height, and the thin glyph bar reads as text rather than a fat block.
8405
8312
  */
8406
8313
  function snapToLines(height, parent, styles) {
@@ -8603,12 +8510,11 @@ function rightmostCompound(selector) {
8603
8510
  return selector;
8604
8511
  }
8605
8512
  /**
8606
- * Index author rules the way a browser's rule hash does: bucket each selector
8607
- * by a token its matching element must carry (id, a class, or tag), so an
8608
- * element only tests the rules that could possibly apply to it. CSS-module
8609
- * class names are unique, so this turns an O(rules × nodes) sweep into a
8610
- * near-linear one. Selectors keyed on nothing concrete (`*`, attribute-only)
8611
- * fall back to a universal bucket tested against every element.
8513
+ * Index author rules the way a browser's rule hash does: bucket each selector by a token its
8514
+ * matching element must carry (id, a class, or tag), so an element only tests the rules that could
8515
+ * possibly apply to it. CSS-module class names are unique, so this turns an O(rules × nodes) sweep
8516
+ * into a near-linear one. Selectors keyed on nothing concrete (`*`, attribute-only) fall back to a
8517
+ * universal bucket tested against every element.
8612
8518
  */
8613
8519
  function indexRules(found, diagnostics) {
8614
8520
  const index = {
@@ -8654,15 +8560,14 @@ function indexRules(found, diagnostics) {
8654
8560
  return index;
8655
8561
  }
8656
8562
  /**
8657
- * Scan every flattened author rule for custom-property declarations and group
8658
- * them by token name. A token may be defined many times (a `:root` base plus an
8659
- * `@media` override, or per-theme blocks); each definition keeps its conditions
8660
- * and selector so the carry decision can see whether ANY of them is conditional
8661
- * and where each anchors. Reuses the SAME flatten `copyRules` already paid for
8662
- * (`collectStyleRules`), so this adds one extra pass over the rules, no new CSSOM
8663
- * traversal. Dynamic-pseudo / pseudo-element rules never define layout tokens we
8664
- * carry, but they are cheap to include and harmless (their selectors won't anchor
8665
- * to a captured node or to :root), so the scan stays simple and skips nothing.
8563
+ * Scan every flattened author rule for custom-property declarations and group them by token name. A
8564
+ * token may be defined many times (a `:root` base plus an `@media` override, or per-theme blocks);
8565
+ * each definition keeps its conditions and selector so the carry decision can see whether ANY of
8566
+ * them is conditional and where each anchors. Reuses the SAME flatten `copyRules` already paid for
8567
+ * (`collectStyleRules`), so this adds one extra pass over the rules, no new CSSOM traversal.
8568
+ * Dynamic-pseudo / pseudo-element rules never define carried layout tokens, but they are cheap to
8569
+ * include and harmless (their selectors won't anchor to a captured node or to :root), so the scan
8570
+ * stays simple and skips nothing.
8666
8571
  */
8667
8572
  function collectTokenDefs(found) {
8668
8573
  const defs = /* @__PURE__ */ new Map();
@@ -8695,12 +8600,11 @@ function collectTokenDefs(found) {
8695
8600
  return defs;
8696
8601
  }
8697
8602
  /**
8698
- * A selector that puts a custom property in scope for the WHOLE captured subtree
8699
- * — `:root`, `html`, or the universal `*` (optionally wrapped in zero-specificity
8700
- * `:where()`/`:is()`, and ignoring a trailing dynamic state). Such a definition
8701
- * inherits down into every node, so it anchors on the plate's scoped root (node
8702
- * id 0). Anything more specific is an element-level (re)definition handled by
8703
- * matching it against the captured elements instead.
8603
+ * A selector that puts a custom property in scope for the WHOLE captured subtree — `:root`, `html`,
8604
+ * or the universal `*` (optionally wrapped in zero-specificity `:where()`/`:is()`, and ignoring a
8605
+ * trailing dynamic state). Such a definition inherits down into every node, so it anchors on the
8606
+ * plate's scoped root (node id 0). Anything more specific is an element-level (re)definition
8607
+ * handled by matching it against the captured elements instead.
8704
8608
  */
8705
8609
  const ROOT_SCOPE_SELECTOR = /^(?::root|html|\*)$/;
8706
8610
  function isRootScopeSelector(selector) {
@@ -8712,12 +8616,11 @@ function hasConditionalDef(defs) {
8712
8616
  return defs.some((def) => def.media.length > 0 || def.container.length > 0);
8713
8617
  }
8714
8618
  /**
8715
- * The plate-id a token definition anchors to, or null when it cannot be anchored
8716
- * on the captured tree (an un-captured wrapper — Q5). Root-scope selectors
8717
- * (`:root`/`html`/`*`) anchor on the scoped root (id 0, emitted as `ids: []`);
8718
- * an element-level definition anchors on every captured element it matches (a
8719
- * token redefined on a card anchors on that card's node). A definition that
8720
- * matches NO captured element and is not root-scope is un-anchorable.
8619
+ * The plate-id a token definition anchors to, or null when it cannot be anchored on the captured
8620
+ * tree (an un-captured wrapper). Root-scope selectors (`:root`/`html`/`*`) anchor on the scoped
8621
+ * root (id 0, emitted as `ids: []`); an element-level definition anchors on every captured element
8622
+ * it matches (a token redefined on a card anchors on that card's node). A definition that matches
8623
+ * NO captured element and is not root-scope is un-anchorable.
8721
8624
  */
8722
8625
  function anchorIdsFor(def, elNodes) {
8723
8626
  if (isRootScopeSelector(def.selector)) return [0];
@@ -8726,13 +8629,12 @@ function anchorIdsFor(def, elNodes) {
8726
8629
  return ids.length > 0 ? ids : null;
8727
8630
  }
8728
8631
  /**
8729
- * The winning UNCONDITIONAL definition value of a token — its constant value,
8730
- * picked by the cascade among definitions that carry no `@media`/`@container`
8731
- * condition: highest specificity, then latest source order. Null when the token
8732
- * has no unconditional definition (it only exists inside a query purely
8733
- * conditional). This is what a constant token freezes to, sourced from the author
8734
- * CSSOM rather than computed style, so the freeze is correct even where computed
8735
- * style cannot resolve an inherited custom property.
8632
+ * The winning UNCONDITIONAL definition value of a token — its constant value, picked by the cascade
8633
+ * among definitions that carry no `@media`/`@container` condition: highest specificity, then latest
8634
+ * source order. Null when the token has no unconditional definition (it only exists inside a query
8635
+ * purely conditional). This is what a constant token freezes to, sourced from the author CSSOM
8636
+ * rather than computed style, so the freeze is correct even where computed style cannot resolve an
8637
+ * inherited custom property.
8736
8638
  */
8737
8639
  function constantValueOf(defs) {
8738
8640
  let best = null;
@@ -8927,39 +8829,39 @@ function attachCarried(nodeById, id, rule) {
8927
8829
  (node.decls.carried ??= []).push(rule);
8928
8830
  }
8929
8831
  //#endregion
8930
- //#region src/serialize.ts
8832
+ //#region src/measure.ts
8931
8833
  /**
8932
- * Capture-only DOM annotations (ADR 0018), read DURING the walk to shape the
8933
- * `PlateNode` tree and NEVER written into the tree, the Plate, or the persisted
8934
- * JSON. They are the declarative 90% of capture customization; the programmable
8935
- * `captureWalker` is the 10% escape hatch.
8834
+ * Capture-only DOM Hints (ADR 0018), read DURING the walk to shape the `PlateNode` tree and NEVER
8835
+ * written into the tree, the Plate, or the persisted JSON. They are the declarative 90% of capture
8836
+ * customization; the programmable `captureWalker` is the 10% escape hatch.
8936
8837
  *
8937
8838
  * - `data-xr-ignore` drops the element AND its subtree (it contributes no bone).
8938
- * - `data-xr-bone-kind="text|media|box"` collapses the element's whole subtree
8839
+ * - `data-xr-bone-kind="text-line|text-block|media|box|hidden"` collapses the element's whole subtree
8939
8840
  * into ONE Bone leaf of the given kind, sized from the element's own box.
8940
8841
  *
8941
- * Both are subordinate to the stitch guard (ADR 0006): a nested `<Skeleton>`
8942
- * boundary always becomes a stitch first, so neither annotation can make a
8943
- * boundary leak into the parent plate. See `walk`.
8842
+ * Both are subordinate to the stitch guard (ADR 0006): a nested `<Skeleton>` boundary always
8843
+ * becomes a stitch first, so neither hint can make a boundary leak into the parent plate. See
8844
+ * `walk`.
8944
8845
  */
8945
8846
  const XR_IGNORE_ATTR = "data-xr-ignore";
8946
8847
  const XR_BONE_KIND_ATTR = "data-xr-bone-kind";
8947
8848
  /** The `LeafKind` values an author may legally name in `data-xr-bone-kind`. */
8948
8849
  const LEAF_KINDS = [
8949
- "text",
8850
+ "text-line",
8950
8851
  "text-block",
8951
8852
  "media",
8952
- "box"
8853
+ "box",
8854
+ "hidden"
8953
8855
  ];
8954
8856
  /** Narrow an arbitrary `data-xr-bone-kind` string to a `LeafKind`, or null if unknown. */
8955
8857
  function asLeafKind(value) {
8956
8858
  return LEAF_KINDS.find((kind) => kind === value) ?? null;
8957
8859
  }
8958
8860
  /**
8959
- * Typed identity wrapper for authoring a capture walker (ADR 0018) — the
8960
- * `defineConfig` pattern. It returns its argument unchanged at runtime; its only
8961
- * job is to infer and check the `XrayCaptureWalker` shape at the call site so an
8962
- * author gets completion and a typo in a hook name is caught.
8861
+ * Typed identity wrapper for authoring a capture walker (ADR 0018) — the `defineConfig` pattern. It
8862
+ * returns its argument unchanged at runtime; its only job is to infer and check the
8863
+ * `XrayCaptureWalker` shape at the call site so an author gets completion and a typo in a hook name
8864
+ * is caught.
8963
8865
  */
8964
8866
  function defineXrayCaptureWalker(walker) {
8965
8867
  return walker;
@@ -8970,7 +8872,7 @@ function isElementNode(node) {
8970
8872
  function isTextNode(node) {
8971
8873
  return node.nodeType === 3;
8972
8874
  }
8973
- const SKIP_TAGS = new Set([
8875
+ const SKIP_TAGS = /* @__PURE__ */ new Set([
8974
8876
  "SCRIPT",
8975
8877
  "STYLE",
8976
8878
  "LINK",
@@ -8979,7 +8881,7 @@ const SKIP_TAGS = new Set([
8979
8881
  "NOSCRIPT",
8980
8882
  "TITLE"
8981
8883
  ]);
8982
- const MEDIA_TAGS = new Set([
8884
+ const MEDIA_TAGS = /* @__PURE__ */ new Set([
8983
8885
  "IMG",
8984
8886
  "SVG",
8985
8887
  "VIDEO",
@@ -9002,10 +8904,10 @@ const RUN_BREAKERS = [
9002
8904
  `[${ROOT_ATTR}]`
9003
8905
  ].join(",");
9004
8906
  /**
9005
- * A child element whose glyph ink belongs to its parent block's run rather than
9006
- * to a Bone of its own: a pure `display: inline` text wrapper (`<span>`, `<a>`,
9007
- * `<em>`) with no replaced-media or nested-Skeleton descendant. Inline-block and
9008
- * friends are atomic boxes, not text flow, so they are NOT folded.
8907
+ * A child element whose glyph ink belongs to its parent block's run rather than to a Bone of its
8908
+ * own: a pure `display: inline` text wrapper (`<span>`, `<a>`, `<em>`) with no replaced-media or
8909
+ * nested-Skeleton descendant. Inline-block and friends are atomic boxes, not text flow, so they are
8910
+ * NOT folded.
9009
8911
  */
9010
8912
  function isInlineTextElement(el, cs, styles) {
9011
8913
  if (cs.display !== "inline") return false;
@@ -9018,7 +8920,10 @@ function isInlineTextElement(el, cs, styles) {
9018
8920
  }
9019
8921
  return true;
9020
8922
  }
9021
- /** Collect the rendered text nodes under an inline text element into a run, skipping non-rendered tags. */
8923
+ /**
8924
+ * Collect the rendered text nodes under an inline text element into a run, skipping non-rendered
8925
+ * tags.
8926
+ */
9022
8927
  function collectInlineText(el, into) {
9023
8928
  for (const child of Array.from(el.childNodes)) if (isTextNode(child)) into.push(child);
9024
8929
  else if (isElementNode(child) && !SKIP_TAGS.has(child.tagName.toUpperCase())) collectInlineText(child, into);
@@ -9035,9 +8940,9 @@ function runTextRect(run) {
9035
8940
  return r.getBoundingClientRect();
9036
8941
  }
9037
8942
  /**
9038
- * The rect of the text a leading glyph-media would abut: the immediately
9039
- * following text node or inline text element (e.g. the "4.9" after a "★" icon).
9040
- * Replaced media and absent/blank text yield null — a glyph needs text to hug.
8943
+ * The rect of the text a leading glyph-media would abut: the immediately following text node or
8944
+ * inline text element (e.g. the "4.9" after a "★" icon). Replaced media and absent/blank text yield
8945
+ * null — a glyph needs text to hug.
9041
8946
  */
9042
8947
  function nextTextRect(nodes, i, styles) {
9043
8948
  const next = nodes[i + 1];
@@ -9054,15 +8959,13 @@ function nextTextRect(nodes, i, styles) {
9054
8959
  return null;
9055
8960
  }
9056
8961
  /**
9057
- * The pure GEOMETRY of the glyph-fold (ADR 0021 §2): does a media rect read as a
9058
- * GLYPH hugging the text `adj` on the same ROW — emoji-sized (≤ 1.5× line-height),
9059
- * sharing the line (≥ 50% vertical overlap), and abutting within ~a space (gap ≤
9060
- * 0.6× line-height)? A bigger icon, or one set off by a gutter (a section-heading
9061
- * icon), fails and stays its own Bone. DISPLAY-AGNOSTIC on purpose: the caller
9062
- * decides whether to also require inline-level (`foldsAsGlyph`, the in-flow path)
9063
- * or rely on geometry alone (the flex/grid "row anything" post-pass,
9064
- * `foldGlyphRows`). `adj` is the text it abuts. This size + spacing test is
9065
- * Salience's surviving role.
8962
+ * The pure GEOMETRY of the glyph-fold (ADR 0021 §2): does a media rect read as a GLYPH hugging the
8963
+ * text `adj` on the same ROW — emoji-sized (≤ 1.5× line-height), sharing the line (≥ 50% vertical
8964
+ * overlap), and abutting within ~a space (gap ≤ 0.6× line-height)? A bigger icon, or one set off by
8965
+ * a gutter (a section-heading icon), fails and stays its own Bone. DISPLAY-AGNOSTIC on purpose: the
8966
+ * caller decides whether to also require inline-level (`foldsAsGlyph`, the in-flow path) or rely on
8967
+ * geometry alone (the flex/grid "row anything" post-pass, `foldGlyphRows`). `adj` is the text it
8968
+ * abuts. This size + spacing test is Salience's surviving role.
9066
8969
  */
9067
8970
  function glyphGeometry(mediaRect, adj, lineHeight) {
9068
8971
  if (lineHeight <= 0 || !adj) return false;
@@ -9074,27 +8977,23 @@ function foldsAsGlyph(mediaCs, mediaRect, adj, lineHeight) {
9074
8977
  return isInlineish(mediaCs.display) && glyphGeometry(mediaRect, adj, lineHeight);
9075
8978
  }
9076
8979
  /**
9077
- * APPLY-WALKER — the programmable capture walker as a PASS over the measured IR
9078
- * (ADR 0018; ADR 0022 §5). Replaces the old mid-walk ladder: the walker no longer
9079
- * runs DURING the DOM walk (so the full subtree is always measured), it is offered
9080
- * each element TOP-DOWN over the built IR and its decision is LOWERED here by
9081
- * setting the same `markKind`/`dropped` fields the `data-xr-*` attribute path sets
9082
- * (Loop 6.1), so `markInk` (which runs next, in `build`) realizes them uniformly:
9083
- * - `ignore` → `dropped: { reason: 'ignored' }`; the node + subtree lower (NOT
9084
- * recursed — the descent stops, mirroring the old walk-time `ctx.ignore()`).
9085
- * - `bone` → `markKind = kind ?? inferLeafKind(el)`; `markInk`'s re-stamp lowers
9086
- * it to ONE childless bone of that kind (NOT recursed — the old `ctx.bone()`
9087
- * never descended either).
9088
- * - `keep` (or no `element` hook) → recurse into the kid's kids (defer to the
9089
- * default classifier).
8980
+ * APPLY-WALKER — the programmable capture walker as a PASS over the measured IR (ADR 0018; ADR 0022
8981
+ * §5). The walker does not run during the DOM walk (the full subtree is always measured); each
8982
+ * element is offered TOP-DOWN over the built IR and the decision is LOWERED here by setting the
8983
+ * same `markKind`/`dropped` fields the `data-xr-*` hint path sets, so `markInk` (which runs next,
8984
+ * in `build`) realizes them uniformly:
8985
+ *
8986
+ * - `ignore` → `dropped: { reason: 'ignored' }`; the node + subtree lower (NOT recursed — the descent
8987
+ * stops).
8988
+ * - `bone` → `markKind = kind ?? inferLeafKind(el)`; `markInk`'s re-stamp lowers it to ONE childless
8989
+ * bone of that kind (NOT recursed).
8990
+ * - `keep` (or no `element` hook) → recurse into the kid's kids (defer to the default classifier).
9090
8991
  *
9091
- * Iterates a node's KIDS rather than visiting the node itself, so the synthetic
9092
- * root / synthetic text bars (`el === null`) are never offered — but their kids
9093
- * are still visited. A stitch (`kind: 'stitch'`, no `el`) is skipped (opaque —
9094
- * ADR 0006: a walker can never reach a nested `<Skeleton>`), and a kid the walker
9095
- * ignores/bones is not recursed (the pruning the old walk got for free by
9096
- * returning before descending). Pure on `el` — the `ctx` helpers return a
9097
- * decision; this pass is the only mutator.
8992
+ * Iterates a node's KIDS rather than visiting the node itself, so the synthetic root / synthetic
8993
+ * text bars (`el === null`) are never offered — but their kids are still visited. A stitch (`kind:
8994
+ * 'stitch'`, no `el`) is skipped (opaque — ADR 0006: a walker can never reach a nested
8995
+ * `<Skeleton>`), and a kid the walker ignores/bones is not recursed. Pure on `el` the `ctx`
8996
+ * helpers return a decision; this pass is the only mutator.
9098
8997
  */
9099
8998
  function applyWalker(node, walker) {
9100
8999
  if (!walker.element) return;
@@ -9132,13 +9031,12 @@ function parseRgb(value) {
9132
9031
  }
9133
9032
  const rgbCache = /* @__PURE__ */ new Map();
9134
9033
  /**
9135
- * Resolve any computed CSS colour to sRGB bytes. `rgb()/rgba()` parse directly
9136
- * (the cheap path, and the only one a layout-less test env needs); everything
9137
- * else — `oklch()`, `color()`, `hsl()`, named — is painted onto a 1×1 canvas
9138
- * and read back, since browsers serialise a computed colour in the space it was
9139
- * authored in (Chrome returns `oklch(...)` verbatim), which a regex can't read.
9140
- * Without this an oklch-filled control reads as no fill and never becomes a box.
9141
- * Cached by string: the same token recurs across every node of a themed card.
9034
+ * Resolve any computed CSS colour to sRGB bytes. `rgb()/rgba()` parse directly (the cheap path, and
9035
+ * the only one a layout-less test env needs); everything else — `oklch()`, `color()`, `hsl()`,
9036
+ * named — is painted onto a 1×1 canvas and read back, since browsers serialise a computed colour in
9037
+ * the space it was authored in (Chrome returns `oklch(...)` verbatim), which a regex can't read.
9038
+ * Without this an oklch-filled control reads as no fill and never becomes a box. Cached by string:
9039
+ * the same token recurs across every node of a themed card.
9142
9040
  */
9143
9041
  function toRgb(value, win) {
9144
9042
  const direct = parseRgb(value);
@@ -9177,13 +9075,12 @@ function hasVisibleBackground(cs, win) {
9177
9075
  /** Fill that visibly contrasts with the nearest painted ancestor — meaning ~24 per channel. */
9178
9076
  const FILL_CONTRAST = 72;
9179
9077
  /**
9180
- * The nearest ancestor that actually paints a background, resolved to sRGB, or
9181
- * null when nothing up the chain paints. Ancestor styles come from the
9182
- * per-capture memo (piece 5): a deep card's parents are climbed once per check
9183
- * but resolved once per capture. This is the `behind` colour both the
9184
- * `hasContrastingFill` boolean and the Salience contrast axis score a node's own
9185
- * fill/border against extracted so the two read the SAME measured ground (the
9186
- * salience.ts scorer takes `behind` as an input rather than climbing itself).
9078
+ * The nearest ancestor that actually paints a background, resolved to sRGB, or null when nothing up
9079
+ * the chain paints. Ancestor styles come from the per-capture memo (`StyleCache`): a deep card's
9080
+ * parents are climbed once per check but resolved once per capture. This is the `behind` colour
9081
+ * both the `hasContrastingFill` boolean and the Salience contrast axis score a node's own
9082
+ * fill/border against extracted so the two read the SAME measured ground (the salience.ts scorer
9083
+ * takes `behind` as an input rather than climbing itself).
9187
9084
  */
9188
9085
  function nearestPaintedBackground(el, win, styles) {
9189
9086
  for (let ancestor = el.parentElement; ancestor; ancestor = ancestor.parentElement) {
@@ -9193,17 +9090,14 @@ function nearestPaintedBackground(el, win, styles) {
9193
9090
  return null;
9194
9091
  }
9195
9092
  /**
9196
- * Does this element paint a fill that stands out from what's behind it? A
9197
- * background image always counts; a background color counts only when it's
9198
- * mostly opaque and clearly differs from the nearest ancestor's background
9199
- * (so a white button on a white card doesn't, but a blue one does).
9093
+ * Does this element paint a fill that stands out from what's behind it? A background image always
9094
+ * counts; a background color counts only when it's mostly opaque and clearly differs from the
9095
+ * nearest ancestor's background (so a white button on a white card doesn't, but a blue one does).
9200
9096
  *
9201
- * This is the fill-ONLY signal Absorb has always used. Measure runs it once and
9202
- * snapshots the result onto `IRNode.paint.contrastingFill` (3a); the pure
9203
- * `markInk`-side `surfaceAbsorbs` reads that snapshot for the fill case (so every
9204
- * fill that collapsed before still collapses) and ADDS the Salience contrast axis on
9205
- * top to catch a contrasting BORDER or BOX-SHADOW surface (ADR 0021 §3 — "the
9206
- * existing `hasContrastingFill` branch, generalized").
9097
+ * This is the fill-ONLY signal Absorb keys on. Measure runs it once and snapshots the result onto
9098
+ * `IRNode.paint.contrastingFill`; the pure `markInk`-side `surfaceAbsorbs` reads that snapshot for
9099
+ * the fill case and ADDS the Salience contrast axis on top to catch a contrasting BORDER or
9100
+ * BOX-SHADOW surface (ADR 0021 §3 the `hasContrastingFill` branch, generalized).
9207
9101
  */
9208
9102
  function hasContrastingFill(el, cs, win, styles) {
9209
9103
  if (hasPaintedImage(cs)) return true;
@@ -9218,12 +9112,11 @@ function hasContrastingFill(el, cs, win, styles) {
9218
9112
  return Math.abs(bg.r - base.r) + Math.abs(bg.g - base.g) + Math.abs(bg.b - base.b) > FILL_CONTRAST;
9219
9113
  }
9220
9114
  /**
9221
- * The base/context font-size in px the Salience type-scale axis is measured
9222
- * relative to: the inherited font-size of the capture subtree's context (the
9223
- * parent of the first real element), defaulting to the CSS 16px when there is no
9224
- * context or it does not resolve to px. Computed once per capture would be ideal,
9225
- * but the contexts the walk touches are few and the StyleCache makes the parent
9226
- * lookup free, so it is read on demand at each score site.
9115
+ * The base/context font-size in px the Salience type-scale axis is measured relative to: the
9116
+ * inherited font-size of the capture subtree's context (the parent of the first real element),
9117
+ * defaulting to the CSS 16px when there is no context or it does not resolve to px. Computed once
9118
+ * per capture would be ideal, but the contexts the walk touches are few and the StyleCache makes
9119
+ * the parent lookup free, so it is read on demand at each score site.
9227
9120
  */
9228
9121
  function contextBaseFontSizePx(el, styles) {
9229
9122
  const parent = el.parentElement;
@@ -9232,13 +9125,12 @@ function contextBaseFontSizePx(el, styles) {
9232
9125
  return sizePx !== null && sizePx > 0 ? sizePx : 16;
9233
9126
  }
9234
9127
  /**
9235
- * Score one element's visual relevance by handing salience.ts the three inputs
9236
- * the walk already has: the element's computed style, its rendered box, and the
9237
- * measured context (nearest painted ancestor `behind`, base font-size, the
9238
- * capture root's area for the size axis). `rootArea` is threaded from `walkAll`
9239
- * so every node is normalized against the SAME reference; a non-positive value
9240
- * (a layout-less env, or an unmeasured root) disables the size axis in the scorer
9241
- * rather than dividing by zero. Pure pass-through — the scorer owns the math.
9128
+ * Score one element's visual relevance by handing salience.ts the three inputs the walk already
9129
+ * has: the element's computed style, its rendered box, and the measured context (nearest painted
9130
+ * ancestor `behind`, base font-size, the capture root's area for the size axis). `rootArea` is
9131
+ * threaded from `walkAll` so every node is normalized against the SAME reference; a non-positive
9132
+ * value (a layout-less env, or an unmeasured root) disables the size axis in the scorer rather than
9133
+ * dividing by zero. Pure pass-through the scorer owns the math.
9242
9134
  */
9243
9135
  function nodeSalience(el, cs, rect, rootArea, win, styles) {
9244
9136
  return salience(cs, rect, {
@@ -9252,21 +9144,17 @@ function rgbaString(c) {
9252
9144
  return c.a < 1 ? `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})` : `rgb(${c.r}, ${c.g}, ${c.b})`;
9253
9145
  }
9254
9146
  /**
9255
- * Measure-time paint + salience resolution (ADR 0022 invariants 3 & 5; capture
9256
- * pipeline Phase 3a). Runs the SAME canvas-resolving helpers the classification
9257
- * uses lazily today, but eagerly once, per element entry, while layout is warm
9258
- * and snapshots the result onto the node so a future markInk pass reads it off the
9259
- * IR rather than re-touching the DOM. This is ADDITIVE: classification still reads
9260
- * its own live values, so the emitted plate is byte-identical; these fields are
9261
- * populated-but-not-yet-read.
9147
+ * Measure-time paint + salience resolution (ADR 0022 invariants 3 & 5). Runs the canvas-resolving
9148
+ * helpers eagerly once, per element entry, while layout is warm — and snapshots the result onto
9149
+ * the node, so `markInk`/Absorb read it off the IR rather than re-touching the DOM.
9262
9150
  *
9263
- * - `paint.surface`/`bg`/`contrastingFill`/`radius` come from the existing
9264
- * `hasVisibleBackground` / `toRgb` / `hasContrastingFill` helpers and the
9265
- * computed `border-radius`. The `bg` is the canvas-resolved colour, so a later
9266
- * pass single-sources the colour through `toRgb` (no second parse path).
9267
- * - `salience` is `nodeSalience`, which resolves the ancestor-climbed `behind`
9268
- * (via `toRgb`) and base font-size — both legal at measure time — so salience's
9269
- * own colour mirror never has to fall back on the capture path.
9151
+ * - `paint.surface`/`bg`/`contrastingFill`/`radius` come from the existing `hasVisibleBackground` /
9152
+ * `toRgb` / `hasContrastingFill` helpers and the computed `border-radius`. The `bg` is the
9153
+ * canvas-resolved colour, so a later pass single-sources the colour through `toRgb` (no second
9154
+ * parse path).
9155
+ * - `salience` is `nodeSalience`, which resolves the ancestor-climbed `behind` (via `toRgb`) and base
9156
+ * font-size — both legal at measure time — so salience's own colour mirror never has to fall back
9157
+ * on the capture path.
9270
9158
  */
9271
9159
  function resolvePaintAndSalience(el, cs, rect, state) {
9272
9160
  const bg = toRgb(cs.backgroundColor, state.win);
@@ -9281,6 +9169,11 @@ function resolvePaintAndSalience(el, cs, rect, state) {
9281
9169
  salience: nodeSalience(el, cs, rect, state.rootArea, state.win, state.styles)
9282
9170
  };
9283
9171
  }
9172
+ /**
9173
+ * Thrown when a capture's measured tree exceeds a Collect limit — almost always a `<Skeleton>`
9174
+ * sitting too high in the tree. Raised after the cheap walk and before the O(rules × nodes) CSS
9175
+ * extraction, so an oversized capture is refused without freezing the main thread on it.
9176
+ */
9284
9177
  var CaptureTooLargeError = class extends Error {
9285
9178
  /** The full measured size of the surviving tree that tripped the gate. */
9286
9179
  size;
@@ -9301,20 +9194,17 @@ var CaptureTooLargeError = class extends Error {
9301
9194
  }
9302
9195
  };
9303
9196
  /**
9304
- * The build phase (capture-pipeline Phase 1.5): the walk already built the
9305
- * light-DOM IR directly (`walkAll` returns `{ root, nodes, extraRules }`), so this
9306
- * just lifts the author CSS onto it. The CSS-extract (`copyRules`) fills each
9307
- * node's `decls.author` (the lifted CSSOM + inline rules targeting it) and
9308
- * `decls.carried` (the carried token-definition rules anchored to it, ADR 0019 §1),
9309
- * so the IR holds every node's authored CSS — the substrate `emit` reads back out.
9310
- * Gates on node count BEFORE the O(author rules × nodes) extract, as the legacy
9311
- * path did.
9197
+ * The build phase: the walk builds the light-DOM IR directly (`walkAll` returns `{ root, nodes,
9198
+ * extraRules }`); this lifts the author CSS onto it. The CSS-extract (`copyRules`) fills each
9199
+ * node's `decls.author` (the lifted CSSOM + inline rules targeting it) and `decls.carried` (the
9200
+ * carried token-definition rules anchored to it, ADR 0019 §1), so the IR holds every node's
9201
+ * authored CSS — the substrate `emit` reads back out. Gates on the Collect limits BEFORE the
9202
+ * O(author rules × nodes) extract.
9312
9203
  *
9313
- * `copyRules` still returns the full flat rule list (its primary job here is the
9314
- * `decls` population); `emit` rebuilds that list from the IR, so the return is
9315
- * dropped — EXCEPT the one rule not anchored to any node: the text-context rule
9316
- * (`ids: []`, the inherited parent font `baseRules` derives), lifted onto the
9317
- * bundle for `emit` to fold back in.
9204
+ * `copyRules` still returns the full flat rule list (its primary job here is the `decls`
9205
+ * population); `emit` rebuilds that list from the IR, so the return is dropped — EXCEPT the one
9206
+ * rule not anchored to any node: the text-context rule (`ids: []`, the inherited parent font
9207
+ * `baseRules` derives), lifted onto the bundle for `emit` to fold back in.
9318
9208
  */
9319
9209
  function build(walked, doc, win, styles, diagnostics, collect, walker) {
9320
9210
  const bundle = {
@@ -9335,28 +9225,27 @@ function build(walked, doc, win, styles, diagnostics, collect, walker) {
9335
9225
  return bundle;
9336
9226
  }
9337
9227
  /**
9338
- * Run the per-View reduction passes over one measured `IRBundle`, in their fixed,
9339
- * load-bearing order (ADR 0022 §3). Annotate-only — each pass sets fields (`folded`/
9340
- * `flattened`/`count`/`dropped`); `emit`/Serialize lower them. Lifted out of `build`
9341
- * (the Measure core — `markInk` + the node-count gate + the CSS-extract stays
9342
- * there, since `markInk` must run before the gate) so the reductions are a pure
9343
- * `bundle → bundle` step `runPlatePasses` runs once per View over a whole `PlateIR`
9344
- * after the sweep (reading the snapshotted `ViewIR.viewportHeight`, since `window`
9345
- * has moved on). `diagnostics` collects the Superset/Crop notes.
9228
+ * Run the per-View Distil passes over one measured `IRBundle`, in their fixed, load-bearing order
9229
+ * (ADR 0022 §3). Annotate-only — each pass sets fields (`folded`/ `flattened`/`count`/`dropped`);
9230
+ * `emit`/Serialize lower them. Kept apart from `build` (the Measure core — `markInk` + the Collect
9231
+ * gate + the CSS-extractwhere `markInk` must run before the gate) so Distil is a pure `bundle →
9232
+ * bundle` step `distilPlate` runs once per View over a whole `PlateIR` after the sweep (reading the
9233
+ * snapshotted `ViewIR.viewportHeight`, since `window` has moved on). `diagnostics` collects the
9234
+ * Superset/Crop notes.
9346
9235
  *
9347
9236
  * Order: glyph-fold → Flatten → Superset → Crop → Template.
9348
- * - glyph-fold first: folds glyph-sized icons into adjacent text lines (ADR 0021 §2).
9349
- * - Flatten next: collapses passthrough wrapper chains over the folded tree (ADR 0021 §1).
9350
- * These two are lossless normalization the removal passes read.
9351
- * - Superset / Crop: the "drop the imperceptible" pair — bone-on-bone (Superset) and
9352
- * off-screen (Crop) both only annotate `dropped`, so their relative order is
9353
- * immaterial; Superset stays before Crop. Crop's base-case folds are `viewportHeight`
9354
- * (vertical) and `viewportWidth` (horizontal); CROP_BUFFER stays internal to the pass.
9355
- * - Template LAST: compression operates only on what SURVIVES the removal passes, so it
9356
- * only ever compresses what a human would actually see (the owner's guiding principle).
9357
- * Crop drops a run's off-screen tail first; Template then groups the on-screen prefix
9358
- * and the faithful surviving `count` falls out of the fold (a dropped cell breaks a
9359
- * template run).
9237
+ *
9238
+ * - Glyph-fold first: folds glyph-sized icons into adjacent text lines (ADR 0021 §2).
9239
+ * - Flatten next: collapses passthrough wrapper chains over the folded tree (ADR 0021 §1). These two
9240
+ * are lossless normalization the removal passes read.
9241
+ * - Superset / Crop: the "drop the imperceptible" pair bone-on-bone (Superset) and off-screen
9242
+ * (Crop) both only annotate `dropped`, so their relative order is immaterial; Superset stays
9243
+ * before Crop. Crop's base-case folds are `viewportHeight` (vertical) and `viewportWidth`
9244
+ * (horizontal); CROP_BUFFER stays internal to the pass.
9245
+ * - Template LAST: compression operates only on what SURVIVES the removal passes — what a viewer
9246
+ * would actually see. Crop drops a run's off-screen tail first; Template then groups the
9247
+ * on-screen prefix and the faithful surviving `count` falls out of the fold (a dropped cell
9248
+ * breaks a template run).
9360
9249
  */
9361
9250
  function runPasses(bundle, viewportHeight, viewportWidth, diagnostics) {
9362
9251
  foldGlyphRowsIR(bundle.root);
@@ -9366,19 +9255,17 @@ function runPasses(bundle, viewportHeight, viewportWidth, diagnostics) {
9366
9255
  templateIR(bundle);
9367
9256
  }
9368
9257
  /**
9369
- * Run the reduction passes over every View of a `PlateIR` (ADR 0022 §3) — the
9370
- * `PlateIR → PlateIR` pass model, via `eachView`. Each View gets its own
9371
- * diagnostics collector (the passes' Superset/Crop notes), merged onto the View's
9372
- * existing (Measure-time) diagnostics a dev-only transient (stripped before disk),
9373
- * so the merge order is immaterial. Crop reads the View's snapshotted folds: the
9374
- * vertical base case is `viewportHeight` (an unset one passes `Infinity`, disabling
9375
- * the base-case viewport fold a fold of 0 would crop EVERYTHING, `top >= 1.5 * 0`,
9376
- * so a manually-built bundle still folds only at a per-node clipping ancestor); the
9377
- * horizontal base case is the View's `width` (its captured `innerWidth`, always set).
9378
- * The orchestration (step 4.4) calls this once after building the bundle in one
9379
- * session.
9258
+ * Run the Distil passes over every View of a `PlateIR` (ADR 0022 §3) — the `PlateIR → PlateIR` pass
9259
+ * model, via `eachView`. Each View gets its own diagnostics collector (the passes' Superset/Crop
9260
+ * notes), merged onto the View's existing (Measure-time) diagnostics — a dev-only transient
9261
+ * (stripped before disk), so the merge order is immaterial. Crop reads the View's snapshotted
9262
+ * folds: the vertical base case is `viewportHeight` (an unset one passes `Infinity`, disabling the
9263
+ * base-case viewport fold a fold of 0 would crop EVERYTHING, `top >= 1.5 * 0`, so a
9264
+ * manually-built bundle still folds only at a per-node clipping ancestor); the horizontal base case
9265
+ * is the View's `width` (its captured `innerWidth`, always set). The orchestration calls this once
9266
+ * after building the bundle in one session.
9380
9267
  */
9381
- function runPlatePasses(plate) {
9268
+ function distilPlate(plate) {
9382
9269
  eachView(plate, (view) => {
9383
9270
  const diagnostics = createDiagnostics();
9384
9271
  runPasses(view.bundle, view.viewportHeight ?? Number.POSITIVE_INFINITY, view.width, diagnostics);
@@ -9387,19 +9274,17 @@ function runPlatePasses(plate) {
9387
9274
  });
9388
9275
  }
9389
9276
  /**
9390
- * Measure the SURVIVING (non-dropped) tree in one DFS pass: total `nodes`, max
9391
- * nesting `depth`, and `breadth` — the widest level (most nodes at any one depth).
9277
+ * Measure the SURVIVING (non-dropped) tree in one DFS pass: total `nodes`, max nesting `depth`, and
9278
+ * `breadth` — the widest level (most nodes at any one depth).
9392
9279
  *
9393
- * Walks via `effectiveKids`, so a `dropped` node (and its non-re-shown subtree) and
9394
- * a `flattened` wrapper are handled exactly as `emit` will lower them — the gate
9395
- * measures the real post-mark output, not every element Measure descended. The
9396
- * synthetic root (id 0, el null) is NOT a measured entry, so it is excluded,
9397
- * keeping `nodes` in the same units as the legacy `entries.length` gate.
9280
+ * Walks via `effectiveKids`, so a `dropped` node (and its non-re-shown subtree) and a `flattened`
9281
+ * wrapper are handled exactly as `emit` will lower them — the gate measures the real post-mark
9282
+ * output, not every element Measure descended. The synthetic root (id 0, el null) is NOT a measured
9283
+ * entry, so it is excluded and `nodes` counts only measured entries.
9398
9284
  *
9399
- * Widest-level `breadth` is the realistic "how wide does this tree get" signal: a
9400
- * DOM tree's total tracks its widest level (~5×), not `breadth × depth` (which
9401
- * assumes every level is maximally wide — a fractal that never occurs). Feeds the
9402
- * Collect gate.
9285
+ * Widest-level `breadth` is the realistic "how wide does this tree get" signal: a DOM tree's total
9286
+ * tracks its widest level (~5×), not `breadth × depth` (which assumes every level is maximally wide
9287
+ * — a fractal that never occurs). Feeds the Collect gate.
9403
9288
  */
9404
9289
  function measureTree(root) {
9405
9290
  let nodes = 0;
@@ -9424,17 +9309,15 @@ function measureTree(root) {
9424
9309
  };
9425
9310
  }
9426
9311
  /**
9427
- * Measure one View's subtree into an `IRBundle` — the Measure stage (ADR 0022
9428
- * invariant 5): the sole DOM-touching code on the capture path. Sets up the
9429
- * per-capture computed-style memo (built here, discarded when this synchronous
9430
- * capture returns never outlives the call, so no cross-capture caching of live
9431
- * styles), runs the walk (which builds the light-DOM IR directly), and runs the
9432
- * build phase (capture-pipeline Phase 1.5): lift the author CSS onto each node's
9433
- * `decls`, then run the annotate passes. Returns the bundle alongside the
9434
- * `win` (for the viewport width) and the `diagnostics` collector, so the two
9435
- * callers — `captureBundle` (the raw bundle, for tests) and `captureView` (→ a
9436
- * `PlateIR` View) — each shape the result without re-walking. `emit`/Serialize
9437
- * read the IR back out.
9312
+ * Measure one View's subtree into an `IRBundle` — the Measure stage (ADR 0022 invariant 5): the
9313
+ * sole DOM-touching code on the capture path. Sets up the per-capture computed-style memo (built
9314
+ * here, discarded when this synchronous capture returns — never outlives the call, so no
9315
+ * cross-capture caching of live styles), runs the walk (which builds the light-DOM IR directly),
9316
+ * and runs the build phase: lift the author CSS onto each node's `decls`, then run the annotate
9317
+ * passes. Returns the bundle alongside the `win` (for the viewport width) and the `diagnostics`
9318
+ * collector, so the two callers `captureBundle` (the raw bundle, for tests) and `captureView` (→
9319
+ * a `PlateIR` View) — each shape the result without re-walking. `emit`/Serialize read the IR back
9320
+ * out.
9438
9321
  */
9439
9322
  function measureBundle(roots, options = {}) {
9440
9323
  const doc = roots[0]?.ownerDocument;
@@ -9449,17 +9332,15 @@ function measureBundle(roots, options = {}) {
9449
9332
  };
9450
9333
  }
9451
9334
  /**
9452
- * The framework-agnostic capture core (ADR 0022 §1,§2): measure one View's subtree
9453
- * and APPEND it to a per-Plate `PlateIR` bundle — `capture(el, ir?) → ir` in the
9454
- * end-state vocabulary. The single entry point for capture: the dev client sweeps
9455
- * every View through it, and the framework-neutral `captureElement` (core.ts) runs
9456
- * it once for the single-View case, both then reducing via `runPlatePasses` and
9457
- * projecting via `serializePlateIR`. Walks the View's `roots` into an `IRBundle`
9458
- * (Measure + build), wraps it as a `ViewIR` at the current viewport width with the
9459
- * capture's transient diagnostics, and appends it to the passed-in `plate` (the
9460
- * orchestration threads ONE bundle through a Plate's per-View calls in one session,
9461
- * ADR 0022 §2). With no `plate` it starts a fresh, as-yet-UNNAMED bundle — naming
9462
- * and the breakpoint union are the orchestration's job (Views are orchestration,
9335
+ * The framework-agnostic capture core (ADR 0022 §1,§2): measure one View's subtree and APPEND it to
9336
+ * a per-Plate `PlateIR` bundle — ADR 0022 §1's `capture(el, ir?) → ir`. The single entry point for
9337
+ * capture: the dev client sweeps every View through it, and the framework-neutral `captureElement`
9338
+ * (core.ts) runs it once for the single-View case, both then distilling via `distilPlate` and
9339
+ * projecting via `serializePlateIR`. Walks the View's `roots` into an `IRBundle` (Measure + build),
9340
+ * wraps it as a `ViewIR` at the current viewport width with the capture's transient diagnostics,
9341
+ * and appends it to the passed-in `plate` (the orchestration threads ONE bundle through a Plate's
9342
+ * per-View calls in one session, ADR 0022 §2). With no `plate` it starts a fresh, as-yet-UNNAMED
9343
+ * bundle naming and the breakpoint union are the orchestration's job (Views are orchestration,
9463
9344
  * not core, §1), so the caller sets `name`/`breakpoints` on the returned `PlateIR`.
9464
9345
  */
9465
9346
  function captureView(roots, options = {}, plate) {
@@ -9537,16 +9418,15 @@ function walk(el, state, depth, isCaptureRoot = false) {
9537
9418
  ref: refName
9538
9419
  };
9539
9420
  }
9540
- return annotationWalk(el, cs, state, depth);
9421
+ return hintWalk(el, cs, state, depth);
9541
9422
  }
9542
9423
  /**
9543
- * The built-in annotation walker (ADR 0018), the rung between the stitch guard
9544
- * and the default classifier. It honors the capture-only `data-xr-*`
9545
- * annotations and otherwise falls through to the default classifier. The stitch
9546
- * guard already ran in `walk`, so a boundary element never reaches here — an
9547
- * annotation can never override a nested `<Skeleton>` (ADR 0006).
9424
+ * The built-in Hint rung (ADR 0018), between the stitch guard and the default classifier. It honors
9425
+ * the capture-only `data-xr-*` hints and otherwise falls through to the default classifier. The
9426
+ * stitch guard already ran in `walk`, so a boundary element never reaches here — a hint can never
9427
+ * override a nested `<Skeleton>` (ADR 0006).
9548
9428
  */
9549
- function annotationWalk(el, cs, state, depth) {
9429
+ function hintWalk(el, cs, state, depth) {
9550
9430
  if (el.hasAttribute(XR_IGNORE_ATTR)) return ignoreElement(state);
9551
9431
  const kindAttr = el.getAttribute(XR_BONE_KIND_ATTR);
9552
9432
  if (kindAttr !== null) {
@@ -9557,27 +9437,24 @@ function annotationWalk(el, cs, state, depth) {
9557
9437
  return defaultClassify(el, cs, state, depth);
9558
9438
  }
9559
9439
  /**
9560
- * `data-xr-ignore`: drop the element and its subtree at WALK TIME. No Entry, no
9561
- * node, no descent — it contributes nothing to the Plate, and never reaches the
9562
- * IR (so it never produces a `dropped: 'ignored'`, which is the walker-only
9563
- * post-measure path; ADR 0022 §5). Aggregated under the existing `dropped-tag`
9564
- * diagnostic (a hidden/non-visual drop is the same fidelity story; do not spam a
9565
- * new code per ignored element).
9440
+ * `data-xr-ignore`: drop the element and its subtree at WALK TIME. No Entry, no node, no descent —
9441
+ * it contributes nothing to the Plate, and never reaches the IR (so it never produces a `dropped:
9442
+ * 'ignored'`, which is the walker-only post-measure path; ADR 0022 §5). Aggregated under the
9443
+ * existing `dropped-tag` diagnostic (a hidden/non-visual drop is the same fidelity story; do not
9444
+ * spam a new code per ignored element).
9566
9445
  */
9567
9446
  function ignoreElement(state) {
9568
9447
  state.diagnostics.add("dropped-tag");
9569
9448
  return null;
9570
9449
  }
9571
9450
  /**
9572
- * Snapshot the raw clip inputs onto an IR node for the 2D Crop pass — one pair per
9573
- * axis, each gated on its OWN overflow not being `visible`. The vertical pair
9574
- * (`scrollHeight`/`clientHeight`, gated on `overflow-y`) and the horizontal pair
9575
- * (`scrollWidth`/`clientWidth`, gated on `overflow-x`) are element PROPS, not on the
9576
- * computed-style object and not derivable later, so they must be read at measure
9577
- * time while layout is valid; reading them CAN force a layout, so each gate keeps it
9578
- * off the `overflow: visible` majority for that axis. Layout is already warm here
9579
- * (the caller just read `getBoundingClientRect`). The walk only records; `cropIR`
9580
- * derives the fold.
9451
+ * Snapshot the raw clip inputs onto an IR node for the 2D Crop pass — one pair per axis, each gated
9452
+ * on its OWN overflow not being `visible`. The vertical pair (`scrollHeight`/`clientHeight`, gated
9453
+ * on `overflow-y`) and the horizontal pair (`scrollWidth`/`clientWidth`, gated on `overflow-x`) are
9454
+ * element PROPS, not on the computed-style object and not derivable later, so they must be read at
9455
+ * measure time while layout is valid; reading them CAN force a layout, so each gate keeps it off
9456
+ * the `overflow: visible` majority for that axis. Layout is already warm here (the caller just read
9457
+ * `getBoundingClientRect`). The walk only records; `cropIR` derives the fold.
9581
9458
  */
9582
9459
  function snapshotClipInputs(el, cs, node) {
9583
9460
  if (cs.overflowY !== "visible") {
@@ -9590,23 +9467,19 @@ function snapshotClipInputs(el, cs, node) {
9590
9467
  }
9591
9468
  }
9592
9469
  /**
9593
- * The kind-inference heuristic shared by the walker's `ctx.bone()` without an
9594
- * explicit kind (via the `applyWalker` pass) and the default classifier (ADR
9595
- * 0018, Q4 ONE code path). A media tag is
9596
- * `media`; anything else collapsed to a single bone reads as a `box`. The
9597
- * default classifier reaches the same outcomes through its own structural
9598
- * branches (MEDIA_TAGS -> media, contrasting fill / painted empty -> box), so
9599
- * keeping this one function is the single source of truth for "what kind is an
9600
- * element that has no children to descend into".
9470
+ * The kind-inference heuristic shared by the walker's `ctx.bone()` without an explicit kind (via
9471
+ * the `applyWalker` pass) and the default classifier (ADR 0018, Q4 — ONE code path). A media tag is
9472
+ * `media`; anything else collapsed to a single bone reads as a `box`. The default classifier
9473
+ * reaches the same outcomes through its own structural branches (MEDIA_TAGS -> media, contrasting
9474
+ * fill / painted empty -> box), so keeping this one function is the single source of truth for
9475
+ * "what kind is an element that has no children to descend into".
9601
9476
  */
9602
9477
  function inferLeafKind(el) {
9603
9478
  return MEDIA_TAGS.has(el.tagName.toUpperCase()) ? "media" : "box";
9604
9479
  }
9605
9480
  /**
9606
- * The DEFAULT classifier — the unchanged step-4 body, extracted so the
9607
- * precedence ladder's `next()` can call it (ADR 0018). With no walker and no
9608
- * annotations this is the only path taken, and its output is byte-identical to
9609
- * before the ladder was introduced.
9481
+ * The DEFAULT classifier — the precedence ladder's last rung (ADR 0018). With no walker and no
9482
+ * hints this is the only path taken.
9610
9483
  */
9611
9484
  function defaultClassify(el, cs, state, depth, markKind) {
9612
9485
  const tag = el.tagName.toUpperCase();
@@ -9726,22 +9599,21 @@ function defaultClassify(el, cs, state, depth, markKind) {
9726
9599
  //#endregion
9727
9600
  //#region src/chunk.ts
9728
9601
  /**
9729
- * Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR
9730
- * 0008). This is the framework-neutral half of rendering: a skeleton is static,
9731
- * text-free DOM, so the structure is flattened to HTML strings here, at build,
9732
- * once instead of every adapter walking a tree on the latency-critical first
9733
- * paint. The adapter only injects the strings and mounts a child plate at each
9734
- * stitch (see the React adapters). No escaping risk: bones carry only the
9735
- * content-addressed class tokens (ADR 0007), never user content.
9602
+ * Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR 0008). This is the
9603
+ * framework-neutral half of rendering: a skeleton is static, text-free DOM, so the structure is
9604
+ * flattened to HTML strings here, at build, once — instead of every adapter walking a tree on the
9605
+ * latency-critical first paint. The adapter only injects the strings and mounts a child plate at
9606
+ * each stitch (see the React adapters). No escaping risk: bones carry only the content-addressed
9607
+ * class tokens (ADR 0007), never user content.
9736
9608
  */
9737
9609
  function nodeClass(node) {
9738
9610
  return NODE_CLASS + (node.leaf ? ` ${LEAF_CLASS} ${LEAF_CLASS}-${node.leaf}` : "") + (node.cls ? ` ${node.cls}` : "");
9739
9611
  }
9740
9612
  /**
9741
- * Whether this subtree holds a render-time DYNAMIC node anywhere — a stitch
9742
- * (`ref`, a mount point resolved to a child plate) or a template (`count`, a cell
9743
- * the adapter repeats). Either keeps the subtree from collapsing to one flat
9744
- * string, since both are expanded at render rather than baked into the markup.
9613
+ * Whether this subtree holds a render-time DYNAMIC node anywhere — a stitch (`ref`, a mount point
9614
+ * resolved to a child plate) or a template (`count`, a cell the adapter repeats). Either keeps the
9615
+ * subtree from collapsing to one flat string, since both are expanded at render rather than baked
9616
+ * into the markup.
9745
9617
  */
9746
9618
  function hasDynamic(node) {
9747
9619
  return node.ref !== void 0 || node.count !== void 0 || (node.kids?.some(hasDynamic) ?? false);
@@ -9752,13 +9624,12 @@ function toHtml(node) {
9752
9624
  return `<div class="${nodeClass(node)}">${kids}</div>`;
9753
9625
  }
9754
9626
  /**
9755
- * Serialize a node list into chunks: each maximal run with no render-time dynamic
9756
- * node becomes one HTML `string`; a `ref` node becomes `{ r }`; a `count` node
9757
- * becomes `{ t: html, n }` (its cell's HTML once + the repeat count); a node that
9758
- * isn't itself dynamic but holds one deeper stays a real element (`{ c, k }`) and
9759
- * recurses. Mirrors the element shape the runtime renderer would otherwise build
9760
- * per node. A `count` cell is guaranteed stitch-free by the capture pass, so its
9761
- * `toHtml` is a plain string the adapter repeats.
9627
+ * Serialize a node list into chunks: each maximal run with no render-time dynamic node becomes one
9628
+ * HTML `string`; a `ref` node becomes `{ r }`; a `count` node becomes `{ t: html, n }` (its cell's
9629
+ * HTML once + the repeat count); a node that isn't itself dynamic but holds one deeper stays a real
9630
+ * element (`{ c, k }`) and recurses. Mirrors the element shape the runtime renderer would otherwise
9631
+ * build per node. A `count` cell is guaranteed stitch-free by the capture pass, so its `toHtml` is
9632
+ * a plain string the adapter repeats.
9762
9633
  */
9763
9634
  function toChunks(nodes) {
9764
9635
  const out = [];
@@ -9789,17 +9660,15 @@ function toChunks(nodes) {
9789
9660
  return out;
9790
9661
  }
9791
9662
  /**
9792
- * Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The
9793
- * root's own content classes travel as `rootCls` (the adapter puts them on the
9794
- * root element); its children become `chunks`. A capture-less plate has null
9795
- * chunks and renders the fallback.
9663
+ * Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The root's own content
9664
+ * classes travel as `rootCls` (the adapter puts them on the root element); its children become
9665
+ * `chunks`. A capture-less plate has null chunks and renders the fallback.
9796
9666
  *
9797
- * Two-artifact contract (plan 005 round 2): the returned `Plate` is the TREE
9798
- * artifact only — the input's `css` is deliberately NOT copied onto it. The
9799
- * css string stays the producer's (`MergedPlate`/`StoredPlate`) field; each
9800
- * lowering site decides independently how its css travels (the plugin's
9801
- * virtual `.css` modules + `export const css`, `captureElement`'s
9802
- * `{ plate, css }` pair, or the `xray:plate` dev event, which ships no css).
9667
+ * Two-artifact contract (ADR 0026): the returned `Plate` is the TREE artifact only; the input's
9668
+ * `css` is deliberately NOT copied onto it. The css string stays the producer's
9669
+ * (`GatedPlate`/`StoredPlate`) field, and each lowering site decides independently how its css
9670
+ * travels: the plugin's virtual `.css` modules + `export const css`, `captureElement`'s `{ plate,
9671
+ * css }` pair, or the `xray:plate` dev event, which ships no css.
9803
9672
  */
9804
9673
  function serializePlate(merged) {
9805
9674
  const { v, name, tree } = merged;
@@ -9821,13 +9690,11 @@ function serializePlate(merged) {
9821
9690
  };
9822
9691
  }
9823
9692
  /**
9824
- * Distinct plate names referenced by the tree's stitches, in first-seen order.
9825
- * Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
9826
- * gen-side stitch import graph straight off a `StoredPlate.tree`
9827
- * (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
9828
- * in the ADR 0022 cutover — it is the one node-side primitive that survived the
9829
- * `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
9830
- * in-browser `serializePlateIR`, project.ts).
9693
+ * Distinct plate names referenced by the tree's stitches, in first-seen order. Walks a `RenderNode`
9694
+ * tree (the stored, post-classify shape), so it drives the gen-side stitch import graph straight
9695
+ * off a `StoredPlate.tree` (`renderPlateModule`, index.ts). The one node-side primitive needed at
9696
+ * build time; multi-View gating happens in the in-browser `serializePlateIR` (serialize.ts, ADR
9697
+ * 0022).
9831
9698
  */
9832
9699
  function collectRefs(tree) {
9833
9700
  const names = [];
@@ -9845,14 +9712,13 @@ function collectRefs(tree) {
9845
9712
  //#endregion
9846
9713
  //#region src/breakpoints.ts
9847
9714
  /**
9848
- * Per-plate breakpoint derivation (ADR 0004): the width thresholds that could
9849
- * change this plate come from the `@media` conditions on its own copied rules
9850
- * plus the queries the app evaluated through `matchMedia` during capture.
9851
- * Thresholds partition the width axis into views; identical captures in
9852
- * adjacent views collapse at merge time.
9715
+ * Per-plate breakpoint derivation (ADR 0004): the width thresholds that could change this plate
9716
+ * come from the `@media` conditions on its own copied rules plus the queries the app evaluated
9717
+ * through `matchMedia` during capture. Thresholds partition the width axis into Views; identical
9718
+ * captures in adjacent Views collapse at Serialize.
9853
9719
  */
9854
9720
  const FONT_SIZE_PX = 16;
9855
- /** A breakpoint is the first integer px width of the view ABOVE the boundary. */
9721
+ /** A breakpoint is the first integer px width of the View ABOVE the boundary. */
9856
9722
  function thresholdsOf(condition) {
9857
9723
  const out = [];
9858
9724
  const toPx = (value, unit) => Number.parseFloat(value) * (unit === "px" ? 1 : FONT_SIZE_PX);
@@ -9874,8 +9740,8 @@ function deriveBreakpoints(rules, matchMediaQueries = []) {
9874
9740
  for (const query of matchMediaQueries) for (const bp of thresholdsOf(query)) set.add(bp);
9875
9741
  return [...set].toSorted((a, b) => a - b);
9876
9742
  }
9877
- /** The view interval `[min, max)` a given viewport width falls into. */
9878
- function regimeFor(width, breakpoints) {
9743
+ /** The View interval `[min, max)` a given viewport width falls into. */
9744
+ function viewSpanFor(width, breakpoints) {
9879
9745
  let min;
9880
9746
  let max;
9881
9747
  for (const bp of breakpoints) if (bp <= width) min = bp;
@@ -9891,17 +9757,14 @@ function regimeFor(width, breakpoints) {
9891
9757
  //#endregion
9892
9758
  //#region src/classify.ts
9893
9759
  /**
9894
- * Turn id-form rules + an id-tree into the shipped plate: each distinct
9895
- * `(tier, media, declarations)` becomes one class, named by a per-plate
9896
- * sequential id (`[data-xr-root] .xr-<plate>-<n>`), and every node carries the
9897
- * class tokens it needs instead of an id (ADR 0007). The id is a plain counter,
9898
- * not a content hash: the plate prefix already keeps tokens disjoint across plates
9899
- * (the stitch-isolation guarantee), so a per-plate counter is collision-free by
9900
- * construction and low-entropy ids compress far better than random hashes
9901
- * (~31% smaller brotli on a real plate; see the ADR 0007 amendment). Rules stay
9902
- * unlayered; the existing cascade is reproduced by emission order (losers
9903
- * first), so the classes are emitted in `(layered, spec, order)` order exactly
9904
- * as `renderRules` did.
9760
+ * Turn id-form rules + an id-tree into the shipped plate: each distinct `(tier, media,
9761
+ * declarations)` becomes one class, named by a per-plate sequential id (`[data-xr-root]
9762
+ * .xr-<plate>-<n>`), and every node carries the class tokens it needs instead of an id (ADR 0007).
9763
+ * The id is a plain counter, not a content hash: the plate prefix already keeps tokens disjoint
9764
+ * across plates (the stitch-isolation guarantee), so a per-plate counter is collision-free by
9765
+ * construction — and low-entropy ids compress far better than random hashes (~31% smaller brotli on
9766
+ * a real plate; see the ADR 0007 amendment). Rules stay unlayered; the cascade is reproduced by
9767
+ * emission order (losers first), so the classes are emitted in `(layered, spec, order)` order.
9905
9768
  *
9906
9769
  * Pure data-in/data-out — runs at plugin load (node) and in tests.
9907
9770
  */
@@ -9982,15 +9845,14 @@ function render(node, classes) {
9982
9845
  /** Content-class prefix; distinct from the fixed `xr-root/node/leaf` base words. */
9983
9846
  const CLASS_PREFIX = "xr-";
9984
9847
  /**
9985
- * Font/text-rendering properties that are ALWAYS dead at generation. A bone paints
9986
- * no glyphs, so these never reach its box — yet capture still emits them (the
9987
- * inherited context rule, node fallbacks, lifted author rules), and
9988
- * `filterLayoutDecls`/the validator legitimately accept them. `line-height` and
9989
- * `font-size` are NOT here: each can be load-bearing, so `dropDeadDecls` keeps
9990
- * them conditionally. KEEP `vertical-align`/`text-align`: they position
9991
- * inline-block bones, not text.
9848
+ * Font/text-rendering properties that are ALWAYS dead at generation. A bone paints no glyphs, so
9849
+ * these never reach its box — yet capture still emits them (the inherited context rule, node
9850
+ * fallbacks, lifted author rules), and `filterLayoutDecls`/the validator legitimately accept them.
9851
+ * `line-height` and `font-size` are NOT here: each can be load-bearing, so `dropDeadDecls` keeps
9852
+ * them conditionally. KEEP `vertical-align`/`text-align`: they position inline-block bones, not
9853
+ * text.
9992
9854
  */
9993
- const DROP_AT_GEN_ALWAYS = new Set([
9855
+ const DROP_AT_GEN_ALWAYS = /* @__PURE__ */ new Set([
9994
9856
  "font",
9995
9857
  "font-family",
9996
9858
  "font-weight",
@@ -10004,11 +9866,11 @@ const DROP_AT_GEN_ALWAYS = new Set([
10004
9866
  "text-wrap"
10005
9867
  ]);
10006
9868
  /**
10007
- * An element-font-relative length (`em`/`ex`/`ch`) — resolves against the
10008
- * element's own `font-size`, so dropping a per-node `font-size` would shift it.
10009
- * EXCLUDES `rem` (root-relative, unaffected): its `em` is preceded by `r`, never
10010
- * by a digit. Case-insensitive (author CSS may write `1EM`); the trailing `\b`
10011
- * stops `2em` matching inside a longer identifier (`flex`, `text`).
9869
+ * An element-font-relative length (`em`/`ex`/`ch`) — resolves against the element's own
9870
+ * `font-size`, so dropping a per-node `font-size` would shift it. EXCLUDES `rem` (root-relative,
9871
+ * unaffected): its `em` is preceded by `r`, never by a digit. Case-insensitive (author CSS may
9872
+ * write `1EM`); the trailing `\b` stops `2em` matching inside a longer identifier (`flex`,
9873
+ * `text`).
10012
9874
  */
10013
9875
  const FONT_RELATIVE_LENGTH = /\d(?:em|ex|ch)\b/iu;
10014
9876
  /** A flex/grid/table/ruby display — its child boxes get no inline line box. */
@@ -10019,21 +9881,20 @@ function declProp(decl) {
10019
9881
  return colon < 0 ? "" : decl.slice(0, colon).trim().toLowerCase();
10020
9882
  }
10021
9883
  /**
10022
- * Drop dead font/text decls before content-addressing. The always-dead props go
10023
- * unconditionally; the two that CAN matter are kept narrowly:
9884
+ * Drop dead font/text decls before content-addressing. The always-dead props go unconditionally;
9885
+ * the two that CAN matter are kept narrowly:
10024
9886
  *
10025
- * - `line-height` survives only as the walk's per-row pin: a `SPEC_FALLBACK` rule
10026
- * on an inline-flow node (no flex/grid/table/ruby display). That pin
10027
- * (serialize.ts, `hasTextBar`) sizes the inline-block text bone's line box, so
10028
- * dropping it drifts the row. The shared context `line-height`, author copies,
10029
- * and flex/grid rows (text is a flex *item* there — no line box) are all dead.
10030
- * - `font-size` survives only if some kept decl uses an element-font-relative
10031
- * unit (`em`/`ex`/`ch`) that resolves against it. A plate on `px`/`rem` only
10032
- * (e.g. MW) drops it everywhere. The scan is plate-wide: one such unit anywhere
10033
- * means a dropped per-node `font-size` could shift a size.
9887
+ * - `line-height` survives only as the walk's per-row pin: a `SPEC_FALLBACK` rule on an inline-flow
9888
+ * node (no flex/grid/table/ruby display). That pin (measure.ts, `hasTextBar`) sizes the
9889
+ * inline-block text bone's line box, so dropping it drifts the row. The shared context
9890
+ * `line-height`, author copies, and flex/grid rows (text is a flex _item_ there — no line box)
9891
+ * are all dead.
9892
+ * - `font-size` survives only if some kept decl uses an element-font-relative unit (`em`/`ex`/`ch`)
9893
+ * that resolves against it. A plate on `px`/`rem` only (e.g. MW) drops it everywhere. The scan is
9894
+ * plate-wide: one such unit anywhere means a dropped per-node `font-size` could shift a size.
10034
9895
  *
10035
- * A rule emptied by the filter (the all-font context rule) is dropped downstream
10036
- * by the existing `decls.length === 0` guard.
9896
+ * A rule emptied by the filter (the all-font context rule) is dropped downstream by the existing
9897
+ * `decls.length === 0` guard.
10037
9898
  */
10038
9899
  function dropDeadDecls(rules) {
10039
9900
  const keepFontSize = rules.some((rule) => rule.decls.some((decl) => !DROP_AT_GEN_ALWAYS.has(declProp(decl)) && FONT_RELATIVE_LENGTH.test(decl)));
@@ -10053,30 +9914,26 @@ function dropDeadDecls(rules) {
10053
9914
  });
10054
9915
  }
10055
9916
  /**
10056
- * Encode a plate name into a class-token prefix, injectively. This prefix *is*
10057
- * the isolation boundary across stitched plates (ADR 0007), so distinct plate
10058
- * names must never produce the same prefix — a collision would let one plate's
10059
- * `<style>` block reorder another's classes.
9917
+ * Encode a plate name into a class-token prefix, injectively. This prefix _is_ the isolation
9918
+ * boundary across stitched plates (ADR 0007), so distinct plate names must never produce the same
9919
+ * prefix — a collision would let one plate's `<style>` block reorder another's classes.
10060
9920
  *
10061
- * Plate names are constrained by `isValidPlateName`: each path segment is
10062
- * `\w+(-\w+)*` (word chars with single interior hyphens, no doubled or edge
10063
- * hyphen) and segments are joined by `/`. The slash is the only char that is
10064
- * not class-safe, so rendering it as `--` is enough and because no segment
10065
- * holds a literal `--` or an edge hyphen, no `-` ever abuts a `/`, so every
10066
- * `--` in the prefix unambiguously came from a `/`, making the map injective.
10067
- * Keeping `/` visually as `--` also leaves nested names readable in the
10068
- * generated CSS (`product--card`).
9921
+ * Plate names are constrained by `isValidPlateName`: each path segment is `\w+(-\w+)*` (word chars
9922
+ * with single interior hyphens, no doubled or edge hyphen) and segments are joined by `/`. The
9923
+ * slash is the only char that is not class-safe, so rendering it as `--` is enough and because no
9924
+ * segment holds a literal `--` or an edge hyphen, no `-` ever abuts a `/`, so every `--` in the
9925
+ * prefix unambiguously came from a `/`, making the map injective. Keeping `/` visually as `--` also
9926
+ * leaves nested names readable in the generated CSS (`product--card`).
10069
9927
  */
10070
9928
  function encodePlateName(name) {
10071
9929
  return name.replace(/\//g, "--");
10072
9930
  }
10073
9931
  /**
10074
- * What the class identity keys on, alongside media + declarations:
10075
- * - `'tier'`: the discrete cascade tier — smaller (more bodies dedupe), but a
10076
- * rare same-tier inversion within the continuous `author` band can mis-order
10077
- * one property on one node (ADR 0007). The default.
10078
- * - `'full'`: the exact `spec` — provably no inversion, marginally larger.
10079
- * One-line flip if QA ever surfaces a mis-order.
9932
+ * What the class identity keys on, alongside media + declarations: - `'full'` (the shipped
9933
+ * setting): the exact `spec` — provably no inversion. - `'tier'`: the discrete cascade tier —
9934
+ * marginally smaller (more bodies dedupe), but a rare same-tier inversion within the continuous
9935
+ * `author` band can mis-order one property on one node (ADR 0007). One-line flip if size ever
9936
+ * matters more than the inversion risk.
10080
9937
  */
10081
9938
  const SPEC_FOLD = "full";
10082
9939
  function tierLabel(spec) {
@@ -10088,44 +9945,39 @@ function tierLabel(spec) {
10088
9945
  return "au";
10089
9946
  }
10090
9947
  //#endregion
10091
- //#region src/project.ts
9948
+ //#region src/serialize.ts
10092
9949
  /**
10093
- * SERIALIZE — the single in-browser projection of a per-Plate IR into the shipped
10094
- * `Plate` (ADR 0022 §5). The capture session measures every View into one
10095
- * `PlateIR` (`captureView`, serialize.ts) and runs the reduction passes over it
10096
- * ONCE (`runPlatePasses`, serialize.ts); Serialize is the step AFTER it only
10097
- * PROJECTS, it runs no passes. It assumes each `view.bundle` is already measured
10098
- * AND reduced (the annotate-only passes have set their `dropped`/`folded`/
10099
- * `flattened`/`count` fields), and lowers those annotations into the shipped shape.
9950
+ * SERIALIZE — the single in-browser projection of a per-Plate IR into the shipped `Plate` (ADR 0022
9951
+ * §5). The capture session measures every View into one `PlateIR` (`captureView`, measure.ts) and
9952
+ * runs the Distil passes over it ONCE (`distilPlate`, measure.ts); Serialize is the step AFTER it
9953
+ * only PROJECTS, it runs no passes. It assumes each `view.bundle` is already measured AND distilled
9954
+ * (the annotate-only passes have set their `dropped`/`folded`/ `flattened`/`count` fields), and
9955
+ * lowers those annotations into the shipped shape.
10100
9956
  *
10101
- * It FOLDS `emit` (ir.ts) — emit is the per-View projection that reads the IR back
10102
- * out into id-form `{ tree, rules }`, lowering the annotations via `effectiveKids`/
10103
- * `boneBox`/`effectiveKind`/`effectiveDecls`. Serialize calls it once per View, then
10104
- * builds ONE merged id-tree and gates each View by breakpoint, in-line (the
10105
- * multi-View merge/gate lives HERE now the gen-side `mergeViews` it replaced is
10106
- * gone, ADR 0022 cutover), and runs
10107
- * `classify` to content-address the rules. It returns a `StoredPlate` the gated,
10108
- * content-addressed `{ tree, css }` PLUS `breakpoints` NOT finished chunks: the
10109
- * `serializePlate` (chunk.ts) lowering to `Plate` chunks stays GEN-SIDE at module-load
10110
- * (ADR 0022 §5), keeping the Tailwind/Bundle passthrough a clean gen-side seam and the
10111
- * analysis-only geometry off the wire (only this structured projection is posted).
9957
+ * It FOLDS `emit` (ir.ts) — emit is the per-View projection that reads the IR back out into id-form
9958
+ * `{ tree, rules }`, lowering the annotations via `effectiveKids`/
9959
+ * `boneBox`/`effectiveKind`/`effectiveDecls`. Serialize calls it once per View, then carries every
9960
+ * View whole under ONE id-tree and gates each View by breakpoint in-line (the multi-View gate lives
9961
+ * HERE ADR 0022), and runs `classify` to content-address the rules. It returns a `StoredPlate`
9962
+ * the gated, content-addressed `{ tree, css }` PLUS `breakpoints` — NOT finished chunks: the
9963
+ * `serializePlate` (chunk.ts) lowering to `Plate` chunks stays GEN-SIDE at module-load (ADR 0022
9964
+ * §5), keeping the Tailwind/Bundle passthrough a clean gen-side seam and the analysis-only geometry
9965
+ * off the wire (only this structured projection is posted).
10112
9966
  *
10113
- * The wrapper-island gate (ADR 0022 §2 invariant). A View's breakpoint gate must
10114
- * hide the View's ENTIRE subtree off-range — INCLUDING any stitch — regardless of
10115
- * whether a node has a serializable class slot. The old gen-side `mergeViews` gated
10116
- * each top kid individually; that silently DROPPED a gate landing on a stitch, because
10117
- * `toChunks` serializes a stitch as a bare `{ r }` with no class slot (issue #28: a
10118
- * parent skeleton stitching nested children collapsed off-range). Here each View's
10119
- * top kids are wrapped in ONE synthetic CONTAINER "island" that ALWAYS carries a
10120
- * class; the island is gated ONCE and its `display: none !important` inherits to any
10121
- * stitch inside. Active, the island is `display: contents` so it generates no box
10122
- * layout-neutral (`.xr-node` sets no display, `.xr-root` is itself `display:
10123
- * contents`, so nothing overrides it). This also reduces the gate rules from
10124
- * N-per-View to 1-per-View and is uniform across stitch top-kids, flattened-to-top
9967
+ * The wrapper-island gate (ADR 0022 §2 invariant). A View's breakpoint gate must hide the View's
9968
+ * ENTIRE subtree off-range — INCLUDING any stitch — regardless of whether a node has a serializable
9969
+ * class slot. Gating each top kid individually silently drops a gate that lands on a stitch,
9970
+ * because `toChunks` serializes a stitch as a bare `{ r }` with no class slot (the off-range View
9971
+ * collapse: a parent skeleton stitching nested children collapsed off-range). So each View's top
9972
+ * kids are wrapped in ONE synthetic CONTAINER "island" that ALWAYS carries a class; the island is
9973
+ * gated ONCE and its `display: none !important` inherits to any stitch inside. Active, the island
9974
+ * is `display: contents` so it generates no box layout-neutral (`.xr-node` sets no display,
9975
+ * `.xr-root` is itself `display: contents`, so nothing overrides it). This also reduces the gate
9976
+ * rules from N-per-View to 1-per-View and is uniform across stitch top-kids, flattened-to-top
10125
9977
  * stitches, and nested stitches alike.
10126
9978
  *
10127
- * Pure data-in/data-out over the (already-reduced) IR — `emit` and everything below
10128
- * read only the passed structures, re-measure nothing.
9979
+ * Pure data-in/data-out over the (already-distilled) IR — `emit` and everything below read only the
9980
+ * passed structures, re-measure nothing.
10129
9981
  */
10130
9982
  function serializePlateIR(plate) {
10131
9983
  const views = plate.views.map((view) => emit(view.bundle)).map((projected, i) => ({
@@ -10135,7 +9987,7 @@ function serializePlateIR(plate) {
10135
9987
  if (views.length <= 1) {
10136
9988
  const only = views[0];
10137
9989
  if (!only) return {
10138
- v: 2,
9990
+ v: 1,
10139
9991
  name: plate.name,
10140
9992
  breakpoints: plate.breakpoints,
10141
9993
  tree: null,
@@ -10143,14 +9995,14 @@ function serializePlateIR(plate) {
10143
9995
  };
10144
9996
  const { tree, css } = classify(only.rules, only.tree, plate.name);
10145
9997
  return {
10146
- v: 2,
9998
+ v: 1,
10147
9999
  name: plate.name,
10148
10000
  breakpoints: plate.breakpoints,
10149
10001
  tree,
10150
10002
  css
10151
10003
  };
10152
10004
  }
10153
- const starts = views.map((view) => regimeFor(view.width, plate.breakpoints).min ?? 0);
10005
+ const starts = views.map((view) => viewSpanFor(view.width, plate.breakpoints).min ?? 0);
10154
10006
  let nextId = 1;
10155
10007
  const islands = [];
10156
10008
  const rules = [];
@@ -10179,7 +10031,7 @@ function serializePlateIR(plate) {
10179
10031
  kids: islands
10180
10032
  }, plate.name);
10181
10033
  return {
10182
- v: 2,
10034
+ v: 1,
10183
10035
  name: plate.name,
10184
10036
  breakpoints: plate.breakpoints,
10185
10037
  tree,
@@ -10196,11 +10048,7 @@ function gate(id, condition) {
10196
10048
  order: id
10197
10049
  };
10198
10050
  }
10199
- /**
10200
- * Deep-clone a captured subtree into fresh ids, recording the mapping. The
10201
- * in-browser home of the old gen-side merge's id remap (`mergeViews`, removed in
10202
- * the ADR 0022 cutover).
10203
- */
10051
+ /** Deep-clone a captured subtree into fresh ids, recording the mapping. */
10204
10052
  function cloneWithIds(node, idMap, allocate) {
10205
10053
  const id = allocate();
10206
10054
  idMap.set(node.id, id);
@@ -10214,21 +10062,19 @@ function cloneWithIds(node, idMap, allocate) {
10214
10062
  };
10215
10063
  }
10216
10064
  /**
10217
- * Remap a View's rule ids onto its fresh-id-remapped nodes (the old gen-side merge's
10218
- * remap). Every View's nodes are re-id'd into one shared global id space here, so a
10219
- * rule id that is NOT in this View's `idMap` must NOT keep its old per-View number:
10220
- * that number is some OTHER View's global id, and aliasing onto it cross-contaminates
10221
- * Views (a box-sizing/flex rule from View B landing on View A's island/container —
10222
- * #28 redux, the crooked multi-view layout). An unmapped id is one of two things:
10065
+ * Remap a View's rule ids onto its fresh-id-remapped nodes. Every View's nodes are re-id'd into one
10066
+ * shared global id space here, so a rule id that is NOT in this View's `idMap` must NOT keep its
10067
+ * per-View number: that number is some OTHER View's global id, and aliasing onto it
10068
+ * cross-contaminates Views (a box-sizing/flex rule from View B landing on View A's island/container
10069
+ * the crooked multi-view layout). An unmapped id is one of two things:
10223
10070
  *
10224
- * - `0` — the per-View synthetic root. Its rules (carried tokens, the context rule)
10225
- * belong on the ONE shared merged root, also id 0, so map `0 → 0`.
10226
- * - any other unmapped id — a node a reduction pass DROPPED (Superset/Crop/glyph-fold)
10227
- * that a shared author rule still lists by id. It has no node in this View's
10228
- * projected tree, so drop the id (emit already prunes `extraRules` this way; author
10229
- * rules were the gap). A rule left with no ids is itself dropped — note an
10230
- * originally-rootward rule (`ids: []`) is preserved as-is, since `classify` resolves
10231
- * an empty `ids` to the root.
10071
+ * - `0` — the per-View synthetic root. Its rules (carried tokens, the context rule) belong on the ONE
10072
+ * shared plate root, also id 0, so map `0 → 0`.
10073
+ * - Any other unmapped id — a node a Distil pass DROPPED (Superset/Crop/glyph-fold) that a shared
10074
+ * author rule still lists by id. It has no node in this View's projected tree, so drop the id
10075
+ * (emit already prunes `extraRules` this way; author rules were the gap). A rule left with no ids
10076
+ * is itself dropped note an originally-rootward rule (`ids: []`) is preserved as-is, since
10077
+ * `classify` resolves an empty `ids` to the root.
10232
10078
  */
10233
10079
  function remapRules(rules, idMap) {
10234
10080
  const out = [];
@@ -10251,4 +10097,4 @@ function remapRules(rules, idMap) {
10251
10097
  return out;
10252
10098
  }
10253
10099
  //#endregion
10254
- export { serializePlate as a, defineXrayCaptureWalker as c, formatDiagnostic as d, makeDiagnostic as f, collectRefs as i, runPlatePasses as l, deriveBreakpoints as n, CaptureTooLargeError as o, emit as p, regimeFor as r, captureView as s, serializePlateIR as t, diagnosticsHeader as u };
10100
+ export { serializePlate as a, defineXrayCaptureWalker as c, formatDiagnostic as d, makeDiagnostic as f, collectRefs as i, distilPlate as l, deriveBreakpoints as n, CaptureTooLargeError as o, emit as p, viewSpanFor as r, captureView as s, serializePlateIR as t, diagnosticsHeader as u };