1
//! Layout tree construction from a styled DOM, including anonymous box generation
2
use std::{
3
    cell::Cell,
4
    collections::BTreeMap,
5
    hash::{Hash, Hasher},
6
    sync::Arc,
7
};
8

            
9
use azul_core::diff::NodeDataFingerprint;
10

            
11
use crate::text3::cache::UnifiedConstraints;
12

            
13
thread_local! {
14
    /// Per-thread counter for IFC IDs, reset to 0 at the start of each layout
15
    /// pass (see [`IfcId::reset_counter`]).
16
    ///
17
    /// This was previously a process-global `AtomicU32`. Two `layout_document`
18
    /// calls running concurrently on different threads (the `Sync` layout bound
19
    /// permits this) shared that single counter, so their IFC IDs interleaved and
20
    /// collided. A thread-local counter gives each pass its own sequence — a single
21
    /// pass is single-threaded (`LayoutContext` holds non-`Sync` `RefCell` caches),
22
    /// so IDs stay deterministic and stable across frames while never colliding
23
    /// across concurrent passes.
24
    static IFC_ID_COUNTER: Cell<u32> = const { Cell::new(0) };
25
}
26

            
27
/// Unique identifier for an Inline Formatting Context (IFC).
28
///
29
/// An IFC represents a region where inline content (text, inline-blocks, images)
30
/// is laid out together. One IFC can contain content from multiple DOM nodes
31
/// (e.g., `<p>Hello <span>world</span>!</p>` is one IFC with 3 text runs).
32
///
33
/// The ID is generated using a per-thread counter that resets at the start
34
/// of each layout pass. This ensures:
35
/// - IDs are unique within a layout pass
36
/// - The same logical IFC gets the same ID across frames (for selection stability)
37
/// - Concurrent `layout_document` passes on different threads can't collide
38
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39
pub struct IfcId(pub u32);
40

            
41
impl IfcId {
42
    /// Generate a new unique IFC ID (within the current thread's layout pass).
43
50967
    #[must_use] pub fn unique() -> Self {
44
50967
        IFC_ID_COUNTER.with(|c| {
45
50967
            let v = c.get();
46
50967
            c.set(v.wrapping_add(1));
47
50967
            Self(v)
48
50967
        })
49
50967
    }
50

            
51
    /// Reset the IFC ID counter. Called at the start of each layout pass.
52
4902
    pub fn reset_counter() {
53
4902
        IFC_ID_COUNTER.with(|c| c.set(0));
54
4902
    }
55
}
56

            
57
/// Tracks a layout node's membership in an Inline Formatting Context.
58
///
59
/// Text nodes don't store their own `inline_layout_result` - instead, they
60
/// participate in their parent's IFC. This struct provides the link from
61
/// a text node back to its IFC's layout data.
62
///
63
/// # Architecture
64
///
65
/// ```text
66
/// DOM:  <p>Hello <span>world</span>!</p>
67
///
68
/// Layout Tree:
69
/// ├── LayoutNode (p) - IFC root
70
/// │   └── inline_layout_result: Some(UnifiedLayout)
71
/// │   └── ifc_id: IfcId(5)
72
/// │
73
/// ├── LayoutNode (::text "Hello ")
74
/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 0 })
75
/// │
76
/// ├── LayoutNode (span)
77
/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
78
/// │   └── LayoutNode (::text "world")
79
/// │       └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
80
/// │
81
/// └── LayoutNode (::text "!")
82
///     └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 2 })
83
/// ```
84
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85
pub struct IfcMembership {
86
    /// The IFC ID this node's content was laid out in.
87
    pub ifc_id: IfcId,
88
    /// The index of the IFC root `LayoutNode` in the layout tree.
89
    /// Used to quickly find the node with `inline_layout_result`.
90
    pub ifc_root_layout_index: usize,
91
    /// Which run index within the IFC corresponds to this node's text.
92
    /// Maps to `ContentIndex::run_index` in the shaped items.
93
    pub run_index: u32,
94
}
95

            
96
use azul_core::{
97
    dom::{FormattingContext, NodeData, NodeId, NodeType},
98
    geom::{LogicalPosition, LogicalRect, LogicalSize},
99
    styled_dom::StyledDom,
100
};
101
use azul_css::{
102
    corety::LayoutDebugMessage,
103
    css::CssPropertyValue,
104
    codegen::format::GetHash,
105
    props::{
106
        basic::{
107
            pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
108
        },
109
        layout::{
110
            LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
111
            LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
112
            LayoutWritingMode,
113
        },
114
        property::{CssProperty, CssPropertyType},
115
        style::{StyleTextAlign, StyleWhiteSpace},
116
    },
117
};
118
use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
119

            
120
#[cfg(feature = "text_layout")]
121
use crate::text3;
122
use crate::{
123
    debug_log,
124
    font::parsed::ParsedFont,
125
    font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
126
    solver3::{
127
        geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
128
        getters::{
129
            get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
130
            get_css_min_width, get_css_width, get_direction_property as get_direction,
131
            get_display_property, get_float, get_overflow_x,
132
            get_overflow_y, get_position, get_text_align,
133
            get_text_orientation_property as get_text_orientation,
134
            get_white_space_property, get_writing_mode, MultiValue,
135
        },
136
        scrollbar::ScrollbarRequirements,
137
        LayoutContext, Result,
138
    },
139
    text3::cache::AvailableSpace,
140
};
141

            
142
/// Represents the invalidation state of a layout node.
143
///
144
/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
145
/// A node marked for `Layout` does not also need to be marked for `Paint`.
146
///
147
/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
148
///
149
/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
150
/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
151
/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
152
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
153
pub enum DirtyFlag {
154
    /// The node's layout is valid and no repaint is needed. This is the "clean" state.
155
    #[default]
156
    None,
157
    /// The node's geometry is valid, but its appearance (e.g., color) has changed.
158
    /// Requires a display list update only.
159
    Paint,
160
    /// The node's geometry (size or position) is invalid.
161
    /// Requires a full layout pass and a display list update.
162
    Layout,
163
}
164

            
165
/// A hash that represents the content and style of a node PLUS all of its descendants.
166
/// If two `SubtreeHashes` are equal, their entire subtrees are considered identical for layout
167
/// purposes.
168
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
169
pub struct SubtreeHash(pub u64);
170

            
171
/// Per-item metrics cached from the last IFC layout.
172
///
173
/// These metrics enable incremental IFC relayout (Phase 2 optimization):
174
/// when a single inline item changes, we can check whether its advance width
175
/// changed and potentially skip full line-breaking for unaffected lines.
176
///
177
/// Index in `CachedInlineLayout::item_metrics` matches the item order in
178
/// `UnifiedLayout::items`.
179
#[derive(Copy, Debug, Clone)]
180
pub struct InlineItemMetrics {
181
    /// The DOM `NodeId` of the source node for this item (for dirty checking).
182
    /// `None` for generated content (list markers, hyphens, etc.)
183
    pub source_node_id: Option<NodeId>,
184
    /// Advance width of this item (glyph run width, inline-block width, etc.)
185
    pub advance_width: f32,
186
    /// Advance height contribution from this item to its line box.
187
    pub line_height_contribution: f32,
188
    /// Whether this item can participate in line breaking.
189
    /// `false` for items inside `white-space: nowrap` or `white-space: pre`.
190
    pub can_break: bool,
191
    /// Which line this item was placed on (0-indexed).
192
    pub line_index: u32,
193
    /// X offset within its line.
194
    pub x_offset: f32,
195
}
196

            
197
/// Cached inline layout result with the constraints used to compute it.
198
///
199
/// This structure solves a fundamental architectural problem: inline layouts
200
/// (text wrapping, inline-block positioning) depend on the available width.
201
/// Different layout phases may compute the layout with different widths:
202
///
203
/// 1. **Min-content measurement**: width = `MinContent` (effectively 0)
204
/// 2. **Max-content measurement**: width = `MaxContent` (effectively infinite)
205
/// Cached output of `collect_and_measure_inline_content` (see
206
/// `LayoutNodeWarm::inline_content_cache`).
207
#[derive(Debug, Clone)]
208
pub struct CachedInlineContent {
209
    /// The collected inline content (text runs, atomics, markers).
210
    pub content: Vec<crate::text3::cache::InlineContent>,
211
    /// `ContentIndex` -> child layout-node index, as collection built it.
212
    pub child_map: std::collections::HashMap<crate::text3::cache::ContentIndex, usize>,
213
    /// Validity key: the fingerprints of the IFC root and its descendants,
214
    /// folded in tree order. Anything that changes text, style or structure
215
    /// changes a fingerprint, so an equal key means an identical collection.
216
    pub subtree_fingerprint: u64,
217
    /// Hash of `content` alone, computed ONCE when the collection is (re)built.
218
    ///
219
    /// The per-visit "current content hash" used to re-hash the full inline
220
    /// content on EVERY `layout_ifc` entry — 2 292 hashes × ~12.7 µs ≈ 29 ms
221
    /// per resize relayout on big.md, hashing bytes that by definition had not
222
    /// changed (an equal `subtree_fingerprint` is what admitted the cached
223
    /// collection in the first place). Visits now fold only the seven
224
    /// constraint-level container properties on top of this base.
225
    pub content_hash_base: u64,
226
}
227

            
228
/// 3. **Final layout**: width = `Definite(actual_column_width)`
229
///
230
/// Without tracking which constraints were used, a cached result from phase 1
231
/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
232
/// positions (the root cause of table cell width bugs).
233
///
234
/// By storing the constraints alongside the result, we can:
235
/// - Invalidate the cache when constraints change
236
/// - Keep multiple cached results for different constraint types if needed
237
/// - Ensure the final render always uses a layout computed with correct widths
238
#[derive(Debug, Clone)]
239
pub struct CachedInlineLayout {
240
    /// The computed inline layout.
241
    ///
242
    /// (d6h RETIREMENT) Under `AZ_DENSE_TEXT=1`, when the dense view is
243
    /// retained AND covers every item (pure clusters), this holds a
244
    /// SHARED EMPTY sentinel instead of the real thing — the ~192 B/item
245
    /// sparse retention is the campaign's target. Every production
246
    /// reader is dense-first by d1-d6h; `verify` mode retains BOTH and
247
    /// A/B-asserts; flag-off is unchanged. `overflow` (below) carries
248
    /// the one field the sentinel loses.
249
    pub layout: Arc<UnifiedLayout>,
250
    /// (d6h) The real layout's overflow info, captured at store time —
251
    /// the incremental-relayout guard reads it after the sentinel swap.
252
    pub overflow: crate::text3::cache::OverflowInfo,
253
    /// The available width constraint used to compute this layout.
254
    /// This is the key for cache validity checking.
255
    /// +spec:writing-modes:1dcba2 - "available width" (CSS2.1) = auto size in inline axis
256
    pub available_width: AvailableSpace,
257
    /// Whether this layout was computed with float exclusions.
258
    /// Float-aware layouts should not be overwritten by non-float layouts.
259
    pub has_floats: bool,
260
    /// The full constraints used to compute this layout.
261
    /// Used for quick relayout after text edits without rebuilding from CSS.
262
    pub constraints: Option<UnifiedConstraints>,
263
    /// Glyph runs derived from `layout` at STORE time (a pure function of
264
    /// it — see `get_glyph_runs_simple`). The display-list generator used to
265
    /// re-derive these on EVERY paint of every IFC (the run-grouping walk was
266
    /// the bulk of `dl_inline_text`'s 6.8 ms/resize on big.md); computing them
267
    /// once alongside the layout makes every later paint a lookup. Rides
268
    /// `GlyphSwap` reuse for free — a reused layout is a reused run list.
269
    ///
270
    /// (#25) Stored COMPACT — 8 B/glyph instead of the 20 B `GlyphInstance`
271
    /// (y and size are per-run-uniform in the simple-run contract; deviants
272
    /// ride the exception table). Paint expands into the same offset Vec it
273
    /// always built; `AZ_DENSE_TEXT=verify` roundtrip-gates the encoding.
274
    pub glyph_runs: Arc<Vec<crate::text3::glyphs::CompactGlyphRun>>,
275
    /// Per-item metrics for incremental IFC relayout (Phase 2).
276
    ///
277
    /// Each entry corresponds to one `PositionedItem` in `layout.items`.
278
    /// These metrics enable the IFC relayout decision tree:
279
    /// - Check if a dirty node's `advance_width` changed → skip repositioning if not
280
    /// - Use `can_break` + `line_index` for the nowrap fast path
281
    /// - Use `x_offset` for shifting subsequent items without full line-breaking
282
    pub item_metrics: Vec<InlineItemMetrics>,
283
    /// Cached line break boundaries for incremental relayout.
284
    /// Enables checking if a width change fits on the same line without
285
    /// re-running the full line-breaking algorithm.
286
    pub line_breaks: Option<crate::text3::cache::CachedLineBreaks>,
287
    /// §3.2 step (d5): the ONE cache-stable Arc the display list carries
288
    /// as its `TextLayout` payload. `TextPayload{dense,sparse}` when the
289
    /// dense view is retained, else the `layout` Arc itself (identical
290
    /// to the pre-d5 payload). Built at store time so damage `ptr_eq`
291
    /// stays stable across paints.
292
    pub payload: Arc<dyn std::any::Any + Send + Sync>,
293
    /// §3.2 step (d1): the dense view of `layout`, retained when
294
    /// `AZ_DENSE_TEXT` is active (`glyph_runs` derive from THIS instance).
295
    /// Readers migrate from `layout` onto it one by one; the sparse form
296
    /// retires when the last reader has moved (plan §10.4).
297
    pub dense: Option<Arc<crate::text3::dense::DenseText>>,
298
    /// Hash of the `InlineContent` this layout was shaped from. The Phase 2d
299
    /// fast-path reuse in fc.rs keys cache validity on WIDTH only; without this,
300
    /// a same-width `RefreshDom` whose text CHANGED would reuse the stale shaped
301
    /// layout (#11 stale display list). 0 = unknown ⇒ never fast-path-reuse.
302
    pub inline_content_hash: u64,
303
}
304

            
305
/// §3.2 step (d5): the display list's `TextLayout` payload when the dense
306
/// view is retained — BOTH forms behind one cache-stable Arc, so
307
/// `TextLayout` damage diffing (`Arc::ptr_eq`) keeps firing exactly as
308
/// before. Downcasters try this first and fall back to a bare
309
/// `UnifiedLayout` payload (the flag-off path). The `sparse` half is
310
/// what current consumers read; it empties at the d6 retirement, which
311
/// is when the consumers move onto `dense`.
312
#[derive(Debug)]
313
pub struct TextPayload {
314
    pub dense: Arc<crate::text3::dense::DenseText>,
315
    pub sparse: Arc<UnifiedLayout>,
316
}
317

            
318
/// §3.2 flip step (a): the paint-run computation, routed by `AZ_DENSE_TEXT`.
319
///
320
/// - unset/`0` — the sparse reference walker (production default).
321
/// - `1`      — build [`crate::text3::dense::DenseText`] and walk THAT
322
///   (`get_glyph_runs_simple_dense`), the compact model's first production
323
///   consumer.
324
/// - `verify` — dense path PLUS an A/B assert against the reference; the
325
///   e2e corpus run with this set is the flip proof over real layouts.
326
///
327
/// Layouts containing combined blocks (tate-chu-yoko) fall back to the
328
/// reference under the flag: the dense model keeps atomics on the sparse
329
/// side by design and the twin would silently DROP their glyphs.
330
150556
fn compute_glyph_runs(
331
150556
    layout: &UnifiedLayout,
332
150556
) -> (
333
150556
    Vec<crate::text3::glyphs::SimpleGlyphRun>,
334
150556
    Option<Arc<crate::text3::dense::DenseText>>,
335
150556
) {
336
150556
    compute_glyph_runs_with_mode(layout, dense_text_mode())
337
150556
}
338

            
339
/// The process-wide `AZ_DENSE_TEXT` mode: 0 off, 1 on, 2 verify.
340
///
341
/// (d7) DEFAULT ON: the dense model is the stored form in production —
342
/// the retirement (d6h) plateaued at 65.2 MB heap vs 73.6 flag-off on
343
/// the 960-line corpus, with the full battery + flag-on corpus +
344
/// flag-on reftests green. `AZ_DENSE_TEXT=0` is the escape hatch;
345
/// `verify` retains both forms and A/B-asserts every dense-first arm.
346
451794
pub(crate) fn dense_text_mode() -> u8 {
347
    static MODE: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
348
451794
    *MODE.get_or_init(|| match std::env::var("AZ_DENSE_TEXT").as_deref() {
349
        Ok("0" | "off") => 0,
350
        Ok("verify") => 2,
351
20
        _ => 1,
352
20
    })
353
451794
}
354

            
355
/// #25: converts the transient builder output into the stored compact form.
356
/// Under `AZ_DENSE_TEXT=verify` every run is roundtripped and compared
357
/// BIT-EXACTLY against its original — the same proof obligation the dense
358
/// campaign carried: the stored form must be an encoding, never a rewrite.
359
150556
fn compact_glyph_runs(
360
150556
    runs: Vec<crate::text3::glyphs::SimpleGlyphRun>,
361
150556
) -> Vec<crate::text3::glyphs::CompactGlyphRun> {
362
150556
    let verify = dense_text_mode() == 2;
363
150556
    runs.into_iter()
364
176046
        .map(|r| {
365
175997
            if verify {
366
                let compact = crate::text3::glyphs::CompactGlyphRun::from(r.clone());
367
                assert!(
368
                    crate::text3::glyphs::simple_runs_bit_equal(&compact.expand(), &r),
369
                    "#25 compact glyph-run roundtrip diverged from the original"
370
                );
371
                compact
372
            } else {
373
175997
                crate::text3::glyphs::CompactGlyphRun::from(r)
374
            }
375
175997
        })
376
150556
        .collect()
377
150556
}
378

            
379
/// Testable core of [`compute_glyph_runs`] — `mode` injected so the d1
380
/// retention pin can exercise the dense path without env-var races.
381
150559
fn compute_glyph_runs_with_mode(
382
150559
    layout: &UnifiedLayout,
383
150559
    mode: u8,
384
150559
) -> (
385
150559
    Vec<crate::text3::glyphs::SimpleGlyphRun>,
386
150559
    Option<Arc<crate::text3::dense::DenseText>>,
387
150559
) {
388
    use crate::text3::cache::ShapedItem;
389
150559
    if mode == 0
390
150558
        || layout
391
150558
            .items
392
150558
            .iter()
393
2221122
            .any(|it| matches!(it.item, ShapedItem::CombinedBlock { .. }))
394
    {
395
1
        return (crate::text3::glyphs::get_glyph_runs_simple(layout), None);
396
150558
    }
397
    // §3.2 step (d1): the dense view is RETAINED on the cache entry, not
398
    // rebuilt-and-dropped per walk — the readers migrate onto it one by
399
    // one, and the sparse `layout` retires when the last one has.
400
150558
    let dense = crate::text3::dense::DenseText::from_unified(layout);
401
150558
    let ours = crate::text3::dense::get_glyph_runs_simple_dense(&dense);
402
150558
    if mode == 2 {
403
1
        let reference = crate::text3::glyphs::get_glyph_runs_simple(layout);
404
1
        assert_eq!(reference.len(), ours.len(), "AZ_DENSE_TEXT=verify: run count diverged");
405
1
        for (i, (r, o)) in reference.iter().zip(ours.iter()).enumerate() {
406
            assert!(
407
                r.color == o.color
408
                    && r.background_color == o.background_color
409
                    && r.background_content == o.background_content
410
                    && r.border == o.border
411
                    && r.font_hash == o.font_hash
412
                    && (r.font_size_px - o.font_size_px).abs() < 0.01
413
                    && r.text_decoration == o.text_decoration
414
                    && r.source_node_id == o.source_node_id
415
                    && r.glyphs.len() == o.glyphs.len()
416
                    && r.glyphs.iter().zip(o.glyphs.iter()).all(|(a, b)| {
417
                        a.index == b.index
418
                            && (a.point.x - b.point.x).abs() < 0.01
419
                            && (a.point.y - b.point.y).abs() < 0.01
420
                    }),
421
                "AZ_DENSE_TEXT=verify: run {i} diverged from the reference\n  \
422
                 color {:?}/{:?} bg {:?}/{:?} font_hash {:?}/{:?} size {}/{} \
423
                 deco {:?}/{:?} node {:?}/{:?}\n  glyph count {} vs {}\n  \
424
                 ref glyphs {:?}\n  our glyphs {:?}",
425
                r.color, o.color,
426
                r.background_color, o.background_color,
427
                r.font_hash, o.font_hash,
428
                r.font_size_px, o.font_size_px,
429
                r.text_decoration, o.text_decoration,
430
                r.source_node_id, o.source_node_id,
431
                r.glyphs.len(), o.glyphs.len(),
432
                r.glyphs.iter().map(|g| (g.index, g.point.x, g.point.y)).collect::<Vec<_>>(),
433
                o.glyphs.iter().map(|g| (g.index, g.point.x, g.point.y)).collect::<Vec<_>>(),
434
            );
435
        }
436
150557
    }
437
150558
    (ours, Some(Arc::new(dense)))
438
150559
}
439

            
440
/// §3.2 step (d2): per-item metrics FROM the dense view. Returns None
441
/// unless the layout was PURE clusters (`dense.clusters.len()` must
442
/// equal `layout.items.len()` — the dense builder silently skips
443
/// objects/tabs/breaks/combined blocks, and `item_metrics` is
444
/// index-aligned with ALL items, so a shorter vec would silently
445
/// misalign the incremental-relayout decision tree).
446
///
447
/// Field-for-field vs the sparse extraction (the d2 gate pins this):
448
/// `advance_width` = ClusterCompact.advance (BASE advance == bounds().width
449
/// since the d2 redefinition), `line_height_contribution` = the run's
450
/// resolved line height (== ascent+descent by the half-leading identity),
451
/// `can_break` = true (clusters are never `ShapedItem::Break`), `line_index` =
452
/// `LineRecord.source_index`, `x_offset` = ClusterCompact.x.
453
150556
fn extract_item_metrics_dense(
454
150556
    dense: &crate::text3::dense::DenseText,
455
150556
    item_count: usize,
456
150556
) -> Option<Vec<InlineItemMetrics>> {
457
150556
    if dense.clusters.len() != item_count {
458
329
        return None;
459
150227
    }
460
150227
    let mut out = Vec::with_capacity(dense.clusters.len());
461
150227
    let mut line_iter = dense.lines.iter().peekable();
462
2219335
    for (ci, run) in dense
463
150227
        .runs
464
150227
        .iter()
465
2219335
        .flat_map(|r| (r.clusters.start..r.clusters.end).map(move |i| (i, r)))
466
    {
467
2219335
        let c = &dense.clusters[ci as usize];
468
2242746
        while let Some(l) = line_iter.peek() {
469
2242746
            if ci >= l.clusters.1 {
470
23411
                line_iter.next();
471
23411
            } else {
472
2219335
                break;
473
            }
474
        }
475
2219335
        let line_index = line_iter.peek().map_or(0, |l| l.source_index);
476
2219335
        let m = &run.font_metrics;
477
2219335
        let lh = if m.units_per_em == 0 {
478
13
            0.0
479
        } else {
480
2219322
            run.style
481
2219322
                .line_height
482
2219322
                .resolve_with_metrics(run.style.font_size_px, m)
483
        };
484
2219335
        out.push(InlineItemMetrics {
485
2219335
            source_node_id: (run.source_node != u32::MAX)
486
2219335
                .then(|| NodeId::new(run.source_node as usize)),
487
2219335
            advance_width: c.advance,
488
2219335
            line_height_contribution: lh,
489
            can_break: true,
490
2219335
            line_index,
491
2219335
            x_offset: c.x,
492
        });
493
    }
494
150227
    Some(out)
495
150556
}
496

            
497
impl CachedInlineLayout {
498
    /// Creates a new cached inline layout.
499
103
    #[must_use] pub fn new(
500
103
        layout: Arc<UnifiedLayout>,
501
103
        available_width: AvailableSpace,
502
103
        has_floats: bool,
503
103
    ) -> Self {
504
103
        let (runs, dense) = compute_glyph_runs(&layout);
505
103
        let glyph_runs = Arc::new(compact_glyph_runs(runs));
506
103
        let item_metrics = Self::choose_item_metrics(&layout, dense.as_deref());
507
103
        let (layout, overflow) = Self::retire_sparse(layout, dense.as_deref());
508
        // Payload built from the STORED form — under the retirement the
509
        // sparse half is the sentinel and consumers expand from dense.
510
103
        let payload = Self::build_payload(&layout, dense.as_ref());
511
103
        Self {
512
103
            layout,
513
103
            overflow,
514
103
            available_width,
515
103
            has_floats,
516
103
            constraints: None,
517
103
            payload,
518
103
            glyph_runs,
519
103
            item_metrics,
520
103
            line_breaks: None,
521
103
            dense,
522
103
            inline_content_hash: 0,
523
103
        }
524
103
    }
525

            
526
    /// Creates a new cached inline layout with full constraints.
527
150453
    #[must_use] pub fn new_with_constraints(
528
150453
        layout: Arc<UnifiedLayout>,
529
150453
        available_width: AvailableSpace,
530
150453
        has_floats: bool,
531
150453
        constraints: UnifiedConstraints,
532
150453
    ) -> Self {
533
150453
        let available_width_px = match available_width {
534
83512
            AvailableSpace::Definite(w) => w,
535
66941
            _ => f32::MAX,
536
        };
537
150453
        let line_breaks = Some(crate::text3::cache::extract_line_breaks(
538
150453
            &layout.items, available_width_px,
539
150453
        ));
540
150453
        let (runs, dense) = compute_glyph_runs(&layout);
541
150453
        let glyph_runs = Arc::new(compact_glyph_runs(runs));
542
150453
        let item_metrics = Self::choose_item_metrics(&layout, dense.as_deref());
543
150453
        let (layout, overflow) = Self::retire_sparse(layout, dense.as_deref());
544
        // Payload built from the STORED form — under the retirement the
545
        // sparse half is the sentinel and consumers expand from dense.
546
150453
        let payload = Self::build_payload(&layout, dense.as_ref());
547
150453
        Self {
548
150453
            layout,
549
150453
            overflow,
550
150453
            available_width,
551
150453
            has_floats,
552
150453
            constraints: Some(constraints),
553
150453
            payload,
554
150453
            glyph_runs,
555
150453
            item_metrics,
556
150453
            line_breaks,
557
150453
            dense,
558
150453
            inline_content_hash: 0,
559
150453
        }
560
150453
    }
561

            
562
    /// (d6h) The stored layout, MATERIALIZED if it is the retirement
563
    /// sentinel: dense expands (exact by the d6h expansion gate) into a
564
    /// transient sparse copy. The universal accessor for readers that
565
    /// measure or walk content; flag-off / verify / non-pure entries
566
    /// return the stored Arc untouched.
567
    #[must_use]
568
91828
    pub fn materialized(&self) -> Arc<UnifiedLayout> {
569
91828
        if self.layout.items.is_empty() {
570
91522
            if let Some(d) = self.dense.as_deref() {
571
91522
                if !d.clusters.is_empty() {
572
91504
                    return Arc::new(UnifiedLayout {
573
91504
                        items: d.to_unified_items(),
574
91504
                        overflow: self.overflow.clone(),
575
91504
                    });
576
18
                }
577
            }
578
306
        }
579
324
        self.layout.clone()
580
91828
    }
581

            
582
    /// (d6h) THE RETIREMENT: under `AZ_DENSE_TEXT=1`, a pure-cluster
583
    /// layout whose dense view is retained stores a SHARED EMPTY
584
    /// sentinel in `layout` — the per-item sparse retention (the
585
    /// campaign's ~192 B/cluster target) drops here. The real overflow
586
    /// info is captured first (the incremental guard reads it).
587
    /// `verify` mode (2) retains BOTH so every dense-first arm keeps
588
    /// A/B-asserting; flag-off (0) is byte-for-byte unchanged.
589
150556
    fn retire_sparse(
590
150556
        layout: Arc<UnifiedLayout>,
591
150556
        dense: Option<&crate::text3::dense::DenseText>,
592
150556
    ) -> (Arc<UnifiedLayout>, crate::text3::cache::OverflowInfo) {
593
150556
        let overflow = layout.overflow.clone();
594
        // Empty layouts are skipped: nothing to save, and the identity
595
        // swap would be pure Arc churn (unit fixtures rely on ptr_eq).
596
150556
        let fully_covered = !layout.items.is_empty()
597
150467
            && dense.is_some_and(|d| d.clusters.len() == layout.items.len());
598
150556
        if dense_text_mode() == 1 && fully_covered {
599
            static EMPTY: std::sync::OnceLock<Arc<UnifiedLayout>> = std::sync::OnceLock::new();
600
150138
            let sentinel = EMPTY
601
150138
                .get_or_init(|| {
602
20
                    Arc::new(UnifiedLayout {
603
20
                        items: Vec::new(),
604
20
                        overflow: crate::text3::cache::OverflowInfo::default(),
605
20
                    })
606
20
                })
607
150138
                .clone();
608
150138
            return (sentinel, overflow);
609
418
        }
610
418
        (layout, overflow)
611
150556
    }
612

            
613
    /// (d5) The cache-stable DL payload — see the `payload` field.
614
150556
    fn build_payload(
615
150556
        layout: &Arc<UnifiedLayout>,
616
150556
        dense: Option<&Arc<crate::text3::dense::DenseText>>,
617
150556
    ) -> Arc<dyn std::any::Any + Send + Sync> {
618
150556
        if let Some(d) = dense { Arc::new(TextPayload {
619
150556
            dense: d.clone(),
620
150556
            sparse: layout.clone(),
621
150556
        }) } else {
622
            let shared: Arc<dyn std::any::Any + Send + Sync> = layout.clone();
623
            shared
624
        }
625
150556
    }
626

            
627
    /// (d2) Metrics source selection: dense when retained AND the layout
628
    /// is pure clusters; sparse otherwise. Under `AZ_DENSE_TEXT=verify` the
629
    /// dense result is A/B-asserted against the sparse extraction.
630
150556
    fn choose_item_metrics(
631
150556
        layout: &UnifiedLayout,
632
150556
        dense: Option<&crate::text3::dense::DenseText>,
633
150556
    ) -> Vec<InlineItemMetrics> {
634
150556
        let Some(d) = dense else {
635
            return Self::extract_item_metrics(layout);
636
        };
637
150556
        let Some(from_dense) = extract_item_metrics_dense(d, layout.items.len()) else {
638
329
            return Self::extract_item_metrics(layout);
639
        };
640
150227
        let verify = std::env::var("AZ_DENSE_TEXT").as_deref() == Ok("verify");
641
150227
        if verify {
642
            let reference = Self::extract_item_metrics(layout);
643
            assert_eq!(reference.len(), from_dense.len(), "d2 verify: metrics count");
644
            for (i, (r, o)) in reference.iter().zip(from_dense.iter()).enumerate() {
645
                assert!(
646
                    r.source_node_id == o.source_node_id
647
                        && (r.advance_width - o.advance_width).abs() < 0.01
648
                        && (r.line_height_contribution - o.line_height_contribution).abs() < 0.01
649
                        && r.can_break == o.can_break
650
                        && r.line_index == o.line_index
651
                        && (r.x_offset - o.x_offset).abs() < 0.01,
652
                    "d2 verify: metrics @{i} diverged (ref {r:?} vs dense {o:?})"
653
                );
654
            }
655
150227
        }
656
150227
        from_dense
657
150556
    }
658

            
659
    /// Extracts per-item metrics from a computed `UnifiedLayout`.
660
    ///
661
    /// This is called automatically by the constructors. The metrics
662
    /// enable incremental IFC relayout in Phase 2c/2d by providing
663
    /// cached advance widths, line assignments, and break information
664
    /// for each positioned item.
665
    #[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
666
334
    fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
667
        use crate::text3::cache::{ShapedItem, get_item_vertical_metrics_approx};
668

            
669
1793
        layout.items.iter().map(|positioned_item| {
670
1793
            let bounds = positioned_item.item.bounds();
671
1793
            let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
672

            
673
1793
            let source_node_id = match &positioned_item.item {
674
1386
                ShapedItem::Cluster(c) => c.source_node_id,
675
                // Objects (inline-blocks, images) and other generated items
676
                // don't expose source_node_id directly on ShapedItem.
677
                // Phase 2c will refine this via the ContentIndex mapping.
678
                ShapedItem::Object { .. }
679
                | ShapedItem::CombinedBlock { .. }
680
                | ShapedItem::Tab { .. }
681
407
                | ShapedItem::Break { .. } => None,
682
            };
683

            
684
            // For Phase 2a, default can_break = true for all items.
685
            // Phase 2c will refine this by checking the white-space property
686
            // on the IFC root's style or the item's own style context.
687
            // (Note: text3::StyleProperties doesn't carry white-space;
688
            //  that's resolved at the IFC/BFC boundary level.)
689
1793
            let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
690

            
691
1793
            InlineItemMetrics {
692
1793
                source_node_id,
693
1793
                advance_width: bounds.width,
694
1793
                line_height_contribution: ascent + descent,
695
1793
                can_break,
696
1793
                line_index: positioned_item.line_index as u32,
697
1793
                x_offset: positioned_item.position.x,
698
1793
            }
699
1793
        }).collect()
700
334
    }
701

            
702
    /// Checks if this cached layout is valid for the given constraints.
703
    ///
704
    /// A cached layout is valid if:
705
    /// 1. The available width matches (definite widths must be equal, or both are the same
706
    ///    indefinite type)
707
    /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
708
    ///
709
    /// The second condition preserves float-aware layouts, which are more "correct" than
710
    /// non-float layouts and shouldn't be overwritten.
711
188260
    #[must_use] pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
712
        // A cached layout with NO floats must not be reused when the new request DOES
713
        // have floats: the line boxes have to re-wrap around the float exclusions.
714
        // Without this, the two-pass IFC path (no-float sizing pass, then float-aware
715
        // re-layout) reuses the pass-1 no-float line breaks and text ignores the float
716
        // entirely (#19). Mirrors should_replace_with()'s gain-float branch so the two
717
        // stay consistent.
718
188260
        if new_has_floats && !self.has_floats {
719
133
            return false;
720
188127
        }
721

            
722
        // If we have a float-aware layout and the new request doesn't have floats,
723
        // keep the float-aware layout (it's more accurate)
724
188127
        if self.has_floats && !new_has_floats {
725
            // But only if the width constraint type matches
726
34
            return self.width_constraint_matches(new_width);
727
188093
        }
728

            
729
        // Otherwise, require exact width match
730
188093
        self.width_constraint_matches(new_width)
731
188260
    }
732

            
733
    /// Tolerance for comparing definite layout widths (in logical pixels).
734
    /// Sub-pixel differences below this threshold are treated as identical
735
    /// to avoid unnecessary relayout from floating-point rounding.
736
    const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
737

            
738
    /// Checks if the width constraint matches.
739
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
740
286255
    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
741
286255
        match (self.available_width, new_width) {
742
            // Definite widths must match within a small epsilon
743
146511
            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
744
146511
                (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
745
            }
746
            // MinContent matches MinContent
747
10
            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
748
            // MaxContent matches MaxContent
749
10
            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
750
            // Different constraint types don't match
751
139724
            _ => false,
752
        }
753
286255
    }
754

            
755
    /// Determines if this cached layout should be replaced by a new layout.
756
    ///
757
    /// Returns true if the new layout should replace this one.
758
98213
    #[must_use] pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
759
        // Always replace if we gain float information
760
98213
        if new_has_floats && !self.has_floats {
761
135
            return true;
762
98078
        }
763

            
764
        // Replace if width constraint changed
765
98078
        !self.width_constraint_matches(new_width)
766
98213
    }
767

            
768
    /// Returns a reference to the inner `UnifiedLayout`.
769
    ///
770
    /// This is a convenience method for code that only needs the layout data
771
    /// and doesn't care about the caching metadata.
772
    #[inline]
773
196
    #[must_use] pub const fn get_layout(&self) -> &Arc<UnifiedLayout> {
