@beltype/cicero 0.1.5 → 0.1.7

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.
@@ -128,6 +128,33 @@ export declare function counterStateAt(ops: ReturnType<typeof documentOpsOf>, po
128
128
  from: number;
129
129
  to: number;
130
130
  }>): Record<string, number[]>;
131
+ /**
132
+ * The page number of page `pageIndex`, which begins at `pos`: the run's own count, or, where an element reset the page counter, that element's value counted on from the page it begins on.
133
+ *
134
+ * @remarks
135
+ * css-page-3 names a reset of the page counter only in the page context, but an author writes `counter-reset: page 1` on a chapter or on each letter of a print run to number its pages from one, and once the engine applies it the pages after must count on from there rather than stay at the value (Codeberg issue 1). The page counter is the page's own, reset on the host as `page N` (`chase.js`'s `setCounters`), so the reset is folded into N here and never delivered as an author counter: delivered, it reset the flow and the margin cells to the same value on every later page in the reset's scope. `target-counter()` to a page reads the same N (`reads.js`'s `pageValues`), so a reference answers the value the counter has at its target (Codeberg issue 2).
136
+ *
137
+ * The last `counter-reset` or `counter-set` of `page` at or before `pos` is the one in force: **at**, not strictly before, because the element that begins on the page is the one that numbers it, where an author counter's reset waits for the next page (css-page-3 §6.1 is about author counters). Scope is not replayed, since the page counter has one instance: a reset stands until the next one, whatever the depth of either. A reset on an element that begins mid-page numbers the next page.
138
+ *
139
+ * **A blank page the engine makes to bring the element to its side begins where the element does and stands before it** (Codeberg issue 13): the reset is not in force on it, so it continues the count it stands in, and it is not counted toward the pages after it, so the element's page bears the value. A blank page inside the scope, after the element began, counts as every page does.
140
+ *
141
+ * @param pages - each page before `pageIndex`, by index: its first word position and whether it is blank.
142
+ * @param blank - whether page `pageIndex` is one the engine made blank.
143
+ * @param away - the remainders the page's opening cut carries in, as for {@link counterStateAt}.
144
+ */
145
+ export declare function pageNumberAt(ops: ReadonlyArray<Op>, pageIndex: number, pos: number, { pages, blank, away }: {
146
+ pages: (i: number) => {
147
+ start: number;
148
+ blank: boolean;
149
+ };
150
+ blank?: boolean;
151
+ away?: ReadonlyArray<{
152
+ from: number;
153
+ to: number;
154
+ }>;
155
+ }): number;
156
+ /** The author's counters alone: the page's own two, `page` and `pages`, are the host's (`pageNumberAt`), and delivered as an author's they would reset the flow and the cells under the host's. */
157
+ export declare function withoutPageCounters(state: Record<string, number[]>): Record<string, number[]>;
131
158
  /**
132
159
  * Every named-string assignment the document makes, in document order.
133
160
  *
@@ -289,6 +289,53 @@ const runningOp = (r, at) => ({ reset: [], increment: [], set: [], strings: null
289
289
  export function counterStateAt(ops, pos, away = []) {
290
290
  return replay(ops, { until: pos, away });
291
291
  }
292
+ /**
293
+ * The page number of page `pageIndex`, which begins at `pos`: the run's own count, or, where an element reset the page counter, that element's value counted on from the page it begins on.
294
+ *
295
+ * @remarks
296
+ * css-page-3 names a reset of the page counter only in the page context, but an author writes `counter-reset: page 1` on a chapter or on each letter of a print run to number its pages from one, and once the engine applies it the pages after must count on from there rather than stay at the value (Codeberg issue 1). The page counter is the page's own, reset on the host as `page N` (`chase.js`'s `setCounters`), so the reset is folded into N here and never delivered as an author counter: delivered, it reset the flow and the margin cells to the same value on every later page in the reset's scope. `target-counter()` to a page reads the same N (`reads.js`'s `pageValues`), so a reference answers the value the counter has at its target (Codeberg issue 2).
297
+ *
298
+ * The last `counter-reset` or `counter-set` of `page` at or before `pos` is the one in force: **at**, not strictly before, because the element that begins on the page is the one that numbers it, where an author counter's reset waits for the next page (css-page-3 §6.1 is about author counters). Scope is not replayed, since the page counter has one instance: a reset stands until the next one, whatever the depth of either. A reset on an element that begins mid-page numbers the next page.
299
+ *
300
+ * **A blank page the engine makes to bring the element to its side begins where the element does and stands before it** (Codeberg issue 13): the reset is not in force on it, so it continues the count it stands in, and it is not counted toward the pages after it, so the element's page bears the value. A blank page inside the scope, after the element began, counts as every page does.
301
+ *
302
+ * @param pages - each page before `pageIndex`, by index: its first word position and whether it is blank.
303
+ * @param blank - whether page `pageIndex` is one the engine made blank.
304
+ * @param away - the remainders the page's opening cut carries in, as for {@link counterStateAt}.
305
+ */
306
+ export function pageNumberAt(ops, pageIndex, pos, { pages, blank = false, away = [] }) {
307
+ const inForce = (at, start, isBlank) => at < start || (at === start && !isBlank);
308
+ let reset = null;
309
+ for (const op of ops) {
310
+ if (op.pos > pos)
311
+ break;
312
+ if (!inForce(op.pos, pos, blank))
313
+ continue;
314
+ if (away.length && inRemainders(away, op.pos))
315
+ continue;
316
+ for (const [name, value] of [...op.reset, ...op.set])
317
+ if (name === 'page')
318
+ reset = { pos: op.pos, value };
319
+ }
320
+ if (!reset)
321
+ return pageIndex + 1;
322
+ let since = 0;
323
+ for (let i = 0; i < pageIndex; i++) {
324
+ const page = pages(i);
325
+ if (inForce(reset.pos, page.start, page.blank))
326
+ since++;
327
+ }
328
+ return reset.value + since;
329
+ }
330
+ /** The author's counters alone: the page's own two, `page` and `pages`, are the host's (`pageNumberAt`), and delivered as an author's they would reset the flow and the cells under the host's. */
331
+ export function withoutPageCounters(state) {
332
+ if (!('page' in state) && !('pages' in state))
333
+ return state;
334
+ const out = { ...state };
335
+ delete out.page;
336
+ delete out.pages;
337
+ return out;
338
+ }
292
339
  /** One replay, two readers. `counterStateAt` stops at a position and returns the state there; `stringAssignmentsOf` runs the log out and is handed the state at each element that assigns a named string. Written once because the scope arithmetic is the delicate part and two copies of it would agree only until one of them was fixed. */
