@beltype/cicero 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +7 -6
  2. package/dist/seam/client.d.ts +12 -4
  3. package/dist/seam/client.js +57 -21
  4. package/dist/seam/facsimile.d.ts +13 -5
  5. package/dist/seam/facsimile.js +28 -12
  6. package/dist/seam/index.js +1 -1
  7. package/dist/seam/rewrite.js +7 -1
  8. package/dist/seam/rim.d.ts +6 -2
  9. package/dist/seam/rim.js +54 -11
  10. package/dist/src/capture.d.ts +28 -0
  11. package/dist/src/capture.js +121 -45
  12. package/dist/src/compose-dom.d.ts +8 -0
  13. package/dist/src/compose-dom.js +62 -8
  14. package/dist/src/compositor.d.ts +11 -2
  15. package/dist/src/compositor.js +47 -15
  16. package/dist/src/data-beltype.d.ts +11 -1
  17. package/dist/src/data-beltype.js +20 -1
  18. package/dist/src/document-state.d.ts +6 -2
  19. package/dist/src/document-state.js +87 -11
  20. package/dist/src/driver.d.ts +6 -0
  21. package/dist/src/driver.js +36 -46
  22. package/dist/src/forcing-memo.d.ts +10 -0
  23. package/dist/src/forcing-memo.js +22 -0
  24. package/dist/src/fragmentation.d.ts +3 -3
  25. package/dist/src/fragmentation.js +3 -3
  26. package/dist/src/fragments.d.ts +1 -1
  27. package/dist/src/fragments.js +47 -16
  28. package/dist/src/gauge.d.ts +0 -4
  29. package/dist/src/gauge.js +16 -30
  30. package/dist/src/gcpm-encoding.d.ts +2 -0
  31. package/dist/src/gcpm-encoding.js +2 -0
  32. package/dist/src/measure.d.ts +17 -12
  33. package/dist/src/measure.js +30 -16
  34. package/dist/src/positions.js +21 -9
  35. package/dist/src/reads.d.ts +4 -2
  36. package/dist/src/reads.js +10 -4
  37. package/dist/src/root.d.ts +18 -1
  38. package/dist/src/root.js +29 -3
  39. package/dist/src/stonehand.js +2 -6
  40. package/dist/src/word-space.d.ts +10 -8
  41. package/dist/src/word-space.js +44 -16
  42. package/package.json +5 -1