774
196
        &self.layout
775
196
    }
776

            
777
    /// Returns a clone of the inner Arc<UnifiedLayout>.
778
    ///
779
    /// This is useful for APIs that need to return an owned reference
780
    /// to the layout without exposing the caching metadata.
781
    #[inline]
782
2
    #[must_use] pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
783
2
        self.layout.clone()
784
2
    }
785
}
786

            
787
/// A layout tree node representing the CSS box model.
788
///
789
/// ## Memory Layout Optimization (`#[repr(C)]`)
790
///
791
/// Fields are ordered by access frequency (hottest first) to maximize CPU
792
/// cache line utilization during tree traversal. With `#[repr(C)]`, the
793
/// compiler preserves this ordering. The 6 hottest fields (~140 bytes)
794
/// occupy the first 2-3 cache lines (64 bytes each), which are loaded
795
/// first by the hardware prefetcher.
796
///
797
/// | Tier   | Fields                                  | ~Bytes | Accesses |
798
/// |--------|-----------------------------------------|--------|----------|
799
/// | HOT    | `box_props`, `dom_node_id`, children,       |  ~140  |  410+    |
800
/// |        | `used_size`, `formatting_context`, parent    |        |          |
801
/// | WARM   | `intrinsic_sizes..computed_style`          |  ~220  |  ~80     |
802
/// | COLD   | `dirty_flag..is_anonymous`                 |  ~190  |  ~20     |
803
///
804
/// Note: An absolute position is a final paint-time value and shouldn't be
805
/// cached on the node itself, as it can change even if the node's
806
/// layout is clean (e.g., if a sibling changes size). We will calculate
807
/// it in a separate map.
808
#[derive(Debug, Clone)]
809
#[repr(C)]
810
pub struct LayoutNode {
811
    // ── HOT tier: accessed on every node in every layout pass ────────────
812
    // These fields should fit in the first 2-3 cache lines (~128-192 bytes).
813

            
814
    /// The resolved box model properties (margin, border, padding)
815
    /// in logical pixels. Cached after first resolution.
816
    /// (148 accesses — hottest field)
817
    pub box_props: BoxProps,
818
    /// Reference back to the original DOM node (None for anonymous boxes)
819
    /// (111 accesses)
820
    pub dom_node_id: Option<NodeId>,
821
    /// Children indices in the layout tree
822
    /// (53 accesses)
823
    pub children: Vec<usize>,
824
    /// The size used during the last layout pass.
825
    /// (43 accesses)
826
    pub used_size: Option<LogicalSize>,
827
    /// The formatting context this node establishes or participates in.
828
    /// (30 accesses)
829
    pub formatting_context: FormattingContext,
830
    /// Parent index (None for root)
831
    /// (25 accesses)
832
    pub parent: Option<usize>,
833

            
834
    // ── WARM tier: frequently accessed but not on every node ─────────────
835

            
836
    /// Cached intrinsic sizes (min-content, max-content, etc.)
837
    /// (16 accesses — sizing pass only)
838
    pub intrinsic_sizes: Option<IntrinsicSizes>,
839
    // +spec:display-property:af3a89 - alignment baseline for inline-level boxes
840
    /// The baseline of this box, if applicable, measured from its content-box top edge.
841
    /// (14 accesses — IFC/table alignment)
842
    pub baseline: Option<f32>,
843
    /// Cached inline layout result with the constraints used to compute it.
844
    ///
845
    /// This field stores both the computed layout AND the constraints (available width,
846
    /// float state) under which it was computed. This is essential for correctness:
847
    /// 
848
    /// - Table cells are measured multiple times with different widths
849
    /// - Min-content/max-content intrinsic sizing uses special constraint values
850
    /// - The final layout must use the actual available width, not a measurement width
851
    ///
852
    /// By tracking the constraints, we avoid the bug where a min-content measurement
853
    /// (with width=0) would be incorrectly reused for final rendering.
854
    /// (13 accesses — IFC roots / table cells)
855
    /// BOXED (rare-data split, 2026-08-11): populated only on IFC roots /
856
    /// table cells; the 296-B inline Option was paid by EVERY node.
857
    pub inline_layout_result: Option<Box<CachedInlineLayout>>,
858
    /// See [`LayoutNodeWarm::inline_content_cache`]. BOXED likewise (88 B).
859
    pub inline_content_cache: Option<Box<CachedInlineContent>>,
860
    /// Cached scrollbar information (calculated during layout)
861
    /// Used to determine if scrollbars appeared/disappeared requiring reflow
862
    /// (12 accesses — scrollable containers only)
863
    pub scrollbar_info: Option<ScrollbarRequirements>,
864
    /// The position of this node *relative to its parent's content box*.
865
    /// (9 accesses — positioning pass)
866
    pub relative_position: Option<LogicalPosition>,
867
    /// The actual content size (children overflow size) for scrollable containers.
868
    /// This is the size of all content that might need to be scrolled, which can
869
    /// be larger than `used_size` when content overflows the container.
870
    /// (7 accesses — scrollable containers)
871
    pub overflow_content_size: Option<LogicalSize>,
872
    /// Cache for Taffy layout computations for this node.
873
    /// (6 accesses — Taffy bridge)
874
    pub taffy_cache: TaffyCache,
875
    /// Pure min-content / max-content measure results from the taffy bridge
876
    /// (`(min, max)` outer sizes), keyed by NOTHING — a pure content
877
    /// measurement is viewport-independent, so it stays valid until the
878
    /// node's content changes. Invalidated wherever `taffy_cache` is
879
    /// (the intrinsic-dirty ancestor closure). WHY separate from
880
    /// `taffy_cache`: taffy's slot classes collide under the flex
881
    /// algorithm's candidate-width probing (Definite(0)/Definite(w1)/…
882
    /// share a slot and evict each other within one pass), so min/max
883
    /// entries did not survive to the NEXT pass — 312 full min/max-content
884
    /// subtree re-layouts per steady resize on big.md, each re-breaking
885
    /// every IFC line in the subtree AND evicting the Definite entry of
886
    /// the single-slot IFC cache on the way (the 429 `text_layout_flow`
887
    /// calls). (2 accesses — Taffy bridge measure path)
888
    pub measured_content_sizes: (Option<LayoutOutput>, Option<LayoutOutput>),
889
    /// Pre-computed CSS properties needed during layout.
890
    /// Computed once during layout tree build to avoid repeated style lookups.
891
    /// (5 accesses — cache.rs only)
892
    pub computed_style: ComputedLayoutStyle,
893
    /// Pseudo-element type (`::marker`, `::before`, `::after`) if this node is a pseudo-element
894
    /// (5 accesses — pseudo-elements only)
895
    pub pseudo_element: Option<PseudoElement>,
896
    /// Escaped top margin (CSS 2.1 margin collapsing)
897
    /// If this BFC's first child's top margin "escaped" the BFC, this contains
898
    /// the collapsed margin that should be applied by the parent.
899
    /// (4 accesses — BFC margin collapsing)
900
    pub escaped_top_margin: Option<f32>,
901
    /// Escaped bottom margin (CSS 2.1 margin collapsing)\
902
    /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
903
    /// the collapsed margin that should be applied by the parent.
904
    /// (4 accesses)
905
    pub escaped_bottom_margin: Option<f32>,
906
    /// Parent's formatting context (needed to determine if stretch applies)
907
    /// (4 accesses — flex/grid children)
908
    pub parent_formatting_context: Option<FormattingContext>,
909
    /// If this node participates in an IFC (is inline content like text),
910
    /// stores the reference back to the IFC root and the run index.
911
    /// This allows text nodes to find their layout data in the parent's IFC.
912
    /// (3 accesses — text nodes only)
913
    pub ifc_membership: Option<IfcMembership>,
914
    /// The layout tree index of this node's containing block.
915
    /// - For abs-pos elements: nearest positioned (non-static) ancestor
916
    /// - For fixed elements: root / None (viewport)
917
    /// - For normal-flow: parent (None = implicit)
918
    ///   Used for clip exemption: abs-pos elements whose containing block
919
    ///   is above an overflow clipper should not be clipped.
920
    pub containing_block_index: Option<usize>,
921

            
922
    // ── COLD tier: construction / reconciliation / debugging only ────────
923

            
924
    /// Type of anonymous box (if applicable)
925
    /// (2 accesses)
926
    pub anonymous_type: Option<AnonymousBoxType>,
927
    /// O3-render: when this node is one PART of a structural-split PREVIEW,
928
    /// the flat byte range of the source node's inline content this part
929
    /// displays (`u32::MAX` end = to the end). The IFC build slices its
930
    /// collected content by this range. `None` = the node shows everything
931
    /// (the universal case outside previews).
932
    pub preview_byte_range: Option<(u32, u32)>,
933
    /// Multi-field fingerprint of this node's data (style, text, etc.)
934
    /// for granular change detection during reconciliation.
935
    /// (2 accesses — reconciliation only)
936
    pub node_data_fingerprint: NodeDataFingerprint,
937
    /// A hash of this node's data and all of its descendants. Used for
938
    /// fast reconciliation.
939
    /// (9 accesses — all in cache.rs reconciliation)
940
    pub subtree_hash: SubtreeHash,
941
    /// Dirty flags to track what needs recalculation.
942
    /// (7 accesses — reconciliation setup)
943
    pub dirty_flag: DirtyFlag,
944
    /// Unresolved box model properties (raw CSS values).
945
    /// These are resolved lazily during layout when containing block is known.
946
    /// (1 access — initial resolution only)
947
    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
948
    /// If this node is an IFC root, stores the IFC ID.
949
    /// Used to identify which IFC this node's `inline_layout_result` belongs to.
950
    /// (1 access — IFC creation only)
951
    pub ifc_id: Option<IfcId>,
952
}
953

            
954
/// Pre-computed CSS properties needed during layout.
955
/// 
956
/// This struct stores resolved CSS values that are frequently accessed during
957
/// layout calculations. By computing these once during layout tree construction,
958
/// we avoid O(n * m) style lookups where n = nodes and m = layout passes.
959
///
960
/// All values are resolved to their final form (no 'inherit', 'initial', etc.)
961
#[derive(Debug, Clone, Default)]
962
pub struct ComputedLayoutStyle {
963
    /// CSS `display` property
964
    pub display: LayoutDisplay,
965
    /// CSS `position` property
966
    pub position: LayoutPosition,
967
    /// CSS `float` property
968
    pub float: LayoutFloat,
969
    /// CSS `overflow-x` property
970
    pub overflow_x: LayoutOverflow,
971
    /// CSS `overflow-y` property
972
    pub overflow_y: LayoutOverflow,
973
    /// CSS `writing-mode` property
974
    pub writing_mode: azul_css::props::layout::LayoutWritingMode,
975
    /// CSS `direction` property (ltr/rtl)
976
    pub direction: azul_css::props::style::StyleDirection,
977
    /// CSS `text-orientation` property (for vertical writing modes)
978
    pub text_orientation: azul_css::props::style::effects::StyleTextOrientation,
979
    /// CSS `width` property (None = auto)
980
    pub width: Option<azul_css::props::layout::LayoutWidth>,
981
    /// CSS `height` property (None = auto)
982
    pub height: Option<azul_css::props::layout::LayoutHeight>,
983
    /// CSS `min-width` property
984
    pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
985
    /// CSS `min-height` property
986
    pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
987
    /// CSS `max-width` property
988
    pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
989
    /// CSS `max-height` property
990
    pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
991
    /// CSS `text-align` property
992
    pub text_align: StyleTextAlign,
993
}
994

            
995
// Note: LayoutNode methods that cross hot/warm/cold boundaries have been
996
// moved to LayoutTree methods (resolve_box_props, get_content_size).
997

            
998
/// CSS pseudo-elements that can be generated
999
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PseudoElement {
    /// `::marker` pseudo-element for list items
    Marker,
    /// `::before` pseudo-element
    Before,
    /// `::after` pseudo-element
    After,
}
// +spec:display-property:b7f4bf - anonymous inline/block boxes are both called "anonymous boxes"
/// Types of anonymous boxes that can be generated
// +spec:display-property:ae4f16 - anonymous boxes are treated as descendants alongside pseudo-elements
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnonymousBoxType {
    /// Anonymous block box wrapping inline content
    InlineWrapper,
    /// O3-render: the SECOND (or later) part of a structural-split preview —
    /// a box with a real `dom_node_id` (the split node, via the
    /// `dom_to_layout` multimap) that displays a byte-slice of its content.
    /// Exists only between recording a split and the app's re-render.
    SplitPreviewPart,
    /// Anonymous box for a list item marker (bullet or number)
    /// DEPRECATED: Use `PseudoElement::Marker` instead
    ListItemMarker,
    /// Anonymous table wrapper
    TableWrapper,
    /// Anonymous table row group (tbody)
    TableRowGroup,
    /// Anonymous table row
    TableRow,
    /// Anonymous table cell
    TableCell,
}
// =============================================================================
// SoA (struct-of-arrays) layout node split for cache performance
// =============================================================================
/// Hot layout node fields — accessed on every node in every layout pass.
///
/// Stored in a separate `Vec` for cache locality. At ~100 bytes per node,
/// 1000 nodes fit in ~100 KB (L2 cache), vs ~550 KB with the monolithic struct.
// ~100B per-node hot type stored/moved in Vecs across every layout pass; kept
// non-Copy on purpose so it isn't silently bulk-copied (Copy would mask the
// cost and churn the many `.clone()` call sites).
#[allow(missing_copy_implementations)]
#[derive(Debug, Clone)]
pub struct LayoutNodeHot {
    /// The resolved box model properties (margin, border, padding)
    /// Stored in packed i16×10 encoding to reduce cache footprint.
    /// Use `box_props.unpack()` to get f32 `ResolvedBoxProps` for computation.
    pub box_props: crate::solver3::geometry::PackedBoxProps,
    /// Reference back to the original DOM node (None for anonymous boxes)
    pub dom_node_id: Option<NodeId>,
    /// The size used during the last layout pass.
    pub used_size: Option<LogicalSize>,
    /// The formatting context this node establishes or participates in.
    pub formatting_context: FormattingContext,
    /// Parent index (None for root)
    pub parent: Option<usize>,
}
/// Warm layout node fields — accessed frequently but not on every node.
///
/// Stored in a separate `Vec`. These fields are accessed during specific
/// layout phases (sizing, IFC, table alignment) but not during the main
/// constraint-solving loop.
#[derive(Debug, Clone, Default)]
pub struct LayoutNodeWarm {
    /// Cached intrinsic sizes (min-content, max-content, etc.)
    pub intrinsic_sizes: Option<IntrinsicSizes>,
    /// The baseline of this box, measured from its content-box top edge.
    pub baseline: Option<f32>,
    /// Cached inline layout result with the constraints used to compute it.
    /// BOXED (rare-data split, 2026-08-11): only IFC roots populate this,
    /// but the 296-B inline Option was paid by EVERY node. 8 B here now.
    pub inline_layout_result: Option<Box<CachedInlineLayout>>,
    /// Cached result of `collect_and_measure_inline_content` for this IFC
    /// root, valid while the subtree's fingerprints are unchanged.
    ///
    /// Collection resolves the FULL cascade (`get_style_properties`) for
    /// every text run and inline span. That ran on every layout — even one
    /// that went on to reuse the cached line layout — and measured 2 ms per
    /// IFC, 32 ms per pagination: the single largest remaining cost after
    /// the line-breaker and font-signature fixes.
    /// BOXED (rare-data split): IFC roots only; was 88 B inline per node.
    pub inline_content_cache: Option<Box<CachedInlineContent>>,
    /// Cached scrollbar information
    pub scrollbar_info: Option<ScrollbarRequirements>,
    /// The position relative to parent's content box.
    pub relative_position: Option<LogicalPosition>,
    /// The actual content size for scrollable containers.
    pub overflow_content_size: Option<LogicalSize>,
    /// Cache for Taffy layout computations.
    pub taffy_cache: TaffyCache,
    /// Pure min/max-content measure results — see the facade field's doc.
    pub measured_content_sizes: (Option<LayoutOutput>, Option<LayoutOutput>),
    /// Pre-computed CSS properties needed during layout.
    pub computed_style: ComputedLayoutStyle,
    /// Pseudo-element type if this node is a pseudo-element
    pub pseudo_element: Option<PseudoElement>,
    /// Escaped top margin (CSS 2.1 margin collapsing)
    pub escaped_top_margin: Option<f32>,
    /// Escaped bottom margin (CSS 2.1 margin collapsing)
    pub escaped_bottom_margin: Option<f32>,
    /// Parent's formatting context
    pub parent_formatting_context: Option<FormattingContext>,
    /// IFC membership for text nodes
    pub ifc_membership: Option<IfcMembership>,
    /// Containing block index for clip exemption
    pub containing_block_index: Option<usize>,
}
/// Cold layout node fields — construction / reconciliation / debugging only.
///
/// Stored in a separate `Vec`. These fields are rarely accessed during layout;
/// mostly used during tree construction, reconciliation, and dirty tracking.
#[derive(Debug, Clone)]
#[derive(Default)]
pub struct LayoutNodeCold {
    /// Type of anonymous box (if applicable)
    pub anonymous_type: Option<AnonymousBoxType>,
    /// O3-render: when this node is one PART of a structural-split PREVIEW,
    /// the flat byte range of the source node's inline content this part
    /// displays (`u32::MAX` end = to the end). The IFC build slices its
    /// collected content by this range. `None` = the node shows everything
    /// (the universal case outside previews).
    pub preview_byte_range: Option<(u32, u32)>,
    /// Multi-field fingerprint for granular change detection.
    pub node_data_fingerprint: NodeDataFingerprint,
    /// Hash of this node's data + all descendants.
    pub subtree_hash: SubtreeHash,
    /// Dirty flags for recalculation tracking.
    pub dirty_flag: DirtyFlag,
    /// Unresolved box model properties (raw CSS values).
    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
    /// IFC ID if this node is an IFC root.
    pub ifc_id: Option<IfcId>,
}
impl LayoutNode {
    /// Split this full layout node into hot/warm/cold components.
    /// Used during `LayoutTreeBuilder::build()` to create the `SoA` layout.
222665
    #[must_use] pub fn split(self) -> (LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold) {
222665
        (
222665
            LayoutNodeHot {
222665
                box_props: crate::solver3::geometry::PackedBoxProps::pack(&self.box_props),
222665
                dom_node_id: self.dom_node_id,
222665
                used_size: self.used_size,
222665
                formatting_context: self.formatting_context,
222665
                parent: self.parent,
222665
            },
222665
            LayoutNodeWarm {
222665
                intrinsic_sizes: self.intrinsic_sizes,
222665
                baseline: self.baseline,
222665
                inline_layout_result: self.inline_layout_result,
222665
                inline_content_cache: self.inline_content_cache,
222665
                scrollbar_info: self.scrollbar_info,
222665
                relative_position: self.relative_position,
222665
                overflow_content_size: self.overflow_content_size,
222665
                taffy_cache: self.taffy_cache,
222665
                measured_content_sizes: self.measured_content_sizes,
222665
                computed_style: self.computed_style,
222665
                pseudo_element: self.pseudo_element,
222665
                escaped_top_margin: self.escaped_top_margin,
222665
                escaped_bottom_margin: self.escaped_bottom_margin,
222665
                parent_formatting_context: self.parent_formatting_context,
222665
                ifc_membership: self.ifc_membership,
222665
                containing_block_index: self.containing_block_index,
222665
            },
222665
            LayoutNodeCold {
222665
                anonymous_type: self.anonymous_type,
222665
                preview_byte_range: self.preview_byte_range,
222665
                node_data_fingerprint: self.node_data_fingerprint,
222665
                subtree_hash: self.subtree_hash,
222665
                dirty_flag: self.dirty_flag,
222665
                unresolved_box_props: self.unresolved_box_props,
222665
                ifc_id: self.ifc_id,
222665
            },
222665
        )
222665
    }
}
/// How far [`LayoutTree::get_ifc_root_layout_index`] walks up looking for the
/// box that owns an inline descendant's layout. Inline nesting is shallow in
/// practice; the bound only stops a malformed tree from spinning.
const IFC_ANCESTOR_WALK_LIMIT: usize = 64;
/// A position in the LAYOUT tree — distinct from a DOM [`NodeId`] BY TYPE.
///
/// The layout tree interleaves anonymous boxes and splits during
/// construction, so layout indices and DOM indices diverge for any real DOM.
/// Conflating them (`node_id.index()` used as a layout index) froze CPU-path
/// scrolling once (`build_scroll_offset_map`) and is exactly the mistake this
/// newtype turns into a compile error: the ONLY sanctioned ways to obtain a
/// `LayoutNodeId` from a DOM node are [`LayoutTree::dom_to_layout`] and the
/// scroll-id tables. Solver-interior code that produces indices locally wraps
/// them with [`LayoutNodeId::new`] — an explicit, greppable assertion that
/// the value really is a layout index.
///
/// The raw `SoA` vectors (`nodes`, `warm`, `cold`, `children_arena`) stay
/// `usize`-indexed for interior arithmetic; the typed boundary is the
/// accessor surface (`get`/`warm`/`cold`/`get_content_size`/...), which is
/// what map-derived consumer code calls.
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LayoutNodeId(pub usize);
impl LayoutNodeId {
    #[inline]
    #[must_use]
61774251
    pub const fn new(index: usize) -> Self {
61774251
        Self(index)
61774251
    }
    /// The raw vector index. Greppable by design: every `.index()` is a
    /// deliberate exit from the typed space.
    #[inline]
    #[must_use]
62300762
    pub const fn index(self) -> usize {
62300762
        self.0
62300762
    }
}
impl core::fmt::Display for LayoutNodeId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "L{}", self.0)
    }
}
/// The complete layout tree structure.
///
/// Uses a struct-of-arrays (`SoA`) layout for cache performance:
/// - `nodes` (hot): accessed on every node in every layout pass
/// - `warm`: accessed during specific layout phases
/// - `cold`: construction / reconciliation only
#[derive(Debug, Clone)]
pub struct LayoutTree {
    /// Hot layout data — box props, parent, `used_size`, formatting context
    pub nodes: Vec<LayoutNodeHot>,
    /// Warm layout data — intrinsic sizes, baseline, inline layout, etc.
    pub warm: Vec<LayoutNodeWarm>,
    /// Cold layout data — dirty flags, fingerprints, reconciliation data
    pub cold: Vec<LayoutNodeCold>,
    /// Root node index
    pub root: usize,
    /// Mapping from DOM node IDs to layout node indices
    // BTreeMap (not HashMap): std HashMap's RandomState hasher needs an RNG seed
    // that isn't available in the remill-lifted wasm (no getrandom), so inserts
    // silently no-op there — dom_to_layout came back empty (node mapping lost,
    // get_node_size/position returned None → 0-rects). BTreeMap is deterministic,
    // matches the rest of azul-core, and lifts reliably (M12.7).
    pub dom_to_layout: BTreeMap<NodeId, Vec<LayoutNodeId>>,
    /// Flat arena holding all children indices contiguously.
    pub children_arena: Vec<usize>,
    /// Per-node (start, len) into `children_arena`. Indexed by node index.
    pub children_offsets: Vec<(u32, u32)>,
    /// Per-node bit: this node or any descendant establishes a shrink-to-fit
    /// (STF) context whose sizing algorithm reads children's intrinsic sizes
    /// (flex/grid/table/inline-block containers, floats, or abspos elements).
    ///
    /// If `subtree_needs_intrinsic[i]` is false AND no ancestor of `i` is STF
    /// either, the intrinsic sizing pass can skip the entire subtree — nothing
    /// will ever read those values. This is the static-DOM optimization from
    /// §58 Win #3 (the "safely re-enabled Fix C").
    ///
    /// Computed once at tree build time in `generate_layout_tree`. An empty
    /// vec means "assume every subtree needs intrinsics" (safe fallback for
    /// code paths that construct `LayoutTree` without going through the
    /// builder — currently none, but preserves the invariant for tests).
    pub subtree_needs_intrinsic: Vec<bool>,
}
/// Approximate per-field heap-byte breakdown of a [`LayoutTree`].
#[derive(Copy, Debug, Clone, Default)]
pub struct LayoutTreeMemoryReport {
    pub node_count: usize,
    pub hot_bytes: usize,
    pub warm_bytes: usize,
    pub warm_inline_layout_bytes: usize,
    /// Shaped clusters retained across the whole tree. A cluster is one
    /// grapheme, so on Latin text this is roughly the CHARACTER COUNT of
    /// everything laid out — the number that explains `warm.inline`.
    pub shaped_cluster_count: usize,
    /// Shaped glyphs retained. Differs from the cluster count only for
    /// ligatures and combining marks.
    pub shaped_glyph_count: usize,
    /// Bytes of per-cluster heap `String` (each cluster owns a copy of its
    /// own text). Called out separately because it is pure duplication of
    /// text the DOM already holds.
    pub shaped_cluster_text_bytes: usize,
    /// Distinct `Arc<StyleProperties>` allocations the retained glyphs point
    /// at, by pointer identity.
    ///
    /// A document has a handful of distinct text styles (body, headings,
    /// code), so a healthy number here is single digits no matter how many
    /// glyphs there are. A count that tracks the GLYPH count means the
    /// sharing predicate has silently stopped sharing — Stylo hit exactly
    /// this, ending up with 109k `ComputedValues` where 2,200 were expected
    /// because one pseudo-element rule defeated the predicate. At 248 B per
    /// `StyleProperties` that failure is invisible and expensive.
    pub distinct_glyph_style_arcs: usize,
    /// Paint-run cache (`CachedInlineLayout::glyph_runs`): heap bytes of the
    /// retained `Vec<SimpleGlyphRun>` lists, each distinct Arc counted once
    /// (`GlyphSwap` reuse shares them). This is the store-time derivation that
    /// makes every paint a lookup — the retained cost of that trade.
    pub glyph_run_bytes: usize,
    /// Distinct `SimpleGlyphRun`s behind `glyph_run_bytes`.
    pub glyph_run_count: usize,
    /// Total `GlyphInstance`s retained across those runs (the 20 B/each bulk).
    pub glyph_instance_count: usize,
    pub warm_taffy_cache_bytes: usize,
    pub cold_bytes: usize,
    pub dom_to_layout_bytes: usize,
    pub children_arena_bytes: usize,
    pub children_offsets_bytes: usize,
}
impl LayoutTreeMemoryReport {
6
    #[must_use] pub const fn total_bytes(&self) -> usize {
6
        self.hot_bytes
6
            + self.warm_bytes
6
            + self.warm_inline_layout_bytes
6
            + self.glyph_run_bytes
6
            + self.warm_taffy_cache_bytes
6
            + self.cold_bytes
6
            + self.dom_to_layout_bytes
6
            + self.children_arena_bytes
6
            + self.children_offsets_bytes
6
    }
}
impl LayoutTree {
    /// Approximate heap bytes retained by this `LayoutTree`.
11
    #[must_use] pub fn memory_report(&self) -> LayoutTreeMemoryReport {
11
        let mut report = LayoutTreeMemoryReport {
11
            node_count: self.nodes.len(),
11
            hot_bytes: self.nodes.capacity() * size_of::<LayoutNodeHot>(),
11
            warm_bytes: self.warm.capacity() * size_of::<LayoutNodeWarm>(),
11
            cold_bytes: self.cold.capacity() * size_of::<LayoutNodeCold>(),
11
            children_arena_bytes: self.children_arena.capacity() * size_of::<usize>(),
11
            children_offsets_bytes: self.children_offsets.capacity() * size_of::<(u32, u32)>(),
11
            dom_to_layout_bytes: 0,
11
            warm_inline_layout_bytes: 0,
11
            shaped_cluster_count: 0,
11
            shaped_glyph_count: 0,
11
            shaped_cluster_text_bytes: 0,
11
            distinct_glyph_style_arcs: 0,
11
            glyph_run_bytes: 0,
11
            glyph_run_count: 0,
11
            glyph_instance_count: 0,
11
            warm_taffy_cache_bytes: 0,
11
        };
        // HashMap<NodeId, Vec<usize>> — approximate: (key + Vec-header) per entry
        // plus heap for each inner Vec.
11
        let entries = self.dom_to_layout.len();
11
        report.dom_to_layout_bytes = entries * (size_of::<NodeId>() + size_of::<Vec<usize>>());
11
        for v in self.dom_to_layout.values() {
9
            report.dom_to_layout_bytes += v.capacity() * size_of::<usize>();
9
        }
        // Inline layout data lives behind Arc — count Arc heap-shares once
        // per node that has a cached layout. Counted conservatively.
11
        let mut text_arcs: alloc::collections::BTreeSet<usize> =
11
            alloc::collections::BTreeSet::new();
11
        let mut style_arcs: alloc::collections::BTreeSet<usize> =
11
            alloc::collections::BTreeSet::new();
11
        let mut run_arcs: alloc::collections::BTreeSet<usize> =
11
            alloc::collections::BTreeSet::new();
32
        for w in &self.warm {
21
            if let Some(cached) = &w.inline_layout_result {
                // The Box allocation itself — CachedInlineLayout is boxed off
                // LayoutNodeWarm, so `warm_bytes` above does NOT include it.
                // This also covers the INLINE `Option<UnifiedConstraints>`.
7
                report.warm_inline_layout_bytes += size_of::<CachedInlineLayout>();
                // Heap Vecs hanging off the inline constraints.
7
                if let Some(c) = &cached.constraints {
                    report.warm_inline_layout_bytes += c.shape_boundaries.capacity()
                        * size_of::<crate::text3::cache::ShapeBoundary>()
                        + c.shape_exclusions.capacity()
                            * size_of::<crate::text3::cache::ShapeBoundary>();
7
                }
                // Cached line breaks (edit fast-path).
7
                if let Some(lb) = &cached.line_breaks {
                    report.warm_inline_layout_bytes +=
                        lb.line_ranges.capacity() * size_of::<(usize, usize)>()
                            + lb.line_widths.capacity() * size_of::<f32>();
7
                }
                // The payload Arc: when it is a TextPayload wrapper, that
                // allocation holds two Arc CLONES of buffers counted
                // elsewhere — only its own header is new bytes. When it is
                // the layout Arc itself (flag-off), nothing new to count.
7
                if cached.payload.downcast_ref::<TextPayload>().is_some() {
7
                    report.warm_inline_layout_bytes += size_of::<TextPayload>();
7
                }
                // Paint-run cache — its own report line (see the field doc).
                // Distinct Arcs only: GlyphSwap reuse clones the Arc.
7
                if run_arcs
7
                    .insert(Arc::as_ptr(&cached.glyph_runs).cast::<u8>() as usize)
                {
6
                    report.glyph_run_bytes += cached.glyph_runs.capacity()
6
                        * size_of::<crate::text3::glyphs::CompactGlyphRun>();
6
                    for run in cached.glyph_runs.iter() {
4
                        report.glyph_run_bytes += run.glyphs.retained_bytes()
4
                            + run.background_content.capacity()
4
                                * size_of::<azul_css::props::style::StyleBackgroundContent>();
4
                        report.glyph_run_count += 1;
4
                        report.glyph_instance_count += run.glyphs.len();
4
                    }
1
                }
                // Arc<UnifiedLayout> — count the UnifiedLayout header + its items.
7
                report.warm_inline_layout_bytes += size_of::<UnifiedLayout>();
7
                report.warm_inline_layout_bytes += cached.layout.items.capacity()
7
                    * size_of::<crate::text3::cache::PositionedItem>();
7
                report.warm_inline_layout_bytes += cached.item_metrics.capacity()
7
                    * size_of::<InlineItemMetrics>();
                // (d1): the retained dense view, when AZ_DENSE_TEXT keeps it.
                // Counted per-field so the sparse-vs-dense split is honest
                // during the reader migration (both alive = the transitional
                // cost; the sparse side retires at the end of (d)).
7
                if let Some(d) = &cached.dense {
                    use crate::text3::dense as dn;
7
                    report.warm_inline_layout_bytes += size_of::<dn::DenseText>()
7
                        + d.clusters.capacity() * size_of::<dn::ClusterCompact>()
7
                        + d.runs.capacity() * size_of::<dn::DenseRun>()
7
                        + d.lines.capacity() * size_of::<dn::LineRecord>()
7
                        + d.details.capacity() * size_of::<dn::ClusterDetail>()
7
                        + d.detail_glyphs.capacity() * size_of::<dn::DetailGlyph>();
                }
                // Glyphs and per-cluster text.
                //
                // ONLY the SPILLED glyphs are counted here. `glyphs` is a
                // `SmallVec<[ShapedGlyph; 1]>`, so its first element lives
                // INSIDE the ShapedCluster — which is already counted as part
                // of `PositionedItem` above. Multiplying `capacity()` (which
                // includes the inline slot) by `size_of::<ShapedGlyph>()`
                // double-counted one 96-byte glyph for EVERY cluster: on a
                // 31k-cluster document that inflated the report by ~3 MB, and
                // a memory report that over-reports sends the optimisation
                // work at the wrong target.
7
                for item in &cached.layout.items {
2
                    if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
                        let spilled = c.glyphs.capacity().saturating_sub(1);
                        report.warm_inline_layout_bytes +=
                            spilled * size_of::<crate::text3::cache::ShapedGlyph>();
                        // 3c: per-cluster text is a SHARED Arc slice now —
                        // attribute each distinct source Arc's buffer ONCE.
                        if text_arcs.insert(Arc::as_ptr(&c.source_text).cast::<u8>() as usize) {
                            report.warm_inline_layout_bytes += c.source_text.len();
                            report.shaped_cluster_text_bytes += c.source_text.len();
                        }
                        report.shaped_cluster_count += 1;
                        report.shaped_glyph_count += c.glyphs.len();
                        style_arcs.insert(Arc::as_ptr(&c.style) as usize);
2
                    }
                }
14
            }
            // Taffy cache — each slot is an Option, ~50 B empty
21
            report.warm_taffy_cache_bytes += size_of::<TaffyCache>();
        }
11
        report.distinct_glyph_style_arcs = style_arcs.len();
11
        report
11
    }
    /// Returns the children of node `index` as a contiguous slice from the arena.
    #[inline]
4177900
    #[must_use] pub fn children(&self, index: usize) -> &[usize] {
4177900
        if let Some(&(start, len)) = self.children_offsets.get(index) {
4177893
            &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
        } else {
7
            &[]
        }
4177900
    }
    /// Get hot layout data for a node (`box_props`, `dom_node_id`, `used_size`, etc.)
    #[inline]
44205872
    #[must_use] pub fn get(&self, index: LayoutNodeId) -> Option<&LayoutNodeHot> {
44205872
        self.nodes.get(index.index())
44205872
    }
    /// Get mutable hot layout data for a node.
    #[inline]
1887145
    pub fn get_mut(&mut self, index: LayoutNodeId) -> Option<&mut LayoutNodeHot> {
1887145
        self.nodes.get_mut(index.index())
1887145
    }
    /// Get warm layout data for a node (`intrinsic_sizes`, baseline, `inline_layout`, etc.)
    #[inline]
11754645
    #[must_use] pub fn warm(&self, index: LayoutNodeId) -> Option<&LayoutNodeWarm> {
11754645
        self.warm.get(index.index())
11754645
    }
    /// Get mutable warm layout data for a node.
    #[inline]
2066563
    pub fn warm_mut(&mut self, index: LayoutNodeId) -> Option<&mut LayoutNodeWarm> {
2066563
        self.warm.get_mut(index.index())
2066563
    }
    /// Get cold layout data for a node (`dirty_flag`, `subtree_hash`, fingerprint, etc.)
    #[inline]
770490
    #[must_use] pub fn cold(&self, index: LayoutNodeId) -> Option<&LayoutNodeCold> {
770490
        self.cold.get(index.index())
770490
    }
    /// Get mutable cold layout data for a node.
    #[inline]
50965
    pub fn cold_mut(&mut self, index: LayoutNodeId) -> Option<&mut LayoutNodeCold> {
50965
        self.cold.get_mut(index.index())
50965
    }