293
340
  function replay(ops, { until = Infinity, onOp = null, away = [] } = {}) {
294
341
  /**
@@ -47,6 +47,8 @@ export type ForcingEvent = {
47
47
  sealedBy: number;
48
48
  /** It stands inside a box that lays its children side by side. */
49
49
  rowish: boolean;
50
+ /** For a run of lines after a block child, the event is the run's first word in `el`, which holds it, and this is the block child it follows; the feed is cut after it (*the anonymous box is in the procession*). */
51
+ runAfter?: Element;
50
52
  };
51
53
  /** One block of a page's feed: its clone, the source it was cloned from, and the address it resumes at. */
52
54
  export type FeedItem = {
@@ -111,7 +113,7 @@ export declare function pageNameDownFrom(el: Element, { styleOf, runBefore, bloc
111
113
  blockChildren?: typeof blockChildrenOf;
112
114
  }): string;
113
115
  /**
114
- * Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included (`capture.js`'s `styleSheetsOf`; the capture reads adopted sheets). Returns page and content box sizes in px.
116
+ * Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included, and the rules inside its layers and conditions (`capture.ts`'s `pageRulesOf`). Returns page and content box sizes in px.
115
117
  *
116
118
  * **Margins only, and that is now a stated limit rather than the whole story** *(a page box has a border and a padding, 2026-09-07)*. The capture models a page's `border` and `padding` and subtracts both from the page area; this reader does not, so a document declaring either gets a content box too large here. Its one caller is `packages/essay`'s benchmark, over a corpus that writes `size` and margins into its page rules and nothing else — so the difference is unreachable there. It is not widened rather than left quietly wrong: this is the legacy single-geometry reader, and a second implementation of the page area is exactly what `readGeometryTable` exists to be the only one of.
117
119
  */
@@ -198,14 +200,18 @@ export declare const combineParity: (earlier: 'left' | 'right' | null, later: 'l
198
200
  *
199
201
  * @param blocks - the top-level blocks, in document order.
200
202
  */
201
- export declare function forcingIndexOf(blocks: Element[], { styleOf, pageNameOf, cutOf, posOf, posOfCut, isMonolith: monolith, blockChildren }: {
203
+ export declare function forcingIndexOf(blocks: Element[], { styleOf, pageNameOf, cutOf, posOf, posOfCut, isMonolith: monolith, blockChildren, extentOf }: {
202
204
  styleOf: (el: Element) => CSSStyleDeclaration;
203
205
  pageNameOf: (el: Element) => string;
204
- cutOf: (el: Element, b: number) => Address;
206
+ cutOf: (el: Element, b: number, word?: number | null) => Address;
205
207
  posOf: (el: Element, b: number) => number;
206
208
  posOfCut?: ((cut: Address) => number) | null;
207
209
  isMonolith?: typeof isMonolith;
208
210
  blockChildren?: typeof blockChildrenOf;
211
+ extentOf?: ((el: Element) => {
212
+ before: number;
213
+ count: number;
214
+ }) | null;
209
215
  }): {
210
216
  size: number;
211
217
  /** The first forcing event strictly after `cursorCut` for a page named `name`, or null. */
@@ -226,6 +232,7 @@ export declare function forcingIndexOf(blocks: Element[], { styleOf, pageNameOf,
226
232
  };
227
233
  parity: "left" | "right" | null;
228
234
  rowish: boolean;
235
+ runAfter: Element | undefined;
229
236
  } | null;
230
237
  /**
231
238
  * The Cast-off's place (*the Cast-off bounds the feed*, 2026-09-26): the first block boundary strictly after `cursorCut` whose word position is at least `pos`, where a feed may be cut without moving any box before it — or null, where no such place is left and the feed is the whole remainder.
@@ -15,18 +15,18 @@
15
15
  */
16
16
  import { Ledger, createEntry } from './ledger.js';
17
17
  import { assertRenderConditions, awaitCloneImages, awaitFonts, awaitImages, createMeasureContext } from './measure.js';
18
- import { takeReport } from './gauge.js';
18
+ import { holdsAnonymousBlocks, takeReport } from './gauge.js';
19
19
  import { adjudicate, adjudicateFlow, firstBelowTheEdge, isAbsolutelyPositioned, isOutOfFlow, isMonolith, neverCutInside, reachesBelowTheEdge, processionBottoms, fitsPage } from './stonehand.js';
20
20
  import { composeDocument, applyRecordsUnder } from './compose-dom.js';
21
21
  import { CompositionCache } from './composition-key.js';
22
- import { readGeometryTable, effectivePageName, styleSheetsOf, registerGcpmProperties, sheetsUseHas, sheetReach } from './capture.js';
22
+ import { readGeometryTable, effectivePageName, pageRulesOf, styleSheetsOf, registerGcpmProperties, sheetsUseHas, sheetReach } from './capture.js';
23
23
  import { snapshotOfStyle } from './forcing-memo.js';
24
24
  import { cloneFlowPiece, declineRepeats, mayRepeat, mutateToContinuation, truncateFrom } from './fragments.js';
25
25
  import { cutAfter, divides, elAtIn, flattenResume, indexBlocks, inFlowIndex, inRemainders, normalizePath, pathOf, placeAt, remaindersOf } from './positions.js';
26
- import { documentOpsOf, counterStateAt, stringAssignmentsOf, runningAssignmentsOf, namedStringsForPage } from './document-state.js';
26
+ import { documentOpsOf, counterStateAt, pageNumberAt, withoutPageCounters, stringAssignmentsOf, runningAssignmentsOf, namedStringsForPage } from './document-state.js';
27
27
  import { RUNNING_PROP } from './gcpm-encoding.js';
28
28
  import { ARTIFACT, hasWord } from './data-beltype.js';
29
- import { blockChildrenOf, footerBeforeRows, isFixedBox, SIDE_BY_SIDE, sourceOf } from './word-space.js';
29
+ import { blockChildrenOf, footerBeforeRows, headerAfterRows, isFixedBox, SIDE_BY_SIDE, sourceOf } from './word-space.js';
30
30
  import { changedPairs, settleReads } from './reads.js';
31
31
  import { converge, holderPage, namesNoPlace, priorKeys, restartPage } from './resume.js';
32
32
  import { answerHas, boxesWhoseWordsMoved } from './has-words.js';
@@ -37,6 +37,21 @@ function toPx(len) {
37
37
  throw new Error(`unsupported page length: ${len}`);
38
38
  return parseFloat(m[1]) * PX_PER[m[2]];
39
39
  }
40
+ /**
41
+ * The message a table shape refuses with (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): the table, by its id or class or its first words, and the shape. Both shapes lost rows with no message, and a refusal by name is the detector until the address can say where §17.2 lays the group out.
42
+ *
43
+ * @remarks The two shapes, both groups whose words and box disagree in order. A header group written after the rows is laid out above them (CSS 2.1 §17.2) while its words stand after them in the word space: one that repeats is walked first and shown by its copy on every page before its words, and one too tall to repeat, in a divided table, is refused, because which page shows it is the open ruling on §17.2's display order in the word space. A footer group written before the rows is walked where it is laid out, last and whole, and one taller than the page area could be placed nowhere, so the table ended on its first page.
44
+ *
45
+ * @param group - the source `thead` or `tfoot`.
46
+ * @param shape - the shape, in words.
47
+ * @param remedy - what the author can write instead.
48
+ */
49
+ function refusedTable(group, shape, remedy = 'write the groups in HTML\'s order: thead, tbody, tfoot') {
50
+ const table = group.closest('table');
51
+ const words = (table?.textContent ?? '').trim().replace(/\s+/g, ' ').slice(0, 40);
52
+ const name = table?.id ? `<table id="${table.id}">` : table?.className ? `<table class="${table.className}">` : `the table beginning "${words}"`;
53
+ return `cicero: ${name} has ${shape}, which this engine cannot divide across pages yet; it refuses rather than lose rows. To print it, ${remedy}.`;
54
+ }
40
55
  /** The nearest copy the engine repeats (`artifact`) standing ahead of this element among its siblings — the table's header, in front of its rows — or null. */
41
56
  const artifactBefore = (el) => {
42
57
  for (let s = el?.previousElementSibling; s; s = s.previousElementSibling)
@@ -44,6 +59,13 @@ const artifactBefore = (el) => {
44
59
  return s;
45
60
  return null;
46
61
  };
62
+ /** The header group written after this element among its siblings, which §17.2 lays out ahead of it all the same (`headerAfterRows`), or null. It is a copy on the pages before its words and the original in a feed, which pours it whole, and it stands ahead of the rows either way. */
63
+ const headerCopyAfter = (el) => {
64
+ for (let s = el?.nextElementSibling; s; s = s.nextElementSibling)
65
+ if (headerAfterRows(s))
66
+ return s;
67
+ return null;
68
+ };
47
69
  /** Does a copy the engine repeats (`artifact`) stand ahead of this element among its siblings — the table's header, in front of its rows? */
48
70
  export const standsAfterArtifact = (el) => artifactBefore(el) !== null;
49
71
  /**
@@ -60,9 +82,10 @@ export function gapAbove(box, report, win) {
60
82
  for (let child = box, a = box.parent; a; child = a, a = a.parent) {
61
83
  const cs = win.getComputedStyle(a.el);
62
84
  // A copy the engine repeats ahead of the child (the table's header) is furniture with a footprint, not a spent lead (*the header repeats*): the level owes only what stands above the copy, which under separated borders is the table's `border-spacing` above its first row, and Chromium draws none above a repeated header (*a repeated header joins the rows as Chromium prints them*, 2026-10-22). Under collapsed borders the copy's top is the table's own and nothing is owed.
63
- if (standsAfterArtifact(child.el)) {
64
- // The header's copy, never a footer's: a footer written before its rows stands among the siblings too, and is laid out at the foot (*the footer before the rows*).
65
- let copy = cs.borderCollapse === 'separate' ? artifactBefore(child.el) : null;
85
+ const late = headerCopyAfter(child.el);
86
+ if (late || standsAfterArtifact(child.el)) {
87
+ // The header's copy, never a footer's: a footer written before its rows stands among the siblings too, and is laid out at the foot (*the footer before the rows*). A header written after its rows is laid out ahead of them, and its copy is the one.
88
+ let copy = cs.borderCollapse === 'separate' ? late ?? artifactBefore(child.el) : null;
66
89
  while (copy && !/^THEAD$/i.test(copy.tagName))
67
90
  copy = artifactBefore(copy);
68
91
  const box = copy ? report.boxOf(copy) : null;
@@ -93,28 +116,25 @@ export function pageNameDownFrom(el, { styleOf, runBefore, blockChildren = block
93
116
  return effectivePageName(el);
94
117
  }
95
118
  /**
96
- * Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included (`capture.js`'s `styleSheetsOf`; the capture reads adopted sheets). Returns page and content box sizes in px.
119
+ * Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included, and the rules inside its layers and conditions (`capture.ts`'s `pageRulesOf`). Returns page and content box sizes in px.
97
120
  *
98
121
  * **Margins only, and that is now a stated limit rather than the whole story** *(a page box has a border and a padding, 2026-09-07)*. The capture models a page's `border` and `padding` and subtracts both from the page area; this reader does not, so a document declaring either gets a content box too large here. Its one caller is `packages/essay`'s benchmark, over a corpus that writes `size` and margins into its page rules and nothing else — so the difference is unreachable there. It is not widened rather than left quietly wrong: this is the legacy single-geometry reader, and a second implementation of the page area is exactly what `readGeometryTable` exists to be the only one of.
99
122
  */
100
123
  export function readPageGeometry(doc) {
101
- for (const sheet of styleSheetsOf(doc)) {
102
- for (const rule of sheet.cssRules) {
103
- if (!(rule instanceof doc.defaultView.CSSPageRule))
104
- continue;
105
- const text = rule.cssText;
106
- const size = /size:\s*([^;}]+)/.exec(text)?.[1].trim().split(/\s+/) ?? [];
107
- const marginRaw = /(?:^|[\s{;])margin:\s*([^;}]+)/.exec(text)?.[1].trim().split(/\s+/) ??
108
- ['top', 'right', 'bottom', 'left'].map((side) => new RegExp(`margin-${side}:\\s*([^;}]+)`).exec(text)?.[1].trim() ?? '0px');
109
- if (size.length === 0)
110
- throw new Error('@page without an explicit size — the corpus always pins geometry (I5)');
111
- const [w, h] = size.length === 1 ? [size[0], size[0]] : size;
112
- const [mt, mr = mt, mb = mt, ml = mr] = marginRaw;
113
- const pageW = toPx(w);
114
- const pageH = toPx(h);
115
- const margins = { top: toPx(mt), right: toPx(mr), bottom: toPx(mb), left: toPx(ml) };
116
- return { pageW, pageH, margins, contentW: pageW - margins.left - margins.right, contentH: pageH - margins.top - margins.bottom };
117
- }
124
+ const { layered, unlayered } = pageRulesOf(doc);
125
+ for (const rule of [...unlayered, ...layered]) {
126
+ const text = rule.cssText;
127
+ const size = /size:\s*([^;}]+)/.exec(text)?.[1].trim().split(/\s+/) ?? [];
128
+ const marginRaw = /(?:^|[\s{;])margin:\s*([^;}]+)/.exec(text)?.[1].trim().split(/\s+/) ??
129
+ ['top', 'right', 'bottom', 'left'].map((side) => new RegExp(`margin-${side}:\\s*([^;}]+)`).exec(text)?.[1].trim() ?? '0px');
130
+ if (size.length === 0)
131
+ throw new Error('@page without an explicit size — the corpus always pins geometry (I5)');
132
+ const [w, h] = size.length === 1 ? [size[0], size[0]] : size;
133
+ const [mt, mr = mt, mb = mt, ml = mr] = marginRaw;
134
+ const pageW = toPx(w);
135
+ const pageH = toPx(h);
136
+ const margins = { top: toPx(mt), right: toPx(mr), bottom: toPx(mb), left: toPx(ml) };
137
+ return { pageW, pageH, margins, contentW: pageW - margins.left - margins.right, contentH: pageH - margins.top - margins.bottom };
118
138
  }
119
139
  throw new Error('no @page rule found');
120
140
  }
@@ -218,7 +238,7 @@ export const combineParity = (earlier, later) => later ?? earlier;
218
238
  *
219
239
  * @param blocks - the top-level blocks, in document order.
220
240
  */
221
- export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posOfCut = null, isMonolith: monolith = isMonolith, blockChildren = blockChildrenOf }) {
241
+ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posOfCut = null, isMonolith: monolith = isMonolith, blockChildren = blockChildrenOf, extentOf = null }) {
222
242
  const events = [];
223
243
  const blockStart = [];
224
244
  /**
@@ -233,9 +253,25 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
233
253
  const k = events.length;
234
254
  events.push(e);
235
255
  const inner = sealedBy >= 0 ? sealedBy : neverCutInside(el, st) ? k : -1;
236
- if (!parallel && !monolith(el, st))
237
- for (const child of blockChildren(el))
238
- visit(child, b, inner, rowish || SIDE_BY_SIDE.has(st.display ?? ''));
256
+ if (!parallel && !monolith(el, st)) {
257
+ const kids = blockChildren(el);
258
+ const runs = !!extentOf && holdsAnonymousBlocks(st.display ?? '');
259
+ const row = rowish || SIDE_BY_SIDE.has(st.display ?? '');
260
+ for (let i = 0; i < kids.length; i++) {
261
+ visit(kids[i], b, inner, row);
262
+ if (!runs)
263
+ continue;
264
+ // A run of lines after this child is an anonymous block box, and the box a forced `break-after` closing here is pending into: an event at its first word, in the word space of the box that holds it (*the anonymous box is in the procession*). Words standing between the child's end and the next child's start, or this box's end, are the run; none is no box.
265
+ const ext = extentOf;
266
+ const own = ext(el);
267
+ const done = ext(kids[i]);
268
+ const word = done.before + done.count;
269
+ const next = kids[i + 1];
270
+ if ((next ? ext(next).before : own.before + own.count) <= word)
271
+ continue;
272
+ events.push({ b, el, cut: cutOf(el, b, word - own.before), name: e.name, before: false, beforeParity: null, after: false, afterParity: null, end: events.length + 1, pos: e.pos + word - own.before, inFlow: true, sealedBy: inner, rowish: row, runAfter: kids[i] });
273
+ }
274
+ }
239
275
  e.after = !parallel && isForced(st.breakAfter); // a float's own break-after was never read, and still is not
240
276
  e.afterParity = parityOf(st.breakAfter);
241
277
  e.end = events.length;
@@ -262,7 +298,7 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
262
298
  const e = events[k];
263
299
  const before = !namesOnly && e.before;
264
300
  // §4.3's combination at the one break point where two forced values meet: the `break-after` pending into the box, and the box's own `break-before`, the later element in the flow, whose parity wins where it has one. The address is a copy, so no page can reach into the index through it.
265
- return { b: e.b, namedSwitch: e.name !== name, el: e.el, cut: { ...e.cut, path: [...e.cut.path] }, parity: combineParity(pending?.parity ?? null, before ? e.beforeParity : null), rowish: e.rowish };
301
+ return { b: e.b, namedSwitch: e.name !== name, el: e.el, cut: { ...e.cut, path: [...e.cut.path] }, parity: combineParity(pending?.parity ?? null, before ? e.beforeParity : null), rowish: e.rowish, runAfter: e.runAfter };
266
302
  };
267
303
  /** The first event strictly after the cursor — a binary search, because a bare address never precedes the one before it in walk order — or `n` where there is none. */
268
304
  const firstAfter = (cursorCut) => {
@@ -318,6 +354,8 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
318
354
  k = events[e.sealedBy].end;
319
355
  else if (!e.inFlow)
320
356
  k = e.end;
357
+ else if (e.runAfter)
358
+ k++; // a run's place is the grab's alone: the Cast-off cuts at a box, and moving forward only grows the feed
321
359
  else
322
360
  return { b: e.b, el: e.el, cut: { ...e.cut, path: [...e.cut.path] } };
323
361
  }
@@ -622,16 +660,18 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
622
660
  /** The grab's events, indexed here, once (*the page asks the index*, 2026-09-25) — beside the document's own counters and for the same reason: the source is never written (*the source document is never written*, C5), so what the grab reads about a box is the same on every page, and the walk that asked it of the tree on every page cost the BGD manual 66 ms a page whatever the cursor. `forcingIndexOf` above carries the question and its rules; the page asks `forcing.find`. */
623
661
  // The memo, if the seam offered one and the sheets leave it standing (*the forced-break index is kept across runs*, 2026-10-09; settled once for the call by `settledMemo`): the reads below are answered from it where an element was read before, and read from the browser and kept where not.
624
662
  memo?.restart();
663
+ const extentOf = (el) => ({ before: index.wordsBefore(el), count: index.wordCountIn(el) });
625
664
  const forcing = memo
626
665
  ? forcingIndexOf(blocks, {
627
666
  styleOf: (el) => memo.facts(el, () => snapshotOfStyle(win.getComputedStyle(el))),
628
667
  pageNameOf: (el) => memo.pageName(el, () => effectivePageName(el)),
629
668
  blockChildren: (el) => memo.blockChildren(el, () => blockChildrenOf(el)),
630
- cutOf: (el, b) => cutTo(el, b, { rawPath: pathOf(el, blocks[b], b, isFixedBox, (e) => memo.indexIn(e, () => inFlowIndex(e))) }),
669
+ cutOf: (el, b, word = null) => cutTo(el, b, { word, rawPath: pathOf(el, blocks[b], b, isFixedBox, (e) => memo.indexIn(e, () => inFlowIndex(e))) }),
631
670
  posOf: (el, b) => index.posOf(b, index.wordsBefore(el)),
632
- posOfCut
671
+ posOfCut,
672
+ extentOf
633
673
  })
634
- : forcingIndexOf(blocks, { styleOf: (el) => win.getComputedStyle(el), pageNameOf: effectivePageName, cutOf: (el, b) => cutTo(el, b), posOf: (el, b) => index.posOf(b, index.wordsBefore(el)), posOfCut });
674
+ : forcingIndexOf(blocks, { styleOf: (el) => win.getComputedStyle(el), pageNameOf: effectivePageName, cutOf: (el, b, word = null) => cutTo(el, b, { word }), posOf: (el, b) => index.posOf(b, index.wordsBefore(el)), posOfCut, extentOf });
635
675
  performance.measure('cicero:forcing-index', { start: t, end: (t = performance.now()), detail: { events: forcing.size, ...(memo ? { memoHits: memo.hits, memoMisses: memo.misses } : {}) } });
636
676
  /**
637
677
  * The effective page name at an address: the deepest in-flow block that begins there, then css-page-3 §8.1's ancestor walk from it. A page may begin inside a block whose interior named a different page, so the cursor's block is not the thing to ask.
@@ -720,10 +760,18 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
720
760
  * @param who - the caller, for the error.
721
761
  */
722
762
  const cutCloneAt = (clone, cloneOfSource, place, who) => {
723
- const target = cloneOfSource.get(place.el);
763
+ const target = cloneOfSource.get(place.runAfter ?? place.el);
724
764
  if (!target || !clone.contains(target))
725
765
  throw new Error(`${who} could not address its own place in block ${place.b} — a defect to file, not a bail to hide`);
726
- truncateFrom(clone, target, { hollow });
766
+ if (!place.runAfter)
767
+ return void truncateFrom(clone, target, { hollow });
768
+ // A run has no element: the feed is cut at the first node after the block child it follows, which stands in the run, so the child stays whole and the run leaves with everything after it.
769
+ let at = target;
770
+ while (!at.nextSibling && at.parentNode && at !== clone)
771
+ at = at.parentNode;
772
+ if (!at.nextSibling)
773
+ throw new Error(`${who} found no run after its block child in block ${place.b}: a defect to file, not a bail to hide`);
774
+ truncateFrom(clone, at.nextSibling, { hollow });
727
775
  };
728
776
  const ledger = new Ledger();
729
777
  // The cursor is the document's own first address — the path to block zero, which divides nothing — plus its projection and whatever flows are still running beside it.
@@ -735,11 +783,13 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
735
783
  /** The conditions of page `pageIndex`, which begins at `cut` (projected to `pos`) — {@link conditionsFor} with the cursor made a parameter, so a resumed run can write the conditions of a page it keeps from the address that page began at (*the run resumes from an entry*, build two, 2026-10-08). */
736
784
  const conditionsAt = (pageIndex, cut, pos, { blank = false, carried = null } = {}) => {
737
785
  const name = nameAt(cut);
738
- const pageNumber = pageIndex + 1;
786
+ const away = remaindersOf(carried, index.endOf);
787
+ // The run's count, or an author's reset of the page counter counted on from the page it begins on (`document-state.js`'s `pageNumberAt`). Entry `i` closes page `i`, so page `i` begins where entry `i - 1` ended.
788
+ const pageNumber = pageNumberAt(documentOps, pageIndex, pos, { pages: (i) => ({ start: i === 0 ? 0 : account[i - 1].pos ?? 0, blank: account[i].blank === true }), blank, away });
739
789
  const parity = parityAt(pageIndex);
740
790
  const geo = table.pageFor({ name, first: pageIndex === 0, parity, blank });
741
- // The document's counters as they stand at this page's first word — strictly before it, which is §6.1's *spans the page break* in the flat position space (`document-state.js`) — less what the flows carried onto this page have still to show, which stands before that word and is on this page or after it (beltype#43).
742
- const counters = counterStateAt(documentOps, pos, remaindersOf(carried, index.endOf));
791
+ // The document's counters as they stand at this page's first word — strictly before it, which is §6.1's *spans the page break* in the flat position space (`document-state.js`) — less what the flows carried onto this page have still to show, which stands before that word and is on this page or after it (beltype#43). The page's own two are not among them: they are the host's, and `pageNumber` carries the reset.
792
+ const counters = withoutPageCounters(counterStateAt(documentOps, pos, away));
743
793
  return Object.freeze({ ...geo, name, pageNumber, parity, blank, counters, dpr: win.devicePixelRatio });
744
794
  };
745
795
  /**
@@ -778,7 +828,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
778
828
  const source = blocks[b];
779
829
  const cut = b === first ? cursor.cut : null;
780
830
  const ends = !!place && b === place.b;
781
- if (ends && place.cut.path.length === 1)
831
+ if (ends && place.cut.path.length === 1 && place.cut.word == null)
782
832
  break; // the place is this block's own start: the whole block leaves, exactly as the item-granular grab always cut
783
833
  const { clone } = cloneFlowPiece(source, { start: divides(cut) ? cut : null, end: ends ? place.cut : null, items: itemsIn(b), hollow, into: cloneOfSource }); // no composition: the tail is cut first, then the hints
784
834
  if (ends)
@@ -1013,6 +1063,8 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1013
1063
  if (!mayRepeat(src))
1014
1064
  continue;
1015
1065
  decided.add(src);
1066
+ if (footerBeforeRows(src) && g.getBoundingClientRect().height > conditions.contentH)
1067
+ throw new Error(refusedTable(src, 'a footer group taller than the page area, written before its rows', 'write the footer after the rows, where it is divided like them, or keep it within the page area'));
1016
1068
  const repeat = g.getBoundingClientRect().height <= conditions.contentH / 4;
1017
1069
  const b = blocks.findIndex((block) => block === src || block.contains(src));
1018
1070
  madeHere.push({ path: pathOf(src, blocks[b], b), repeat });
@@ -1416,6 +1468,13 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1416
1468
  if (!fits && hasGroups) {
1417
1469
  const ct = container.getBoundingClientRect().top;
1418
1470
  const edge = pageStart + conditions.contentH;
1471
+ // A header written after its rows that does not repeat, in a table the page's edge divides, is refused (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): its words are on the table's last fragment and its box above the first, and which page shows it is the open ruling on §17.2's display order in the word space. Undivided, the one fragment holds both.
1472
+ for (const h of container.querySelectorAll('thead')) {
1473
+ const src = sourceOf(h);
1474
+ const r = h.closest('table')?.getBoundingClientRect();
1475
+ if (src && r && declined.has(src) && headerAfterRows(src) && r.top - ct < edge && r.bottom - ct > edge)
1476
+ throw new Error(refusedTable(src, 'a header group written after its rows, too tall to repeat'));
1477
+ }
1419
1478
  for (const f of container.querySelectorAll('tfoot')) {
1420
1479
  const src = sourceOf(f);
1421
1480
  // A footer written before its rows that does not repeat is still reserved where it is shown: its words stand before the rows, so it is on the table's first fragment and nowhere after (*the footer before the rows*, 2026-10-15; Chromium shows it on the last, which the address cannot say).
@@ -20,5 +20,5 @@ export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureCont
20
20
  export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining, pageIntervals, pathOf, placeAt } from './positions.ts';
21
21
  export { present, presentation } from './presenters.ts';
22
22
  export { cssString, findPageSites, settleReads } from './reads.ts';
23
- export { adjudicate, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.ts';
23
+ export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.ts';
24
24
  export { BREAKING_SPACE, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.ts';
@@ -19,5 +19,5 @@ export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureCont
19
19
  export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining, pageIntervals, pathOf, placeAt } from './positions.js';
20
20
  export { present, presentation } from './presenters.js';
21
21
  export { cssString, findPageSites, settleReads } from './reads.js';
22
- export { adjudicate, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.js';
22
+ export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.js';
23
23
  export { BREAKING_SPACE, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.js';
@@ -96,10 +96,10 @@ export declare function mayRepeat(src: Element): boolean;
96
96
  * **`keep` is the first fragment's door into the same surgery** (*the first fragment is constructed*, 2026-09-15). A shell continues a box whose own content closed on the page before, so page one wants everything after that box gone and the box itself standing — the one difference from the grab's reading, where the boundary box is what resumes. With `keep`, the boundary element stays and is dressed as a fragment that ends there, which it is.
97
97
  *
98
98
  * @param clone - the top-level block's clone, already hinted and already consumed to the cursor.
99
- * @param el - the boundary element, addressed in the clone's own coordinates.
99
+ * @param el - the boundary node, addressed in the clone's own coordinates: an element, or the first node of a run of lines, which has no element (*the anonymous box is in the procession*).
100
100
  * @param opts - `keep`: the boundary element stays in the fragment (and is dressed), rather than leaving with its successors. `hollow`: what leaves is kept hollow (`putAway`).
101
101
  */
102
- export declare function truncateFrom(clone: Element, el: Element, { keep, hollow }?: {
102
+ export declare function truncateFrom(clone: Element, el: ChildNode, { keep, hollow }?: {
103
103
  keep?: boolean;
104
104
  hollow?: boolean;
105
105
  }): Element;
@@ -317,7 +317,7 @@ function liftRepeatedHeader(group) {
317
317
  * **`keep` is the first fragment's door into the same surgery** (*the first fragment is constructed*, 2026-09-15). A shell continues a box whose own content closed on the page before, so page one wants everything after that box gone and the box itself standing — the one difference from the grab's reading, where the boundary box is what resumes. With `keep`, the boundary element stays and is dressed as a fragment that ends there, which it is.
318
318
  *
319
319
  * @param clone - the top-level block's clone, already hinted and already consumed to the cursor.
320
- * @param el - the boundary element, addressed in the clone's own coordinates.
320
+ * @param el - the boundary node, addressed in the clone's own coordinates: an element, or the first node of a run of lines, which has no element (*the anonymous box is in the procession*).
321
321
  * @param opts - `keep`: the boundary element stays in the fragment (and is dressed), rather than leaving with its successors. `hollow`: what leaves is kept hollow (`putAway`).
322
322
  */
323
323
  export function truncateFrom(clone, el, { keep = false, hollow = false } = {}) {
@@ -377,22 +377,59 @@ function charRanges(word, from, to) {
377
377
  }
378
378
  return out;
379
379
  }
380
+ /** Whether the whitespace of a text node stands in its layout (css-text-4 §4.1): where it collapses it is nothing at a line's edge, and where it is preserved each newline draws a line. */
381
+ const preservesSpace = (t) => !!t.parentElement && /^(pre|pre-wrap|pre-line|break-spaces)$/.test(sourceStyle(t.parentElement)?.whiteSpace ?? '');
382
+ /** Take a stretch of a cut's whitespace out of the fragment: leave it where it collapses, so layout reads the data exactly as before, and remove it where it is preserved, or a continuation opens on the empty lines of every line cut away. Either way the record says it is not shown. */
383
+ function dropSpace(t, from, to) {
384
+ if (to <= from)
385
+ return;
386
+ if (preservesSpace(t))
387
+ cutRanges(t, [{ start: from, end: to }]);
388
+ else
389
+ leaveOut(t, from, to);
390
+ }
380
391
  /**
381
- * Say that everything before the resume point is held and not shown (*the engine names the source point*, 2026-10-13). Consuming blanks the words and leaves the whitespace between them, so a continuation's data still begins with the spaces of every word cut away; those are the source's characters and no part of the interval, and so are the whitespace before the first kept word and every text node that stands wholly before it. Records only: layout reads the data exactly as before.
392
+ * The indentation of the line a resume word opens: the whitespace before it back to a newline, in characters. Read before the words before it go, because only they tell the space that opens a line from the space that followed a word. A cut at a soft wrap has no newline in that gap and no indentation, the space hanging at the end of the line before; a line after a `<br>` looks the same and loses its indentation at the top of a page.
393
+ */
394
+ function indentationBefore(clone, node, start) {
395
+ let text = '';
396
+ for (const t of textsIn(clone)) {
397
+ if (t === node) {
398
+ text += t.data.slice(0, start);
399
+ break;
400
+ }
401
+ text += t.data;
402
+ }
403
+ let i = text.length;
404
+ while (i > 0 && BREAKING_SPACE.test(text[i - 1]))
405
+ i--;
406
+ const newline = text.lastIndexOf('\n');
407
+ return newline < i ? 0 : text.length - newline - 1;
408
+ }
409
+ /**
410
+ * Say that everything before the resume point is held and not shown (*the engine names the source point*, 2026-10-13). Consuming blanks the words and leaves the whitespace between them, so a continuation's data still begins with the spaces of every word cut away; those are the source's characters and no part of the interval, and so are the whitespace before the first kept word and every text node that stands wholly before it. Where the whitespace collapses these are records only, and where it is preserved they leave the data (`dropSpace`), all but the indentation of the line the page opens on.
382
411
  *
383
412
  * @param resume - the node holding the first kept character, or null when nothing is kept.
413
+ * @param indent - the characters immediately before it to keep, from {@link indentationBefore}.
384
414
  */
385
- function leaveOutBefore(clone, resume) {
415
+ function leaveOutBefore(clone, resume, indent) {
416
+ const lead = [];
386
417
  for (const t of textsIn(clone)) {
387
418
  if (t === resume)
388
419
  break;
389
- leaveOut(t, 0, t.data.length);
420
+ lead.push({ node: t, end: t.data.length });
390
421
  }
391
422
  if (resume) {
392
423
  let at = 0;
393
424
  while (at < resume.data.length && BREAKING_SPACE.test(resume.data[at]))
394
425
  at++;
395
- leaveOut(resume, 0, at);
426
+ lead.push({ node: resume, end: at });
427
+ }
428
+ let left = indent;
429
+ for (const { node, end } of lead.reverse()) {
430
+ const kept = preservesSpace(node) ? Math.min(left, end) : 0;
431
+ left -= kept;
432
+ dropSpace(node, 0, end - kept);
396
433
  }
397
434
  }
398
435
  /** The mirror: everything after the last kept character is held and not shown.@param keep - the node holding the last kept character. */
@@ -400,14 +437,14 @@ function leaveOutAfter(clone, keep) {
400
437
  let end = keep.data.length;
401
438
  while (end > 0 && BREAKING_SPACE.test(keep.data[end - 1]))
402
439
  end--;
403
- leaveOut(keep, end, keep.data.length);
404
440
  let after = false;
405
- for (const t of textsIn(clone)) {
441
+ for (const t of [...textsIn(clone)]) {
406
442
  if (after)
407
- leaveOut(t, 0, t.data.length);
443
+ dropSpace(t, 0, t.data.length);
408
444
  else if (t === keep)
409
445
  after = true;
410
446
  }
447
+ dropSpace(keep, end, keep.data.length);
411
448
  }
412
449
  /** The text nodes under an element, in document order — by `childNodes`, as `joinedWords` reads, so the DOM-free suites' trees serve. */
413
450
  function* textsIn(el) {
@@ -426,7 +463,9 @@ function consumeLeadingWords(clone, n, char = 0, hollow = false) {
426
463
  // A resume inside word n (*a line inside a word is a break point*, 2026-09-23): its first `char` characters were shown on the page before, and go with the words before it. The page resumes at the part holding the first character left.
427
464
  const inside = char > 0 && n < words.length ? charRanges(words[n], 0, char) : [];
428
465
  const resumeWord = words[Math.min(n, words.length - 1)];
429
- const resumeNode = n < words.length ? (partAt(resumeWord, char) ?? resumeWord.parts[0]).node : null;
466
+ const resumePart = n < words.length ? partAt(resumeWord, char) ?? resumeWord.parts[0] : null;
467
+ const resumeNode = resumePart?.node ?? null;
468
+ const indent = resumePart && char === 0 ? indentationBefore(clone, resumePart.node, resumePart.start) : 0;
430
469
  // Blank the consumed words' exact character ranges, one pass a node (`word-space.js`' `cutRanges`, which records what went).
431
470
  const perNode = new Map();
432
471
  for (const part of [...words.slice(0, n).flatMap((w) => w.parts), ...inside]) {
@@ -436,7 +475,7 @@ function consumeLeadingWords(clone, n, char = 0, hollow = false) {
436
475
  }
437
476
  for (const [node, ranges] of perNode)
438
477
  cutRanges(node, ranges);
439
- leaveOutBefore(clone, resumeNode);
478
+ leaveOutBefore(clone, resumeNode, indent);
440
479
  // Structural pass: an element with no remaining words, standing entirely before the resume point, is spent — its edges adjoin the cut (§5.2) and it leaves the fragment.
441
480
  const spent = [];
442
481
  (function walk(el) {
@@ -45,6 +45,8 @@ export type Box = {
45
45
  anonymous?: boolean;
46
46
  /** A table's footer group written before its rows (`footerBeforeRows`), moved to the end of the table's procession, where §17.2 lays it out. */
47
47
  laidOutLast?: boolean;
48
+ /** A table's header group written after its rows (`headerAfterRows`), moved to the front of the table's procession, where §17.2 lays it out. */
49
+ laidOutFirst?: boolean;
48
50
  };
49
51
  /** One line box, recovered arithmetically from the rects clustered onto it and the line-heights they are set on (`computeLines`). */
50
52
  export type LineBox = {
@@ -95,6 +97,8 @@ export type Tracks = {
95
97
  };
96
98
  /** The sealed report {@link takeReport} hands back. */
97
99
  export type MeasureReport = ReturnType<typeof takeReport>;
100
+ /** A `display` whose inline content CSS 2.1 §9.2.1.1 wraps in anonymous block boxes — a block container's. A flex or grid container makes a run an anonymous item and a table an anonymous cell, which are other boxes and other questions. */
101
+ export declare const holdsAnonymousBlocks: (display: string) => boolean;
98
102
  /**
99
103
  * Read one settled layout into an immutable snapshot — obtaining it seals the mutation phase (*render-identical conditions*, I5, by type).
100
104
  *
package/dist/src/gauge.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * @packageDocumentation
16
16
  */
17
17
  import { BREAK, selectorOf } from './data-beltype.js';
18
- import { blockChildrenOf, footerBeforeRows, isAtom, isEngineText, isInlineBox, isLineBreak, mayBeAutoHyphenated, rangeOf, READS_ITS_CHILDREN, rowsAmong, SIDE_BY_SIDE, wordRuns } from './word-space.js';
18
+ import { blockChildrenOf, footerBeforeRows, headerAfterRows, isAtom, isEngineText, isInlineBox, isLineBreak, mayBeAutoHyphenated, rangeOf, READS_ITS_CHILDREN, rowsAmong, SIDE_BY_SIDE, wordRuns } from './word-space.js';
19
19
  /** The Compositor's own `<br>`, found by the browser and never by splitting the attribute here (the walkers below run per box). */
20
20
  const BREAK_SELECTOR = `br${selectorOf(BREAK)}`;
21
21
  /**
@@ -28,7 +28,7 @@ const sharesLine = (aTop, aBottom, bTop, bBottom) => Math.min(aBottom, bBottom)
28
28
  /** A character a word is made of: anything `BREAKING_SPACE` does not divide at. */
29
29
  const WORD_CHAR = /[\S  ]/;
30
30
  /** A `display` whose inline content CSS 2.1 §9.2.1.1 wraps in anonymous block boxes — a block container's. A flex or grid container makes a run an anonymous item and a table an anonymous cell, which are other boxes and other questions. */
31
- const holdsAnonymousBlocks = (display) => !/flex|grid|-webkit-box/.test(display) && (!/table/.test(display) || /^table-(cell|caption)$/.test(display));
31
+ export const holdsAnonymousBlocks = (display) => !/flex|grid|-webkit-box/.test(display) && (!/table/.test(display) || /^table-(cell|caption)$/.test(display));
32
32
  const GAP_MIN = 0.75; // px — a third rect-reading rule, learned from the differential: half-leading float arithmetic leaves sub-pixel residue between a text box's last line and its border, and reporting that residue as a gap fabricates Class C cuts that bypass keeps. Real gaps (padding, margins, constrained heights) are ≥ 1px; noise stays under half a pixel.
33
33
  /**
34
34
  * Read one settled layout into an immutable snapshot — obtaining it seals the mutation phase (*render-identical conditions*, I5, by type).
@@ -91,6 +91,18 @@ export function takeReport(container, { contentHeight }) {
91
91
  list.splice(last, 0, footer); // `last` moved down by one with the removal, so this is the place after it
92
92
  footer.laidOutLast = moved = true;
93
93
  }
94
+ // The header stands first, the mirror (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): §17.2 lays a `table-header-group` out *"before all other rows and row groups"*, so one written after them is moved before the first. Its words stay after the rows in the word space, which is why the Stonehand offers no place inside it or right after it, and why the driver refuses one that does not repeat: only a copy can show it on a page before its words.
95
+ for (const list of [box.blockChildren, box.children]) {
96
+ const h = list.findIndex((c) => headerAfterRows(c.el));
97
+ if (h < 0)
98
+ continue;
99
+ const first = list.findIndex((c) => /^table-row(-group)?$/.test(win.getComputedStyle(c.el).display));
100
+ if (first < 0 || first > h)
101
+ continue;
102
+ const [header] = list.splice(h, 1);
103
+ list.splice(first, 0, header);
104
+ header.laidOutFirst = moved = true;
105
+ }
94
106
  }
95
107
  /**
96
108
  * The anonymous boxes (*the anonymous box is in the procession*, 2026-09-29; CSS 2.1 §9.2.1.1). A block container holding a block-level box beside inline content wraps each maximal run of that content in an anonymous block box, and css-break-3 §4.1 divides the result like any procession: a Class A point between the run and the block beside it, Class B points between its lines, and no Class C point where it stands, since a line is child content. The procession used to be the element tree's block children alone, so a run had no box: its lines were read as nothing but the distance they opened, a start gap before the first block child and an end gap after the last, and a page edge crossing thirteen trailing lines shelled them blank (`packages/essay/cases/76-the-line-before-the-first-block`).