@@ -23,6 +23,8 @@ export type ContentFn = {
23
23
  export declare const STRING_SET_PROP = "--beltype-string-set";
24
24
  /** `position: running(head)` survives as this custom property, holding the identifier alone. */
25
25
  export declare const RUNNING_PROP = "--beltype-running";
26
+ /** `hyphenate-limit-lines` survives as this custom property, its value text carried verbatim (Chromium drops the property, so the Compositor reads the author's limit from here). It inherits, as the original does, and so is not among {@link CUSTOM_PROPS}. */
27
+ export declare const HYPHENATE_LIMIT_LINES_PROP = "--beltype-hyphenate-limit-lines";
26
28
  /** Both, for a host that registers them. `syntax: '*'` because the values are the author's own text and this engine is not their grammar; `inherits: false` because neither original inherits. */
27
29
  export declare const CUSTOM_PROPS: string[];
28
30
  /**
@@ -13,6 +13,8 @@
13
13
  export const STRING_SET_PROP = '--beltype-string-set';
14
14
  /** `position: running(head)` survives as this custom property, holding the identifier alone. */
15
15
  export const RUNNING_PROP = '--beltype-running';
16
+ /** `hyphenate-limit-lines` survives as this custom property, its value text carried verbatim (Chromium drops the property, so the Compositor reads the author's limit from here). It inherits, as the original does, and so is not among {@link CUSTOM_PROPS}. */
17
+ export const HYPHENATE_LIMIT_LINES_PROP = '--beltype-hyphenate-limit-lines';
16
18
  /** Both, for a host that registers them. `syntax: '*'` because the values are the author's own text and this engine is not their grammar; `inherits: false` because neither original inherits. */
17
19
  export const CUSTOM_PROPS = [STRING_SET_PROP, RUNNING_PROP];
18
20
  /**
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The measure pass (first-coding-round.md, item 2): a hidden measurement container at true content width, off-viewport, one block flow, shared by every page of one geometry. The Chase is not in it and does not measure. Geometry comes from clones laid out here; law (break-* constraints, orphans, widows) is read from the source elements in the live document, so neutralization below never blinds legality reading.
3
3
  *
4
- * *Render-identical conditions* (I5) are pinned here: they are asserted at intake (unit DPR, explicit line-height — the DPR ruling), fonts and images are awaited once before the first measure, and native break properties plus orphans/widows are neutralized in the measure context. They are inert in an unfragmented column anyway — neutralization is stated, not assumed.
4
+ * *Render-identical conditions* (I5) are pinned here: they are asserted at intake (unit DPR, explicit line-height — the DPR ruling), fonts are awaited once before the first measure, and native break properties plus orphans/widows are neutralized in the measure context. They are inert in an unfragmented column anyway — neutralization is stated, not assumed.
5
5
  *
6
- * The images are awaited as a stand-in for the first run, and `awaitImages` says why in full.
6
+ * The first run no longer awaits the document's images (ruled 2026-11-08): it makes them eager ({@link eagerImages}), and the rim answers an arrival that changed a box. A measured slice's own images are still awaited ({@link awaitCloneImages}).
7
7
  *
8
8
  * @packageDocumentation
9
9
  */
@@ -28,26 +28,31 @@ export declare function assertRenderConditions(doc: Document, { relaxDpr }?: {
28
28
  */
29
29
  export declare function awaitFonts(doc: Document, faces?: string[]): Promise<void>;
30
30
  /**
31
- * Await every image that has not settled — [*images are awaited at intake*](../../../.claude/decisions/cicero/images-are-awaited-at-intake.md), reported as beltype#39.
31
+ * Make every `loading="lazy"` image eager, and wait for none: what the first run does with the document's pictures since 2026-11-08.
32
32
  *
33
- * **This is a stand-in, and the difference matters to whoever reads it next.** The tablet's model does not say images are awaited: it says they are *measured at declared or placeholder sizes*, and that the wrong measurement is then corrected through the one machinery all late-arriving truth shares — void from the box, rebuild forward — which since 2026-10-13 is the resumable run, called by the rim at the seam (`seam/rim.js`, *late truth is an edit*): a picture that becomes pending after the first measure is heard and resumed from, so for such a picture the correction exists. The await below is therefore a stand-in for the first run only: the choice there is still between awaiting and measuring an `<img>` at zero for the first paint, which is what the engine did until 2026-09-18, and what put whole blocks on no page. Retiring it trades a known-good first paint for one resume per image, and what that costs on the manual is a measurement owed (the record's Consequences), not a ruling.
33
+ * @remarks Egildo ruled on 2026-11-08 that the intake's and the driver's first-run awaits are retired, on the measurement `.claude/plans/quick-rulings-2026-11-08/image-await.md` in the monorepo: on a 59-page document with 60 images served 300 to 800 ms late, the first paint fell from 5.8 s to 0.74 s, the final layout was the same, and with declared sizes nothing moved on arrival. A picture measured before it arrived is late truth, and the rim answers it (`seam/rim.js`, *late truth is an edit*, amended 2026-11-08): one resume where its arrival changed its box, none where it did not. So the fast first paint is the author's to give: an image that declares its `width` and `height` is measured at its size before a byte of it arrives.
34
34
  *
35
- * **A measurement the engine has no way to correct must not be taken early.** The first run has the way now and does not use it; the await goes when that trade is measured and ruled.
35
+ * **A lazy image is still made eager** (*late truth is an edit*, 2026-10-13). Measured that day: a lazy image with no box never issues its request, and a parked original has no box, so it was measured at zero and never revisited. Lazy loading optimises for a viewport, and print has no viewport: there is nothing for it to save, and *the source document is never written* (C5) is untouched because the attribute is set on the original's `loading` property, which is a request for the bytes and not a change to what the document says. The clones the measure container receives copy the attribute, so they are eager too.
36
36
  *
37
- * A failed load settles at its error size: the wait resolves on `error` as it resolves on `load`, because what adjudication needs is that the geometry has stopped changing, not that it is the one the author wanted. `allSettled` for the same reason — one picture that never arrives must not hold the others.
37
+ * **It reads the originals and never the copies** (2026-10-08). A presented document carries a clone of every box on every page, and `document.images` met each picture once per copy (19 468 images for 157 real ones on a 123-page manual). A host that has presented hands the top-level boxes it took at intake; a first run has no copies and is handed the document.
38
38
  *
39
- * **A `loading="lazy"` image is made eager and awaited like the others** (*late truth is an edit*, 2026-10-13). Measured that day: a lazy image with no box never issues its request, and a parked original has no box, so the load the old paragraph hoped for ("the event may simply never come") was not a risk but the rule — the picture was measured at zero and never revisited. Lazy loading optimises for a viewport, and print has no viewport: there is nothing for it to save, and *the source document is never written* (C5) is untouched because the attribute is set on the original's `loading` property, which is a request for the bytes and not a change to what the document says. The clones the measure container receives copy the attribute, so they are eager too.
39
+ * @param scope - as {@link awaitImages}'s.
40
+ * @returns how many images were looked at, which the run's timing reads and nothing that decides does.
41
+ */
42
+ export declare function eagerImages(scope: Document | Element | Element[]): number;
43
+ /**
44
+ * Await every image in a scope that has not settled, a lazy one made eager first as {@link eagerImages} makes it. Since 2026-11-08 its one caller in the engine is {@link awaitCloneImages}: the first run no longer waits for the document's pictures (*images are awaited at intake*, retired that day for the first run).
40
45
  *
41
- * **It reads the originals and never the copies** (2026-10-08). A presented document carries a clone of every box on every page, and `document.images` met each picture once per copy — 19 468 images for 157 real ones on a 123-page manual, walked on every edit. A host that has presented hands the top-level boxes it took at intake and only their pictures are asked; a first run has no copies and is handed the document.
46
+ * A failed load settles at its error size: the wait resolves on `error` as it resolves on `load`, because what adjudication needs is that the geometry has stopped changing, not that it is the one the author wanted. `allSettled` for the same reason — one picture that never arrives must not hold the others.
42
47
  *
43
- * @param scope - a Document answers `images`; an element is asked for its own; a list of elements (the intake's top-level boxes) is asked box by box, each box included.
44
- * @returns how many images were looked at — read by the run's timing and by nothing that decides.
48
+ * @param scope - a Document answers `images`; an element is asked for its own; a list of elements is asked box by box, each box included.
49
+ * @returns how many images were looked at.
45
50
  */
46
51
  export declare function awaitImages(scope: Document | Element | Element[]): Promise<number>;
47
52
  /**
48
- * The clones' images, awaited where they are measured — the same stand-in, one step further in (2026-09-21, found by the Chromium 153 census).
53
+ * The clones' images, awaited where they are measured (2026-09-21, found by the Chromium 153 census). **It stays when the first run's two awaits went (ruled 2026-11-08) until the Service Worker case below is measured without it**: the measurement behind that ruling ran under no worker, and with this await gone too its first paint fell again, from 0.74 s to 0.39 s, and undeclared sizes refilled the pages as each picture came.
49
54
  *
50
- * `awaitImages(doc)` settles the document's own `<img>` elements, and the engine measures clones of them, mounted in the measure container. That was sound only on an unstated premise: that a clone of a loaded image is available synchronously — HTML's *list of available images*, which lets a second `<img>` for an already-fetched URL be `complete` the moment it is inserted. Chromium 150 honoured it everywhere. **Chromium 153 does not in a document a Service Worker controls**: measured in the editing tools' pane, every clone answered `complete: false` and a height of 0 in the turn it was mounted, and its real height 300 ms later — so every figure measured nothing, the cuts fell late, and the presentation then clipped what the account believed it had placed (the Caster specimen lost Figure 2 and the drawing after it, on no page at all). The CLI's dev server has no worker and was untouched, which is why the battery did not see it.
55
+ * Until 2026-11-08 `awaitImages(doc)` settled the document's own `<img>` elements first, and the engine measures clones of them, mounted in the measure container. That was sound only on an unstated premise: that a clone of a loaded image is available synchronously — HTML's *list of available images*, which lets a second `<img>` for an already-fetched URL be `complete` the moment it is inserted. Chromium 150 honoured it everywhere. **Chromium 153 does not in a document a Service Worker controls**: measured in the editing tools' pane, every clone answered `complete: false` and a height of 0 in the turn it was mounted, and its real height 300 ms later — so every figure measured nothing, the cuts fell late, and the presentation then clipped what the account believed it had placed (the Caster specimen lost Figure 2 and the drawing after it, on no page at all). The CLI's dev server has no worker and was untouched, which is why the battery did not see it.
51
56
  *
52
57
  * So the premise is checked instead of assumed: after a mount, if any image in the container has not settled, wait for it and lay out again. It costs nothing where the premise holds, and *a measurement the engine has no way to correct must not be taken early* is already this file's rule.
53
58
  *
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The measure pass (first-coding-round.md, item 2): a hidden measurement container at true content width, off-viewport, one block flow, shared by every page of one geometry. The Chase is not in it and does not measure. Geometry comes from clones laid out here; law (break-* constraints, orphans, widows) is read from the source elements in the live document, so neutralization below never blinds legality reading.
3
3
  *
4
- * *Render-identical conditions* (I5) are pinned here: they are asserted at intake (unit DPR, explicit line-height — the DPR ruling), fonts and images are awaited once before the first measure, and native break properties plus orphans/widows are neutralized in the measure context. They are inert in an unfragmented column anyway — neutralization is stated, not assumed.
4
+ * *Render-identical conditions* (I5) are pinned here: they are asserted at intake (unit DPR, explicit line-height — the DPR ruling), fonts are awaited once before the first measure, and native break properties plus orphans/widows are neutralized in the measure context. They are inert in an unfragmented column anyway — neutralization is stated, not assumed.
5
5
  *
6
- * The images are awaited as a stand-in for the first run, and `awaitImages` says why in full.
6
+ * The first run no longer awaits the document's images (ruled 2026-11-08): it makes them eager ({@link eagerImages}), and the rim answers an arrival that changed a box. A measured slice's own images are still awaited ({@link awaitCloneImages}).
7
7
  *
8
8
  * @packageDocumentation
9
9
  */
@@ -45,30 +45,44 @@ export async function awaitFonts(doc, faces = []) {
45
45
  await Promise.all(faces.map((face) => doc.fonts.load(face).catch(() => [])));
46
46
  await doc.fonts.ready;
47
47
  }
48
+ /** The `<img>` elements of a scope: a Document answers `images`; an element is asked for its own; a list of elements (the intake's top-level boxes) is asked box by box, each box included. */
49
+ const imagesOf = (scope) => Array.isArray(scope)
50
+ ? scope.flatMap((el) => (el.localName === 'img' ? [el] : [...el.querySelectorAll('img')]))
51
+ : scope.images ?? scope.querySelectorAll('img');
48
52
  /**
49
- * Await every image that has not settled — [*images are awaited at intake*](../../../.claude/decisions/cicero/images-are-awaited-at-intake.md), reported as beltype#39.
53
+ * Make every `loading="lazy"` image eager, and wait for none: what the first run does with the document's pictures since 2026-11-08.
50
54
  *
51
- * **This is a stand-in, and the difference matters to whoever reads it next.** The tablet's model does not say images are awaited: it says they are *measured at declared or placeholder sizes*, and that the wrong measurement is then corrected through the one machinery all late-arriving truth shares — void from the box, rebuild forward — which since 2026-10-13 is the resumable run, called by the rim at the seam (`seam/rim.js`, *late truth is an edit*): a picture that becomes pending after the first measure is heard and resumed from, so for such a picture the correction exists. The await below is therefore a stand-in for the first run only: the choice there is still between awaiting and measuring an `<img>` at zero for the first paint, which is what the engine did until 2026-09-18, and what put whole blocks on no page. Retiring it trades a known-good first paint for one resume per image, and what that costs on the manual is a measurement owed (the record's Consequences), not a ruling.
55
+ * @remarks Egildo ruled on 2026-11-08 that the intake's and the driver's first-run awaits are retired, on the measurement `.claude/plans/quick-rulings-2026-11-08/image-await.md` in the monorepo: on a 59-page document with 60 images served 300 to 800 ms late, the first paint fell from 5.8 s to 0.74 s, the final layout was the same, and with declared sizes nothing moved on arrival. A picture measured before it arrived is late truth, and the rim answers it (`seam/rim.js`, *late truth is an edit*, amended 2026-11-08): one resume where its arrival changed its box, none where it did not. So the fast first paint is the author's to give: an image that declares its `width` and `height` is measured at its size before a byte of it arrives.
52
56
  *
53
- * **A measurement the engine has no way to correct must not be taken early.** The first run has the way now and does not use it; the await goes when that trade is measured and ruled.
57
+ * **A lazy image is still made eager** (*late truth is an edit*, 2026-10-13). Measured that day: a lazy image with no box never issues its request, and a parked original has no box, so it was measured at zero and never revisited. Lazy loading optimises for a viewport, and print has no viewport: there is nothing for it to save, and *the source document is never written* (C5) is untouched because the attribute is set on the original's `loading` property, which is a request for the bytes and not a change to what the document says. The clones the measure container receives copy the attribute, so they are eager too.
54
58
  *
55
- * A failed load settles at its error size: the wait resolves on `error` as it resolves on `load`, because what adjudication needs is that the geometry has stopped changing, not that it is the one the author wanted. `allSettled` for the same reason — one picture that never arrives must not hold the others.
59
+ * **It reads the originals and never the copies** (2026-10-08). A presented document carries a clone of every box on every page, and `document.images` met each picture once per copy (19 468 images for 157 real ones on a 123-page manual). A host that has presented hands the top-level boxes it took at intake; a first run has no copies and is handed the document.
56
60
  *
57
- * **A `loading="lazy"` image is made eager and awaited like the others** (*late truth is an edit*, 2026-10-13). Measured that day: a lazy image with no box never issues its request, and a parked original has no box, so the load the old paragraph hoped for ("the event may simply never come") was not a risk but the rule — the picture was measured at zero and never revisited. Lazy loading optimises for a viewport, and print has no viewport: there is nothing for it to save, and *the source document is never written* (C5) is untouched because the attribute is set on the original's `loading` property, which is a request for the bytes and not a change to what the document says. The clones the measure container receives copy the attribute, so they are eager too.
61
+ * @param scope - as {@link awaitImages}'s.
62
+ * @returns how many images were looked at, which the run's timing reads and nothing that decides does.
63
+ */
64
+ export function eagerImages(scope) {
65
+ let seen = 0;
66
+ for (const img of imagesOf(scope)) {
67
+ seen++;
68
+ if (!img.complete && img.loading === 'lazy')
69
+ img.loading = 'eager'; // setting it starts the request a lazy image never made
70
+ }
71
+ return seen;
72
+ }
73
+ /**
74
+ * Await every image in a scope that has not settled, a lazy one made eager first as {@link eagerImages} makes it. Since 2026-11-08 its one caller in the engine is {@link awaitCloneImages}: the first run no longer waits for the document's pictures (*images are awaited at intake*, retired that day for the first run).
58
75
  *
59
- * **It reads the originals and never the copies** (2026-10-08). A presented document carries a clone of every box on every page, and `document.images` met each picture once per copy — 19 468 images for 157 real ones on a 123-page manual, walked on every edit. A host that has presented hands the top-level boxes it took at intake and only their pictures are asked; a first run has no copies and is handed the document.
76
+ * A failed load settles at its error size: the wait resolves on `error` as it resolves on `load`, because what adjudication needs is that the geometry has stopped changing, not that it is the one the author wanted. `allSettled` for the same reason — one picture that never arrives must not hold the others.
60
77
  *
61
- * @param scope - a Document answers `images`; an element is asked for its own; a list of elements (the intake's top-level boxes) is asked box by box, each box included.
62
- * @returns how many images were looked at — read by the run's timing and by nothing that decides.
78
+ * @param scope - a Document answers `images`; an element is asked for its own; a list of elements is asked box by box, each box included.
79
+ * @returns how many images were looked at.
63
80
  */
64
81
  export async function awaitImages(scope) {
65
82
  const pending = [];
66
83
  let seen = 0;
67
- const images = Array.isArray(scope)
68
- ? scope.flatMap((el) => (el.localName === 'img' ? [el] : [...el.querySelectorAll('img')]))
69
- : scope.images ?? scope.querySelectorAll('img');
70
84
  // A snapshot of a live collection, and no event can fire between the test and the listener: the loop is one synchronous turn.
71
- for (const img of images) {
85
+ for (const img of imagesOf(scope)) {
72
86
  seen++;
73
87
  if (img.complete)
74
88
  continue;
@@ -84,9 +98,9 @@ export async function awaitImages(scope) {
84
98
  return seen;
85
99
  }
86
100
  /**
87
- * The clones' images, awaited where they are measured — the same stand-in, one step further in (2026-09-21, found by the Chromium 153 census).
101
+ * The clones' images, awaited where they are measured (2026-09-21, found by the Chromium 153 census). **It stays when the first run's two awaits went (ruled 2026-11-08) until the Service Worker case below is measured without it**: the measurement behind that ruling ran under no worker, and with this await gone too its first paint fell again, from 0.74 s to 0.39 s, and undeclared sizes refilled the pages as each picture came.
88
102
  *
89
- * `awaitImages(doc)` settles the document's own `<img>` elements, and the engine measures clones of them, mounted in the measure container. That was sound only on an unstated premise: that a clone of a loaded image is available synchronously — HTML's *list of available images*, which lets a second `<img>` for an already-fetched URL be `complete` the moment it is inserted. Chromium 150 honoured it everywhere. **Chromium 153 does not in a document a Service Worker controls**: measured in the editing tools' pane, every clone answered `complete: false` and a height of 0 in the turn it was mounted, and its real height 300 ms later — so every figure measured nothing, the cuts fell late, and the presentation then clipped what the account believed it had placed (the Caster specimen lost Figure 2 and the drawing after it, on no page at all). The CLI's dev server has no worker and was untouched, which is why the battery did not see it.
103
+ * Until 2026-11-08 `awaitImages(doc)` settled the document's own `<img>` elements first, and the engine measures clones of them, mounted in the measure container. That was sound only on an unstated premise: that a clone of a loaded image is available synchronously — HTML's *list of available images*, which lets a second `<img>` for an already-fetched URL be `complete` the moment it is inserted. Chromium 150 honoured it everywhere. **Chromium 153 does not in a document a Service Worker controls**: measured in the editing tools' pane, every clone answered `complete: false` and a height of 0 in the turn it was mounted, and its real height 300 ms later — so every figure measured nothing, the cuts fell late, and the presentation then clipped what the account believed it had placed (the Caster specimen lost Figure 2 and the drawing after it, on no page at all). The CLI's dev server has no worker and was untouched, which is why the battery did not see it.
90
104
  *
91
105
  * So the premise is checked instead of assumed: after a mount, if any image in the container has not settled, wait for it and lay out again. It costs nothing where the premise holds, and *a measurement the engine has no way to correct must not be taken early* is already this file's rule.
92
106
  *
@@ -28,7 +28,7 @@
28
28
  *
29
29
  * @packageDocumentation
30
30
  */
31
- import { BREAKING_SPACE, isFixedBox, isInlineBox, isLineBreak } from './word-space.js';
31
+ import { BREAKING_SPACE, inDisplayOrder, isFixedBox, isInlineBox, isLineBreak } from './word-space.js';
32
32
  /** `outer` is a proper prefix of `inner`: the row item at `outer` holds the one at `inner`. */
33
33
  const holds = (outer, inner) => outer.length < inner.length && outer.every((n, i) => n === inner[i]);
34
34
  /**
@@ -86,11 +86,11 @@ export function flattenResume(tree) {
86
86
  }
87
87
  /** The address, resolved and compared. Six small functions, all of them pure and all of them touching nothing but `.children`, `.parentElement` and plain arrays — so the DOM-free suites feed them object trees, and so a realm that holds only an account can still say which cut comes first. */
88
88
  /**
89
- * A box's in-flow element children — what a path's indices count.
89
+ * A box's in-flow element children — what a path's indices count — in display order, which differs from the order written inside a table alone (`word-space.js`' `inDisplayOrder`; *the-word-space-follows-the-table-display-order*, 2026-11-07): a header group written after its rows is a table's child 0, so paths order a table's boxes as its words are ordered.
90
90
  *
91
91
  * **A `position: fixed` box is not among them, and that is the only exclusion.** Its footprint is zero at any depth (CSS 2.1 §9.6.1, *a fixed box at any depth*), so it is in no flow, no word space, no feed and no presentation — and the address is the flow's own coordinate, so it is in no path either. That is also the only reading that resolves in every tree a path is resolved in, which is a stronger argument than the principle and was measured rather than assumed: the feed's clones have had fixed boxes stripped, the presentation's clones likewise, and the source is not one tree — `root.append(...blocks, ...fixed)` at intake reparents a nested fixed box out of the block it was declared in, so the tree the driver builds a path against and the tree the presentation resolves it against differ by exactly those boxes. Filtering makes the index the same number in all three; counting them raw made it right in one and wrong in the others, which is what put a shell on the box next door (`packages/essay/cases/55-the-shell-behind-the-fixed-box`).
92
92
  */
93
- const inFlowChildren = (el, isFixed) => [...(el.children ?? [])].filter((c) => !isFixed(c));
93
+ const inFlowChildren = (el, isFixed) => inDisplayOrder(el, [...(el.children ?? [])].filter((c) => !isFixed(c)));
94
94
  /** The box a path names, descending from the top-level block it starts at. `null` where the tree no longer answers the path, which is an edited document rather than a defect. */
95
95
  export function elAtIn(blockEl, path, isFixed = isFixedBox) {
96
96
  let el = blockEl ?? null;
@@ -235,7 +235,7 @@ export function indexBlock(block, b, isInline = isInlineBox, isFixed = isFixedBo
235
235
  const inside = inWord;
236
236
  const start = count;
237
237
  const before = inside ? count - 1 : count;
238
- for (const node of el.childNodes) {
238
+ for (const node of inDisplayOrder(el, [...el.childNodes])) {
239
239
  if (node.nodeType === 3) {
240
240
  const s = node.data;
241
241
  for (let i = 0; i < s.length; i++) {
@@ -304,25 +304,39 @@ export function indexBlocks(blocks, isInline = isInlineBox, isFixed = isFixedBox
304
304
  export function wordsBeforeChar(el, charOffset, isInline = isInlineBox) {
305
305
  if (!(charOffset >= 0))
306
306
  return null;
307
+ // The offset counts characters in the order written, and the words are counted in display order (*the-word-space-follows-the-table-display-order*): each text node's first character is found in the first order, and the walk below runs in the second.
308
+ const startOf = new Map();
307
309
  let chars = 0;
310
+ (function count(node) {
311
+ for (const child of node.childNodes) {
312
+ if (child.nodeType === 3) {
313
+ startOf.set(child, chars);
314
+ chars += child.data.length;
315
+ }
316
+ else if (child.nodeType === 1)
317
+ count(child);
318
+ }
319
+ })(el);
320
+ if (charOffset > chars)
321
+ return null;
308
322
  let words = 0;
309
323
  let inWord = false;
310
324
  let done = false;
311
325
  let inside = false;
312
326
  const walk = (node) => {
313
- for (const child of node.childNodes) {
327
+ for (const child of inDisplayOrder(node, [...node.childNodes])) {
314
328
  if (done)
315
329
  return;
316
330
  if (child.nodeType === 3) {
317
331
  const s = child.data;
332
+ const start = startOf.get(child);
318
333
  for (let i = 0; i < s.length; i++) {
319
334
  // The character at the offset continues a word that began before it exactly when the walk is inside a word and this character is not a breaking space: `inWord` is the tokenization's own state, reset at every non-inline boundary, so a word does not straddle across one.
320
- if (chars === charOffset) {
335
+ if (start + i === charOffset) {
321
336
  inside = inWord && !BREAKING_SPACE.test(s[i]);
322
337
  done = true;
323
338
  return;
324
339
  }
325
- chars++;
326
340
  if (BREAKING_SPACE.test(s[i]))
327
341
  inWord = false;
328
342
  else if (!inWord) {
@@ -342,8 +356,6 @@ export function wordsBeforeChar(el, charOffset, isInline = isInlineBox) {
342
356
  }
343
357
  };
344
358
  walk(el);
345
- if (!done && chars !== charOffset)
346
- return null;
347
359
  return inside ? words - 1 : words;
348
360
  }
349
361
  /**
@@ -110,6 +110,8 @@ export type PageSite = {
110
110
  * @returns null where no rule reads a page fact.
111
111
  */
112
112
  export declare function findPageSites(doc: Document, originals?: Element[] | null, keep?: Keep | null): PageSite[] | null;
113
+ /** The flow sites that read the page total, as the rule that reads it names them: what {@link hideReservations} paints invisible while the total is unknown, since a flow site reads `0` until then (`chase.ts`'s `setCounters`). A `counter(page)` and a named string are true from the first page and are not among them. */
114
+ export declare function totalSites(doc: Document): Pick<Site, 'key' | 'pseudos'>[];
113
115
  /** What every page of a run publishes, as the measure wears it: the page's number and the document's total, and its named strings' properties (`chase.js`'s `setCounters` and `setStrings`, the same values). */
114
116
  export declare function pageFactsOf(account: Array<{
115
117
  closes: {
@@ -143,13 +145,13 @@ export declare const isDeliverySheet: (doc: Document, sheet: CSSStyleSheet) => b
143
145
  * @param sites - the page reads that are still at the reservation.
144
146
  * @param scope - the selector of the element the presented pages stand in.
145
147
  */
146
- export declare function hideSheetText(sites: Site[], scope?: string): string;
148
+ export declare function hideSheetText(sites: Pick<Site, 'key' | 'pseudos'>[], scope?: string): string;
147
149
  /**
148
150
  * Paint the reservations invisible: the page numbers the cuts so far do not know yet (*the pages are cut as far as they are asked*, Egildo's ruling 1 of 2026-10-12, on the note's recommendation). A document shown before it is complete must not show `0` where a page number will stand.
149
151
  *
150
152
  * @param scope - as {@link hideSheetText}.
151
153
  */
152
- export declare function hideReservations(doc: Document, sites: Site[], scope?: string | undefined): void;
154
+ export declare function hideReservations(doc: Document, sites: Pick<Site, 'key' | 'pseudos'>[], scope?: string | undefined): void;
153
155
  /** Take the hiding sheet off: the true numbers are delivered, or there is nothing left to hide. */
154
156
  export declare function showReservations(doc: Document): void;
155
157
  /** Take the delivery sheet off the document, where one was adopted — a second paginate of a document that no longer declares a read leaves nothing behind. */
package/dist/src/reads.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import { READ_PREFIX, STRING_KEYWORDS, STRING_PROP_PREFIX, decodeRead, encodeStringProp, splitTopLevel } from './gcpm-encoding.js';
15
15
  import { sheetsCountInPseudos, styleSheetsOf } from './capture.js';
16
16
  import { format as formatCounter } from './counter-styles.js';
17
- import { collapse, counterStatesAfter, counterValue, counterValues, documentOpsOf } from './document-state.js';
17
+ import { collapse, contentTextOf, counterStatesAfter, counterValue, counterValues, documentOpsOf } from './document-state.js';
18
18
  import { indexBlocks, pageContaining, pageIntervals, pathOf } from './positions.js';
19
19
  import { isInlineBox } from './word-space.js';
20
20
  import { composeResumed } from './resume.js';
@@ -237,6 +237,12 @@ export function findPageSites(doc, originals = null, keep = null) {
237
237
  }
238
238
  return sites.length ? sites : null;
239
239
  }
240
+ /** The flow sites that read the page total, as the rule that reads it names them: what {@link hideReservations} paints invisible while the total is unknown, since a flow site reads `0` until then (`chase.ts`'s `setCounters`). A `counter(page)` and a named string are true from the first page and are not among them. */
241
+ export function totalSites(doc) {
242
+ return contentRules(doc)
243
+ .filter((r) => /counters?\(\s*pages\s*[,)]/.test(r.content))
244
+ .map((r) => ({ key: hostSelector(r.selector), pseudos: r.pseudos }));
245
+ }
240
246
  /** What every page of a run publishes, as the measure wears it: the page's number and the document's total, and its named strings' properties (`chase.js`'s `setCounters` and `setStrings`, the same values). */
241
247
  export function pageFactsOf(account) {
242
248
  return account.map((entry) => {
@@ -401,8 +407,8 @@ export function staticValues(sites, { doc, blocks, index = null, keep = null })
401
407
  if (!s.target || isPageRead(s.read))
402
408
  continue;
403
409
  if (s.read.fn === 'text') {
404
- // css-content-3 §2.6.3's `content` keyword is the element's string value, `content()`'s own reading (document-state's `collapse`); `before`, `after` and `first-letter` are read as `content`, the same stated limit `string-set` has.
405
- put(values, s.key, s.prop, cssString(collapse(s.target.textContent)));
410
+ // css-content-3 §2.6.3's `content` keyword is the element's string value, `content()`'s own reading (document-state's `contentTextOf` and `collapse`, which leave out what a printer made from generated content); `before`, `after` and `first-letter` are read as `content`, the same stated limit `string-set` has.
411
+ put(values, s.key, s.prop, cssString(collapse(contentTextOf(s.target))));
406
412
  continue;
407
413
  }
408
414
  const state = states.get(s.target);
@@ -557,7 +563,7 @@ export async function settleReads(doc, { run, blocksOf, originals = null, warm =
557
563
  else
558
564
  withdraw(doc);
559
565
  if (hide)
560
- hideReservations(doc, paged.filter((s) => delivered.get(s.key ?? '')?.get(s.prop) === cssString(RESERVATION)), hide === true ? undefined : hide);
566
+ hideReservations(doc, [...paged.filter((s) => delivered.get(s.key ?? '')?.get(s.prop) === cssString(RESERVATION)), ...(pageSites ? totalSites(doc) : [])], hide === true ? undefined : hide);
561
567
  performance.measure('cicero:reads', { start: t, end: (t = performance.now()), detail: { references: sites.length, pageSites: pageSites?.length ?? 0 } });
562
568
  let runs = 0;
563
569
  let moved = [];
@@ -87,7 +87,7 @@ export declare class BeltypeRoot extends HTMLElement {
87
87
  /**
88
88
  * THE INTAKE: the document's boxes become this element's light DOM (*content is one document*, I6), and are parked at once — before the first cut, not at the first presentation (*the pages are cut as far as they are asked*, 2026-10-12). A run that can stop at a page measures the sources while pages stand in the document, so it measures them where a resumed run always has: inside the root, in the flat tree under a slot that renders nothing, where `getComputedStyle` answers (*an unplaced original is parked, not unassigned*). Appended without parking they would answer nothing at all.
89
89
  *
90
- * @param boxes - the blocks, then the fixed boxes, then the running elements.
90
+ * @param boxes - the top-level boxes: the blocks, then the fixed boxes, then the running elements. A fixed box or running element inside a block comes in with its block.
91
91
  */
92
92
  intake(...boxes: Element[]): void;
93
93
  /**
@@ -125,6 +125,23 @@ export declare class BeltypeRoot extends HTMLElement {
125
125
  oldTo: number;
126
126
  priorLength: number;
127
127
  } | null): void;
128
+ /**
129
+ * Draw again the chrome of every standing page that shows the running element at `at`, with nothing cut and nothing built (*the run resumes from an entry*, amended 2026-11-08): the seam's client replaced that element and the run's list now names the new one.
130
+ *
131
+ * @remarks A running element's edit is a chrome event: it is out of the flow, so no cut reads it, and which element a page shows is the page's `closes.running`, a fact about positions that did not move. So each page whose `closes.running` names the place lets go of its running-element clones and has its chrome drawn as a kept page whose chrome moved has it drawn in {@link present}, the bands measured again because a margin box sizes from what it says; its content, its key and every other page are left as they are.
132
+ * @param at - the element's place in `running`.
133
+ * @param running - the run's list, the new element in its place.
134
+ * @param marginBoxesFor - as {@link present} takes it.
135
+ * @returns how many pages were drawn again.
136
+ */
137
+ redrawRunning(at: number, running: {
138
+ el: Element;
139
+ }[], marginBoxesFor: (cond: {
140
+ name: string;
141
+ first: boolean;
142
+ parity: string;
143
+ blank: boolean;
144
+ }) => object): number;
128
145
  /**
129
146
  * @returns how many pages there are and how many have their content.
130
147
  */
package/dist/src/root.js CHANGED
@@ -19,7 +19,7 @@ import { pointShownBy, sourcePointOf } from './source-point.js';
19
19
  import { copyOf, holdsFixedBox, sourceOf } from './word-space.js';
20
20
  import { holdsPresented } from './presenters.js';
21
21
  import { transliterateContent, neutralizeMarginBoxes, sheetsUseHas } from './capture.js';
22
- import { ARTIFACT, addWord, FIXED, FRAGMENT, selectorOf, setValue, wordOf } from './data-beltype.js';
22
+ import { ARTIFACT, decodeFragment, addWord, FIXED, FRAGMENT, selectorOf, setValue, wordOf } from './data-beltype.js';
23
23
  import { deliverValues, ensureContinuationDressing, ensureDressing, isWordsSheet } from './dressing.js';
24
24
  import { carriedPast, divides, elAtIn, extensionPx, flattenResume, flowsOf, indexBlocks, pageIntervals, pageOfPos, widthKeyOf, wordsBeforeChar } from './positions.js';
25
25
  const PRESS_STYLE_ID = '__beltype-press';
@@ -120,7 +120,7 @@ export class BeltypeRoot extends HTMLElement {
120
120
  /**
121
121
  * THE INTAKE: the document's boxes become this element's light DOM (*content is one document*, I6), and are parked at once — before the first cut, not at the first presentation (*the pages are cut as far as they are asked*, 2026-10-12). A run that can stop at a page measures the sources while pages stand in the document, so it measures them where a resumed run always has: inside the root, in the flat tree under a slot that renders nothing, where `getComputedStyle` answers (*an unplaced original is parked, not unassigned*). Appended without parking they would answer nothing at all.
122
122
  *
123
- * @param boxes - the blocks, then the fixed boxes, then the running elements.
123
+ * @param boxes - the top-level boxes: the blocks, then the fixed boxes, then the running elements. A fixed box or running element inside a block comes in with its block.
124
124
  */
125
125
  intake(...boxes) {
126
126
  this.append(...boxes);
@@ -368,6 +368,32 @@ export class BeltypeRoot extends HTMLElement {
368
368
  page.setMarginBox(box, t ? t.parts.map((part) => (typeof part === 'string' ? part : this.#runningPart(pg.closes.running?.[part.element]?.[part.which], running))) : null);
369
369
  page.layoutBands();
370
370
  }
371
+ /**
372
+ * Draw again the chrome of every standing page that shows the running element at `at`, with nothing cut and nothing built (*the run resumes from an entry*, amended 2026-11-08): the seam's client replaced that element and the run's list now names the new one.
373
+ *
374
+ * @remarks A running element's edit is a chrome event: it is out of the flow, so no cut reads it, and which element a page shows is the page's `closes.running`, a fact about positions that did not move. So each page whose `closes.running` names the place lets go of its running-element clones and has its chrome drawn as a kept page whose chrome moved has it drawn in {@link present}, the bands measured again because a margin box sizes from what it says; its content, its key and every other page are left as they are.
375
+ * @param at - the element's place in `running`.
376
+ * @param running - the run's list, the new element in its place.
377
+ * @param marginBoxesFor - as {@link present} takes it.
378
+ * @returns how many pages were drawn again.
379
+ */
380
+ redrawRunning(at, running, marginBoxesFor) {
381
+ let drawn = 0;
382
+ for (const record of this.#records) {
383
+ const pg = record.ctx.pages[record.k];
384
+ if (!Object.values(pg.closes.running ?? {}).some((which) => Object.values(which).includes(at)))
385
+ continue;
386
+ for (const node of record.nodes)
387
+ if (!record.assigned.includes(node))
388
+ node.remove();
389
+ record.nodes = record.nodes.filter((node) => record.assigned.includes(node));
390
+ this.#collect = record.nodes;
391
+ this.#chrome(record.page, pg, this.#marginPlan(pg, record.ctx.account, marginBoxesFor), running);
392
+ this.#collect = null;
393
+ drawn++;
394
+ }
395
+ return drawn;
396
+ }
371
397
  /**
372
398
  * GCPM §1.2's `element()`, delivered — a clone of the running element per page, forwarded into the margin cell through a slot.
373
399
  *
@@ -664,7 +690,7 @@ export class BeltypeRoot extends HTMLElement {
664
690
  this.#listening = true;
665
691
  const view = this.ownerDocument.defaultView;
666
692
  const targetOf = (hash) => {
667
- const id = hash.length > 1 ? decodeURIComponent(hash.slice(1)) : '';
693
+ const id = hash.length > 1 ? decodeFragment(hash) : '';
668
694
  const found = id ? this.ownerDocument.getElementById(id) ?? this.ownerDocument.getElementsByName(id)[0] : null;
669
695
  return found && this.contains(found) ? found : null;
670
696
  };
@@ -159,8 +159,6 @@ function walkOf(report, sourceOf, root = null, styleCache = new Map(), forcing =
159
159
  const monolithic = (box) => {
160
160
  if (box.anonymous)
161
161
  return false; // a run of lines, never one unit
162
- if (box.laidOutLast || box.laidOutFirst)
163
- return true; // a footer written before its rows: its words stand before the rows in the word space, so no place inside it could be addressed (*the footer before the rows*)
164
162
  const live = sourceOf(box.el) ?? box.el;
165
163
  const held = isMonolith(box.el, styleOfBox(box), live);
166
164
  // Named once per box, on the console, so a page that looks wrong for this reason says so.
@@ -210,7 +208,7 @@ function walkOf(report, sourceOf, root = null, styleCache = new Map(), forcing =
210
208
  /**
211
209
  * The walk stops at the edge (*the page asks the index*, 2026-09-25): a box the walk reads, in the procession, whose top lies below the limit. Nothing it or anything after it could offer can fit, so the walk takes the Class A boundary before it and stops.
212
210
  *
213
- * A float, an absolutely positioned box and a row are not where it stops: a float's top is where the float was pushed, not the procession's, and a row's first item is not the row's top. The premise is stated rather than proved — that in-flow tops do not go backward in document order — and a negative margin taller than the box above it, or a relative offset, would break it. A table's footer written before its rows broke it too, and is walked where it is laid out instead (`gauge.js`'s `laidOutLast`, case 110).
211
+ * A float, an absolutely positioned box and a row are not where it stops: a float's top is where the float was pushed, not the procession's, and a row's first item is not the row's top. The premise is stated rather than proved — that in-flow tops do not go backward in document order — and a negative margin taller than the box above it, or a relative offset, would break it. A table's footer written before its rows broke it too, and is walked where it is laid out instead (`gauge.js`, in display order, case 110).
214
212
  */
215
213
  const standsBelow = (box, limit, eps) => !rowOf(box) && !parallelBox(box) && box.rect.top > limit + eps;
216
214
  return { report, sourceOf, boxes, win, root, styleCache, forcing, styleOfBox, monolithic, insideMonolith, parallelBox, insideParallel, rowOf, insideRowItem, copy, walks, standsBelow };
@@ -355,9 +353,7 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
355
353
  // The root of a flow's own walk stands in no procession of its own: its row-mates are outside it, and a boundary before it is the outer walk's (*a parallel flow is a fragmentainer*, 2026-10-08).
356
354
  const siblings = box === root ? [] : report.processionOf(box).filter((b) => !isAbsolutelyPositioned(styleOfBox(b)));
357
355
  const si = siblings.indexOf(box);
358
- // A footer written before its rows offers no boundary before it (*the footer before the rows*, 2026-10-15): the place is after the last row in the layout and before the first in the document, and an address can only say the second.
359
- // Nor does the box after a header written after its rows (the mirror): the place is after the header in the layout and before every row in the document, so the page would hold the header's copy alone.
360
- if (si > 0 && !box.laidOutLast && !siblings[si - 1].laidOutFirst) {
356
+ if (si > 0) {
361
357
  // Two siblings answer here, and they are not always the same one. **The keeps** are the immediately-adjacent box's, because §4.1 names *"a float and an immediately-adjacent in-flow or floated box"* as a Class A pair in as many words — a float's own `break-after` binds the boundary after it, which is §3.1's *should* on floats that this engine honours and Chromium declines. **The position** is the previous in-flow sibling's bottom border edge, because a parallel flow's extent is not a place in the procession the cuts divide (§2.1): the float is laid out beside the content, so the page closes where the previous in-flow box ended and the float's own bottom says nothing about it. Pricing this at the float's bottom is how a 96mm rail put every slab standing beside it off the page — measured, `cases/21-parallel-flows`, where the boundary before the first slab was priced at y 400 in a 264px page area.
362
358
  const pair = siblings[si - 1];
363
359
  const anchor = last(siblings.slice(0, si).filter((b) => !parallelBox(b))) ?? null;
@@ -26,7 +26,7 @@ export declare const UNSPACED_SCRIPT: RegExp;
26
26
  /**
27
27
  * Could the browser have drawn a hyphen at a line boundary between these two characters that the source does not carry? The one question that decides whether a line inside a word may be a cut the fragment can write: a silent break needs nothing at the cut, and a break after U+00AD needs the soft hyphen the source holds written as the hyphenation character (`fragments.js`, *the hyphen is written at the cut*). What is left is a hyphen the browser drew between two letters under `hyphens: auto`, where no character of the source says where.
28
28
  *
29
- * **Refuses where it cannot tell.** Under `hyphens: auto` the browser may hyphenate between any two letters of a script that hyphenates, and nothing in the DOM says whether it did — so that break is refused, the word stays whole, and the page ends a line early rather than print half a word with no hyphen. (The browser does leave its drawing in the range rects, an inked rect for the hyphen on the line it ends, so the question is answerable by measurement; what an address would say for it is its own round.) `hyphens: none` and `manual` draw one only at a soft hyphen.
29
+ * **Answered by measurement, read by the source on both sides.** Under `hyphens: auto` the browser may hyphenate between any two letters of a script that hyphenates, and nothing in the DOM says whether it did; the range rects do, an inked rect for the hyphen on the line the break ends. So the Gauge opens a piece at such a break only where it measured that hyphen (`gauge.js`, `drewHyphen`), and refuses one that drew none, where the page ends a line early rather than print a hyphen the browser did not draw; the fragment writer asks this same predicate of the same characters and adds the hyphenation character after the last kept letter (`fragments.js`). The address carries no mark: the measurement decides which breaks are cuts, and the source decides what the cut writes. `hyphens: none` and `manual` draw one only at a soft hyphen.
30
30
  *
31
31
  * @param before - the character before the break
32
32
  * @param after - the character after it
@@ -209,21 +209,23 @@ export declare function clonesDecoration(el: Element): boolean;
209
209
  */
210
210
  export declare function blockChildrenOf(el: Element, isInline?: (el: Element) => boolean): Element[];
211
211
  /**
212
- * Is this a table's footer group written before a row or row group of its table — HTML 4's order, still legal — which CSS 2.1 §17.2 lays out after them all the same (*"always displayed after all other rows and row groups"*)?
212
+ * A table's children in CSS 2.1 §17.2's display order: its header group before every other row and row group, its footer group after them, and everything else as written. Any other element's children are returned as they are, the same array.
213
213
  *
214
- * @remarks The one place the order and the layout disagree inside a table's procession (*the footer before the rows*, 2026-10-15): the Gauge walks such a footer last, the Stonehand offers no place before it or inside it, and the footer's reservation is made where it stands. Read by computed display, so a clone answers for its source.
214
+ * @remarks THE WORD SPACE follows this order inside a table (*the-word-space-follows-the-table-display-order*, 2026-11-07), and so does every reader of it: the position index numbers a table's words in it, a path's indices count a table's children in it, the Gauge walks a table's procession in it, and a fragment's two cuts put away the children standing before or after them in it. §17.2 lays a `table-header-group` out *"before all other rows and row groups"* and a `table-footer-group` *"after all other rows and row groups"*, wherever each is written, and Chromium paints them so; numbered in the order written, a header after its rows had its words last and its box first, which lost rows with no message (*thead-after-rows-and-tall-footer-lose-rows*). Only the first group of each kind moves, because §17.2 renders any later one as a row group. A move only ever goes toward the rows, so a table written in HTML's order is returned unchanged. Read by computed display, so a clone answers for its source, and a tree that cannot be asked (the DOM-free suites) keeps the order written.
215
215
  *
216
- * @param el - a clone or a source element.
216
+ * @param parent - the element whose children these are.
217
+ * @param items - its children, or what stands for them (the Gauge's boxes), in the order written.
218
+ * @param elOf - the element an item stands for; `null` for a text node, which stays where it is.
217
219
  */
218
- export declare function footerBeforeRows(el: Element): boolean;
220
+ export declare function inDisplayOrder<T>(parent: Element, items: T[], elOf?: (item: T) => Element | null): T[];
219
221
  /**
220
- * Is this a table's header group written after a row or row group of its table, which CSS 2.1 §17.2 lays out before them all the same (*"always displayed before all other rows and row groups"*)?
222
+ * Is this a table's footer group written before a row or row group of its table — HTML 4's order, still legal — which CSS 2.1 §17.2 lays out after them all the same?
221
223
  *
222
- * @remarks The mirror of {@link footerBeforeRows} (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): the Gauge walks such a header first, the Stonehand offers no place inside it or right after it, and its words stand after the rows in the word space, so a page before its words shows it by its repeated copy alone. One that does not repeat, in a divided table, is refused by the driver. Read by computed display, so a clone answers for its source.
224
+ * @remarks Asked by the driver alone, for the one shape that stays refused: such a footer taller than the page area (*the footer before the rows*, 2026-10-15). Its words follow the rows in the word space ({@link inDisplayOrder}), so everywhere else it is a footer like any other. Read by computed display, so a clone answers for its source.
223
225
  *
224
226
  * @param el - a clone or a source element.
225
227
  */
226
- export declare function headerAfterRows(el: Element): boolean;
228
+ export declare function footerBeforeRows(el: Element): boolean;
227
229
  /**
228
230
  * The computed style of the box an author declared — the clone's registered source, or the element itself where it is live and connected. `null` for everything else, which is the honest answer rather than a default: a detached clone with no registration is a `<br>` or a filler span the Compositor inserted, and an element nobody declared has no declaration to read.
229
231
  *