1
    fn root_node(&self) -> &LayoutNodeHot {
1
        &self.nodes[self.root]
1
    }
    /// Reconstruct a full `LayoutNode` from the split hot/warm/cold arrays.
    ///
    /// Used when passing node data to `LayoutTreeBuilder::clone_node_from_old()`.
39223
    #[must_use] pub fn get_full_node(&self, index: usize) -> Option<LayoutNode> {
39223
        let hot = self.nodes.get(index)?;
39221
        let warm = self.warm.get(index).cloned().unwrap_or_default();
39221
        let cold = self.cold.get(index).cloned().unwrap_or_default();
39221
        let children = self.children(index).to_vec();
39221
        Some(LayoutNode {
39221
            box_props: hot.box_props.unpack(),
39221
            dom_node_id: hot.dom_node_id,
39221
            children,
39221
            used_size: hot.used_size,
39221
            formatting_context: hot.formatting_context,
39221
            parent: hot.parent,
39221
            intrinsic_sizes: warm.intrinsic_sizes,
39221
            baseline: warm.baseline,
39221
            inline_layout_result: warm.inline_layout_result,
39221
            inline_content_cache: warm.inline_content_cache,
39221
            scrollbar_info: warm.scrollbar_info,
39221
            relative_position: warm.relative_position,
39221
            overflow_content_size: warm.overflow_content_size,
39221
            taffy_cache: warm.taffy_cache,
39221
            measured_content_sizes: warm.measured_content_sizes,
39221
            computed_style: warm.computed_style,
39221
            pseudo_element: warm.pseudo_element,
39221
            escaped_top_margin: warm.escaped_top_margin,
39221
            escaped_bottom_margin: warm.escaped_bottom_margin,
39221
            parent_formatting_context: warm.parent_formatting_context,
39221
            ifc_membership: warm.ifc_membership,
39221
            containing_block_index: warm.containing_block_index,
39221
            anonymous_type: cold.anonymous_type,
39221
            preview_byte_range: cold.preview_byte_range,
39221
            node_data_fingerprint: cold.node_data_fingerprint,
39221
            subtree_hash: cold.subtree_hash,
39221
            dirty_flag: cold.dirty_flag,
39221
            unresolved_box_props: cold.unresolved_box_props,
39221
            ifc_id: cold.ifc_id,
39221
        })
39223
    }
    /// Re-resolve box properties for a node with the actual containing block size.
40201
    pub(crate) fn resolve_box_props(
40201
        &mut self,
40201
        node_index: usize,
40201
        containing_block: LogicalSize,
40201
        viewport_size: LogicalSize,
40201
        element_font_size: f32,
40201
        root_font_size: f32,
40201
    ) {
40201
        let params = crate::solver3::geometry::ResolutionParams {
40201
            containing_block,
40201
            viewport_size,
40201
            element_font_size,
40201
            root_font_size,
40201
        };
40201
        if let (Some(hot), Some(cold)) = (self.nodes.get_mut(node_index), self.cold.get(node_index)) {
40199
            hot.box_props = crate::solver3::geometry::PackedBoxProps::pack(&cold.unresolved_box_props.resolve(&params));
40199
        }
40201
    }
    /// Marks a node and its ancestors as dirty with the given flag.
12
    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
12
        if flag == DirtyFlag::None {
1
            return;
11
        }
11
        let mut current_index = Some(start_index);
30
        while let Some(index) = current_index {
25
            let Some(cold) = self.cold.get_mut(index) else {
2
                break;
            };
23
            if cold.dirty_flag >= flag {
4
                break;
19
            }
19
            cold.dirty_flag = flag;
19
            current_index = self.nodes.get(index).and_then(|n| n.parent);
        }
12
    }
    /// Marks a node and its entire subtree of descendants with the given dirty flag.
6
    fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
6
        if flag == DirtyFlag::None {
1
            return;
5
        }
5
        let mut stack = vec![start_index];
21
        while let Some(index) = stack.pop() {
16
            let children = self.children(index).to_vec();
16
            if let Some(cold) = self.cold.get_mut(index) {
15
                if cold.dirty_flag < flag {
11
                    cold.dirty_flag = flag;
11
                }
15
                stack.extend_from_slice(&children);
1
            }
        }
6
    }
    /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
2
    fn clear_all_dirty_flags(&mut self) {
6
        for cold in &mut self.cold {
4
            cold.dirty_flag = DirtyFlag::None;
4
        }
2
    }
    /// Get inline layout for a node, navigating through IFC membership if needed.
    /// (d4) The dense view for a node, resolved through the SAME IFC
    /// membership walk as [`Self::get_inline_layout_for_node`]. `None`
    /// when no dense view is retained (flag off) or the node has no IFC.
651
    #[must_use] pub fn get_dense_for_node(&self, layout_index: usize) -> Option<&Arc<crate::text3::dense::DenseText>> {
651
        let warm = self.warm.get(layout_index)?;
651
        if let Some(cached) = &warm.inline_layout_result {
622
            return cached.dense.as_ref();
29
        }
29
        if let Some(ifc_membership) = &warm.ifc_membership {
29
            let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
29
            if let Some(cached) = &ifc_root_warm.inline_layout_result {
29
                return cached.dense.as_ref();
            }
        }
        None
651
    }
    /// (d6h) The stored inline layout for a node, MATERIALIZED when it
    /// is the retirement sentinel: dense expands (exact by the d6h
    /// expansion gate) into a transient copy for geometry readers —
    /// the caret and selection painters. Flag-off / verify / non-pure
    /// layouts return the stored Arc untouched.
    #[must_use]
672258
    pub fn materialized_inline_layout_for_node(
672258
        &self,
672258
        layout_index: usize,
672258
    ) -> Option<Arc<UnifiedLayout>> {
672258
        let warm = self.warm(LayoutNodeId::new(layout_index))?;
672258
        let cached = warm.inline_layout_result.as_ref()?;
292539
        if cached.layout.items.is_empty() {
292215
            if let Some(d) = cached.dense.as_deref() {
292215
                if !d.clusters.is_empty() {
292197
                    return Some(Arc::new(UnifiedLayout {
292197
                        items: d.to_unified_items(),
292197
                        overflow: cached.overflow.clone(),
292197
                    }));
18
                }
            }
324
        }
342
        Some(cached.layout.clone())
672258
    }
201
    #[must_use] pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&Arc<UnifiedLayout>> {
201
        let warm = self.warm.get(layout_index)?;
        // First, check if this node has its own inline_layout_result (it's an IFC root)
196
        if let Some(cached) = &warm.inline_layout_result {
163
            return Some(cached.get_layout());
33
        }
        // For text nodes, check if they have ifc_membership pointing to the IFC root
33
        if let Some(ifc_membership) = &warm.ifc_membership {
32
            let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
31
            if let Some(cached) = &ifc_root_warm.inline_layout_result {
30
                return Some(cached.get_layout());
1
            }
1
        }
2
        None
201
    }
    /// Return the layout index of the IFC root that owns `layout_index`'s inline content.
    /// If the node IS an IFC root (has its own `inline_layout_result`) or has no
    /// `ifc_membership`, returns `layout_index` unchanged. Inline text nodes never get
    /// their own box position (it stays the `f32::MIN` sentinel) — their geometry lives
    /// in the IFC root's content box, so selection/inline painting must anchor to the
    /// IFC root's position, not the text node's. See `get_inline_layout_for_node`.
300042
    #[must_use] pub fn get_ifc_root_layout_index(&self, layout_index: usize) -> usize {
300042
        if let Some(warm) = self.warm.get(layout_index) {
300040
            if warm.inline_layout_result.is_none() {
4474
                if let Some(ifc_membership) = &warm.ifc_membership {
4455
                    return ifc_membership.ifc_root_layout_index;
19
                }
                // No membership recorded. `ifc_membership` is only ever
                // assigned to text nodes the IFC root's own walk collected
                // directly; `collect_inline_span_recursive` assigns none, so
                // anything under a `<span>` carries nothing and ownership used
                // to resolve to the text node ITSELF. That is why the caret in
                // `p > span > text` was never painted, and why keyboard focus
                // into any editable containing a span produced a session whose
                // caret never appeared.
                //
                // The nearest ancestor that OWNS inline layout is the IFC root
                // by definition, so walk up to it. Nodes outside any IFC have
                // no such ancestor and fall through to the identity below.
19
                let mut cursor = layout_index;
37
                for _ in 0..IFC_ANCESTOR_WALK_LIMIT {
37
                    let Some(parent) = self.nodes.get(cursor).and_then(|n| n.parent) else {
1
                        break;
                    };
36
                    match self.warm.get(parent) {
36
                        Some(w) if w.inline_layout_result.is_some() => return parent,
18
                        Some(_) => cursor = parent,
                        None => break,
                    }
                }
295566
            }
2
        }
295569
        layout_index
300042
    }
    /// Get the content size of a node (for scrollbar calculations).
992055
    #[must_use] pub fn get_content_size(&self, index: LayoutNodeId) -> LogicalSize {
992055
        let Some(warm) = self.warm.get(index.index()) else {
3
            return LogicalSize::default();
        };
992052
        if let Some(content_size) = warm.overflow_content_size {
704101
            return content_size;
287951
        }
287951
        let Some(hot) = self.nodes.get(index.index()) else {
            return LogicalSize::default();
        };
287951
        let mut content_size = hot.used_size.unwrap_or_default();
287951
        if let Some(ref cached_layout) = warm.inline_layout_result {
2
            let text_layout = &cached_layout.layout;
2
            let mut max_x: f32 = 0.0;
2
            let mut max_y: f32 = 0.0;
2
            for positioned_item in &text_layout.items {
2
                let item_bounds = positioned_item.item.bounds();
2
                max_x = max_x.max(positioned_item.position.x + item_bounds.width);
2
                max_y = max_y.max(positioned_item.position.y + item_bounds.height);
2
            }
2
            content_size.width = content_size.width.max(max_x);
2
            content_size.height = content_size.height.max(max_y);
287949
        }
287951
        content_size
992055
    }
}
/// Generate layout tree from styled DOM with proper anonymous box generation
/// # Errors
///
/// Returns a `LayoutError` if the layout tree cannot be built.
209
pub fn generate_layout_tree<T: ParsedFontTrait>(
209
    ctx: &mut LayoutContext<'_, T>,
209
) -> Result<LayoutTree> {
209
    let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
209
    let root_id = ctx
209
        .styled_dom
209
        .root
209
        .into_crate_internal()
209
        .unwrap_or(NodeId::ZERO);
209
    let root_index =
209
        builder.process_node(ctx.styled_dom, root_id, None, ctx.debug_messages)?;
209
    builder.apply_split_previews(ctx.content_overlay, ctx.styled_dom);
209
    let mut layout_tree = builder.build(root_index);
    // Pre-compute the STF (shrink-to-fit) subtree bitmap. This is static-DOM
    // information: whether a subtree establishes any shrink-to-fit context
    // depends only on the DOM structure + formatting context, both of which
    // are frozen from here until the next layout-tree rebuild. The intrinsic
    // sizing pass reads this to skip subtrees whose intrinsics are never
    // consumed (§58 Win #3).
209
    layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
209
    debug_log!(
131
        ctx,
131
        "Generated layout tree with {} nodes (incl. anonymous)",
131
        layout_tree.nodes.len()
    );
209
    Ok(layout_tree)
209
}
/// Returns true if `(dom_node_id, fc)` establishes a formatting context whose
/// sizing algorithm reads children's intrinsic sizes. Covers:
/// - flex containers (flex item sizing uses child min/max-content),
/// - grid containers (grid-track sizing likewise),
/// - tables and table cells,
/// - inline-block (its own width may be shrink-to-fit),
/// - floats and abspos elements (their `auto` width resolves to shrink-to-fit).
///
/// A `FormattingContext::Block` with a definite CSS width is NOT shrink-to-fit —
/// its inner layout gets the width top-down, so descendant intrinsics don't
/// feed back up. That's the path Fix C short-circuits.
188765
pub(crate) fn is_shrink_to_fit_context(
188765
    styled_dom: &StyledDom,
188765
    dom_node_id: Option<NodeId>,
188765
    fc: FormattingContext,
188765
) -> bool {
    use crate::solver3::getters::{get_float, MultiValue};
    use crate::solver3::positioning::get_position_type;
    use azul_css::props::layout::{LayoutFloat, LayoutPosition};
188765
    match fc {
        FormattingContext::Flex
        | FormattingContext::Grid
        | FormattingContext::Table
55889
        | FormattingContext::InlineBlock => return true,
132876
        _ => {}
    }
132876
    let Some(dom_id) = dom_node_id else { return false; };
132710
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
132710
    let float_val = match get_float(styled_dom, dom_id, node_state) {
132704
        MultiValue::Exact(v) => v,
6
        _ => LayoutFloat::None,
    };
132710
    if float_val != LayoutFloat::None {
541
        return true;
132169
    }
132169
    let pos = get_position_type(styled_dom, Some(dom_id));
132169
    if pos == LayoutPosition::Absolute || pos == LayoutPosition::Fixed {
        // Abspos only becomes shrink-to-fit when width is `auto`.
        // Being conservative: treat as STF whenever abspos so we still
        // compute intrinsics for the auto-width case. Misses no work.
2668
        return true;
129501
    }
129501
    false
188765
}
/// Per-node bitmap of "this node or any descendant establishes a shrink-to-fit
/// context." Post-order walk: `out[i] = self_stf(i) || any(out[child_of_i])`.
/// Layout tree nodes are built top-down (pre-order), so iterating from the end
/// visits children before parents.
1482
fn compute_subtree_needs_intrinsic(
1482
    styled_dom: &StyledDom,
1482
    tree: &LayoutTree,
1482
) -> Vec<bool> {
1482
    let n = tree.nodes.len();
1482
    let mut out = vec![false; n];
18417
    for idx in (0..n).rev() {
18417
        let hot = &tree.nodes[idx];
18417
        let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, hot.formatting_context);
18417
        let mut any = self_stf;
18417
        if !any {
17755
            for &child in tree.children(idx) {
16057
                if out.get(child).copied().unwrap_or(false) {
1071
                    any = true;
1071
                    break;
14986
                }
            }
662
        }
18417
        out[idx] = any;
    }
1482
    out
1482
}
/// Incrementally builds a [`LayoutTree`] from a [`StyledDom`].
///
/// Usage: create via [`LayoutTreeBuilder::new`], call [`process_node`](Self::process_node)
/// on the root DOM node, then call [`build`](Self::build) to produce the final
/// SoA-split `LayoutTree`. During `process_node`, anonymous boxes are generated
/// as required by CSS 2.2 §9.2.1.1 (inline wrappers) and §17.2.1 (table fixup).
#[derive(Debug)]
pub struct LayoutTreeBuilder {
    nodes: Vec<LayoutNode>,
    dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
    viewport_size: LogicalSize,
}
impl LayoutTreeBuilder {
6465
    #[must_use] pub const fn new(viewport_size: LogicalSize) -> Self {
6465
        Self {
6465
            nodes: Vec::new(),
6465
            dom_to_layout: BTreeMap::new(),
6465
            viewport_size,
6465
        }
6465
    }
561432
    #[must_use] pub fn get(&self, index: usize) -> Option<&LayoutNode> {
561432
        self.nodes.get(index)
561432
    }
202687
    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
202687
        self.nodes.get_mut(index)
202687
    }
    // +spec:display-property:2188b7 - builds box tree: each element's principal box is child of nearest ancestor's principal box, with anonymous boxes for tables/inline wrapping
    /// Main entry point for recursively building the layout tree.
    /// This function dispatches to specialized handlers based on the node's
    /// `display` property to correctly generate anonymous boxes.
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
18099
    fn process_node(
18099
        &mut self,
18099
        styled_dom: &StyledDom,
18099
        dom_id: NodeId,
18099
        parent_idx: Option<usize>,
18099
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
18099
    ) -> Result<usize> {
18099
        let node_data = &styled_dom.node_data.as_container()[dom_id];
18099
        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
18099
        let raw_display = get_display_type(styled_dom, dom_id);
        // +spec:display-property:042f56 - replaced elements with layout-internal display use inline
        // CSS Display 3 §2.4: "When the display property of a replaced element computes to
        // one of the layout-internal values, it is handled as having a used value of inline."
18099
        let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
            LayoutDisplay::Inline
        } else {
18099
            raw_display
        };
        // +spec:display-property:0b40af - display/position/float interaction per CSS 2.2 §9.7
        // +spec:display-property:ba53ba - float!=none or position!=static causes display to blockify
        // +spec:positioning:69468c - absolute/fixed blockifies the box, float computes to none
        // +spec:table-layout:cfc60a - CSS 2.2 §9.7: display/position/float interaction
        // Blockification rules (CSS Display 3 §2.7 / §2.8):
        // 1. Root element → blockify
        // 2. position:absolute or position:fixed → float computes to 'none', blockify
        // 3. float is not 'none' → blockify
        // 4. Flex/Grid children → blockify
18099
        let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
18099
        let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
18099
        let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
18099
        let is_floated = node_float != LayoutFloat::None;
18099
        let is_root = parent_idx.is_none();
        // Per CSS 2.2 §9.7: if position is absolute or fixed, float computes to 'none'
18099
        if is_absolute_or_fixed && is_floated {
            if let Some(node) = self.nodes.get_mut(node_idx) {
                node.computed_style.float = LayoutFloat::None;
            }
18099
        }
18099
        let is_flex_grid_child = parent_idx
18099
            .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
18099
            .unwrap_or(false);
18099
        let display_type = crate::solver3::getters::get_computed_display(
18099
            raw_display, is_absolute_or_fixed, is_floated, is_root, is_flex_grid_child,
        );
        // If blockification changed the display type, update the node's formatting context
18099
        if display_type != raw_display {
30
            if let Some(node) = self.nodes.get_mut(node_idx) {
30
                node.computed_style.display = display_type;
30
                node.formatting_context = determine_formatting_context_for_display(
30
                    styled_dom, dom_id, display_type,
30
                );
30
            }
18069
        }
        // Compute containing block index for abs-pos clip exemption
18099
        if is_absolute_or_fixed {
18
            let cb_index = if matches!(node_position, LayoutPosition::Fixed) {
                // Fixed elements: containing block is the root (viewport)
                None
            } else {
                // Absolute elements: containing block is nearest positioned ancestor
18
                let mut ancestor = parent_idx;
                loop {
18
                    match ancestor {
18
                        Some(idx) => {
18
                            let pos = self.nodes.get(idx)
18
                                .map(|n| n.computed_style.position)
18
                                .unwrap_or_default();
18
                            if pos.is_positioned() {
18
                                break Some(idx);
                            }
                            ancestor = self.nodes.get(idx).and_then(|n| n.parent);
                        }
                        None => break None, // root
                    }
                }
            };
18
            if let Some(node) = self.nodes.get_mut(node_idx) {
18
                node.containing_block_index = cb_index;
18
            }
18081
        }
18099
        if parent_idx.is_none() {
1479
            if let Some(node) = self.nodes.get_mut(node_idx) {
1479
                if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
1452
                    *establishes_new_context = true;
1452
                }
            }
16620
        }
        // +spec:display-property:1f4039 - list-item generates ::marker pseudo-element + principal box
        // +spec:display-property:2bb592 - list-item generates ::marker pseudo-element with list-style content
        // +spec:display-property:3b507e - list-item generates ::marker pseudo-element
        // +spec:display-property:a48f00 - additional boxes (marker, table wrapper) placed w.r.t. principal box
        // +spec:display-property:998063 - list-item generates principal block box + marker box
        // If this is a list-item, inject a ::marker pseudo-element as its first child
        // +spec:display-property:a42905 - list-item generates ::marker pseudo-element with list-style content, principal box outer=block inner=flow
18099
        if display_type == LayoutDisplay::ListItem {
263
            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
17836
        }
        // +spec:display-contents:376f2e - display:contents removes principal box, children render normally
        // +spec:display-contents:3c7066 - display:contents strips element from formatting tree, hoists children
        // +spec:display-contents:3f4884 - replaced elements / form controls not specially handled yet (spec note: use display:none instead)
        // +spec:display-contents:4f9129 - semantic container role preserved: children promoted but DOM structure unchanged
        // +spec:display-contents:7558e8 - display:contents is rendering-time only; DOM relationships unaffected
        // +spec:display-contents:a079e3 - display:contents generates no box; children promoted to nearest non-contents ancestor (writing-mode parent lookup skips these)
        // +spec:display-contents:e202d5 - display:contents removes principal box, children render as normal
        // +spec:display-contents:6bbdf4 - display:contents preserves semantic container role (visibility context)
        // +spec:display-property:d7a8de - display:none/contents elements generate no box; anonymous box generation ignores them
        // +spec:display-property:dc2132 - display:none and display:contents control box generation
        // display:contents - element generates no box; promote children to parent
        // +spec:display-contents:61992e - element itself generates no boxes, children promoted to parent
        // +spec:display-contents:af8feb - treated as if replaced in element tree by its contents
        // +spec:display-contents:353e71 - display:contents box generation behavior
        // +spec:display-contents:b0a76b - display:contents generates no box; children promoted to parent
        // +spec:display-property:e370af - display:contents generates no box; children promoted to parent
        //
        // +spec:display-contents:852a59 - display:contents computes to display:none for replaced elements
        // +spec:display-contents:4a524e - display:contents computes to display:none on replaced elements
        // +spec:replaced-elements:af1e68 - display:contents on replaced elements has no effect (element renders normally)
        // Per CSS Display 3 §2.5 / Appendix B: replaced elements (img, canvas, embed, object,
        // audio, iframe, video, input, textarea, select, br, wbr, meter, progress)
        // and similar cannot be "un-boxed" — display:contents becomes display:none.
18099
        if display_type == LayoutDisplay::Contents && is_replaced_element(node_data) {
            // Treat as display:none — remove node from parent and skip children
1
            if let Some(parent) = parent_idx {
1
                if let Some(p) = self.nodes.get_mut(parent) {
1
                    p.children.retain(|&c| c != node_idx);
                }
            }
1
            if let Some(node) = self.nodes.get_mut(node_idx) {
1
                node.computed_style.display = LayoutDisplay::None;
1
                node.formatting_context = FormattingContext::None;
1
            }
1
            return Ok(node_idx);
18098
        }
18098
        if display_type == LayoutDisplay::Contents {
            // Remove the node we just created — it shouldn't generate a box
1
            if let Some(parent) = parent_idx {
1
                if let Some(p) = self.nodes.get_mut(parent) {
1
                    p.children.retain(|&c| c != node_idx);
                }
            }
            // Process children as if they belong to the parent (or root if no parent)
1
            let effective_parent = parent_idx.unwrap_or(node_idx);
1
            for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1
                self.process_node(styled_dom, child_dom_id, Some(effective_parent), debug_messages)?;
            }
1
            return Ok(node_idx);
18097
        }
18097
        match display_type {
            LayoutDisplay::Block
            | LayoutDisplay::InlineBlock
            | LayoutDisplay::FlowRoot
            | LayoutDisplay::ListItem => {
10996
                self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?;
            }
            // +spec:table-layout:d52e09 - display:table/inline-table cause element to behave like a table element
            // +spec:table-layout:360da0 - table display values cause table formatting behavior
            LayoutDisplay::Table | LayoutDisplay::InlineTable => {
3
                self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?;
            }
            LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup => {
                self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?;
            }
            LayoutDisplay::TableRow => {
1
                self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?;
            }
1
            LayoutDisplay::TableColumn => {
1
                // +spec:table-layout:77974f - Stage 1: all children of table-column treated as display:none
1
                // +spec:table-layout:c8dc69 - Stage 1: remove irrelevant boxes from table-column
1
                // CSS 2.2 §17.2.1: "All child boxes of a 'table-column' parent are
1
                // treated as if they had 'display: none'." - skip all children.
1
            }
            LayoutDisplay::TableColumnGroup => {
                // CSS 2.2 §17.2.1: "If a child C of a 'table-column-group' parent is not
                // a 'table-column' box, then it is treated as if it had 'display: none'."
                for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
                    let child_display = get_display_type(styled_dom, child_dom_id);
                    if child_display == LayoutDisplay::TableColumn {
                        self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
                    }
                    // Non-table-column children are suppressed (treated as display:none)
                }
            }
            // Inline, TableCell, etc., have their children processed as part of their
            // formatting context layout and don't require anonymous box generation at this stage.
            // of table-internal display values is handled via blockify_flex_item_if_table_internal
            _ => {
                // +spec:display-contents:34008d - display:none elements generate no boxes; excluded from formatting structure
                // +spec:display-property:1f38b2 - display:none creates no box at all, filter from layout tree
                // +spec:display-property:eb53f7 - display:none suppresses box generation; visibility:hidden boxes still affect layout
                // Filter out display: none children - they don't participate in layout
                // +spec:display-property:d1600a - display:none suppresses box generation; visibility:hidden boxes still affect layout
                // ALSO filter out whitespace-only text nodes for Flex/Grid/etc containers
                // to prevent them from becoming unwanted anonymous items.
7096
                let children: Vec<NodeId> = dom_id
7096
                    .az_children(&styled_dom.node_hierarchy.as_container())
                    // +spec:display-property:9f02c6 - display:none elements generate no boxes
7096
                    .filter(|&child_id| {
                        // +spec:display-property:3b507e - display:none excludes subtree from box tree
540
                        if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
                            return false;
540
                        }
                        // Check for whitespace-only text
540
                        let node_data = &styled_dom.node_data.as_container()[child_id];
540
                        if let NodeType::Text(text) = node_data.get_node_type() {
                            // Skip if text is empty or just whitespace
325
                            return !text.as_str().trim().is_empty();
215
                        }
215
                        true
540
                    })
7096
                    .collect();
7096
                let is_flex_or_grid = matches!(
7096
                    display_type,
                    LayoutDisplay::Flex | LayoutDisplay::InlineFlex
                    | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
                );
7501
                for child_dom_id in children {
                    // +spec:display-property:934c84 - table wrapper box generation: display:table/inline-table generates a principal block container (table wrapper box) that establishes BFC and contains the table box + caption boxes
                    // +spec:width-calculation:59d456 - table wrapper box is block-level, establishes BFC (CSS 2.2 §17.4)
                    // the table wrapper box becomes the flex item; align-self applies to the
                    // wrapper, flex longhands apply to the inner table box, caption contents
                    // contribute to wrapper min/max-content sizes
405
                    let child_display = get_display_type(styled_dom, child_dom_id);
405
                    if is_flex_or_grid && child_display.creates_table_context() {
                        let wrapper_idx = self.create_anonymous_node(
                            node_idx,
                            AnonymousBoxType::TableWrapper,
                            FormattingContext::Block { establishes_new_context: true },
                        );
                        self.process_node(styled_dom, child_dom_id, Some(wrapper_idx), debug_messages)?;
                    } else {
405
                        let child_idx = self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
                        // table-internal flex items are blockified, preventing anonymous table
                        // box generation (e.g. two display:table-cell flex items become two
                        // separate display:block flex items)
405
                        if is_flex_or_grid {
242
                            blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
262
                        }
                    }
                }
            }
        }
18097
        Ok(node_idx)
18099
    }
    // +spec:display-property:5572e7 - Anonymous block boxes: wrap inline runs when block container has mixed block/inline children
    // +spec:display-property:090043 - Anonymous block box properties inherited from enclosing non-anonymous box; non-inherited props get initial values
    // +spec:display-property:7b9f7a - Block-level vs inline-level classification and anonymous block box creation
    // +spec:display-property:078fe5 - Anonymous block boxes wrapping inline content in mixed block/inline contexts
    // +spec:display-property:8d8ef3 - block container anonymous box generation: wraps inline runs in anonymous block boxes to ensure block containers contain only block-level or only inline-level boxes
    // +spec:display-property:1fe2be - inline box construction with anonymous text interspersed with inline elements
    // +spec:display-property:be80e3 - Anonymous inline boxes: text in block containers treated as anonymous inlines, whitespace-only runs collapsed
    /// Handles children of a block-level element, creating anonymous block
    /// wrappers for consecutive runs of inline-level children if necessary.
    // +spec:display-property:b73c50 - blockify inline content by wrapping in anonymous block containers
10996
    fn process_block_children(
10996
        &mut self,
10996
        styled_dom: &StyledDom,
10996
        parent_dom_id: NodeId,
10996
        parent_idx: usize,
10996
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
10996
    ) -> Result<()> {
        // Filter out display: none children - they don't participate in layout
10996
        let children: Vec<NodeId> = parent_dom_id
10996
            .az_children(&styled_dom.node_hierarchy.as_container())
21064
            .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
10996
            .collect();
        // Debug: log which children we found
10996
        if let Some(msgs) = debug_messages.as_mut() {
6840
            msgs.push(LayoutDebugMessage::info(format!(
6840
                "[process_block_children] DOM node {} has {} children: {:?}",
6840
                parent_dom_id.index(),
6840
                children.len(),
6840
                children.iter().map(NodeId::index).collect::<Vec<_>>()
6840
            )));
7441
        }
11671
        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
10996
        if let Some(msgs) = debug_messages.as_mut() {
6840
            msgs.push(LayoutDebugMessage::info(format!(
6840
                "[process_block_children] has_block_child={}, children display types: {:?}",
                has_block_child,
6840
                children
6840
                    .iter()
11043
                    .map(|c| {
11043
                        let dt = get_display_type(styled_dom, *c);
11043
                        let is_block = is_block_level(styled_dom, *c);
11043
                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
11043
                    })
6840
                    .collect::<Vec<_>>()
            )));
4156
        }
10996
        if !has_block_child {
            // All children are inline, no anonymous boxes needed.
7370
            if let Some(msgs) = debug_messages.as_mut() {
4023
                msgs.push(LayoutDebugMessage::info(format!(
4023
                    "[process_block_children] All inline, processing {} children directly",
4023
                    children.len()
4023
                )));
4301
            }
14303
            for child_id in children {
6933
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
            }
7370
            return Ok(());
3626
        }
        // Mixed block and inline content requires anonymous wrappers.
3626
        let mut inline_run = Vec::new();
17756
        for child_id in children {
14130
            if is_block_level(styled_dom, child_id) {
                // +spec:display-contents:02a534 - contiguous text sequences with no text don't generate boxes
                // End the current inline run — but skip if all nodes are whitespace-only text.
                // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
                // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
                // CSS 2.1 §9.2.2.1: "White space content that would subsequently be collapsed
                // away according to the 'white-space' property does not generate any anonymous
                // inline boxes."
9232
                if !inline_run.is_empty() {
3961
                    self.flush_inline_run(styled_dom, parent_idx, &mut inline_run, debug_messages)?;
5271
                }
                // Process the block-level child directly
9232
                if let Some(msgs) = debug_messages.as_mut() {
5373
                    msgs.push(LayoutDebugMessage::info(format!(
5373
                        "[process_block_children] Processing block child DOM {}",
5373
                        child_id.index()
5373
                    )));
5875
                }
9232
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
4898
            } else {
4898
                inline_run.push(child_id);
4898
            }
        }
        // Process any remaining inline children at the end — skip if all whitespace
3626
        if !inline_run.is_empty() {
937
            self.flush_inline_run(styled_dom, parent_idx, &mut inline_run, debug_messages)?;
2689
        }
3626
        Ok(())
10996
    }
    // +spec:table-layout:6bb84e - Anonymous table object generation (stages 1-3: remove irrelevant boxes, generate missing child wrappers, generate missing parents)
    // +spec:table-layout:77974f - Stage 2: generate missing child wrappers for table/inline-table
    // +spec:table-layout:c8dc69 - Stage 2: wrap non-proper children in anonymous table-row
    // +spec:display-property:6f8f13 - anonymous table object generation (§17.2.1): suppress table-column/table-column-group children, wrap non-proper children in anonymous rows/cells
4
    fn process_table_level_children(
4
        &mut self,
4
        styled_dom: &StyledDom,
4
        parent_dom_id: NodeId,
4
        parent_idx: usize,
4
        is_expected_child: fn(LayoutDisplay) -> bool,
4
        anon_type: AnonymousBoxType,
4
        anon_fc: FormattingContext,
4
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4
    ) -> Result<()> {
4
        let parent_display = get_display_type(styled_dom, parent_dom_id);
4
        let mut non_matching_children = Vec::new();
4
        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
4
            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1
                continue;
3
            }
3
            let child_display = get_display_type(styled_dom, child_id);
3
            if is_expected_child(child_display) {
1
                if !non_matching_children.is_empty() {
                    let anon_idx = self.create_anonymous_node(
                        parent_idx,
                        anon_type,
                        anon_fc,
                    );
                    #[allow(clippy::iter_with_drain)] // accumulator Vec reused across runs; drain(..) empties it while retaining the allocation
                    for np_id in non_matching_children.drain(..) {
                        self.process_node(styled_dom, np_id, Some(anon_idx), debug_messages)?;
                    }
1
                }
1
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
2
            } else {
2
                non_matching_children.push(child_id);
2
            }
        }
4
        if !non_matching_children.is_empty() {
2
            let anon_idx = self.create_anonymous_node(
2
                parent_idx,
2
                anon_type,
2
                anon_fc,
            );
4
            for np_id in non_matching_children {
2
                self.process_node(styled_dom, np_id, Some(anon_idx), debug_messages)?;
            }
2
        }
4
        Ok(())
4
    }
3
    fn process_table_children(
3
        &mut self,
3
        styled_dom: &StyledDom,
3
        parent_dom_id: NodeId,
3
        parent_idx: usize,
3
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
3
    ) -> Result<()> {
3
        self.process_table_level_children(
3
            styled_dom, parent_dom_id, parent_idx,
3
            is_proper_table_child,
3
            AnonymousBoxType::TableRow,
3
            FormattingContext::TableRow,
3
            debug_messages,
        )
3
    }
    fn process_table_row_group_children(
        &mut self,
        styled_dom: &StyledDom,
        parent_dom_id: NodeId,
        parent_idx: usize,
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    ) -> Result<()> {
        self.process_table_level_children(
            styled_dom, parent_dom_id, parent_idx,
            |d| d == LayoutDisplay::TableRow,
            AnonymousBoxType::TableRow,
            FormattingContext::TableRow,
            debug_messages,
        )
    }
1
    fn process_table_row_children(
1
        &mut self,
1
        styled_dom: &StyledDom,
1
        parent_dom_id: NodeId,
1
        parent_idx: usize,
1
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1
    ) -> Result<()> {
1
        self.process_table_level_children(
1
            styled_dom, parent_dom_id, parent_idx,
            |d| d == LayoutDisplay::TableCell,
1
            AnonymousBoxType::TableCell,
1
            FormattingContext::Block { establishes_new_context: true },
1
            debug_messages,
        )
1
    }
    // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
    // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
4898
    fn flush_inline_run(
4898
        &mut self,
4898
        styled_dom: &StyledDom,
4898
        parent_idx: usize,
4898
        inline_run: &mut Vec<NodeId>,
4898
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4898
    ) -> Result<()> {
4898
        let all_whitespace = inline_run
4898
            .iter()
4898
            .all(|id| is_whitespace_only_text(styled_dom, *id));
4898
        if all_whitespace {
4852
            if let Some(msgs) = debug_messages.as_mut() {
1926
                msgs.push(LayoutDebugMessage::info(format!(
1926
                    "[process_block_children] Skipping whitespace-only inline run: {:?}",
1926
                    inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
                )));
2926
            }
4852
            inline_run.clear();
        } else {
46
            if let Some(msgs) = debug_messages.as_mut() {
18
                msgs.push(LayoutDebugMessage::info(format!(
18
                    "[process_block_children] Creating anon wrapper for inline run: {:?}",
18
                    inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
                )));
28
            }
46
            let anon_idx = self.create_anonymous_node(
46
                parent_idx,
46
                AnonymousBoxType::InlineWrapper,
46
                FormattingContext::Block {
46
                    establishes_new_context: true,
46
                },
            );
46
            for inline_child_id in inline_run.drain(..) {
46
                self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
            }
        }
4898
        Ok(())
4898
    }
    // +spec:display-property:52f497 - anonymous inline boxes inherit inheritable properties from block parent; non-inherited properties use initial values (dom_node_id: None + BoxProps::default())
    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
    /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
    /// to satisfy the constraints of the table model."
    ///
    // +spec:display-property:ee83bf - Anonymous box generation: boxes not associated with elements, inheriting through box tree parentage
    /// Helper to create an anonymous node in the tree.
    /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
    /// the CSS box model structure (e.g., wrapping inline content in blocks,
    /// or creating missing table structural elements).
    // +spec:display-property:6ff51a - anonymous block boxes have no styles (box_props default), so parent element properties still apply to its content
163
    pub fn create_anonymous_node(
163
        &mut self,
163
        parent: usize,
163
        anon_type: AnonymousBoxType,
163
        fc: FormattingContext,
163
    ) -> usize {
163
        let index = self.nodes.len();
        // +spec:display-property:e67146 - Anonymous boxes inherit from enclosing non-anonymous box; non-inherited props use initial values
163
        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context);
163
        self.nodes.push(LayoutNode {
163
            // ── HOT ──
163
            box_props: BoxProps::default(),
163
            dom_node_id: None,
163
            children: Vec::new(),
163
            used_size: None,
163
            formatting_context: fc,
163
            parent: Some(parent),
163
            // ── WARM ──
163
            intrinsic_sizes: None,
163
            baseline: None,
163
            inline_layout_result: None,
163
            inline_content_cache: None,
163
            scrollbar_info: None,
163
            relative_position: None,
163
            overflow_content_size: None,
163
            taffy_cache: TaffyCache::new(),
163
            measured_content_sizes: (None, None),
163
            computed_style: ComputedLayoutStyle::default(),
163
            pseudo_element: None,
163
            escaped_top_margin: None,
163
            escaped_bottom_margin: None,
163
            parent_formatting_context: parent_fc,
163
            ifc_membership: None,
163
            containing_block_index: None,
163
            // ── COLD ──
163
            anonymous_type: Some(anon_type),
163
            preview_byte_range: None,
163
            node_data_fingerprint: NodeDataFingerprint::default(),
163
            subtree_hash: SubtreeHash(0),
163
            dirty_flag: DirtyFlag::Layout,
163
            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
163
            ifc_id: None,
163
        });
163
        self.nodes[parent].children.push(index);
163
        index
163
    }
    /// Creates a `::marker` pseudo-element as the first child of a list-item.
    ///
    /// Per CSS Lists Module Level 3, Section 3.1:
    /// "For elements with display: list-item, user agents must generate a
    /// `::marker` pseudo-element as the first child of the principal box."
    ///
    /// The `::marker` references the same DOM node as its parent list-item,
    /// but is marked as a pseudo-element for proper counter resolution and styling.
1685
    pub fn create_marker_pseudo_element(
1685
        &mut self,
1685
        styled_dom: &StyledDom,
1685
        list_item_dom_id: NodeId,
1685
        list_item_idx: usize,
1685
    ) -> usize {
1685
        let index = self.nodes.len();
        // The marker references the same DOM node as the list-item
        // This is important for style resolution (the marker inherits from the list-item)
1685
        let parent_fc = self
1685
            .nodes
1685
            .get(list_item_idx)
1685
            .map(|n| n.formatting_context);
1685
        self.nodes.push(LayoutNode {
1685
            // ── HOT ──
1685
            box_props: BoxProps::default(),
1685
            dom_node_id: Some(list_item_dom_id),
1685
            children: Vec::new(),
1685
            used_size: None,
1685
            formatting_context: FormattingContext::Inline,
1685
            parent: Some(list_item_idx),
1685
            // ── WARM ──
1685
            intrinsic_sizes: None,
1685
            baseline: None,
1685
            inline_layout_result: None,
1685
            inline_content_cache: None,
1685
            scrollbar_info: None,
1685
            relative_position: None,
1685
            overflow_content_size: None,
1685
            taffy_cache: TaffyCache::new(),
1685
            measured_content_sizes: (None, None),
1685
            computed_style: ComputedLayoutStyle::default(),
1685
            pseudo_element: Some(PseudoElement::Marker),
1685
            escaped_top_margin: None,
1685
            escaped_bottom_margin: None,
1685
            parent_formatting_context: parent_fc,
1685
            ifc_membership: None,
1685
            containing_block_index: None,
1685
            // ── COLD ──
1685
            anonymous_type: None,
1685
            preview_byte_range: None,
1685
            node_data_fingerprint: NodeDataFingerprint::default(),
1685
            subtree_hash: SubtreeHash(0),
1685
            dirty_flag: DirtyFlag::Layout,
1685
            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1685
            ifc_id: None,
1685
        });
        // Insert as FIRST child (per spec)
1685
        self.nodes[list_item_idx].children.insert(0, index);
        // Register with DOM mapping for counter resolution
1685
        self.dom_to_layout
1685
            .entry(list_item_dom_id)
1685
            .or_default()
1685
            .push(index);
1685
        index
1685
    }
    // M12.7: returns `usize`, NOT `Result<usize>` — this fn has no error path
    // (always `Ok(index)`). The `Result` forced callers to use `?`, whose lifted
    // discriminant decode mis-reads the Ok as Err (the rc=5 root cause: reconcile
    // reaches this fn but returns Err before its own Ok). Dropping the Result
    // removes that mis-lifting `?`.
    /// Apply CSS Display 3 §2.7/§2.8 blockification to a freshly-created node:
    /// a flex/grid item (or root / abs-pos / floated box) whose specified display
    /// is inline-level computes to its block-level equivalent.
    ///
    /// `process_node` (the full tree build) does this inline, but the INCREMENTAL
    /// tree builder (`cache.rs` reconcile → `create_node_from_dom`) bypassed it.
    /// Without it, a replaced inline flex item — e.g. an `<img>` canvas with
    /// `flex-grow: 1` (`AzulPaint`) — stayed inline, so its flex-grow was ignored
    /// and it was laid out 300×0 (the replaced-element default width, 0 height).
    /// Must be called AFTER the node is created and AFTER its parent's
    /// formatting context is known (the build is top-down, so the parent exists).
163476
    pub fn blockify_node_display(
163476
        &mut self,
163476
        styled_dom: &StyledDom,
163476
        dom_id: NodeId,
163476
        node_idx: usize,
163476
        parent_idx: Option<usize>,
163476
    ) {
163476
        let node_data = &styled_dom.node_data.as_container()[dom_id];
        // CSS Display 3 §2.4: a replaced element with a layout-internal display
        // value uses 'inline' — so it's inline-level and thus blockifiable.
163476
        let raw_display = {
163476
            let d = get_display_type(styled_dom, dom_id);
163476
            if d.is_layout_internal() && is_replaced_element(node_data) {
                LayoutDisplay::Inline
            } else {
163476
                d
            }
        };
163476
        let (position, float) = self
163476
            .nodes
163476
            .get(node_idx)
163476
            .map(|n| (n.computed_style.position, n.computed_style.float))
163476
            .unwrap_or_default();
163476
        let is_absolute_or_fixed =
163476
            matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed);
163476
        let is_floated = float != LayoutFloat::None;
163476
        let is_root = parent_idx.is_none();
163476
        let is_flex_grid_child = parent_idx
163476
            .and_then(|p| self.nodes.get(p))
163476
            .is_some_and(|n| {
55039
                matches!(
159239
                    n.formatting_context,
                    FormattingContext::Flex | FormattingContext::Grid
                )
159239
            });
163476
        let display_type = crate::solver3::getters::get_computed_display(
163476
            raw_display,
163476
            is_absolute_or_fixed,
163476
            is_floated,
163476
            is_root,
163476
            is_flex_grid_child,
        );
163476
        if display_type != raw_display {
18930
            if let Some(node) = self.nodes.get_mut(node_idx) {
18930
                node.computed_style.display = display_type;
18930
                node.formatting_context =
18930
                    determine_formatting_context_for_display(styled_dom, dom_id, display_type);
18930
            }
144546
        }
163476
    }
    #[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
181585
    pub fn create_node_from_dom(
181585
        &mut self,
181585
        styled_dom: &StyledDom,
181585
        dom_id: NodeId,
181585
        parent: Option<usize>,
181585
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
181585
    ) -> usize {
181585
        let index = self.nodes.len();
        // as IT sees it). If this is 0 but build() sees 0 nodes, the push is lost
        // between here and build (builder &mut threading); if garbage, len mis-reads.
181585
        { let _ = (0xCE00_0000u32 | (index as u32 & 0xffff)); }
181585
        let parent_fc =
181585
            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context));
        // this is reached but step A is NOT, collect_box_props diverges; if this is
        // NOT reached, the parent Option discriminant mis-lifts (None→Some garbage).
181585
        { let _ = (0xCD00_0001u32 | (u32::from(parent_fc.is_some()) << 8)); }
181585
        let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
181585
        { let _ = (0xCA00_0001u32); }
181585
        self.nodes.push(LayoutNode {
            // ── HOT ──
181585
            box_props: collected.resolved,
181585
            dom_node_id: Some(dom_id),
181585
            children: Vec::new(),
181585
            used_size: None,
181585
            formatting_context: determine_formatting_context(styled_dom, dom_id),
181585
            parent,
            // ── WARM ──
181585
            intrinsic_sizes: None,
181585
            baseline: None,
181585
            inline_layout_result: None,
181585
            inline_content_cache: None,
181585
            scrollbar_info: None,
181585
            relative_position: None,
181585
            overflow_content_size: None,
181585
            taffy_cache: TaffyCache::new(),
181585
            measured_content_sizes: (None, None),
            // +spec:overflow:8f9f7e - viewport overflow propagation: visible→auto, clip→hidden
            computed_style: {
181585
                let mut style = compute_layout_style(styled_dom, dom_id);
181585
                if parent.is_none() {
                    // CSS Overflow 3 §3.3: If visible is applied to the viewport,
                    // it must be interpreted as auto. If clip is applied to the
                    // viewport, it must be interpreted as hidden.
                    use azul_css::props::layout::LayoutOverflow;
5722
                    if style.overflow_x == LayoutOverflow::Visible {
5524
                        style.overflow_x = LayoutOverflow::Auto;
5524
                    } else if style.overflow_x == LayoutOverflow::Clip {
                        style.overflow_x = LayoutOverflow::Hidden;
198
                    }
5722
                    if style.overflow_y == LayoutOverflow::Visible {
5506
                        style.overflow_y = LayoutOverflow::Auto;
5506
                    } else if style.overflow_y == LayoutOverflow::Clip {
                        style.overflow_y = LayoutOverflow::Hidden;
216
                    }
175863
                }
181585
                style
            },
181585
            pseudo_element: None,
181585
            escaped_top_margin: None,
181585
            escaped_bottom_margin: None,
181585
            parent_formatting_context: parent_fc,
181585
            ifc_membership: None,
181585
            containing_block_index: None,
            // ── COLD ──
181585
            anonymous_type: None,
181585
            preview_byte_range: None,
181585
            node_data_fingerprint: NodeDataFingerprint::compute(
181585
                &styled_dom.node_data.as_container()[dom_id],
181585
                styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
            ),
181585
            subtree_hash: SubtreeHash(0),
181585
            dirty_flag: DirtyFlag::Layout,
181585
            unresolved_box_props: collected.unresolved,
181585
            ifc_id: None,
        });
181585
        { let _ = (0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
181585
        if let Some(p) = parent {
175863
            self.nodes[p].children.push(index);
175863
        }
181585
        self.dom_to_layout.entry(dom_id).or_default().push(index);
        // DEBUG (2026-06-02 children-None tree-build): count create_node_from_dom
        // calls @0x40500 + record each dom_id into a 14-slot ring @0x40504. REVERT
        // before commit. Runs only in lifted wasm (server lifts, never runs natively).
        unsafe {
181585
            let c = crate::az_mark_read(0x40500);
181585
            crate::az_mark(0x60500_u32, (c.wrapping_add(1)));
181585
            if (c as usize) < 14 {
181585
                crate::az_mark((0x40504 + (c as usize) * 4) as u32, (0xDD00_0000 | (dom_id.index() as u32 & 0xffff)));
181585
            }
        }
181585
        index
181585
    }
    /// O3-render: materialize pending SPLIT previews as layout nodes — the
    /// "fake structural edit" becomes VISIBLE. For each recorded split whose
    /// node is an IFC leaf (no layout children; the `<p>text…</p>` shape),
    /// the node becomes part 1 (content byte range `[0, at)`), and a SECOND
    /// node with the SAME `dom_node_id` (the `dom_to_layout` multimap seat)
    /// and `AnonymousBoxType::SplitPreviewPart` shows `[at, ∞)` — a real
    /// layout identity like any anonymous box, NO DOM mutation, no stale
    /// `NodeIds`. Runs as a post-pass just before `build()` on BOTH tree-build
    /// paths (full + reconcile).
    ///
    /// v1 renders text-byte splits; element-children splits stay
    /// recorded-only (the child-routing per part is the staged follow-up).
6402
    pub fn apply_split_previews(
6402
        &mut self,
6402
        overlay: Option<&crate::overlay::ContentOverlay>,
6402
        styled_dom: &StyledDom,
6402
    ) {
6402
        let Some(overlay) = overlay else { return };
4548
        let pending = overlay.pending_structure(styled_dom.dom_id);
4548
        if pending.is_empty() {
4485
            return;
63
        }
63
        let node_data = styled_dom.node_data.as_container();
63
        let hierarchy = styled_dom.node_hierarchy.as_container();
126
        for entry in pending {
63
            let (node, at) = match &entry.preview {
36
                crate::overlay::StructuralPreview::Split { node, at, .. } => (node, at),
9
                crate::overlay::StructuralPreview::Remove { parent, start, end }
                | crate::overlay::StructuralPreview::Replace {
9
                    parent, start, end, ..
                } => {
                    // Removal side renders NOW: the range's layout children
                    // detach from the tree (unreachable from the root, so
                    // never positioned nor painted). Replace's PENDING
                    // content is the staged follow-up — it needs the styled
                    // materialization of a raw `Dom` fragment; until then a
                    // Replace previews as its removal half.
18
                    self.apply_range_suppression_preview(
18
                        *parent,
18
                        *start,
18
                        *end,
18
                        &ordinal_map(&hierarchy, *parent),
                    );
18
                    continue;
                }
9
                crate::overlay::StructuralPreview::Merge { first, second } => {
                    // Backspace-at-start UX: `second` disappears from its
                    // parent, its layout children move (subtrees wholesale)
                    // onto the END of `first` — the inverse mechanics of the
                    // element split above.
9
                    self.apply_merge_preview(*first, *second);
9
                    continue;
                }
                crate::overlay::StructuralPreview::Insert { .. } => {
                    // Staged: rendering a pending `Dom` needs a styled
                    // sub-mount (fragment styling against the retained author
                    // css). Resolvers already expose it (children_for_node);
                    // the box tree does not materialize it yet.
                    continue;
                }
            };
36
            let Some(indices) = self.dom_to_layout.get(node) else {
                continue;
            };
36
            if indices.len() != 1 {
                continue; // already split (or exotic mapping)
36
            }
36
            let idx = indices[0];
            // Child-boundary split (no text byte): a container splits BETWEEN
            // its children — a `<ul>` between `<li>`s, a section between
            // paragraphs. Children before `at.child_index` stay in part 1;
            // the rest MOVE (wholesale, subtrees intact) to the preview part.
36
            let Some(byte) = at.text_byte.into_option() else {
                // Child-boundary split: layout children route to a part by
                // their DOM position.
27
                self.apply_element_split_preview(
27
                    idx,
27
                    *node,
27
                    at.child_index,
27
                    &ordinal_map(&hierarchy, *node),
                );
27
                continue;
            };
            // v1 gate: plain-text IFC roots — every layout child must be
            // text-backed (or anonymous inline machinery). Element children
            // need per-part routing (the staged follow-up).
9
            let all_text_children = self.nodes[idx].children.iter().all(|&c| {
9
                self.nodes.get(c).is_none_or(|n| {
9
                    n.dom_node_id.is_none_or(|d| {
9
                        node_data
9
                            .get(d)
9
                            .is_some_and(|nd| matches!(nd.get_node_type(), NodeType::Text(_)))
9
                    })
9
                })
9
            });
9
            if !all_text_children {
                continue;
9
            }
            // Flat byte offset of the split within the node's concatenated
            // direct-text content (the same flattening fc collects).
9
            let mut flat_at: u32 = 0;
9
            let mut child = hierarchy.get(*node).and_then(|h| h.first_child_id(*node));
9
            let mut i: u32 = 0;
9
            while let Some(c) = child {
9
                if i == at.child_index {
9
                    break;
                }
                if let Some(nd) = node_data.get(c) {
                    if let NodeType::Text(t) = nd.get_node_type() {
                        flat_at += u32::try_from(t.as_str().len()).unwrap_or(u32::MAX);
                    }
                }
                i += 1;
                child = hierarchy.get(c).and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id);
            }
9
            flat_at += byte;
9
            self.nodes[idx].preview_byte_range = Some((0, flat_at));
9
            self.nodes[idx].inline_layout_result = None;
9
            self.nodes[idx].dirty_flag = DirtyFlag::Layout;
9
            let part2_idx = self.nodes.len();
9
            let mut part2 = self.nodes[idx].clone();
9
            part2.children = Vec::new();
9
            part2.preview_byte_range = Some((flat_at, u32::MAX));
9
            part2.anonymous_type = Some(AnonymousBoxType::SplitPreviewPart);
9
            part2.inline_layout_result = None;
9
            part2.taffy_cache = TaffyCache::new();
9
            part2.measured_content_sizes = (None, None);
9
            part2.dirty_flag = DirtyFlag::Layout;
9
            let parent = part2.parent;
9
            self.nodes.push(part2);
9
            if let Some(p) = parent {
9
                let pos = self.nodes[p]
9
                    .children
9
                    .iter()
9
                    .position(|&c| c == idx)
9
                    .map_or(self.nodes[p].children.len(), |i| i + 1);
9
                self.nodes[p].children.insert(pos, part2_idx);
9
                self.nodes[p].dirty_flag = DirtyFlag::Layout;
            }
9
            self.dom_to_layout.entry(*node).or_default().push(part2_idx);
        }
6402
    }
    /// Child-boundary split preview: partition `idx`'s layout children by DOM
    /// ordinal — `< split_index` stays, the rest MOVE to a fresh
    /// `SplitPreviewPart` sibling that shares `dom_node`'s id (the anon-box
    /// model: rendering knows, callbacks stay blind). Subtrees move
    /// wholesale; nothing is byte-sliced.
    ///
    /// Anonymous wrappers (mixed inline/block content puts inline runs inside
    /// anonymous blocks, so layout children are NOT 1:1 with DOM children)
    /// route through their wrapped runs' ordinals: a wrapper wholly on one
    /// side moves wholesale; a wrapper STRADDLING the split point splits
    /// itself — its second half becomes a fresh anonymous wrapper inside the
    /// preview part, mirroring how the builder would have wrapped the
    /// post-apply trees.
27
    fn apply_element_split_preview(
27
        &mut self,
27
        idx: usize,
27
        dom_node: NodeId,
27
        split_index: u32,
27
        ordinal_of: &BTreeMap<NodeId, u32>,
27
    ) {
27
        let children = self.nodes[idx].children.clone();
27
        let mut keep: Vec<usize> = Vec::new();
27
        let mut moved: Vec<usize> = Vec::new();
99
        for c in children {
72
            if let Some(d) = self.nodes[c].dom_node_id {
                // A layout child mapping to something that is NOT a direct
                // DOM child (unexpected) degrades to "stays" — part 1 keeps
                // rendering it rather than dropping content from a preview.
45
                match ordinal_of.get(&d) {
45
                    Some(&o) if o >= split_index => moved.push(c),
18
                    _ => keep.push(c),
                }
45
                continue;
27
            }
            // Anonymous wrapper: side = the ordinal span of its wrapped runs.
27
            match self.wrapper_ordinal_span(c, ordinal_of) {
27
                Some((_, hi)) if hi < split_index => keep.push(c),
18
                Some((lo, _)) if lo >= split_index => moved.push(c),
                Some(_) => {
                    // Straddling: the wrapper itself splits at the boundary.
9
                    keep.push(c);
9
                    if let Some(second) = self.split_anonymous_wrapper(c, split_index, ordinal_of)
9
                    {
9
                        moved.push(second);
9
                    }
                }
                // Empty / opaque wrapper (no mapped runs): keep in part 1.
                None => keep.push(c),
            }
        }
27
        if moved.is_empty() {
            return; // split at/after the end: nothing to preview
27
        }
27
        self.nodes[idx].children = keep;
27
        self.nodes[idx].inline_layout_result = None;
27
        self.nodes[idx].dirty_flag = DirtyFlag::Layout;
27
        let part2_idx = self.nodes.len();
27
        let mut part2 = self.nodes[idx].clone();
27
        part2.children.clone_from(&moved);
27
        part2.anonymous_type = Some(AnonymousBoxType::SplitPreviewPart);
27
        part2.inline_layout_result = None;
27
        part2.taffy_cache = TaffyCache::new();
27
        part2.measured_content_sizes = (None, None);
27
        part2.dirty_flag = DirtyFlag::Layout;
27
        let parent = part2.parent;
27
        self.nodes.push(part2);
72
        for &m in &moved {
45
            self.nodes[m].parent = Some(part2_idx);
45
            self.nodes[m].dirty_flag = DirtyFlag::Layout;
45
        }
27
        if let Some(p) = parent {
27
            let pos = self.nodes[p]
27
                .children
27
                .iter()
27
                .position(|&c| c == idx)
27
                .map_or(self.nodes[p].children.len(), |i| i + 1);
27
            self.nodes[p].children.insert(pos, part2_idx);
27
            self.nodes[p].dirty_flag = DirtyFlag::Layout;
        }
27
        self.dom_to_layout.entry(dom_node).or_default().push(part2_idx);
27
    }
    /// The min/max DOM ordinal among an anonymous wrapper's DIRECT layout
    /// children that map to real DOM children of the node being edited.
    /// `None` = the wrapper wraps nothing addressable (empty, or generated
    /// content only).
27
    fn wrapper_ordinal_span(
27
        &self,
27
        wrapper: usize,
27
        ordinal_of: &BTreeMap<NodeId, u32>,
27
    ) -> Option<(u32, u32)> {
27
        let mut span: Option<(u32, u32)> = None;
36
        for &c in &self.nodes[wrapper].children {
36
            let Some(o) = self.nodes[c]
36
                .dom_node_id
36
                .and_then(|d| ordinal_of.get(&d).copied())
            else {
                continue;
            };
36
            span = Some(match span {
27
                None => (o, o),
9
                Some((lo, hi)) => (lo.min(o), hi.max(o)),
            });
        }
27
        span
27
    }
    /// Split a straddling anonymous wrapper at `split_index`: runs with
    /// ordinal `>= split_index` move into a NEW wrapper of the same shape
    /// (returned; the caller seats it in the preview part). Runs that map to
    /// nothing stay in the first half. Returns `None` when nothing moves.
9
    fn split_anonymous_wrapper(
9
        &mut self,
9
        wrapper: usize,
9
        split_index: u32,
9
        ordinal_of: &BTreeMap<NodeId, u32>,
9
    ) -> Option<usize> {
9
        let children = core::mem::take(&mut self.nodes[wrapper].children);
18
        let (keep, moved): (Vec<usize>, Vec<usize>) = children.into_iter().partition(|&c| {
18
            self.nodes[c]
18
                .dom_node_id
18
                .and_then(|d| ordinal_of.get(&d).copied())
18
                .is_none_or(|o| o < split_index)
18
        });
9
        self.nodes[wrapper].children = keep;
9
        self.nodes[wrapper].inline_layout_result = None;
9
        self.nodes[wrapper].taffy_cache = TaffyCache::new();
9
        self.nodes[wrapper].measured_content_sizes = (None, None);
9
        self.nodes[wrapper].dirty_flag = DirtyFlag::Layout;
9
        if moved.is_empty() {
            return None;
9
        }
9
        let second_idx = self.nodes.len();
9
        let mut second = self.nodes[wrapper].clone();
9
        second.children = moved;
9
        second.inline_layout_result = None;
9
        second.taffy_cache = TaffyCache::new();
9
        second.measured_content_sizes = (None, None);
9
        second.dirty_flag = DirtyFlag::Layout;
9
        self.nodes.push(second);
9
        let moved_children = self.nodes[second_idx].children.clone();
18
        for m in moved_children {
9
            self.nodes[m].parent = Some(second_idx);
9
            self.nodes[m].dirty_flag = DirtyFlag::Layout;
9
        }
        // The caller re-parents `second_idx` into the preview part; anonymous
        // wrappers have no dom_node_id, so dom_to_layout needs no entry.
9
        Some(second_idx)
9
    }
    /// Remove/Replace preview: detach the layout children of `parent_dom`
    /// whose DOM ordinal falls in `[start, end)`. Detached subtrees stay in
    /// the node arena but are unreachable from the root — never positioned,
    /// never painted (the same fate as any orphaned builder node).
    ///
    /// Anonymous wrappers route through their wrapped runs: wholly inside the
    /// range → the wrapper detaches; straddling → only the wrapper's in-range
    /// runs detach (the wrapper stays for the survivors).
18
    fn apply_range_suppression_preview(
18
        &mut self,
18
        parent_dom: NodeId,
18
        start: u32,
18
        end: u32,
18
        ordinal_of: &BTreeMap<NodeId, u32>,
18
    ) {
18
        let Some(indices) = self.dom_to_layout.get(&parent_dom) else {
            return;
        };
18
        if indices.len() != 1 {
            return; // exotic mapping (already part-split); one preview at a time
18
        }
18
        let idx = indices[0];
18
        let children = self.nodes[idx].children.clone();
18
        let mut kept: Vec<usize> = Vec::new();
18
        let mut changed = false;
72
        for c in children {
54
            if let Some(d) = self.nodes[c].dom_node_id {
54
                let in_range = ordinal_of
54
                    .get(&d)
54
                    .is_some_and(|&o| o >= start && o < end);
54
                if in_range {
27
                    changed = true;
27
                } else {
27
                    kept.push(c);
27
                }
54
                continue;
            }
            match self.wrapper_ordinal_span(c, ordinal_of) {
                Some((lo, hi)) if lo >= start && hi < end => changed = true, // wholly removed
                Some((lo, hi)) if hi >= start && lo < end => {
                    // Straddling: filter the wrapper's own runs in place.
                    let before = self.nodes[c].children.len();
                    let nodes = &self.nodes;
                    let survivors: Vec<usize> = self.nodes[c]
                        .children
                        .iter()
                        .copied()
                        .filter(|&r| {
                            nodes[r]
                                .dom_node_id
                                .and_then(|d| ordinal_of.get(&d).copied())
                                .is_none_or(|o| o < start || o >= end)
                        })
                        .collect();
                    if survivors.len() != before {
                        self.nodes[c].children = survivors;
                        self.nodes[c].inline_layout_result = None;
                        self.nodes[c].taffy_cache = TaffyCache::new();
                        self.nodes[c].measured_content_sizes = (None, None);
                        self.nodes[c].dirty_flag = DirtyFlag::Layout;
                        changed = true;
                    }
                    kept.push(c);
                }
                _ => kept.push(c), // wholly outside, or opaque: keep
            }
        }
18
        if changed {
18
            self.nodes[idx].children = kept;
18
            self.nodes[idx].inline_layout_result = None;
18
            self.nodes[idx].dirty_flag = DirtyFlag::Layout;
18
        }
18
    }
    /// Merge preview: `second_dom` disappears from its parent; its layout
    /// children move (subtrees wholesale) onto the END of `first_dom`.
9
    fn apply_merge_preview(&mut self, first_dom: NodeId, second_dom: NodeId) {
        // Unmapped or exotic multi-part mappings are staged.
9
        let ((Some(&[first_idx]), Some(&[second_idx]))) = (
9
            self.dom_to_layout.get(&first_dom).map(Vec::as_slice),
9
            self.dom_to_layout.get(&second_dom).map(Vec::as_slice),
        ) else {
            return;
        };
9
        let moved = core::mem::take(&mut self.nodes[second_idx].children);
18
        for &m in &moved {
9
            self.nodes[m].parent = Some(first_idx);
9
            self.nodes[m].dirty_flag = DirtyFlag::Layout;
9
        }
9
        self.nodes[first_idx].children.extend(moved);
9
        self.nodes[first_idx].inline_layout_result = None;
9
        self.nodes[first_idx].dirty_flag = DirtyFlag::Layout;
        // Detach `second` itself from its parent (unreachable = unpainted).
9
        if let Some(p) = self.nodes[second_idx].parent {
18
            self.nodes[p].children.retain(|&c| c != second_idx);
9
            self.nodes[p].dirty_flag = DirtyFlag::Layout;
        }
9
    }
    /// Reuse an old node's cached layout under a NEW DOM identity.
    ///
    /// `new_dom_id` is what the node IS; the clone only carries what it
    /// COSTS (used size, taffy cache, inline layout result). Reconciliation
    /// matches an old child to a new one by DOM id where it can, but falls
    /// back to POSITION when the id is absent from the old child list — and
    /// a positional match pairs nodes with different ids. Copying the old
    /// id through then produced a layout node claiming to be a DOM node
    /// that has moved, or (when the DOM shrank) one that no longer exists:
    /// `compute_counters` panicked indexing `node_data[52]` on a 37-node
    /// `StyledDom`, and every quieter consumer — style lookups, node rects,
    /// damage attribution — silently read a DIFFERENT node's data.
    ///
    /// Passing `None` keeps the node anonymous (no DOM identity), which is
    /// correct for pure layout wrappers.
39199
    pub fn clone_node_from_old(
39199
        &mut self,
39199
        old_node: &LayoutNode,
39199
        parent: Option<usize>,
39199
        new_dom_id: Option<NodeId>,
39199
    ) -> usize {
39199
        let index = self.nodes.len();
39199
        let mut new_node = old_node.clone();
39199
        new_node.parent = parent;
        new_node.parent_formatting_context =
39199
            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context));
39199
        new_node.children = Vec::new();
39199
        new_node.dirty_flag = DirtyFlag::None;
        // The measurement cache does NOT survive the clone.
        //
        // A clone is taken because the node's own data is unchanged — but
        // "unchanged" says nothing about its SURROUNDINGS. Its cached
        // (available space -> computed size) entries were measured against
        // the previous frame's siblings, and a clone is taken precisely when
        // a sibling changed enough to re-lay the parent out. `layout_document`
        // clears this cache for `intrinsic_dirty` nodes; the clean clones
        // beside them kept theirs.
        //
        // Symptom: clicking ribbon tab 1 left tab 2 — the tab that never
        // changed state — drawing its label 1.5px lower than a fresh render
        // of the same state, with an IDENTICAL header box. The cached entry
        // was answering with a measurement taken while tab 0 was active.
        //
        // What the clone is FOR is still preserved: the tree structure, the
        // fingerprint, and `inline_layout_result` — so the text is not
        // re-shaped, only re-measured. Measured flat on
        // layout/tests/frame_perf.rs (idle 19.21 vs 19.70 ms, cold 75.96 vs
        // 74.57 ms, edit 22.72 vs 25.39 ms — all inside run-to-run noise).
39199
        new_node.taffy_cache.clear();
39199
        new_node.measured_content_sizes = (None, None);
39199
        new_node.dom_node_id = new_dom_id;
39199
        self.nodes.push(new_node);
39199
        if let Some(p) = parent {
38462
            self.nodes[p].children.push(index);
38462
        }
39199
        if let Some(dom_id) = new_dom_id {
39198
            self.dom_to_layout.entry(dom_id).or_default().push(index);
39198
        }
39199
        index
39199
    }
    #[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
6449
    #[must_use] pub fn build(self, root_idx: usize) -> LayoutTree {
6449
        let nodes = self.nodes;
6449
        let node_count = nodes.len();
        // Flatten per-node children Vecs into a single contiguous arena.
222656
        let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
6449
        let mut arena = Vec::with_capacity(total_children);
6449
        let mut offsets = Vec::with_capacity(node_count);
        // Split monolithic LayoutNodes into hot/warm/cold SoA arrays
6449
        let mut hot_nodes = Vec::with_capacity(node_count);
6449
        let mut warm_nodes = Vec::with_capacity(node_count);
6449
        let mut cold_nodes = Vec::with_capacity(node_count);
229105
        for node in nodes {
222656
            // Flatten children into arena first
222656
            let start = arena.len() as u32;
222656
            let len = node.children.len() as u32;
222656
            arena.extend_from_slice(&node.children);
222656
            offsets.push((start, len));
222656

            
222656
            // Split into hot/warm/cold
222656
            let (hot, warm, cold) = node.split();
222656
            hot_nodes.push(hot);
222656
            warm_nodes.push(warm);
222656
            cold_nodes.push(cold);
222656
        }
        // discriminant). If len>0 but calculate_intrinsic_recursive's
        // `tree.get(root).ok_or(InvalidTree)?` still errors, that `?`/null-check
        // mis-discriminates Some→None. If len==0, build's input was empty.
        // if build>0 but get_node_size sees 0, the tree.clone() (hashbrown) drops the map.
        LayoutTree {
6449
            nodes: hot_nodes,
6449
            warm: warm_nodes,
6449
            cold: cold_nodes,
6449
            root: root_idx,
6449
            dom_to_layout: self
6449
                .dom_to_layout
6449
                .into_iter()
220766
                .map(|(k, v)| (k, v.into_iter().map(LayoutNodeId::new).collect()))
6449
                .collect(),
6449
            children_arena: arena,
6449
            children_offsets: offsets,
            // Populated by `generate_layout_tree` after the tree is built,
            // since the computation needs styled_dom for float/position lookup.
6449
            subtree_needs_intrinsic: Vec::new(),
        }
6449
    }
}
/// DOM ordinal per direct child of `parent` — the coordinate structural
/// previews use to route layout children (`NodePosition.child_index` space).
45
fn ordinal_map(
45
    hierarchy: &azul_core::id::NodeDataContainerRef<'_, azul_core::styled_dom::NodeHierarchyItem>,
45
    parent: NodeId,
45
) -> BTreeMap<NodeId, u32> {
45
    let mut ordinal_of = BTreeMap::new();
45
    let mut c = hierarchy.get(parent).and_then(|h| h.first_child_id(parent));
45
    let mut ord: u32 = 0;
180
    while let Some(cc) = c {
135
        ordinal_of.insert(cc, ord);
135
        ord += 1;
135
        c = hierarchy
135
            .get(cc)
135
            .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id);
135
    }
45
    ordinal_of
45
}
// +spec:display-property:697082 - outer display type determines principal box's role in flow layout (block vs inline)
// +spec:display-property:0d251b - Block-level elements: display 'block', 'list-item', 'table' generate block-level boxes
// +spec:display-property:9464be - block-level vs block container distinction: not all block-level boxes are block containers (e.g. replaced elements, flex containers)
516663
#[must_use] pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
222159
    matches!(
516663
        get_display_type(styled_dom, node_id),
        LayoutDisplay::Block
            | LayoutDisplay::FlowRoot
            | LayoutDisplay::Flex
            | LayoutDisplay::Grid
            | LayoutDisplay::Table
            | LayoutDisplay::TableCaption
            | LayoutDisplay::TableRow
            | LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup
            | LayoutDisplay::TableCell
            | LayoutDisplay::ListItem
    )
516663
}
// +spec:display-property:23f111 - Inline-level elements: inline, inline-block, inline-table, inline-flex, inline-grid
/// Checks if a node is inline-level (including text nodes).
/// According to CSS spec, inline-level content includes:
///
/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
/// - Text nodes
/// - Generated content
56204
fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    // Text nodes are always inline-level
56204
    let node_data = &styled_dom.node_data.as_container()[node_id];
56204
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
45567
        return true;
10637
    }
    // Check the display property
9845
    matches!(
10637
        get_display_type(styled_dom, node_id),
        LayoutDisplay::Inline
            | LayoutDisplay::InlineBlock
            | LayoutDisplay::InlineTable
            | LayoutDisplay::InlineFlex
            | LayoutDisplay::InlineGrid
    )
56204
}
// +spec:display-property:c2520b - Block containers with only inline-level children establish IFC; mixed content gets anonymous block wrappers
/// Checks if a block container has only inline-level children.
/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
/// by a block container box that contains no block-level boxes."
// +spec:display-property:75d642 - block container with only inline-level content establishes IFC
// +spec:display-property:c188d6 - IFC: all inline content within a containing block flows together as continuous text
72117
pub(crate) fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
72117
    let hierarchy = styled_dom.node_hierarchy.as_container();
72117
    let Some(node_hier) = hierarchy.get(node_id) else {
2
        return false;
    };
    // Get the first child
72115
    let mut current_child = node_hier.first_child_id(node_id);
    // If there are no children, it's not an IFC (it's empty)
72115
    if current_child.is_none() {
18307
        return false;
53808
    }
    // Check all children
100161
    while let Some(child_id) = current_child {
56188
        let is_inline = is_inline_level(styled_dom, child_id);
56188
        if !is_inline {
            // Found a block-level child
9835
            return false;
46353
        }
        // Move to next sibling
46353
        if let Some(child_hier) = hierarchy.get(child_id) {
46353
            current_child = child_hier.next_sibling_id();
46353
        } else {
            break;
        }
    }
    // All children are inline-level
43973
    true
72117
}
/// Pre-computes all CSS properties needed during layout for a single node.
/// 
/// This is called once per node during layout tree construction, avoiding
/// repeated style lookups during the actual layout pass (O(n) vs O(n²)).
181598
fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
181598
    let styled_node_state = styled_dom
181598
        .styled_nodes
181598
        .as_container()
181598
        .get(dom_id)
181598
        .map(|n| n.styled_node_state)
181598
        .unwrap_or_default();
    // Get display property
181598
    let display = match get_display_property(styled_dom, Some(dom_id)) {
181598
        MultiValue::Exact(d) => d,
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
    };
    // Get position property
181598
    let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get float property  
181598
    let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get overflow properties
    // +spec:overflow:48890c - overflow:hidden treated as overflow:clip on replaced elements
181598
    let is_replaced = matches!(
181598
        styled_dom.node_data.as_container()[dom_id].get_node_type(),
        NodeType::Image(_) | NodeType::VirtualView
    );
181598
    let overflow_x = {
181598
        let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
181598
        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
    };
181598
    let overflow_y = {
181598
        let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
181598
        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
    };
    // Get writing mode, direction, and text-orientation
    // +spec:writing-modes:2af307 - Propagate used writing-mode from <body> to <html> root
181598
    let writing_mode = {
181598
        let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
181598
        let nd = &styled_dom.node_data.as_container()[dom_id];
181598
        if matches!(nd.node_type, NodeType::Html) {
            // If root <html>, propagate writing-mode from first <body> child
1482
            styled_dom
1482
                .node_hierarchy
1482
                .as_container()
1482
                .get(dom_id)
1482
                .and_then(|node| node.first_child_id(dom_id))
1482
                .and_then(|child_id| {
1482
                    let child_data = &styled_dom.node_data.as_container()[child_id];
1482
                    if matches!(child_data.node_type, NodeType::Body) {
1473
                        let child_state = &styled_dom
1473
                            .styled_nodes
1473
                            .as_container()[child_id]
1473
                            .styled_node_state;
1473
                        Some(get_writing_mode(styled_dom, child_id, child_state)
1473
                            .unwrap_or_default())
                    } else {
9
                        None
                    }
1482
                })
1482
                .unwrap_or(own_wm)
        } else {
180116
            own_wm
        }
    };
181598
    let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
181598
    let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get text-align
181598
    let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get explicit width/height (None = auto)
181598
    let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
15531
        MultiValue::Exact(w) => Some(w),
166067
        _ => None,
    };
181598
    let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
32625
        MultiValue::Exact(h) => Some(h),
148973
        _ => None,
    };
    // Get min/max constraints
181598
    let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
4209
        MultiValue::Exact(v) => Some(v),
177389
        _ => None,
    };
181598
    let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
186
        MultiValue::Exact(v) => Some(v),
181412
        _ => None,
    };
181598
    let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
19
        MultiValue::Exact(v) => Some(v),
181579
        _ => None,
    };
181598
    let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
1
        MultiValue::Exact(v) => Some(v),
181597
        _ => None,
    };
181598
    ComputedLayoutStyle {
181598
        display,
181598
        position,
181598
        float,
181598
        overflow_x,
181598
        overflow_y,
181598
        writing_mode,
181598
        direction,
181598
        text_orientation,
181598
        width,
181598
        height,
181598
        min_width,
181598
        min_height,
181598
        max_width,
181598
        max_height,
181598
        text_align,
181598
    }
181598
}
// hash_node_data() removed — replaced by NodeDataFingerprint::compute()
/// Helper function to get element's computed font-size
673440
pub(crate) fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
673440
    { let _ = (0xC3_000001u32); } // 2-arg wrapper entered
673440
    let node_state = styled_dom
673440
        .styled_nodes
673440
        .as_container()
673440
        .get(dom_id)
673440
        .map(|n| &n.styled_node_state)
673440
        .copied()
673440
        .unwrap_or_default();
673440
    { let _ = (0xC3_000002u32); } // after node_state (clone); next = 3-arg call
673440
    crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
673440
}
/// Helper function to get parent's computed font-size
181627
fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
181627
    styled_dom
181627
        .node_hierarchy
181627
        .as_container()
181627
        .get(dom_id)
181627
        .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
181627
        .map_or(DEFAULT_FONT_SIZE, |parent_id| get_element_font_size(styled_dom, parent_id))
181627
}
/// Helper function to get root element's font-size
275715
pub(crate) fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
    // Root is always NodeId(0) in Azul
275715
    get_element_font_size(styled_dom, NodeId::new(0))
275715
}
/// Create a `ResolutionContext` for a given node
181616
fn create_resolution_context(
181616
    styled_dom: &StyledDom,
181616
    dom_id: NodeId,
181616
    containing_block_size: Option<PhysicalSize>,
181616
    viewport_size: LogicalSize,
181616
) -> ResolutionContext {
181616
    { let _ = (0xC1_000001u32); } // create_resolution_context entered
181616
    let element_font_size = get_element_font_size(styled_dom, dom_id);
181616
    { let _ = (0xC1_000002u32); } // after get_element_font_size
181616
    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
181616
    { let _ = (0xC1_000003u32); } // after get_parent_font_size
181616
    let root_font_size = get_root_font_size(styled_dom);
181616
    { let _ = (0xC1_000004u32); } // after get_root_font_size
181616
    ResolutionContext {
181616
        vertical_writing_mode: false,
181616
        element_font_size,
181616
        parent_font_size,
181616
        root_font_size,
181616
        // +spec:box-model:ec6466 - percentage margins/padding resolve to 0 when containing block is unknown (intrinsic sizing), breaking cyclic dependencies per css-sizing-3 §5.2.1
181616
        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
181616
        element_size: None, // Not yet laid out
181616
        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
181616
    }
181616
}
/// Result of collecting box properties from the styled DOM.
struct CollectedBoxProps {
    unresolved: crate::solver3::geometry::UnresolvedBoxProps,
    resolved: BoxProps,
}
/// Collects box properties from the styled DOM and returns both unresolved and resolved forms.
///
/// The unresolved form stores the raw CSS values for later re-resolution when
/// the containing block size is known. The resolved form is an initial resolution
/// using `viewport_size` for viewport-relative units.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
181605
fn collect_box_props(
181605
    styled_dom: &StyledDom,
181605
    dom_id: NodeId,
181605
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
181605
    viewport_size: LogicalSize,
181605
) -> CollectedBoxProps {
    use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
    #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
    use crate::solver3::getters::*;
    use azul_css::props::style::border::BorderStyle;
    // before create_node step A is the diverging call.
181605
    { let _ = (0xC0_000001u32); } // entered
181605
    let node_data = &styled_dom.node_data.as_container()[dom_id];
    // Get styled node state
181605
    let node_state = styled_dom
181605
        .styled_nodes
181605
        .as_container()
181605
        .get(dom_id)
181605
        .map(|n| &n.styled_node_state)
181605
        .copied()
181605
        .unwrap_or_default();
181605
    { let _ = (0xC0_000002u32); } // after node_state (clone)
    // Create resolution context for this element
    // Note: containing_block_size is None here because we don't have it yet
    // This is fine for initial resolution - will be re-resolved during layout
181605
    let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
181605
    { let _ = (0xC0_000003u32); } // after create_resolution_context
    // Read margin values from styled_dom
181605
    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
181605
    { let _ = (0xC0_000004u32); } // after get_css_margin_top
181605
    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
181605
    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
181605
    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
    // Convert MultiValue to UnresolvedMargin
726420
    let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
726420
        match mv {
54
            MultiValue::Auto => UnresolvedMargin::Auto,
726366
            MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
            _ => UnresolvedMargin::Zero,
        }
726420
    };
    // Build unresolved margins
181605
    let unresolved_margin = UnresolvedEdge {
181605
        top: to_unresolved_margin(&margin_top_mv),
181605
        right: to_unresolved_margin(&margin_right_mv),
181605
        bottom: to_unresolved_margin(&margin_bottom_mv),
181605
        left: to_unresolved_margin(&margin_left_mv),
181605
    };
181605
    { let _ = (0xC0_000005u32); } // after margin block
    // Read padding values
181605
    let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
181605
    let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
181605
    let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
181605
    let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
    // Convert MultiValue to PixelValue (default to 0px)
831845
    let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
831845
        match mv {
831841
            MultiValue::Exact(pv) => pv,
4
            _ => PixelValue::const_px(0),
        }
831845
    };
    // Build unresolved padding
181605
    let unresolved_padding = UnresolvedEdge {
181605
        top: to_pixel_value(padding_top_mv),
181605
        right: to_pixel_value(padding_right_mv),
181605
        bottom: to_pixel_value(padding_bottom_mv),
181605
        left: to_pixel_value(padding_left_mv),
181605
    };
181605
    { let _ = (0xC0_000056u32); } // after padding getters+values, before get_display_type
    // +spec:table-layout:038f9d - padding does not apply to table-row-group, table-header-group, table-footer-group, table-row, table-column-group, table-column
    // Non-cell internal table elements (rows, row groups, columns, column groups) do not have padding.
    // 0xC0_57<dt> the CALL returned (dt = LayoutDisplay discriminant) and the MATCH below
    // diverges; if it stays 0x56, get_display_type (the enum extraction) itself diverges.
    // M12.7 NOTE: get_display_type RETURNS a valid dt here (captured =2), but the code
    // immediately after diverges — and replacing the `match` below with a branchless
    // bitmask test did NOT help (so it's NOT the multi-way-branch codegen). So the
    // get_display_type CALL corrupts the caller frame / control flow (same class as
    // create_node's return 0→48704), specific to ENUM-returning getters (pixel getters
    // like get_css_margin_* lift fine). Remill-level. The match is kept (original).
181605
    let unresolved_padding = match get_display_type(styled_dom, dom_id) {
        LayoutDisplay::TableRow
        | LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableColumn
386
        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
386
            top: PixelValue::const_px(0),
386
            right: PixelValue::const_px(0),
386
            bottom: PixelValue::const_px(0),
386
            left: PixelValue::const_px(0),
386
        },
181219
        _ => unresolved_padding,
    };
181605
    { let _ = (0xC0_000006u32); } // after padding block
    // Read border values
181605
    let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
181605
    let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
181605
    let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
181605
    let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
    // +spec:box-model:17c0e0 - computed border-width is 0 if border-style is none or hidden
    // +spec:box-model:5d2b66 - border-style none/hidden means no border
    // CSS 2.2 §8.5.1: "Computed value: absolute length; '0' if the border style is 'none' or 'hidden'"
726420
    let style_zeroes_width = |s: BorderStyle| matches!(s, BorderStyle::None | BorderStyle::Hidden);
    // Read border styles to check if widths should be zeroed.
    // FAST PATH: compact cache returns styles directly for normal state — no
    // cascade walks. Prior code here did 4 cascade walks × 586 nodes.
181605
    let (bs_top, bs_right, bs_bottom, bs_left) = {
181605
        let cache_ptr = &styled_dom.css_property_cache.ptr;
181605
        if node_state.is_normal() {
181605
            cache_ptr.compact_cache.as_ref().map_or_else(|| (
1
                    cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
1
                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
1
                    cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
1
                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
1
                    cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
1
                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
1
                    cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
1
                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
181604
                ), |cc| {
181604
                let idx = dom_id.index();
181604
                (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
181604
                 cc.get_border_bottom_style(idx), cc.get_border_left_style(idx))
181604
            })
        } else {
            (
                cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
                cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
                cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
                cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
            )
        }
    };
    // Build unresolved border, zeroing width when style is none or hidden
181605
    let unresolved_border = UnresolvedEdge {
181605
        top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
181605
        right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
181605
        bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
181605
        left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
    };
181605
    { let _ = (0xC0_000007u32); } // after border block (incl is_normal/compact_cache fast-path)
    // +spec:box-model:8538a9 - Internal table elements do not have margins (CSS 2.2 §17.5)
    // "These boxes have content and borders and cells have padding as well.
    //  Internal table elements do not have margins."
    // +spec:box-model:b4923a - Internal table elements do not have margins (CSS 2.2 § 17.5)
    // +spec:box-model:0a9f8e - Internal table elements do not have margins (CSS 2.2 § 17.5)
181605
    let display_type = get_display_type(styled_dom, dom_id);
181605
    let unresolved_margin = match display_type {
        LayoutDisplay::TableRow
        | LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableCell
        | LayoutDisplay::TableColumn
1238
        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
1238
            top: UnresolvedMargin::Zero,
1238
            right: UnresolvedMargin::Zero,
1238
            bottom: UnresolvedMargin::Zero,
1238
            left: UnresolvedMargin::Zero,
1238
        },
        // +spec:box-model:1197a5 - height property does not apply to non-replaced inline elements; vertical margins zeroed
        // +spec:replaced-elements:f07118 - non-replaced elements have rendering dictated by CSS model
        // "These properties apply to all elements, but vertical margins will not have
        //  any effect on non-replaced inline elements."
        LayoutDisplay::Inline => {
52784
            let is_replaced = matches!(
52784
                node_data.get_node_type(),
                NodeType::Image(_) | NodeType::VirtualView
            );
52784
            if is_replaced {
                unresolved_margin
            } else {
52784
                UnresolvedEdge {
52784
                    top: UnresolvedMargin::Zero,
52784
                    bottom: UnresolvedMargin::Zero,
52784
                    ..unresolved_margin
52784
                }
            }
        },
127583
        _ => unresolved_margin,
    };
    // Build the UnresolvedBoxProps
181605
    let unresolved = UnresolvedBoxProps {
181605
        margin: unresolved_margin,
181605
        padding: unresolved_padding,
181605
        border: unresolved_border,
        // Percentage basis axis for margin/padding (writing-modes-4 §7.2).
181578
        vertical_writing_mode: matches!(
181605
            get_writing_mode(styled_dom, dom_id, &node_state),
            MultiValue::Exact(
                azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl
                    | azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr
            )
        ),
    };
    // Create initial resolution params (with viewport as containing block for now)
181605
    let params = crate::solver3::geometry::ResolutionParams {
181605
        containing_block: viewport_size,
181605
        viewport_size,
181605
        element_font_size: context.parent_font_size,
181605
        root_font_size: context.root_font_size,
181605
    };
    // Resolve to get initial box_props
181605
    let resolved = unresolved.resolve(&params);
181605
    if let Some(msgs) = debug_messages.as_mut() {
171303
        msgs.push(LayoutDebugMessage::box_props(format!(
171303
            "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
171303
            dom_id.index(), node_data.node_type,
            resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
            resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
            resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
        )));
171303
        let has_vh = match &unresolved_margin.top {
122262
            UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
49041
            _ => false,
        };
171303
        if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
35051
            msgs.push(LayoutDebugMessage::box_props(format!(
35051
                "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
35051
                dom_id, node_data.node_type,
35051
                unresolved_margin.top,
35051
                resolved.margin.top,
35051
                viewport_size
35051
            )));
136252
        }
171303
        msgs.push(LayoutDebugMessage::box_props(format!(
171303
            "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
            dom_id, node_data.node_type,
            resolved.margin_auto.left, resolved.margin_auto.right,
            resolved.margin_auto.top, resolved.margin_auto.bottom,
            unresolved_margin.left
        )));
171303
        if matches!(node_data.node_type, NodeType::Body) {
4067
            msgs.push(LayoutDebugMessage::box_props(format!(
4067
                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
4067
                resolved.margin.top, resolved.margin.right,
4067
                resolved.margin.bottom, resolved.margin.left
4067
            )));
167236
        }
10302
    }
181605
    CollectedBoxProps { unresolved, resolved }
181605
}
/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
///
/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
/// and do not themselves have 'display' set to a table-related value. In this context,
/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
///
/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
/// Returns true if the node is a text node containing only whitespace characters
/// that would be collapsed away by the white-space property.
// according to the 'white-space' property does not generate any anonymous inline boxes (CSS2§9.2.2.1)
258220
#[must_use] pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
258220
    let binding = styled_dom.node_data.as_container();
258220
    let node_data = binding.get(node_id);
258220
    if let Some(data) = node_data {
258218
        if let NodeType::Text(text) = data.get_node_type() {
            // Check if the text contains only CSS document white space characters
            // Per CSS Text 3 §4.1: document white space = U+0020, U+0009, segment breaks
61383
            if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
16101
                return false;
20318
            }
            // Per CSS2§9.2.2.1: "White space content that would subsequently be
            // collapsed away according to the 'white-space' property does not
            // generate any anonymous inline boxes."
            // For white-space: pre / pre-wrap / break-spaces, whitespace is preserved
            // and should NOT be treated as collapsible.
20318
            let white_space = styled_dom
20318
                .styled_nodes
20318
                .as_container()
20318
                .get(node_id)
20318
                .map_or(StyleWhiteSpace::Normal, |n| {
20318
                    match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
20318
                        MultiValue::Exact(ws) => ws,
                        _ => StyleWhiteSpace::Normal,
                    }
20318
                });
20318
            return match white_space {
                // These values collapse whitespace — whitespace-only text is collapsible
20297
                StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
                // These values preserve whitespace — whitespace-only text is NOT collapsible
21
                StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
            };
221799
        }
2
    }
221801
    false
258220
}
/// Anonymous box generation: is this node a whitespace-only text child that
/// must NOT generate a box for this kind of parent?
///
/// - CSS 2.2 section 17.2.1 (tables): whitespace between table structural
///   elements is "irrelevant" and generates nothing.
/// - css-flexbox-1 section 4: "an anonymous flex item that contains only
///   white space is not rendered".
/// - css-grid-1 section 6: same rule for grid items.
///
/// Without the flex/grid arms, every newline between `<div>` children of a
/// grid container became an anonymous grid ITEM, occupying auto-placement
/// cells: grid-minmax-fr-001 rendered its 9 items in a checkerboard with
/// phantom gaps and overflow rows.
42
fn should_skip_for_table_structure(
42
    styled_dom: &StyledDom,
42
    node_id: NodeId,
42
    parent_display: LayoutDisplay,
42
) -> bool {
18
    matches!(
42
        parent_display,
        LayoutDisplay::Table
            | LayoutDisplay::InlineTable
            | LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup
            | LayoutDisplay::TableRow
            | LayoutDisplay::Grid
            | LayoutDisplay::InlineGrid
            | LayoutDisplay::Flex
            | LayoutDisplay::InlineFlex
24
    ) && is_whitespace_only_text(styled_dom, node_id)
42
}
/// Returns true if the given display type is a "proper table child" of a table/inline-table box.
/// Per CSS 2.2 §17.2.1, proper table children are: table-row-group, table-header-group,
/// table-footer-group, table-row, table-column-group, table-column, table-caption.
27
const fn is_proper_table_child(display: LayoutDisplay) -> bool {
19
    matches!(
27
        display,
        LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup
            | LayoutDisplay::TableRow
            | LayoutDisplay::TableColumnGroup
            | LayoutDisplay::TableColumn
            | LayoutDisplay::TableCaption
    )
27
}
// Determines the display type of a node based on its tag and CSS properties.
// Delegates to getters::get_display_property which uses the compact cache fast path.
// M12.7 ROOT: get_display_type (and every layout enum getter) mis-lifts to wasm via the
// remill enum-return/decode path — the geometry-chain blocker. FOUR Rust workarounds all
// FAILED to advance (none reached collect_box_props past get_display_type):
//   1. skip the get_css_property! enum compact-cache fast path  → no change
//   2. replace the LayoutDisplay `match` with a branchless bitmask → no change
//   3. #[inline(never)] (wrap the call w/ enforce_sp_preservation) → made it diverge earlier
//   4. bypass MultiValue<LayoutDisplay> by reading cc.get_display() directly → diverges earlier
// So it is NOT the match codegen, NOT the MultiValue wrapper, NOT a frame/SP issue — it is
// the lift of a fn RETURNING a small fieldless enum (LayoutDisplay) corrupting control flow
// (pixel/i16-returning getters lift fine). Needs the remill m12-q-reg-x8-sret fork's
// enum-return handling — not fixable in Rust. (Original kept.)
1822429
#[must_use] pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
    use crate::solver3::getters::get_display_property;
1822429
    get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
1822429
}
// +spec:display-contents:95faa5 - blockification has no effect on none/contents (other => other)
// +spec:display-property:f68848 - Automatic box type transformations: blockification of computed display values
/// Blockify a display type per CSS Display 3 §2.7.
// +spec:display-property:760c5f - blockification sets computed outer display type to block
/// +spec:display-property:d50f70 - blockification affects computed values, determining principal box type only
/// // +spec:inline-block:692e44 - blockification of inline-block per CSS2 compatibility
// +spec:display-property:c3aca2 - inline-block blockifies to block, not flow-root
// +spec:display-property:ee2d65 - blockification of inline-level display types (CSS Display 3 §2.7)
// +spec:display-property:e4a8b7 - layout-internal boxes blockified to flow (block container)
/// CSS Flexbox §3: flex items with table-internal display values
/// (table-cell, table-row, table-row-group, table-header-group, table-footer-group,
/// table-column, table-column-group, table-caption) are blockified to display:block
/// before anonymous table box generation can occur. E.g. two consecutive
/// display:table-cell flex items become two separate display:block flex items.
259
fn blockify_flex_item_if_table_internal(nodes: &mut [LayoutNode], node_idx: usize) {
259
    if let Some(node) = nodes.get_mut(node_idx) {
255
        let is_table_internal = matches!(
255
            node.formatting_context,
            FormattingContext::TableCell
                | FormattingContext::TableRow
                | FormattingContext::TableRowGroup
                | FormattingContext::TableColumnGroup
                | FormattingContext::TableCaption
                | FormattingContext::Table
        );
255
        if is_table_internal {
6
            node.formatting_context = FormattingContext::Block {
6
                establishes_new_context: true,
6
            };
249
        }
4
    }
259
}
/// Returns true if the node is a replaced element per CSS Display 3 Appendix B.
/// Replaced elements (img, canvas, embed, object, audio, video, input, textarea,
/// select, br, wbr, meter, progress, virtual views) cannot be un-boxed by
/// `display: contents` and always establish an independent formatting context.
13272
const fn is_replaced_element(node_data: &NodeData) -> bool {
13256
    matches!(
13272
        node_data.get_node_type(),
        NodeType::Image(_)
        | NodeType::VirtualView
        | NodeType::Br
        | NodeType::Wbr
        | NodeType::Meter
        | NodeType::Progress
        | NodeType::Canvas
        | NodeType::Embed
        | NodeType::Object
        | NodeType::Audio
        | NodeType::Video
        | NodeType::Input
        | NodeType::TextArea
        | NodeType::Select
    )
13272
}
// +spec:display-property:285fe7 - block box establishing a BFC (block-level block container with new BFC)
/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
/// A BFC contains floats and prevents margin collapse.
28154
fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
28154
    let display = get_display_type(styled_dom, node_id);
14222
    if matches!(
28154
        display,
        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
    ) {
13932
        return true;
14222
    }
14222
    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
        // Only an EXPLICIT overflow of hidden/scroll/auto establishes a BFC. The
        // initial value is `visible` (no BFC), so an unset overflow — which the
        // slow cascade path returns as the `MultiValue::Auto` "not set" sentinel —
        // must NOT trigger one (`!is_visible_or_clip()` wrongly did, since the
        // sentinel is neither visible nor clip).
14222
        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
14222
        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
14222
        if overflow_x.establishes_bfc() || overflow_y.establishes_bfc() {
663
            return true;
13559
        }
13559
        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
13559
        if position.is_absolute_or_fixed() {
1308
            return true;
12251
        }
12251
        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
12251
        if !float.is_none() {
227
            return true;
12024
        }
    }
    // CSS Writing Modes 4 § 3.2: block container with different writing-mode than parent establishes BFC
12024
    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
12024
        let hierarchy = styled_dom.node_hierarchy.as_container();
12024
        if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
7461
            let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
7461
            let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
7461
            let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
7461
            if child_wm != parent_wm {
9
                return true;
7452
            }
4563
        }
    }
    // +spec:replaced-elements:4f494d - replaced elements always establish an independent formatting context
12015
    let node_data = &styled_dom.node_data.as_container()[node_id];
12015
    if is_replaced_element(node_data) {
1
        return true;
12014
    }
    // The root element (<html>) also establishes a BFC.
12014
    if styled_dom.root.into_crate_internal() == Some(node_id) {
4563
        return true;
7451
    }
7451
    false
28154
}
// +spec:display-property:0d93f1 - maps display value to box generation (principal box, none, or contents)
/// Like `determine_formatting_context`, but uses an explicit (possibly blockified) display type
/// instead of reading it from the DOM. Used when blockification changes the display.
// +spec:display-property:80f43f - inner display type defines formatting context for non-replaced elements
// +spec:display-property:46e71c - Maps outer display (block/inline) and inner display (flow/flow-root/table/flex/grid) to FormattingContext
// +spec:display-property:aa582d - maps display types to formatting contexts (inline-level, block-level, atomic inline, block container)
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
149223
fn determine_formatting_context_for_display(
149223
    styled_dom: &StyledDom,
149223
    node_id: NodeId,
149223
    display_type: LayoutDisplay,
149223
) -> FormattingContext {
149223
    let node_data = &styled_dom.node_data.as_container()[node_id];
149223
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
        // [g147h az-web-lift DIAG] CONSTANT marker of the COMPUTED FC per DOM node_id (0x60B60+slot),
        // written WITHOUT reading the stored field. 1=text→Inline, 2=block-with-inline→Inline, 4=Block.
        // For the divs (node_id 1,3): 2 ⇒ computed Inline correctly (bug is store/clone/read); 4 ⇒
        // has_only_inline_children mis-lifted to false (computed Block).
        #[cfg(feature = "web_lift")]
        unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0001) as u32); }
5054
        return FormattingContext::Inline;
144169
    }
    // +spec:display-property:2a8d62 - block containers with inline-level content establish an IFC
144169
    match display_type {
        // +spec:display-property:37bcf3 - inline outer display type generates an inline box
        // +spec:display-property:30a935 - outer display without inner defaults to flow (block/inline both use flow context)
1391
        LayoutDisplay::Inline => FormattingContext::Inline,
        // +spec:block-formatting-context:97b03b - flow-root always establishes a new BFC; block/list-item may establish one based on other conditions
        // +spec:display-property:0bac26 - list-item limited to flow layout inner types (block/flow-root)
        // +spec:display-property:0beffc - block container with only inline children establishes IFC
        // +spec:display-property:7c49c1 - block container with only inline children establishes an IFC
        // +spec:display-property:90ba2a - flow-root always establishes a new BFC
3
        LayoutDisplay::FlowRoot => FormattingContext::Block {
3
            establishes_new_context: true,
3
        },
        LayoutDisplay::Block | LayoutDisplay::ListItem => {
72112
            if has_only_inline_children(styled_dom, node_id) {
                #[cfg(feature = "web_lift")]
                unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0002) as u32); }
43972
                FormattingContext::Inline
            } else {
                #[cfg(feature = "web_lift")]
                unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0004) as u32); }
28140
                FormattingContext::Block {
28140
                    establishes_new_context: establishes_new_block_formatting_context(
28140
                        styled_dom, node_id,
28140
                    ),
28140
                }
            }
        }
14198
        LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
        // +spec:display-property:723fe8 - CSS 2.2 §17.2 table model: display types map to formatting contexts, table-column/column-group not rendered, anonymous table objects generated
        // +spec:table-layout:023714 - map display values to table formatting contexts per CSS 2.2 §17.2
        // +spec:table-layout:6c5039 - row-primary table model: rows/cells/captions/columns mapped here
        // +spec:table-layout:75eea9 - display property values for table elements (table, tr, td, etc.)
        // +spec:table-layout:3ee121 - layout-internal display types map to table formatting context
        // +spec:display-property:b02b7f - table display types map to table formatting contexts;
        // table-column/table-column-group not rendered (treated as display:none for box generation)
342
        LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
        LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
36
        | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
355
        LayoutDisplay::TableRow => FormattingContext::TableRow,
854
        LayoutDisplay::TableCell => FormattingContext::TableCell,
        // +spec:display-property:da3fc7 - display:none/contents generate no boxes (no inner/outer display types)
        // +spec:display-property:e370af - display:none generates no boxes or text sequences
3
        LayoutDisplay::None => FormattingContext::None,
54821
        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
3
        LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
3
        LayoutDisplay::TableCaption => FormattingContext::TableCaption,
33
        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
        // table-column elements are used only for column styling, not for generating boxes
4
        LayoutDisplay::TableColumn => FormattingContext::None,
        // +spec:display-contents:584072 - no special behavior for legend/HTML elements; contents handled normally
        // display:contents - element generates no box, children are promoted to parent
5
        LayoutDisplay::Contents => FormattingContext::Contents,
        // +spec:display-property:b89b80 - run-in box falls back to block (merging into next block not implemented)
        // +spec:display-property:ccd4e6 - run-in falls back to block; reparenting not implemented
        // These less common display types default to block behavior
        // +spec:display-property:7d77f5 - run-in treated as block (run-in sequencing fixup not yet implemented)
        // +spec:display-property:0c30c4 - run-in boxes fall back to block (run-in reparenting not implemented, matches browser behavior)
        // +spec:display-property:2f5c52 - run-in treated as block (full run-in merging not implemented)
        LayoutDisplay::RunIn | LayoutDisplay::Marker => {
6
            FormattingContext::Block {
6
                establishes_new_context: true,
6
            }
        }
    }
149223
}
/// The logic now correctly identifies all BFC roots.
181591
fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
181591
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // [g147j az-web-lift DIAG] OUTER determine_ entry (0x60BB0+slot): 1=Text early-exit,
    // 0x10|disc = went through for_display and returned that repr(C,u8) discriminant.
    // Discriminates "never called during the lifted build" (slot stays 0) vs "called but
    // the for_display match mis-routes" (here=0x10|x while the g147h inner markers stay 0)
    // vs "value correct at build, corrupted later" (here says Inline, dispatch reads garbage).
181591
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
        #[cfg(feature = "web_lift")]
        unsafe { crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0001); }
51418
        return FormattingContext::Inline;
130173
    }
130173
    let display_type = get_display_type(styled_dom, node_id);
130173
    let fc = determine_formatting_context_for_display(styled_dom, node_id, display_type);
    #[cfg(feature = "web_lift")]
    unsafe {
        let disc: u8 = core::ptr::read_volatile((&fc) as *const FormattingContext as *const u8);
        crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0010 | disc as u32);
    }
130173
    fc
181591
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
    use azul_core::{
        dom::{Dom, IdOrClass},
        resources::{ImageRef, RawImageFormat},
        selection::ContentIndex,
    };
    use super::*;
    use crate::{
        solver3::geometry::{EdgeSizes, PackedBoxProps},
        text3::{
            cache::{
                BidiDirection, BreakType, ClearType, ClusterFlags, GlyphKind, GraphemeClusterId,
                InlineBreak, LayoutFontMetrics, OverflowInfo, Point, PositionedItem, Rect,
                ShapedCluster, ShapedGlyph, ShapedItem, StyleProperties,
            },
            script::Script,
        },
    };
    // ==================================================================
    // Fixtures
    // ==================================================================
    const VIEWPORT: LogicalSize = LogicalSize {
        width: 800.0,
        height: 600.0,
    };
    fn styled(dom: Dom, css_str: &str) -> StyledDom {
        let mut dom = dom;
        let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
        StyledDom::create(&mut dom, css)
    }
    fn div_class(class: &str) -> Dom {
        Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
    }
    /// Runs the real build pipeline (`process_node` → `build` → STF bitmap) —
    /// i.e. everything `generate_layout_tree` does minus the `LayoutContext`
    /// (which would need a system-font `FontManager`).
    fn build_tree(styled_dom: &StyledDom) -> LayoutTree {
        let mut builder = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs: Option<Vec<LayoutDebugMessage>> = None;
        let root_id = styled_dom
            .root
            .into_crate_internal()
            .unwrap_or(NodeId::ZERO);
        let root_index = builder
            .process_node(styled_dom, root_id, None, &mut msgs)
            .expect("process_node on a well-formed DOM");
        let mut tree = builder.build(root_index);
        tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(styled_dom, &tree);
        tree
    }
    /// `body(0) > [ .block(1) > [ text "hello"(2), .inline(3) > text "world"(4) ],
    ///              .mixed(5) > [ text " \n\t"(6), .block2(7), text "tail"(8) ] ]`
    ///
    /// The `.mixed` subtree is the interesting one: a whitespace-only inline run
    /// followed by a block sibling and a real inline run — exactly the CSS 2.1
    /// §9.2.2.1 anonymous-box case.
    fn mixed_dom() -> StyledDom {
        styled(
            Dom::create_body()
                .with_child(
                    div_class("block")
                        .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("hello"))
                        .with_child(div_class("inline").with_child(Dom::create_text_do_not_use_without_block_level_wrapper("world"))),
                )
                .with_child(
                    div_class("mixed")
                        .with_child(Dom::create_text_do_not_use_without_block_level_wrapper(" \n\t"))
                        .with_child(div_class("block2"))
                        .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("tail")),
                ),
            ".block { display: block; } .inline { display: inline; } .mixed { display: block; } \
             .block2 { display: block; }",
        )
    }
    /// Locates a DOM node by its exact text content. Keeps the tests structural
    /// instead of hard-coding `CompactDom` pre-order indices.
    fn text_node(styled_dom: &StyledDom, needle: &str) -> NodeId {
        let container = styled_dom.node_data.as_container();
        for i in 0..styled_dom.node_data.len() {
            let id = NodeId::new(i);
            if let NodeType::Text(text) = container[id].get_node_type() {
                if text.as_str() == needle {
                    return id;
                }
            }
        }
        panic!("no text node with content {needle:?}");
    }
    fn empty_layout() -> Arc<UnifiedLayout> {
        Arc::new(UnifiedLayout {
            items: Vec::new(),
            overflow: OverflowInfo::default(),
        })
    }
    fn layout_of(items: Vec<PositionedItem>) -> Arc<UnifiedLayout> {
        Arc::new(UnifiedLayout {
            items,
            overflow: OverflowInfo::default(),
        })
    }
    fn tab_item(width: f32, height: f32, x: f32, line_index: usize) -> PositionedItem {
        PositionedItem {
            item: ShapedItem::Tab {
                source: ContentIndex {
                    run_index: 0,
                    item_index: 0,
                },
                bounds: Rect {
                    x: 0.0,
                    y: 0.0,
                    width,
                    height,
                },
            },
            position: Point { x, y: 0.0 },
            line_index,
        }
    }
    /// A positioned text cluster with `n_glyphs` degenerate glyphs, all on
    /// one shared default style/font — so same-line clusters MERGE into one
    /// paint run (the report fixtures need runs that actually hold glyphs;
    /// `tab_item` yields none).
    fn glyph_cluster_item(text: &str, n_glyphs: usize, x: f32, line_index: usize) -> PositionedItem {
        let glyphs: Vec<ShapedGlyph> = (0..n_glyphs)
            .map(|i| ShapedGlyph {
                kind: GlyphKind::Character,
                glyph_id: u16::try_from(i + 1).unwrap(),
                cluster_offset: 0,
                advance: 6.0,
                kerning: 0.0,
                offset: Point { x: 0.0, y: 0.0 },
                vertical_advance: 0.0,
                vertical_offset: Point { x: 0.0, y: 0.0 },
                script: Script::Latin,
                font_hash: 0xF0F0,
                font_metrics: LayoutFontMetrics {
                    ascent: 0.0,
                    descent: 0.0,
                    line_gap: 0.0,
                    units_per_em: 0,
                    x_height: None,
                    cap_height: None,
                },
            })
            .collect();
        PositionedItem {
            item: ShapedItem::Cluster(ShapedCluster {
                flags: ClusterFlags::classify(text),
                source_text: Arc::from(text),
                source_byte_len: u16::try_from(text.len()).unwrap(),
                source_cluster_id: GraphemeClusterId {
                    source_run: 0,
                    start_byte_in_run: 0,
                },
                source_content_index: ContentIndex {
                    run_index: 0,
                    item_index: 0,
                },
                source_node_id: None,
                glyphs: glyphs.into_iter().collect(),
                advance: 6.0,
                direction: BidiDirection::Ltr,
                style: Arc::new(StyleProperties::default()),
                marker_position_outside: None,
                is_first_fragment: true,
                is_last_fragment: true,
            }),
            position: Point { x, y: 0.0 },
            line_index,
        }
    }
    fn break_item(line_index: usize) -> PositionedItem {
        PositionedItem {
            item: ShapedItem::Break {
                source: ContentIndex {
                    run_index: 0,
                    item_index: 0,
                },
                break_info: InlineBreak {
                    break_type: BreakType::Hard,
                    clear: ClearType::None,
                    content_index: 0,
                },
            },
            position: Point { x: 0.0, y: 0.0 },
            line_index,
        }
    }
    fn hot(parent: Option<usize>) -> LayoutNodeHot {
        LayoutNodeHot {
            box_props: PackedBoxProps::default(),
            dom_node_id: None,
            used_size: None,
            formatting_context: FormattingContext::Block {
                establishes_new_context: false,
            },
            parent,
        }
    }
    /// Hand-assembles a `LayoutTree` from raw hot nodes + child lists so the
    /// index/cycle edge cases the builder can never produce are still reachable.
    fn raw_tree(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
        let n = nodes.len();
        let mut children_arena: Vec<usize> = Vec::new();
        let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
        for cl in child_lists {
            let start = u32::try_from(children_arena.len()).unwrap();
            children_arena.extend_from_slice(cl);
            children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
        }
        while children_offsets.len() < n {
            children_offsets.push((0, 0));
        }
        LayoutTree {
            nodes,
            warm: vec![LayoutNodeWarm::default(); n],
            cold: vec![LayoutNodeCold::default(); n],
            root: 0,
            dom_to_layout: BTreeMap::new(),
            children_arena,
            children_offsets,
            subtree_needs_intrinsic: Vec::new(),
        }
    }
    const ALL_DISPLAYS: [LayoutDisplay; 23] = [
        LayoutDisplay::None,
        LayoutDisplay::Block,
        LayoutDisplay::Inline,
        LayoutDisplay::InlineBlock,
        LayoutDisplay::Flex,
        LayoutDisplay::InlineFlex,
        LayoutDisplay::Table,
        LayoutDisplay::InlineTable,
        LayoutDisplay::TableRowGroup,
        LayoutDisplay::TableHeaderGroup,
        LayoutDisplay::TableFooterGroup,
        LayoutDisplay::TableRow,
        LayoutDisplay::TableColumnGroup,
        LayoutDisplay::TableColumn,
        LayoutDisplay::TableCell,
        LayoutDisplay::TableCaption,
        LayoutDisplay::FlowRoot,
        LayoutDisplay::ListItem,
        LayoutDisplay::RunIn,
        LayoutDisplay::Marker,
        LayoutDisplay::Grid,
        LayoutDisplay::InlineGrid,
        LayoutDisplay::Contents,
    ];
    // ==================================================================
    // IfcId — thread-local counter (numeric / overflow)
    // ==================================================================
    #[test]
    fn ifcid_unique_hands_out_a_fresh_id_per_call_after_reset() {
        IfcId::reset_counter();
        assert_eq!(IfcId::unique(), IfcId(0));
        assert_eq!(IfcId::unique(), IfcId(1));
        assert_eq!(IfcId::unique(), IfcId(2));
        IfcId::reset_counter();
        assert_eq!(
            IfcId::unique(),
            IfcId(0),
            "reset_counter must restart the sequence, not continue it"
        );
        IfcId::reset_counter();
    }
    #[test]
    fn ifcid_reset_counter_is_idempotent() {
        IfcId::reset_counter();
        IfcId::reset_counter();
        IfcId::reset_counter();
        assert_eq!(IfcId::unique(), IfcId(0));
        IfcId::reset_counter();
    }
    #[test]
    fn ifcid_unique_wraps_at_u32_max_instead_of_panicking() {
        // `wrapping_add` is deliberate: a layout pass with 2^32 IFCs is not a
        // thing, but a debug-mode overflow panic in the middle of layout is.
        IFC_ID_COUNTER.with(|c| c.set(u32::MAX));
        assert_eq!(IfcId::unique(), IfcId(u32::MAX));
        assert_eq!(IfcId::unique(), IfcId(0), "wraps rather than overflow-panics");
        assert_eq!(IfcId::unique(), IfcId(1));
        IfcId::reset_counter();
    }
    // ==================================================================
    // CachedInlineLayout — constructors + metrics extraction
    // ==================================================================
    #[test]
    fn cached_inline_layout_new_keeps_the_args_it_was_given() {
        let arc = empty_layout();
        let c = CachedInlineLayout::new(Arc::clone(&arc), AvailableSpace::Definite(123.5), true);
        assert!(Arc::ptr_eq(&c.layout, &arc));
        assert_eq!(c.available_width, AvailableSpace::Definite(123.5));
        assert!(c.has_floats);
        assert!(c.constraints.is_none(), "new() carries no constraints");
        assert!(c.line_breaks.is_none(), "new() computes no line breaks");
        assert_eq!(c.inline_content_hash, 0, "0 = unknown ⇒ never fast-path-reuse");
        assert!(c.item_metrics.is_empty(), "an empty layout has no item metrics");
    }
    #[test]
    fn cached_inline_layout_new_survives_extreme_widths() {
        for w in [
            AvailableSpace::Definite(0.0),
            AvailableSpace::Definite(-1.0),
            AvailableSpace::Definite(f32::MAX),
            AvailableSpace::Definite(f32::MIN),
            AvailableSpace::Definite(f32::INFINITY),
            AvailableSpace::Definite(f32::NEG_INFINITY),
            AvailableSpace::Definite(f32::NAN),
            AvailableSpace::MinContent,
            AvailableSpace::MaxContent,
        ] {
            let c = CachedInlineLayout::new(empty_layout(), w, false);
            assert!(c.item_metrics.is_empty());
            assert!(c.layout.items.is_empty());
        }
    }
    #[test]
    fn extract_item_metrics_mirrors_every_positioned_item() {
        let layout = layout_of(vec![tab_item(12.0, 20.0, 5.0, 3), tab_item(0.0, 0.0, 0.0, 0)]);
        let m = CachedInlineLayout::extract_item_metrics(&layout);
        assert_eq!(m.len(), 2, "one metric entry per PositionedItem, in order");
        assert_eq!(m[0].advance_width, 12.0);
        assert_eq!(m[0].x_offset, 5.0);
        assert_eq!(m[0].line_index, 3);
        assert!(m[0].can_break, "a Tab is breakable");
        assert!(
            m[0].source_node_id.is_none(),
            "non-Cluster items expose no source_node_id"
        );
        // Tab metrics are the fallback ascent/descent split of the item height.
        assert!(
            (m[0].line_height_contribution - 20.0).abs() < 1e-3,
            "ascent+descent should reconstruct the height, got {}",
            m[0].line_height_contribution
        );
        assert_eq!(m[1].advance_width, 0.0);
        assert_eq!(m[1].line_index, 0);
    }
    #[test]
    fn extract_item_metrics_marks_break_items_as_unbreakable_and_zero_sized() {
        let layout = layout_of(vec![break_item(7)]);
        let m = CachedInlineLayout::extract_item_metrics(&layout);
        assert_eq!(m.len(), 1);
        assert!(!m[0].can_break, "ShapedItem::Break is the one non-breakable item");
        assert_eq!(m[0].advance_width, 0.0, "a break has no visual geometry");
        assert_eq!(m[0].line_height_contribution, 0.0);
        assert_eq!(m[0].line_index, 7);
    }
    #[test]
    fn extract_item_metrics_on_an_empty_layout_is_empty_not_a_panic() {
        assert!(CachedInlineLayout::extract_item_metrics(&empty_layout()).is_empty());
    }
    #[test]
    fn extract_item_metrics_does_not_choke_on_non_finite_item_bounds() {
        let layout = layout_of(vec![
            tab_item(f32::INFINITY, f32::NAN, f32::NEG_INFINITY, u32::MAX as usize),
            tab_item(f32::MAX, f32::MAX, f32::MIN, 0),
        ]);
        let m = CachedInlineLayout::extract_item_metrics(&layout);
        assert_eq!(m.len(), 2);
        assert!(m[0].advance_width.is_infinite());
        assert!(m[0].line_height_contribution.is_nan(), "NaN in, NaN out — but no panic");
        assert_eq!(m[1].advance_width, f32::MAX);
    }
    #[test]
    fn extract_item_metrics_truncates_a_huge_line_index_into_u32() {
        // `line_index` is a usize on PositionedItem but a u32 in the metrics —
        // the cast is `as`, so it wraps rather than panicking.
        let huge = (u32::MAX as usize) + 5;
        let m = CachedInlineLayout::extract_item_metrics(&layout_of(vec![tab_item(
            1.0, 1.0, 0.0, huge,
        )]));
        assert_eq!(m[0].line_index, 4, "wrapping `as u32` truncation, not a panic");
    }
    #[test]
    fn cached_inline_layout_new_with_constraints_records_constraints_and_line_breaks() {
        let arc = layout_of(vec![tab_item(10.0, 20.0, 0.0, 0)]);
        let c = CachedInlineLayout::new_with_constraints(
            Arc::clone(&arc),
            AvailableSpace::Definite(200.0),
            false,
            UnifiedConstraints::default(),
        );
        assert!(c.constraints.is_some());
        let lb = c.line_breaks.expect("new_with_constraints computes line breaks");
        assert_eq!(lb.available_width, 200.0);
        assert_eq!(c.item_metrics.len(), 1);
    }
    #[test]
    fn new_with_constraints_treats_indefinite_widths_as_f32_max() {
        for w in [AvailableSpace::MinContent, AvailableSpace::MaxContent] {
            let c = CachedInlineLayout::new_with_constraints(
                empty_layout(),
                w,
                false,
                UnifiedConstraints::default(),
            );
            let lb = c.line_breaks.expect("line breaks");
            assert_eq!(
                lb.available_width,
                f32::MAX,
                "indefinite width collapses to f32::MAX for break extraction"
            );
            assert_eq!(c.available_width, w, "but the cache key keeps the real variant");
        }
    }
    // ==================================================================
    // CachedInlineLayout — width matching / validity (predicates)
    // ==================================================================
    fn cached(width: AvailableSpace, has_floats: bool) -> CachedInlineLayout {
        CachedInlineLayout::new(empty_layout(), width, has_floats)
    }
    /// d1 retention wiring: mode 0 stores NO dense view; mode 1 (and 2)
    /// store one, and the runs it derives equal the reference's. The
    /// SEMANTIC equality over real shaped content is pinned by
    /// text3_dense_equivalence + the e2e corpus under AZ_DENSE_TEXT=verify;
    /// this pins the CACHE wiring (someone dropping the retention or the
    /// tuple plumbing goes red here).
    #[test]
    fn dense_view_is_retained_exactly_when_the_flag_asks() {
        let l = empty_layout();
        let (runs0, d0) = compute_glyph_runs_with_mode(&l, 0);
        assert!(d0.is_none(), "mode 0 must not retain a dense view");
        let (runs1, d1) = compute_glyph_runs_with_mode(&l, 1);
        assert!(d1.is_some(), "mode 1 must retain the dense view");
        let (runs2, d2) = compute_glyph_runs_with_mode(&l, 2);
        assert!(d2.is_some(), "verify mode must retain the dense view");
        assert_eq!(runs0.len(), runs1.len());
        assert_eq!(runs0.len(), runs2.len());
    }
    #[test]
    fn width_constraint_matches_definite_widths_within_the_epsilon() {
        let c = cached(AvailableSpace::Definite(100.0), false);
        assert!(c.width_constraint_matches(AvailableSpace::Definite(100.0)));
        assert!(
            c.width_constraint_matches(AvailableSpace::Definite(100.09)),
            "sub-0.1px drift must not force a relayout"
        );
        // 100.1 is NOT exactly 0.1 away: the f32 literal is 100.0999984741211, so the
        // real diff is ~0.09999847, which genuinely IS < 0.1. Binary floats, not
        // decimal. (Pick a literal that clears the epsilon after rounding to test the
        // miss branch -- see below.)
        assert!(c.width_constraint_matches(AvailableSpace::Definite(100.1)));
        assert!(
            !c.width_constraint_matches(AvailableSpace::Definite(100.2)),
            "the epsilon is strict (`< 0.1`), so a 0.2 drift must miss"
        );
        assert!(!c.width_constraint_matches(AvailableSpace::Definite(0.0)));
    }
    #[test]
    fn width_constraint_matches_only_pairs_like_with_like() {
        let min = cached(AvailableSpace::MinContent, false);
        let max = cached(AvailableSpace::MaxContent, false);
        let def = cached(AvailableSpace::Definite(50.0), false);
        assert!(min.width_constraint_matches(AvailableSpace::MinContent));
        assert!(max.width_constraint_matches(AvailableSpace::MaxContent));
        assert!(!min.width_constraint_matches(AvailableSpace::MaxContent));
        assert!(!max.width_constraint_matches(AvailableSpace::MinContent));
        assert!(!min.width_constraint_matches(AvailableSpace::Definite(50.0)));
        assert!(!def.width_constraint_matches(AvailableSpace::MinContent));
        assert!(!def.width_constraint_matches(AvailableSpace::MaxContent));
    }
    #[test]
    fn width_constraint_matches_is_false_for_nan_widths_rather_than_panicking() {
        // (NaN - NaN).abs() is NaN, and `NaN < eps` is false — so a NaN-width
        // cache entry never validates. Deterministic (always relayout), not a panic.
        let c = cached(AvailableSpace::Definite(f32::NAN), false);
        assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::NAN)));
        assert!(!c.width_constraint_matches(AvailableSpace::Definite(0.0)));
        let good = cached(AvailableSpace::Definite(10.0), false);
        assert!(!good.width_constraint_matches(AvailableSpace::Definite(f32::NAN)));
    }
    #[test]
    fn width_constraint_matches_is_false_for_an_infinite_width_against_itself() {
        // inf - inf == NaN, so an infinite cached width never matches — the cache
        // simply always misses. Surprising, but safe and deterministic.
        let c = cached(AvailableSpace::Definite(f32::INFINITY), false);
        assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::INFINITY)));
        assert!(!c.is_valid_for(AvailableSpace::Definite(f32::INFINITY), false));
        assert!(c.should_replace_with(AvailableSpace::Definite(f32::INFINITY), false));
    }
    #[test]
    fn width_constraint_matches_handles_huge_finite_widths() {
        let c = cached(AvailableSpace::Definite(f32::MAX), false);
        assert!(c.width_constraint_matches(AvailableSpace::Definite(f32::MAX)));
        assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::MIN)));
    }
    #[test]
    fn is_valid_for_rejects_a_no_float_cache_when_the_request_gains_floats() {
        // A cached layout with no floats must be invalidated once the new request
        // carries floats, so the text re-wraps around them (#19); every other float
        // combination reduces to width_constraint_matches. This mirrors
        // should_replace_with()'s gain-float branch (they must stay consistent).
        let widths = [
            AvailableSpace::Definite(0.0),
            AvailableSpace::Definite(100.0),
            AvailableSpace::MinContent,
            AvailableSpace::MaxContent,
        ];
        for cached_floats in [false, true] {
            for cached_w in widths {
                let c = cached(cached_w, cached_floats);
                for new_w in widths {
                    let width_ok = c.width_constraint_matches(new_w);
                    // No-float request: pure width match (a float-aware cache is
                    // still kept for a no-float request, gated on width).
                    assert_eq!(c.is_valid_for(new_w, false), width_ok);
                    // Float request: a no-float cache is always rejected so the text
                    // re-wraps; a float-aware cache stays gated on width.
                    let expected_with_floats = if cached_floats { width_ok } else { false };
                    assert_eq!(c.is_valid_for(new_w, true), expected_with_floats);
                    // is_valid_for and should_replace_with must stay opposites on the
                    // gain-float axis.
                    if !cached_floats {
                        assert!(c.should_replace_with(new_w, true));
                    }
                }
            }
        }
    }
    #[test]
    fn is_valid_for_returns_the_expected_true_and_false() {
        let c = cached(AvailableSpace::Definite(300.0), false);
        assert!(c.is_valid_for(AvailableSpace::Definite(300.0), false));
        assert!(!c.is_valid_for(AvailableSpace::Definite(299.0), false));
    }
    #[test]
    fn should_replace_with_always_replaces_when_float_info_is_gained() {
        // Even at an identical width: a float-aware layout strictly dominates.
        let c = cached(AvailableSpace::Definite(300.0), false);
        assert!(c.should_replace_with(AvailableSpace::Definite(300.0), true));
        assert!(c.should_replace_with(AvailableSpace::MinContent, true));
    }
    #[test]
    fn should_replace_with_keeps_a_float_aware_layout_at_a_matching_width() {
        let c = cached(AvailableSpace::Definite(300.0), true);
        assert!(
            !c.should_replace_with(AvailableSpace::Definite(300.0), false),
            "a non-float layout must not overwrite a float-aware one at the same width"
        );
        assert!(
            c.should_replace_with(AvailableSpace::Definite(100.0), false),
            "…but a width change still forces a replace"
        );
    }
    #[test]
    fn should_replace_with_is_the_negation_of_is_valid_for_when_floats_are_unchanged() {
        let widths = [
            AvailableSpace::Definite(0.0),
            AvailableSpace::Definite(42.0),
            AvailableSpace::MinContent,
            AvailableSpace::MaxContent,
        ];
        for floats in [false, true] {
            for cached_w in widths {
                let c = cached(cached_w, floats);
                for new_w in widths {
                    assert_eq!(
                        c.should_replace_with(new_w, floats),
                        !c.is_valid_for(new_w, floats),
                        "cached={cached_w:?} new={new_w:?} floats={floats}"
                    );
                }
            }
        }
    }
    // ==================================================================
    // CachedInlineLayout — getters
    // ==================================================================
    #[test]
    fn get_layout_and_clone_layout_hand_back_the_very_same_arc() {
        let arc = layout_of(vec![tab_item(1.0, 2.0, 0.0, 0)]);
        let c = CachedInlineLayout::new(Arc::clone(&arc), AvailableSpace::MaxContent, false);
        assert!(Arc::ptr_eq(c.get_layout(), &arc));
        let cloned = c.clone_layout();
        assert!(Arc::ptr_eq(&cloned, &arc), "clone_layout must not deep-copy");
        assert_eq!(
            Arc::strong_count(&arc),
            4,
            "the original + the cache's + the clone + the DL payload (d5: \
             the payload is built ONCE at store time and holds the layout \
             Arc so TextLayout damage ptr_eq stays stable; 3 -> 4 on \
             2026-08-11)"
        );
        assert_eq!(c.get_layout().items.len(), 1);
    }
    #[test]
    fn get_layout_works_on_an_empty_extreme_instance() {
        let c = cached(AvailableSpace::Definite(f32::NAN), true);
        assert!(c.get_layout().items.is_empty());
        assert!(c.clone_layout().items.is_empty());
    }
    // ==================================================================
    // LayoutNode::split / LayoutTree::get_full_node — round-trip
    // ==================================================================
    #[test]
    fn get_full_node_then_split_round_trips_through_the_soa_arrays() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        assert!(tree.nodes.len() >= 2);
        for i in 0..tree.nodes.len() {
            let full = tree.get_full_node(i).expect("in-range node");
            let (h, w, c) = full.split();
            let hot = tree.get(LayoutNodeId::new(i)).unwrap();
            assert_eq!(h.dom_node_id, hot.dom_node_id, "node {i}");
            assert_eq!(h.parent, hot.parent, "node {i}");
            assert_eq!(h.used_size, hot.used_size, "node {i}");
            assert_eq!(h.formatting_context, hot.formatting_context, "node {i}");
            // box_props survive a pack → unpack → pack round-trip bit-for-bit.
            assert_eq!(h.box_props.margin, hot.box_props.margin, "node {i}");
            assert_eq!(h.box_props.padding, hot.box_props.padding, "node {i}");
            assert_eq!(h.box_props.border, hot.box_props.border, "node {i}");
            let warm = tree.warm(LayoutNodeId::new(i)).unwrap();
            assert_eq!(w.pseudo_element, warm.pseudo_element, "node {i}");
            assert_eq!(w.baseline, warm.baseline, "node {i}");
            assert_eq!(
                w.computed_style.display, warm.computed_style.display,
                "node {i}"
            );
            let cold = tree.cold(LayoutNodeId::new(i)).unwrap();
            assert_eq!(c.anonymous_type, cold.anonymous_type, "node {i}");
            assert_eq!(c.dirty_flag, cold.dirty_flag, "node {i}");
            assert_eq!(c.subtree_hash, cold.subtree_hash, "node {i}");
            assert_eq!(c.ifc_id, cold.ifc_id, "node {i}");
        }
    }
    #[test]
    fn get_full_node_restores_the_children_from_the_arena() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        for i in 0..tree.nodes.len() {
            let full = tree.get_full_node(i).unwrap();
            assert_eq!(full.children, tree.children(i).to_vec(), "node {i}");
        }
    }
    #[test]
    fn get_full_node_is_none_out_of_range() {
        let tree = build_tree(&mixed_dom());
        assert!(tree.get_full_node(tree.nodes.len()).is_none());
        assert!(tree.get_full_node(usize::MAX).is_none());
    }
    // ==================================================================
    // LayoutTree — index-taking accessors (numeric / min-max / overflow)
    // ==================================================================
    #[test]
    fn tree_accessors_return_none_for_every_out_of_range_index() {
        let mut tree = build_tree(&mixed_dom());
        let n = tree.nodes.len();
        for idx in [n, n + 1, usize::MAX, usize::MAX - 1, usize::MAX / 2] {
            assert!(tree.get(LayoutNodeId::new(idx)).is_none(), "get({idx})");
            assert!(tree.warm(LayoutNodeId::new(idx)).is_none(), "warm({idx})");
            assert!(tree.cold(LayoutNodeId::new(idx)).is_none(), "cold({idx})");
            assert!(tree.get_mut(LayoutNodeId::new(idx)).is_none(), "get_mut({idx})");
            assert!(tree.warm_mut(LayoutNodeId::new(idx)).is_none(), "warm_mut({idx})");
            assert!(tree.cold_mut(LayoutNodeId::new(idx)).is_none(), "cold_mut({idx})");
            assert!(tree.get_inline_layout_for_node(idx).is_none());
        }
    }
    #[test]
    fn tree_accessors_all_resolve_at_index_zero() {
        let mut tree = build_tree(&mixed_dom());
        assert!(tree.get(LayoutNodeId::new(0)).is_some());
        assert!(tree.warm(LayoutNodeId::new(0)).is_some());
        assert!(tree.cold(LayoutNodeId::new(0)).is_some());
        assert!(tree.get_mut(LayoutNodeId::new(0)).is_some());
        assert!(tree.warm_mut(LayoutNodeId::new(0)).is_some());
        assert!(tree.cold_mut(LayoutNodeId::new(0)).is_some());
        assert_eq!(tree.get(LayoutNodeId::new(0)).unwrap().parent, None, "index 0 is the root");
    }
    #[test]
    fn children_of_an_out_of_range_index_is_an_empty_slice() {
        let tree = build_tree(&mixed_dom());
        assert!(tree.children(tree.nodes.len()).is_empty());
        assert!(tree.children(usize::MAX).is_empty());
        assert!(tree.children(usize::MAX - 1).is_empty());
    }
    #[test]
    fn children_arena_slices_agree_with_the_parent_pointers() {
        let tree = build_tree(&mixed_dom());
        let n = tree.nodes.len();
        let mut seen: Vec<usize> = Vec::new();
        for i in 0..n {
            for &child in tree.children(i) {
                assert!(child < n, "child {child} of {i} is out of range");
                assert_eq!(
                    tree.get(LayoutNodeId::new(child)).unwrap().parent,
                    Some(i),
                    "child {child} does not point back at parent {i}"
                );
                seen.push(child);
            }
        }
        seen.sort_unstable();
        seen.dedup();
        assert_eq!(seen.len(), n - 1, "every node but the root is someone's child");
        assert!(!seen.contains(&tree.root), "the root is nobody's child");
    }
    #[test]
    fn children_offsets_stay_inside_the_arena() {
        let tree = build_tree(&mixed_dom());
        assert_eq!(tree.children_offsets.len(), tree.nodes.len());
        let total: usize = tree
            .children_offsets
            .iter()
            .map(|&(_, len)| len as usize)
            .sum();
        assert_eq!(total, tree.children_arena.len());
        for &(start, len) in &tree.children_offsets {
            assert!((start as usize) + (len as usize) <= tree.children_arena.len());
        }
    }
    #[test]
    fn get_content_size_is_default_for_an_out_of_range_index() {
        let tree = build_tree(&mixed_dom());
        assert_eq!(tree.get_content_size(LayoutNodeId::new(usize::MAX)), LogicalSize::default());
        assert_eq!(tree.get_content_size(LayoutNodeId::new(tree.nodes.len())), LogicalSize::default());
    }
    #[test]
    fn get_content_size_prefers_the_explicit_overflow_content_size() {
        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
        tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
        tree.warm[0].overflow_content_size = Some(LogicalSize::new(999.0, 888.0));
        assert_eq!(tree.get_content_size(LayoutNodeId::new(0)), LogicalSize::new(999.0, 888.0));
    }
    #[test]
    fn get_content_size_grows_the_used_size_to_cover_the_inline_items() {
        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
        tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
        tree.warm[0].inline_layout_result = Some(Box::new(CachedInlineLayout::new(
            layout_of(vec![tab_item(30.0, 40.0, 25.0, 0)]),
            AvailableSpace::MaxContent,
            false,
        )));
        // item spans x ∈ [25, 55], y ∈ [0, 40]  →  content must cover 55 × 40.
        let cs = tree.get_content_size(LayoutNodeId::new(0));
        assert_eq!(cs.width, 55.0);
        assert_eq!(cs.height, 40.0);
    }
    #[test]
    fn get_content_size_never_shrinks_below_the_used_size() {
        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
        tree.nodes[0].used_size = Some(LogicalSize::new(500.0, 500.0));
        tree.warm[0].inline_layout_result = Some(Box::new(CachedInlineLayout::new(
            layout_of(vec![tab_item(1.0, 1.0, 0.0, 0)]),
            AvailableSpace::MaxContent,
            false,
        )));
        assert_eq!(tree.get_content_size(LayoutNodeId::new(0)), LogicalSize::new(500.0, 500.0));
    }
    #[test]
    fn get_content_size_of_a_node_with_no_used_size_is_zero() {
        let tree = raw_tree(vec![hot(None)], &[vec![]]);
        assert_eq!(tree.get_content_size(LayoutNodeId::new(0)), LogicalSize::default());
    }
    // ==================================================================
    // LayoutTree — IFC navigation
    // ==================================================================
    #[test]
    fn get_ifc_root_layout_index_returns_the_input_unchanged_when_out_of_range() {
        let tree = build_tree(&mixed_dom());
        // Documented contract: no membership ⇒ identity. That must hold for
        // garbage indices too, and it must not panic.
        assert_eq!(tree.get_ifc_root_layout_index(usize::MAX), usize::MAX);
        assert_eq!(tree.get_ifc_root_layout_index(0), 0);
    }
    #[test]
    fn get_ifc_root_layout_index_follows_membership_only_for_non_ifc_roots() {
        let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
        tree.warm[0].inline_layout_result = Some(Box::new(CachedInlineLayout::new(
            empty_layout(),
            AvailableSpace::MaxContent,
            false,
        )));
        tree.warm[1].ifc_membership = Some(IfcMembership {
            ifc_id: IfcId(0),
            ifc_root_layout_index: 0,
            run_index: 0,
        });
        assert_eq!(tree.get_ifc_root_layout_index(1), 0, "a text node anchors to its IFC root");
        assert_eq!(
            tree.get_ifc_root_layout_index(0),
            0,
            "the IFC root itself is its own anchor"
        );
        // A node that owns an inline_layout_result must NOT be redirected, even
        // if it also carries a (stale) membership.
        tree.warm[0].ifc_membership = Some(IfcMembership {
            ifc_id: IfcId(9),
            ifc_root_layout_index: 1,
            run_index: 0,
        });
        assert_eq!(tree.get_ifc_root_layout_index(0), 0);
    }
    #[test]
    fn get_inline_layout_for_node_walks_membership_then_gives_up_cleanly() {
        let mut tree = raw_tree(vec![hot(None), hot(Some(0)), hot(Some(0))], &[vec![1, 2]]);
        let arc = empty_layout();
        tree.warm[0].inline_layout_result = Some(Box::new(CachedInlineLayout::new(
            Arc::clone(&arc),
            AvailableSpace::MaxContent,
            false,
        )));
        tree.warm[1].ifc_membership = Some(IfcMembership {
            ifc_id: IfcId(0),
            ifc_root_layout_index: 0,
            run_index: 0,
        });
        // Node 2 has neither its own layout nor a membership.
        assert!(Arc::ptr_eq(tree.get_inline_layout_for_node(0).unwrap(), &arc));
        assert!(Arc::ptr_eq(tree.get_inline_layout_for_node(1).unwrap(), &arc));
        assert!(tree.get_inline_layout_for_node(2).is_none());
    }
    #[test]
    fn get_inline_layout_for_node_is_none_when_membership_dangles() {
        // A membership pointing at a bogus root index must return None, not panic.
        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
        tree.warm[0].ifc_membership = Some(IfcMembership {
            ifc_id: IfcId(3),
            ifc_root_layout_index: usize::MAX,
            run_index: 0,
        });
        assert!(tree.get_inline_layout_for_node(0).is_none());
        // …and when the referenced root exists but has no cached layout.
        let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
        tree.warm[1].ifc_membership = Some(IfcMembership {
            ifc_id: IfcId(3),
            ifc_root_layout_index: 0,
            run_index: 0,
        });
        assert!(tree.get_inline_layout_for_node(1).is_none());
    }
    // ==================================================================
    // LayoutTree — dirty flags
    // ==================================================================
    /// `0 → 1 → 2` chain plus a sibling `3` under `1`.
    fn dirty_tree() -> LayoutTree {
        raw_tree(
            vec![hot(None), hot(Some(0)), hot(Some(1)), hot(Some(1))],
            &[vec![1], vec![2, 3], vec![], vec![]],
        )
    }
    #[test]
    fn mark_dirty_walks_up_to_the_root() {
        let mut tree = dirty_tree();
        tree.mark_dirty(2, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(2)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(1)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(0)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(
            tree.cold(LayoutNodeId::new(3)).unwrap().dirty_flag,
            DirtyFlag::None,
            "the sibling is untouched"
        );
    }
    #[test]
    fn mark_dirty_with_flag_none_is_a_no_op() {
        let mut tree = dirty_tree();
        tree.mark_dirty(2, DirtyFlag::None);
        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
    }
    #[test]
    fn mark_dirty_never_downgrades_an_existing_flag() {
        let mut tree = dirty_tree();
        tree.mark_dirty(2, DirtyFlag::Layout);
        tree.mark_dirty(2, DirtyFlag::Paint);
        assert_eq!(
            tree.cold(LayoutNodeId::new(2)).unwrap().dirty_flag,
            DirtyFlag::Layout,
            "Layout > Paint — a Paint request must not weaken it"
        );
    }
    #[test]
    fn mark_dirty_upgrades_paint_to_layout_and_keeps_propagating() {
        let mut tree = dirty_tree();
        tree.mark_dirty(2, DirtyFlag::Paint);
        assert_eq!(tree.cold(LayoutNodeId::new(0)).unwrap().dirty_flag, DirtyFlag::Paint);
        tree.mark_dirty(2, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(2)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(0)).unwrap().dirty_flag, DirtyFlag::Layout);
    }
    #[test]
    fn mark_dirty_stops_early_when_an_ancestor_is_already_at_least_as_dirty() {
        let mut tree = dirty_tree();
        tree.mark_dirty(3, DirtyFlag::Layout); // marks 3, 1, 0
        tree.mark_dirty(2, DirtyFlag::Layout); // marks 2, then stops at 1
        assert_eq!(tree.cold(LayoutNodeId::new(2)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(1)).unwrap().dirty_flag, DirtyFlag::Layout);
    }
    #[test]
    fn mark_dirty_out_of_range_is_a_silent_no_op() {
        let mut tree = dirty_tree();
        tree.mark_dirty(usize::MAX, DirtyFlag::Layout);
        tree.mark_dirty(tree.nodes.len(), DirtyFlag::Layout);
        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
    }
    #[test]
    fn mark_dirty_terminates_on_a_cyclic_parent_chain() {
        // Not reachable through the builder, but the `>= flag` early-out is the
        // only thing standing between a corrupted parent pointer and a hang.
        let mut tree = raw_tree(vec![hot(Some(1)), hot(Some(0))], &[vec![], vec![]]);
        tree.mark_dirty(0, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(0)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(1)).unwrap().dirty_flag, DirtyFlag::Layout);
    }
    #[test]
    fn mark_dirty_terminates_when_a_node_is_its_own_parent() {
        let mut tree = raw_tree(vec![hot(Some(0))], &[vec![]]);
        tree.mark_dirty(0, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(0)).unwrap().dirty_flag, DirtyFlag::Layout);
    }
    #[test]
    fn mark_subtree_dirty_marks_descendants_but_not_ancestors_or_siblings() {
        let mut tree = dirty_tree();
        tree.mark_subtree_dirty(1, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(1)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(2)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(tree.cold(LayoutNodeId::new(3)).unwrap().dirty_flag, DirtyFlag::Layout);
        assert_eq!(
            tree.cold(LayoutNodeId::new(0)).unwrap().dirty_flag,
            DirtyFlag::None,
            "mark_subtree_dirty walks DOWN only"
        );
    }
    #[test]
    fn mark_subtree_dirty_with_none_or_a_bad_index_is_a_no_op() {
        let mut tree = dirty_tree();
        tree.mark_subtree_dirty(0, DirtyFlag::None);
        tree.mark_subtree_dirty(usize::MAX, DirtyFlag::Layout);
        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
    }
    #[test]
    fn mark_subtree_dirty_does_not_downgrade() {
        let mut tree = dirty_tree();
        tree.mark_subtree_dirty(0, DirtyFlag::Layout);
        tree.mark_subtree_dirty(0, DirtyFlag::Paint);
        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::Layout));
    }
    #[test]
    fn clear_all_dirty_flags_resets_every_node() {
        let mut tree = dirty_tree();
        tree.mark_subtree_dirty(0, DirtyFlag::Layout);
        assert!(tree.cold.iter().any(|c| c.dirty_flag != DirtyFlag::None));
        tree.clear_all_dirty_flags();
        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
    }
    #[test]
    fn clear_all_dirty_flags_on_an_empty_tree_does_not_panic() {
        let mut tree = raw_tree(Vec::new(), &[]);
        tree.clear_all_dirty_flags();
        assert!(tree.cold.is_empty());
    }
    // ==================================================================
    // LayoutTree — memory report (getters / numeric)
    // ==================================================================
    #[test]
    fn memory_report_total_is_the_sum_of_its_parts() {
        let tree = build_tree(&mixed_dom());
        let r = tree.memory_report();
        assert_eq!(r.node_count, tree.nodes.len());
        assert_eq!(
            r.total_bytes(),
            r.hot_bytes
                + r.warm_bytes
                + r.warm_inline_layout_bytes
                + r.warm_taffy_cache_bytes
                + r.cold_bytes
                + r.dom_to_layout_bytes
                + r.children_arena_bytes
                + r.children_offsets_bytes
        );
        assert!(r.hot_bytes >= r.node_count * size_of::<LayoutNodeHot>());
        assert!(r.total_bytes() > 0, "a non-empty tree retains something");
    }
    #[test]
    fn memory_report_of_an_empty_tree_is_all_zero() {
        let tree = raw_tree(Vec::new(), &[]);
        let r = tree.memory_report();
        assert_eq!(r.node_count, 0);
        assert_eq!(r.total_bytes(), 0);
    }
    #[test]
    fn memory_report_total_bytes_default_is_zero() {
        assert_eq!(LayoutTreeMemoryReport::default().total_bytes(), 0);
    }
    #[test]
    fn memory_report_total_bytes_at_the_usize_boundary_does_not_overflow() {
        // Nine byte-fields, each usize::MAX / 9 → usize::MAX - 6 (MAX ≡ 6 mod 9).
        // One notch further and `total_bytes`'s plain `+` chain would
        // overflow-panic in debug.
        let ninth = usize::MAX / 9;
        let r = LayoutTreeMemoryReport {
            node_count: 0,
            hot_bytes: ninth,
            warm_bytes: ninth,
            warm_inline_layout_bytes: ninth,
            glyph_run_bytes: ninth,
            warm_taffy_cache_bytes: ninth,
            cold_bytes: ninth,
            dom_to_layout_bytes: ninth,
            children_arena_bytes: ninth,
            children_offsets_bytes: ninth,
            // Counters, not byte totals — `total_bytes` must not add them.
            shaped_cluster_count: 0,
            shaped_glyph_count: 0,
            shaped_cluster_text_bytes: 0,
            distinct_glyph_style_arcs: 0,
            glyph_run_count: 0,
            glyph_instance_count: 0,
        };
        assert_eq!(r.total_bytes(), ninth * 9);
        assert_eq!(r.total_bytes(), usize::MAX - 6);
    }
    #[test]
    fn memory_report_counts_a_cached_inline_layout() {
        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
        let bare = tree.memory_report().warm_inline_layout_bytes;
        assert_eq!(bare, 0);
        tree.warm[0].inline_layout_result = Some(Box::new(CachedInlineLayout::new(
            layout_of(vec![tab_item(1.0, 1.0, 0.0, 0)]),
            AvailableSpace::MaxContent,
            false,
        )));
        assert!(
            tree.memory_report().warm_inline_layout_bytes >= size_of::<UnifiedLayout>(),
            "the UnifiedLayout header must at least be counted"
        );
        // The Box allocation itself is retained heap the `warm` line can't
        // see (CachedInlineLayout is boxed off LayoutNodeWarm).
        assert!(
            tree.memory_report().warm_inline_layout_bytes >= size_of::<CachedInlineLayout>(),
            "the boxed CachedInlineLayout struct must be counted"
        );
    }
    #[test]
    fn memory_report_itemizes_the_glyph_run_cache() {
        use crate::text3::glyphs::CompactGlyphRun;
        let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
        let r0 = tree.memory_report();
        assert_eq!(
            (r0.glyph_run_bytes, r0.glyph_run_count, r0.glyph_instance_count),
            (0, 0, 0)
        );
        // Two same-style same-baseline clusters (2 + 1 glyphs) merge into ONE
        // paint run of 3 instances — the run-merging law, pinned through the
        // report counters.
        tree.warm[0].inline_layout_result = Some(Box::new(CachedInlineLayout::new(
            layout_of(vec![
                glyph_cluster_item("a", 2, 0.0, 0),
                glyph_cluster_item("b", 1, 12.0, 0),
            ]),
            AvailableSpace::MaxContent,
            false,
        )));
        let r1 = tree.memory_report();
        assert_eq!(r1.glyph_run_count, 1, "same style+font+baseline must merge");
        assert_eq!(r1.glyph_instance_count, 3);
        assert!(
            r1.glyph_run_bytes
                >= size_of::<CompactGlyphRun>() + 3 * size_of::<(u32, f32)>(),
            "bytes must cover the run header and all three compact entries \
             (got {}, header {})",
            r1.glyph_run_bytes,
            size_of::<CompactGlyphRun>(),
        );
        // Arc-dedup law: a second node SHARING the same run list (GlyphSwap
        // reuse clones the Arc) must add ZERO bytes and ZERO counts.
        let shared = tree.warm[0]
            .inline_layout_result
            .as_ref()
            .unwrap()
            .glyph_runs
            .clone();
        let mut second = CachedInlineLayout::new(
            layout_of(vec![glyph_cluster_item("c", 1, 0.0, 0)]),
            AvailableSpace::MaxContent,
            false,
        );
        second.glyph_runs = shared;
        tree.warm[1].inline_layout_result = Some(Box::new(second));
        let r2 = tree.memory_report();
        assert_eq!(
            (r2.glyph_run_bytes, r2.glyph_run_count, r2.glyph_instance_count),
            (r1.glyph_run_bytes, r1.glyph_run_count, r1.glyph_instance_count),
            "a shared Arc must not be double-counted"
        );
        // A DISTINCT run list does count.
        let third = CachedInlineLayout::new(
            layout_of(vec![glyph_cluster_item("d", 1, 0.0, 0)]),
            AvailableSpace::MaxContent,
            false,
        );
        tree.warm[1].inline_layout_result = Some(Box::new(third));
        let r3 = tree.memory_report();
        assert_eq!(r3.glyph_run_count, 2);
        assert_eq!(r3.glyph_instance_count, 4);
        assert!(r3.glyph_run_bytes > r1.glyph_run_bytes);
    }
    #[test]
    fn root_node_returns_the_hot_node_at_the_root_index() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        let root = tree.root_node();
        assert_eq!(root.parent, None);
        assert_eq!(root.dom_node_id, sd.root.into_crate_internal());
    }
    // ==================================================================
    // LayoutTree::resolve_box_props (numeric / NaN-inf)
    // ==================================================================
    #[test]
    fn resolve_box_props_out_of_range_is_a_no_op() {
        let mut tree = build_tree(&mixed_dom());
        tree.resolve_box_props(usize::MAX, VIEWPORT, VIEWPORT, 16.0, 16.0);
        tree.resolve_box_props(tree.nodes.len(), VIEWPORT, VIEWPORT, 16.0, 16.0);
    }
    #[test]
    fn resolve_box_props_keeps_the_stored_props_finite_for_nan_and_inf_inputs() {
        let sd = styled(
            Dom::create_body().with_child(div_class("m")),
            ".m { margin: 50%; padding: 10em; border: 1px solid black; }",
        );
        let mut tree = build_tree(&sd);
        for (cb, vp, efs, rfs) in [
            (
                LogicalSize::new(f32::NAN, f32::NAN),
                LogicalSize::new(f32::NAN, f32::NAN),
                f32::NAN,
                f32::NAN,
            ),
            (
                LogicalSize::new(f32::INFINITY, f32::INFINITY),
                LogicalSize::new(f32::INFINITY, f32::INFINITY),
                f32::INFINITY,
                f32::INFINITY,
            ),
            (
                LogicalSize::new(f32::NEG_INFINITY, 0.0),
                LogicalSize::new(0.0, f32::NEG_INFINITY),
                f32::NEG_INFINITY,
                0.0,
            ),
            (
                LogicalSize::new(f32::MAX, f32::MAX),
                LogicalSize::new(f32::MAX, f32::MAX),
                f32::MAX,
                f32::MAX,
            ),
            (
                LogicalSize::new(0.0, 0.0),
                LogicalSize::new(0.0, 0.0),
                0.0,
                0.0,
            ),
        ] {
            tree.resolve_box_props(1, cb, vp, efs, rfs);
            let bp = tree.get(LayoutNodeId::new(1)).unwrap().box_props.unpack();
            for v in [
                bp.margin.top,
                bp.margin.right,
                bp.margin.bottom,
                bp.margin.left,
                bp.padding.top,
                bp.padding.left,
                bp.border.top,
                bp.border.left,
            ] {
                assert!(
                    v.is_finite(),
                    "the i16×10 packing must launder NaN/inf into a finite value, got {v}"
                );
                assert!(
                    (-3277.0..=3277.0).contains(&v),
                    "packed edges are clamped to ±3276.8px, got {v}"
                );
            }
        }
    }
    #[test]
    fn resolve_box_props_resolves_percentages_against_the_containing_block() {
        let sd = styled(
            Dom::create_body().with_child(div_class("m")),
            ".m { margin-left: 50%; }",
        );
        let mut tree = build_tree(&sd);
        tree.resolve_box_props(1, LogicalSize::new(200.0, 100.0), VIEWPORT, 16.0, 16.0);
        let bp = tree.get(LayoutNodeId::new(1)).unwrap().box_props.unpack();
        assert!(
            (bp.margin.left - 100.0).abs() < 0.2,
            "50% of a 200px containing block ≈ 100px, got {}",
            bp.margin.left
        );
    }
    // ==================================================================
    // Anonymous box generation via the real builder
    // ==================================================================
    #[test]
    fn a_whitespace_only_inline_run_generates_no_anonymous_box() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        let ws = text_node(&sd, " \n\t");
        assert!(
            !tree.dom_to_layout.contains_key(&ws),
            "CSS 2.1 §9.2.2.1: collapsible whitespace generates no box"
        );
        assert!(
            tree.nodes.iter().all(|n| n.dom_node_id != Some(ws)),
            "…and no layout node references it"
        );
    }
    #[test]
    fn a_real_inline_run_next_to_a_block_sibling_gets_exactly_one_anonymous_wrapper() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        let wrappers: Vec<usize> = (0..tree.nodes.len())
            .filter(|&i| {
                tree.cold(LayoutNodeId::new(i)).unwrap().anonymous_type == Some(AnonymousBoxType::InlineWrapper)
            })
            .collect();
        assert_eq!(
            wrappers.len(),
            1,
            "only the trailing `tail` run needs wrapping"
        );
        let w = wrappers[0];
        assert_eq!(tree.get(LayoutNodeId::new(w)).unwrap().dom_node_id, None, "anon boxes have no DOM node");
        assert_eq!(tree.cold(LayoutNodeId::new(w)).unwrap().dirty_flag, DirtyFlag::Layout);
        let tail = text_node(&sd, "tail");
        let kids = tree.children(w);
        assert_eq!(kids.len(), 1);
        assert_eq!(tree.get(LayoutNodeId::new(kids[0])).unwrap().dom_node_id, Some(tail));
    }
    #[test]
    fn an_all_inline_block_container_gets_no_anonymous_wrapper() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        // `.block` (DOM 1) holds only inline children — the all-inline fast path
        // must hand them straight to the parent, with no wrapper in between.
        let block_idx = (0..tree.nodes.len())
            .find(|&i| tree.get(LayoutNodeId::new(i)).unwrap().dom_node_id == Some(NodeId::new(1)))
            .expect("the .block layout node");
        let kids = tree.children(block_idx);
        assert_eq!(kids.len(), 2, "the text run and the inline div, unwrapped");
        assert!(
            kids.iter()
                .all(|&c| tree.cold(LayoutNodeId::new(c)).unwrap().anonymous_type.is_none()),
            "an all-inline block container needs no anonymous wrapper"
        );
        assert_eq!(
            tree.get(LayoutNodeId::new(block_idx)).unwrap().formatting_context,
            FormattingContext::Inline,
            "it establishes an IFC instead"
        );
    }
    #[test]
    fn the_marker_pseudo_element_is_inserted_as_the_first_child_of_a_list_item() {
        let sd = styled(
            Dom::create_body().with_child(div_class("li").with_child(Dom::create_text_do_not_use_without_block_level_wrapper("item"))),
            ".li { display: list-item; }",
        );
        let tree = build_tree(&sd);
        let marker = (0..tree.nodes.len())
            .find(|&i| tree.warm(LayoutNodeId::new(i)).unwrap().pseudo_element == Some(PseudoElement::Marker))
            .expect("display:list-item must generate a ::marker");
        let li = tree.get(LayoutNodeId::new(marker)).unwrap().parent.expect("marker has a parent");
        assert_eq!(
            tree.children(li)[0],
            marker,
            "CSS Lists 3 §3.1: ::marker is the FIRST child"
        );
        assert_eq!(
            tree.get(LayoutNodeId::new(marker)).unwrap().dom_node_id,
            tree.get(LayoutNodeId::new(li)).unwrap().dom_node_id,
            "the marker shares the list-item's DOM node for counter/style resolution"
        );
        assert_eq!(tree.get(LayoutNodeId::new(marker)).unwrap().formatting_context, FormattingContext::Inline);
        assert!(
            tree.dom_to_layout[&tree.get(LayoutNodeId::new(li)).unwrap().dom_node_id.unwrap()].contains(&LayoutNodeId::new(marker)),
            "the marker is registered in dom_to_layout for counter resolution"
        );
    }
    #[test]
    fn display_none_children_never_reach_the_layout_tree() {
        let sd = styled(
            Dom::create_body()
                .with_child(div_class("gone"))
                .with_child(div_class("here")),
            ".gone { display: none; } .here { display: block; }",
        );
        let tree = build_tree(&sd);
        assert_eq!(
            tree.children(tree.root).len(),
            1,
            "display:none generates no box"
        );
    }
    #[test]
    fn display_contents_promotes_its_children_to_the_grandparent() {
        let sd = styled(
            Dom::create_body().with_child(div_class("c").with_child(div_class("kid"))),
            ".c { display: contents; } .kid { display: block; }",
        );
        let tree = build_tree(&sd);
        // The `.c` box is removed from its parent's child list; `.kid` is hoisted.
        let root_kids = tree.children(tree.root);
        assert!(
            root_kids
                .iter()
                .any(|&i| tree.warm(LayoutNodeId::new(i)).unwrap().computed_style.display == LayoutDisplay::Block),
            "the promoted child must be a direct child of the root"
        );
    }
    #[test]
    fn a_table_with_a_bare_cell_gets_an_anonymous_row() {
        let sd = styled(
            Dom::create_body().with_child(div_class("t").with_child(div_class("cell"))),
            ".t { display: table; } .cell { display: table-cell; }",
        );
        let tree = build_tree(&sd);
        assert!(
            (0..tree.nodes.len()).any(|i| {
                tree.cold(LayoutNodeId::new(i)).unwrap().anonymous_type == Some(AnonymousBoxType::TableRow)
            }),
            "CSS 2.2 §17.2.1 stage 2: a non-proper table child is wrapped in an anonymous row"
        );
    }
    #[test]
    fn whitespace_between_table_rows_is_dropped_not_wrapped() {
        let sd = styled(
            Dom::create_body().with_child(
                div_class("t")
                    .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("   "))
                    .with_child(div_class("row")),
            ),
            ".t { display: table; } .row { display: table-row; }",
        );
        let tree = build_tree(&sd);
        let ws = text_node(&sd, "   ");
        assert!(
            !tree.dom_to_layout.contains_key(&ws),
            "stage 1: irrelevant (whitespace) boxes are removed"
        );
        assert!(
            !(0..tree.nodes.len())
                .any(|i| tree.cold(LayoutNodeId::new(i)).unwrap().anonymous_type == Some(AnonymousBoxType::TableRow)),
            "and no anonymous row is generated for it"
        );
    }
    #[test]
    fn table_column_children_are_suppressed_entirely() {
        let sd = styled(
            Dom::create_body().with_child(div_class("col").with_child(div_class("kid"))),
            ".col { display: table-column; } .kid { display: block; }",
        );
        let tree = build_tree(&sd);
        // CSS 2.2 §17.2.1: all children of a table-column are display:none.
        let col = tree.children(tree.root)[0];
        assert!(tree.children(col).is_empty());
    }
    // ==================================================================
    // LayoutTreeBuilder (constructor / numeric / boundary)
    // ==================================================================
    #[test]
    fn builder_new_starts_completely_empty() {
        let b = LayoutTreeBuilder::new(VIEWPORT);
        assert!(b.get(0).is_none());
        assert!(b.get(usize::MAX).is_none());
        assert!(b.nodes.is_empty());
        assert!(b.dom_to_layout.is_empty());
        assert_eq!(b.viewport_size, VIEWPORT);
    }
    #[test]
    fn builder_new_accepts_degenerate_viewports() {
        for vp in [
            LogicalSize::new(0.0, 0.0),
            LogicalSize::new(-1.0, -1.0),
            LogicalSize::new(f32::MAX, f32::MAX),
            LogicalSize::new(f32::NAN, f32::INFINITY),
        ] {
            let b = LayoutTreeBuilder::new(vp);
            assert!(b.nodes.is_empty());
        }
    }
    #[test]
    fn builder_get_and_get_mut_are_none_out_of_range() {
        let sd = mixed_dom();
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        assert_eq!(root, 0);
        assert!(b.get(0).is_some());
        assert!(b.get_mut(0).is_some());
        for idx in [1, usize::MAX, usize::MAX - 1] {
            assert!(b.get(idx).is_none(), "get({idx})");
            assert!(b.get_mut(idx).is_none(), "get_mut({idx})");
        }
    }
    #[test]
    fn create_anonymous_node_wires_up_parent_children_and_cold_defaults() {
        let sd = mixed_dom();
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        let root_fc = b.get(root).unwrap().formatting_context;
        let anon = b.create_anonymous_node(root, AnonymousBoxType::TableCell, FormattingContext::TableCell);
        assert_eq!(anon, 1, "anon nodes are appended");
        let n = b.get(anon).unwrap();
        assert_eq!(n.dom_node_id, None, "anonymous ⇒ no DOM node");
        assert_eq!(n.anonymous_type, Some(AnonymousBoxType::TableCell));
        assert_eq!(n.formatting_context, FormattingContext::TableCell);
        assert_eq!(n.parent, Some(root));
        assert_eq!(n.parent_formatting_context, Some(root_fc));
        assert_eq!(n.dirty_flag, DirtyFlag::Layout, "a fresh box needs layout");
        assert!(n.children.is_empty());
        assert_eq!(n.subtree_hash, SubtreeHash(0));
        assert!(n.ifc_id.is_none());
        assert_eq!(b.get(root).unwrap().children, vec![anon]);
        assert!(
            b.dom_to_layout.values().all(|v| !v.contains(&anon)),
            "anon boxes are never registered in dom_to_layout"
        );
    }
    #[test]
    fn create_anonymous_node_appends_in_call_order() {
        let sd = mixed_dom();
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        let a = b.create_anonymous_node(root, AnonymousBoxType::TableRow, FormattingContext::TableRow);
        let c = b.create_anonymous_node(root, AnonymousBoxType::TableCell, FormattingContext::TableCell);
        assert_eq!((a, c), (1, 2));
        assert_eq!(b.get(root).unwrap().children, vec![a, c]);
    }
    #[test]
    fn create_node_from_dom_registers_the_dom_mapping_and_the_parent_link() {
        let sd = mixed_dom();
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        let child = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
        assert_eq!(b.get(child).unwrap().dom_node_id, Some(NodeId::new(1)));
        assert_eq!(b.get(child).unwrap().parent, Some(root));
        assert_eq!(b.get(root).unwrap().children, vec![child]);
        assert_eq!(b.dom_to_layout[&NodeId::new(1)], vec![child]);
        assert_eq!(b.get(child).unwrap().dirty_flag, DirtyFlag::Layout);
    }
    #[test]
    fn create_node_from_dom_turns_the_roots_visible_overflow_into_auto() {
        // CSS Overflow 3 §3.3 — only for the root (parent == None).
        let sd = styled(Dom::create_body().with_child(div_class("d")), ".d { display: block; }");
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        let child = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
        let root_style = &b.get(root).unwrap().computed_style;
        assert_ne!(root_style.overflow_x, LayoutOverflow::Visible);
        assert_ne!(root_style.overflow_y, LayoutOverflow::Visible);
        let child_style = &b.get(child).unwrap().computed_style;
        assert_eq!(
            child_style.overflow_x,
            LayoutOverflow::Visible,
            "the rule applies to the viewport only, not to every node"
        );
    }
    #[test]
    fn clone_node_from_old_resets_children_and_dirty_state() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        let old_root = tree.get_full_node(0).unwrap();
        let old_child = tree.get_full_node(1).unwrap();
        assert!(!old_root.children.is_empty(), "the source root has children");
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let root = b.clone_node_from_old(&old_root, None, old_root.dom_node_id);
        let child = b.clone_node_from_old(&old_child, Some(root), old_child.dom_node_id);
        assert_eq!((root, child), (0, 1));
        assert!(
            b.get(root).unwrap().children == vec![child],
            "the clone's children come only from later clone calls"
        );
        assert!(b.get(child).unwrap().children.is_empty());
        assert_eq!(b.get(child).unwrap().parent, Some(root));
        assert_eq!(b.get(child).unwrap().dirty_flag, DirtyFlag::None);
        let root_fc = b.get(root).unwrap().formatting_context;
        assert_eq!(b.get(child).unwrap().parent_formatting_context, Some(root_fc));
    }
    #[test]
    fn clone_node_from_old_skips_dom_registration_for_anonymous_nodes() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        let anon = (0..tree.nodes.len())
            .find(|&i| tree.cold(LayoutNodeId::new(i)).unwrap().anonymous_type.is_some())
            .expect("mixed_dom generates one anonymous wrapper");
        let old = tree.get_full_node(anon).unwrap();
        assert_eq!(old.dom_node_id, None);
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let idx = b.clone_node_from_old(&old, None, old.dom_node_id);
        assert_eq!(idx, 0);
        assert!(
            b.dom_to_layout.is_empty(),
            "a node with no dom_node_id must not create a mapping entry"
        );
    }
    /// A clone takes the identity it is given, not the identity it was
    /// copied from.
    ///
    /// Reconciliation falls back to POSITIONAL matching when a new child's
    /// DOM id is absent from the old child list (`cache.rs`, the
    /// `.or_else(|| old_children_indices.get(i))` arm). That pairs an old
    /// node with a new node of a DIFFERENT id, and the clone is taken to
    /// reuse the old node's measurements. Carrying the old id through made
    /// the new tree describe a DOM that no longer exists: after a ribbon
    /// tab switch shrank the tree, `compute_counters` panicked indexing
    /// `node_data[52]` on a 37-node StyledDom, and everything quieter —
    /// style lookups, node rects, damage attribution — read the wrong node.
    ///
    /// NEGATIVE CONTROL: restoring `new_node.dom_node_id` to the old value
    /// (i.e. dropping the `new_node.dom_node_id = new_dom_id` assignment)
    /// makes both assertions below fail — verified.
    #[test]
    fn a_clone_takes_the_new_dom_identity_not_the_one_it_was_copied_from() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        // Any node that HAS a DOM id; its id is the "old" one.
        let old_idx = (0..tree.nodes.len())
            .find(|&i| tree.get_full_node(i).and_then(|n| n.dom_node_id).is_some())
            .expect("mixed_dom has DOM-backed nodes");
        let old = tree.get_full_node(old_idx).unwrap();
        let old_id = old.dom_node_id.unwrap();
        // Reconcile it against a DIFFERENT DOM node, as the positional
        // fallback does.
        let new_id = NodeId::new(old_id.index() + 7);
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let idx = b.clone_node_from_old(&old, None, Some(new_id));
        assert_eq!(
            b.get(idx).unwrap().dom_node_id,
            Some(new_id),
            "the clone must describe the node it was reconciled AGAINST"
        );
        assert_eq!(
            b.dom_to_layout.get(&new_id).map(Vec::as_slice),
            Some(&[idx][..]),
            "dom_to_layout must index the clone under its NEW id"
        );
        assert!(
            !b.dom_to_layout.contains_key(&old_id),
            "the identity it was copied from must leave no mapping behind - a \
             stale entry here is what hands a later lookup a different node's \
             rect"
        );
    }
    #[test]
    fn build_flattens_children_into_the_arena_losslessly() {
        let sd = mixed_dom();
        let mut builder = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root_id = sd.root.into_crate_internal().unwrap_or(NodeId::ZERO);
        let root = builder.process_node(&sd, root_id, None, &mut msgs).unwrap();
        let expected: Vec<Vec<usize>> = builder.nodes.iter().map(|n| n.children.clone()).collect();
        let tree = builder.build(root);
        assert_eq!(tree.nodes.len(), expected.len());
        assert_eq!(tree.warm.len(), expected.len());
        assert_eq!(tree.cold.len(), expected.len());
        for (i, want) in expected.iter().enumerate() {
            assert_eq!(tree.children(i), want.as_slice(), "node {i}");
        }
    }
    #[test]
    fn build_on_an_empty_builder_yields_an_empty_tree() {
        let tree = LayoutTreeBuilder::new(VIEWPORT).build(0);
        assert!(tree.nodes.is_empty());
        assert!(tree.children_arena.is_empty());
        assert!(tree.children_offsets.is_empty());
        assert!(tree.subtree_needs_intrinsic.is_empty());
        assert!(tree.get(LayoutNodeId::new(0)).is_none());
        assert!(tree.children(0).is_empty());
        assert_eq!(tree.get_content_size(LayoutNodeId::new(0)), LogicalSize::default());
        assert_eq!(tree.memory_report().node_count, 0);
    }
    #[test]
    fn build_with_an_out_of_range_root_index_does_not_panic() {
        let sd = mixed_dom();
        let mut builder = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        builder.process_node(&sd, NodeId::ZERO, None, &mut msgs).unwrap();
        let tree = builder.build(usize::MAX);
        assert_eq!(tree.root, usize::MAX, "build() stores the index verbatim");
        assert!(tree.get(LayoutNodeId::new(tree.root)).is_none());
        assert!(tree.children(tree.root).is_empty());
        assert_eq!(tree.get_ifc_root_layout_index(tree.root), usize::MAX);
    }
    #[test]
    fn blockify_node_display_blockifies_an_inline_flex_item() {
        let sd = styled(
            Dom::create_body().with_child(div_class("f").with_child(div_class("i"))),
            ".f { display: flex; } .i { display: inline; }",
        );
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        let flex = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
        let item = b.create_node_from_dom(&sd, NodeId::new(2), Some(flex), &mut msgs);
        assert_eq!(b.get(flex).unwrap().formatting_context, FormattingContext::Flex);
        assert_eq!(b.get(item).unwrap().computed_style.display, LayoutDisplay::Inline);
        b.blockify_node_display(&sd, NodeId::new(2), item, Some(flex));
        assert_eq!(
            b.get(item).unwrap().computed_style.display,
            LayoutDisplay::Block,
            "CSS Display 3 §2.7: a flex item's inline display blockifies"
        );
        assert!(matches!(
            b.get(item).unwrap().formatting_context,
            FormattingContext::Block { .. }
        ));
    }
    #[test]
    fn blockify_node_display_leaves_a_plain_block_child_alone() {
        let sd = styled(
            Dom::create_body().with_child(div_class("p").with_child(div_class("b"))),
            ".p { display: block; } .b { display: block; }",
        );
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        let p = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
        let child = b.create_node_from_dom(&sd, NodeId::new(2), Some(p), &mut msgs);
        let before = b.get(child).unwrap().formatting_context;
        b.blockify_node_display(&sd, NodeId::new(2), child, Some(p));
        assert_eq!(b.get(child).unwrap().computed_style.display, LayoutDisplay::Block);
        assert_eq!(b.get(child).unwrap().formatting_context, before);
    }
    #[test]
    fn blockify_node_display_with_a_bogus_node_index_is_a_no_op() {
        let sd = mixed_dom();
        let mut b = LayoutTreeBuilder::new(VIEWPORT);
        let mut msgs = None;
        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
        // A valid DOM id with a garbage layout index must not panic: both the
        // read and the write go through `get`/`get_mut`.
        b.blockify_node_display(&sd, NodeId::ZERO, usize::MAX, None);
        b.blockify_node_display(&sd, NodeId::ZERO, 999, Some(usize::MAX));
        assert_eq!(b.nodes.len(), 1);
        assert!(b.get(root).is_some());
    }
    // ==================================================================
    // blockify_flex_item_if_table_internal (numeric / slice bounds)
    // ==================================================================
    #[test]
    fn blockify_flex_item_rewrites_every_table_internal_context() {
        let tree = build_tree(&mixed_dom());
        let mut nodes = vec![tree.get_full_node(0).unwrap()];
        for fc in [
            FormattingContext::TableCell,
            FormattingContext::TableRow,
            FormattingContext::TableRowGroup,
            FormattingContext::TableColumnGroup,
            FormattingContext::TableCaption,
            FormattingContext::Table,
        ] {
            nodes[0].formatting_context = fc;
            blockify_flex_item_if_table_internal(&mut nodes, 0);
            assert_eq!(
                nodes[0].formatting_context,
                FormattingContext::Block {
                    establishes_new_context: true
                },
                "{fc:?} is table-internal and must blockify"
            );
        }
    }
    #[test]
    fn blockify_flex_item_leaves_non_table_contexts_untouched() {
        let tree = build_tree(&mixed_dom());
        let mut nodes = vec![tree.get_full_node(0).unwrap()];
        for fc in [
            FormattingContext::Inline,
            FormattingContext::InlineBlock,
            FormattingContext::Flex,
            FormattingContext::Grid,
            FormattingContext::None,
            FormattingContext::Contents,
            FormattingContext::Block {
                establishes_new_context: false,
            },
        ] {
            nodes[0].formatting_context = fc;
            blockify_flex_item_if_table_internal(&mut nodes, 0);
            assert_eq!(nodes[0].formatting_context, fc, "{fc:?} must be left alone");
        }
    }
    #[test]
    fn blockify_flex_item_out_of_range_or_empty_is_a_no_op() {
        let tree = build_tree(&mixed_dom());
        let mut nodes = vec![tree.get_full_node(0).unwrap()];
        nodes[0].formatting_context = FormattingContext::TableCell;
        blockify_flex_item_if_table_internal(&mut nodes, 1);
        blockify_flex_item_if_table_internal(&mut nodes, usize::MAX);
        blockify_flex_item_if_table_internal(&mut [], 0);
        blockify_flex_item_if_table_internal(&mut [], usize::MAX);
        assert_eq!(nodes[0].formatting_context, FormattingContext::TableCell);
    }
    #[test]
    fn table_cell_flex_items_do_not_produce_anonymous_table_boxes() {
        // CSS Flexbox §3: two `display:table-cell` flex items become two
        // independent block flex items, NOT one anonymous table row.
        let sd = styled(
            Dom::create_body().with_child(
                div_class("f")
                    .with_child(div_class("c"))
                    .with_child(div_class("c")),
            ),
            ".f { display: flex; } .c { display: table-cell; }",
        );
        let tree = build_tree(&sd);
        assert!(
            (0..tree.nodes.len()).all(|i| tree.cold(LayoutNodeId::new(i)).unwrap().anonymous_type.is_none()),
            "no anonymous table boxes for blockified flex items"
        );
        let flex = tree.children(tree.root)[0];
        for &c in tree.children(flex) {
            assert!(matches!(
                tree.get(LayoutNodeId::new(c)).unwrap().formatting_context,
                FormattingContext::Block { .. }
            ));
        }
    }
    // ==================================================================
    // Shrink-to-fit bitmap
    // ==================================================================
    #[test]
    fn is_shrink_to_fit_context_is_true_for_the_intrinsic_reading_contexts() {
        let sd = mixed_dom();
        for fc in [
            FormattingContext::Flex,
            FormattingContext::Grid,
            FormattingContext::Table,
            FormattingContext::InlineBlock,
        ] {
            assert!(
                is_shrink_to_fit_context(&sd, None, fc),
                "{fc:?} sizes from children's intrinsics"
            );
        }
    }
    #[test]
    fn is_shrink_to_fit_context_is_false_for_a_plain_block_with_no_dom_node() {
        let sd = mixed_dom();
        for fc in [
            FormattingContext::Block {
                establishes_new_context: false,
            },
            FormattingContext::Block {
                establishes_new_context: true,
            },
            FormattingContext::Inline,
            FormattingContext::None,
            FormattingContext::TableRow,
        ] {
            assert!(
                !is_shrink_to_fit_context(&sd, None, fc),
                "{fc:?} with no DOM node cannot be float/abspos ⇒ not STF"
            );
        }
    }
    #[test]
    fn is_shrink_to_fit_context_catches_floats_and_abspos() {
        let sd = styled(
            Dom::create_body()
                .with_child(div_class("fl"))
                .with_child(div_class("ab"))
                .with_child(div_class("fx"))
                .with_child(div_class("plain")),
            ".fl { float: left; } .ab { position: absolute; } .fx { position: fixed; } .plain { \
             display: block; }",
        );
        let block = FormattingContext::Block {
            establishes_new_context: false,
        };
        assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(1)), block), "float:left");
        assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(2)), block), "position:absolute");
        assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(3)), block), "position:fixed");
        assert!(
            !is_shrink_to_fit_context(&sd, Some(NodeId::new(4)), block),
            "an in-flow static block is sized top-down ⇒ not STF"
        );
    }
    #[test]
    fn compute_subtree_needs_intrinsic_is_one_bit_per_node() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        let bits = compute_subtree_needs_intrinsic(&sd, &tree);
        assert_eq!(bits.len(), tree.nodes.len());
        assert_eq!(tree.subtree_needs_intrinsic.len(), tree.nodes.len());
    }
    #[test]
    fn compute_subtree_needs_intrinsic_is_all_false_for_a_pure_block_tree() {
        let sd = mixed_dom();
        let tree = build_tree(&sd);
        assert!(
            compute_subtree_needs_intrinsic(&sd, &tree)
                .iter()
                .all(|b| !b),
            "nothing in mixed_dom() is flex/grid/table/float/abspos"
        );
    }
    #[test]
    fn compute_subtree_needs_intrinsic_propagates_a_deep_flex_up_to_the_root() {
        let sd = styled(
            Dom::create_body().with_child(
                div_class("a")
                    .with_child(div_class("b").with_child(div_class("f"))),
            ),
            ".a { display: block; } .b { display: block; } .f { display: flex; }",
        );
        let tree = build_tree(&sd);
        let bits = compute_subtree_needs_intrinsic(&sd, &tree);
        assert!(bits[tree.root], "out[i] = self || any(children) — must reach the root");
        assert!(bits.iter().all(|b| *b), "every node on the chain is on the flex path");
    }
    #[test]
    fn compute_subtree_needs_intrinsic_leaves_a_flex_free_sibling_branch_false() {
        let sd = styled(
            Dom::create_body()
                .with_child(div_class("f"))
                .with_child(div_class("plain")),
            ".f { display: flex; } .plain { display: block; }",
        );
        let tree = build_tree(&sd);
        let bits = compute_subtree_needs_intrinsic(&sd, &tree);
        let kids = tree.children(tree.root);
        let flex = kids
            .iter()
            .copied()
            .find(|&i| tree.get(LayoutNodeId::new(i)).unwrap().formatting_context == FormattingContext::Flex)
            .expect("the flex child");
        let plain = kids.iter().copied().find(|&i| i != flex).expect("the plain child");
        assert!(bits[flex]);
        assert!(!bits[plain], "a sibling that reads no intrinsics stays false");
        assert!(bits[tree.root], "…but the root still sees the flex branch");
    }
    #[test]
    fn compute_subtree_needs_intrinsic_on_an_empty_tree_is_empty() {
        let sd = mixed_dom();
        let tree = raw_tree(Vec::new(), &[]);
        assert!(compute_subtree_needs_intrinsic(&sd, &tree).is_empty());
    }
    // ==================================================================
    // Level / display predicates
    // ==================================================================
    #[test]
    fn is_block_level_matches_the_block_level_display_values() {
        let sd = styled(
            Dom::create_body()
                .with_child(div_class("b"))
                .with_child(div_class("i")),
            ".b { display: block; } .i { display: inline; }",
        );
        assert!(is_block_level(&sd, NodeId::new(1)));
        assert!(!is_block_level(&sd, NodeId::new(2)));
    }
    #[test]
    fn is_block_level_covers_the_table_and_list_item_families() {
        for (css_display, want) in [
            ("block", true),
            ("flow-root", true),
            ("flex", true),
            ("grid", true),
            ("table", true),
            ("table-row", true),
            ("table-cell", true),
            ("table-caption", true),
            ("list-item", true),
            ("inline", false),
            ("inline-block", false),
            ("inline-flex", false),
            ("inline-grid", false),
            ("inline-table", false),
            ("none", false),
        ] {
            let sd = styled(
                Dom::create_body().with_child(div_class("x")),
                &format!(".x {{ display: {css_display}; }}"),
            );
            assert_eq!(
                is_block_level(&sd, NodeId::new(1)),
                want,
                "display:{css_display}"
            );
        }
    }
    #[test]
    fn is_inline_level_is_always_true_for_text_regardless_of_display() {
        let sd = styled(
            Dom::create_body().with_child(div_class("b").with_child(Dom::create_text_do_not_use_without_block_level_wrapper("t"))),
            ".b { display: block; }",
        );
        let t = text_node(&sd, "t");
        assert!(is_inline_level(&sd, t), "text nodes are inline-level by definition");
        assert!(!is_inline_level(&sd, NodeId::new(1)), "the block div is not");
    }
    #[test]
    fn is_inline_level_matches_the_inline_display_family() {
        for (css_display, want) in [
            ("inline", true),
            ("inline-block", true),
            ("inline-table", true),
            ("inline-flex", true),
            ("inline-grid", true),
            ("block", false),
            ("flex", false),
            ("table", false),
            ("list-item", false),
        ] {
            let sd = styled(
                Dom::create_body().with_child(div_class("x")),
                &format!(".x {{ display: {css_display}; }}"),
            );
            assert_eq!(
                is_inline_level(&sd, NodeId::new(1)),
                want,
                "display:{css_display}"
            );
        }
    }
    #[test]
    fn block_and_inline_level_are_mutually_exclusive_for_element_nodes() {
        for css_display in [
            "block",
            "inline",
            "inline-block",
            "flex",
            "inline-flex",
            "grid",
            "table",
            "list-item",
        ] {
            let sd = styled(
                Dom::create_body().with_child(div_class("x")),
                &format!(".x {{ display: {css_display}; }}"),
            );
            let id = NodeId::new(1);
            assert!(
                !(is_block_level(&sd, id) && is_inline_level(&sd, id)),
                "display:{css_display} cannot be both block- and inline-level"
            );
        }
    }
    #[test]
    fn has_only_inline_children_is_false_for_a_childless_node() {
        let sd = styled(Dom::create_body().with_child(div_class("e")), ".e { display: block; }");
        assert!(
            !has_only_inline_children(&sd, NodeId::new(1)),
            "no children ⇒ no IFC (it's empty, not inline)"
        );
    }
    #[test]
    fn has_only_inline_children_is_true_for_an_all_inline_run() {
        let sd = mixed_dom();
        // `.block` (DOM 1) holds a text node and an inline div.
        assert!(has_only_inline_children(&sd, NodeId::new(1)));
    }
    #[test]
    fn has_only_inline_children_is_false_as_soon_as_one_block_child_appears() {
        let sd = mixed_dom();
        // `.mixed` (DOM 5) holds text + a block div + text.
        assert!(!has_only_inline_children(&sd, NodeId::new(5)));
    }
    #[test]
    fn has_only_inline_children_is_false_for_an_out_of_range_node_id() {
        let sd = mixed_dom();
        let past_end = NodeId::new(sd.node_data.len() + 10);
        assert!(
            !has_only_inline_children(&sd, past_end),
            "the hierarchy lookup is a `.get`, so a bogus id must be false, not a panic"
        );
        assert!(!has_only_inline_children(&sd, NodeId::new(usize::MAX / 2)));
    }
    // ==================================================================
    // is_whitespace_only_text (predicate / unicode / boundary)
    // ==================================================================
    fn ws_dom(text: &str, css: &str) -> StyledDom {
        styled(
            Dom::create_body().with_child(div_class("p").with_child(Dom::create_text_do_not_use_without_block_level_wrapper(text))),
            css,
        )
    }
    #[test]
    fn is_whitespace_only_text_recognises_the_css_document_whitespace_set() {
        // CSS Text 3 §4.1: space, tab, CR, LF, FF.
        for text in [" ", "\t", "\n", "\r", "\u{000C}", " \t\r\n\u{000C} "] {
            let sd = ws_dom(text, "");
            let id = NodeId::new(2);
            assert!(
                is_whitespace_only_text(&sd, id),
                "{text:?} is collapsible document whitespace"
            );
        }
    }
    #[test]
    fn is_whitespace_only_text_rejects_unicode_spaces_that_css_does_not_collapse() {
        // NBSP, ideographic space, en/em space, zero-width space, line separator:
        // none of these are in the CSS document-whitespace set.
        for text in [
            "\u{00A0}",
            "\u{3000}",
            "\u{2002}",
            "\u{2003}",
            "\u{200B}",
            "\u{2028}",
            " \u{00A0} ",
        ] {
            let sd = ws_dom(text, "");
            assert!(
                !is_whitespace_only_text(&sd, NodeId::new(2)),
                "{text:?} must NOT be treated as collapsible whitespace"
            );
        }
    }
    #[test]
    fn is_whitespace_only_text_is_false_for_real_text() {
        for text in ["hi", " hi ", "\u{1F600}", "a\nb"] {
            let sd = ws_dom(text, "");
            assert!(!is_whitespace_only_text(&sd, NodeId::new(2)), "{text:?}");
        }
    }
    #[test]
    fn is_whitespace_only_text_treats_the_empty_string_as_whitespace() {
        // `"".chars().all(..)` is vacuously true — an empty text node is
        // collapsible and generates no anonymous inline box.
        let sd = ws_dom("", "");
        assert!(is_whitespace_only_text(&sd, NodeId::new(2)));
    }
    #[test]
    fn is_whitespace_only_text_respects_whitespace_preserving_modes() {
        for (ws, collapses) in [
            ("normal", true),
            ("nowrap", true),
            ("pre-line", true),
            ("pre", false),
            ("pre-wrap", false),
            ("break-spaces", false),
        ] {
            let sd = ws_dom(" \n ", &format!(".p {{ white-space: {ws}; }}"));
            assert_eq!(
                is_whitespace_only_text(&sd, NodeId::new(2)),
                collapses,
                "white-space:{ws} — preserved whitespace still generates a box"
            );
        }
    }
    #[test]
    fn is_whitespace_only_text_is_false_for_non_text_and_bogus_nodes() {
        let sd = mixed_dom();
        assert!(!is_whitespace_only_text(&sd, NodeId::new(1)), "a div is not text");
        assert!(!is_whitespace_only_text(&sd, NodeId::ZERO), "the body is not text");
        let past_end = NodeId::new(sd.node_data.len() + 1);
        assert!(
            !is_whitespace_only_text(&sd, past_end),
            "an out-of-range id must return false, not panic"
        );
        assert!(!is_whitespace_only_text(&sd, NodeId::new(usize::MAX / 2)));
    }
    // ==================================================================
    // Table-structure predicates
    // ==================================================================
    #[test]
    fn should_skip_for_table_structure_only_fires_inside_table_parents() {
        let sd = ws_dom(" ", "");
        let ws = NodeId::new(2);
        for parent in [
            LayoutDisplay::Table,
            LayoutDisplay::InlineTable,
            LayoutDisplay::TableRowGroup,
            LayoutDisplay::TableHeaderGroup,
            LayoutDisplay::TableFooterGroup,
            LayoutDisplay::TableRow,
            // css-flexbox-1 section 4 / css-grid-1 section 6: whitespace-only
            // anonymous items are not rendered in flex/grid containers.
            LayoutDisplay::Flex,
            LayoutDisplay::InlineFlex,
            LayoutDisplay::Grid,
            LayoutDisplay::InlineGrid,
        ] {
            assert!(
                should_skip_for_table_structure(&sd, ws, parent),
                "whitespace under {parent:?} is an irrelevant box"
            );
        }
        for parent in [
            LayoutDisplay::Block,
            LayoutDisplay::Inline,
            LayoutDisplay::TableCell,
            LayoutDisplay::TableCaption,
            LayoutDisplay::TableColumn,
        ] {
            assert!(
                !should_skip_for_table_structure(&sd, ws, parent),
                "whitespace under {parent:?} is NOT an irrelevant box"
            );
        }
    }
    #[test]
    fn should_skip_for_table_structure_never_skips_real_content() {
        let sd = ws_dom("cell text", "");
        for parent in ALL_DISPLAYS {
            assert!(
                !should_skip_for_table_structure(&sd, NodeId::new(2), parent),
                "non-whitespace text must never be dropped (parent {parent:?})"
            );
        }
    }
    #[test]
    fn is_proper_table_child_matches_exactly_the_seven_spec_values() {
        let proper = [
            LayoutDisplay::TableRowGroup,
            LayoutDisplay::TableHeaderGroup,
            LayoutDisplay::TableFooterGroup,
            LayoutDisplay::TableRow,
            LayoutDisplay::TableColumnGroup,
            LayoutDisplay::TableColumn,
            LayoutDisplay::TableCaption,
        ];
        for d in ALL_DISPLAYS {
            assert_eq!(
                is_proper_table_child(d),
                proper.contains(&d),
                "CSS 2.2 §17.2.1 proper-table-child set: {d:?}"
            );
        }
        assert!(
            !is_proper_table_child(LayoutDisplay::TableCell),
            "a cell is a proper child of a ROW, not of a table"
        );
    }
    // ==================================================================
    // is_replaced_element
    // ==================================================================
    #[test]
    fn is_replaced_element_covers_the_css_display_3_appendix_b_set() {
        for nt in [
            NodeType::Br,
            NodeType::Wbr,
            NodeType::Meter,
            NodeType::Progress,
            NodeType::Canvas,
            NodeType::Embed,
            NodeType::Object,
            NodeType::Audio,
            NodeType::Video,
            NodeType::Input,
            NodeType::TextArea,
            NodeType::Select,
            NodeType::VirtualView,
        ] {
            let nd = NodeData::create_node(nt.clone());
            assert!(is_replaced_element(&nd), "{nt:?} is a replaced element");
        }
        let img = NodeData::create_image(ImageRef::null_image(
            1,
            1,
            RawImageFormat::R8,
            Vec::new(),
        ));
        assert!(is_replaced_element(&img), "an <img> is the canonical replaced element");
    }
    #[test]
    fn is_replaced_element_is_false_for_ordinary_containers_and_text() {
        for nt in [
            NodeType::Div,
            NodeType::Body,
            NodeType::Html,
            NodeType::P,
            NodeType::Span,
            NodeType::Table,
            NodeType::Button,
            NodeType::Label,
            NodeType::Hr,
        ] {
            let nd = NodeData::create_node(nt.clone());
            assert!(!is_replaced_element(&nd), "{nt:?} is not replaced");
        }
        assert!(!is_replaced_element(&NodeData::create_text_do_not_use_without_block_level_wrapper("hello")));
    }
    #[test]
    fn display_contents_on_a_replaced_element_degrades_to_display_none() {
        // CSS Display 3 §2.5: a replaced element cannot be un-boxed.
        let sd = styled(
            Dom::create_body().with_child(
                Dom::create_from_data(NodeData::create_node(NodeType::Br))
                    .with_ids_and_classes(vec![IdOrClass::Class("c".into())].into()),
            ),
            ".c { display: contents; }",
        );
        let tree = build_tree(&sd);
        assert!(
            tree.children(tree.root).is_empty(),
            "the <br> must be dropped from its parent's child list"
        );
        let br = (0..tree.nodes.len())
            .find(|&i| tree.get(LayoutNodeId::new(i)).unwrap().dom_node_id == Some(NodeId::new(1)))
            .expect("the node object still exists, just unparented");
        assert_eq!(
            tree.warm(LayoutNodeId::new(br)).unwrap().computed_style.display,
            LayoutDisplay::None
        );
        assert_eq!(tree.get(LayoutNodeId::new(br)).unwrap().formatting_context, FormattingContext::None);
    }
    // ==================================================================
    // get_display_type
    // ==================================================================
    #[test]
    fn get_display_type_reads_the_computed_display() {
        for (css_display, want) in [
            ("none", LayoutDisplay::None),
            ("block", LayoutDisplay::Block),
            ("inline", LayoutDisplay::Inline),
            ("inline-block", LayoutDisplay::InlineBlock),
            ("flex", LayoutDisplay::Flex),
            ("grid", LayoutDisplay::Grid),
            ("table", LayoutDisplay::Table),
            ("table-row", LayoutDisplay::TableRow),
            ("table-cell", LayoutDisplay::TableCell),
            ("flow-root", LayoutDisplay::FlowRoot),
            ("list-item", LayoutDisplay::ListItem),
            ("contents", LayoutDisplay::Contents),
        ] {
            let sd = styled(
                Dom::create_body().with_child(div_class("x")),
                &format!(".x {{ display: {css_display}; }}"),
            );
            assert_eq!(
                get_display_type(&sd, NodeId::new(1)),
                want,
                "display:{css_display}"
            );
        }
    }
    #[test]
    fn get_display_type_is_stable_across_repeated_calls() {
        let sd = mixed_dom();
        for i in 0..sd.node_data.len() {
            let id = NodeId::new(i);
            let a = get_display_type(&sd, id);
            let b = get_display_type(&sd, id);
            assert_eq!(a, b, "node {i} must be deterministic");
        }
    }
    // ==================================================================
    // Formatting-context determination
    // ==================================================================
    #[test]
    fn determine_formatting_context_is_inline_for_every_text_node() {
        let sd = mixed_dom();
        for needle in ["hello", "world", "tail", " \n\t"] {
            let id = text_node(&sd, needle);
            assert_eq!(
                determine_formatting_context(&sd, id),
                FormattingContext::Inline,
                "text node {needle:?}"
            );
        }
    }
    #[test]
    fn determine_formatting_context_for_display_ignores_display_on_text_nodes() {
        // The text early-out fires before the display match — a text node is
        // Inline even if you hand it `display: grid`.
        let sd = mixed_dom();
        let t = text_node(&sd, "hello");
        for d in ALL_DISPLAYS {
            assert_eq!(
                determine_formatting_context_for_display(&sd, t, d),
                FormattingContext::Inline,
                "text + display:{d:?}"
            );
        }
    }
    #[test]
    fn determine_formatting_context_for_display_maps_each_display_value() {
        let sd = styled(Dom::create_body().with_child(div_class("x")), ".x { display: block; }");
        let id = NodeId::new(1);
        for (d, want) in [
            (LayoutDisplay::Inline, FormattingContext::Inline),
            (
                LayoutDisplay::FlowRoot,
                FormattingContext::Block {
                    establishes_new_context: true,
                },
            ),
            (LayoutDisplay::InlineBlock, FormattingContext::InlineBlock),
            (LayoutDisplay::Table, FormattingContext::Table),
            (LayoutDisplay::InlineTable, FormattingContext::Table),
            (LayoutDisplay::TableRowGroup, FormattingContext::TableRowGroup),
            (LayoutDisplay::TableHeaderGroup, FormattingContext::TableRowGroup),
            (LayoutDisplay::TableFooterGroup, FormattingContext::TableRowGroup),
            (LayoutDisplay::TableRow, FormattingContext::TableRow),
            (LayoutDisplay::TableCell, FormattingContext::TableCell),
            (LayoutDisplay::TableColumnGroup, FormattingContext::TableColumnGroup),
            (LayoutDisplay::TableCaption, FormattingContext::TableCaption),
            (LayoutDisplay::TableColumn, FormattingContext::None),
            (LayoutDisplay::None, FormattingContext::None),
            (LayoutDisplay::Flex, FormattingContext::Flex),
            (LayoutDisplay::InlineFlex, FormattingContext::Flex),
            (LayoutDisplay::Grid, FormattingContext::Grid),
            (LayoutDisplay::InlineGrid, FormattingContext::Grid),
            (LayoutDisplay::Contents, FormattingContext::Contents),
            (
                LayoutDisplay::RunIn,
                FormattingContext::Block {
                    establishes_new_context: true,
                },
            ),
            (
                LayoutDisplay::Marker,
                FormattingContext::Block {
                    establishes_new_context: true,
                },
            ),
        ] {
            assert_eq!(
                determine_formatting_context_for_display(&sd, id, d),
                want,
                "display:{d:?}"
            );
        }
    }
    #[test]
    fn determine_formatting_context_for_display_never_panics_on_any_display_value() {
        let sd = styled(Dom::create_body().with_child(div_class("x")), ".x { display: block; }");
        for d in ALL_DISPLAYS {
            let _ = determine_formatting_context_for_display(&sd, NodeId::new(1), d);
            let _ = determine_formatting_context_for_display(&sd, NodeId::ZERO, d);
        }
    }
    #[test]
    fn a_block_with_only_inline_children_establishes_an_ifc() {
        let sd = mixed_dom();
        assert_eq!(
            determine_formatting_context(&sd, NodeId::new(1)),
            FormattingContext::Inline,
            "CSS 2.2 §9.4.2: a block container with no block-level boxes establishes an IFC"
        );
    }
    #[test]
    fn a_block_with_a_block_child_stays_a_bfc() {
        let sd = mixed_dom();
        assert!(matches!(
            determine_formatting_context(&sd, NodeId::new(5)),
            FormattingContext::Block { .. }
        ));
    }
    #[test]
    fn establishes_new_bfc_for_the_unconditional_display_values() {
        for css_display in ["inline-block", "table-cell", "table-caption", "flow-root"] {
            let sd = styled(
                Dom::create_body().with_child(div_class("x")),
                &format!(".x {{ display: {css_display}; }}"),
            );
            assert!(
                establishes_new_block_formatting_context(&sd, NodeId::new(1)),
                "display:{css_display} always establishes a BFC"
            );
        }
    }
    #[test]
    fn establishes_new_bfc_for_non_visible_overflow_floats_and_abspos() {
        for css in [
            ".x { display: block; overflow-x: hidden; }",
            ".x { display: block; overflow-y: scroll; }",
            ".x { display: block; overflow: auto; }",
            ".x { display: block; float: left; }",
            ".x { display: block; float: right; }",
            ".x { display: block; position: absolute; }",
            ".x { display: block; position: fixed; }",
        ] {
            let sd = styled(Dom::create_body().with_child(div_class("x")), css);
            assert!(
                establishes_new_block_formatting_context(&sd, NodeId::new(1)),
                "{css} must establish a BFC"
            );
        }
    }
    #[test]
    fn establishes_new_bfc_is_false_for_a_plain_in_flow_block() {
        let sd = styled(
            Dom::create_body().with_child(div_class("x")),
            ".x { display: block; }",
        );
        assert!(
            !establishes_new_block_formatting_context(&sd, NodeId::new(1)),
            "a static, visible-overflow, unfloated block does not open a BFC"
        );
    }
    #[test]
    fn establishes_new_bfc_for_the_root_and_for_replaced_elements() {
        let sd = styled(
            Dom::create_body().with_child(Dom::create_from_data(NodeData::create_node(NodeType::Br))),
            "",
        );
        assert!(
            establishes_new_block_formatting_context(&sd, NodeId::ZERO),
            "the root element always establishes a BFC"
        );
        assert!(
            establishes_new_block_formatting_context(&sd, NodeId::new(1)),
            "replaced elements always establish an independent formatting context"
        );
    }
    // ==================================================================
    // compute_layout_style
    // ==================================================================
    #[test]
    fn compute_layout_style_captures_every_property_it_advertises() {
        let sd = styled(
            Dom::create_body().with_child(div_class("x")),
            ".x { display: flex; position: absolute; overflow-x: hidden; overflow-y: scroll; \
             width: 50px; height: 60px; min-width: 10px; min-height: 11px; max-width: 99px; \
             max-height: 98px; text-align: center; }",
        );
        let s = compute_layout_style(&sd, NodeId::new(1));
        assert_eq!(s.display, LayoutDisplay::Flex);
        assert_eq!(s.position, LayoutPosition::Absolute);
        assert_eq!(s.overflow_x, LayoutOverflow::Hidden);
        assert_eq!(s.overflow_y, LayoutOverflow::Scroll);
        assert_eq!(s.text_align, StyleTextAlign::Center);
        assert!(s.width.is_some());
        assert!(s.height.is_some());
        assert!(s.min_width.is_some());
        assert!(s.min_height.is_some());
        assert!(s.max_width.is_some());
        assert!(s.max_height.is_some());
    }
    #[test]
    fn compute_layout_style_leaves_auto_sizes_as_none() {
        let sd = styled(
            Dom::create_body().with_child(div_class("x")),
            ".x { display: block; }",
        );
        let s = compute_layout_style(&sd, NodeId::new(1));
        assert!(s.width.is_none(), "auto width must be None, not 0px");
        assert!(s.height.is_none());
        assert!(s.max_width.is_none());
        assert!(s.max_height.is_none());
        assert_eq!(s.float, LayoutFloat::None);
        assert_eq!(s.position, LayoutPosition::Static);
    }
    #[test]
    fn compute_layout_style_reads_float_left_and_right() {
        for (css, want) in [("left", LayoutFloat::Left), ("right", LayoutFloat::Right)] {
            let sd = styled(
                Dom::create_body().with_child(div_class("x")),
                &format!(".x {{ float: {css}; }}"),
            );
            assert_eq!(compute_layout_style(&sd, NodeId::new(1)).float, want);
        }
    }
    #[test]
    fn compute_layout_style_never_panics_on_any_node_of_a_real_dom() {
        let sd = mixed_dom();
        for i in 0..sd.node_data.len() {
            let _ = compute_layout_style(&sd, NodeId::new(i));
        }
    }
    // ==================================================================
    // Font-size helpers
    // ==================================================================
    #[test]
    fn font_size_helpers_fall_back_to_the_default_when_nothing_is_specified() {
        let sd = styled(Dom::create_body().with_child(div_class("x")), "");
        assert_eq!(get_root_font_size(&sd), DEFAULT_FONT_SIZE);
        assert_eq!(
            get_parent_font_size(&sd, NodeId::ZERO),
            DEFAULT_FONT_SIZE,
            "the root has no parent ⇒ documented DEFAULT_FONT_SIZE fallback"
        );
        assert_eq!(get_element_font_size(&sd, NodeId::new(1)), DEFAULT_FONT_SIZE);
    }
    #[test]
    fn get_element_and_parent_font_size_track_the_cascade() {
        let sd = styled(
            Dom::create_body().with_child(div_class("big").with_child(div_class("small"))),
            ".big { font-size: 32px; } .small { font-size: 8px; }",
        );
        assert_eq!(get_element_font_size(&sd, NodeId::new(1)), 32.0);
        assert_eq!(get_element_font_size(&sd, NodeId::new(2)), 8.0);
        assert_eq!(
            get_parent_font_size(&sd, NodeId::new(2)),
            32.0,
            "the parent's size, not the element's own"
        );
    }
    #[test]
    fn get_root_font_size_reads_node_zero() {
        let root = Dom::create_body()
            .with_ids_and_classes(vec![IdOrClass::Class("root".into())].into())
            .with_child(div_class("x"));
        let sd = styled(root, ".root { font-size: 20px; }");
        assert_eq!(get_root_font_size(&sd), 20.0, "get_root_font_size hard-codes NodeId(0)");
        assert_eq!(get_root_font_size(&sd), get_element_font_size(&sd, NodeId::ZERO));
    }
    #[test]
    fn font_size_helpers_return_a_finite_positive_size_for_every_node() {
        let sd = mixed_dom();
        for i in 0..sd.node_data.len() {
            let id = NodeId::new(i);
            for size in [get_element_font_size(&sd, id), get_parent_font_size(&sd, id)] {
                assert!(size.is_finite(), "node {i}: {size}");
                assert!(size > 0.0, "node {i}: a zero/negative font-size breaks em math");
            }
        }
    }
    // ==================================================================
    // create_resolution_context (numeric / NaN-inf passthrough)
    // ==================================================================
    #[test]
    fn create_resolution_context_zeroes_an_unknown_containing_block() {
        // css-sizing-3 §5.2.1: % margins/padding resolve against 0 when the
        // containing block isn't known yet (cycle breaking).
        let sd = mixed_dom();
        let ctx = create_resolution_context(&sd, NodeId::new(1), None, VIEWPORT);
        assert_eq!(ctx.containing_block_size.width, 0.0);
        assert_eq!(ctx.containing_block_size.height, 0.0);
        assert!(ctx.element_size.is_none(), "not laid out yet");
        assert_eq!(ctx.viewport_size.width, VIEWPORT.width);
        assert_eq!(ctx.viewport_size.height, VIEWPORT.height);
    }
    #[test]
    fn create_resolution_context_passes_a_known_containing_block_through() {
        let sd = mixed_dom();
        let cb = PhysicalSize::new(321.0, 123.0);
        let ctx = create_resolution_context(&sd, NodeId::new(1), Some(cb), VIEWPORT);
        assert_eq!(ctx.containing_block_size.width, 321.0);
        assert_eq!(ctx.containing_block_size.height, 123.0);
    }
    #[test]
    fn create_resolution_context_survives_a_degenerate_viewport() {
        let sd = mixed_dom();
        for vp in [
            LogicalSize::new(0.0, 0.0),
            LogicalSize::new(-100.0, -100.0),
            LogicalSize::new(f32::MAX, f32::MAX),
            LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
            LogicalSize::new(f32::NAN, f32::NAN),
        ] {
            let ctx = create_resolution_context(&sd, NodeId::new(1), None, vp);
            // Viewport is passed through verbatim; the font sizes must stay sane.
            assert!(ctx.element_font_size.is_finite());
            assert!(ctx.parent_font_size.is_finite());
            assert!(ctx.root_font_size.is_finite());
        }
    }
    #[test]
    fn create_resolution_context_survives_a_degenerate_containing_block() {
        let sd = mixed_dom();
        for cb in [
            PhysicalSize::new(0.0, 0.0),
            PhysicalSize::new(-1.0, -1.0),
            PhysicalSize::new(f32::NAN, f32::INFINITY),
            PhysicalSize::new(f32::MAX, f32::MIN),
        ] {
            let ctx = create_resolution_context(&sd, NodeId::new(1), Some(cb), VIEWPORT);
            assert!(ctx.root_font_size.is_finite());
        }
    }
    // ==================================================================
    // collect_box_props (numeric / saturation / spec zeroing)
    // ==================================================================
    fn collect_for(css: &str, node: usize, viewport: LogicalSize) -> CollectedBoxProps {
        let sd = styled(Dom::create_body().with_child(div_class("x")), css);
        let mut msgs = None;
        collect_box_props(&sd, NodeId::new(node), &mut msgs, viewport)
    }
    #[test]
    fn collect_box_props_resolves_plain_pixel_edges() {
        let c = collect_for(
            ".x { margin: 10px; padding: 5px; border: 2px solid black; }",
            1,
            VIEWPORT,
        );
        assert_eq!(c.resolved.margin.top, 10.0);
        assert_eq!(c.resolved.margin.left, 10.0);
        assert_eq!(c.resolved.padding.right, 5.0);
        assert_eq!(c.resolved.border.bottom, 2.0);
    }
    #[test]
    fn collect_box_props_zeroes_a_border_whose_style_is_none() {
        // CSS 2.2 §8.5.1: computed border-width is 0 when border-style is none/hidden.
        let c = collect_for(".x { border-width: 9px; border-style: none; }", 1, VIEWPORT);
        assert_eq!(c.resolved.border.top, 0.0);
        assert_eq!(c.resolved.border.left, 0.0);
        let c = collect_for(".x { border-width: 9px; border-style: hidden; }", 1, VIEWPORT);
        assert_eq!(c.resolved.border.right, 0.0);
    }
    #[test]
    fn collect_box_props_strips_margins_and_padding_from_internal_table_boxes() {
        // CSS 2.2 §17.5: internal table elements have no margins; rows/groups/
        // columns additionally have no padding.
        for display in [
            "table-row",
            "table-row-group",
            "table-header-group",
            "table-footer-group",
            "table-column",
            "table-column-group",
        ] {
            let c = collect_for(
                &format!(".x {{ display: {display}; margin: 10px; padding: 7px; }}"),
                1,
                VIEWPORT,
            );
            assert_eq!(c.resolved.margin.top, 0.0, "display:{display} margin");
            assert_eq!(c.resolved.padding.top, 0.0, "display:{display} padding");
        }
        // A cell keeps its padding but loses its margin.
        let c = collect_for(".x { display: table-cell; margin: 10px; padding: 7px; }", 1, VIEWPORT);
        assert_eq!(c.resolved.margin.left, 0.0, "cells have no margins");
        assert_eq!(c.resolved.padding.left, 7.0, "…but they do have padding");
    }
    #[test]
    fn collect_box_props_zeroes_vertical_margins_on_a_non_replaced_inline() {
        let c = collect_for(".x { display: inline; margin: 10px; }", 1, VIEWPORT);
        assert_eq!(c.resolved.margin.top, 0.0);
        assert_eq!(c.resolved.margin.bottom, 0.0);
        assert_eq!(
            c.resolved.margin.left, 10.0,
            "horizontal margins still apply to inline boxes"
        );
        assert_eq!(c.resolved.margin.right, 10.0);
    }
    #[test]
    fn collect_box_props_does_not_clamp_huge_lengths_before_packing() {
        // collect_box_props returns f32; the ±3276.8px saturation happens later,
        // in PackedBoxProps. Assert the split so a regression in either is visible.
        let c = collect_for(".x { margin: 99999px; }", 1, VIEWPORT);
        assert_eq!(c.resolved.margin.top, 99_999.0);
        let packed = PackedBoxProps::pack(&c.resolved);
        assert_eq!(packed.margin[0], i16::MAX, "the packing saturates, it does not wrap");
    }
    #[test]
    fn collect_box_props_survives_a_degenerate_viewport() {
        for vp in [
            LogicalSize::new(0.0, 0.0),
            LogicalSize::new(-800.0, -600.0),
            LogicalSize::new(f32::MAX, f32::MAX),
            LogicalSize::new(f32::INFINITY, f32::INFINITY),
            LogicalSize::new(f32::NAN, f32::NAN),
        ] {
            // vh/vw units make the viewport actually load-bearing here.
            let c = collect_for(".x { margin: 10vh; padding: 5vw; }", 1, vp);
            let packed = PackedBoxProps::pack(&c.resolved);
            for v in packed.margin.iter().chain(packed.padding.iter()) {
                assert!(
                    (i16::MIN..=i16::MAX).contains(v),
                    "packing must stay in range for viewport {vp:?}"
                );
            }
        }
    }
    #[test]
    fn collect_box_props_fills_debug_messages_when_asked() {
        let sd = styled(
            Dom::create_body().with_child(div_class("x")),
            ".x { margin: 3px; }",
        );
        let mut msgs: Option<Vec<LayoutDebugMessage>> = Some(Vec::new());
        let _ = collect_box_props(&sd, NodeId::new(1), &mut msgs, VIEWPORT);
        assert!(
            !msgs.expect("still Some").is_empty(),
            "a Some(vec) sink must actually receive the [BOX] trace"
        );
        // …and a None sink must be left alone (no allocation, no panic).
        let mut none_sink: Option<Vec<LayoutDebugMessage>> = None;
        let _ = collect_box_props(&sd, NodeId::new(1), &mut none_sink, VIEWPORT);
        assert!(none_sink.is_none());
    }
    #[test]
    fn collect_box_props_unresolved_and_resolved_agree_after_a_re_resolve() {
        let c = collect_for(".x { margin: 4px; padding: 6px; }", 1, VIEWPORT);
        let params = crate::solver3::geometry::ResolutionParams {
            containing_block: VIEWPORT,
            viewport_size: VIEWPORT,
            element_font_size: DEFAULT_FONT_SIZE,
            root_font_size: DEFAULT_FONT_SIZE,
        };
        let again = c.unresolved.resolve(&params);
        assert_eq!(again.margin.top, c.resolved.margin.top);
        assert_eq!(again.padding.left, c.resolved.padding.left);
        assert_eq!(again.border.top, c.resolved.border.top);
    }
    #[test]
    fn edge_sizes_default_is_all_zero() {
        let e = EdgeSizes::default();
        assert_eq!((e.top, e.right, e.bottom, e.left), (0.0, 0.0, 0.0, 0.0));
    }
    // ==================================================================
    // Whole-pipeline invariants
    // ==================================================================
    #[test]
    fn a_freshly_built_tree_satisfies_every_structural_invariant() {
        for sd in [
            mixed_dom(),
            styled(Dom::create_body(), ""),
            styled(
                Dom::create_body().with_child(div_class("f").with_child(div_class("c"))),
                ".f { display: flex; } .c { display: table-cell; }",
            ),
            styled(
                Dom::create_body().with_child(div_class("t").with_child(div_class("c"))),
                ".t { display: table; } .c { display: table-cell; }",
            ),
            styled(
                Dom::create_body().with_child(div_class("li").with_child(Dom::create_text_do_not_use_without_block_level_wrapper("x"))),
                ".li { display: list-item; }",
            ),
        ] {
            let tree = build_tree(&sd);
            let n = tree.nodes.len();
            assert!(n >= 1);
            assert_eq!(tree.warm.len(), n);
            assert_eq!(tree.cold.len(), n);
            assert_eq!(tree.children_offsets.len(), n);
            assert_eq!(tree.subtree_needs_intrinsic.len(), n);
            assert!(tree.root < n);
            assert_eq!(tree.get(LayoutNodeId::new(tree.root)).unwrap().parent, None);
            for i in 0..n {
                if let Some(p) = tree.get(LayoutNodeId::new(i)).unwrap().parent {
                    assert!(p < n, "node {i}'s parent {p} is out of range");
                }
                for &c in tree.children(i) {
                    assert!(c < n, "node {i}'s child {c} is out of range");
                    assert_ne!(c, i, "no node may be its own child");
                }
            }
            for (dom_id, indices) in &tree.dom_to_layout {
                for &i in indices {
                    assert!(i < LayoutNodeId::new(n), "dom_to_layout[{dom_id:?}] points at {i}, out of range");
                    assert_eq!(tree.get(i).unwrap().dom_node_id, Some(*dom_id));
                }
            }
        }
    }
    #[test]
    fn building_a_body_only_dom_yields_exactly_one_node() {
        let sd = styled(Dom::create_body(), "");
        let tree = build_tree(&sd);
        assert_eq!(tree.nodes.len(), 1);
        assert_eq!(tree.root, 0);
        assert!(tree.children(0).is_empty());
        assert!(tree.children_arena.is_empty());
        assert_eq!(tree.children_offsets, vec![(0, 0)]);
        assert_eq!(tree.memory_report().node_count, 1);
    }
    #[test]
    fn the_root_box_always_establishes_a_new_block_formatting_context() {
        let tree = build_tree(&mixed_dom());
        match tree.get(LayoutNodeId::new(tree.root)).unwrap().formatting_context {
            FormattingContext::Block {
                establishes_new_context,
            } => assert!(establishes_new_context, "process_node forces this for the root"),
            other => panic!("the root should be a Block FC, got {other:?}"),
        }
    }
    #[test]
    fn a_deeply_nested_dom_builds_without_blowing_the_stack() {
        // process_node recurses once per level; 200 is well inside a test thread's
        // stack but deep enough to catch an accidental per-level allocation blowup.
        let mut dom = div_class("d");
        for _ in 0..200 {
            dom = div_class("d").with_child(dom);
        }
        let sd = styled(Dom::create_body().with_child(dom), ".d { display: block; }");
        let tree = build_tree(&sd);
        assert_eq!(tree.nodes.len(), 202, "body + 201 divs");
        // The chain must be a straight line: every node but the last has 1 child.
        let mut i = tree.root;
        let mut depth = 0;
        while let Some(&next) = tree.children(i).first() {
            i = next;
            depth += 1;
            assert!(depth <= 202, "the parent/child links formed a cycle");
        }
        assert_eq!(depth, 201);
    }
}