1
//! Formatting context layout (block, inline, table, and flex/grid via Taffy)
2

            
3
use crate::solver3::layout_tree::LayoutNodeId;
4
use std::{
5
    collections::{BTreeMap, HashMap},
6
    sync::Arc,
7
};
8

            
9
use azul_core::{
10
    dom::{FormattingContext, NodeId, NodeType},
11
    geom::{LogicalPosition, LogicalRect, LogicalSize},
12
    resources::RendererResources,
13
    styled_dom::{StyledDom, StyledNodeState},
14
};
15
use azul_css::{
16
    css::CssPropertyValue,
17
    props::{
18
        basic::{
19
            font::{StyleFontStyle, StyleFontWeight},
20
            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
21
            ColorU, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric,
22
        },
23
        layout::{
24
            ColumnCount, ColumnWidth, LayoutBorderSpacing, LayoutClear, LayoutDisplay, LayoutFloat,
25
            LayoutHeight, LayoutJustifyContent, LayoutOverflow, LayoutPosition, LayoutTableLayout,
26
            LayoutTextJustify, LayoutWidth, LayoutWritingMode, ShapeInside, ShapeOutside,
27
            StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells,
28
        },
29
        property::CssProperty,
30
        style::{
31
            BorderStyle, StyleDirection, StyleHyphens, StyleLineBreak, StyleListStylePosition,
32
            StyleListStyleType, StyleOverflowWrap, StyleTextAlign, StyleTextAlignLast,
33
            StyleTextBoxTrim, StyleTextCombineUpright, StyleTextOrientation, StyleUnicodeBidi,
34
            StyleVerticalAlign, StyleVisibility, StyleWhiteSpace, StyleWordBreak,
35
        },
36
    },
37
};
38
use rust_fontconfig::FcWeight;
39
use taffy::{AvailableSpace, LayoutInput, Line, Size as TaffySize};
40

            
41
#[cfg(feature = "text_layout")]
42
use crate::text3;
43
use crate::{
44
    debug_ifc_layout, debug_info, debug_log, debug_table_layout, debug_warning,
45
    font_traits::{
46
        ContentIndex, FontLoaderTrait, ImageSource, InlineContent, InlineImage, InlineShape,
47
        LayoutFragment, ObjectFit, ParsedFontTrait, SegmentAlignment, ShapeBoundary,
48
        ShapeDefinition, ShapedItem, Size, StyleProperties, StyledRun, TextLayoutCache,
49
        UnifiedConstraints,
50
    },
51
    solver3::{
52
        geometry::{BoxProps, EdgeSizes, IntrinsicSizes},
53
        getters::{
54
            get_css_border_bottom_width, get_css_border_top_width, get_css_box_sizing,
55
            get_css_height, get_css_padding_bottom, get_css_padding_top,
56
            get_css_width, get_direction_property, get_unicode_bidi_property,
57
            get_display_property, get_element_font_size, get_float, get_clear,
58
            get_list_style_position, get_list_style_type, get_overflow_x, get_overflow_y,
59
            get_parent_font_size, get_root_font_size, get_style_properties,
60
            get_text_align, get_text_box_edge_property, get_text_box_trim_property,
61
            get_text_orientation_property,
62
            get_vertical_align_property, get_visibility, get_white_space_property,
63
            get_writing_mode, MultiValue,
64
        },
65
        layout_tree::{
66
            AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold, LayoutTree, PseudoElement,
67
        },
68
        positioning::get_position_type,
69
        scrollbar::ScrollbarRequirements,
70
        sizing::extract_text_from_node,
71
        taffy_bridge, LayoutContext, LayoutDebugMessage, LayoutError, Result,
72
    },
73
    text3::cache::{
74
        AvailableSpace as Text3AvailableSpace, BreakType, ClearType, InlineBreak,
75
        TextAlign as Text3TextAlign,
76
    },
77
};
78

            
79
/// Default scrollbar width in pixels (CSS `scrollbar-width: auto`).
80
///
81
/// This is only used as a fallback when per-node CSS cannot be queried.
82
/// Prefer `getters::get_layout_scrollbar_width_px()` for per-node resolution.
83
pub const DEFAULT_SCROLLBAR_WIDTH_PX: f32 = 16.0;
84

            
85
// Note: DEFAULT_FONT_SIZE and PT_TO_PX are imported from pixel
86

            
87
/// Result of BFC layout with margin escape information
88
#[derive(Debug, Clone)]
89
pub(crate) struct BfcLayoutResult {
90
    /// Standard layout output (positions, overflow size, baseline)
91
    pub output: LayoutOutput,
92
    /// Top margin that escaped the BFC (for parent-child collapse)
93
    /// If Some, this margin should be used by parent instead of positioning this BFC
94
    pub escaped_top_margin: Option<f32>,
95
    /// Bottom margin that escaped the BFC (for parent-child collapse)
96
    /// If Some, this margin should collapse with next sibling
97
    pub escaped_bottom_margin: Option<f32>,
98
    /// K30b: `Some` = this BFC ran out of fragmentainer space; the token
99
    /// resumes it in the next fragmentainer. Always `None` on the
100
    /// continuous path (`constraints.fragmentainer == None`).
101
    pub outgoing_token: Option<crate::solver3::break_token::BreakToken>,
102
}
103

            
104
impl BfcLayoutResult {
105
240581
    pub(crate) const fn from_output(output: LayoutOutput) -> Self {
106
240581
        Self {
107
240581
            output,
108
240581
            escaped_top_margin: None,
109
240581
            escaped_bottom_margin: None,
110
240581
            outgoing_token: None,
111
240581
        }
112
240581
    }
113
}
114

            
115
/// The CSS `overflow` property behavior.
116
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117
pub enum OverflowBehavior {
118
    Visible,
119
    Hidden,
120
    Clip,
121
    Scroll,
122
    Auto,
123
}
124

            
125
impl OverflowBehavior {
126
17
    #[must_use] pub const fn is_clipped(&self) -> bool {
127
17
        matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
128
17
    }
129

            
130
15
    #[must_use] pub const fn is_scroll(&self) -> bool {
131
15
        matches!(self, Self::Scroll | Self::Auto)
132
15
    }
133
}
134

            
135
/// K30b: the fragmentainer the current layout call is filling (design:
136
/// `scripts/BREAK_TOKENS_DESIGN.md` §4.3). `None` = continuous media — every
137
/// existing path passes `None` and behaves bit-for-bit as before.
138
#[derive(Debug, Clone, Copy, PartialEq)]
139
pub struct FragmentainerSpace<'a> {
140
    /// Block-extent remaining in the CURRENT fragmentainer, measured from
141
    /// this box's block-start (the pen compares against it directly).
142
    pub remaining_block_extent: f32,
143
    /// Extent of a FRESH next fragmentainer ("would it fit on the next
144
    /// page at all?" — monolith classification).
145
    pub next_fragmentainer_extent: f32,
146
    /// True while filling the very first fragmentainer of the flow.
147
    pub is_first: bool,
148
    /// Incoming resume state for THIS box (the page loop threads page
149
    /// N−1's outgoing token back in). The token rides the fragmentainer
150
    /// input instead of a separate parameter so the continuous path stays
151
    /// signature-identical.
152
    pub resume: Option<&'a crate::solver3::break_token::BlockBreakToken>,
153
}
154

            
155
/// Input constraints for a layout function.
156
#[derive(Debug)]
157
pub struct LayoutConstraints<'a> {
158
    /// The available space for the content, excluding padding and borders.
159
    pub available_size: LogicalSize,
160
    /// The CSS writing-mode of the context.
161
    pub writing_mode: LayoutWritingMode,
162
    /// Full writing mode context (writing-mode + direction + text-orientation).
163
    /// Used by writing-mode-aware layout code to correctly map inline/block
164
    /// dimensions to physical x/y coordinates.
165
    pub writing_mode_ctx: super::geometry::WritingModeContext,
166
    /// The state of the parent Block Formatting Context, if applicable.
167
    /// This is how state (like floats) is passed down.
168
    pub bfc_state: Option<&'a mut BfcState>,
169
    // Other properties like text-align would go here.
170
    pub text_align: TextAlign,
171
    /// The size of the containing block (parent's content box).
172
    /// This is used for resolving percentage-based sizes and as `parent_size` for Taffy.
173
    pub containing_block_size: LogicalSize,
174
    /// The semantic type of the available width constraint.
175
    ///
176
    /// This field is crucial for correct inline layout caching:
177
    /// - `Definite(w)`: Normal layout with a specific available width
178
    /// - `MinContent`: Intrinsic minimum width measurement (maximum wrapping)
179
    /// - `MaxContent`: Intrinsic maximum width measurement (no wrapping)
180
    ///
181
    /// When caching inline layouts, we must track which constraint type was used
182
    /// to compute the cached result. A layout computed with `MinContent` (width=0)
183
    /// must not be reused when the actual available width is known.
184
    pub available_width_type: Text3AvailableSpace,
185
    /// K30b fragmentation: `None` = continuous (screen) layout, identical
186
    /// to pre-token behavior. `Some` arms the fit checks in `layout_bfc`.
187
    pub fragmentainer: Option<FragmentainerSpace<'a>>,
188
}
189

            
190
/// Manages all layout state for a single Block Formatting Context.
191
/// This struct is created by the BFC root and lives for the duration of its layout.
192
#[derive(Debug, Clone)]
193
pub struct BfcState {
194
    /// The current position for the next in-flow block element.
195
    pub pen: LogicalPosition,
196
    /// The state of all floated elements within this BFC.
197
    pub floats: FloatingContext,
198
    /// The state of margin collapsing within this BFC.
199
    pub margins: MarginCollapseContext,
200
}
201

            
202
impl Default for BfcState {
203
1
    fn default() -> Self {
204
1
        Self::new()
205
1
    }
206
}
207

            
208
impl BfcState {
209
2
    #[must_use] pub fn new() -> Self {
210
2
        Self {
211
2
            pen: LogicalPosition::zero(),
212
2
            floats: FloatingContext::default(),
213
2
            margins: MarginCollapseContext::default(),
214
2
        }
215
2
    }
216
}
217

            
218
/// Manages vertical margin collapsing within a BFC.
219
#[derive(Copy, Debug, Default, Clone)]
220
pub struct MarginCollapseContext {
221
    /// The bottom margin of the last in-flow, block-level element.
222
    /// Can be positive or negative.
223
    pub last_in_flow_margin_bottom: f32,
224
}
225

            
226
/// The result of laying out a formatting context.
227
#[derive(Debug, Default, Clone)]
228
pub struct LayoutOutput {
229
    /// The final positions of child nodes, relative to the container's content-box origin.
230
    pub positions: BTreeMap<usize, LogicalPosition>,
231
    /// The total size occupied by the content, which may exceed `available_size`.
232
    pub overflow_size: LogicalSize,
233
    // +spec:inline-formatting-context:f7eebb - baseline along inline axis for glyph alignment
234
    /// The baseline of the context, if applicable, measured from the top of its content box.
235
    pub baseline: Option<f32>,
236
}
237

            
238
/// Text alignment options
239
#[derive(Debug, Clone, Copy, Default)]
240
pub enum TextAlign {
241
    #[default]
242
    Start,
243
    End,
244
    Center,
245
    Justify,
246
}
247

            
248
/// Represents a single floated element within a BFC.
249
#[derive(Debug, Clone, Copy)]
250
struct FloatBox {
251
    /// The type of float (Left or Right).
252
    kind: LayoutFloat,
253
    /// The rectangle of the float's content box (origin includes top/left margin offset).
254
    rect: LogicalRect,
255
    /// The margin sizes (needed to calculate true margin-box bounds).
256
    margin: EdgeSizes,
257
}
258

            
259
/// Manages the state of all floated elements within a Block Formatting Context.
260
// +spec:block-formatting-context:a4e6f9 - float rules reference only elements in the same BFC (scoped via BfcState)
261
// +spec:floats:2fa329 - Float positioning (left/right shift), content flow along sides, and clear property
262
/// +spec:floats:970b4c - Implements CSS2§9.5 float positioning and flow interaction
263
#[derive(Debug, Default, Clone)]
264
pub struct FloatingContext {
265
    /// All currently positioned floats within the BFC.
266
    pub floats: Vec<FloatBox>,
267
}
268

            
269
impl FloatingContext {
270
    /// Add a newly positioned float to the context
271
735
    pub fn add_float(&mut self, kind: LayoutFloat, rect: LogicalRect, margin: EdgeSizes) {
272
735
        self.floats.push(FloatBox { kind, rect, margin });
273
735
    }
274

            
275
    // +spec:box-model:0c9b13 - line boxes next to floats are shortened to make room
276
    // +spec:floats:148fcd - floating boxes reduce available line box width between containing block edges
277
    // +spec:floats:49a491 - Line boxes stacked with no separation except float clearance, never overlap
278
    // +spec:floats:8974e6 - text flows into vacated space by narrowing line boxes around floats
279
    // +spec:floats:af94f2 - content displaced by float: line boxes shrink to avoid float margin boxes
280
    // +spec:floats:e5961b - remaining text flows into vacated space via available_line_box_space
281
    // +spec:inline-formatting-context:7cbe58 - shortened line boxes due to floats; shift down if too small
282
    /// Finds the available space on the cross-axis for a line box at a given main-axis range.
283
    // +spec:containing-block:4b0c44 - line boxes shortened by floats resume containing block width after float
284
    ///
285
    /// Returns a tuple of (`cross_start_offset`, `cross_end_offset`) relative to the
286
    /// BFC content box, defining the available space for an in-flow element.
287
    // +spec:inline-formatting-context:e70328 - line box width reduced by floats between containing block edges
288
2417
    #[must_use] pub fn available_line_box_space(
289
2417
        &self,
290
2417
        main_start: f32,
291
2417
        main_end: f32,
292
2417
        bfc_cross_size: f32,
293
2417
        wm: LayoutWritingMode,
294
2417
    ) -> (f32, f32) {
295
2417
        let mut available_cross_start = 0.0_f32;
296
2417
        let mut available_cross_end = bfc_cross_size;
297

            
298
2673
        for float in &self.floats {
299
            // Get the logical main-axis span of the existing float's MARGIN BOX.
300
256
            let float_main_start = float.rect.origin.main(wm) - float.margin.main_start(wm);
301
256
            let float_main_end = float_main_start + float.rect.size.main(wm)
302
256
                + float.margin.main_start(wm) + float.margin.main_end(wm);
303

            
304
            // Check for overlap on the main axis.
305
256
            if main_end > float_main_start && main_start < float_main_end {
306
                // CSS 2.2 § 9.5: border box must not overlap MARGIN BOX of floats,
307
                // so we include the float's margins in the cross-axis bounds.
308
218
                let float_cross_start = float.rect.origin.cross(wm) - float.margin.cross_start(wm);
309
218
                let float_cross_end = float_cross_start + float.rect.size.cross(wm)
310
218
                    + float.margin.cross_start(wm) + float.margin.cross_end(wm);
311

            
312
                // +spec:floats:17a63f - float left/right map to line-left/line-right via logical coords
313
                // +spec:writing-modes:e55820 - line-relative mappings: left/right interpreted as line-left/line-right per writing mode
314
218
                if float.kind == LayoutFloat::Left {
315
171
                    // "line-left", i.e., cross-start
316
171
                    available_cross_start = available_cross_start.max(float_cross_end);
317
171
                } else {
318
47
                    // Float::Right, i.e., cross-end
319
47
                    available_cross_end = available_cross_end.min(float_cross_start);
320
47
                }
321
38
            }
322
        }
323
2417
        (available_cross_start, available_cross_end)
324
2417
    }
325

            
326
    // +spec:block-formatting-context:d06e6e - clearance computation for clear property on blocks and floats (CSS 2.2 § 9.5.2)
327
    // +spec:floats:31a3d5 - Clearance computation: places border edge even with bottom outer edge of lowest float to be cleared
328
    // +spec:floats:f9bef1 - clear property moves element below preceding floats
329
    /// Returns the main-axis offset needed to be clear of floats of the given type.
330
    // +spec:block-formatting-context:7f6bde - CSS 2.2 § 9.5.2 clear property: clearance places border edge below bottom outer edge of cleared floats
331
    // +spec:block-formatting-context:ef493f - clearance computation: places border edge even with bottom outer edge of lowest float to be cleared; inhibits margin collapsing
332
    // +spec:box-model:b118fe - top border edge must be below bottom outer edge of earlier floats
333
    // +spec:floats:415066 - Clear property: top border edge below bottom outer edge of cleared floats
334
    // +spec:floats:7e4ad6 - clear property: element box may not be adjacent to earlier floats; only considers floats in same BFC
335
    // +spec:floats:32e45d - clear:right causes sibling to flow below right floats
336
    // +spec:floats:7f417a - clear property prevents content from flowing next to floats
337
    // +spec:floats:d06304 - clear property moves element below floats, leaving blank space
338
    // +spec:overflow:1a7aff - clearance calculation (incl. negative clearance) and clear on floats (constraint #10)
339
    // +spec:positioning:1c2508 - clearance calculation: places border edge even with bottom outer edge of lowest cleared float (CSS 2.2 § 9.5.2)
340
    // +spec:positioning:fe0912 - clearance computation: places border edge below bottom outer edge of cleared floats
341
    // (clearance = amount to place border edge even with bottom outer edge of lowest
342
    // float to be cleared); clearance can be negative per spec example 2
343
    // +spec:floats:054a1e - Clearance computation: positions border edge below bottom outer edge of cleared floats
344
    // +spec:floats:cb984c - Clearance can be negative per spec example 2; inhibits margin collapsing
345
147
    #[must_use] pub fn clearance_offset(
346
147
        &self,
347
147
        clear: LayoutClear,
348
147
        current_main_offset: f32,
349
147
        wm: LayoutWritingMode,
350
147
    ) -> f32 {
351
147
        let mut max_end_offset = 0.0_f32;
352

            
353
147
        let check_left = clear == LayoutClear::Left || clear == LayoutClear::Both;
354
147
        let check_right = clear == LayoutClear::Right || clear == LayoutClear::Both;
355

            
356
323
        for float in &self.floats {
357
176
            let should_clear_this_float = (check_left && float.kind == LayoutFloat::Left)
358
89
                || (check_right && float.kind == LayoutFloat::Right);
359

            
360
176
            if should_clear_this_float {
361
136
                // CSS 2.2 § 9.5.2: "the top border edge of the box be below the bottom outer edge"
362
136
                // Outer edge = margin-box boundary (content + padding + border + margin)
363
136
                let float_margin_box_end = float.rect.origin.main(wm)
364
136
                    + float.rect.size.main(wm)
365
136
                    + float.margin.main_end(wm);
366
136
                max_end_offset = max_end_offset.max(float_margin_box_end);
367
136
            }
368
        }
369

            
370
147
        if max_end_offset > current_main_offset {
371
88
            max_end_offset
372
        } else {
373
59
            current_main_offset
374
        }
375
147
    }
376
}
377

            
378
/// Encapsulates all state needed to lay out a single Block Formatting Context.
379
struct BfcLayoutState {
380
    /// The current position for the next in-flow block element.
381
    pen: LogicalPosition,
382
    floats: FloatingContext,
383
    margins: MarginCollapseContext,
384
    /// The writing mode of the BFC root.
385
    writing_mode: LayoutWritingMode,
386
}
387

            
388
// Entry Point & Dispatcher
389

            
390
/// Main dispatcher for formatting context layout.
391
///
392
/// Routes layout to the appropriate formatting context handler based on the node's
393
/// `formatting_context` property. This is the main entry point for all layout operations.
394
///
395
/// # CSS Spec References
396
/// - CSS 2.2 § 9.4: Formatting contexts
397
/// - CSS Flexbox § 3: Flex formatting contexts
398
/// - CSS Grid § 5: Grid formatting contexts
399
// +spec:block-formatting-context:b04653 - dispatches layout by formatting context type (BFC, IFC, Table, Flex, Grid)
400
// +spec:block-formatting-context:e46499 - inner display type determines formatting context (BFC, IFC, table, flex, grid)
401
#[allow(clippy::implicit_hasher)] // internal helper; only ever called with the default-hasher HashMap/HashSet
402
/// # Errors
403
///
404
/// Returns a `LayoutError` if laying out the formatting context fails.
405
315732
pub fn layout_formatting_context<T: ParsedFontTrait>(
406
315732
    ctx: &mut LayoutContext<'_, T>,
407
315732
    tree: &mut LayoutTree,
408
315732
    text_cache: &mut TextLayoutCache,
409
315732
    node_index: usize,
410
315732
    constraints: &LayoutConstraints<'_>,
411
315732
    float_cache: &mut HashMap<usize, FloatingContext>,
412
315732
) -> Result<BfcLayoutResult> {
413
    // [g147e az-web-lift DIAG] PURE-CONSTANT entry marker (0x609E0+slot) — fires before any node read,
414
    // so it reliably shows whether layout_formatting_context is ENTERED for the nested div nodes 1,2.
415
    #[cfg(feature = "web_lift")]
416
    unsafe { crate::az_mark(((0x609E0 + (node_index & 7) * 4)) as u32, (0xC0DE0042) as u32); }
417
315732
    let node = tree.get(LayoutNodeId::new(node_index)).ok_or(LayoutError::InvalidTree)?;
418
    // [g147i az-web-lift DIAG] node REFERENCE address (0x60B80+slot) — NOT a field deref, so reliable.
419
    // If nodes 0,1,2 aren't spaced by sizeof(LayoutNodeHot) → tree.get(index>0) mis-lifts the Vec stride,
420
    // making nodes 1,2 garbage references (which would explain FC reading garbage + reads destabilizing).
421
    #[cfg(feature = "web_lift")]
422
    unsafe { crate::az_mark(((0x60B80 + (node_index & 7) * 4)) as u32, ((node as *const _ as usize) as u32) as u32); }
423

            
424
    // [g147 az-web-lift] Recompute the IFC decision from the DOM: on the lift, the stored
425
    // `node.formatting_context` reads GARBAGE for nested inline divs (2026-06-10 re-test WITHOUT
426
    // this bypass: nodes 1/2 dispatch to the `_` arm + determine_formatting_context_for_display's
427
    // markers never fire → the FC ASSIGNMENT path itself mis-lifts upstream — NOT fixed by the
428
    // repr(C,u8) guard, NOT fixed by the leak-gated SP restore; same family as the enum/jump-table
429
    // devirt class). The styled_dom IS reliable, so a block container whose children are all
430
    // inline-level establishes an IFC (CSS 2.2 §9.2.1) — semantically valid recomputation, not a
431
    // hack on top of garbage. web_lift-gated → native untouched. Remove when the FC-assignment
432
    // mis-lift is root-caused (follow-up: bisect LayoutTreeBuilder's determine_/display match).
433
    #[cfg(feature = "web_lift")]
434
    {
435
        let force_ifc = node
436
            .dom_node_id
437
            .map_or(false, |dom_id| {
438
                crate::solver3::layout_tree::has_only_inline_children(ctx.styled_dom, dom_id)
439
            });
440
        if force_ifc {
441
            unsafe { crate::az_mark(((0x60BA0 + (node_index & 7) * 4)) as u32, (0xC0DE1FC0) as u32); }
442
            return layout_ifc(ctx, text_cache, tree, node_index, constraints)
443
                .map(BfcLayoutResult::from_output);
444
        }
445
    }
446

            
447
    // [g147b az-web-lift DIAG] per-node FormattingContext discriminant at layout_formatting_context
448
    // entry (0x609A0+slot). Pairs with the dispatch-arm marker (0x609C0+slot) inside each match arm:
449
    // if a text-div's FC reads Inline(2) but the arm marker shows Block(1) → match dispatch mis-lifts;
450
    // if FC reads Block(1) → tree-construction FC assignment is wrong; if 0x609A0 stays unset for the
451
    // div node → layout_formatting_context is never called for it (cache-hit short-circuit upstream).
452
    #[cfg(feature = "web_lift")]
453
    unsafe {
454
        let fc_disc = match node.formatting_context {
455
            FormattingContext::Block { .. } => 1u32,
456
            FormattingContext::Inline => 2,
457
            FormattingContext::InlineBlock => 3,
458
            FormattingContext::Flex => 4,
459
            FormattingContext::Grid => 5,
460
            FormattingContext::Table => 6,
461
            FormattingContext::TableCell => 7,
462
            FormattingContext::TableCaption => 8,
463
            _ => 0,
464
        };
465
        crate::az_mark(((0x609A0 + (node_index & 7) * 4)) as u32, (fc_disc | 0xC0DE0000) as u32);
466
    }
467

            
468
315732
    debug_info!(
469
299781
        ctx,
470
299781
        "[layout_formatting_context] node_index={}, fc={:?}, available_size={:?}",
471
        node_index,
472
        node.formatting_context,
473
        constraints.available_size
474
    );
475

            
476
    // +spec:block-formatting-context:06a24f - CSS 2.2 § 9.4: block-level boxes → BFC, inline-level → IFC
477
    // +spec:block-formatting-context:9428cf - block container can establish both BFC and IFC simultaneously
478
    // +spec:inline-formatting-context:8bfe73 - display:flow generates inline box (Inline) or block container (Block) based on outer display type
479
315732
    match node.formatting_context {
480
        FormattingContext::Block { .. } => {
481
            #[cfg(feature = "web_lift")]
482
            unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0001) as u32); }
483
87382
            let _p = crate::probe::Probe::span("fc_block");
484
87382
            layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
485
        }
486
        // +spec:inline-formatting-context:a180ed - IFC establishment: inline-level boxes fragmented into line boxes with baseline alignment
487
        FormattingContext::Inline => {
488
            #[cfg(feature = "web_lift")]
489
            unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0002) as u32); }
490
223726
            let _p = crate::probe::Probe::span("fc_inline");
491
223726
            layout_ifc(ctx, text_cache, tree, node_index, constraints)
492
223726
                .map(BfcLayoutResult::from_output)
493
        }
494
        FormattingContext::InlineBlock => {
495
            #[cfg(feature = "web_lift")]
496
            unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0003) as u32); }
497
            // +spec:display-property:1f5ddf - inline-level boxes with non-flow inner display establish new formatting context
498
            // +spec:inline-formatting-context:1ad004 - atomic inline (inline-block) establishes new formatting context
499
            // CSS 2.2 § 9.4.1: "inline-blocks... establish new block formatting contexts"
500
            // +spec:inline-block:8d21f6 - inline-block generates inline-level block container (BFC inside, atomic inline outside)
501
            // InlineBlock ALWAYS establishes a BFC for its contents.
502
            // The element itself participates as an atomic inline in its parent's IFC,
503
            // but its children are laid out in a BFC, not an IFC.
504
86
            let _p = crate::probe::Probe::span("fc_inline_block");
505
86
            let mut temp_float_cache = HashMap::new();
506
86
            layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
507
        }
508
        // +spec:table-layout:753687 - CSS 2.2 §17.2 table model: display values map to FormattingContext variants and dispatch table layout
509
        FormattingContext::Table => {
510
            #[cfg(feature = "web_lift")]
511
            unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0006) as u32); }
512
333
            layout_table_fc(ctx, tree, text_cache, node_index, constraints)
513
333
                .map(BfcLayoutResult::from_output)
514
        }
515
        // Table-internal flex items are blockified during tree construction
516
        // (blockify_flex_item_if_table_internal in layout_tree.rs), so they arrive
517
        // here as Block, not TableCell etc.
518
        FormattingContext::Flex | FormattingContext::Grid => {
519
            #[cfg(feature = "web_lift")]
520
            unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0004) as u32); }
521
1937
            let _p = crate::probe::Probe::span("fc_flex_grid");
522
1937
            layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
523
        }
524
        // that are not block boxes, so they establish new BFCs for their contents
525
        FormattingContext::TableCell | FormattingContext::TableCaption => {
526
            #[cfg(feature = "web_lift")]
527
            unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0007) as u32); }
528
2268
            let mut temp_float_cache = HashMap::new();
529
2268
            layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
530
        }
531
        _ => {
532
            // [g147g az-web-lift DIAG] read the RAW discriminant byte (offset 0 under repr(C,u8)) of the
533
            // node that fell through to `_`. node 0 won't hit `_`; nodes 1,2 (divs) write their disc to
534
            // 0x60B40+slot. disc=1 ⇒ value IS Inline but the dispatch match mis-branched (match/jump-table
535
            // lift bug); disc≠1 ⇒ tree-construction stored the wrong/garbage FC for the nested div.
536
            #[cfg(feature = "web_lift")]
537
            unsafe {
538
                crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0009) as u32);
539
                let disc: u8 = core::ptr::read_volatile((&node.formatting_context) as *const FormattingContext as *const u8);
540
                crate::az_mark(((0x60B40 + (node_index & 7) * 4)) as u32, (0xC0DE0000 | (disc as u32)) as u32);
541
            }
542
            // Unknown formatting context - fall back to BFC
543
            let mut temp_float_cache = HashMap::new();
544
            layout_bfc(
545
                ctx,
546
                tree,
547
                text_cache,
548
                node_index,
549
                constraints,
550
                &mut temp_float_cache,
551
            )
552
        }
553
    }
554
315732
}
555

            
556
// Flex / grid layout (taffy Bridge)
557
// containing block determined by grid-placement properties; Taffy handles this internally
558
// (grid auto-placement §8.5 and abspos grid items use grid-area CB, not just padding box)
559

            
560
/// Lays out a Flex or Grid formatting context using the Taffy layout engine.
561
///
562
/// # CSS Spec References
563
///
564
/// - CSS Flexbox § 9: Flex Layout Algorithm
565
/// - CSS Grid § 12: Grid Layout Algorithm
566
// gutters on either side of collapsed tracks collapse including distributed alignment space,
567
// minimum contribution = outer size from min-width/min-height if specified size is auto else
568
// min-content contribution) — all handled by Taffy grid implementation
569
///
570
/// # Implementation Notes
571
///
572
/// - Resolves explicit CSS dimensions to pixel values for `known_dimensions`
573
/// - Uses `InherentSize` mode when explicit dimensions are set
574
/// - Uses `ContentSize` mode for auto-sizing (shrink-to-fit)
575
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
576
1937
fn layout_flex_grid<T: ParsedFontTrait>(
577
1937
    ctx: &mut LayoutContext<'_, T>,
578
1937
    tree: &mut LayoutTree,
579
1937
    text_cache: &mut TextLayoutCache,
580
1937
    node_index: usize,
581
1937
    constraints: &LayoutConstraints<'_>,
582
1937
) -> Result<BfcLayoutResult> {
583
    // Available space comes directly from constraints - margins are handled by Taffy
584
1937
    let available_space = TaffySize {
585
1937
        width: AvailableSpace::Definite(constraints.available_size.width),
586
1937
        height: AvailableSpace::Definite(constraints.available_size.height),
587
1937
    };
588

            
589
1937
    let node = tree.get(LayoutNodeId::new(node_index)).ok_or(LayoutError::InvalidTree)?;
590

            
591
    // from flex line's cross size (clamped by min/max) when align-self:stretch, cross-size:auto,
592
    // and neither cross-axis margin is auto. Otherwise uses hypothetical cross size.
593
    // NOTE: visibility:collapse strut size for flex items is handled internally by Taffy.
594
    //
595
    // Resolve explicit CSS dimensions to pixel values.
596
    // This is CRITICAL for align-items: stretch to work correctly!
597
    // Taffy uses known_dimensions to calculate cross_axis_available_space for children.
598
1937
    let (explicit_width, has_explicit_width) =
599
1937
        resolve_explicit_dimension_width(ctx, node, constraints);
600
1937
    let (explicit_height, has_explicit_height) =
601
1937
        resolve_explicit_dimension_height(ctx, node, constraints);
602

            
603
    // FIX: For root nodes or nodes where the parent provides a definite size,
604
    // use the available_size as known_dimensions if no explicit CSS width/height is set.
605
    // This is critical for `align-self: stretch` to work - Taffy needs to know the
606
    // cross-axis size of the container to stretch children to fill it.
607
1937
    let is_root = node.parent.is_none();
608

            
609
1937
    let bp = node.box_props.unpack();
610
1937
    let width_adjustment = bp.border.left
611
1937
        + bp.border.right
612
1937
        + bp.padding.left
613
1937
        + bp.padding.right;
614
1937
    let height_adjustment = bp.border.top
615
1937
        + bp.border.bottom
616
1937
        + bp.padding.top
617
1937
        + bp.padding.bottom;
618

            
619
    // `constraints.available_size` is the root's CONTENT-BOX (produced by
620
    // `prepare_layout_context::inner_size(final_used_size)`), not the viewport
621
    // border-box. Previously, the code used it as if it were border-box,
622
    // causing taffy to subtract padding a second time and shrink the content
623
    // area by 2x padding. For the root, pull the actual border-box from
624
    // `node.used_size` (set by `calculate_used_size_for_node` before this call).
625
1937
    let root_border_box = node.used_size;
626

            
627
1937
    let effective_width = if has_explicit_width {
628
395
        explicit_width
629
1542
    } else if is_root {
630
262
        root_border_box.as_ref().map(|s| s.width).or_else(|| {
631
            if constraints.available_size.width.is_finite() {
632
                // Fallback: convert content-box to border-box.
633
                Some(constraints.available_size.width + width_adjustment)
634
            } else {
635
                None
636
            }
637
        })
638
    } else {
639
        // Non-root flex/grid container with `width: auto`: for a block-level
640
        // child the parent's block layout has ALREADY resolved the used width
641
        // (auto → fill containing block) before descending into this FC — pass
642
        // it through as the definite border-box width, exactly like the root
643
        // branch does with its used_size. Without this, known_dimensions.width
644
        // stays None and taffy treats a column container's cross axis as
645
        // INDEFINITE, so `align-items: stretch` items get the flex line's
646
        // max-content width instead of the container width (live bug: under
647
        // the injected Html menubar wrapper, AzulPaint's body laid out its
648
        // header AND canvas at 315.776px — the header text's max-content —
649
        // instead of the body's 624px).
650
1280
        node.used_size.as_ref().map(|s| s.width)
651
    };
652
1937
    let effective_height = if has_explicit_height {
653
495
        explicit_height
654
1442
    } else if is_root {
655
153
        match root_border_box.as_ref().map(|s| s.height) {
656
            // An auto-height root's `used_size` is content-derived and is
657
            // still ZERO here (children have not been laid out yet).
658
            // Handing that to taffy as a DEFINITE main size makes a column
659
            // flex container think it has -60px of free space, so every
660
            // item with the default `flex-shrink: 1` collapses to 0 — a
661
            // fixed-height toolbar under `body { display: flex;
662
            // flex-direction: column }` simply vanished.
663
            //
664
            // CSS Flexbox §9.7: a container whose main size is INDEFINITE
665
            // sizes items to their hypothetical main size and performs no
666
            // shrinking. Leaving the dimension unknown is what makes taffy
667
            // content-size the container.
668
153
            Some(h) if h <= 0.0 => None,
669
            Some(h) => Some(h),
670
            None => {
671
                if constraints.available_size.height.is_finite() {
672
                    Some(constraints.available_size.height + height_adjustment)
673
                } else {
674
                    None
675
                }
676
            }
677
        }
678
    } else {
679
        // Non-root height pass-through, mirroring the width arm above,
680
        // but ONLY for absolutely/fixed-positioned containers: their
681
        // used height was resolved by the §10.6.4 equations (stretch-fit
682
        // between insets) BEFORE this run — a >0 value is the DEFINITE
683
        // containing-block size, exactly like the width case. Without
684
        // this, `inset:0; display:flex; align-items:center` centered
685
        // within the CONTENT height (taffy re-derived the main size and
686
        // clobbered the solved stretch-fit — miniword ENGINE-ISSUE 5a).
687
        // In-flow auto-height containers stay None (content-sized;
688
        // their used_size may hold a stale height on warm re-layouts).
689
1289
        let is_abs = matches!(
690
1289
            get_position_type(
691
1289
                ctx.styled_dom,
692
1289
                node.dom_node_id,
693
1289
            ),
694
            LayoutPosition::Absolute
695
                | LayoutPosition::Fixed
696
        );
697
1289
        match (is_abs, node.used_size.as_ref().map(|s| s.height)) {
698
9
            (true, Some(h)) if h > 0.0 => Some(h),
699
1280
            _ => None,
700
        }
701
    };
702
1937
    let has_effective_width = effective_width.is_some();
703
1937
    let has_effective_height = effective_height.is_some();
704

            
705
    // Taffy interprets known_dimensions as border-box. CSS width/height default
706
    // to content-box, so explicit values need +padding+border added. For the
707
    // ROOT element, however, we auto-apply box-sizing: border-box — the common
708
    // CSS reset pattern — so `height:100%` + padding fits the viewport instead
709
    // of overflowing by padding (which the default content-box interpretation
710
    // would produce, since 100% of ICB is viewport-sized content, with padding
711
    // added outside pushing border-box past the viewport).
712
1937
    let adjusted_width = if has_explicit_width && !is_root {
713
97
        explicit_width.map(|w| w + width_adjustment)
714
1840
    } else if has_explicit_width && is_root {
715
298
        explicit_width
716
    } else {
717
1542
        effective_width
718
    };
719
1937
    let adjusted_height = if has_explicit_height && !is_root {
720
88
        explicit_height.map(|h| h + height_adjustment)
721
1849
    } else if has_explicit_height && is_root {
722
407
        explicit_height
723
    } else {
724
1442
        effective_height
725
    };
726

            
727
    // CSS Flexbox § 9.2: Use InherentSize when explicit dimensions are set,
728
    // ContentSize for auto-sizing (shrink-to-fit behavior).
729
1937
    let sizing_mode = if has_effective_width || has_effective_height {
730
1937
        taffy::SizingMode::InherentSize
731
    } else {
732
        taffy::SizingMode::ContentSize
733
    };
734

            
735
1937
    let known_dimensions = TaffySize {
736
1937
        width: adjusted_width,
737
1937
        height: adjusted_height,
738
1937
    };
739

            
740
    // parent_size tells Taffy the size of the container's parent.
741
    // For root nodes, the "parent" is the viewport, but since margins are already
742
    // handled by calculate_used_size_for_node(), we use containing_block_size directly.
743
    // For non-root nodes, containing_block_size is already the parent's content-box.
744
1937
    let parent_size = translate_taffy_size(constraints.containing_block_size);
745

            
746
1937
    let taffy_inputs = LayoutInput {
747
1937
        known_dimensions,
748
1937
        parent_size,
749
1937
        available_space,
750
1937
        run_mode: taffy::RunMode::PerformLayout,
751
1937
        sizing_mode,
752
1937
        axis: taffy::RequestedAxis::Both,
753
1937
        // Flex and Grid containers establish a new BFC, preventing margin collapse.
754
1937
        vertical_margins_are_collapsible: Line::FALSE,
755
1937
    };
756

            
757
1937
    debug_info!(
758
1900
        ctx,
759
1900
        "CALLING LAYOUT_TAFFY FOR FLEX/GRID FC node_index={:?}",
760
        node_index
761
    );
762

            
763
    // For the root with auto-applied border-box: sync node.used_size so
764
    // display-list rendering matches the border-box we handed taffy.
765
    // Without this, the root's background/border would paint at the
766
    // inflated size from calculate_used_size_for_node while taffy placed
767
    // children inside a smaller content-box.
768
1937
    if is_root {
769
560
        if let (Some(aw), Some(ah)) = (adjusted_width, adjusted_height) {
770
407
            if let Some(node_mut) = tree.get_mut(LayoutNodeId::new(node_index)) {
771
407
                node_mut.used_size = Some(LogicalSize::new(aw, ah));
772
407
            }
773
153
        }
774
1377
    }
775

            
776
    // Cache border values before the mutable borrow in layout_taffy_subtree
777
1937
    let border_left = bp.border.left;
778
1937
    let border_top = bp.border.top;
779

            
780
1937
    let taffy_output =
781
1937
        taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
782

            
783
    // Adopt taffy's computed container border-box as this node's used_size. This is
784
    // the height/width taffy actually laid the tracks/lines into. The auto-height
785
    // path (cache.rs apply_content_based_height) otherwise derives the container
786
    // height from taffy's `content_size`, but for a GRID that field measures each
787
    // item relative to its OWN grid area (≈ the item's own height, blind to which
788
    // row it sits in), so a multi-row grid collapsed to a single row's height
789
    // (a 2×2 grid reported 16px instead of 42px). `taffy_output.size` is the correct
790
    // row/line sum + gaps. Flex's `output.size` already equals the correct container
791
    // size, so this is a no-op there. The root syncs its own used_size above.
792
1937
    if !is_root {
793
1377
        let container_bb = translate_taffy_size_back(taffy_output.size);
794
1377
        if let Some(node_mut) = tree.get_mut(LayoutNodeId::new(node_index)) {
795
1377
            node_mut.used_size = Some(container_bb);
796
1377
        }
797
560
    }
798

            
799
    // Collect child positions from the tree (Taffy stores results directly on nodes).
800
1937
    let mut output = LayoutOutput::default();
801
    // Use content_size for overflow detection, not container size.
802
    // content_size represents the actual size of all children, which may exceed the container.
803
    //
804
    // Taffy's content_size is measured from (0,0) of the border-box, so it includes
805
    // border.top/left as a leading offset.  The scrollbar geometry and scroll clamp
806
    // both measure inside the padding-box (border stripped).  Subtract the start
807
    // border so that overflow_size is in the same coordinate space as the viewport
808
    // (padding-box), preventing extra scroll range equal to the border width.
809
1937
    let raw = translate_taffy_size_back(taffy_output.content_size);
810
1937
    output.overflow_size = LogicalSize::new(
811
1937
        (raw.width - border_left).max(0.0),
812
1937
        (raw.height - border_top).max(0.0),
813
1937
    );
814

            
815
1937
    let children: Vec<usize> = tree.children(node_index).to_vec();
816
5708
    for &child_idx in &children {
817
3771
        if let Some(warm_node) = tree.warm(LayoutNodeId::new(child_idx)) {
818
3771
            if let Some(pos) = warm_node.relative_position {
819
3771
                output.positions.insert(child_idx, pos);
820
3771
            }
821
        }
822
    }
823

            
824
1937
    Ok(BfcLayoutResult::from_output(output))
825
1937
}
826

            
827
/// Resolves explicit CSS width to pixel value for Taffy layout.
828
/// Axis selector for `border_box_to_content`.
829
#[derive(Clone, Copy)]
830
enum Axis {
831
    Width,
832
    Height,
833
}
834

            
835
/// Convert a resolved explicit CSS dimension to the CONTENT-box value the
836
/// `known_dimensions` pipeline expects.
837
///
838
/// The flex/grid `known_dimensions` code resolves the explicit CSS dimension and
839
/// then unconditionally re-adds border+padding to reach taffy's border-box (see
840
/// `adjusted_width`/`adjusted_height`). That is correct only when the resolved
841
/// value is a content-box measurement. For `box-sizing:border-box`, an ABSOLUTE
842
/// length (px/em/calc) IS already the border-box size, so re-adding border+padding
843
/// double-counts it (a `height:100px; border:5px` container came out 110px instead
844
/// of 100). Subtract the axis border+padding here so the caller's re-add restores
845
/// the intended border-box. Percentages already resolve against the content-box
846
/// available size, so they are left unchanged.
847
890
fn border_box_to_content<T: ParsedFontTrait>(
848
890
    ctx: &LayoutContext<'_, T>,
849
890
    node: &LayoutNodeHot,
850
890
    id: NodeId,
851
890
    node_state: &StyledNodeState,
852
890
    resolved: f32,
853
890
    is_percentage: bool,
854
890
    axis: Axis,
855
890
) -> f32 {
856
890
    if is_percentage {
857
311
        return resolved;
858
579
    }
859
579
    let is_border_box = matches!(
860
579
        get_css_box_sizing(ctx.styled_dom, id, node_state),
861
        MultiValue::Exact(azul_css::props::layout::LayoutBoxSizing::BorderBox)
862
    );
863
579
    if !is_border_box {
864
570
        return resolved;
865
9
    }
866
9
    let bp = node.box_props.unpack();
867
9
    let adjustment = match axis {
868
        Axis::Width => bp.border.left + bp.border.right + bp.padding.left + bp.padding.right,
869
9
        Axis::Height => bp.border.top + bp.border.bottom + bp.padding.top + bp.padding.bottom,
870
    };
871
9
    (resolved - adjustment).max(0.0)
872
890
}
873

            
874
1937
fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
875
1937
    ctx: &LayoutContext<'_, T>,
876
1937
    node: &LayoutNodeHot,
877
1937
    constraints: &LayoutConstraints<'_>,
878
1937
) -> (Option<f32>, bool) {
879
1937
    node.dom_node_id
880
1937
        .map_or((None, false), |id| {
881
1937
            let width = get_css_width(
882
1937
                ctx.styled_dom,
883
1937
                id,
884
1937
                &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
885
            );
886
1937
            match width.unwrap_or_default() {
887
                LayoutWidth::Auto
888
                | LayoutWidth::MinContent
889
                | LayoutWidth::MaxContent
890
1542
                | LayoutWidth::FitContent(_) => (None, false),
891
395
                LayoutWidth::Px(px) => {
892
395
                    let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
893
395
                    let pixels = resolve_size_metric(
894
395
                        px.metric,
895
395
                        px.number.get(),
896
395
                        constraints.available_size.width,
897
395
                        ctx.viewport_size,
898
395
                        get_element_font_size(ctx.styled_dom, id, node_state),
899
395
                        get_root_font_size(ctx.styled_dom, node_state),
900
                    );
901
395
                    let content_px = border_box_to_content(
902
395
                        ctx, node, id, node_state, pixels, px.metric == SizeMetric::Percent, Axis::Width,
903
                    );
904
395
                    (Some(content_px), true)
905
                }
906
                LayoutWidth::Calc(items) => {
907
                    let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
908
                    let em = get_element_font_size(ctx.styled_dom, id, node_state);
909
                    let calc_ctx = super::calc::CalcResolveContext {
910
                        items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
911
                    };
912
                    let px = super::calc::evaluate_calc(&calc_ctx, constraints.available_size.width);
913
                    let content_px = border_box_to_content(
914
                        ctx, node, id, node_state, px, false, Axis::Width,
915
                    );
916
                    (Some(content_px), true)
917
                }
918
            }
919
1937
        })
920
1937
}
921

            
922
/// Resolves explicit CSS height to pixel value for Taffy layout.
923
1937
fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
924
1937
    ctx: &LayoutContext<'_, T>,
925
1937
    node: &LayoutNodeHot,
926
1937
    constraints: &LayoutConstraints<'_>,
927
1937
) -> (Option<f32>, bool) {
928
1937
    node.dom_node_id
929
1937
        .map_or((None, false), |id| {
930
1937
            let height = get_css_height(
931
1937
                ctx.styled_dom,
932
1937
                id,
933
1937
                &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
934
            );
935
1937
            match height.unwrap_or_default() {
936
                LayoutHeight::Auto
937
                | LayoutHeight::MinContent
938
                | LayoutHeight::MaxContent
939
1442
                | LayoutHeight::FitContent(_) => (None, false),
940
495
                LayoutHeight::Px(px) => {
941
495
                    let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
942
495
                    let pixels = resolve_size_metric(
943
495
                        px.metric,
944
495
                        px.number.get(),
945
495
                        constraints.available_size.height,
946
495
                        ctx.viewport_size,
947
495
                        get_element_font_size(ctx.styled_dom, id, node_state),
948
495
                        get_root_font_size(ctx.styled_dom, node_state),
949
                    );
950
                    // box-sizing:border-box + an ABSOLUTE length is a border-box
951
                    // value; the caller re-adds border+padding to reach the
952
                    // taffy border-box, so convert to content-box here to avoid
953
                    // double-counting. (Percentages already resolve against the
954
                    // content-box available size, so leave those alone.)
955
495
                    let content_px = border_box_to_content(
956
495
                        ctx, node, id, node_state, pixels, px.metric == SizeMetric::Percent, Axis::Height,
957
                    );
958
495
                    (Some(content_px), true)
959
                }
960
                LayoutHeight::Calc(items) => {
961
                    let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
962
                    let em = get_element_font_size(ctx.styled_dom, id, node_state);
963
                    let calc_ctx = super::calc::CalcResolveContext {
964
                        items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
965
                    };
966
                    let px = super::calc::evaluate_calc(&calc_ctx, constraints.available_size.height);
967
                    let content_px = border_box_to_content(
968
                        ctx, node, id, node_state, px, false, Axis::Height,
969
                    );
970
                    (Some(content_px), true)
971
                }
972
            }
973
1937
        })
974
1937
}
975

            
976
// +spec:floats:167a2c - Float positioning rules (CSS 2.2 § 9.5.1): left/right/none, precise placement constraints
977
// +spec:floats:6a1769 - Float shortens line boxes, margins never collapse, stacking order
978
// +spec:floats:15bfd9 - float:right positions element at line-right edge within BFC
979
// +spec:floats:afc8e2 - Float positioning rules (CSS 2.2 § 9.5 rules 1-8): left/right edge containment, earlier-float stacking, outer-top constraints, and "move down" when insufficient space
980
/// Position a float within a BFC, considering existing floats.
981
/// Returns the `LogicalRect` (margin box) for the float.
982
// +spec:box-model:db0f02 - Float positioning: line boxes shortened by floats, floats shift down if no space, BFC elements must not overlap float margin boxes
983
// +spec:containing-block:136e45 - Float shifted left/right until outer edge touches containing block edge or another float
984
// +spec:containing-block:3ebb4e - Content moves below floats when containing block too narrow
985
// +spec:floats:45fce7 - Float positioning: pulled out of flow, line boxes shortened around float
986
// +spec:floats:f6c218 - float pulled out of flow, line boxes shorten around it
987
// +spec:height-calculation:86142a - CSS 2.2 §9.5 float positioning, clearance, and margin non-collapsing
988
// +spec:width-calculation:761677 - float positioning: content flows around floats, line boxes shortened by float presence
989
334
fn position_float(
990
334
    float_ctx: &FloatingContext,
991
334
    float_type: LayoutFloat,
992
334
    size: LogicalSize,
993
334
    margin: &EdgeSizes,
994
334
    current_main_offset: f32,
995
334
    bfc_cross_size: f32,
996
334
    wm: LayoutWritingMode,
997
334
) -> LogicalRect {
998
    // Start at the current main-axis position (Y in horizontal-tb)
999
334
    let mut main_start = current_main_offset;
    // Calculate total size including margins
334
    let total_main = size.main(wm) + margin.main_start(wm) + margin.main_end(wm);
334
    let total_cross = size.cross(wm) + margin.cross_start(wm) + margin.cross_end(wm);
    // +spec:floats:3d89d8 - shift float downward when not enough horizontal room
    // Find a position where the float fits
334
    let cross_start = loop {
345
        let (avail_start, avail_end) = float_ctx.available_line_box_space(
345
            main_start,
345
            main_start + total_main,
345
            bfc_cross_size,
345
            wm,
345
        );
345
        let available_width = avail_end - avail_start;
345
        if available_width >= total_cross {
            // +spec:floats:449158 - left float positioned at line-left, content flows on right
            // Found space that fits
330
            if float_type == LayoutFloat::Left {
                // +spec:writing-modes:84bcba - floats positioned at line-left / line-right
                // Position at line-left (avail_start)
257
                break avail_start + margin.cross_start(wm);
73
            }
            // Position at line-right (avail_end - size)
73
            break avail_end - total_cross + margin.cross_start(wm);
15
        }
        // top is moved lower than earlier float's bottom (outer edge / margin box bottom)
        // Not enough space at this Y, move down past the lowest overlapping float's margin box bottom
15
        let next_main = float_ctx
15
            .floats
15
            .iter()
15
            .filter(|f| {
13
                let f_main_start = f.rect.origin.main(wm) - f.margin.main_start(wm);
13
                let f_main_end = f_main_start + f.rect.size.main(wm)
13
                    + f.margin.main_start(wm) + f.margin.main_end(wm);
13
                f_main_end > main_start && f_main_start < main_start + total_main
13
            })
15
            .map(|f| f.rect.origin.main(wm) + f.rect.size.main(wm) + f.margin.main_end(wm))
15
            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
15
        if let Some(next) = next_main {
11
            main_start = next;
11
        } else {
            // No overlapping floats found, use current position anyway
4
            if float_type == LayoutFloat::Left {
3
                break avail_start + margin.cross_start(wm);
1
            }
1
            break avail_end - total_cross + margin.cross_start(wm);
        }
    };
334
    LogicalRect {
334
        origin: LogicalPosition::from_main_cross(
334
            main_start + margin.main_start(wm),
334
            cross_start,
334
            wm,
334
        ),
334
        size,
334
    }
334
}
// Block Formatting Context (CSS 2.2 § 9.4.1)
/// Lays out a Block Formatting Context (BFC).
///
/// This is the corrected, architecturally-sound implementation. It solves the
/// "chicken-and-egg" problem by performing its own two-pass layout:
///
/// 1. **Sizing Pass:** It first iterates through its children and triggers their layout recursively
///    by calling `calculate_layout_for_subtree`. This ensures that the `used_size` property of each
///    child is correctly populated.
///
/// 2. **Positioning Pass:** It then iterates through the children again. Now that each child has a
///    valid size, it can apply the standard block-flow logic: stacking them vertically and
///    advancing a "pen" by each child's outer height.
///
/// # Margin Collapsing Architecture
///
/// CSS 2.1 Section 8.3.1 compliant margin collapsing:
///
/// ```text
/// layout_bfc()
///   ├─ Check parent border/padding blockers
///   ├─ For each child:
///   │   ├─ Check child border/padding blockers
///   │   ├─ is_first_child?
///   │   │   └─ Check parent-child top collapse
///   │   ├─ Sibling collapse?
///   │   │   └─ advance_pen_with_margin_collapse()
///   │   │       └─ collapse_margins(prev_bottom, curr_top)
///   │   ├─ Position child
///   │   ├─ is_empty_block()?
///   │   │   └─ Collapse own top+bottom margins (collapse through)
///   │   └─ Save bottom margin for next sibling
///   └─ Check parent-child bottom collapse
/// ```
///
/// **Collapsing Rules:**
///
/// - Sibling margins: Adjacent vertical margins collapse to max (or sum if mixed signs)
/// - Parent-child: First child's top margin can escape parent (if no border/padding)
/// - Parent-child: Last child's bottom margin can escape parent (if no border/padding/height)
/// - Empty blocks: Top+bottom margins collapse with each other, then with siblings
/// - Blockers: Border, padding, inline content, or new BFC prevents collapsing
///
/// This approach is compliant with the CSS visual formatting model and works within
/// the constraints of the existing layout engine architecture.
// +spec:display-property:f38f52 - BFC handles normal flow, relative positioning offsets, and float extraction (CSS 2.2 § 9.8)
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
89736
fn layout_bfc<T: ParsedFontTrait>(
89736
    ctx: &mut LayoutContext<'_, T>,
89736
    tree: &mut LayoutTree,
89736
    text_cache: &mut TextLayoutCache,
89736
    node_index: usize,
89736
    constraints: &LayoutConstraints<'_>,
89736
    float_cache: &mut HashMap<usize, FloatingContext>,
89736
) -> Result<BfcLayoutResult> {
89736
    let node = tree
89736
        .get(LayoutNodeId::new(node_index))
89736
        .ok_or(LayoutError::InvalidTree)?
89736
        .clone();
    // +spec:block-formatting-context:4f4ff6 - writing-mode determines block flow direction (main axis) for ordering block-level boxes in BFC
89736
    let writing_mode = constraints.writing_mode;
89736
    let mut output = LayoutOutput::default();
89736
    debug_info!(
88004
        ctx,
88004
        "\n[layout_bfc] ENTERED for node_index={}, children.len()={}, incoming_bfc_state={}",
        node_index,
88004
        tree.children(node_index).len(),
88004
        constraints.bfc_state.is_some()
    );
    // Initialize FloatingContext for this BFC
    //
    // We always recalculate float positions in this pass, but we'll store them in the cache
    // so that subsequent layout passes (for auto-sizing) have access to the positioned floats
89736
    let mut float_context = FloatingContext::default();
    // +spec:containing-block:42b75f - Block element establishes containing block for inline content (IFC)
    // Calculate this node's content-box size for use as containing block for children
    // CSS 2.2 § 10.1: The containing block for in-flow children is formed by the
    // content edge of the parent's content box.
    //
    // We use constraints.available_size directly as this already represents the
    // content-box available to this node (set by parent). For nodes with explicit
    // sizes, used_size contains the border-box which we convert to content-box.
    //
    // NOTE(writing-modes): The containing block size uses physical width/height.
    // In vertical writing modes, the block progression direction is horizontal,
    // so the "available width" for children maps to the physical height of
    // the containing block. The main_pen variable below tracks block progression
    // using logical main-axis coordinates; the WritingModeContext in constraints
    // determines how main/cross map to physical x/y via from_main_cross().
    // +spec:inline-block:17944a - orthogonal flow roots get infinite available inline space here (not yet detected)
    // +spec:inline-block:a60e22 - other layout models pass through infinite inline space to contained block containers
89736
    let mut children_containing_block_size = node.used_size.map_or_else(
        // No used_size yet - use available_size directly (this is already content-box
        // when coming from parent's layout constraints)
        || constraints.available_size,
45208
        |used_size| {
            // Node has used_size (border-box) - convert to content-box.
            // For auto-height containers, the pre-layout `used_size.height` is a
            // placeholder (calculate_used_size_for_node returns 0 for block-level
            // auto-height; apply_content_based_height resolves it after children lay
            // out). In that window, `constraints.available_size.height` holds the
            // containing block's height — the value children should use as their own
            // containing block for percentage-height / indefinite-height semantics.
45208
            let inner = node.box_props.inner_size(used_size, writing_mode);
45208
            let height_is_auto = tree
45208
                .warm(LayoutNodeId::new(node_index))
45208
                .is_none_or(|w| w.computed_style.height.is_none());
45208
            if height_is_auto {
38091
                LogicalSize::new(inner.width, constraints.available_size.height)
            } else {
7117
                inner
            }
45208
        },
    );
    // +spec:overflow:ffe6f7 - scrollbar space subtracted from containing block per spec §11.1.1
    // Reserve space for vertical scrollbar when appropriate.
    //
    // - overflow: scroll  → ALWAYS reserve (CSS spec: scrollbar always shown)
    // - overflow: auto    → Reserve ONLY when a previous pass already determined
    //   a scrollbar is needed.
    //   On the very first pass the node has no scrollbar_info yet, so no space
    //   is reserved.  After `compute_scrollbar_info` detects overflow it sets
    //   `reflow_needed_for_scrollbars = true`, triggering a second pass where
    //   `node.scrollbar_info.needs_vertical == true` and space IS reserved.
    //   Each pass replaces `scrollbar_info` with the current state; the outer
    //   layout loop's iteration cap handles oscillation safety.
89736
    let scrollbar_reservation = node
89736
        .dom_node_id
89736
        .map_or(0.0, |dom_id| {
89734
            let styled_node_state = ctx
89734
                .styled_dom
89734
                .styled_nodes
89734
                .as_container()
89734
                .get(dom_id)
89734
                .map(|s| s.styled_node_state)
89734
                .unwrap_or_default();
89734
            let overflow_y =
89734
                get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
89734
            match overflow_y.unwrap_or_default() {
                LayoutOverflow::Scroll => {
114
                    crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
                }
                LayoutOverflow::Auto => {
333
                    let already_needs = tree.warm(LayoutNodeId::new(node_index))
333
                        .and_then(|w| w.scrollbar_info.as_ref())
333
                        .is_some_and(|s| s.needs_vertical);
333
                    if already_needs {
9
                        crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
                    } else {
324
                        0.0
                    }
                }
89287
                _ => 0.0,
            }
89734
        });
89736
    if scrollbar_reservation > 0.0 {
123
        children_containing_block_size.width =
123
            (children_containing_block_size.width - scrollbar_reservation).max(0.0);
89613
    }
    // === Pass 1: Pre-compute child sizes (restored two-pass BFC) ===
    //
    // Inspired by Taffy's two-pass approach: first measure, then position.
    //
    // This was removed in commit 1a3e5850 and replaced with a single-pass approach
    // that computed sizes just-in-time during positioning. The single-pass approach
    // caused regression 8e092a2e because positioning decisions (margin collapsing,
    // float clearance, available width after floats) depend on knowing ALL sibling
    // sizes upfront, not just the ones visited so far.
    //
    // With the per-node cache (§9.1-§9.2), the re-added Pass 1 is efficient:
    // - Each child subtree is computed once and stored in NodeCache
    // - Pass 2 positioning reads sizes from tree nodes (used_size set by Pass 1)
    // - When calculate_layout_for_subtree recurses into children after layout_bfc
    //   returns, it hits the per-node cache (same available_size) — O(1) per child.
    //
    // Performance: O(n) for the tree. No double-computation thanks to caching.
    {
89736
        let mut temp_positions: super::PositionVec = Vec::new();
89736
        let mut temp_scrollbar_reflow = false;
89736
        let bfc_children = tree.children(node_index).to_vec();
        // [g147c az-web-lift DIAG] layout_bfc Pass-1 child-sizing loop: record bfc_children.len per parent
        // node (0x60A00+slot). If body shows len=2 but the divs never get the per-child "sized" marker
        // (0x60A40+childslot) below → the loop skips them; if they DO get it but layout_formatting_context
        // (0x609A0) stays unset → calculate(child,ComputeSize) cache-hit (vs 0x60A60 miss-flag in cache.rs).
        #[cfg(feature = "web_lift")]
        unsafe { crate::az_mark(((0x60A00 + (node_index & 7) * 4)) as u32, (bfc_children.len() as u32 | 0xC0DE0000) as u32); }
119467
        for &child_index in &bfc_children {
29731
            let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
29731
            let child_dom_id = child_node.dom_node_id;
            // +spec:positioning:447b06 - Absolute positioning pulls element out of flow, skip from normal layout
            // +spec:positioning:77a2d2 - Absolutely positioned children are ignored for auto height
            // +spec:positioning:b47ac2 - Only normal flow children taken into account for auto height
            // Skip absolutely/fixed positioned children — they're laid out separately
            // +spec:positioning:c7e5c5 - out-of-flow elements ignored for word boundary / hyphenation
            // +spec:positioning:7dd6d1 - Absolutely positioned boxes are taken out of the normal flow (no impact on later siblings, no margin collapsing)
29731
            let position_type = get_position_type(ctx.styled_dom, child_dom_id);
29731
            if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
5272
                continue;
24459
            }
            // Compute the child's full subtree layout with temporary positions.
            // Position (0,0) is intentionally wrong — Pass 1 only cares about sizing.
            // The correct positions are determined in Pass 2 below.
            // [g147c] this child IS reached by Pass-1 sizing (per-child slot).
            #[cfg(feature = "web_lift")]
            unsafe { crate::az_mark(((0x60A40 + (child_index & 7) * 4)) as u32, (0xC0DE0000 | (child_index as u32 & 0xffff)) as u32); }
24459
            crate::solver3::cache::calculate_layout_for_subtree(
24459
                ctx,
24459
                tree,
24459
                text_cache,
24459
                child_index,
24459
                LogicalPosition::zero(),
24459
                children_containing_block_size,
24459
                &mut temp_positions,
24459
                &mut temp_scrollbar_reflow,
24459
                float_cache,
24459
                crate::solver3::cache::ComputeMode::ComputeSize,
            )?;
        }
    }
    // +spec:block-formatting-context:98b633 - CSS 2.2 § 9.4.1: boxes laid out vertically, margins collapse
    // === Pass 2: Position children using known sizes ===
    //
    // All children now have used_size set from Pass 1. This pass handles:
    // - Margin collapsing (parent-child + sibling-sibling)
    // - Float positioning and clearance
    // - Normal flow block positioning
89736
    let mut main_pen = 0.0f32;
89736
    let mut max_cross_size = 0.0f32;
    // Track escaped margins separately from content-box height
    // CSS 2.2 § 8.3.1: Escaped margins don't contribute to parent's content-box height,
    // but DO affect sibling positioning within the parent
89736
    let mut total_escaped_top_margin = 0.0f32;
    // Track all inter-sibling margins (collapsed) - these are also not part of content height
89736
    let mut total_sibling_margins = 0.0f32;
    // Margin collapsing state
89736
    let mut last_margin_bottom = 0.0f32;
89736
    let mut is_first_child = true;
89736
    let mut first_child_index: Option<usize> = None;
89736
    let mut last_child_index: Option<usize> = None;
    // Parent's own margins (for escape calculation)
89736
    let node_bp = node.box_props.unpack();
89736
    let parent_margin_top = node_bp.margin.main_start(writing_mode);
89736
    let parent_margin_bottom = node_bp.margin.main_end(writing_mode);
    // margins do not collapse across formatting context boundaries: an independent
    // BFC (float, overflow != visible, display: flex/grid, etc.) isolates its
    // children's margins. The DOM root is NOT a BFC boundary for this purpose —
    // its first child's margin still collapses through it (then gets absorbed at
    // the root, since there's no grandparent to escape to).
89736
    let establishes_own_bfc = establishes_new_bfc(ctx, &node, tree.cold(LayoutNodeId::new(node_index)));
89736
    let is_bfc_root = node.parent.is_none() || establishes_own_bfc;
    // parent_has_*_blocker inhibits parent-child margin collapse per CSS 2.2 §8.3.1.
    // An explicit border/padding blocks, and an independent BFC blocks, but the
    // root on its own does not.
89736
    let parent_has_top_blocker = establishes_own_bfc
8065
        || has_margin_collapse_blocker(&node_bp, writing_mode, true);
89736
    let parent_has_bottom_blocker = establishes_own_bfc
8065
        || has_margin_collapse_blocker(&node_bp, writing_mode, false);
    // Track accumulated top margin for first-child escape
89736
    let mut accumulated_top_margin = 0.0f32;
89736
    let mut top_margin_resolved = false;
    // Track if first child's margin escaped (for return value)
89736
    let mut top_margin_escaped = false;
    // Track if we have any actual content (non-empty blocks)
89736
    let mut has_content = false;
    // +spec:display-property:9f6e18 - BFC dispatches normal flow, floats, and relative positioning (CSS 2.2 §9.8)
89736
    let pos_children = tree.children(node_index).to_vec();
    // +spec:width-calculation:bef810 - margin percentages resolve against the containing block
    // +spec:box-model:66e123 - ...whose INLINE size is the basis in CSS3 (writing-modes-4 §7.2)
    // The tree-build resolution used the VIEWPORT as a placeholder containing
    // block (the real one is only known here), so every percentage margin or
    // padding in block flow was viewport-based. Re-resolve each child's box
    // props against this BFC's content box before any of them are read; the
    // correct em/rem bases are re-derived from the cascade.
    {
89736
        let root_fs = crate::solver3::layout_tree::get_root_font_size(ctx.styled_dom);
119467
        for &child_index in &pos_children {
29731
            let Some(child_dom_id) = tree.get(LayoutNodeId::new(child_index)).and_then(|n| n.dom_node_id) else {
122
                continue;
            };
29609
            let efs =
29609
                crate::solver3::layout_tree::get_element_font_size(ctx.styled_dom, child_dom_id);
29609
            tree.resolve_box_props(
29609
                child_index,
29609
                children_containing_block_size,
29609
                ctx.viewport_size,
29609
                efs,
29609
                root_fs,
            );
        }
    }
    // K30b fragmentation state (inert when `constraints.fragmentainer` is
    // None — the continuous path). Resume = skip every finished sibling
    // before the token's first unfinished child WITH ZERO side effects
    // (before any margin/pen/float bookkeeping); break = emit the
    // unfinished tail as the outgoing token and stop consuming children.
89736
    let fragment_resume_from: Option<usize> = constraints
89736
        .fragmentainer
89736
        .as_ref()
89736
        .and_then(|fs| fs.resume)
89736
        .and_then(crate::solver3::break_token::resume_plan)
89736
        .map(|p| p.first_unfinished);
    // K30b part 2: per-child resume tokens (ResumeIn entries). A child in
    // this map continues from ITS token in a re-laid subtree; BreakBefore
    // children (and Inline tokens, v1) lay out from scratch.
89736
    let fragment_resume_tokens: BTreeMap<
89736
        usize,
89736
        &crate::solver3::break_token::BreakToken,
89736
    > = constraints
89736
        .fragmentainer
89736
        .as_ref()
89736
        .and_then(|fs| fs.resume)
89736
        .map(|tok| {
36
            tok.children
36
                .iter()
61
                .filter_map(|e| match e {
                    crate::solver3::break_token::ChildBreakEntry::ResumeIn {
20
                        child,
20
                        token,
20
                    } => Some((*child, &**token)),
41
                    crate::solver3::break_token::ChildBreakEntry::BreakBefore { .. } => None,
61
                })
36
                .collect()
36
        })
89736
        .unwrap_or_default();
89736
    let mut fragment_resume_reached = fragment_resume_from.is_none();
89736
    let mut fragment_placed_content = false;
89736
    let mut fragment_token_out: Option<crate::solver3::break_token::BreakToken> = None;
    // css-break-3 §5.2: margins adjoining an UNFORCED break truncate; a
    // FORCED break (break-before: page / <pagebreak/>) keeps them. Pending
    // until the first resumed child places.
89736
    let mut fragment_truncate_first_margin: bool = constraints
89736
        .fragmentainer
89736
        .as_ref()
89736
        .and_then(|fs| fs.resume)
89736
        .and_then(|tok| tok.children.first())
89736
        .is_some_and(|entry| match entry {
16
            crate::solver3::break_token::ChildBreakEntry::BreakBefore { forced, .. } => !*forced,
            // A ResumeIn child CONTINUES mid-box: its top decoration/margin
            // belongs to its first fragment — nothing to apply here anyway.
20
            crate::solver3::break_token::ChildBreakEntry::ResumeIn { .. } => true,
36
        });
    // Fragment passes mark every child they DON'T place with a sentinel
    // relative position: the positioning descent then computes far-negative
    // absolutes for the whole stale subtree and the display-list builder's
    // unassigned-position guard drops the items (the sentinel survives
    // offset arithmetic by magnitude — that is why UNASSIGNED_POSITION_LIMIT
    // is f32::MIN / 2). Without this, skipped/broken children reappear on
    // every page at their CONTINUOUS positions.
89736
    let fragment_pass = constraints.fragmentainer.is_some();
    macro_rules! clear_fragment_pos {
        ($child:expr) => {
            if fragment_pass {
                if let Some(w) = tree.warm_mut($child) {
                    w.relative_position =
                        Some(LogicalPosition::new(f32::MIN, f32::MIN));
                }
            }
        };
    }
119467
    for &child_index in &pos_children {
        // A token emitted while PLACING the previous child (break-descend /
        // resumed-child continuation) stops sibling consumption here — the
        // rest of the tail is not on this page.
29731
        if fragment_token_out.is_some() {
25
            clear_fragment_pos!(LayoutNodeId::new(child_index));
25
            continue;
29706
        }
29706
        if !fragment_resume_reached {
76
            if Some(child_index) == fragment_resume_from {
36
                fragment_resume_reached = true;
36
            } else {
                // Finished on an earlier fragmentainer.
40
                clear_fragment_pos!(LayoutNodeId::new(child_index));
40
                continue;
            }
29630
        }
        // K31: snapshot the pen BEFORE this child contributes anything —
        // a break emitted at this child rolls the pen back here (the margin
        // adjoining an unforced break truncates on BOTH sides; the pen had
        // already advanced past the child's collapsed top margin when the
        // fit check runs).
29666
        let fragment_pen_at_child = main_pen;
        // K31 forced breaks: `break-before: page` (incl. the UA rule on
        // `<pagebreak/>` nodes — which are EMPTY blocks and short-circuit
        // before the fit check, hence this sits at the loop top). Only
        // once this fragmentainer holds content (a forced break at the top
        // of a fresh page is vacuously satisfied, else every page would
        // re-break forever).
29666
        if fragment_pass
99
            && fragment_placed_content
38
            && fragment_token_out.is_none()
38
            && crate::solver3::getters::get_break_before(
38
                ctx.styled_dom,
38
                tree.get(LayoutNodeId::new(child_index)).and_then(|n| n.dom_node_id),
38
            ) != azul_css::props::layout::fragmentation::PageBreak::Auto
        {
1
            let later: Vec<usize> = pos_children
1
                .iter()
1
                .copied()
2
                .skip_while(|&c| c != child_index)
1
                .skip(1)
1
                .filter(|&c| {
1
                    let pt = get_position_type(
1
                        ctx.styled_dom,
1
                        tree.get(LayoutNodeId::new(c)).and_then(|n| n.dom_node_id),
                    );
1
                    pt != LayoutPosition::Absolute && pt != LayoutPosition::Fixed
1
                })
1
                .collect();
1
            let mut children =
1
                alloc::vec![crate::solver3::break_token::ChildBreakEntry::BreakBefore {
1
                    child: child_index,
1
                    forced: true,
1
                }];
1
            children.extend(later.into_iter().map(|child| {
1
                crate::solver3::break_token::ChildBreakEntry::BreakBefore {
1
                    child,
1
                    forced: false,
1
                }
1
            }));
1
            fragment_token_out = Some(crate::solver3::break_token::BreakToken::Block(
1
                crate::solver3::break_token::BlockBreakToken {
1
                    node: node_index,
1
                    consumed_block_size: main_pen,
1
                    children,
1
                    generation: 0,
1
                },
1
            ));
1
            clear_fragment_pos!(LayoutNodeId::new(child_index));
1
            continue;
29665
        }
29665
        let mut fragment_child_resumed = false;
        // K30b part 2, RESUME arm: this child carries a Block resume token —
        // RE-LAY its subtree from that token inside the remaining extent
        // (its used_size then reflects only the remaining content). If it
        // STILL does not finish, emit its continuation and stop after
        // placing it. Inline tokens re-lay from scratch in v1.
29665
        if let Some(fs) = constraints.fragmentainer.as_ref().copied() {
20
            if let Some(crate::solver3::break_token::BreakToken::Block(child_tok)) =
98
                fragment_resume_tokens.get(&child_index).copied()
            {
20
                let child_space = FragmentainerSpace {
20
                    remaining_block_extent: (fs.remaining_block_extent - main_pen).max(0.0),
20
                    next_fragmentainer_extent: fs.next_fragmentainer_extent,
20
                    is_first: false,
20
                    resume: Some(child_tok),
20
                };
20
                let mut child_out: Option<crate::solver3::break_token::BreakToken> = None;
20
                let mut tmp_positions: super::PositionVec = Vec::new();
20
                let mut tmp_scrollbars = false;
20
                crate::solver3::cache::calculate_layout_for_subtree_fragment(
20
                    ctx,
20
                    tree,
20
                    text_cache,
20
                    child_index,
20
                    LogicalPosition::zero(),
20
                    children_containing_block_size,
20
                    &mut tmp_positions,
20
                    &mut tmp_scrollbars,
20
                    float_cache,
20
                    crate::solver3::cache::ComputeMode::ComputeSize,
20
                    Some(child_space),
20
                    Some(&mut child_out),
                )?;
20
                fragment_child_resumed = true;
20
                if let Some(cont) = child_out {
8
                    let later: Vec<usize> = pos_children
8
                        .iter()
8
                        .copied()
8
                        .skip_while(|&c| c != child_index)
8
                        .skip(1)
8
                        .filter(|&c| {
                            let pt = get_position_type(
                                ctx.styled_dom,
                                tree.get(LayoutNodeId::new(c)).and_then(|n| n.dom_node_id),
                            );
                            pt != LayoutPosition::Absolute && pt != LayoutPosition::Fixed
                        })
8
                        .collect();
8
                    let mut children =
8
                        alloc::vec![crate::solver3::break_token::ChildBreakEntry::ResumeIn {
8
                            child: child_index,
8
                            token: Box::new(cont),
8
                        }];
8
                    children.extend(later.into_iter().map(|child| {
                        crate::solver3::break_token::ChildBreakEntry::BreakBefore {
                            child,
                            forced: false,
                        }
                    }));
8
                    fragment_token_out = Some(crate::solver3::break_token::BreakToken::Block(
8
                        crate::solver3::break_token::BlockBreakToken {
8
                            node: node_index,
8
                            consumed_block_size: main_pen,
8
                            children,
8
                            generation: 0,
8
                        },
8
                    ));
                    // fall through: PLACE the fitted part of this child;
                    // the loop-top guard stops the following siblings.
12
                }
78
            }
29567
        }
29665
        let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
29665
        let child_dom_id = child_node.dom_node_id;
        // +spec:floats:2cec1b - 'position' and 'float' determine the positioning algorithm
        // +spec:positioning:dccad6 - floats only apply to non-absolutely-positioned boxes
29665
        let position_type = get_position_type(ctx.styled_dom, child_dom_id);
29665
        if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
5272
            continue;
24393
        }
        // +spec:floats:2cec1b - float property determines positioning algorithm (float path)
        // +spec:floats:f6c0b2 - floats only processed in BFC; other formatting contexts (flex/grid) inhibit floating
        // Check if this child is a float - if so, position it at current main_pen
24393
        if let Some(node_id) = child_dom_id {
24271
            let float_type = get_float_property(ctx.styled_dom, Some(node_id));
24271
            if float_type != LayoutFloat::None {
                // Calculate float size just-in-time if not already computed
52
                let float_size = if let Some(size) = child_node.used_size { size } else {
                    let intrinsic = tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
                    let child_bp = child_node.box_props.unpack();
                    let computed_size = crate::solver3::sizing::calculate_used_size_for_node(
                        ctx.styled_dom,
                        child_dom_id,
                        &children_containing_block_size,
                        intrinsic,
                        &child_bp,
                        &ctx.viewport_size,
                    )?;
                    if let Some(node_mut) = tree.get_mut(LayoutNodeId::new(child_index)) {
                        node_mut.used_size = Some(computed_size);
                    }
                    computed_size
                };
                // Re-borrow after potential mutation
52
                let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
52
                let child_bp2 = child_node.box_props.unpack();
52
                let float_margin = &child_bp2.margin;
                // +spec:floats:d0d163 - clear on floats adds constraint #10: float top below cleared floats' bottom
                // +spec:floats:7adb9d - Clear on floats: constraint #10, top outer edge must be below earlier cleared floats
52
                let float_clear = get_clear_property(ctx.styled_dom, Some(node_id));
52
                let float_y = if float_clear == LayoutClear::None {
                    // +spec:floats:ef96cb - Float margins never collapse with adjacent margins
                    // CSS 2.2 § 9.5: Float margins don't collapse with any other margins.
52
                    main_pen + last_margin_bottom
                } else {
                    float_context.clearance_offset(float_clear, main_pen + last_margin_bottom, writing_mode)
                };
52
                debug_info!(
26
                    ctx,
26
                    "[layout_bfc] Positioning float: index={}, type={:?}, size={:?}, at Y={} \
26
                     (main_pen={} + last_margin={})",
                    child_index,
                    float_type,
                    float_size,
                    float_y,
                    main_pen,
                    last_margin_bottom
                );
                // Position the float at the CURRENT main_pen + last margin (respects DOM order!)
52
                let float_rect = position_float(
52
                    &float_context,
52
                    float_type,
52
                    float_size,
52
                    float_margin,
                    // Include last_margin_bottom since float margins don't collapse!
52
                    float_y,
52
                    constraints.available_size.cross(writing_mode),
52
                    writing_mode,
                );
52
                debug_info!(ctx, "[layout_bfc] Float positioned at: {:?}", float_rect);
                // K32: floats participate in fragmentation ATOMICALLY (an
                // anchored image never splits — the Word model). A float
                // that does not fit the remaining extent moves WHOLE to the
                // next fragmentainer via the unfinished tail; its exclusion
                // geometry then belongs to THAT page only (each layout_bfc
                // call seeds a fresh FloatingContext, so nothing leaks
                // across fragmentainers by construction).
52
                if let Some(fs) = constraints.fragmentainer.as_ref() {
5
                    let float_bottom = float_rect.origin.main(writing_mode)
5
                        + float_rect.size.main(writing_mode);
5
                    let fits = float_bottom <= fs.remaining_block_extent + 0.01;
5
                    if !fits && fragment_placed_content {
2
                        let later: Vec<usize> = pos_children
2
                            .iter()
2
                            .copied()
6
                            .skip_while(|&c| c != child_index)
2
                            .skip(1)
4
                            .filter(|&c| {
4
                                let pt = get_position_type(
4
                                    ctx.styled_dom,
4
                                    tree.get(LayoutNodeId::new(c)).and_then(|n| n.dom_node_id),
                                );
4
                                pt != LayoutPosition::Absolute
4
                                    && pt != LayoutPosition::Fixed
4
                            })
2
                            .collect();
2
                        main_pen = fragment_pen_at_child;
2
                        fragment_token_out =
2
                            Some(crate::solver3::break_token::tail_token(
2
                                node_index,
2
                                main_pen,
2
                                child_index,
2
                                later.into_iter(),
2
                            ));
2
                        clear_fragment_pos!(LayoutNodeId::new(child_index));
2
                        continue;
3
                    }
                    // First content overflowing every page: monolith-place
                    // (falls through), same rule as atomic blocks.
47
                }
                // Add to float context BEFORE positioning next element
50
                float_context.add_float(float_type, float_rect, *float_margin);
                // Store position in output
50
                output.positions.insert(child_index, float_rect.origin);
50
                debug_info!(
26
                    ctx,
26
                    "[layout_bfc] *** FLOAT POSITIONED: child={}, main_pen={} (unchanged - floats \
26
                     don't advance pen)",
                    child_index,
                    main_pen
                );
50
                if constraints.fragmentainer.is_some() {
3
                    fragment_placed_content = true;
47
                }
                // Floats are taken out of normal flow - DON'T advance main_pen
                // Continue to next child
50
                continue;
24219
            }
122
        }
        // Floats `continue` above; everything reaching here is normal-flow
        // (non-float) content.
        // From here: normal flow (non-float) children only
        // Track first and last in-flow children for parent-child collapse
24341
        if first_child_index.is_none() {
8731
            first_child_index = Some(child_index);
15712
        }
24341
        last_child_index = Some(child_index);
        // Calculate child's used_size just-in-time if not already computed
        // This replaces the old "Pass 1" that recursively laid out grandchildren with wrong positions
24341
        let child_size = if let Some(size) = child_node.used_size { size } else {
            // Calculate size without recursive layout
            let intrinsic = tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
            let child_used_size = crate::solver3::sizing::calculate_used_size_for_node(
                ctx.styled_dom,
                child_dom_id,
                &children_containing_block_size,
                intrinsic,
                &child_node.box_props.unpack(),
                &ctx.viewport_size,
            )?;
            // Update the node with computed size (we need to re-borrow mutably)
            if let Some(node_mut) = tree.get_mut(LayoutNodeId::new(child_index)) {
                node_mut.used_size = Some(child_used_size);
            }
            child_used_size
        };
        // Re-borrow child_node after potential mutation
24341
        let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
24341
        let child_bp = child_node.box_props.unpack();
24341
        let child_margin = &child_bp.margin;
24341
        debug_info!(
13500
            ctx,
13500
            "[layout_bfc] Child {} margin from box_props: top={}, right={}, bottom={}, left={}",
            child_index,
            child_margin.top,
            child_margin.right,
            child_margin.bottom,
            child_margin.left
        );
        // +spec:block-formatting-context:0f802c - margins use containing block's writing mode for collapsing/auto expansion in orthogonal flows
24341
        let child_own_margin_top = child_margin.main_start(writing_mode);
24341
        let child_own_margin_bottom = child_margin.main_end(writing_mode);
        // CSS 2.2 § 8.3.1: If a child has no top blocker (no padding/border) and its
        // own BFC layout produced an escaped_top_margin, that margin represents the
        // collapsed value of (child's margin, child's first child's margin, ...).
        // Use it for sibling collapse instead of the child's own margin.
24341
        let child_escaped_top = if has_margin_collapse_blocker(&child_bp, writing_mode, true) { None } else {
23471
            tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.escaped_top_margin)
        };
24341
        let child_escaped_bottom = if has_margin_collapse_blocker(&child_bp, writing_mode, false) { None } else {
22277
            tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.escaped_bottom_margin)
        };
24341
        let mut child_margin_top = child_escaped_top.unwrap_or(child_own_margin_top);
24341
        let child_margin_bottom = child_escaped_bottom.unwrap_or(child_own_margin_bottom);
        // K31: the first child placed after an UNFORCED fragmentation break
        // starts flush at the fragmentainer top (css-break-3 §5.2).
24341
        if fragment_truncate_first_margin && !fragment_placed_content {
33
            child_margin_top = 0.0;
33
            fragment_truncate_first_margin = false;
24308
        }
24341
        debug_info!(
13500
            ctx,
13500
            "[layout_bfc] Child {} final margins: margin_top={}, margin_bottom={}",
            child_index,
            child_margin_top,
            child_margin_bottom
        );
        // Check if this child has border/padding that prevents margin collapsing
24341
        let child_has_top_blocker =
24341
            has_margin_collapse_blocker(&child_bp, writing_mode, true);
24341
        let child_has_bottom_blocker =
24341
            has_margin_collapse_blocker(&child_bp, writing_mode, false);
        // +spec:floats:dc195a - Clear property only applies to block-level elements (CSS 2.2 § 9.5.2)
        // Check for clear property FIRST - clearance affects whether element is considered empty
        // CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing"
        // An element with clearance is NOT empty even if it has no content
24341
        let child_clear = if let Some(node_id) = child_dom_id {
24219
            get_clear_property(ctx.styled_dom, Some(node_id))
        } else {
122
            LayoutClear::None
        };
24341
        debug_info!(
13500
            ctx,
13500
            "[layout_bfc] Child {} clear property: {:?}",
            child_index,
            child_clear
        );
        // PHASE 1: Empty Block Detection & Self-Collapse
24341
        let is_empty = is_empty_block(tree, child_index);
        // Handle empty blocks FIRST (they collapse through and don't participate in layout)
        // EXCEPTION: Elements with clear property are NOT skipped even if empty!
        // CSS 2.2 § 9.5.2: Clear property affects positioning even for empty elements
24341
        if is_empty
31
            && !child_has_top_blocker
31
            && !child_has_bottom_blocker
31
            && child_clear == LayoutClear::None
        {
            // Empty block: collapse its own top and bottom margins FIRST
31
            let self_collapsed = collapse_margins(child_margin_top, child_margin_bottom);
            // Then collapse with previous margin (sibling or parent)
            let seam_main;
31
            if is_first_child {
7
                is_first_child = false;
                // Empty first child: its collapsed margin can escape with parent's
7
                if parent_has_top_blocker {
                    // Parent has blocker: add margins
                    if accumulated_top_margin == 0.0 {
                        accumulated_top_margin = parent_margin_top;
                    }
                    main_pen += accumulated_top_margin + self_collapsed;
                    top_margin_resolved = true;
                    accumulated_top_margin = 0.0;
7
                } else {
7
                    accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
7
                }
                // Both arms seat the seam at the (possibly advanced) pen.
7
                seam_main = main_pen;
7
                last_margin_bottom = self_collapsed;
24
            } else {
24
                // Empty sibling: collapse with previous sibling's bottom margin
24
                last_margin_bottom = collapse_margins(last_margin_bottom, self_collapsed);
24
                seam_main = main_pen + last_margin_bottom;
24
            }
            // A collapsed-through empty block still HAS a position — CSS 2.2
            // §8.3.1 makes its top and bottom border edges coincide inside the
            // collapsed seam, it does not remove the box. Omitting it from
            // `output.positions` left the node AND its whole subtree at the
            // POSITION_UNSET sentinel, so their Border/HitTestArea items were
            // emitted at (f32::MIN, f32::MIN) and dropped by the display-list
            // guard (the MicrophoneWidget/CameraWidget invisible-div pattern:
            // an empty dataset-carrier div silently lost its hit area).
31
            output.positions.insert(
31
                child_index,
31
                LogicalPosition::from_main_cross(
31
                    seam_main,
31
                    child_bp.margin.cross_start(writing_mode),
31
                    writing_mode,
                ),
            );
            // Skip pen advance (empty has no visual presence)
31
            continue;
24310
        }
        // From here on: non-empty blocks only (or empty blocks with clear property)
        // Apply clearance if needed
        // +spec:floats:148ee6 - clear:left pushes element below float; clearance added above top margin
        // CSS 2.2 § 9.5.2: Clearance inhibits margin collapsing.
        //
        // Per CSS 2.2 § 9.5.2, the clearance computation works as follows:
        // 1. Compute the "hypothetical position" — where the border edge would be
        //    with normal margin collapsing (as if clear:none).
        // 2. If the hypothetical position is NOT past the relevant floats,
        //    clearance is introduced and the border edge is placed at float bottom.
        // 3. The final border edge = max(float_bottom, hypothetical_position).
        //
        // This means child_margin_top is already accounted for in the hypothetical
        // position and must NOT be added again after clearance positions main_pen.
24310
        let clearance_applied = if child_clear == LayoutClear::None {
24305
            false
        } else {
5
            let hypothetical = main_pen + collapse_margins(last_margin_bottom, child_margin_top);
5
            let cleared_position =
5
                float_context.clearance_offset(child_clear, hypothetical, writing_mode);
5
            debug_info!(
5
                ctx,
5
                "[layout_bfc] Child {} clearance check: cleared_position={}, hypothetical={} (main_pen={} + collapse({}, {}))",
                child_index,
                cleared_position,
                hypothetical,
                main_pen,
                last_margin_bottom,
                child_margin_top
            );
5
            if cleared_position > hypothetical {
4
                debug_info!(
4
                    ctx,
4
                    "[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
                    child_index,
                    child_clear,
                    main_pen,
                    cleared_position
                );
4
                main_pen = cleared_position;
4
                true // Signal that clearance was applied
            } else {
1
                false
            }
        };
        // PHASE 2: Parent-Child Top Margin Escape (First Child)
        //
        // CSS 2.2 § 8.3.1: "The top margin of a box is adjacent to the top margin of its first
        // in-flow child if the box has no top border, no top padding, and the child has no
        // clearance." CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing"
24310
        if is_first_child {
8724
            is_first_child = false;
            // Clearance prevents collapse (acts as invisible blocker)
8724
            if clearance_applied {
                // Clearance inhibits all margin collapsing for this element
                // The clearance has already positioned main_pen at the correct
                // border-edge position (= max(float_bottom, hypothetical)).
                // The hypothetical already includes child_margin_top via
                // collapse_margins, so we must NOT add it again here.
3
                debug_info!(
3
                    ctx,
3
                    "[layout_bfc] First child {} with CLEARANCE: no collapse, child_margin={}, \
3
                     main_pen={}",
                    child_index,
                    child_margin_top,
                    main_pen
                );
8721
            } else if !parent_has_top_blocker {
                // Margin Escape Case
                //
                // CSS 2.2 § 8.3.1: "The top margin of an in-flow block element collapses with
                // its first in-flow block-level child's top margin if the element has no top
                // border, no top padding, and the child has no clearance."
                //
                // When margins collapse, they "escape" upward through the parent to be resolved
                // in the grandparent's coordinate space. This is critical for understanding the
                // coordinate system separation:
                //
                // Example:
                // <body padding=20>
                //  <div margin=0>
                //      <div margin=30></div>
                //  </div>
                // </body>
                //
                //   - Middle div (our parent) has no padding → margins can escape
                //   - Inner div's 30px margin collapses with middle div's 0px margin = 30px
                //   - This 30px margin "escapes" to be handled by body's BFC
                //   - Body positions middle div at Y=30 (relative to body's content-box)
                //   - Middle div's content-box height does NOT include the escaped 30px
                //   - Inner div is positioned at Y=0 in middle div's content-box
                //
                // **NOTE**: This is a subtle but critical distinction in coordinate systems:
                //
                //   - Parent's margin belongs to grandparent's coordinate space
                //   - Child's margin (when escaped) also belongs to grandparent's coordinate space
                //   - They collapse BEFORE entering this BFC's coordinate space
                //   - We return the collapsed margin so grandparent can position parent correctly
                //
                // **NOTE**: Child's own blocker status (padding/border) is IRRELEVANT for
                // parent-child  collapse. The child may have padding that prevents
                // collapse with ITS OWN  children, but this doesn't prevent its
                // margin from escaping  through its parent.
                //
                // **NOTE**: Previously, we incorrectly added parent_margin_top to main_pen in
                //  the blocked case, which double-counted the margin by mixing
                //  coordinate systems. The parent's margin is NEVER in our (the
                //  parent's content-box) coordinate system!
                //
                // We collapse the parent's margin with the child's margin.
                // This combined margin is what "escapes" to the grandparent.
                // The grandparent uses this to position the parent.
                //
                // Effectively, we are saying "The parent starts here, but its effective
                // top margin is now max(parent_margin, child_margin)".
5495
                accumulated_top_margin = collapse_margins(parent_margin_top, child_margin_top);
5495
                top_margin_resolved = true;
5495
                top_margin_escaped = true;
                // Track escaped margin so it gets subtracted from content-box height
                // The escaped margin is NOT part of our content-box - it belongs to our
                // parent's parent
5495
                total_escaped_top_margin = accumulated_top_margin;
                // Position child at pen (no margin applied - it escaped!)
5495
                debug_info!(
4399
                    ctx,
4399
                    "[layout_bfc] First child {} margin ESCAPES: parent_margin={}, \
4399
                     child_margin={}, collapsed={}, total_escaped={}",
                    child_index,
                    parent_margin_top,
                    child_margin_top,
                    accumulated_top_margin,
                    total_escaped_top_margin
                );
            } else {
                // Margin Blocked Case
                //
                // CSS 2.2 § 8.3.1: "no top padding and no top border" required for collapse.
                // When padding or border exists, margins do NOT collapse and exist in different
                // coordinate spaces.
                //
                // CRITICAL COORDINATE SYSTEM SEPARATION:
                //
                //   This is where the architecture becomes subtle. When layout_bfc() is called:
                //   1. We are INSIDE the parent's content-box coordinate space (main_pen starts at
                //      0)
                //   2. The parent's own margin was ALREADY RESOLVED by the grandparent's BFC
                //   3. The parent's margin is in the grandparent's coordinate space, not ours
                //   4. We NEVER reference the parent's margin in this BFC - it's outside our scope
                //
                // Example:
                //
                // <body padding=20>
                //   <div margin=30 padding=20>
                //      <div margin=30></div>
                //   </div>
                // </body>
                //
                //   - Middle div has padding=20 → blocker exists, margins don't collapse
                //   - Body's BFC positions middle div at Y=30 (middle div's margin, in body's
                //     space)
                //   - Middle div's BFC starts at its content-box (after the padding)
                //   - main_pen=0 at the top of middle div's content-box
                //   - Inner div has margin=30 → we add 30 to main_pen (in OUR coordinate space)
                //   - Inner div positioned at Y=30 (relative to middle div's content-box)
                //   - Absolute position: 20 (body padding) + 30 (middle margin) + 20 (middle
                //     padding) + 30 (inner margin) = 100px
                //
                // **NOTE**: Previous code incorrectly added parent_margin_top to main_pen here:
                //
                //     - main_pen += parent_margin_top;  // WRONG! Mixes coordinate systems
                //     - main_pen += child_margin_top;
                //
                //   This caused the "double margin" bug where margins were applied twice:
                //
                //   - Once by grandparent positioning parent (correct)
                //   - Again inside parent's BFC (INCORRECT - wrong coordinate system)
                //
                //   The parent's margin belongs to GRANDPARENT's coordinate space and was already
                //   used to position the parent. Adding it again here is like adding feet to
                //   meters.
                //
                //   We ONLY add the child's margin in our (parent's content-box) coordinate space.
                //   The parent's margin is irrelevant to us - it's outside our scope.
3226
                main_pen += child_margin_top;
3226
                debug_info!(
3214
                    ctx,
3214
                    "[layout_bfc] First child {} BLOCKED: parent_has_blocker={}, advanced by \
3214
                     child_margin={}, main_pen={}",
                    child_index,
                    parent_has_top_blocker,
                    child_margin_top,
                    main_pen
                );
            }
        } else {
            // Not first child: handle sibling collapse
            // CSS 2.2 § 8.3.1 Rule 1: "Vertical margins of adjacent block boxes in the normal flow
            // collapse" CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing"
            // Resolve accumulated top margin if not yet done (for parent's first in-flow child)
15586
            if !top_margin_resolved {
332
                main_pen += accumulated_top_margin;
332
                top_margin_resolved = true;
332
                debug_info!(
332
                    ctx,
332
                    "[layout_bfc] RESOLVED top margin for node {} at sibling {}: accumulated={}, \
332
                     main_pen={}",
                    node_index,
                    child_index,
                    accumulated_top_margin,
                    main_pen
                );
15254
            }
15586
            if clearance_applied {
                // Clearance has already positioned main_pen at the correct
                // border-edge = max(float_bottom, hypothetical). The hypothetical
                // already includes collapse_margins(last_margin_bottom, child_margin_top),
                // so we must NOT add child_margin_top again here.
1
                debug_info!(
1
                    ctx,
1
                    "[layout_bfc] Child {} with CLEARANCE: no collapse with sibling, \
1
                     child_margin_top={}, main_pen={}",
                    child_index,
                    child_margin_top,
                    main_pen
                );
            } else {
                // Sibling Margin Collapse
                //
                // CSS 2.2 § 8.3.1: "Vertical margins of adjacent block boxes in the normal
                // flow collapse." The collapsed margin is the maximum of the two margins.
                //
                // IMPORTANT: Sibling margins ARE part of the parent's content-box height!
                //
                // Unlike escaped margins (which belong to grandparent's space), sibling margins
                // are the space BETWEEN children within our content-box.
                //
                // Example:
                //
                // <div>
                //  <div margin-bottom=30></div>
                //  <div margin-top=40></div>
                // </div>
                //
                //   - First child ends at Y=100 (including its content + margins)
                //   - Collapsed margin = max(30, 40) = 40px
                //   - Second child starts at Y=140 (100 + 40)
                //   - Parent's content-box height includes this 40px gap
                //
                // We track total_sibling_margins for debugging, but NOTE: we do **not**
                // subtract these from content-box height! They are part of the layout space.
                //
                // Previously we subtracted total_sibling_margins from content-box height:
                //
                //   content_box_height = main_pen - total_escaped_top_margin -
                // total_sibling_margins;
                //
                // This was wrong because sibling margins are between boxes (part of content),
                // not outside boxes (like escaped margins).
15585
                let collapsed = collapse_margins(last_margin_bottom, child_margin_top);
15585
                main_pen += collapsed;
15585
                total_sibling_margins += collapsed;
15585
                debug_info!(
5869
                    ctx,
5869
                    "[layout_bfc] Sibling collapse for child {}: last_margin_bottom={}, \
5869
                     child_margin_top={}, collapsed={}, main_pen={}, total_sibling_margins={}",
                    child_index,
                    last_margin_bottom,
                    child_margin_top,
                    collapsed,
                    main_pen,
                    total_sibling_margins
                );
            }
        }
        // K30b fit check: `main_pen` is final for this child (margins
        // resolved above). A child that does not fit the remaining
        // fragmentainer extent breaks BEFORE itself — it and every later
        // in-flow sibling become the outgoing token's unfinished tail.
        // A first-content child that can never fit places as a MONOLITH
        // (overflows the fragmentainer; never torn, never looped —
        // reporting arrives with the page-loop driver).
24310
        if let Some(fs) = constraints.fragmentainer.as_ref() {
            use crate::solver3::break_token::{fragment_fit, tail_token, FitDecision};
            // A BLOCK CONTAINER with children is never a true monolith —
            // the monolith rule (place-overflowing) is for ATOMS. A
            // container that does not fit DESCENDS whenever there is usable
            // space, regardless of the placed_any/monolith classification
            // (a first-child wrapper taller than every page must split, not
            // overflow).
92
            let child_fits = main_pen + child_size.main(writing_mode)
92
                <= fs.remaining_block_extent + 0.01;
92
            let container_descend = !child_fits
26
                && tree.get(LayoutNodeId::new(child_index)).is_some_and(|n| {
26
                    matches!(n.formatting_context, FormattingContext::Block { .. })
12
                        && !tree.children(child_index).is_empty()
26
                })
12
                && (fs.remaining_block_extent - main_pen) >= 40.0;
92
            match if container_descend {
12
                FitDecision::BreakBeforeHere
            } else {
80
                fragment_fit(
80
                    main_pen,
80
                    child_size.main(writing_mode),
80
                    fs.remaining_block_extent,
80
                    fs.next_fragmentainer_extent,
80
                    fragment_placed_content,
                )
            } {
                FitDecision::Fits => {
                    // The fragmentainer PROPAGATES into fitting container
                    // children too: a forced break (or a deep unforced one
                    // behind conservative Pass-1 sizes) can hide INSIDE a
                    // child that fits — e.g. body fits the page whole, but
                    // a <pagebreak/> lives in it. Re-lay containers under
                    // the remaining extent; a returned token wraps as
                    // ResumeIn and stops sibling consumption after this
                    // child places its fitted part. NEVER for the child the
                    // RESUME arm just re-laid — a second pass with
                    // resume: None would clobber the resumed fragment and
                    // regenerate page 1's token forever (no-progress halt).
66
                    let child_is_block_container = !fragment_child_resumed
46
                        && tree
46
                        .get(LayoutNodeId::new(child_index))
46
                        .is_some_and(|n| {
45
                            matches!(
46
                                n.formatting_context,
                                FormattingContext::Block { .. }
1
                            ) && !tree.children(child_index).is_empty()
46
                        });
66
                    if child_is_block_container {
1
                        let child_space = FragmentainerSpace {
1
                            remaining_block_extent: fs.remaining_block_extent - main_pen,
1
                            next_fragmentainer_extent: fs.next_fragmentainer_extent,
1
                            is_first: fs.is_first && !fragment_placed_content,
1
                            resume: None,
                        };
1
                        let mut child_out: Option<
1
                            crate::solver3::break_token::BreakToken,
1
                        > = None;
1
                        let mut tmp_positions: super::PositionVec = Vec::new();
1
                        let mut tmp_scrollbars = false;
1
                        crate::solver3::cache::calculate_layout_for_subtree_fragment(
1
                            ctx,
1
                            tree,
1
                            text_cache,
1
                            child_index,
1
                            LogicalPosition::zero(),
1
                            children_containing_block_size,
1
                            &mut tmp_positions,
1
                            &mut tmp_scrollbars,
1
                            float_cache,
1
                            crate::solver3::cache::ComputeMode::ComputeSize,
1
                            Some(child_space),
1
                            Some(&mut child_out),
                        )?;
1
                        if let Some(cont) = child_out {
1
                            let later: Vec<usize> = pos_children
1
                                .iter()
1
                                .copied()
1
                                .skip_while(|&c| c != child_index)
1
                                .skip(1)
1
                                .filter(|&c| {
                                    let pt = get_position_type(
                                        ctx.styled_dom,
                                        tree.get(LayoutNodeId::new(c)).and_then(|n| n.dom_node_id),
                                    );
                                    pt != LayoutPosition::Absolute
                                        && pt != LayoutPosition::Fixed
                                })
1
                                .collect();
1
                            let mut children = alloc::vec![
1
                                crate::solver3::break_token::ChildBreakEntry::ResumeIn {
1
                                    child: child_index,
1
                                    token: Box::new(cont),
1
                                }
                            ];
1
                            children.extend(later.into_iter().map(|child| {
                                crate::solver3::break_token::ChildBreakEntry::BreakBefore {
                                    child,
                                    forced: false,
                                }
                            }));
1
                            fragment_token_out =
1
                                Some(crate::solver3::break_token::BreakToken::Block(
1
                                    crate::solver3::break_token::BlockBreakToken {
1
                                        node: node_index,
1
                                        consumed_block_size: main_pen,
1
                                        children,
1
                                        generation: 0,
1
                                    },
1
                                ));
                            // No break: this child PLACES its fitted part.
                        }
65
                    }
                }
1
                FitDecision::MonolithOverflow => {}
                FitDecision::BreakBeforeHere => {
25
                    let later: Vec<usize> = pos_children
25
                        .iter()
25
                        .copied()
60
                        .skip_while(|&c| c != child_index)
25
                        .skip(1)
25
                        .filter(|&c| {
20
                            let pt = get_position_type(
20
                                ctx.styled_dom,
20
                                tree.get(LayoutNodeId::new(c)).and_then(|n| n.dom_node_id),
                            );
20
                            pt != LayoutPosition::Absolute && pt != LayoutPosition::Fixed
20
                        })
25
                        .collect();
                    // K30b part 2, BREAK-DESCEND arm: a breakable BLOCK
                    // container with usable space left gets PART of itself
                    // on this fragmentainer — re-lay it inside the
                    // remaining extent; its own token becomes a ResumeIn
                    // entry. Leaf/IFC/atomic children (and sliver spaces
                    // < MIN_DESCEND_EXTENT) keep the whole-child
                    // BreakBefore of part 1.
                    const MIN_DESCEND_EXTENT: f32 = 40.0;
25
                    let child_is_block_container = tree
25
                        .get(LayoutNodeId::new(child_index))
25
                        .is_some_and(|n| {
13
                            matches!(
25
                                n.formatting_context,
                                FormattingContext::Block { .. }
12
                            ) && !tree.children(child_index).is_empty()
25
                        });
25
                    let usable = fs.remaining_block_extent - main_pen;
25
                    if child_is_block_container && usable >= MIN_DESCEND_EXTENT {
12
                        let child_space = FragmentainerSpace {
12
                            remaining_block_extent: usable,
12
                            next_fragmentainer_extent: fs.next_fragmentainer_extent,
12
                            is_first: fs.is_first && !fragment_placed_content,
12
                            resume: None,
                        };
12
                        let mut child_out: Option<
12
                            crate::solver3::break_token::BreakToken,
12
                        > = None;
12
                        let mut tmp_positions: super::PositionVec = Vec::new();
12
                        let mut tmp_scrollbars = false;
12
                        crate::solver3::cache::calculate_layout_for_subtree_fragment(
12
                            ctx,
12
                            tree,
12
                            text_cache,
12
                            child_index,
12
                            LogicalPosition::zero(),
12
                            children_containing_block_size,
12
                            &mut tmp_positions,
12
                            &mut tmp_scrollbars,
12
                            float_cache,
12
                            crate::solver3::cache::ComputeMode::ComputeSize,
12
                            Some(child_space),
12
                            Some(&mut child_out),
                        )?;
12
                        if let Some(cont) = child_out {
                            // The child SPLIT: place its fitted part (fall
                            // through with the shortened used_size) and
                            // resume the rest on the next fragmentainer.
11
                            let mut children = alloc::vec![
11
                                crate::solver3::break_token::ChildBreakEntry::ResumeIn {
11
                                    child: child_index,
11
                                    token: Box::new(cont),
11
                                }
                            ];
11
                            children.extend(later.into_iter().map(|child| {
                                crate::solver3::break_token::ChildBreakEntry::BreakBefore {
                                    child,
                                    forced: false,
                                }
                            }));
11
                            fragment_token_out =
11
                                Some(crate::solver3::break_token::BreakToken::Block(
11
                                    crate::solver3::break_token::BlockBreakToken {
11
                                        node: node_index,
11
                                        consumed_block_size: main_pen,
11
                                        children,
11
                                        generation: 0,
11
                                    },
11
                                ));
                            // NO `break`: the loop-top guard stops the NEXT
                            // sibling; this child still places below.
1
                        } else {
1
                            // The child fit entirely once re-laid (its
1
                            // Pass-1 size was stale/conservative): place it,
1
                            // no token from this child.
1
                        }
                    } else {
                        // Roll the pen back: the margin that advanced it
                        // for THIS child adjoins the break and truncates.
13
                        main_pen = fragment_pen_at_child;
13
                        fragment_token_out = Some(tail_token(
13
                            node_index,
13
                            main_pen,
13
                            child_index,
13
                            later.into_iter(),
13
                        ));
13
                        clear_fragment_pos!(LayoutNodeId::new(child_index));
13
                        continue;
                    }
                }
            }
79
            fragment_placed_content = true;
24218
        }
        // K30b: a descend/resume re-lay above may have SHORTENED this
        // child's used_size — refresh the local before positioning.
24297
        let child_size = tree
24297
            .get(LayoutNodeId::new(child_index))
24297
            .and_then(|n| n.used_size)
24297
            .unwrap_or(child_size);
        // Position child (non-empty blocks only reach here)
        //
        // +spec:block-formatting-context:1dada5 - Normal flow boxes in BFC touch containing block edge
        // +spec:block-formatting-context:9f56cb - each box's left outer edge touches containing block left edge; new BFC may shrink due to floats
        // CSS 2.2 § 9.4.1: "In a block formatting context, each box's left outer edge touches
        // the left edge of the containing block (for right-to-left formatting, right edges touch).
        // This is true even in the presence of floats (although a box's line boxes may shrink
        // due to the floats), unless the box establishes a new block formatting context
        // (in which case the box itself may become narrower due to the floats)."
        //
        // +spec:block-formatting-context:3d2811 - Float overlap with normal flow element borders
        // +spec:display-property:796059 - BFC/replaced/table border box must not overlap float margin boxes; line boxes shorten around floats
        // +spec:floats:5214a6 - BFC/replaced/table border box must not overlap float margin boxes; shrink or clear below
        // CSS 2.2 § 9.5: "The border box of a table, a block-level replaced element, or an element
        // in the normal flow that establishes a new block formatting context (such as an element
        // with 'overflow' other than 'visible') must not overlap any floats in the same block
        // formatting context as the element itself."
        // +spec:floats:a29f70 - BFC roots, tables, and block-level replaced elements must not overlap float margin boxes
24297
        let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
24297
        let avoids_floats = establishes_new_bfc(ctx, child_node, tree.cold(LayoutNodeId::new(child_index)))
22465
            || is_block_level_replaced(ctx, child_node);
        // Query available space considering floats ONLY if child avoids floats
24297
        let (cross_start, cross_end, available_cross) = if avoids_floats {
            // New BFC / replaced / table: Must shrink or move down to avoid overlapping floats
1832
            let child_cross_needed = child_size.cross(writing_mode);
1832
            let bfc_cross = constraints.available_size.cross(writing_mode);
1832
            let (mut start, mut end) = float_context.available_line_box_space(
1832
                main_pen,
1832
                main_pen + child_size.main(writing_mode),
1832
                bfc_cross,
1832
                writing_mode,
1832
            );
1832
            let mut available = end - start;
            // CSS 2.2 § 9.5: "If necessary, implementations should clear the said element
            // by placing it below any preceding floats, but may place it adjacent to such
            // floats if there is sufficient space."
1832
            if available < child_cross_needed && !float_context.floats.is_empty() {
1
                let clear_to = float_context.floats.iter()
1
                    .filter(|f| {
1
                        let f_main_start = f.rect.origin.main(writing_mode) - f.margin.main_start(writing_mode);
1
                        let f_main_end = f_main_start + f.rect.size.main(writing_mode)
1
                            + f.margin.main_start(writing_mode) + f.margin.main_end(writing_mode);
1
                        f_main_end > main_pen && f_main_start < main_pen + child_size.main(writing_mode)
1
                    })
1
                    .map(|f| {
1
                        f.rect.origin.main(writing_mode) + f.rect.size.main(writing_mode)
1
                            + f.margin.main_end(writing_mode)
1
                    })
1
                    .fold(main_pen, f32::max);
1
                if clear_to > main_pen {
1
                    main_pen = clear_to;
1
                    let (s, e) = float_context.available_line_box_space(
1
                        main_pen,
1
                        main_pen + child_size.main(writing_mode),
1
                        bfc_cross,
1
                        writing_mode,
1
                    );
1
                    start = s;
1
                    end = e;
1
                    available = end - start;
1
                }
1831
            }
1832
            debug_info!(
1813
                ctx,
1813
                "[layout_bfc] Child {} avoids floats: shrinking to avoid floats, \
1813
                 cross_range={}..{}, available_cross={}",
                child_index,
                start,
                end,
                available
            );
1832
            (start, end, available)
        } else {
            // Normal flow: Overlaps floats, positioned at full width
            // Only the child's INLINE CONTENT (if any) wraps around floats
22465
            let start = 0.0;
22465
            let end = constraints.available_size.cross(writing_mode);
22465
            let available = end - start;
22465
            debug_info!(
11668
                ctx,
11668
                "[layout_bfc] Child {} is normal flow: overlapping floats at full width, \
11668
                 available_cross={}",
                child_index,
                available
            );
22465
            (start, end, available)
        };
        // Get child's margin, margin_auto, size, and formatting context
24297
        let (child_margin_cloned, child_margin_auto, child_used_size, is_inline_fc, child_dom_id_for_debug) = {
24297
            let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
24297
            let cbp = child_node.box_props.unpack();
24297
            (
24297
                cbp.margin,
24297
                cbp.margin_auto,
24297
                child_node.used_size.unwrap_or_default(),
24297
                child_node.formatting_context == FormattingContext::Inline,
24297
                child_node.dom_node_id,
24297
            )
        };
24297
        let child_margin = &child_margin_cloned;
24297
        debug_info!(
13481
            ctx,
13481
            "[layout_bfc] Child {} margin_auto: left={}, right={}, top={}, bottom={}",
            child_index,
            child_margin_auto.left,
            child_margin_auto.right,
            child_margin_auto.top,
            child_margin_auto.bottom
        );
24297
        debug_info!(
13481
            ctx,
13481
            "[layout_bfc] Child {} used_size: width={}, height={}",
            child_index,
            child_used_size.width,
            child_used_size.height
        );
        // Position child
        // For normal flow blocks (including IFCs): position at full width (cross_start = 0)
        // For BFC-establishing blocks: position in available space between floats
        //
        // CSS 2.2 § 10.3.3: If margin-left and margin-right are both auto,
        // their used values are equal, centering the element horizontally.
24297
        let (child_cross_pos, mut child_main_pos) = if avoids_floats {
            // BFC: Position in float-free space, but also check margin:auto centering.
            // A flex container or overflow:hidden box establishes a BFC (must avoid floats)
            // but can still be centered via margin:auto — these are independent concepts.
1832
            let cross_pos = if child_margin_auto.left && child_margin_auto.right {
                let remaining = (available_cross - child_used_size.cross(writing_mode)).max(0.0);
                debug_info!(
                    ctx,
                    "[layout_bfc] Child {} BFC + margin:auto centering: available={}, size={}, offset={}",
                    child_index, available_cross, child_used_size.cross(writing_mode), remaining / 2.0
                );
                cross_start + remaining / 2.0
1832
            } else if child_margin_auto.left {
                let remaining = (available_cross - child_used_size.cross(writing_mode) - child_margin.right).max(0.0);
                cross_start + remaining
            } else {
1832
                cross_start + child_margin.cross_start(writing_mode)
            };
1832
            (cross_pos, main_pen)
        } else {
            // Normal flow: Check for margin: auto centering
22465
            let available_cross = constraints.available_size.cross(writing_mode);
22465
            let child_cross_size = child_used_size.cross(writing_mode);
22465
            debug_info!(
11668
                ctx,
11668
                "[layout_bfc] Child {} centering check: available_cross={}, child_cross_size={}, margin_auto.left={}, margin_auto.right={}",
                child_index,
                available_cross,
                child_cross_size,
                child_margin_auto.left,
                child_margin_auto.right
            );
            // +spec:block-formatting-context:d52ce5 - auto margins resolved per containing block's writing mode for centering
            // +spec:width-calculation:0c5044 - auto margins center element on cross axis (respects writing mode)
            // +spec:width-calculation:25c2fc - §10.3.3: block-level margin auto centering and over-constrained resolution
            // +spec:width-calculation:ba691f - auto margins treated as zero when element overflows containing block (via .max(0.0) on remaining_space)
            // +spec:width-calculation:324e7e - both margin-left and margin-right auto => equal used values (centering)
            // CSS 2.2 § 10.3.3: If both margin-left and margin-right are auto,
            // center the element within the available space
22465
            let cross_pos = if child_margin_auto.left && child_margin_auto.right {
                // Center: (available - child_width) / 2
1
                let remaining_space = (available_cross - child_cross_size).max(0.0);
1
                debug_info!(
1
                    ctx,
1
                    "[layout_bfc] Child {} CENTERING: remaining_space={}, cross_pos={}",
                    child_index,
                    remaining_space,
1
                    remaining_space / 2.0
                );
1
                remaining_space / 2.0
22464
            } else if child_margin_auto.left {
                // Only left is auto: push element to the right
                let remaining_space = (available_cross - child_cross_size - child_margin.right).max(0.0);
                debug_info!(
                    ctx,
                    "[layout_bfc] Child {} margin-left:auto only, pushing right: remaining_space={}",
                    child_index,
                    remaining_space
                );
                remaining_space
22464
            } else if child_margin_auto.right {
                // Only right is auto: element stays at left with its margin
                debug_info!(
                    ctx,
                    "[layout_bfc] Child {} margin-right:auto only, using left margin={}",
                    child_index,
                    child_margin.cross_start(writing_mode)
                );
                child_margin.cross_start(writing_mode)
            } else {
                // +spec:box-model:218643 - over-constrained: drop end margin per containing block writing mode
                // +spec:width-calculation:d172a4 - over-constrained: LTR ignores margin-right, RTL ignores margin-left
                // in LTR, margin-right is ignored (element positioned at margin-left);
                // in RTL, margin-left is ignored (element positioned from right edge)
22464
                let is_rtl = tree.get(LayoutNodeId::new(node_index))
22464
                    .and_then(|n| n.dom_node_id)
22464
                    .is_some_and(|cb_dom_id| {
22462
                        let node_state = ctx.styled_dom.styled_nodes.as_container()
22462
                            .get(cb_dom_id)
22462
                            .map(|s| s.styled_node_state)
22462
                            .unwrap_or_default();
22462
                        matches!(
22462
                            get_direction_property(ctx.styled_dom, cb_dom_id, &node_state),
                            MultiValue::Exact(StyleDirection::Rtl)
                        )
22462
                    });
22464
                let cross_pos = if is_rtl {
                    // RTL: ignore margin-left, position from right edge
                    available_cross - child_cross_size - child_margin.cross_end(writing_mode)
                } else {
                    // LTR (default): ignore margin-right, position at margin-left
22464
                    child_margin.cross_start(writing_mode)
                };
22464
                debug_info!(
11667
                    ctx,
11667
                    "[layout_bfc] Child {} NO auto margins (over-constrained), is_rtl={}, cross_pos={}",
                    child_index,
                    is_rtl,
                    cross_pos
                );
22464
                cross_pos
            };
22465
            (cross_pos, main_pen)
        };
        // NOTE: We do NOT adjust child_main_pos based on child's escaped_top_margin here!
        // The escaped_top_margin represents margins that escaped FROM the child's own children.
        // The child's position in THIS BFC is determined by main_pen and the child's own margin
        // (which was already handled in the margin collapse logic above).
        //
        // Previously, this code incorrectly added child_escaped_margin to child_main_pos,
        // which caused double-application of margins because:
        // 1. The child's margin was used to calculate its position in THIS BFC
        // 2. Then its escaped_top_margin (which included its own margin) was added again
        //
        // The correct behavior per CSS 2.2 § 8.3.1 is:
        // - The child's escaped_top_margin is used by THIS node's parent to position THIS node
        // - It does NOT affect how we position the child within our content-box
        // final_pos is [CoordinateSpace::Parent] - relative to this BFC's content-box
24297
        let final_pos =
24297
            LogicalPosition::from_main_cross(child_main_pos, child_cross_pos, writing_mode);
24297
        debug_info!(
13481
            ctx,
13481
            "[layout_bfc] *** NORMAL FLOW BLOCK POSITIONED: child={}, final_pos={:?}, \
13481
             main_pen={}, avoids_floats={}",
            child_index,
            final_pos,
            main_pen,
            avoids_floats
        );
        // Re-layout IFC children with float context for correct text wrapping
        // Normal flow blocks WITH inline content need float context propagated
24297
        if is_inline_fc && !avoids_floats {
            // Use cached floats if available (from previous layout passes),
            // otherwise use the floats positioned in this pass
18192
            let floats_for_ifc = float_cache.get(&node_index).unwrap_or(&float_context);
18192
            debug_info!(
8367
                ctx,
8367
                "[layout_bfc] Re-layouting IFC child {} (normal flow) with parent's float context \
8367
                 at Y={}, child_cross_pos={}",
                child_index,
                main_pen,
                child_cross_pos
            );
18192
            debug_info!(
8367
                ctx,
8367
                "[layout_bfc]   Using {} floats (from cache: {})",
8367
                floats_for_ifc.floats.len(),
8367
                float_cache.contains_key(&node_index)
            );
            // Translate float coordinates from BFC-relative to IFC-relative
            // The IFC child is positioned at (child_cross_pos, main_pen) in BFC coordinates
            // Floats need to be relative to the IFC's CONTENT-BOX origin (inside padding/border)
18192
            let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
18192
            let cbp = child_node.box_props.unpack();
18192
            let padding_border_cross = cbp.padding.cross_start(writing_mode)
18192
                + cbp.border.cross_start(writing_mode);
18192
            let padding_border_main = cbp.padding.main_start(writing_mode)
18192
                + cbp.border.main_start(writing_mode);
            // Content-box origin in BFC coordinates
18192
            let content_box_cross = child_cross_pos + padding_border_cross;
18192
            let content_box_main = main_pen + padding_border_main;
18192
            debug_info!(
8367
                ctx,
8367
                "[layout_bfc]   Border-box at ({}, {}), Content-box at ({}, {}), \
8367
                 padding+border=({}, {})",
                child_cross_pos,
                main_pen,
                content_box_cross,
                content_box_main,
                padding_border_cross,
                padding_border_main
            );
18192
            let mut ifc_floats = FloatingContext::default();
18230
            for float_box in &floats_for_ifc.floats {
                // Convert float position from BFC coords to IFC CONTENT-BOX relative coords
38
                let float_rel_to_ifc = LogicalRect {
38
                    origin: LogicalPosition {
38
                        x: float_box.rect.origin.x - content_box_cross,
38
                        y: float_box.rect.origin.y - content_box_main,
38
                    },
38
                    size: float_box.rect.size,
38
                };
38
                debug_info!(
6
                    ctx,
6
                    "[layout_bfc] Float {:?}: BFC coords = {:?}, IFC-content-relative = {:?}",
                    float_box.kind,
                    float_box.rect,
                    float_rel_to_ifc
                );
38
                ifc_floats.add_float(float_box.kind, float_rel_to_ifc, float_box.margin);
            }
            // Create a BfcState with IFC-relative float coordinates
18192
            let mut bfc_state = BfcState {
18192
                pen: LogicalPosition::zero(), // IFC starts at its own origin
18192
                floats: ifc_floats.clone(),
18192
                margins: MarginCollapseContext::default(),
18192
            };
18192
            debug_info!(
8367
                ctx,
8367
                "[layout_bfc]   Created IFC-relative FloatingContext with {} floats",
8367
                ifc_floats.floats.len()
            );
            // Get the IFC child's content-box size (after padding/border)
18192
            let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
18192
            let child_dom_id = child_node.dom_node_id;
            // +spec:containing-block:a8ada9 - line box width determined by containing block and floats
            // For inline elements (display: inline), use containing block width as available
            // width. Inline elements flow within the containing block and wrap at its width.
            // CSS 2.2 § 10.3.1: For inline elements, available width = containing block width.
18192
            let display = get_display_property(ctx.styled_dom, child_dom_id).unwrap_or_default();
18192
            let child_content_size = if display == LayoutDisplay::Inline {
                // Inline elements use the containing block's content-box width
2153
                LogicalSize::new(
2153
                    children_containing_block_size.width,
2153
                    children_containing_block_size.height,
                )
            } else {
                // Block-level elements use their own content-box
16039
                child_node.box_props.inner_size(child_size, writing_mode)
            };
18192
            debug_info!(
8367
                ctx,
8367
                "[layout_bfc]   IFC child size: border-box={:?}, content-box={:?}",
                child_size,
                child_content_size
            );
            // Create new constraints with float context
            // IMPORTANT: Use the child's CONTENT-BOX width, not the BFC width!
18192
            let ifc_constraints = LayoutConstraints {
18192
                available_size: child_content_size,
18192
                bfc_state: Some(&mut bfc_state),
18192
                writing_mode,
18192
                writing_mode_ctx: constraints.writing_mode_ctx,
18192
                text_align: constraints.text_align,
18192
                containing_block_size: constraints.containing_block_size,
18192
                available_width_type: Text3AvailableSpace::Definite(child_content_size.width),
18192
                fragmentainer: None,
18192
            };
            // Re-layout the IFC with float awareness
            // This will pass floats as exclusion zones to text3 for line wrapping
18192
            let ifc_result = layout_formatting_context(
18192
                ctx,
18192
                tree,
18192
                text_cache,
18192
                child_index,
18192
                &ifc_constraints,
18192
                float_cache,
            )?;
            // DON'T update used_size - the box keeps its full width!
            // Only the text layout inside changes to wrap around floats
18192
            debug_info!(
8367
                ctx,
8367
                "[layout_bfc] IFC child {} re-layouted with float context (text will wrap, box \
8367
                 stays full width)",
                child_index
            );
            // NOTE: We do NOT merge inline-block positions from the IFC's output.positions here!
            // The IFC's inline-block children will be correctly positioned when 
            // calculate_layout_for_subtree recursively processes the IFC node (child_index).
            // At that point, layout_ifc will be called again, and the inline-block positions
            // will be relative to the IFC's content-box, which is what we want.
            //
            // Merging them here would cause them to be processed by process_inflow_child
            // with the BFC's content-box position (self_content_box_pos of the BFC), 
            // resulting in incorrect absolute positions.
6105
        }
24297
        output.positions.insert(child_index, final_pos);
        // CSS margin collapse: escaped margins are handled via accumulated_top_margin
        // at the START of layout, not by adjusting positions after layout.
        // We simply advance by the child's actual size.
24297
        main_pen += child_size.main(writing_mode);
24297
        has_content = true;
        // Update last margin for next sibling
        // CSS 2.2 § 8.3.1: The bottom margin of this box will collapse with the top margin
        // of the next sibling (if no clearance or blockers intervene)
        // element (between prev sibling's bottom and this element's top margin). The cleared
        // element's bottom margin is still available for normal collapsing with the next sibling.
        // CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing and acts as spacing above
        // the margin-top of an element."
24297
        last_margin_bottom = child_margin_bottom;
24297
        debug_info!(
13481
            ctx,
13481
            "[layout_bfc] Child {} positioned at final_pos={:?}, size={:?}, advanced main_pen to \
13481
             {}, last_margin_bottom={}, clearance_applied={}",
            child_index,
            final_pos,
            child_size,
            main_pen,
            last_margin_bottom,
            clearance_applied
        );
        // Track the maximum cross-axis size to determine the BFC's overflow size.
24297
        let child_cross_extent =
24297
            child_cross_pos + child_size.cross(writing_mode) + child_margin.cross_end(writing_mode);
24297
        max_cross_size = max_cross_size.max(child_cross_extent);
    }
    // Store the float context in cache for future layout passes
    // This happens after ALL children (floats and normal) have been positioned
89736
    debug_info!(
88004
        ctx,
88004
        "[layout_bfc] Storing {} floats in cache for node {}",
88004
        float_context.floats.len(),
        node_index
    );
89736
    float_cache.insert(node_index, float_context.clone());
    // PHASE 3: Parent-Child Bottom Margin Escape
89736
    let mut escaped_top_margin = None;
89736
    let mut escaped_bottom_margin = None;
    // Handle top margin escape
89736
    if top_margin_escaped {
        // First child's margin escaped through parent
5495
        escaped_top_margin = Some(accumulated_top_margin);
5495
        debug_info!(
4399
            ctx,
4399
            "[layout_bfc] Returning escaped top margin: accumulated={}, node={}",
            accumulated_top_margin,
            node_index
        );
84241
    } else if !top_margin_resolved && accumulated_top_margin > 0.0 {
        // No content was positioned, all margins accumulated (empty blocks)
6
        escaped_top_margin = Some(accumulated_top_margin);
6
        debug_info!(
1
            ctx,
1
            "[layout_bfc] Escaping top margin (no content): accumulated={}, node={}",
            accumulated_top_margin,
            node_index
        );
    } else {
        // Don't set escaped_top_margin = Some(0) — that would override the child's
        // own margin (e.g., 30px) with 0 during sibling collapse.
84235
        debug_info!(
83604
            ctx,
83604
            "[layout_bfc] NOT escaping top margin: top_margin_resolved={}, escaped={}, \
83604
             accumulated={}, node={}",
            top_margin_resolved,
            top_margin_escaped,
            accumulated_top_margin,
            node_index
        );
    }
    // Handle bottom margin escape
89736
    if let Some(last_idx) = last_child_index {
8731
        let last_child = tree.get(LayoutNodeId::new(last_idx)).ok_or(LayoutError::InvalidTree)?;
8731
        let last_child_bp = last_child.box_props.unpack();
8731
        let last_has_bottom_blocker =
8731
            has_margin_collapse_blocker(&last_child_bp, writing_mode, false);
8731
        debug_info!(
7618
            ctx,
7618
            "[layout_bfc] Bottom margin for node {}: parent_has_bottom_blocker={}, \
7618
             last_has_bottom_blocker={}, last_margin_bottom={}, main_pen_before={}",
            node_index,
            parent_has_bottom_blocker,
            last_has_bottom_blocker,
            last_margin_bottom,
            main_pen
        );
8731
        if !parent_has_bottom_blocker && has_content {
            // CSS 2.2 section 8.3.1: the bottom margin of the LAST in-flow child
            // adjoins the parent's bottom margin whenever the parent has auto
            // height and no bottom padding/border. The child's OWN bottom
            // padding/border is irrelevant to THIS adjacency — it only decides
            // whether the child's descendants' margins were already merged into
            // `last_margin_bottom` (handled where child_escaped_bottom is read).
            // An earlier version required the last child to be blocker-free too
            // and exported only the parent's own margin otherwise: a padded
            // child with margin-bottom 50 under a parent with margin-bottom 40
            // produced a 40px sibling gap instead of Chrome's 50px, shifting
            // everything below (block-margin-collapse-complex-001, -10px per
            // section). The margin is NOT added to main_pen either way — it
            // escapes the content box (counting it double-counted the height,
            // nested-container came out 180px instead of 130px).
5510
            let collapsed_bottom = collapse_margins(parent_margin_bottom, last_margin_bottom);
5510
            escaped_bottom_margin = Some(collapsed_bottom);
5510
            debug_info!(
4414
                ctx,
4414
                "[layout_bfc] Bottom margin ESCAPED for node {}: collapsed={}",
                node_index,
                collapsed_bottom
            );
        } else {
            // Can't escape: add to pen
3221
            main_pen += last_margin_bottom;
            // NOTE: We do NOT add parent_margin_bottom to main_pen here!
            // parent_margin_bottom is added OUTSIDE the content-box (in the margin-box)
            // The content-box height should only include children's content and margins
3221
            debug_info!(
3204
                ctx,
3204
                "[layout_bfc] Bottom margin BLOCKED for node {}: added last_margin_bottom={}, \
3204
                 main_pen_after={}",
                node_index,
                last_margin_bottom,
                main_pen
            );
        }
    } else {
        // No children: just use parent's margins
81005
        if !top_margin_resolved {
81005
            main_pen += parent_margin_top;
81005
        }
81005
        main_pen += parent_margin_bottom;
    }
    // CRITICAL: If this is a root node (no parent), apply escaped margins directly
    // instead of propagating them upward (since there's no parent to receive them)
89736
    let is_root_node = node.parent.is_none();
89736
    if is_root_node {
4031
        if let Some(top) = escaped_top_margin {
            // Adjust all child positions downward by the escaped top margin
6653
            for pos in output.positions.values_mut() {
6653
                let current_main = pos.main(writing_mode);
6653
                *pos = LogicalPosition::from_main_cross(
6653
                    current_main + top,
6653
                    pos.cross(writing_mode),
6653
                    writing_mode,
6653
                );
6653
            }
3541
            main_pen += top;
490
        }
4031
        if let Some(bottom) = escaped_bottom_margin {
3545
            main_pen += bottom;
3545
        }
        // For root nodes, don't propagate margins further
4031
        escaped_top_margin = None;
4031
        escaped_bottom_margin = None;
85705
    }
    // CSS 2.2 § 9.5: Floats don't contribute to container height with overflow:visible
    //
    // However, browsers DO expand containers to contain floats in specific cases:
    //
    // 1. If there's NO in-flow content (main_pen == 0), floats determine height
    // 2. If container establishes a BFC (overflow != visible)
    //
    // In this case, we have in-flow content (main_pen > 0) and overflow:visible,
    // so floats should NOT expand the container. Their margins can "bleed" beyond
    // the container boundaries into the parent.
    //
    // This matches Chrome/Firefox behavior where float margins escape through
    // the container's padding when there's existing in-flow content.
    // +spec:block-formatting-context:7954a2 - 10.6.3: auto height for block-level non-replaced elements in normal flow
    // Content-box Height Calculation
    //
    // CSS 2.2 § 8.3.1: "The top border edge of the box is defined to coincide with
    // the top border edge of the [first] child" when margins collapse/escape.
    //
    // This means escaped margins do NOT contribute to the parent's content-box height.
    //
    // Calculation:
    //
    //   main_pen = total vertical space used by all children and margins
    //
    //   Components of main_pen:
    //
    //   1. Children's border-boxes (always included)
    //   2. Sibling collapsed margins (space BETWEEN children - part of content)
    //   3. First child's position (0 if margin escaped, margin_top if blocked)
    //
    //   What to subtract:
    //
    //   - total_escaped_top_margin: First child's margin that went to grandparent's space This
    //     margin is OUTSIDE our content-box, so we must subtract it.
    //
    //   What NOT to subtract:
    //
    //   - total_sibling_margins: These are the gaps BETWEEN children, which are
    //    legitimately part of our content area's layout space.
    //
    // Example with escaped margin:
    //   <div class="parent" padding=0>              <!-- Node 2 -->
    //     <div class="child1" margin=30></div>      <!-- Node 3, margin escapes -->
    //     <div class="child2" margin=40></div>      <!-- Node 5 -->
    //   </div>
    //
    //   Layout process:
    //
    //   - Node 3 positioned at main_pen=0 (margin escaped)
    //   - Node 3 size=140px → main_pen advances to 140
    //   - Sibling collapse: max(30 child1 bottom, 40 child2 top) = 40px
    //   - main_pen advances to 180
    //   - Node 5 size=130px → main_pen advances to 310
    //   - total_escaped_top_margin = 30
    //   - total_sibling_margins = 40 (tracked but NOT subtracted)
    //   - content_box_height = 310 - 30 = 280px ✓
    //
    // Previously, we calculated:
    //
    //   content_box_height = main_pen - total_escaped_top_margin - total_sibling_margins
    //
    // This incorrectly subtracted sibling margins, making parent too small.
    // Sibling margins are *between* boxes (part of layout), not *outside* boxes
    // (like escaped margins).
    // +spec:box-model:4eebed - auto height for BFC = top margin-edge of topmost child to bottom margin-edge of bottommost child
    // +spec:box-model:4eebed - auto height = top margin-edge of topmost child to bottom margin-edge of bottommost child
    // +spec:height-calculation:d65226 - §10.6.7 auto heights for BFC roots: block children use
    // margin-edge of topmost/bottommost, floats extend height if below content edge
    // +spec:positioning:1a05bb - 10.6.7 auto height for BFC roots: block children use margin edges,
    // abspos ignored (skipped in Pass 1/2), relative considered without offset (applied after layout),
    // floats whose bottom margin edge exceeds content edge expand height (below)
    // +spec:positioning:e6712c - Auto height for BFC: distance between top/bottom margin-edges of
    // block children (minus escaped margins), ignoring absolutely positioned children (skipped at
    // line ~966), considering relatively positioned boxes without offset (applied after layout),
    // and extending to include floats whose bottom margin edge exceeds content edge
    // +spec:positioning:f94d22 - 10.6.3: block-level non-replaced auto height = distance from top content edge to last in-flow child bottom margin edge (or zero)
    // CSS 2.2 §8.3.1: escaped margins (both top and bottom) don't contribute to parent height
89736
    let mut content_box_height = if is_root_node {
        // Root: the escaped margins were re-added to `main_pen` just above (there is no
        // grandparent to receive them); subtract them back out so the root's content box
        // still excludes them. Net effect is the pre-escape span.
4031
        main_pen - total_escaped_top_margin - escaped_bottom_margin.unwrap_or(0.0)
    } else {
        // Non-root: the first in-flow child was positioned at main_pen == 0 (its top
        // margin escaped, NOT added to the pen) and an escaped bottom margin was never
        // advanced into the pen either. So `main_pen` already spans the first child's
        // border-top to the last child's border-bottom — exactly the content-box height
        // (CSS 2.2 §8.3.1). The escaped margins live in the PARENT's coordinate space and
        // reach it via `escaped_top_margin` / `escaped_bottom_margin`. Subtracting them
        // from THIS box's height double-removes them and collapses it (#20: a <div> around
        // a single <p> came out 0px tall, pulling the following sibling up by a line).
85705
        main_pen
    };
    // +spec:block-formatting-context:f73d3e - BFC root grows to fully contain its floats; floats from outside cannot protrude in
    // whose bottom margin edge exceeds bottom content edge; only floats participating
    // in this BFC are counted (not floats inside abspos descendants or nested BFCs)
    // +spec:box-model:1d4798 - auto height includes floats whose bottom margin edge exceeds content edge
    // only floats participating in this BFC are counted (not floats inside abspos descendants or nested BFCs)
89736
    if is_bfc_root {
85405
        for float_box in &float_context.floats {
            let float_bottom_margin_edge = float_box.rect.origin.main(writing_mode)
                + float_box.rect.size.main(writing_mode)
                + float_box.margin.main_end(writing_mode);
            if float_bottom_margin_edge > content_box_height {
                content_box_height = float_bottom_margin_edge;
            }
        }
4331
    }
    // +spec:display-contents:f6de1a - content height overflow tracked via overflow_size
    // +spec:overflow:043182 - overflow computed from box bounds + children overflow
89736
    output.overflow_size =
89736
        LogicalSize::from_main_cross(content_box_height, max_cross_size, writing_mode);
89736
    debug_info!(
88004
        ctx,
88004
        "[layout_bfc] FINAL for node {}: main_pen={}, total_escaped_top={}, \
88004
         total_sibling_margins={}, content_box_height={}",
        node_index,
        main_pen,
        total_escaped_top_margin,
        total_sibling_margins,
        content_box_height
    );
    // +spec:inline-formatting-context:2227a4 - atomic inline baseline for inline-block/inline-table
    // Baseline calculation would happen here in a full implementation.
    // CSS2 §10.8.1: For inline-block, baseline is the baseline of the last
    // line box in normal flow, or the bottom margin edge if no line boxes.
89736
    output.baseline = None;
    // Store escaped margins in the LayoutNode for use by parent
89736
    if let Some(warm_mut) = tree.warm_mut(LayoutNodeId::new(node_index)) {
89736
        warm_mut.escaped_top_margin = escaped_top_margin;
89736
        warm_mut.escaped_bottom_margin = escaped_bottom_margin;
89736
    }
89736
    if let Some(warm_mut) = tree.warm_mut(LayoutNodeId::new(node_index)) {
89736
        warm_mut.baseline = output.baseline;
89736
    }
89736
    Ok(BfcLayoutResult {
89736
        output,
89736
        escaped_top_margin,
89736
        escaped_bottom_margin,
89736
        outgoing_token: fragment_token_out,
89736
    })
89736
}
// Inline Formatting Context (CSS 2.2 § 9.4.2)
// +spec:display-property:ede6f4 - inline layout: mixed stream of text and inline-level boxes
/// Lays out an Inline Formatting Context (IFC) by delegating to the `text3` engine.
///
/// This function acts as a bridge between the box-tree world of `solver3` and the
/// rich text layout world of `text3`. Its responsibilities are:
///
/// 1. **Collect Content**: Traverse the direct children of the IFC root and convert them into a
///    `Vec<InlineContent>`, the input format for `text3`. This involves:
///
///     - Recursively laying out `inline-block` children to determine their final size and baseline,
///       which are then passed to `text3` as opaque objects.
///     - Extracting raw text runs from inline text nodes.
///
/// 2. **Translate Constraints**: Convert the `LayoutConstraints` (available space, floats) from
///    `solver3` into the more detailed `UnifiedConstraints` that `text3` requires.
///
/// 3. **Invoke Text Layout**: Call the `text3` cache's `layout_flow` method to perform the complex
///    tasks of BIDI analysis, shaping, line breaking, justification, and vertical alignment.
///    +spec:display-property:e96c82 - inline formatting context: flow of elements/text wrapped into lines
///
/// 4. **Integrate Results**: Process the `UnifiedLayout` returned by `text3`:
///
///     - Store the rich layout result on the IFC root `LayoutNode` for the display list generation
///       pass.
///     - Update the `positions` map for all `inline-block` children based on the positions
///       calculated by `text3`.
///     - Extract the final overflow size and baseline for the IFC root itself
// NOTE(writing-modes): The IFC currently assumes inline direction = horizontal
// and block direction = vertical. In vertical writing modes, line boxes would
// stack horizontally and inline content would flow vertically. The writing mode
// is now available via constraints.writing_mode_ctx for agents to use when
// implementing vertical text layout in the text3 engine.
// +spec:display-property:574e7b - text-box-trim for inline boxes trims block-end to content edge (TODO: implement trimming per text-box-edge metric)
// +spec:display-property:da284a - IFC: flow inline-level boxes into line boxes, size/position each fragment
// +spec:inline-formatting-context:275f64 - IFC: boxes laid out horizontally into line boxes, respecting margins/borders/padding
#[allow(clippy::field_reassign_with_default)] // struct built incrementally / test setup; a struct literal is not clearer here
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
/// CSS Inline 3 §6.2 text-box-trim for the IFC's block container, applied to
/// the finished [`LayoutOutput`]. MUST run on EVERY `layout_ifc` exit that
/// produces content bounds - the incremental cache-reuse arms included -
/// or measure passes that hit the cache size the box untrimmed.
238462
fn apply_text_box_trim(
238462
    styled_dom: &StyledDom,
238462
    ifc_root_dom_id: NodeId,
238462
    cached_constraints: &UnifiedConstraints,
238462
    has_items: bool,
238462
    output: &mut LayoutOutput,
238462
) {
        // +spec:box-model:929f42 - text-box-trim: trim half-leading from first/last formatted line
        // +spec:box-model:02e0f9 - text-box-trim: trim-end and trim-both, no effect with non-zero padding/border
        //
        // CSS Inline 3 § 6.2: For block containers, trim the block-start/block-end side
        // of the first/last formatted line. If there is intervening non-zero padding or
        // borders, there is no effect. Does not apply to flex, grid, or table contexts.
238462
        let ifc_node_state = &styled_dom.styled_nodes.as_container()[ifc_root_dom_id].styled_node_state;
        // Fast path: if no node in the DOM declared text-box-trim, the cascade
        // walk would always return None → skip it.
238462
        let text_box_trim = {
238462
            let skip = styled_dom
238462
                .css_property_cache
238462
                .ptr
238462
                .compact_cache
238462
                .as_ref()
238462
                .is_some_and(|cc| cc.dom_declared_flags & azul_css::compact_cache::DOM_HAS_TEXT_BOX_TRIM == 0);
238462
            if skip {
238390
                StyleTextBoxTrim::None
            } else {
72
                get_text_box_trim_property(styled_dom, ifc_root_dom_id, ifc_node_state)
72
                    .unwrap_or(StyleTextBoxTrim::None)
            }
        };
238462
        if text_box_trim != StyleTextBoxTrim::None && has_items {
            // Half-leading = (line-height - (ascent + descent)) / 2
72
            let half_leading = (cached_constraints.resolved_line_height()
72
                - (cached_constraints.strut_ascent + cached_constraints.strut_descent))
72
                / 2.0;
72
            let half_leading = half_leading.max(0.0);
            // +spec:display-property:db5125 - text-box-edge selects the metric the trim cuts to
            // +spec:font-metrics:d3b654 - cap/alphabetic edges use the cap-height and alphabetic baseline
            // The over edge trims PAST the half-leading down to the chosen
            // metric: `cap` cuts ascent - cap-height further, `ex` cuts
            // ascent - x-height; the under edge's `alphabetic` cuts the whole
            // descent (down to the baseline). `text` (and `auto`, and the
            // ideographic metrics we have no strut data for) trim the
            // half-leading only. Trimming reduces the IFC's block size; the
            // first line's glyphs keep their positions (the same model the
            // half-leading-only implementation used - a start-trim shift of
            // the line stack is still TODO).
72
            let edge = get_text_box_edge_property(styled_dom, ifc_root_dom_id, ifc_node_state)
72
                .unwrap_or(azul_css::props::style::text::StyleTextBoxEdge::AUTO);
72
            let over_extra = match edge.over {
                azul_css::props::style::text::TextBoxEdgeOver::Cap => {
36
                    (cached_constraints.strut_ascent - cached_constraints.strut_cap_height)
36
                        .max(0.0)
                }
                azul_css::props::style::text::TextBoxEdgeOver::Ex => {
                    (cached_constraints.strut_ascent - cached_constraints.strut_x_height).max(0.0)
                }
36
                _ => 0.0,
            };
72
            let under_extra = match edge.under {
                azul_css::props::style::text::TextBoxEdgeUnder::Alphabetic => {
36
                    cached_constraints.strut_descent.max(0.0)
                }
36
                _ => 0.0,
            };
            // Check for intervening non-zero padding/border on block-start (top)
72
            let has_pad_or_border_top = match get_css_padding_top(styled_dom, ifc_root_dom_id, ifc_node_state) {
72
                MultiValue::Exact(pv) => pv.number.get() != 0.0,
                _ => false,
72
            } || match get_css_border_top_width(styled_dom, ifc_root_dom_id, ifc_node_state) {
72
                MultiValue::Exact(pv) => pv.number.get() != 0.0,
                _ => false,
            };
            // Check for intervening non-zero padding/border on block-end (bottom)
72
            let has_pad_or_border_bottom = match get_css_padding_bottom(styled_dom, ifc_root_dom_id, ifc_node_state) {
72
                MultiValue::Exact(pv) => pv.number.get() != 0.0,
                _ => false,
72
            } || match get_css_border_bottom_width(styled_dom, ifc_root_dom_id, ifc_node_state) {
72
                MultiValue::Exact(pv) => pv.number.get() != 0.0,
                _ => false,
            };
72
            let trim_start = matches!(text_box_trim, StyleTextBoxTrim::TrimStart | StyleTextBoxTrim::TrimBoth)
72
                && !has_pad_or_border_top;
72
            let trim_end = matches!(text_box_trim, StyleTextBoxTrim::TrimEnd | StyleTextBoxTrim::TrimBoth)
72
                && !has_pad_or_border_bottom;
72
            let mut height_reduction = 0.0;
72
            if trim_start {
72
                height_reduction += half_leading + over_extra;
72
            }
72
            if trim_end {
72
                height_reduction += half_leading + under_extra;
72
            }
72
            if height_reduction > 0.0 {
72
                output.overflow_size.height = (output.overflow_size.height - height_reduction).max(0.0);
72
            }
238390
        }
238462
    }
223996
fn layout_ifc<T: ParsedFontTrait>(
223996
    ctx: &mut LayoutContext<'_, T>,
223996
    text_cache: &mut TextLayoutCache,
223996
    tree: &mut LayoutTree,
223996
    node_index: usize,
223996
    constraints: &LayoutConstraints<'_>,
223996
) -> Result<LayoutOutput> {
223996
    unsafe { crate::az_mark(0x60704_u32, (0x20u32)); }
    // [g147 az-web-lift DIAG] CALLER-side tree validity at layout_ifc entry, indexed by node_index
    // (0x60900+ = nodes.len, 0x60920+ = tree ptr) to dodge marker-overwrite across multiple IFCs.
    // Compare vs _impl's CALLEE-side (0x60940+/0x60960+): ptr differs ⇒ &mut tree mis-passes across
    // the call; ptr same but len differs ⇒ the tree's `nodes` Vec is emptied in place.
    #[cfg(feature = "web_lift")]
    unsafe {
        let slot = (node_index & 7) * 4;
        crate::az_mark(((0x60900 + slot)) as u32, (tree.nodes.len() as u32) as u32);
        crate::az_mark(((0x60920 + slot)) as u32, ((&*tree as *const LayoutTree as usize) as u32) as u32);
    }
223996
    let float_count = constraints
223996
        .bfc_state
223996
        .as_ref()
223996
        .map_or(0, |s| s.floats.floats.len());
223996
    debug_info!(
209814
        ctx,
209814
        "[layout_ifc] ENTRY: node_index={}, has_bfc_state={}, float_count={}",
        node_index,
209814
        constraints.bfc_state.is_some(),
        float_count
    );
223996
    debug_ifc_layout!(ctx, "CALLED for node_index={}", node_index);
    // +spec:display-property:7f3c1d - Anonymous inline boxes: text directly in block containers treated as anonymous inline elements in IFC
    // +spec:display-property:5a795c - root inline box: block container generates anonymous inline box holding all inline-level contents, inheriting from parent
    // For anonymous boxes, we need to find the DOM ID from a parent or child
    // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit properties from their enclosing box
223996
    let node = tree.get(LayoutNodeId::new(node_index)).ok_or(LayoutError::InvalidTree)?;
223996
    let ifc_root_dom_id = if let Some(id) = node.dom_node_id { id } else {
        // Anonymous box - get DOM ID from parent or first child with DOM ID
240
        let parent_dom_id = node
240
            .parent
240
            .and_then(|p| tree.get(LayoutNodeId::new(p)))
240
            .and_then(|n| n.dom_node_id);
240
        if let Some(id) = parent_dom_id {
240
            id
        } else {
            // Try to find DOM ID from first child
            tree.children(node_index)
                .iter()
                .filter_map(|&child_idx| tree.get(LayoutNodeId::new(child_idx))).find_map(|n| n.dom_node_id)
                .ok_or(LayoutError::InvalidTree)?
        }
    };
223996
    debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
    // +spec:display-property:a469a6 - line boxes created as needed for inline-level content in IFC
    // +spec:display-property:f3c875 - calculate layout bounds (size contributions) of each inline-level box
    // Phase 1: Collect and measure all inline-level children.
    // Fold the IFC subtree's per-node fingerprints into one key. The
    // reconcile pass already computes and stores these (they are what marks
    // a node clean/dirty), so this is a handful of hashes over data already
    // in cache — versus re-resolving the FULL cascade
    // (`get_style_properties`) for every text run and inline span, which is
    // what collection does and what made it 2 ms per IFC / 32 ms per
    // pagination even when the line layout was going to be reused.
223996
    let subtree_fingerprint = {
        use core::hash::{Hash, Hasher};
223996
        let mut h = std::collections::hash_map::DefaultHasher::new();
        // vw/vh/vmin/vmax resolve against the viewport, so a resize MUST
        // invalidate collected styles — but ONLY for documents that actually
        // use a viewport unit. Folding the viewport in unconditionally
        // invalidated EVERY collection on EVERY resize (552 re-collections
        // ≈ 19.6 ms per resize on big.md) to protect a feature the document
        // did not use. `uses_viewport_units` is detected at compact-build
        // time from the same values the cache encodes; a missing compact
        // cache degrades to the old always-invalidate behaviour.
223996
        let doc_uses_viewport_units = ctx
223996
            .styled_dom
223996
            .css_property_cache
223996
            .ptr
223996
            .compact_cache
223996
            .as_ref()
223996
            .is_none_or(|cc| cc.uses_viewport_units);
223996
        if doc_uses_viewport_units {
2
            ctx.viewport_size.width.to_bits().hash(&mut h);
2
            ctx.viewport_size.height.to_bits().hash(&mut h);
223994
        }
223996
        let compact = ctx.styled_dom.css_property_cache.ptr.compact_cache.as_ref();
223996
        let mut stack = alloc::vec![node_index];
654498
        while let Some(idx) = stack.pop() {
430502
            if let Some(cold) = tree.cold(LayoutNodeId::new(idx)) {
430502
                cold.node_data_fingerprint.hash(&mut h);
                // NodeDataFingerprint covers the node's own data and INLINE
                // css — not what the AUTHOR STYLESHEET resolved onto it. Two
                // DOMs with identical nodes but different stylesheets would
                // otherwise share a key and serve each other's styles. The
                // compact cache holds the RESOLVED values, so folding this
                // node's entries in makes any cascade change invalidate.
430502
                let dom_id_opt = tree.get(LayoutNodeId::new(idx)).and_then(|n| n.dom_node_id);
430502
                if let (Some(cc), Some(dom_id)) = (compact, dom_id_opt) {
430262
                    let i = dom_id.index();
430262
                    if let Some(t1) = cc.tier1_enums.get(i) {
430262
                        t1.hash(&mut h);
430262
                    }
430262
                    if let Some(t2) = cc.tier2b_text.get(i) {
430262
                        t2.font_family_hash.hash(&mut h);
430262
                    }
240
                }
            }
430502
            idx.hash(&mut h);
430502
            for &c in tree.children(idx) {
206506
                stack.push(c);
206506
            }
        }
223996
        h.finish()
    };
    // Visit-type census (AZ_PROFILE=cpu): which constraint TYPE reaches this
    // IFC. Min/Max-content visits that fall through to layout_flow are the
    // measure-vs-final cache thrash — intrinsic WIDTHS are cached on warm,
    // so a min/max-content visit that still re-runs line breaking is either
    // a min-content-HEIGHT request or a bug.
223996
    drop(crate::probe::Probe::span(match constraints.available_width_type {
157289
        Text3AvailableSpace::Definite(_) => "ifc_visit_definite",
38459
        Text3AvailableSpace::MinContent => "ifc_visit_min",
28248
        Text3AvailableSpace::MaxContent => "ifc_visit_max",
    }));
223996
    let cached_collection = tree
223996
        .warm(LayoutNodeId::new(node_index))
223996
        .and_then(|w| w.inline_content_cache.as_ref())
223996
        .filter(|c| c.subtree_fingerprint == subtree_fingerprint)
223996
        .map(|c| (c.content.clone(), c.child_map.clone(), c.content_hash_base));
    // `content_hash_base` rides with the collection: hashed ONCE per rebuild,
    // reused by every subsequent visit (see CachedInlineContent::content_hash_base
    // for the 29 ms this replaces).
223996
    let (collect_result, content_hash_base) = if let Some((content, child_map, base)) = cached_collection {
178805
        drop(crate::probe::Probe::span("ifc_collect_cached"));
178805
        (Ok((content, child_map)), Some(base))
    } else {
45191
        let _p = crate::probe::Probe::span("ifc_collect_content");
45191
        let res = collect_and_measure_inline_content(
45191
            ctx,
45191
            text_cache,
45191
            tree,
45191
            node_index,
45191
            constraints,
        );
45191
        let mut base = None;
45191
        if let Ok((content, child_map)) = res.as_ref() {
45191
            let computed_base = {
45191
                let _p = crate::probe::Probe::span("ifc_content_hash_base");
                use std::hash::{Hash, Hasher};
45191
                let mut h = std::collections::hash_map::DefaultHasher::new();
45191
                content.hash(&mut h);
45191
                h.finish()
            };
45191
            base = Some(computed_base);
45191
            if let Some(w) = tree.warm_mut(LayoutNodeId::new(node_index)) {
45191
                w.inline_content_cache =
45191
                    Some(Box::new(crate::solver3::layout_tree::CachedInlineContent {
45191
                        content: content.clone(),
45191
                        child_map: child_map.clone(),
45191
                        subtree_fingerprint,
45191
                        content_hash_base: computed_base,
45191
                    }));
45191
            }
        }
45191
        (res, base)
    };
    // [g133 az-web-lift DIAG] which early-return fires in POSITIONING's layout_ifc.
    #[cfg(feature = "web_lift")]
    unsafe {
        crate::az_mark((0x60680) as u32, (collect_result.as_ref().map(|(c, _)| c.len()).unwrap_or(0) as u32) as u32);
        crate::az_mark((0x60684) as u32, (if collect_result.is_ok() { 0xC0DE0680u32 } else { 0x000000EEu32 }) as u32);
    }
223996
    let (inline_content, child_map) = collect_result?;
    // #11 fix: hash the inline content once. Used to (a) skip stale Phase 2d
    // fast-path reuse and (b) force a cache REPLACE when content changed even
    // though available width is unchanged — the display-list generator paints
    // text from the cached `inline_layout_result` (display_list.rs), so a
    // content change at a same-width constraint MUST overwrite it or the old
    // glyphs keep rendering (#11 stale display list).
    // Phase 2 (translate early): resolve the container-level (IFC) constraints now,
    // so the Phase 2d cache-reuse decision below can key on them too. Reuse was keyed
    // on available width + per-run content hash only; a change to a container-level
    // property (text-align, text-align-last, text-indent, direction, line-height,
    // white-space, columns) — which is NOT covered by the per-run content hash — would
    // otherwise silently reuse a stale, differently-aligned/indented cached layout.
223996
    let text3_constraints =
223996
        translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
223996
    let current_content_hash = {
223996
        let _p = crate::probe::Probe::span("ifc_content_hash");
        use std::hash::{Hash, Hasher};
223996
        let mut h = std::collections::hash_map::DefaultHasher::new();
        // The content component comes pre-hashed from the collection cache —
        // an equal subtree_fingerprint admitted it, so its bytes are the ones
        // this hash used to re-derive per visit. `None` cannot happen when
        // `inline_content` exists (the miss arm computes a base for every Ok
        // collection), but fall back to hashing rather than unwrapping.
223996
        match content_hash_base {
223996
            Some(base) => base.hash(&mut h),
            None => inline_content.hash(&mut h),
        }
        // Fold the constraint-relevant container properties into the validity key.
223996
        text3_constraints.text_align.hash(&mut h);
223996
        text3_constraints.text_align_last.hash(&mut h);
223996
        text3_constraints.white_space_mode.hash(&mut h);
223996
        text3_constraints.direction.hash(&mut h);
223996
        text3_constraints.columns.hash(&mut h);
223996
        text3_constraints.text_indent.to_bits().hash(&mut h);
223996
        match text3_constraints.line_height {
210240
            text3::cache::LineHeight::Normal => 0u64.hash(&mut h),
13756
            text3::cache::LineHeight::Px(v) => {
13756
                1u64.hash(&mut h);
13756
                v.to_bits().hash(&mut h);
13756
            }
        }
223996
        h.finish()
    };
223996
    debug_info!(
209814
        ctx,
209814
        "[layout_ifc] Collected {} inline content items for node {}",
209814
        inline_content.len(),
        node_index
    );
226534
    for (i, item) in inline_content.iter().enumerate() {
226534
        match item {
225406
            InlineContent::Text(run) => debug_info!(ctx, "  [{}] Text: '{}'", i, run.text),
            InlineContent::Marker {
946
                run,
946
                position_outside,
946
            } => debug_info!(
288
                ctx,
288
                "  [{}] Marker: '{}' (outside={})",
                i,
                run.text,
                position_outside
            ),
154
            InlineContent::Shape(_) => debug_info!(ctx, "  [{}] Shape", i),
            InlineContent::Image(_) => debug_info!(ctx, "  [{}] Image", i),
28
            _ => debug_info!(ctx, "  [{}] Other", i),
        }
    }
223996
    debug_ifc_layout!(
209814
        ctx,
209814
        "Collected {} inline content items",
209814
        inline_content.len()
    );
223996
    if inline_content.is_empty() {
36
        debug_warning!(ctx, "inline_content is empty, returning default output!");
        // The node has no inline-level content this pass (e.g. its only
        // inline child — a text run or an inline image — was removed by a
        // relayout). Any `inline_layout_result` left over from a previous
        // frame is now stale: the display-list generator paints inline
        // objects (images, inline-block shapes) straight out of this cached
        // layout (see display_list.rs `paint_inline_*`), so a leftover entry
        // would re-emit the removed content AND index `styled_nodes` with a
        // `source_node_id` that no longer exists in the new DOM (OOB panic).
        // Clear it so the empty IFC renders nothing.
36
        if let Some(warm_node) = tree.warm_mut(LayoutNodeId::new(node_index)) {
36
            warm_node.inline_layout_result = None;
36
        }
36
        return Ok(LayoutOutput::default());
223960
    }
    // === Phase 2d: IFC incremental relayout decision tree ===
    //
    // Check if a cached layout exists with matching constraints. If so,
    // try incremental relayout (GlyphSwap or LineShift) before falling
    // back to full layout_flow().
    {
223960
        let cached_ifc = tree
223960
            .warm(LayoutNodeId::new(node_index))
223960
            .and_then(|n| n.inline_layout_result.as_ref());
        // Only reuse the cached inline layout when the available WIDTH is unchanged.
        // This fast path was built for text edits (content changes, width constant); on a
        // viewport/container resize the width differs and the text must RE-WRAP, so the
        // cached old-width layout must NOT be reused — fall through to full layout_flow()
        // below. Without this guard, resizing kept the stale line breaks (#45). Real
        // text-edit incremental relayout (with dirty items) lives in
        // LayoutWindow::try_incremental_text_relayout.
223960
        let resize_has_floats = constraints
223960
            .bfc_state
223960
            .as_ref()
223960
            .is_some_and(|s| !s.floats.floats.is_empty());
        // #11 fix: cache validity is keyed on WIDTH only, so a same-width
        // RefreshDom whose text CHANGED would otherwise reuse the stale shaped
        // layout. Require the inline content hash to match too.
        //
        // Re-flow triage (AZ_PROFILE=cpu): a steady-state fixed-page-width
        // resize still ran text_layout_flow 477× — these markers name which
        // gate rejected the cached layout for every one of those.
223960
        let cached_ifc = match cached_ifc {
            None => {
44562
                drop(crate::probe::Probe::span("ifc_reflow_cold"));
44562
                None
            }
179398
            Some(c) if !c.is_valid_for(constraints.available_width_type, resize_has_floats) => {
                // Finer buckets: dd = both DEFINITE (then by delta size —
                // "small" is sub-pixel jitter above the 0.1 eps, a rounding
                // provenance bug, not a real width change), type = the
                // constraint TYPE flipped (measure min/max-content vs final
                // definite), float = the float-gain rule.
                use Text3AvailableSpace as Avs;
97425
                let reason = if resize_has_floats && !c.has_floats {
29
                    "ifc_reflow_width_floatgain"
                } else {
97396
                    match (c.available_width, constraints.available_width_type) {
27826
                        (Avs::Definite(old), Avs::Definite(new)) => {
27826
                            if (old - new).abs() < 1.0 {
                                "ifc_reflow_width_dd_small"
                            } else {
27826
                                "ifc_reflow_width_dd_big"
                            }
                        }
69570
                        _ => "ifc_reflow_width_type",
                    }
                };
97425
                drop(crate::probe::Probe::span(reason));
97425
                None
            }
81973
            Some(c) if c.inline_content_hash != current_content_hash => {
357
                drop(crate::probe::Probe::span("ifc_reflow_content"));
357
                None
            }
81616
            Some(c) => Some(c),
        };
223960
        if let Some(cached) = cached_ifc {
81616
            if cached.line_breaks.is_none() {
                drop(crate::probe::Probe::span("ifc_reflow_no_linebreaks"));
81616
            }
81616
            if let Some(ref line_breaks) = cached.line_breaks {
                // Collect per-item advance widths from cached metrics
81616
                let old_advances: Vec<f32> = cached.item_metrics.iter()
81616
                    .map(|m| m.advance_width)
81616
                    .collect();
                // Cache-reuse fast path. Real incremental relayout for text
                // edits lives in LayoutWindow::try_incremental_text_relayout
                // (window.rs) — it has the newly-shaped items and the edited
                // node id, so it can compute real dirty_item_indices and
                // take the GlyphSwap / LineShift branches. Here we only
                // know the IFC is being re-entered (e.g. viewport resize on
                // a static IFC); with nothing re-shaped yet, the best we can
                // do is "no items changed at this level" → trivial GlyphSwap
                // to return the cached layout unchanged.
81616
                let result = text3::cache::try_incremental_relayout(
81616
                    &[], // empty = no dirty items detected at this level
81616
                    &old_advances,
81616
                    &old_advances, // same advances since we haven't reshaped yet
81616
                    line_breaks,
                );
81616
                if matches!(result, text3::cache::IncrementalRelayoutResult::GlyphSwap) {
                    // No items changed — return cached layout directly
81616
                    debug_info!(ctx, "[layout_ifc] Phase 2d: GlyphSwap — reusing cached layout");
                    // (d6h) Materialized: the stored layout may be the
                    // retirement sentinel; measuring it raw zeroed the
                    // reuse path's overflow_size (scrollbars vanished on
                    // every GlyphSwap reuse).
81616
                    let main_frag = cached.materialized();
81616
                    let frag_bounds = main_frag.bounds();
81616
                    let mut output = LayoutOutput {
81616
                        overflow_size: LogicalSize::new(
81616
                            frag_bounds.width,
81616
                            frag_bounds.height,
81616
                        ),
81616
                        baseline: main_frag.last_baseline(),
81616
                        ..Default::default()
81616
                    };
                    // The cache-reuse exit must trim like the full path: a
                    // measure pass that lands here would otherwise size the
                    // box untrimmed while the final pass trims (see
                    // apply_text_box_trim).
81616
                    apply_text_box_trim(
81616
                        ctx.styled_dom,
81616
                        ifc_root_dom_id,
81616
                        &text3_constraints,
81616
                        !main_frag.items.is_empty(),
81616
                        &mut output,
                    );
                    // Re-position inline-block children from cached layout
2152794
                    for positioned_item in &main_frag.items {
2152794
                        if let ShapedItem::Object { source, .. } = &positioned_item.item {
68
                            if let Some(&child_node_index) = child_map.get(source) {
68
                                output.positions.insert(child_node_index, LogicalPosition {
68
                                    x: positioned_item.position.x,
68
                                    y: positioned_item.position.y,
68
                                });
68
                            }
2152726
                        }
                    }
81616
                    return Ok(output);
                }
                // Fall through to full layout_flow
                drop(crate::probe::Probe::span("ifc_reflow_incr_declined"));
            }
142344
        }
    }
    // Phase 2: text3_constraints was resolved early (above) so the cache-reuse key
    // could include container-level properties.
    // Clone constraints for caching (before they're moved into fragments)
142344
    let cached_constraints = text3_constraints.clone();
142344
    debug_info!(
138012
        ctx,
138012
        "[layout_ifc] CALLING text_cache.layout_flow for node {} with {} exclusions",
        node_index,
138012
        text3_constraints.shape_exclusions.len()
    );
142344
    let fragments = vec![LayoutFragment {
142344
        id: "main".to_string(),
142344
        constraints: text3_constraints,
142344
    }];
    // Phase 3: Invoke the text layout engine.
    // Get pre-loaded fonts from font manager (fonts should be loaded before layout)
142344
    let loaded_fonts = ctx.font_manager.get_loaded_fonts();
142344
    let text_layout_result = match text_cache.layout_flow(
142344
        &inline_content,
142344
        &[],
142344
        &fragments,
142344
        &ctx.font_manager.font_chain_cache,
142344
        &ctx.font_manager.fc_cache,
142344
        &loaded_fonts,
142344
        ctx.debug_messages,
142344
    ) {
142344
        Ok(result) => {
            // [g133 az-web-lift DIAG] layout_flow returned Ok.
            #[cfg(feature = "web_lift")]
            unsafe { crate::az_mark((0x60688) as u32, (0xC0DE0688u32) as u32); }
142344
            result
        }
        Err(e) => {
            // [g133 az-web-lift DIAG] layout_flow returned Err → zero-sized (text not positioned).
            #[cfg(feature = "web_lift")]
            unsafe {
                crate::az_mark((0x60688) as u32, (0x000000EEu32) as u32);
                // Read the error's first byte (discriminant) for the marker — a
                // `*const u8` read is always aligned + in-bounds; the old
                // `*const u32` read was UB on a 1-aligned / <4-byte enum.
                crate::az_mark((0x6068C) as u32, (*(&e as *const _ as *const u8)) as u32);
            }
            // Font errors should not stop layout of other elements.
            // Log the error and return a zero-sized layout.
            debug_warning!(ctx, "Text layout failed: {:?}", e);
            debug_warning!(
                ctx,
                "Continuing with zero-sized layout for node {}",
                node_index
            );
            return Ok(LayoutOutput {
                overflow_size: LogicalSize::new(0.0, 0.0),
                ..Default::default()
            });
        }
    };
    // Phase 4: Integrate results back into the solver3 layout tree.
142344
    let mut output = LayoutOutput::default();
142344
    debug_ifc_layout!(
138012
        ctx,
138012
        "text_layout_result has {} fragment_layouts",
138012
        text_layout_result.fragment_layouts.len()
    );
142344
    if let Some(main_frag) = text_layout_result.fragment_layouts.get("main") {
142344
        let frag_bounds = main_frag.bounds();
142344
        debug_ifc_layout!(
138012
            ctx,
138012
            "Found 'main' fragment with {} items, bounds={}x{}",
138012
            main_frag.items.len(),
            frag_bounds.width,
            frag_bounds.height
        );
142344
        debug_ifc_layout!(ctx, "Storing inline_layout_result on node {}", node_index);
        // Determine if we should store this layout result using the new
        // CachedInlineLayout system. The key insight is that inline layouts
        // depend on available width:
        //
        // - Min-content measurement uses width ≈ 0 (maximum line wrapping)
        // - Max-content measurement uses width = ∞ (no line wrapping)
        // - Final layout uses the actual column/container width
        //
        // We must track which constraint type was used, otherwise a min-content
        // measurement would incorrectly be reused for final rendering.
142344
        let has_floats = constraints
142344
            .bfc_state
142344
            .as_ref()
142344
            .is_some_and(|s| !s.floats.floats.is_empty());
142344
        let current_width_type = constraints.available_width_type;
        // A layout that placed NOTHING for non-empty text content is a
        // font-race artifact, not a layout: the first pass can run before
        // `load_missing_for_chains` has parsed this run's font, shaping
        // yields zero items, and text3's own caches self-heal on the next
        // call — but THIS per-node store is keyed by width+content only, so
        // an empty result would be served to every later pass ("reuse")
        // and the paragraph would measure 0.0 forever. (miniword: the
        // sample document reported 1 page; WHICH node got poisoned flipped
        // on a single leading whitespace character re-ordering the first
        // font-less pass.) Skip the store; the next pass recomputes with
        // fonts present.
142344
        let content_has_text = inline_content
142344
            .iter()
142346
            .any(|c| matches!(c, InlineContent::Text(r) if !r.text.trim().is_empty()));
142344
        if content_has_text && main_frag.items.is_empty() {
2
            debug_info!(
                ctx,
                "[layout_ifc] NOT caching empty layout for node {} (fonts not loaded yet?)",
                node_index
            );
2
            output.overflow_size = LogicalSize::zero();
2
            return Ok(output);
142342
        }
142342
        let warm_node = tree.warm_mut(LayoutNodeId::new(node_index)).ok_or(LayoutError::InvalidTree)?;
142342
        let should_store = match &warm_node.inline_layout_result {
            None => {
                // No cached result - always store
44560
                debug_info!(
43689
                    ctx,
43689
                    "[layout_ifc] Storing NEW inline_layout_result for node {} (width_type={:?}, \
43689
                     has_floats={})",
                    node_index,
                    current_width_type,
                    has_floats
                );
44560
                true
            }
97782
            Some(cached) => {
                // Check if the new result should replace the cached one
97782
                if cached.should_replace_with(current_width_type, has_floats)
357
                    || cached.inline_content_hash != current_content_hash
                {
                    // #11 fix: the cached layout is what the display-list
                    // generator paints from; replace it when the inline content
                    // changed, even if the width constraint is unchanged.
97782
                    debug_info!(
94323
                        ctx,
94323
                        "[layout_ifc] REPLACING inline_layout_result for node {} (old: \
94323
                         width={:?}, floats={}) with (new: width={:?}, floats={})",
                        node_index,
                        cached.available_width,
                        cached.has_floats,
                        current_width_type,
                        has_floats
                    );
97782
                    true
                } else {
                    debug_info!(
                        ctx,
                        "[layout_ifc] KEEPING cached inline_layout_result for node {} (cached: \
                         width={:?}, floats={}, new: width={:?}, floats={})",
                        node_index,
                        cached.available_width,
                        cached.has_floats,
                        current_width_type,
                        has_floats
                    );
                    false
                }
            }
        };
142342
        if should_store {
142342
            let mut cil = CachedInlineLayout::new_with_constraints(
142342
                main_frag.clone(),
142342
                current_width_type,
142342
                has_floats,
142342
                cached_constraints.clone(),
142342
            );
142342
            // #11 fix: record the content hash so Phase 2d only fast-path-reuses
142342
            // this layout when the inline content is genuinely unchanged, and so
142342
            // the store decision above can detect content changes.
142342
            cil.inline_content_hash = current_content_hash;
142342
            warm_node.inline_layout_result = Some(Box::new(cil));
142342
            // DL-patching invalidation: this IFC's line layout was
142342
            // recomputed — its text items must re-emit on a patched pass.
142342
            ctx.reflowed_ifcs.insert(node_index);
142342
        }
        // Extract the overall size and baseline for the IFC root.
        // +spec:display-property:a0d0ab - IFC height = top of topmost line box to bottom of bottommost line box
        // +spec:display-property:a63b8f - baseline-source defaults to auto (last baseline for inline-block/IFC)
142342
        output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
142342
        output.baseline = main_frag.last_baseline();
142342
        warm_node.baseline = output.baseline;
142342
        apply_text_box_trim(
142342
            ctx.styled_dom,
142342
            ifc_root_dom_id,
142342
            &cached_constraints,
142342
            !main_frag.items.is_empty(),
142342
            &mut output,
        );
        // Position all the inline-block children based on text3's calculations.
        // [CoordinateSpace::Parent] - positions are relative to IFC's content-box (0,0)
1746512
        for positioned_item in &main_frag.items {
1746512
            if let ShapedItem::Object { source, content, .. } = &positioned_item.item {
86
                if let Some(&child_node_index) = child_map.get(source) {
86
                    // new_relative_pos is [CoordinateSpace::Parent] - relative to this IFC's content-box
86
                    let new_relative_pos = LogicalPosition {
86
                        x: positioned_item.position.x,
86
                        y: positioned_item.position.y,
86
                    };
86
                    output.positions.insert(child_node_index, new_relative_pos);
86
                }
1746426
            }
        }
    }
    // [g132 az-web-lift VERIFY] Capture the IFC content geometry (the line-box bounds from
    // main_frag.bounds(), set above as output.overflow_size). height>0 proves the text LAID OUT
    // (not just shaped). Free-band addrs, f32 bits. REVERT at cleanup.
    #[cfg(feature = "web_lift")]
    unsafe {
        crate::az_mark((0x60670) as u32, (output.overflow_size.width.to_bits()) as u32);
        crate::az_mark((0x60674) as u32, (output.overflow_size.height.to_bits()) as u32);
        crate::az_mark((0x60678) as u32, (output.positions.len() as u32) as u32);
        crate::az_mark((0x6067C) as u32, (0xC0DE0132u32) as u32);
    }
142342
    Ok(output)
223996
}
2024
const fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
2024
    TaffySize {
2024
        width: Some(size.width),
2024
        height: Some(size.height),
2024
    }
2024
}
/// Helper: Convert `StyleFontStyle` to `text3::cache::FontStyle`
59745
#[must_use] pub const fn convert_font_style(style: StyleFontStyle) -> crate::font_traits::FontStyle {
59745
    match style {
59689
        StyleFontStyle::Normal => crate::font_traits::FontStyle::Normal,
55
        StyleFontStyle::Italic => crate::font_traits::FontStyle::Italic,
1
        StyleFontStyle::Oblique => crate::font_traits::FontStyle::Oblique,
    }
59745
}
/// Helper: Convert `StyleFontWeight` to `FcWeight`
122712
#[must_use] pub const fn convert_font_weight(weight: StyleFontWeight) -> FcWeight {
122712
    match weight {
1
        StyleFontWeight::W100 => FcWeight::Thin,
1
        StyleFontWeight::W200 => FcWeight::ExtraLight,
2
        StyleFontWeight::W300 | StyleFontWeight::Lighter => FcWeight::Light,
122117
        StyleFontWeight::Normal => FcWeight::Normal,
1
        StyleFontWeight::W500 => FcWeight::Medium,
1
        StyleFontWeight::W600 => FcWeight::SemiBold,
586
        StyleFontWeight::Bold => FcWeight::Bold,
1
        StyleFontWeight::W800 => FcWeight::ExtraBold,
2
        StyleFontWeight::W900 | StyleFontWeight::Bolder => FcWeight::Black,
    }
122712
}
/// Resolves a CSS size metric to pixels.
///
/// - `metric`: The CSS unit (px, pt, em, vw, etc.)
/// - `value`: The numeric value
/// - `containing_block_size`: Size of containing block (for percentage)
/// - `viewport_size`: Viewport dimensions (for vw, vh, vmin, vmax)
/// - `element_font_size`: The element's own computed font-size (for `em`)
/// - `root_font_size`: The root element's computed font-size (for `rem`)
#[inline]
991
fn resolve_size_metric(
991
    metric: SizeMetric,
991
    value: f32,
991
    containing_block_size: f32,
991
    viewport_size: LogicalSize,
991
    element_font_size: f32,
991
    root_font_size: f32,
991
) -> f32 {
991
    match metric {
608
        SizeMetric::Px => value,
3
        SizeMetric::Pt => value * PT_TO_PX,
349
        SizeMetric::Percent => value / 100.0 * containing_block_size,
5
        SizeMetric::Em => value * element_font_size,
3
        SizeMetric::Rem => value * root_font_size,
3
        SizeMetric::Vw => value / 100.0 * viewport_size.width,
3
        SizeMetric::Vh => value / 100.0 * viewport_size.height,
4
        SizeMetric::Vmin => value / 100.0 * viewport_size.width.min(viewport_size.height),
4
        SizeMetric::Vmax => value / 100.0 * viewport_size.width.max(viewport_size.height),
3
        SizeMetric::In => value * super::calc::PX_PER_INCH,
3
        SizeMetric::Cm => value * super::calc::PX_PER_INCH / super::calc::CM_PER_INCH,
3
        SizeMetric::Mm => value * super::calc::PX_PER_INCH / super::calc::MM_PER_INCH,
    }
991
}
1263098
#[must_use] pub const fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
1263098
    LogicalSize {
1263098
        width: size.width,
1263098
        height: size.height,
1263098
    }
1263098
}
219321
#[must_use] pub const fn translate_taffy_point_back(point: taffy::Point<f32>) -> LogicalPosition {
219321
    LogicalPosition {
219321
        x: point.x,
219321
        y: point.y,
219321
    }
219321
}
// +spec:block-formatting-context:40e03e - BFC root: block container establishing new BFC (contains floats, excludes external floats, suppresses margin collapsing)
/// Checks if a node establishes a new Block Formatting Context (BFC).
///
/// Per CSS 2.2 § 9.4.1, a BFC is established by:
/// - Floats (elements with float other than 'none')
/// - Absolutely positioned elements (position: absolute or fixed)
/// - Block containers that are not block boxes (e.g., inline-blocks, table-cells)
/// - Block boxes with 'overflow' other than 'visible' and 'clip'
/// - Elements with 'display: flow-root'
/// - Table cells, table captions, and inline-blocks
///
/// Normal flow block-level boxes do NOT establish a new BFC.
///
/// This is critical for correct float interaction: normal blocks should overlap floats
/// (not shrink around them), while their inline content wraps around floats.
// +spec:block-formatting-context:241d22 - block container establishes new BFC or continues parent's, based on overflow/position/float/display
// +spec:block-formatting-context:9fe441 - BFC establishment based on position, float, overflow, and display properties
// +spec:display-property:3c7369 - block boxes establishing independent FC create new BFC; flex containers already do; non-replaced inlines cannot
// +spec:positioning:1e94f6 - floats, abspos, inline-blocks/table-cells/table-captions, overflow!=visible establish new BFC
114033
fn establishes_new_bfc<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNodeHot, cold: Option<&LayoutNodeCold>) -> bool {
    // +spec:block-formatting-context:f39cd3 - table wrapper box establishes a BFC (CSS 2.2 §17.4)
    // Anonymous table wrapper boxes have no dom_node_id but must still establish BFC
    // +spec:height-calculation:e20498 - table wrapper box establishes BFC (CSS 2.2 §17.4)
    // +spec:positioning:b780d3 - Table wrapper box establishes BFC (CSS 2.2 § 17.4)
114033
    if cold.and_then(|c| c.anonymous_type) == Some(AnonymousBoxType::TableWrapper) {
        return true;
114033
    }
114033
    let Some(dom_id) = node.dom_node_id else {
124
        return false;
    };
113909
    let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
    // 1. Floats establish BFC
113909
    let float_val = get_float(ctx.styled_dom, dom_id, node_state);
113884
    if matches!(
113902
        float_val,
        MultiValue::Exact(LayoutFloat::Left | LayoutFloat::Right)
    ) {
25
        return true;
113884
    }
    // +spec:positioning:69468c - absolute/fixed forces independent formatting context
113884
    let position = get_position_type(ctx.styled_dom, Some(dom_id));
113884
    if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
2686
        return true;
111198
    }
    // 3. Inline-blocks, table-cells, table-captions establish BFC
111198
    let display = get_display_property(ctx.styled_dom, Some(dom_id));
44913
    if matches!(
111198
        display,
        MultiValue::Exact(
            LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption
        )
    ) {
66285
        return true;
44913
    }
    // 4. display: flow-root establishes BFC
    // +spec:display-property:14bae6 - flow-root establishes a formatting context that contains/excludes floats
44913
    if matches!(display, MultiValue::Exact(LayoutDisplay::FlowRoot)) {
        return true;
44913
    }
    // +spec:overflow:0a944d - clip does NOT establish BFC; hidden/scroll/auto do establish BFC
    // +spec:overflow:631a4c - scroll containers establish independent formatting context (BFC)
    // +spec:overflow:f6a186 - overflow:clip does NOT establish BFC; use display:flow-root for that
    // +spec:overflow:717de1 - overflow != visible/clip establishes BFC per CSS 2.2 §9.4.1
    // +spec:positioning:6feb32 - overflow:clip does NOT establish new formatting context; hidden/scroll/auto do
    // 5. Block boxes with overflow other than 'visible' or 'clip' establish BFC
    // +spec:overflow:b34aef - Block boxes with overflow other than 'visible' or 'clip' establish BFC
    // Note: 'clip' does NOT establish BFC per CSS Overflow Module Level 3
44913
    let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, node_state);
44913
    let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, node_state);
88686
    let creates_bfc_via_overflow = |ov: &MultiValue<LayoutOverflow>| {
87316
        matches!(
88674
            ov,
            &MultiValue::Exact(
                LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
            )
        )
88686
    };
44913
    if creates_bfc_via_overflow(&overflow_x) || creates_bfc_via_overflow(&overflow_y) {
1370
        return true;
43543
    }
    // 6. Table, Flex, and Grid containers establish BFC (via FormattingContext)
    // +spec:block-formatting-context:f15b87 - display:table participates in a BFC
42103
    if matches!(
43543
        node.formatting_context,
        FormattingContext::Table | FormattingContext::Flex | FormattingContext::Grid
    ) {
1440
        return true;
42103
    }
    // +spec:block-formatting-context:f15b87 - a flex/grid ITEM establishes an
    // independent formatting context for its contents (CSS Flexbox 1 § 3, CSS Grid 1
    // § 6). Its children's margins are therefore contained and must NOT collapse
    // through it — without this, the last child's margin-bottom escapes and the item
    // (e.g. the invoice `.head`'s inner div) reports a cross size short by that margin,
    // so the whole flex container is under-tall. Detect it from the parent's display.
    {
42103
        let hierarchy = ctx.styled_dom.node_hierarchy.as_container();
42103
        if let Some(parent_dom_id) = hierarchy[dom_id].parent_id() {
38369
            let parent_display = get_display_property(ctx.styled_dom, Some(parent_dom_id));
26699
            if matches!(
38369
                parent_display,
                MultiValue::Exact(
                    LayoutDisplay::Flex
                        | LayoutDisplay::InlineFlex
                        | LayoutDisplay::Grid
                        | LayoutDisplay::InlineGrid
                )
            ) {
11670
                return true;
26699
            }
3734
        }
    }
    // +spec:block-formatting-context:33e6cd - block container with different writing-mode than parent establishes independent BFC
    // CSS Writing Modes 4 § 3.2: if a block container has a different writing-mode
    // than its parent, its inner display type computes to flow-root (i.e., it establishes BFC).
    {
30433
        let hierarchy = ctx.styled_dom.node_hierarchy.as_container();
30433
        if let Some(parent_dom_id) = hierarchy[dom_id].parent_id() {
26699
            let parent_state = &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
26699
            let child_wm = get_writing_mode(ctx.styled_dom, dom_id, node_state).unwrap_or_default();
26699
            let parent_wm = get_writing_mode(ctx.styled_dom, parent_dom_id, parent_state).unwrap_or_default();
26699
            if child_wm != parent_wm {
27
                return true;
26672
            }
3734
        }
    }
    // Normal flow block boxes do NOT establish BFC
    // NOTE: align-content != normal should also establish BFC per CSS-DISPLAY-3, but align-content is not yet implemented for block containers
30406
    false
114033
}
// +spec:display-property:5e5420 - replaced element identification (glossary: replaced elements have natural dimensions, establish independent formatting context)
/// CSS 2.2 § 9.5: "The border box of a table, a block-level replaced element, or an element
/// in the normal flow that establishes a new block formatting context [...] must not overlap
/// the margin box of any floats in the same block formatting context as the element itself."
22465
fn is_block_level_replaced<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNodeHot) -> bool {
22465
    let Some(dom_id) = node.dom_node_id else {
122
        return false;
    };
    // Check display is block-level
22343
    let display = get_display_property(ctx.styled_dom, Some(dom_id));
22343
    let is_block_level = matches!(
22343
        display,
        MultiValue::Exact(LayoutDisplay::Block | LayoutDisplay::ListItem | LayoutDisplay::FlowRoot)
    );
22343
    if !is_block_level {
2033
        return false;
20310
    }
    // Check if the element is a replaced element (image, video, etc.)
20310
    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
20310
    matches!(
20310
        node_data.get_node_type(),
        NodeType::Image(_)
    )
22465
}
/// Translates solver3 layout constraints into the text3 engine's unified constraints.
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
223996
fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
223996
    ctx: &mut LayoutContext<'_, T>,
223996
    constraints: &'a LayoutConstraints<'a>,
223996
    styled_dom: &StyledDom,
223996
    dom_id: NodeId,
223996
) -> UnifiedConstraints {
    use azul_css::compact_cache::{
        DOM_HAS_SHAPE_INSIDE, DOM_HAS_SHAPE_OUTSIDE, DOM_HAS_TEXT_JUSTIFY,
        DOM_HAS_TEXT_INDENT, DOM_HAS_COLUMN_COUNT, DOM_HAS_COLUMN_GAP,
        DOM_HAS_COLUMN_WIDTH,
        DOM_HAS_INITIAL_LETTER, DOM_HAS_INITIAL_LETTER_ALIGN,
        DOM_HAS_LINE_CLAMP, DOM_HAS_HANGING_PUNCTUATION,
        DOM_HAS_TEXT_COMBINE_UPRIGHT, DOM_HAS_EXCLUSION_MARGIN,
        DOM_HAS_SHAPE_MARGIN,
        DOM_HAS_HYPHENATION_LANGUAGE, DOM_HAS_UNICODE_BIDI,
        DOM_HAS_HYPHENS, DOM_HAS_WORD_BREAK, DOM_HAS_OVERFLOW_WRAP,
        DOM_HAS_LINE_BREAK, DOM_HAS_TEXT_ALIGN_LAST, DOM_HAS_LINE_HEIGHT,
    };
223996
    unsafe { crate::az_mark(0x60704_u32, (0x30u32)); }
    // DOM-level declared flags: if a bit is clear, no node in this DOM
    // declared the corresponding property → cascade walks always return
    // None, and we use the default value directly. All flags default to
    // "set" when there is no compact cache (paranoid fallback).
223996
    let dom_declared = styled_dom
223996
        .css_property_cache
223996
        .ptr
223996
        .compact_cache
223996
        .as_ref()
223996
        .map_or(!0u32, |cc| cc.dom_declared_flags);
    // Convert floats into exclusion zones for text3 to flow around.
223996
    let mut shape_exclusions = if let Some(ref bfc_state) = constraints.bfc_state {
18192
        debug_info!(
8367
            ctx,
8367
            "[translate_to_text3] dom_id={:?}, converting {} floats to exclusions",
            dom_id,
8367
            bfc_state.floats.floats.len()
        );
18192
        bfc_state
18192
            .floats
18192
            .floats
18192
            .iter()
18192
            .enumerate()
18192
            .map(|(i, float_box)| {
38
                let rect = text3::cache::Rect {
38
                    x: float_box.rect.origin.x,
38
                    y: float_box.rect.origin.y,
38
                    width: float_box.rect.size.width,
38
                    height: float_box.rect.size.height,
38
                };
38
                debug_info!(
6
                    ctx,
6
                    "[translate_to_text3]   Exclusion #{}: {:?} at ({}, {}) size {}x{}",
                    i,
                    float_box.kind,
                    rect.x,
                    rect.y,
                    rect.width,
                    rect.height
                );
38
                ShapeBoundary::Rectangle(rect)
38
            })
18192
            .collect()
    } else {
205804
        debug_info!(
201447
            ctx,
201447
            "[translate_to_text3] dom_id={:?}, NO bfc_state - no float exclusions",
            dom_id
        );
205804
        Vec::new()
    };
223996
    debug_info!(
209814
        ctx,
209814
        "[translate_to_text3] dom_id={:?}, available_size={}x{}, shape_exclusions.len()={}",
        dom_id,
        constraints.available_size.width,
        constraints.available_size.height,
209814
        shape_exclusions.len()
    );
    // Map text-align and justify-content from CSS to text3 enums.
223996
    let id = dom_id;
223996
    let node_data = &styled_dom.node_data.as_container()[id];
223996
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
    // Read CSS Shapes properties
    // For reference box, use the element's CSS height if available, otherwise available_size
    // This is important because available_size.height might be infinite during auto height
    // calculation
223996
    let ref_box_height = if constraints.available_size.height.is_finite() {
134933
        constraints.available_size.height
    } else {
        // Try to get explicit CSS height
        // NOTE: If height is infinite, we can't properly resolve % heights
        // This is a limitation - shape-inside with % heights requires finite containing block
89063
        styled_dom
89063
            .css_property_cache
89063
            .ptr
89063
            .get_height(node_data, &id, node_state)
89063
            .and_then(|v| v.get_property())
89063
            .and_then(|h| match h {
25
                LayoutHeight::Px(v) => {
                    // Only accept absolute units (px, pt, in, cm, mm) - no %, em, rem
                    // since we can't resolve relative units without proper context
25
                    match v.metric {
25
                        SizeMetric::Px => Some(v.number.get()),
                        SizeMetric::Pt => Some(v.number.get() * PT_TO_PX),
                        SizeMetric::In => Some(v.number.get() * super::calc::PX_PER_INCH),
                        SizeMetric::Cm => Some(v.number.get() * super::calc::PX_PER_INCH / super::calc::CM_PER_INCH),
                        SizeMetric::Mm => Some(v.number.get() * super::calc::PX_PER_INCH / super::calc::MM_PER_INCH),
                        _ => None, // Ignore %, em, rem
                    }
                }
                _ => None,
25
            })
89063
            .unwrap_or(constraints.available_size.width) // Fallback: use width as height (square)
    };
223996
    let reference_box = text3::cache::Rect {
223996
        x: 0.0,
223996
        y: 0.0,
223996
        width: constraints.available_size.width,
223996
        height: ref_box_height,
223996
    };
    // shape-inside: Text flows within the shape boundary
223996
    debug_info!(ctx, "Checking shape-inside for node {:?}", id);
223996
    debug_info!(
209814
        ctx,
209814
        "Reference box: {:?} (available_size height was: {})",
        reference_box,
        constraints.available_size.height
    );
223996
    let shape_boundaries = if dom_declared & DOM_HAS_SHAPE_INSIDE != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_shape_inside(node_data, &id, node_state)
            .and_then(|v| {
                debug_info!(ctx, "Got shape-inside value: {:?}", v);
                v.get_property()
            })
            .and_then(|shape_inside| {
                debug_info!(ctx, "shape-inside property: {:?}", shape_inside);
                if let ShapeInside::Shape(css_shape) = shape_inside {
                    debug_info!(
                        ctx,
                        "Converting CSS shape to ShapeBoundary: {:?}",
                        css_shape
                    );
                    let boundary =
                        ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
                    debug_info!(ctx, "Created ShapeBoundary: {:?}", boundary);
                    Some(vec![boundary])
                } else {
                    debug_info!(ctx, "shape-inside is None");
                    None
                }
            })
            .unwrap_or_default()
    } else {
223996
        Vec::new()
    };
223996
    debug_info!(
209814
        ctx,
209814
        "Final shape_boundaries count: {}",
209814
        shape_boundaries.len()
    );
    // shape-outside: Text wraps around the shape (adds to exclusions)
223996
    debug_info!(ctx, "Checking shape-outside for node {:?}", id);
223996
    if dom_declared & DOM_HAS_SHAPE_OUTSIDE != 0 {
        if let Some(shape_outside_value) = styled_dom
            .css_property_cache
            .ptr
            .get_shape_outside(node_data, &id, node_state)
        {
            debug_info!(ctx, "Got shape-outside value: {:?}", shape_outside_value);
            if let Some(shape_outside) = shape_outside_value.get_property() {
                debug_info!(ctx, "shape-outside property: {:?}", shape_outside);
                if let ShapeOutside::Shape(css_shape) = shape_outside {
                    debug_info!(
                        ctx,
                        "Converting CSS shape-outside to ShapeBoundary: {:?}",
                        css_shape
                    );
                    let boundary =
                        ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
                    debug_info!(ctx, "Created ShapeBoundary (exclusion): {:?}", boundary);
                    shape_exclusions.push(boundary);
                }
            }
        } else {
            debug_info!(ctx, "No shape-outside value found");
        }
223996
    }
    // TODO: clip-path will be used for rendering clipping (not text layout)
223996
    let writing_mode = get_writing_mode(styled_dom, id, node_state).unwrap_or_default();
223996
    let text_align = get_text_align(styled_dom, id, node_state).unwrap_or_default();
223996
    let text_justify = if dom_declared & DOM_HAS_TEXT_JUSTIFY != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_text_justify(node_data, &id, node_state)
            .and_then(|s| s.get_property().copied())
            .unwrap_or_default()
    } else {
223996
        LayoutTextJustify::default()
    };
    // Get font-size for resolving line-height
    // Use helper function which checks dependency chain first
223996
    let font_size = get_element_font_size(styled_dom, id, node_state);
223996
    let line_height_value = if dom_declared & DOM_HAS_LINE_HEIGHT != 0 {
13756
        styled_dom
13756
            .css_property_cache
13756
            .ptr
13756
            .get_line_height(node_data, &id, node_state)
13756
            .and_then(|s| s.get_property().copied())
13756
            .unwrap_or_default()
    } else {
210240
        azul_css::props::style::text::StyleLineHeight::default()
    };
223996
    let hyphenation = if dom_declared & DOM_HAS_HYPHENS != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_hyphens(node_data, &id, node_state)
            .and_then(|s| s.get_property().copied())
            .unwrap_or_default()
    } else {
223996
        StyleHyphens::default()
    };
223996
    let word_break_css = if dom_declared & DOM_HAS_WORD_BREAK != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_word_break(node_data, &id, node_state)
            .and_then(|s| s.get_property().copied())
            .unwrap_or_default()
    } else {
223996
        StyleWordBreak::default()
    };
223996
    let overflow_wrap_css = if dom_declared & DOM_HAS_OVERFLOW_WRAP != 0 {
18
        styled_dom
18
            .css_property_cache
18
            .ptr
18
            .get_overflow_wrap(node_data, &id, node_state)
18
            .and_then(|s| s.get_property().copied())
18
            .unwrap_or_default()
    } else {
223978
        StyleOverflowWrap::default()
    };
223996
    let line_break_css = if dom_declared & DOM_HAS_LINE_BREAK != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_line_break(node_data, &id, node_state)
            .and_then(|s| s.get_property().copied())
            .unwrap_or_default()
    } else {
223996
        StyleLineBreak::default()
    };
223996
    let text_align_last_css = if dom_declared & DOM_HAS_TEXT_ALIGN_LAST != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_text_align_last(node_data, &id, node_state)
            .and_then(|s| s.get_property().copied())
            .unwrap_or_default()
    } else {
223996
        StyleTextAlignLast::default()
    };
223996
    let overflow_behaviour = get_overflow_x(styled_dom, id, node_state).unwrap_or_default();
    // +spec:display-property:21f728 - vertical-align shorthand resolves inline-level box alignment
    // +spec:display-property:98fa8e - alignment-baseline values for inline-level boxes in IFC (implemented via vertical-align shorthand)
    // +spec:display-property:1f71ad - baseline-shift + alignment-baseline longhands mapped through vertical-align
    // +spec:display-property:89dd7b - line-relative shift values (top/center/bottom) and aligned subtree alignment
    // +spec:inline-formatting-context:21da06 - vertical-align uses line-over/line-under sides via writing_mode logical mapping
    // +spec:inline-formatting-context:295603 - baseline alignment: vertical-align determines how inline boxes align (baseline, super, sub, etc.)
    // +spec:inline-formatting-context:7351bf - default alignment baseline is alphabetic in horizontal typographic mode
    // +spec:inline-formatting-context:85de3d - vertical-align shorthand: alignment within line box
    // +spec:inline-formatting-context:aa8af0 - alignment baseline chosen by vertical-align, defaults to parent's dominant baseline
    // +spec:inline-formatting-context:e475d2 - baseline and vertical-align control transverse alignment of inline content on line boxes
    // +spec:overflow:d44eac - vertical-align inline box alignment (CSS 2.2 model covers baseline/top/middle/bottom/sub/super/text-top/text-bottom)
    // +spec:writing-modes:313575 - alignment-baseline: inline-level boxes align baselines within parent inline box's alignment context along inline axis
    // +spec:writing-modes:60ad67 - inline layout aligns boxes in block axis via baselines
    // +spec:writing-modes:0127e5 - line-relative directions: line-over/under map to vertical-align top/bottom
    // Get vertical-align from CSS property cache (defaults to Baseline per CSS spec)
    // +spec:inline-formatting-context:686f8b - vertical-align shorthand: alignment-baseline + baseline-shift for inline boxes
    // +spec:inline-formatting-context:e579b6 - vertical-align / baseline alignment in inline context
    // +spec:inline-formatting-context:a01a75 - dominant baseline alignment for atomic inlines
    //
    // CSS 2.2 section 10.8.1: vertical-align applies to INLINE-LEVEL boxes
    // and TABLE CELLS only. `id` here is the IFC ROOT (a block container:
    // div, td, th, ...) — its own vertical-align must NOT become the line
    // alignment of its anonymous inline content, which always starts from
    // the initial value (baseline). Inline spans and atomic inlines inside
    // the IFC carry their alignment per-item. A table cell's vertical-align
    // is consumed by position_table_cells (cell content block alignment),
    // and letting it leak in here double-applied it as an inline shift:
    // the UA `th { vertical-align: middle }` pushed every header glyph in
    // table-basic-001 ~9px below the padding box.
223996
    let vertical_align = StyleVerticalAlign::Baseline;
    // +spec:display-property:c03a6b - baseline-shift (sub/super/length/percentage) and line-relative (top/center/bottom) shifts handled via vertical-align
223996
    let vertical_align = match vertical_align {
223996
        StyleVerticalAlign::Baseline => text3::cache::VerticalAlign::Baseline,
        StyleVerticalAlign::Top => text3::cache::VerticalAlign::Top,
        StyleVerticalAlign::Middle => text3::cache::VerticalAlign::Middle,
        StyleVerticalAlign::Bottom => text3::cache::VerticalAlign::Bottom,
        StyleVerticalAlign::Sub => text3::cache::VerticalAlign::Sub,
        // +spec:inline-formatting-context:fe563c - vertical-align: super shifts inline to superscript position
        // +spec:inline-formatting-context:fe563c - vertical-align:super shifts child to superscript position
        StyleVerticalAlign::Superscript => text3::cache::VerticalAlign::Super,
        StyleVerticalAlign::TextTop => text3::cache::VerticalAlign::TextTop,
        StyleVerticalAlign::TextBottom => text3::cache::VerticalAlign::TextBottom,
        // §10.8.1: <percentage> refers to line-height of the element itself
        StyleVerticalAlign::Percentage(p) => {
            let lh_n = line_height_value.inner.normalized();
            let resolved_lh = if lh_n < 0.0 { -lh_n } else { lh_n * font_size };
            let offset = p.normalized() * resolved_lh;
            text3::cache::VerticalAlign::Offset(offset)
        }
        // §10.8.1: <length> is absolute offset from baseline
        StyleVerticalAlign::Length(l) => {
            // Resolve viewport units (vw/vh/vmin/vmax) against the real viewport
            // instead of falling through `resolve_pixel_value`'s "treat 50vw as 50px".
            let offset = super::calc::resolve_pixel_value_with_viewport(
                &l,
                0.0,
                font_size,
                font_size,
                ctx.viewport_size.width,
                ctx.viewport_size.height,
            );
            text3::cache::VerticalAlign::Offset(offset)
        }
    };
    // +spec:block-formatting-context:987746 - text-orientation property (mixed/upright/sideways) for vertical writing modes
    // +spec:inline-formatting-context:cbe738 - text-orientation (mixed/upright/sideways) bi-orientational transform for vertical text
    // +spec:writing-modes:09a1bb - vertical typesetting orientation (upright/sideways) for vertical-rl/vertical-lr
    // +spec:writing-modes:2eb1b2 - text-orientation (mixed/upright/sideways) applied to vertical text layout
223996
    let text_orientation = match get_text_orientation_property(styled_dom, id, node_state) {
        MultiValue::Exact(o) => match o {
            StyleTextOrientation::Mixed => text3::cache::TextOrientation::Mixed,
            StyleTextOrientation::Upright => text3::cache::TextOrientation::Upright,
            // +spec:block-formatting-context:a606e6 - sideways text typeset rotated 90° CW in vertical modes
            StyleTextOrientation::Sideways => text3::cache::TextOrientation::Sideways,
        },
223996
        _ => text3::cache::TextOrientation::default(),
    };
    // +spec:display-property:8364c0 - direction property (ltr/rtl) sets paragraph embedding level for bidi algorithm
    // +spec:text-alignment-spacing:97b93a - direction property affects text-align:justify last-line alignment
    // +spec:writing-modes:73aaff - block elements inherit base direction from parent via CSS direction property
    // +spec:writing-modes:8a888b - line box inline base direction from containing block's direction
    // Get the direction property from the CSS cache (defaults to LTR if not set)
    // +spec:display-property:da3b59 - direction property specifies inline base direction for ordering inline-level content
    // +spec:inline-formatting-context:97af40 - direction property sets inline base direction for bidi, text alignment, overflow
    // +spec:writing-modes:2deb38 - bidirectional reordering via CSS direction property
    // +spec:writing-modes:fbb332 - in vertical writing modes, text-orientation:upright forces used direction to ltr
223996
    let direction = match constraints.writing_mode {
        LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr
9
            if matches!(text_orientation, text3::cache::TextOrientation::Upright) =>
        {
            Some(text3::cache::BidiDirection::Ltr)
        }
223996
        _ => match get_direction_property(styled_dom, id, node_state) {
223990
            MultiValue::Exact(d) => Some(match d {
223972
                StyleDirection::Ltr => text3::cache::BidiDirection::Ltr,
18
                StyleDirection::Rtl => text3::cache::BidiDirection::Rtl,
            }),
6
            _ => None,
        },
    };
    // Get unicode-bidi property for bidi algorithm configuration
    // +spec:containing-block:0d4914 - unicode-bidi: plaintext causes P2/P3 heuristics instead of HL1 override
223996
    let unicode_bidi_val = if dom_declared & DOM_HAS_UNICODE_BIDI != 0 {
        match get_unicode_bidi_property(styled_dom, id, node_state) {
            MultiValue::Exact(u) => match u {
                StyleUnicodeBidi::Normal => text3::cache::UnicodeBidi::Normal,
                StyleUnicodeBidi::Embed => text3::cache::UnicodeBidi::Embed,
                StyleUnicodeBidi::Isolate => text3::cache::UnicodeBidi::Isolate,
                StyleUnicodeBidi::BidiOverride => text3::cache::UnicodeBidi::BidiOverride,
                StyleUnicodeBidi::IsolateOverride => text3::cache::UnicodeBidi::IsolateOverride,
                StyleUnicodeBidi::Plaintext => text3::cache::UnicodeBidi::Plaintext,
            },
            _ => text3::cache::UnicodeBidi::Normal,
        }
    } else {
223996
        text3::cache::UnicodeBidi::Normal
    };
223996
    debug_info!(
209814
        ctx,
209814
        "dom_id={:?}, available_size={}x{}, setting available_width={}",
        dom_id,
        constraints.available_size.width,
        constraints.available_size.height,
        constraints.available_size.width
    );
    // +spec:box-model:8113d7 - text-indent treated as margin on start edge of line box
    // +spec:display-contents:5f95ac - text-indent: percentage=0 for intrinsic sizing, each-line and hanging keywords
    // +spec:floats:17c74a - text-indent applied to first line (5em indentation with no floats)
    // +spec:positioning:1e32b1 - text-indent with hanging/each-line keywords resolved and passed to text layout
223996
    let text_indent_prop = if dom_declared & DOM_HAS_TEXT_INDENT != 0 {
2
        styled_dom
2
            .css_property_cache
2
            .ptr
2
            .get_text_indent(node_data, &id, node_state)
2
            .and_then(|s| s.get_property().copied())
    } else {
223994
        None
    };
223996
    let is_intrinsic_sizing = matches!(
223996
        constraints.available_width_type,
        Text3AvailableSpace::MinContent | Text3AvailableSpace::MaxContent
    );
    // +spec:intrinsic-sizing:0e8625 - percentage text-indent treated as 0 for intrinsic size contributions
223996
    let text_indent = text_indent_prop
223996
        .map_or(0.0, |ti| {
            // CSS Text 3 §8.1: "Percentages must be treated as 0 for the purpose
            // of calculating intrinsic size contributions"
2
            if is_intrinsic_sizing && ti.inner.to_percent().is_some() {
                return 0.0;
2
            }
2
            let context = ResolutionContext {
2
                vertical_writing_mode: false,
2
                element_font_size: get_element_font_size(styled_dom, id, node_state),
2
                parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2
                root_font_size: get_root_font_size(styled_dom, node_state),
2
                containing_block_size: PhysicalSize::new(constraints.available_size.width, 0.0),
2
                element_size: None,
2
                viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
2
            };
2
            ti.inner
2
                .resolve_with_context(&context, PropertyContext::Other)
2
        });
223996
    let text_indent_each_line = text_indent_prop.is_some_and(|ti| ti.each_line);
223996
    let text_indent_hanging = text_indent_prop.is_some_and(|ti| ti.hanging);
    // ResolutionContext shared by column-gap and column-width (both resolve
    // lengths against the same font/viewport, with no containing-block size).
223996
    let column_resolve_ctx = ResolutionContext {
223996
        vertical_writing_mode: false,
223996
        element_font_size: get_element_font_size(styled_dom, id, node_state),
223996
        parent_font_size: get_parent_font_size(styled_dom, id, node_state),
223996
        root_font_size: get_root_font_size(styled_dom, node_state),
223996
        containing_block_size: PhysicalSize::new(0.0, 0.0),
223996
        element_size: None,
223996
        viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
223996
    };
    // Read a declared CSS property from the cache, returning None when the
    // DOM-level declared bit is clear (no node sets the property).
    macro_rules! declared_prop {
        ($bit:expr, $getter:ident) => {
            if dom_declared & $bit != 0 {
                styled_dom
                    .css_property_cache
                    .ptr
                    .$getter(node_data, &id, node_state)
                    .and_then(|s| s.get_property())
            } else {
                None
            }
        };
    }
    // Get column-gap for multi-column layout (default: normal = 1em)
223996
    let column_gap = declared_prop!(DOM_HAS_COLUMN_GAP, get_column_gap)
223996
        .map(|cg| {
            cg.inner
                .resolve_with_context(&column_resolve_ctx, PropertyContext::Other)
        })
223996
        .unwrap_or_else(|| get_element_font_size(styled_dom, id, node_state));
    // Get column-width for multi-column layout (None = auto)
223996
    let column_width =
223996
        declared_prop!(DOM_HAS_COLUMN_WIDTH, get_column_width).and_then(|cw| match cw {
            ColumnWidth::Auto => None,
            ColumnWidth::Length(px) => {
                Some(px.resolve_with_context(&column_resolve_ctx, PropertyContext::Other))
            }
        });
    // Get column-count for multi-column layout (default: 1 = no columns)
223996
    let explicit_column_count =
223996
        declared_prop!(DOM_HAS_COLUMN_COUNT, get_column_count).copied();
    // CSS multi-column: derive column count from column-width when column-count is auto.
    // Per spec: N = max(1, floor((available-width + column-gap) / (column-width + column-gap)))
223996
    let columns = match (explicit_column_count, column_width) {
        (Some(ColumnCount::Integer(n)), _) => n,
        (_, Some(cw)) if cw > 0.0 => {
            let avail = constraints.available_size.width;
            ((avail + column_gap) / (cw + column_gap)).floor().max(1.0) as u32
        }
223996
        _ => 1,
    };
    // +spec:line-breaking:b4928e - white-space values mapped to wrap/whitespace processing rules
    // Map white-space CSS property to TextWrap
223996
    let resolved_ws = match get_white_space_property(styled_dom, id, node_state) {
223990
        MultiValue::Exact(ws) => ws,
6
        _ => StyleWhiteSpace::Normal,
    };
223996
    let text_wrap = match resolved_ws {
        StyleWhiteSpace::Normal
        | StyleWhiteSpace::PreWrap
        | StyleWhiteSpace::PreLine
223967
        | StyleWhiteSpace::BreakSpaces => text3::cache::TextWrap::Wrap,
29
        StyleWhiteSpace::Nowrap | StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
    };
223996
    let white_space_mode = match resolved_ws {
223959
        StyleWhiteSpace::Normal => text3::cache::WhiteSpaceMode::Normal,
13
        StyleWhiteSpace::Nowrap => text3::cache::WhiteSpaceMode::Nowrap,
16
        StyleWhiteSpace::Pre => text3::cache::WhiteSpaceMode::Pre,
4
        StyleWhiteSpace::PreWrap => text3::cache::WhiteSpaceMode::PreWrap,
4
        StyleWhiteSpace::PreLine => text3::cache::WhiteSpaceMode::PreLine,
        StyleWhiteSpace::BreakSpaces => text3::cache::WhiteSpaceMode::BreakSpaces,
    };
    // +spec:block-formatting-context:fd60a8 - initial letter box is in-flow in its BFC, originating line box
    // +spec:block-formatting-context:c5ba02 - initial letter inline flow layout (alignment, white space collapsing)
    // +spec:block-formatting-context:83f8a7 - initial letter wrapping modes (none, all, first)
    // +spec:block-formatting-context:fef28d - initial letter box is in-flow in its BFC, part of originating line box
    // +spec:box-model:c3ce58 - initial letter block-start margin edge must be below containing block content edge
    // +spec:display-contents:568fe2 - initial letter participates in same IFC as its line
    // +spec:display-property:a89adb - initial letter boxes from non-replaced inline boxes and atomic inlines
    // +spec:display-property:4b59ce - initial-letter applies to inline-level boxes at start of first line
    // +spec:display-property:756cad - initial-letter sizing: drop/raise/sunken initial computation
    // +spec:display-property:8b08f4 - initial-letter applied to first inline-level child of block container
    // +spec:display-property:8c1dce - initial-letter property: size/sink for drop caps on inline-level boxes
    // +spec:display-property:b453a3 - initial-letter applies to inline-level boxes in IFC
    // +spec:display-property:b5e149 - initial letters are in-flow inline-level content, not floats
    // +spec:display-property:fa044e - initial-letter applies to first-child inline-level boxes
    // +spec:line-height:306d87 - initial-letter sizing must use containing block's line-height, not spanned lines' heights
    // +spec:writing-modes:903310 - atomic initial letters use normal sizing; only positioning is special
    // Get initial-letter for drop caps
    // +spec:display-property:4c69bf - read initial-letter-align for alignment points
223996
    let initial_letter_align = if dom_declared & DOM_HAS_INITIAL_LETTER_ALIGN != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_initial_letter_align(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .map_or(text3::cache::InitialLetterAlign::Auto, |a| match a {
                azul_css::props::style::text::StyleInitialLetterAlign::Auto => text3::cache::InitialLetterAlign::Auto,
                azul_css::props::style::text::StyleInitialLetterAlign::Alphabetic => text3::cache::InitialLetterAlign::Alphabetic,
                azul_css::props::style::text::StyleInitialLetterAlign::Hanging => text3::cache::InitialLetterAlign::Hanging,
                azul_css::props::style::text::StyleInitialLetterAlign::Ideographic => text3::cache::InitialLetterAlign::Ideographic,
            })
    } else {
223996
        text3::cache::InitialLetterAlign::Auto
    };
    // +spec:display-property:5af252 - initial-letter on inline-level box not at line start uses normal
    // +spec:text-alignment-spacing:a17609 - sunken initial letters suppress letter-spacing and justification (not word-spacing) with adjacent content
    // +spec:display-property:68ab22 - initial-letter only applies in IFC (inline-level);
    // float!=none or position!=static causes display to compute to block (BFC), so
    // initial-letter naturally does not apply to those elements
    // +spec:writing-modes:c89d19 - initial-letter block-axis positioning: sink determines block offset
    // +spec:display-property:b67500 - initial-letter size/sink: values other than normal make box an initial letter box (inline-level, in-flow)
    // +spec:display-property:416f27 - initial-letter sink defaults to "drop" (sink = size floored) when omitted
223996
    let initial_letter = if dom_declared & DOM_HAS_INITIAL_LETTER != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_initial_letter(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .map(|il| {
                use std::num::NonZeroUsize;
                let sink = match il.sink {
                    azul_css::corety::OptionU32::Some(s) => s,
                    azul_css::corety::OptionU32::None => il.size, // "drop" assumed: sink = size
                };
                text3::cache::InitialLetter {
                    size: il.size as f32,
                    sink,
                    count: NonZeroUsize::new(1).unwrap(),
                    align: initial_letter_align,
                }
            })
    } else {
223996
        None
    };
    // If initial-letter is set, compute the drop cap exclusion area and add it
    // to the shape exclusions so that text wraps around the enlarged letter.
    // +spec:box-model:d4adf6 - ancestor inline boundaries excluded via geometric exclusion
    // +spec:floats:c5e23f - floats in subsequent lines adjacent to a sunk initial letter must clear it
223996
    if let Some(ref il) = initial_letter {
        let lh_n = line_height_value.inner.normalized();
        let computed_line_height = if lh_n < 0.0 { -lh_n } else { lh_n * font_size };
        let (letter_w, letter_h) = layout_initial_letter(
            il.size,
            il.sink,
            constraints.available_size.width,
            computed_line_height,
        );
        if letter_w > 0.0 && letter_h > 0.0 {
            // Place the exclusion at the inline-start (x=0, y=0 relative to the IFC).
            // This creates a rectangular exclusion that text flows around.
            shape_exclusions.push(ShapeBoundary::Rectangle(text3::cache::Rect {
                x: 0.0,
                y: 0.0,
                width: letter_w,
                height: letter_h,
            }));
        }
223996
    }
    // Get line-clamp for limiting visible lines
223996
    let line_clamp = if dom_declared & DOM_HAS_LINE_CLAMP != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_line_clamp(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .and_then(|lc| std::num::NonZeroUsize::new(lc.max_lines))
    } else {
223996
        None
    };
    // Get hanging-punctuation for hanging punctuation marks
223996
    let hanging_punctuation = if dom_declared & DOM_HAS_HANGING_PUNCTUATION != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_hanging_punctuation(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .is_some_and(azul_css::props::style::StyleHangingPunctuation::is_enabled)
    } else {
223996
        false
    };
    // Get text-combine-upright for vertical text combination
    // +spec:line-breaking:9f150a - text-combine-upright:all composes glyphs horizontally, ignoring letter-spacing and forced line breaks
    // +spec:line-breaking:1b88cd - text-combine-upright:all layout: inline-block with 1em square, ignoring forced line breaks
    // +spec:inline-formatting-context:c8d8d9 - text-combine-upright compression passed to text shaping engine
    // +spec:inline-formatting-context:f4ef7d - text-combine-upright layout rules (1em square composition)
223996
    let text_combine_upright = if dom_declared & DOM_HAS_TEXT_COMBINE_UPRIGHT != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_text_combine_upright(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            // +spec:display-property:6f174d - text-combine-upright horizontal-in-vertical composition
            .map(|tcu| match tcu {
                StyleTextCombineUpright::None => text3::cache::TextCombineUpright::None,
                StyleTextCombineUpright::All => text3::cache::TextCombineUpright::All,
                StyleTextCombineUpright::Digits(n) => text3::cache::TextCombineUpright::Digits(*n),
            })
    } else {
223996
        None
    };
    // Get exclusion-margin (CSS Exclusions L1) and shape-margin (CSS Shapes L1)
    // for shape exclusions. We sum both into a single margin knob — strictly,
    // they apply to different sources (exclusion-margin → CSS Exclusions,
    // shape-margin → shape-outside), but the layout solver currently keeps
    // a single per-IFC margin value, so the two get added.
223996
    let exclusion_margin_base = if dom_declared & DOM_HAS_EXCLUSION_MARGIN != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_exclusion_margin(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .map_or(0.0, |em| em.inner.get())
    } else {
223996
        0.0
    };
223996
    let shape_margin = if dom_declared & DOM_HAS_SHAPE_MARGIN != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_shape_margin(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .map_or(0.0, |sm| sm.inner.number.get())
    } else {
223996
        0.0
    };
223996
    let exclusion_margin = exclusion_margin_base + shape_margin;
    // Get hyphenation-language for language-specific hyphenation
223996
    let hyphenation_language = if dom_declared & DOM_HAS_HYPHENATION_LANGUAGE != 0 {
        styled_dom
            .css_property_cache
            .ptr
            .get_hyphenation_language(node_data, &id, node_state)
            .and_then(|s| s.get_property())
            .and_then(|hl| {
                #[cfg(feature = "text_layout_hyphenation")]
                {
                    use hyphenation::{Language, Load};
                    // Parse BCP 47 language code to hyphenation::Language
                    match hl.inner.as_str() {
                        "en-US" | "en" => Some(Language::EnglishUS),
                        "de-DE" | "de" => Some(Language::German1996),
                        "fr-FR" | "fr" => Some(Language::French),
                        "es-ES" | "es" => Some(Language::Spanish),
                        "it-IT" | "it" => Some(Language::Italian),
                        "pt-PT" | "pt" => Some(Language::Portuguese),
                        "nl-NL" | "nl" => Some(Language::Dutch),
                        "pl-PL" | "pl" => Some(Language::Polish),
                        "ru-RU" | "ru" => Some(Language::Russian),
                        "zh-CN" | "zh" => Some(Language::Chinese),
                        _ => None, // Unsupported language
                    }
                }
                #[cfg(not(feature = "text_layout_hyphenation"))]
                {
                    None::<crate::text3::script::Language>
                }
            })
    } else {
223996
        None
    };
    UnifiedConstraints {
223996
        exclusion_margin,
223996
        hyphenation_language,
223996
        text_indent,
223996
        text_indent_each_line,
223996
        text_indent_hanging,
223996
        initial_letter,
223996
        line_clamp,
223996
        columns,
223996
        column_gap,
223996
        hanging_punctuation,
223996
        text_wrap,
223996
        white_space_mode,
223996
        text_combine_upright,
223996
        segment_alignment: SegmentAlignment::Total,
223996
        overflow: match overflow_behaviour {
223968
            LayoutOverflow::Visible => text3::cache::OverflowBehavior::Visible,
10
            LayoutOverflow::Hidden | LayoutOverflow::Clip => text3::cache::OverflowBehavior::Hidden,
18
            LayoutOverflow::Scroll => text3::cache::OverflowBehavior::Scroll,
            LayoutOverflow::Auto => text3::cache::OverflowBehavior::Auto,
        },
        // Use the semantic available_width_type directly instead of converting from float.
        // This preserves MinContent/MaxContent semantics for intrinsic sizing.
223996
        available_width: constraints.available_width_type,
        // Height only constrains line PRODUCTION where it is semantically
        // load-bearing: multi-column balancing ("column full → next column").
        // Everywhere else the continuous IFC lays out ALL its lines and the
        // true content height flows into overflow_size — in CSS, inline
        // content is never truncated by available height at layout time
        // (overflow is a paint/scroll concern, and nothing consumes
        // text3's remaining_items on this path, so truncated lines were
        // silently LOST). The old `Some(available_size.height)` arm let any
        // 0-height measure pass produce a ZERO-LINE layout that the
        // width+content-keyed caches then served to the real pass — the
        // miniword estimator measured multi-line paragraphs as 0.0 px
        // depending on one leading whitespace character shifting which
        // pass primed the cache.
223996
        available_height: if columns > 1 {
            Some(constraints.available_size.height)
        } else {
223996
            None
        },
223996
        shape_boundaries, // CSS shape-inside: text flows within shape
223996
        shape_exclusions, // CSS shape-outside + floats: text wraps around shapes
223996
        writing_mode: Some(match writing_mode {
223987
            LayoutWritingMode::HorizontalTb => text3::cache::WritingMode::HorizontalTb,
9
            LayoutWritingMode::VerticalRl => text3::cache::WritingMode::VerticalRl,
            LayoutWritingMode::VerticalLr => text3::cache::WritingMode::VerticalLr,
        }),
223996
        direction, // Use the CSS direction property (currently defaulting to LTR)
223996
        unicode_bidi: unicode_bidi_val,
        // +spec:overflow:7ff7d1 - hyphens property: none/manual/auto hyphenation control
223996
        hyphenation: match hyphenation {
            StyleHyphens::None => text3::cache::Hyphens::None,
223996
            StyleHyphens::Manual => text3::cache::Hyphens::Manual,
            StyleHyphens::Auto => text3::cache::Hyphens::Auto,
        },
223996
        text_orientation,
        // +spec:text-alignment-spacing:6cb965 - text-align shorthand sets text-align-all (mapped here from computed value)
        // +spec:text-alignment-spacing:838967 - map text-align values (start/end/left/right/center/justify) to inline alignment
        // +spec:text-alignment-spacing:d9ea45 - property index: text-align, text-justify, letter-spacing mapped to layout
        // +spec:text-alignment-spacing:600fda - text-align values (left/right/center/justify) mapped per CSS Text §6.1
223996
        text_align: match text_align {
185161
            StyleTextAlign::Start => text3::cache::TextAlign::Start,
            StyleTextAlign::End => text3::cache::TextAlign::End,
90
            StyleTextAlign::Left => text3::cache::TextAlign::Left,
            StyleTextAlign::Right => text3::cache::TextAlign::Right,
38727
            StyleTextAlign::Center => text3::cache::TextAlign::Center,
18
            StyleTextAlign::Justify => text3::cache::TextAlign::Justify,
        },
        // +spec:text-alignment-spacing:0ea31d - text-justify inter-word/inter-character/distribute mapped per §6.4
        // +spec:text-alignment-spacing:01244f - text-justify: none disables justification, auto uses inter-word as universal default
223996
        text_justify: match text_justify {
            LayoutTextJustify::None => text3::cache::JustifyContent::None,
            LayoutTextJustify::Auto | LayoutTextJustify::InterWord => {
223996
                text3::cache::JustifyContent::InterWord
            }
            // distribute computes to inter-character
            LayoutTextJustify::InterCharacter | LayoutTextJustify::Distribute => {
                text3::cache::JustifyContent::InterCharacter
            }
        },
        // +spec:line-height:79f3aa - line-height resolved: `normal` uses the font's real
        // metrics (ascent - descent + line_gap), <number>/<percentage> × font-size.
        // When line-height is NOT declared the computed value is `normal`; pass
        // LineHeight::Normal through so text3 resolves it against the run's actual
        // font metrics (CoreText/Chrome parity) instead of a synthetic 1.2 ratio.
        // Negative normalized() = absolute px value (convention from parser for "50px" etc.)
223996
        line_height: if dom_declared & DOM_HAS_LINE_HEIGHT == 0 {
210240
            text3::cache::LineHeight::Normal
        } else {
            text3::cache::LineHeight::Px({
13756
                let n = line_height_value.inner.normalized();
13756
                if n < 0.0 { -n } else { n * font_size }
            })
        },
        // Strut metrics for the container's first available font, approximated as
        // 80%/20%/50% of font_size (typical Latin ratios).
        // TODO(superplan): use the resolved primary font's real OS/2 metrics
        // (`ParsedFontTrait::get_font_metrics` → ascent/descent/x_height scaled by
        // units_per_em) and `get_space_width` for `ch_width`. The font is not
        // resolved here: picking the element's primary `ParsedFont` requires the
        // font-chain machinery in `getters::resolve_font_chains` (font-family →
        // fc_cache → loaded font), which isn't threaded into this function. The
        // strut only sizes empty / whitespace-only lines — non-empty runs already
        // use each run's real font metrics during shaping in text3.
223996
        strut_ascent: font_size * 0.8,
223996
        strut_descent: font_size * 0.2,
223996
        strut_x_height: font_size * 0.5, // 0.5em fallback per CSS Inline 3 Appendix A
        // Typical Latin cap ratio, same approximation spirit as the rest of
        // the strut block (Appendix A.2's formal fallback is "ascent", which
        // would make cap-edge trimming a no-op; 0.7em keeps it meaningful
        // until real OS/2 metrics are threaded here - see the TODO above).
223996
        strut_cap_height: font_size * 0.7,
223996
        ch_width: font_size * 0.5,
223996
        vertical_align,
        // +spec:inline-formatting-context:48ce44 - overflow-wrap property: break at otherwise disallowed points to prevent overflow
        // +spec:line-breaking:bbb5f7 - overflow-wrap: anywhere vs break-word distinction for min-content
223996
        overflow_wrap: if word_break_css == StyleWordBreak::BreakWord {
            // +spec:line-breaking:815882 - break-word forces overflow-wrap: anywhere
            text3::cache::OverflowWrap::Anywhere
        } else {
223996
            match overflow_wrap_css {
223978
                StyleOverflowWrap::Normal => text3::cache::OverflowWrap::Normal,
                StyleOverflowWrap::Anywhere => text3::cache::OverflowWrap::Anywhere,
18
                StyleOverflowWrap::BreakWord => text3::cache::OverflowWrap::BreakWord,
            }
        },
223996
        text_align_last: match text_align_last_css {
223996
            StyleTextAlignLast::Auto => text3::cache::TextAlign::default(),
            StyleTextAlignLast::Start => text3::cache::TextAlign::Start,
            StyleTextAlignLast::End => text3::cache::TextAlign::End,
            StyleTextAlignLast::Left => text3::cache::TextAlign::Left,
            StyleTextAlignLast::Right => text3::cache::TextAlign::Right,
            StyleTextAlignLast::Center => text3::cache::TextAlign::Center,
            StyleTextAlignLast::Justify => text3::cache::TextAlign::Justify,
        },
        // +spec:line-breaking:815882 - word-break: break-word => normal + overflow-wrap: anywhere
223996
        word_break: match word_break_css {
223996
            StyleWordBreak::Normal | StyleWordBreak::BreakWord => text3::cache::WordBreak::Normal,
            StyleWordBreak::BreakAll => text3::cache::WordBreak::BreakAll,
            StyleWordBreak::KeepAll => text3::cache::WordBreak::KeepAll,
        },
        // +spec:white-space-processing:bc5f7b - line-break with break-spaces allows breaking before first space
        // CSS Text Level 3 §5.3: The line-break property affects preserved white space behavior:
        // - normal/pre-line: preserved white space at end/start of line is discarded
        // - nowrap/pre: wrapping is forbidden altogether
        // - pre-wrap: preserved white space hangs
        // - break-spaces: allows breaking before first space of a sequence
        // break-spaces allows wrapping preserved spaces to next line; for other white-space values,
        // preserved spaces at line ends are either discarded (normal, pre-line), wrapping is
        // forbidden (nowrap, pre), or they hang (pre-wrap).
223996
        line_break: match line_break_css {
223996
            StyleLineBreak::Auto => text3::cache::LineBreakStrictness::Auto,
            StyleLineBreak::Loose => text3::cache::LineBreakStrictness::Loose,
            StyleLineBreak::Normal => text3::cache::LineBreakStrictness::Normal,
            StyleLineBreak::Strict => text3::cache::LineBreakStrictness::Strict,
            StyleLineBreak::Anywhere => text3::cache::LineBreakStrictness::Anywhere,
        },
    }
223996
}
// Table Formatting Context (CSS 2.2 § 17)
// +spec:display-property:d887c0 - Table wrapper box BFC, caption-side, table grid layout (§17.4-17.5)
// +spec:positioning:930891 - Table formatting context implementation (CSS 2.2 § 17 introduction)
// +spec:inline-formatting-context:9c272d - CSS table model: row-primary structure, display-to-table-element mapping, visual formatting as rectangular grid
/// Lays out a Table Formatting Context.
/// Table column information for layout calculations
#[derive(Copy, Debug, Clone)]
pub struct TableColumnInfo {
    /// Minimum width required for this column
    pub min_width: f32,
    /// Maximum width desired for this column
    pub max_width: f32,
    /// Computed final width for this column
    pub computed_width: Option<f32>,
}
/// Information about a table cell for layout
#[derive(Copy, Debug, Clone)]
pub struct TableCellInfo {
    /// Node index in the layout tree
    pub node_index: usize,
    /// Column index (0-based)
    pub column: usize,
    /// Number of columns this cell spans
    pub colspan: usize,
    /// Row index (0-based)
    pub row: usize,
    /// Number of rows this cell spans
    pub rowspan: usize,
}
/// Table layout context - holds all information needed for table layout
#[derive(Debug)]
struct TableLayoutContext {
    /// Information about each column
    columns: Vec<TableColumnInfo>,
    /// Information about each cell
    cells: Vec<TableCellInfo>,
    /// Number of rows in the table
    num_rows: usize,
    /// Whether to use fixed or auto layout algorithm
    use_fixed_layout: bool,
    /// Computed height for each row
    row_heights: Vec<f32>,
    /// Computed baseline offset for each row (distance from row top to row baseline)
    row_baselines: Vec<f32>,
    // +spec:inline-formatting-context:440ca9 - border-collapse/border-spacing/visibility:collapse table properties (CSS 2.2 §17.5-17.6)
    /// Border collapse mode
    border_collapse: StyleBorderCollapse,
    /// Border spacing (only used when `border_collapse` is Separate)
    border_spacing: LayoutBorderSpacing,
    /// CSS 2.2 Section 17.4: Index of table-caption child, if any
    caption_index: Option<usize>,
    //   from display without forcing table re-layout
    /// CSS 2.2 Section 17.6: Rows with visibility:collapse (dynamic effects)
    /// Set of row indices that have visibility:collapse
    collapsed_rows: std::collections::HashSet<usize>,
    /// CSS 2.2 Section 17.6: Columns with visibility:collapse (dynamic effects)
    /// Set of column indices that have visibility:collapse
    collapsed_columns: std::collections::HashSet<usize>,
    /// Rows that are hidden-empty (zero height, border-spacing on only one side)
    hidden_empty_rows: std::collections::HashSet<usize>,
    /// Layout tree indices for each row (row index → layout node index)
    row_node_indices: Vec<usize>,
    /// Per-column rowspan occupancy: for column `c`, the number of upcoming rows
    /// (including the current one during processing) still covered by a cell that
    /// began in an earlier row with rowspan > 1. Decremented after each row.
    /// Used so a later row's cells skip columns already taken by a spanning cell.
    col_occupied: Vec<usize>,
}
impl TableLayoutContext {
334
    fn new() -> Self {
334
        Self {
334
            columns: Vec::new(),
334
            cells: Vec::new(),
334
            num_rows: 0,
334
            use_fixed_layout: false,
334
            row_heights: Vec::new(),
334
            row_baselines: Vec::new(),
334
            border_collapse: StyleBorderCollapse::Separate,
334
            border_spacing: LayoutBorderSpacing::default(),
334
            caption_index: None,
334
            collapsed_rows: std::collections::HashSet::new(),
334
            collapsed_columns: std::collections::HashSet::new(),
334
            hidden_empty_rows: std::collections::HashSet::new(),
334
            row_node_indices: Vec::new(),
334
            col_occupied: Vec::new(),
334
        }
334
    }
}
// +spec:table-layout:485791 - Six superimposed table layers: table, column-group, column, row-group, row, cell (bottom to top)
// +spec:table-layout:dcdf1b - Collapsing border model: border conflict resolution uses layer priority (cell > row > row-group > column > column-group > table)
/// Source of a border in the border conflict resolution algorithm
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BorderSource {
    Table = 0,
    ColumnGroup = 1,
    Column = 2,
    RowGroup = 3,
    Row = 4,
    Cell = 5,
}
/// Information about a border for conflict resolution
#[derive(Copy, Debug, Clone)]
pub struct BorderInfo {
    pub width: f32,
    pub style: BorderStyle,
    pub color: ColorU,
    pub source: BorderSource,
}
impl BorderInfo {
1405
    #[must_use] pub const fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
1405
        Self {
1405
            width,
1405
            style,
1405
            color,
1405
            source,
1405
        }
1405
    }
    // +spec:block-formatting-context:f772ae - border style priority for table border conflict resolution
    /// Get the priority of a border style for conflict resolution
    /// Higher number = higher priority
442
    #[must_use] pub const fn style_priority(style: &BorderStyle) -> u8 {
442
        match style {
1
            BorderStyle::Hidden => 255, // Highest - suppresses all borders
1
            BorderStyle::None => 0,     // Lowest - loses to everything
74
            BorderStyle::Double => 8,
237
            BorderStyle::Solid => 7,
29
            BorderStyle::Dashed => 6,
21
            BorderStyle::Dotted => 5,
20
            BorderStyle::Ridge => 4,
20
            BorderStyle::Outset => 3,
20
            BorderStyle::Groove => 2,
19
            BorderStyle::Inset => 1,
        }
442
    }
    // +spec:box-model:2255c2 - Collapsing border conflict resolution (hidden wins, then none loses, then wider wins, then style priority)
    // +spec:box-model:b42c79 - border conflict resolution: hidden wins, then wider, then style priority, then source
    // +spec:box-model:503e9e - border conflict resolution: hidden wins, then wider, then style priority, then source priority
    // +spec:box-model:7eb217 - Border conflict resolution: hidden > none < wider > style priority > source priority > left/top
    // +spec:overflow:1fb482 - Border conflict resolution per CSS 2.2 §17.6.2.1 (hidden wins, then wider, then style priority, then source priority)
    // +spec:table-layout:882560 - Border conflict resolution (17.6.2.1): hidden wins, none loses, wider wins, style priority, source priority
    /// Compare two borders for conflict resolution per CSS 2.2 Section 17.6.2.1
    /// Returns the winning border
    // +spec:table-layout:21053b - border conflict resolution: hidden suppresses all, style priorities
    // +spec:table-layout:076617 - border conflict resolution algorithm and border style semantics in collapsing model
652
    #[must_use] pub fn resolve_conflict(a: &Self, b: &Self) -> Option<Self> {
        // 1. 'hidden' wins and suppresses all borders
652
        if a.style == BorderStyle::Hidden || b.style == BorderStyle::Hidden {
29
            return None;
623
        }
        // 2. Filter out 'none' - if both are none, no border
623
        let a_is_none = a.style == BorderStyle::None;
623
        let b_is_none = b.style == BorderStyle::None;
623
        if a_is_none && b_is_none {
100
            return None;
523
        }
523
        if a_is_none {
10
            return Some(*b);
513
        }
513
        if b_is_none {
235
            return Some(*a);
278
        }
        // 3. Wider border wins
278
        if a.width > b.width {
20
            return Some(*a);
258
        }
258
        if b.width > a.width {
45
            return Some(*b);
213
        }
        // 4. If same width, compare style priority
213
        let a_priority = Self::style_priority(&a.style);
213
        let b_priority = Self::style_priority(&b.style);
213
        if a_priority > b_priority {
99
            return Some(*a);
114
        }
114
        if b_priority > a_priority {
10
            return Some(*b);
104
        }
        // 5. If same style, source priority:
        // Cell > Row > RowGroup > Column > ColumnGroup > Table
104
        if a.source > b.source {
64
            return Some(*a);
40
        }
40
        if b.source > a.source {
2
            return Some(*b);
38
        }
        // 6. Same priority - prefer first one (left/top in LTR)
38
        Some(*a)
652
    }
}
/// Get border information for a node
#[allow(clippy::similar_names)] // domain-standard coordinate/geometry/short-lived names
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
522
pub(crate) fn get_border_info<T: ParsedFontTrait>(
522
    ctx: &LayoutContext<'_, T>,
522
    node: &LayoutNodeHot,
522
    source: BorderSource,
522
) -> (BorderInfo, BorderInfo, BorderInfo, BorderInfo) {
    use azul_css::props::{
        basic::{
            pixel::{PhysicalSize, PropertyContext, ResolutionContext},
            ColorU,
        },
        style::BorderStyle,
    };
    use get_element_font_size;
    use get_parent_font_size;
    use get_root_font_size;
522
    let default_border = BorderInfo::new(
        0.0,
522
        BorderStyle::None,
522
        ColorU {
522
            r: 0,
522
            g: 0,
522
            b: 0,
522
            a: 0,
522
        },
522
        source,
    );
522
    let Some(dom_id) = node.dom_node_id else {
        return (
            default_border,
            default_border,
            default_border,
            default_border,
        );
    };
522
    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
522
    let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
522
    let cache = &ctx.styled_dom.css_property_cache.ptr;
    // FAST PATH: compact cache for normal state
522
    if let Some(ref cc) = cache.compact_cache {
522
        let idx = dom_id.index();
        // Border styles from packed u16
522
        let bts = cc.get_border_top_style(idx);
522
        let brs = cc.get_border_right_style(idx);
522
        let bbs = cc.get_border_bottom_style(idx);
522
        let bls = cc.get_border_left_style(idx);
        // Border colors from u32 RGBA
2088
        let make_color = |raw: u32| -> ColorU {
2088
            if raw == 0 {
1224
                ColorU { r: 0, g: 0, b: 0, a: 0 }
            } else {
864
                ColorU {
864
                    r: ((raw >> 24) & 0xFF) as u8,
864
                    g: ((raw >> 16) & 0xFF) as u8,
864
                    b: ((raw >> 8) & 0xFF) as u8,
864
                    a: (raw & 0xFF) as u8,
864
                }
            }
2088
        };
522
        let btc = make_color(cc.get_border_top_color_raw(idx));
522
        let brc = make_color(cc.get_border_right_color_raw(idx));
522
        let bbc = make_color(cc.get_border_bottom_color_raw(idx));
522
        let blc = make_color(cc.get_border_left_color_raw(idx));
        // Border widths from i16 × 10
2088
        let decode_width = |raw: i16| -> f32 {
2088
            if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
                0.0 // sentinel → fall back to 0
            } else {
2088
                f32::from(raw) / 10.0
            }
2088
        };
522
        let btw = decode_width(cc.get_border_top_width_raw(idx));
522
        let brw = decode_width(cc.get_border_right_width_raw(idx));
522
        let bbw = decode_width(cc.get_border_bottom_width_raw(idx));
522
        let blw = decode_width(cc.get_border_left_width_raw(idx));
522
        let top = if bts == BorderStyle::None { default_border }
216
            else { BorderInfo::new(btw, bts, btc, source) };
522
        let right = if brs == BorderStyle::None { default_border }
216
            else { BorderInfo::new(brw, brs, brc, source) };
522
        let bottom = if bbs == BorderStyle::None { default_border }
216
            else { BorderInfo::new(bbw, bbs, bbc, source) };
522
        let left = if bls == BorderStyle::None { default_border }
216
            else { BorderInfo::new(blw, bls, blc, source) };
522
        return (top, right, bottom, left);
    }
    // SLOW PATH: full cascade resolution
    let cache = &ctx.styled_dom.css_property_cache.ptr;
    // Create resolution context for border-width (em/rem support, no % support)
    let element_font_size = get_element_font_size(ctx.styled_dom, dom_id, &node_state);
    let parent_font_size = get_parent_font_size(ctx.styled_dom, dom_id, &node_state);
    let root_font_size = get_root_font_size(ctx.styled_dom, &node_state);
    let resolution_context = ResolutionContext {
        vertical_writing_mode: false,
        element_font_size,
        parent_font_size,
        root_font_size,
        // Not used for border-width
        containing_block_size: PhysicalSize::new(0.0, 0.0),
        // Not used for border-width
        element_size: None,
        viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
    };
    // Top border
    let top = cache
        .get_border_top_style(node_data, &dom_id, &node_state)
        .and_then(|s| s.get_property())
        .map_or_else(|| default_border, |style_val| {
            let width = cache
                .get_border_top_width(node_data, &dom_id, &node_state)
                .and_then(|w| w.get_property())
                .map_or(0.0, |w| {
                    w.inner
                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
                });
            let color = cache
                .get_border_top_color(node_data, &dom_id, &node_state)
                .and_then(|c| c.get_property())
                .map_or(ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                }, |c| c.inner);
            BorderInfo::new(width, style_val.inner, color, source)
        });
    // Right border
    let right = cache
        .get_border_right_style(node_data, &dom_id, &node_state)
        .and_then(|s| s.get_property())
        .map_or_else(|| default_border, |style_val| {
            let width = cache
                .get_border_right_width(node_data, &dom_id, &node_state)
                .and_then(|w| w.get_property())
                .map_or(0.0, |w| {
                    w.inner
                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
                });
            let color = cache
                .get_border_right_color(node_data, &dom_id, &node_state)
                .and_then(|c| c.get_property())
                .map_or(ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                }, |c| c.inner);
            BorderInfo::new(width, style_val.inner, color, source)
        });
    // Bottom border
    let bottom = cache
        .get_border_bottom_style(node_data, &dom_id, &node_state)
        .and_then(|s| s.get_property())
        .map_or_else(|| default_border, |style_val| {
            let width = cache
                .get_border_bottom_width(node_data, &dom_id, &node_state)
                .and_then(|w| w.get_property())
                .map_or(0.0, |w| {
                    w.inner
                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
                });
            let color = cache
                .get_border_bottom_color(node_data, &dom_id, &node_state)
                .and_then(|c| c.get_property())
                .map_or(ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                }, |c| c.inner);
            BorderInfo::new(width, style_val.inner, color, source)
        });
    // Left border
    let left = cache
        .get_border_left_style(node_data, &dom_id, &node_state)
        .and_then(|s| s.get_property())
        .map_or_else(|| default_border, |style_val| {
            let width = cache
                .get_border_left_width(node_data, &dom_id, &node_state)
                .and_then(|w| w.get_property())
                .map_or(0.0, |w| {
                    w.inner
                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
                });
            let color = cache
                .get_border_left_color(node_data, &dom_id, &node_state)
                .and_then(|c| c.get_property())
                .map_or(ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                }, |c| c.inner);
            BorderInfo::new(width, style_val.inner, color, source)
        });
    (top, right, bottom, left)
522
}
// +spec:table-layout:c5e446 - table-layout property (auto|fixed) controls layout algorithm selection
/// Get the table-layout property for a table node
333
fn get_table_layout_property<T: ParsedFontTrait>(
333
    ctx: &LayoutContext<'_, T>,
333
    node: &LayoutNodeHot,
333
) -> LayoutTableLayout {
333
    let Some(dom_id) = node.dom_node_id else {
        return LayoutTableLayout::Auto;
    };
333
    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
333
    let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
333
    ctx.styled_dom
333
        .css_property_cache
333
        .ptr
333
        .get_table_layout(node_data, &dom_id, &node_state)
333
        .and_then(|prop| prop.get_property().copied())
333
        .unwrap_or(LayoutTableLayout::Auto)
333
}
/// Get the border-collapse property for a table node
333
fn get_border_collapse_property<T: ParsedFontTrait>(
333
    ctx: &LayoutContext<'_, T>,
333
    node: &LayoutNodeHot,
333
) -> StyleBorderCollapse {
333
    let Some(dom_id) = node.dom_node_id else {
        return StyleBorderCollapse::Separate;
    };
    // FAST PATH: compact cache
333
    if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
333
        return cc.get_border_collapse(dom_id.index());
    }
    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
    let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
    ctx.styled_dom
        .css_property_cache
        .ptr
        .get_border_collapse(node_data, &dom_id, &node_state)
        .and_then(|prop| prop.get_property().copied())
        .unwrap_or(StyleBorderCollapse::Separate)
333
}
/// Get the border-spacing property for a table node
333
fn get_border_spacing_property<T: ParsedFontTrait>(
333
    ctx: &LayoutContext<'_, T>,
333
    node: &LayoutNodeHot,
333
) -> LayoutBorderSpacing {
333
    if let Some(dom_id) = node.dom_node_id {
        // FAST PATH: compact cache
333
        if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
333
            let idx = dom_id.index();
333
            let h_raw = cc.get_border_spacing_h_raw(idx);
333
            let v_raw = cc.get_border_spacing_v_raw(idx);
            // If both are non-sentinel, use compact values
333
            if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
333
                && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
            {
333
                return LayoutBorderSpacing::new_separate(
333
                    azul_css::props::basic::pixel::PixelValue::px(f32::from(h_raw) / 10.0),
333
                    azul_css::props::basic::pixel::PixelValue::px(f32::from(v_raw) / 10.0),
                );
            }
            // sentinel → fall through to slow path
        }
        let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
        let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
        if let Some(prop) = ctx.styled_dom.css_property_cache.ptr.get_border_spacing(
            node_data,
            &dom_id,
            &node_state,
        ) {
            if let Some(value) = prop.get_property() {
                return *value;
            }
        }
    }
    LayoutBorderSpacing::default() // Default: 0
333
}
/// Get the empty-cells property for a table-cell node.
/// Returns Show (default) or Hide.
306
fn get_empty_cells_property<T: ParsedFontTrait>(
306
    ctx: &LayoutContext<'_, T>,
306
    node: &LayoutNodeHot,
306
) -> StyleEmptyCells {
306
    let Some(dom_id) = node.dom_node_id else {
        return StyleEmptyCells::Show;
    };
306
    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
306
    let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
306
    ctx.styled_dom
306
        .css_property_cache
306
        .ptr
306
        .get_empty_cells(node_data, &dom_id, &node_state)
306
        .and_then(|prop| prop.get_property().copied())
306
        .unwrap_or(StyleEmptyCells::Show)
306
}
/// CSS 2.2 Section 17.4 - Tables in the visual formatting model:
///
/// "The caption box is a block box that retains its own content, padding,
/// border, and margin areas. The caption-side property specifies the position
/// of the caption box with respect to the table box."
///
/// Get the caption-side property for a table node.
/// Returns Top (default) or Bottom.
333
fn get_caption_side_property<T: ParsedFontTrait>(
333
    ctx: &LayoutContext<'_, T>,
333
    node: &LayoutNodeHot,
333
) -> StyleCaptionSide {
333
    if let Some(dom_id) = node.dom_node_id {
333
        let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
333
        let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
        if let Some(prop) =
333
            ctx.styled_dom
333
                .css_property_cache
333
                .ptr
333
                .get_caption_side(node_data, &dom_id, &node_state)
        {
            if let Some(value) = prop.get_property() {
                return *value;
            }
333
        }
    }
333
    StyleCaptionSide::Top // Default per CSS 2.2
333
}
//   removes entire row or column from display; space made available for other content;
//   spanned content clipped; does not otherwise affect table layout
// +spec:inline-formatting-context:9f5f31 - visibility:collapse for table rows/columns, border-collapse and border-spacing
/// CSS 2.2 Section 17.6 - Dynamic row and column effects:
///
// +spec:box-model:547563 - visibility:collapse removes table rows/columns; elsewhere same as hidden
/// "The 'visibility' value 'collapse' removes a row or column from display,
/// but it has a different effect than 'visibility: hidden' on other elements.
/// When a row or column is collapsed, the space normally occupied by the row
/// or column is removed."
///
/// Check if a node has visibility:collapse set.
///
/// This is used for table rows and columns to optimize dynamic hiding.
/// // +spec:overflow:ebb1f9 - For non-table elements, collapse == hidden (no special handling needed)
351
fn is_visibility_collapsed<T: ParsedFontTrait>(
351
    ctx: &LayoutContext<'_, T>,
351
    node: &LayoutNodeHot,
351
) -> bool {
351
    if let Some(dom_id) = node.dom_node_id {
351
        let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
351
        if let MultiValue::Exact(value) = get_visibility(ctx.styled_dom, dom_id, &node_state) {
351
            return matches!(value, StyleVisibility::Collapse);
        }
    }
    false
351
}
// +spec:overflow:af97a8 - empty-cells in separated borders model; collapsing border overflow
// +spec:table-layout:dcdf1b - empty-cells property controls rendering of borders/backgrounds around empty cells in separated borders model
/// CSS 2.2 Section 17.6.1.1 - Borders and Backgrounds around empty cells
///
/// In the separated borders model, the 'empty-cells' property controls the rendering of
/// borders and backgrounds around cells that have no visible content. Empty means it has no
/// children, or has children that are only collapsed whitespace."
///
/// Check if a table cell is empty (has no visible content).
///
/// This is used by the rendering pipeline to decide whether to paint borders/backgrounds
/// when empty-cells: hide is set in separated border model.
///
//   in-flow content (including empty elements) other than collapsed whitespace
/// A cell is considered empty if:
///
/// - It has no children, OR
/// - It has children but no `inline_layout_result` (no rendered content)
///
/// Note: Full whitespace detection would require checking text content during rendering.
/// This function provides a basic check suitable for layout phase.
5
fn is_cell_empty(tree: &LayoutTree, cell_index: usize) -> bool {
5
    if tree.get(LayoutNodeId::new(cell_index)).is_none() {
2
        return true; // Invalid cell is considered empty
3
    }
    // No children = empty
3
    if tree.children(cell_index).is_empty() {
1
        return true;
2
    }
    // If cell has an inline layout result, check if it's empty
2
    if let Some(warm_node) = tree.warm(LayoutNodeId::new(cell_index)) {
2
        if let Some(ref cached_layout) = warm_node.inline_layout_result {
            // Check if inline layout has any rendered content
            // Empty inline layouts have no items (glyphs/fragments)
            // Note: This is a heuristic - full detection requires text content analysis
            // (d6h) Dense-first: the stored sparse may be the retirement
            // sentinel (empty) while the dense view carries the content.
1
            if let Some(d) = cached_layout.dense.as_deref() {
1
                return d.clusters.is_empty();
            }
            return cached_layout.layout.items.is_empty();
1
        }
    }
    // Check if all children have no content
    // A more thorough check would recursively examine all descendants
    //
    // For now, we use a simple heuristic: if there are children, assume not empty
    // unless proven otherwise by inline_layout_result
    // Cell with children but no inline layout = likely has block-level content = not empty
1
    false
5
}
/// Main function to layout a table formatting context
// +spec:table-layout:235e8e - CSS 2.2 §17.1-17.2 table model: fixed/auto algorithms, row/column/cell/caption structure
// +spec:table-layout:a6422d - CSS table model: table structure analysis, row/column/cell layout, caption, border-collapse
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
/// # Panics
///
/// Panics if the table's root node has no associated DOM node id.
/// # Errors
///
/// Returns a `LayoutError` if laying out the table fails.
333
pub fn layout_table_fc<T: ParsedFontTrait>(
333
    ctx: &mut LayoutContext<'_, T>,
333
    tree: &mut LayoutTree,
333
    text_cache: &mut TextLayoutCache,
333
    node_index: usize,
333
    constraints: &LayoutConstraints<'_>,
333
) -> Result<LayoutOutput> {
333
    debug_log!(ctx, "Laying out table");
333
    debug_table_layout!(
333
        ctx,
333
        "node_index={}, available_size={:?}, writing_mode={:?}",
        node_index,
        constraints.available_size,
        constraints.writing_mode
    );
    // Multi-pass table layout algorithm:
    //
    // 1. Analyze table structure - identify rows, cells, columns
    // 2. Determine table-layout property (fixed vs auto)
    // 3. Calculate column widths
    // 4. Layout cells and calculate row heights
    // 5. Position cells in final grid
    // Get the table node to read CSS properties
333
    let table_node = tree
333
        .get(LayoutNodeId::new(node_index))
333
        .ok_or(LayoutError::InvalidTree)?
333
        .clone();
    // Calculate the table's border-box width for column distribution
    // This accounts for the table's own width property (e.g., width: 100%)
333
    let table_border_box_width = if let Some(dom_id) = table_node.dom_node_id {
        // Use calculate_used_size_for_node to resolve table width (respects width:100%)
333
        let intrinsic = tree.warm(LayoutNodeId::new(node_index)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
333
        let containing_block_size = LogicalSize {
333
            width: constraints.available_size.width,
333
            height: constraints.available_size.height,
333
        };
333
        let table_bp = table_node.box_props.unpack();
333
        let table_size = crate::solver3::sizing::calculate_used_size_for_node(
333
            ctx.styled_dom,
333
            Some(dom_id),
333
            &containing_block_size,
333
            intrinsic,
333
            &table_bp,
333
            &ctx.viewport_size,
        )?;
333
        table_size.width
    } else {
        constraints.available_size.width
    };
    // Subtract padding and border to get content-box width for column distribution
333
    let tbp = table_node.box_props.unpack();
333
    let table_content_box_width = {
333
        let padding_width = tbp.padding.left + tbp.padding.right;
333
        let border_width = tbp.border.left + tbp.border.right;
333
        (table_border_box_width - padding_width - border_width).max(0.0)
    };
333
    debug_table_layout!(ctx, "Table Layout Debug");
333
    debug_table_layout!(ctx, "Node index: {}", node_index);
333
    debug_table_layout!(
333
        ctx,
333
        "Available size from parent: {:.2} x {:.2}",
        constraints.available_size.width,
        constraints.available_size.height
    );
333
    debug_table_layout!(ctx, "Table border-box width: {:.2}", table_border_box_width);
333
    debug_table_layout!(
333
        ctx,
333
        "Table content-box width: {:.2}",
        table_content_box_width
    );
333
    debug_table_layout!(
333
        ctx,
333
        "Table padding: L={:.2} R={:.2}",
        tbp.padding.left,
        tbp.padding.right
    );
333
    debug_table_layout!(
333
        ctx,
333
        "Table border: L={:.2} R={:.2}",
        tbp.border.left,
        tbp.border.right
    );
333
    debug_table_layout!(ctx, "=");
    // Phase 1: Analyze table structure
333
    let mut table_ctx = analyze_table_structure(tree, node_index, ctx)?;
    // +spec:table-layout:ff5671 - table-layout property (fixed vs auto) controls column width algorithm
    // +spec:width-calculation:7a5b23 - table-layout property determines fixed vs auto algorithm (CSS 2.2 §17.5.2)
    // Phase 2: Read CSS properties and determine layout algorithm
333
    let table_layout = get_table_layout_property(ctx, &table_node);
333
    table_ctx.use_fixed_layout = matches!(table_layout, LayoutTableLayout::Fixed);
    // +spec:containing-block:cc1453 - collapsing border model: border-collapse property drives table border handling
    // Read border properties
333
    table_ctx.border_collapse = get_border_collapse_property(ctx, &table_node);
333
    table_ctx.border_spacing = get_border_spacing_property(ctx, &table_node);
333
    debug_log!(
333
        ctx,
333
        "Table layout: {:?}, border-collapse: {:?}, border-spacing: {:?}",
        table_layout,
        table_ctx.border_collapse,
        table_ctx.border_spacing
    );
    // +spec:width-calculation:431d60 - fixed vs auto table layout column width algorithms (CSS 2.2 §17.5.2.1, §17.5.2.2)
    // Phase 3: Calculate column widths
333
    if table_ctx.use_fixed_layout {
        // DEBUG: Log available width passed into fixed column calculation
        debug_table_layout!(
            ctx,
            "FIXED layout: table_content_box_width={:.2}",
            table_content_box_width
        );
        calculate_column_widths_fixed(ctx, tree, &mut table_ctx, table_content_box_width);
    } else {
        // Pass table_content_box_width for column distribution in auto layout
333
        calculate_column_widths_auto_with_width(
333
            &mut table_ctx,
333
            tree,
333
            text_cache,
333
            ctx,
333
            constraints,
333
            table_content_box_width,
        )?;
    }
333
    debug_table_layout!(ctx, "After column width calculation:");
333
    debug_table_layout!(ctx, "  Number of columns: {}", table_ctx.columns.len());
819
    for (i, col) in table_ctx.columns.iter().enumerate() {
819
        debug_table_layout!(
819
            ctx,
819
            "  Column {}: width={:.2}",
            i,
819
            col.computed_width.unwrap_or(0.0)
        );
    }
333
    let total_col_width: f32 = table_ctx
333
        .columns
333
        .iter()
333
        .filter_map(|c| c.computed_width)
333
        .sum();
333
    debug_table_layout!(ctx, "  Total column width: {:.2}", total_col_width);
    // Phase 4: Calculate row heights based on cell content
333
    calculate_row_heights(&mut table_ctx, tree, text_cache, ctx, constraints)?;
    // Phase 5: Position cells in final grid and collect positions
333
    let mut cell_positions =
333
        position_table_cells(&table_ctx, tree, ctx, node_index, constraints)?;
    // Calculate final table size including border-spacing
333
    let mut table_width: f32 = table_ctx
333
        .columns
333
        .iter()
333
        .filter_map(|col| col.computed_width)
333
        .sum();
333
    let mut table_height: f32 = table_ctx.row_heights.iter().sum();
333
    debug_table_layout!(
333
        ctx,
333
        "After calculate_row_heights: table_height={:.2}, row_heights={:?}",
        table_height,
        table_ctx.row_heights
    );
    // +spec:box-model:494f6b - collapsing border model: row-width formula and table border width computation
    // +spec:box-model:e7d0a3 - Separated borders model: border-spacing, empty-cells, collapsing border width calculation
    // +spec:box-sizing:ee702c - separated borders model: border-spacing between adjoining cells
    // Add border-spacing to table size if border-collapse is separate
    // +spec:box-model:acb81f - separated borders model: border-spacing between adjoining cell borders
    // +spec:box-model:e480b1 - table width = left inner padding edge to right inner padding edge (including border-spacing)
333
    if table_ctx.border_collapse == StyleBorderCollapse::Separate {
        use get_element_font_size;
        use get_parent_font_size;
        use get_root_font_size;
        use PhysicalSize;
        use PropertyContext;
        use ResolutionContext;
297
        let styled_dom = ctx.styled_dom;
        // Anonymous table wrapper boxes have no dom_node_id; without a styled
        // node we cannot resolve font-relative border-spacing units, so fall
        // back to zero spacing rather than panicking.
297
        let (h_spacing, v_spacing) = if let Some(table_id) = tree.nodes[node_index].dom_node_id {
297
            let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
297
            let spacing_context = ResolutionContext {
297
                vertical_writing_mode: false,
297
                element_font_size: get_element_font_size(styled_dom, table_id, table_state),
297
                parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
297
                root_font_size: get_root_font_size(styled_dom, table_state),
297
                containing_block_size: PhysicalSize::new(0.0, 0.0),
297
                element_size: None,
297
                viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
297
            };
297
            let h_spacing = table_ctx
297
                .border_spacing
297
                .horizontal
297
                .resolve_with_context(&spacing_context, PropertyContext::Other)
297
                .max(0.0);
297
            let v_spacing = table_ctx
297
                .border_spacing
297
                .vertical
297
                .resolve_with_context(&spacing_context, PropertyContext::Other)
297
                .max(0.0);
297
            (h_spacing, v_spacing)
        } else {
            (0.0f32, 0.0f32)
        };
        // Add spacing: left + (n-1 between columns) + right = n+1 spacings
297
        let num_cols = table_ctx.columns.len();
297
        if num_cols > 0 {
297
            table_width += h_spacing * (num_cols + 1) as f32;
297
        }
        // Add spacing: top + (n-1 between rows) + bottom = n+1 spacings
297
        if table_ctx.num_rows > 0 {
297
            let full_spacings = (table_ctx.num_rows + 1) as f32;
297
            // Each hidden-empty row loses one side of border-spacing
297
            let hidden_empty_count = table_ctx.hidden_empty_rows.len() as f32;
297
            table_height += v_spacing * (full_spacings - hidden_empty_count);
297
        }
36
    }
    // +spec:table-layout:24dbf9 - §17.4 table wrapper box model: caption positioning, BFC establishment
    // +spec:width-calculation:600f98 - caption-side positions caption above/below table box (CSS 2.2 §17.4)
    // CSS 2.2 Section 17.4: Layout and position the caption if present
    //
    // "The caption box is a block box that retains its own content,
    // padding, border, and margin areas."
333
    let caption_side = get_caption_side_property(ctx, &table_node);
333
    let mut caption_height = 0.0;
333
    let mut table_y_offset = 0.0;
333
    if let Some(caption_idx) = table_ctx.caption_index {
        debug_log!(
            ctx,
            "Laying out caption with caption-side: {:?}",
            caption_side
        );
        // Layout caption as a block with the table's width as available width
        let caption_constraints = LayoutConstraints {
            available_size: LogicalSize {
                width: table_width,
                height: constraints.available_size.height,
            },
            writing_mode: constraints.writing_mode,
            writing_mode_ctx: constraints.writing_mode_ctx,
            bfc_state: None, // Caption creates its own BFC
            text_align: constraints.text_align,
            containing_block_size: constraints.containing_block_size,
            available_width_type: Text3AvailableSpace::Definite(table_width),
            fragmentainer: None,
        };
        // Layout the caption node
        let mut empty_float_cache = HashMap::new();
        let caption_result = layout_formatting_context(
            ctx,
            tree,
            text_cache,
            caption_idx,
            &caption_constraints,
            &mut empty_float_cache,
        )?;
        caption_height = caption_result.output.overflow_size.height;
        let caption_position = match caption_side {
            StyleCaptionSide::Top => {
                // Caption on top: position at y=0, table starts below caption
                table_y_offset = caption_height;
                LogicalPosition { x: 0.0, y: 0.0 }
            }
            StyleCaptionSide::Bottom => {
                // Caption on bottom: table starts at y=0, caption below table
                LogicalPosition {
                    x: 0.0,
                    y: table_height,
                }
            }
        };
        // Add caption position to the positions map
        cell_positions.insert(caption_idx, caption_position);
        debug_log!(
            ctx,
            "Caption positioned at x={:.2}, y={:.2}, height={:.2}",
            caption_position.x,
            caption_position.y,
            caption_height
        );
333
    }
    // Adjust all table cell positions if caption is on top
333
    if table_y_offset > 0.0 {
        debug_log!(
            ctx,
            "Adjusting table cells by y offset: {:.2}",
            table_y_offset
        );
        // Adjust cell positions in the map
        for cell_info in &table_ctx.cells {
            if let Some(pos) = cell_positions.get_mut(&cell_info.node_index) {
                pos.y += table_y_offset;
            }
        }
333
    }
333
    let total_height = table_height + caption_height;
333
    debug_table_layout!(ctx, "Final table dimensions:");
333
    debug_table_layout!(ctx, "  Content width (columns): {:.2}", table_width);
333
    debug_table_layout!(ctx, "  Content height (rows): {:.2}", table_height);
333
    debug_table_layout!(ctx, "  Caption height: {:.2}", caption_height);
333
    debug_table_layout!(ctx, "  Total height: {:.2}", total_height);
333
    debug_table_layout!(ctx, "End Table Debug");
    // CSS 2.2 §10.8.1: the baseline of a table is the baseline of its first
    // in-flow row — used when the table is an `inline-table` aligned on a line.
    // `row_baselines[0]` is that row's baseline measured from the row's top;
    // every row-0 cell shares the row top, so add any row-0 cell's (already
    // caption-adjusted) y position. Falls back to `None` for an empty table,
    // where the caller treats the bottom content edge as the baseline.
    // TODO(superplan): a rowspan cell that *starts* in row 0 but whose content
    // baseline sits in a later row is approximated by `row_baselines[0]` here.
333
    let table_baseline = table_ctx
333
        .row_baselines
333
        .first()
333
        .copied()
333
        .and_then(|row0_baseline| {
333
            table_ctx
333
                .cells
333
                .iter()
333
                .find(|c| c.row == 0)
333
                .and_then(|c| cell_positions.get(&c.node_index))
333
                .map(|pos| pos.y + row0_baseline)
333
        });
    // Create output with the table's final size and cell positions
    // +spec:box-model:52fcfe - overflow_size must include borders that spill into margin in collapsing border model
333
    let output = LayoutOutput {
333
        overflow_size: LogicalSize {
333
            width: table_width,
333
            height: total_height,
333
        },
333
        // Cell positions calculated in position_table_cells
333
        positions: cell_positions,
333
        // First in-flow row's baseline (CSS 2.2 §10.8.1); None ⇒ bottom edge.
333
        baseline: table_baseline,
333
    };
333
    Ok(output)
333
}
// +spec:display-property:f47f8a - Table structure analysis: caption positioning, row/column/row-group traversal per CSS 2.2 §17.4-17.5
/// Analyze the table structure to identify rows, cells, and columns
333
fn analyze_table_structure<T: ParsedFontTrait>(
333
    tree: &LayoutTree,
333
    table_index: usize,
333
    ctx: &mut LayoutContext<'_, T>,
333
) -> Result<TableLayoutContext> {
333
    let mut table_ctx = TableLayoutContext::new();
333
    let table_node = tree.get(LayoutNodeId::new(table_index)).ok_or(LayoutError::InvalidTree)?;
    // +spec:width-calculation:0a2766 - table internal elements form rectangular grid of rows/columns (CSS 2.2 §17.5)
    // CSS 2.2 Section 17.4: A table may have one table-caption child.
    // Traverse children to find caption, columns/colgroups, rows, and row groups
351
    for &child_idx in tree.children(table_index) {
351
        if let Some(child) = tree.get(LayoutNodeId::new(child_idx)) {
            // Check if this is a table caption
351
            if matches!(child.formatting_context, FormattingContext::TableCaption) {
                debug_log!(ctx, "Found table caption at index {}", child_idx);
                table_ctx.caption_index = Some(child_idx);
                continue;
351
            }
            // CSS 2.2 Section 17.2: Check for column groups
351
            if matches!(
351
                child.formatting_context,
                FormattingContext::TableColumnGroup
            ) {
                analyze_table_colgroup(tree, child_idx, &table_ctx, ctx)?;
                continue;
351
            }
            // Check if this is a table row or row group
351
            match child.formatting_context {
                FormattingContext::TableRow => {
324
                    analyze_table_row(tree, child_idx, &mut table_ctx, ctx)?;
                }
                FormattingContext::TableRowGroup => {
                    // Process rows within the row group
27
                    for &row_idx in tree.children(child_idx) {
27
                        if let Some(row) = tree.get(LayoutNodeId::new(row_idx)) {
27
                            if matches!(row.formatting_context, FormattingContext::TableRow) {
27
                                analyze_table_row(tree, row_idx, &mut table_ctx, ctx)?;
                            }
                        }
                    }
                }
                _ => {}
            }
        }
    }
333
    debug_log!(
333
        ctx,
333
        "Table structure: {} rows, {} columns, {} cells{}",
        table_ctx.num_rows,
333
        table_ctx.columns.len(),
333
        table_ctx.cells.len(),
333
        if table_ctx.caption_index.is_some() {
            ", has caption"
        } else {
333
            ""
        }
    );
333
    Ok(table_ctx)
333
}
/// Analyze a table column group to identify columns and track collapsed columns
///
/// - CSS 2.2 Section 17.2: Column groups contain columns
/// - CSS 2.2 Section 17.6: Columns can have visibility:collapse
fn analyze_table_colgroup<T: ParsedFontTrait>(
    tree: &LayoutTree,
    colgroup_index: usize,
    table_ctx: &TableLayoutContext,
    ctx: &mut LayoutContext<'_, T>,
) -> Result<()> {
    let colgroup_node = tree.get(LayoutNodeId::new(colgroup_index)).ok_or(LayoutError::InvalidTree)?;
    // Check if the colgroup itself has visibility:collapse
    if is_visibility_collapsed(ctx, colgroup_node) {
        // All columns in this group should be collapsed
        // TODO: For now, just mark the group (actual column indices will be determined later)
        debug_log!(
            ctx,
            "Column group at index {} has visibility:collapse",
            colgroup_index
        );
    }
    // Check for individual column elements within the group
    for &col_idx in tree.children(colgroup_index) {
        if let Some(col_node) = tree.get(LayoutNodeId::new(col_idx)) {
            // Note: Individual columns don't have a FormattingContext::TableColumn
            // They are represented as children of TableColumnGroup
            // Check visibility:collapse on each column
            if is_visibility_collapsed(ctx, col_node) {
                // We need to determine the actual column index this represents
                // For now, we'll track it during cell analysis
                debug_log!(ctx, "Column at index {} has visibility:collapse", col_idx);
            }
        }
    }
    Ok(())
}
/// Read the HTML `colspan` / `rowspan` of a table cell from its DOM node.
///
/// These are HTML presentational attributes (`AttributeType::ColSpan`/`RowSpan`
/// on `NodeData`), not CSS properties. Missing or non-positive values default to
/// 1 per the HTML parsing rules.
#[allow(clippy::cast_sign_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
852
fn get_cell_spans(styled_dom: &StyledDom, dom_id: NodeId) -> (usize, usize) {
852
    let mut colspan = 1usize;
852
    let mut rowspan = 1usize;
852
    let node_data = &styled_dom.node_data.as_container()[dom_id];
856
    for attr in node_data.attributes().as_ref() {
91
        match attr {
            // Clamp to the HTML limits (colspan 1000, rowspan 65534): an
            // unclamped span grows the column/row vectors unboundedly -> OOM/hang.
5
            azul_core::dom::AttributeType::ColSpan(n) => colspan = (*n).clamp(1, 1000) as usize,
5
            azul_core::dom::AttributeType::RowSpan(n) => rowspan = (*n).clamp(1, 65534) as usize,
81
            _ => {}
        }
    }
852
    (colspan, rowspan)
852
}
// +spec:display-property:7f167c - Table grid cell placement: rows fill table top-to-bottom, cells placed left-to-right with colspan/rowspan
/// Analyze a table row to identify cells and update column count
351
fn analyze_table_row<T: ParsedFontTrait>(
351
    tree: &LayoutTree,
351
    row_index: usize,
351
    table_ctx: &mut TableLayoutContext,
351
    ctx: &mut LayoutContext<'_, T>,
351
) -> Result<()> {
    // +spec:inline-formatting-context:3f8091 - table visual layout: cells occupy grid cells, row/column spanning
351
    let row_node = tree.get(LayoutNodeId::new(row_index)).ok_or(LayoutError::InvalidTree)?;
351
    let row_num = table_ctx.num_rows;
351
    table_ctx.num_rows += 1;
    // Track the layout tree index for this row (for positioning/painting)
351
    if table_ctx.row_node_indices.len() <= row_num {
351
        table_ctx.row_node_indices.resize(row_num + 1, 0);
351
    }
351
    table_ctx.row_node_indices[row_num] = row_index;
    // CSS 2.2 Section 17.6: Check if this row has visibility:collapse
351
    if is_visibility_collapsed(ctx, row_node) {
        debug_log!(ctx, "Row {} has visibility:collapse", row_num);
        table_ctx.collapsed_rows.insert(row_num);
351
    }
351
    let mut col_index = 0;
846
    for &cell_idx in tree.children(row_index) {
846
        if let Some(cell) = tree.get(LayoutNodeId::new(cell_idx)) {
846
            if matches!(cell.formatting_context, FormattingContext::TableCell) {
                // Read colspan/rowspan from the cell's HTML attributes (default 1).
846
                let (colspan, rowspan) = cell
846
                    .dom_node_id
846
                    .map_or((1, 1), |dom_id| get_cell_spans(ctx.styled_dom, dom_id));
                // Skip columns still occupied by a rowspan cell from an earlier row,
                // so this cell lands in the next free grid slot (CSS 2.2 §17.5 cell
                // placement). Without this, a cell under a rowspan overlapped it.
846
                while table_ctx
846
                    .col_occupied
846
                    .get(col_index)
846
                    .is_some_and(|&n| n > 0)
                {
                    col_index += 1;
                }
846
                let cell_info = TableCellInfo {
846
                    node_index: cell_idx,
846
                    column: col_index,
846
                    colspan,
846
                    row: row_num,
846
                    rowspan,
846
                };
846
                table_ctx.cells.push(cell_info);
                // Update column count
846
                let max_col = col_index + colspan;
1665
                while table_ctx.columns.len() < max_col {
819
                    table_ctx.columns.push(TableColumnInfo {
819
                        min_width: 0.0,
819
                        max_width: 0.0,
819
                        computed_width: None,
819
                    });
819
                }
                // Reserve this cell's columns for the rows it spans downward. Store
                // the full rowspan; the end-of-row decrement below turns it into the
                // count of REMAINING rows for subsequent rows to skip.
846
                if rowspan > 1 {
                    if table_ctx.col_occupied.len() < max_col {
                        table_ctx.col_occupied.resize(max_col, 0);
                    }
                    for occ in &mut table_ctx.col_occupied[col_index..max_col] {
                        *occ = rowspan;
                    }
846
                }
846
                col_index += colspan;
            }
        }
    }
    // End of row: one row of every pending rowspan has now been consumed.
351
    for occ in &mut table_ctx.col_occupied {
        *occ = occ.saturating_sub(1);
    }
351
    Ok(())
351
}
// +spec:overflow:66f584 - Fixed table layout: cells use overflow property to clip overflowing content
// +spec:positioning:46070a - Fixed table layout (17.5.2.1) and auto table layout (17.5.2.2) column width algorithms
// +spec:table-layout:875401 - Fixed table layout algorithm (17.5.2.1): column widths from first-row cells, remaining columns divide space equally, table width = max(width property, sum of columns)
/// Calculate column widths using the fixed table layout algorithm
/// // +spec:overflow:de613c - Fixed table layout algorithm (CSS 2.2 Section 17.5.2.1)
// +spec:table-layout:8b72b3 - fixed table layout: column width from column elements/first-row cells, remaining columns equal division
///
/// CSS 2.2 Section 17.5.2.1: In fixed table layout, the horizontal layout
/// does not depend on cell contents. Column widths are determined by:
/// 1. Column elements with explicit (non-auto) width
/// 2. First-row cells with explicit (non-auto) width
/// 3. Remaining columns equally divide remaining horizontal space
///
/// CSS 2.2 Section 17.6: Columns with visibility:collapse are excluded
/// from width calculations
// +spec:table-layout:c5e446 - Fixed table layout algorithm: column widths from col elements or first-row cells, remaining columns divide equally
/// +spec:width-calculation:8c958a - Fixed table layout: column widths from col elements, first-row cells, then equal distribution (CSS 2.2 §17.5.2.1)
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
fn calculate_column_widths_fixed<T: ParsedFontTrait>(
    ctx: &mut LayoutContext<'_, T>,
    tree: &LayoutTree,
    table_ctx: &mut TableLayoutContext,
    available_width: f32,
) {
    debug_table_layout!(
        ctx,
        "calculate_column_widths_fixed: num_cols={}, available_width={:.2}",
        table_ctx.columns.len(),
        available_width
    );
    let num_cols = table_ctx.columns.len();
    if num_cols == 0 {
        return;
    }
    let num_visible_cols = num_cols - table_ctx.collapsed_columns.len();
    if num_visible_cols == 0 {
        for col in &mut table_ctx.columns {
            col.computed_width = Some(0.0);
        }
        return;
    }
    // Step 1 (column elements) is skipped because column elements don't store
    // explicit widths in the current table structure analysis.
    // Step 2: Check first-row cells for explicit width properties.
    let mut col_has_width = vec![false; num_cols];
    for cell_info in &table_ctx.cells {
        if cell_info.row != 0 {
            continue; // Only consider cells in the first row
        }
        if table_ctx.collapsed_columns.contains(&cell_info.column) {
            continue;
        }
        // Look up the cell's CSS width via its dom_node_id
        let Some(dom_id) = tree.get(LayoutNodeId::new(cell_info.node_index)).and_then(|n| n.dom_node_id) else {
            continue;
        };
        let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
        let css_width = get_css_width(ctx.styled_dom, dom_id, node_state);
        let explicit_px = match css_width.unwrap_or_default() {
            LayoutWidth::Px(px) => {
                resolve_size_metric(
                    px.metric,
                    px.number.get(),
                    available_width,
                    ctx.viewport_size,
                    get_element_font_size(ctx.styled_dom, dom_id, node_state),
                    get_root_font_size(ctx.styled_dom, node_state),
                )
            }
            LayoutWidth::Auto | LayoutWidth::MinContent | LayoutWidth::MaxContent
            | LayoutWidth::Calc(_) | LayoutWidth::FitContent(_) => continue,
        };
        if cell_info.colspan == 1 {
            table_ctx.columns[cell_info.column].computed_width = Some(explicit_px);
            col_has_width[cell_info.column] = true;
        } else {
            let mut visible_span_count = 0;
            for offset in 0..cell_info.colspan {
                let col_idx = cell_info.column + offset;
                if col_idx < num_cols && !table_ctx.collapsed_columns.contains(&col_idx) {
                    visible_span_count += 1;
                }
            }
            if visible_span_count > 0 {
                let per_col = explicit_px / visible_span_count as f32;
                for offset in 0..cell_info.colspan {
                    let col_idx = cell_info.column + offset;
                    if col_idx < num_cols
                        && !table_ctx.collapsed_columns.contains(&col_idx)
                        && !col_has_width[col_idx]
                    {
                        table_ctx.columns[col_idx].computed_width = Some(per_col);
                        col_has_width[col_idx] = true;
                    }
                }
            }
        }
    }
    let used_width: f32 = table_ctx.columns.iter().enumerate()
        .filter(|(idx, _)| col_has_width[*idx] && !table_ctx.collapsed_columns.contains(idx))
        .filter_map(|(_, c)| c.computed_width)
        .sum();
    let remaining_width = (available_width - used_width).max(0.0);
    let num_remaining = table_ctx.columns.iter().enumerate()
        .filter(|(idx, _)| !col_has_width[*idx] && !table_ctx.collapsed_columns.contains(idx))
        .count();
    if num_remaining > 0 {
        let width_per_remaining = remaining_width / num_remaining as f32;
        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
            if table_ctx.collapsed_columns.contains(&col_idx) {
                col.computed_width = Some(0.0);
            } else if !col_has_width[col_idx] {
                col.computed_width = Some(width_per_remaining);
            }
        }
    }
    // Set collapsed columns to zero width
    for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
        if table_ctx.collapsed_columns.contains(&col_idx) {
            col.computed_width = Some(0.0);
        }
    }
    let total_col_width: f32 = table_ctx.columns.iter()
        .filter_map(|c| c.computed_width)
        .sum();
    if available_width > total_col_width && num_visible_cols > 0 {
        let extra = available_width - total_col_width;
        let extra_per_col = extra / num_visible_cols as f32;
        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
            if !table_ctx.collapsed_columns.contains(&col_idx) {
                if let Some(ref mut w) = col.computed_width {
                    *w += extra_per_col;
                }
            }
        }
    }
}
/// Recursively clear the layout cache for every node in a subtree.
///
/// A fixed-depth walk is not enough: a table cell like
/// `<td><span><a>text</a></span></td>` has 4+ levels once the anonymous IFC
/// wrapper is inserted, and any stale cache below that level would feed a
/// narrow intrinsic width back into `measure_cell_content_width`.
5076
fn clear_subtree_cache(
5076
    tree: &LayoutTree,
5076
    cache_map: &mut crate::solver3::cache::LayoutCacheMap,
5076
    root: usize,
5076
) {
5076
    if root < cache_map.entries.len() {
5076
        cache_map.entries[root].clear();
5076
    }
5076
    let child_ids: Vec<usize> = tree.children(root).to_vec();
8460
    for child in child_ids {
3384
        clear_subtree_cache(tree, cache_map, child);
3384
    }
5076
}
/// Measure a cell's content width for a given intrinsic sizing mode.
///
/// CSS 2.2 Section 17.5.2.2: shared helper for min-content and max-content
/// width measurement. Lays out the cell subtree in `ComputeSize` mode and
/// returns the border-box width (content + padding + border).
1692
fn measure_cell_content_width<T: ParsedFontTrait>(
1692
    ctx: &mut LayoutContext<'_, T>,
1692
    tree: &mut LayoutTree,
1692
    text_cache: &mut TextLayoutCache,
1692
    cell_index: usize,
1692
    constraints: &LayoutConstraints<'_>,
1692
    sizing_mode: text3::cache::AvailableSpace,
1692
) -> Result<f32> {
1692
    let width_type = match sizing_mode {
846
        text3::cache::AvailableSpace::MinContent => Text3AvailableSpace::MinContent,
846
        text3::cache::AvailableSpace::MaxContent => Text3AvailableSpace::MaxContent,
        text3::cache::AvailableSpace::Definite(w) => Text3AvailableSpace::Definite(w),
    };
1692
    let cell_constraints = LayoutConstraints {
1692
        available_size: LogicalSize {
1692
            width: sizing_mode.to_f32_for_layout(),
1692
            height: f32::INFINITY,
1692
        },
1692
        writing_mode: constraints.writing_mode,
1692
        writing_mode_ctx: constraints.writing_mode_ctx,
1692
        bfc_state: None,
1692
        text_align: constraints.text_align,
1692
        containing_block_size: constraints.containing_block_size,
1692
        available_width_type: width_type,
1692
        fragmentainer: None,
1692
    };
1692
    let mut temp_positions: super::PositionVec = Vec::new();
1692
    let mut temp_scrollbar_reflow = false;
1692
    let mut temp_float_cache = HashMap::new();
    // Clear cached layout for this cell and ALL its descendants so that
    // min/max-content measurement uses unconstrained width, not a stale
    // result from a previous pass with narrower constraints. Deeply nested
    // inlines (`<td><span><a>text</a></span></td>`) need recursion; a fixed
    // 2-level walk left the `<a>` at level 3 with a stale cached 0-width.
1692
    clear_subtree_cache(tree, &mut ctx.cache_map, cell_index);
1692
    crate::solver3::cache::calculate_layout_for_subtree(
1692
        ctx,
1692
        tree,
1692
        text_cache,
1692
        cell_index,
1692
        LogicalPosition::zero(),
1692
        cell_constraints.available_size,
1692
        &mut temp_positions,
1692
        &mut temp_scrollbar_reflow,
1692
        &mut temp_float_cache,
1692
        crate::solver3::cache::ComputeMode::ComputeSize,
    )?;
1692
    let cell_bp = tree.get(LayoutNodeId::new(cell_index))
1692
        .ok_or(LayoutError::InvalidTree)?
1692
        .box_props.unpack();
1692
    let padding = &cell_bp.padding;
1692
    let border = &cell_bp.border;
1692
    let wm = constraints.writing_mode;
    // For min/max-content measurement, use the overflow content size (actual
    // content width) rather than used_size. used_size for auto-width blocks
    // fills the containing block, which is huge (f32::MAX/2) during
    // intrinsic sizing — that would make every column appear infinitely wide.
1692
    let content_width = tree.warm(LayoutNodeId::new(cell_index))
1692
        .and_then(|w| w.overflow_content_size)
1692
        .map_or_else(|| {
            tree.get(LayoutNodeId::new(cell_index))
                .and_then(|n| n.used_size)
                .map_or(0.0, |s| s.width)
        }, |s| s.width);
1692
    Ok(content_width
1692
        + padding.cross_start(wm) + padding.cross_end(wm)
1692
        + border.cross_start(wm) + border.cross_end(wm))
1692
}
/// Measure a cell's minimum content width (with maximum wrapping)
846
fn measure_cell_min_content_width<T: ParsedFontTrait>(
846
    ctx: &mut LayoutContext<'_, T>,
846
    tree: &mut LayoutTree,
846
    text_cache: &mut TextLayoutCache,
846
    cell_index: usize,
846
    constraints: &LayoutConstraints<'_>,
846
) -> Result<f32> {
846
    measure_cell_content_width(
846
        ctx, tree, text_cache, cell_index, constraints,
846
        text3::cache::AvailableSpace::MinContent,
    )
846
}
/// Measure a cell's maximum content width (without wrapping)
846
fn measure_cell_max_content_width<T: ParsedFontTrait>(
846
    ctx: &mut LayoutContext<'_, T>,
846
    tree: &mut LayoutTree,
846
    text_cache: &mut TextLayoutCache,
846
    cell_index: usize,
846
    constraints: &LayoutConstraints<'_>,
846
) -> Result<f32> {
846
    measure_cell_content_width(
846
        ctx, tree, text_cache, cell_index, constraints,
846
        text3::cache::AvailableSpace::MaxContent,
    )
846
}
/// Calculate column widths using the auto table layout algorithm
fn calculate_column_widths_auto<T: ParsedFontTrait>(
    table_ctx: &mut TableLayoutContext,
    tree: &mut LayoutTree,
    text_cache: &mut TextLayoutCache,
    ctx: &mut LayoutContext<'_, T>,
    constraints: &LayoutConstraints<'_>,
) -> Result<()> {
    calculate_column_widths_auto_with_width(
        table_ctx,
        tree,
        text_cache,
        ctx,
        constraints,
        constraints.available_size.width,
    )
}
/// Calculate column widths using the auto table layout algorithm with explicit table width
// +spec:display-property:05c8e8 - CSS 2.2 §17.5.2.2 automatic table layout: column min/max widths, table width = max(W or CB, CAPMIN, MIN), extra width distributed over columns
/// +spec:overflow:29edde - CSS 2.2 §17.5.2.2 automatic table layout: MCW/max-content per cell, column min/max, colspan distribution, final width determination
// +spec:table-layout:23a215 - automatic table layout: MCW/max cell widths, column min/max, colspan distribution, table width from MAX/MIN/CAPMIN
// +spec:table-layout:5e1145 - Automatic table layout: MCW/max-content per cell, column min/max, colspan distribution, final width from MIN/MAX
// +spec:width-calculation:42dfca - CSS 2.2 §17.5.2.2 automatic table layout: MCW/max-content per cell, column min/max, multi-span distribution, final table width
/// +spec:width-calculation:335ef1 - Automatic table layout: width given by column widths and borders (CSS 2.2 §17.5.2.2)
#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
333
fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
333
    table_ctx: &mut TableLayoutContext,
333
    tree: &mut LayoutTree,
333
    text_cache: &mut TextLayoutCache,
333
    ctx: &mut LayoutContext<'_, T>,
333
    constraints: &LayoutConstraints<'_>,
333
    table_width: f32,
333
) -> Result<()> {
    // Auto layout: calculate min/max content width for each cell
333
    let num_cols = table_ctx.columns.len();
333
    if num_cols == 0 {
        return Ok(());
333
    }
    // Step 1: Measure all cells to determine column min/max widths
    // CSS 2.2 Section 17.6: Skip cells in collapsed columns
1179
    for cell_info in &table_ctx.cells {
        // Skip cells in collapsed columns
846
        if table_ctx.collapsed_columns.contains(&cell_info.column) {
            continue;
846
        }
        // Skip cells that span into collapsed columns
846
        let mut spans_collapsed = false;
846
        for col_offset in 0..cell_info.colspan {
846
            if table_ctx
846
                .collapsed_columns
846
                .contains(&(cell_info.column + col_offset))
            {
                spans_collapsed = true;
                break;
846
            }
        }
846
        if spans_collapsed {
            continue;
846
        }
846
        let min_width = measure_cell_min_content_width(
846
            ctx,
846
            tree,
846
            text_cache,
846
            cell_info.node_index,
846
            constraints,
        )?;
846
        let max_width = measure_cell_max_content_width(
846
            ctx,
846
            tree,
846
            text_cache,
846
            cell_info.node_index,
846
            constraints,
        )?;
        // Handle single-column cells
846
        if cell_info.colspan == 1 {
846
            let col = &mut table_ctx.columns[cell_info.column];
846
            col.min_width = col.min_width.max(min_width);
846
            col.max_width = col.max_width.max(max_width);
846
        } else {
            // Handle multi-column cells (colspan > 1)
            // Distribute the cell's min/max width across the spanned columns
            distribute_cell_width_across_columns(
                &mut table_ctx.columns,
                cell_info.column,
                cell_info.colspan,
                min_width,
                max_width,
                &table_ctx.collapsed_columns,
            );
        }
    }
    // Step 2: Calculate final column widths based on available space
    // Exclude collapsed columns from total width calculations
333
    let total_min_width: f32 = table_ctx
333
        .columns
333
        .iter()
333
        .enumerate()
819
        .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
333
        .map(|(_, c)| c.min_width)
333
        .sum();
333
    let total_max_width: f32 = table_ctx
333
        .columns
333
        .iter()
333
        .enumerate()
819
        .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
333
        .map(|(_, c)| c.max_width)
333
        .sum();
333
    let available_width = table_width; // Use table's content-box width, not constraints
333
    debug_table_layout!(
333
        ctx,
333
        "calculate_column_widths_auto: min={:.2}, max={:.2}, table_width={:.2}",
        total_min_width,
        total_max_width,
        table_width
    );
    // Handle infinity and NaN cases
333
    if !total_max_width.is_finite() || !available_width.is_finite() {
        // If max_width is infinite or unavailable, distribute available width equally
        let num_non_collapsed = table_ctx.columns.len() - table_ctx.collapsed_columns.len();
        let width_per_column = if num_non_collapsed > 0 {
            available_width / num_non_collapsed as f32
        } else {
            0.0
        };
        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
            if table_ctx.collapsed_columns.contains(&col_idx) {
                col.computed_width = Some(0.0);
            } else {
                // Use the larger of min_width and equal distribution
                col.computed_width = Some(col.min_width.max(width_per_column));
            }
        }
333
    } else if available_width >= total_max_width {
        // Case 1: More space than max-content - distribute excess proportionally
        //
        // CSS 2.1 Section 17.5.2.2: Distribute extra space proportionally to
        // max-content widths
333
        let excess_width = available_width - total_max_width;
        // First pass: collect column info (max_width) to avoid borrowing issues
333
        let column_info: Vec<(usize, f32, bool)> = table_ctx
333
            .columns
333
            .iter()
333
            .enumerate()
819
            .map(|(idx, c)| (idx, c.max_width, table_ctx.collapsed_columns.contains(&idx)))
333
            .collect();
        // Calculate total weight for proportional distribution (use max_width as weight)
333
        let total_weight: f32 = column_info.iter()
819
            .filter(|(_, _, is_collapsed)| !is_collapsed)
819
            .map(|(_, max_w, _)| max_w.max(1.0)) // Avoid division by zero
333
            .sum();
333
        let num_non_collapsed = column_info
333
            .iter()
819
            .filter(|(_, _, is_collapsed)| !is_collapsed)
333
            .count();
        // Second pass: set computed widths
1152
        for (col_idx, max_width, is_collapsed) in column_info {
819
            let col = &mut table_ctx.columns[col_idx];
819
            if is_collapsed {
                col.computed_width = Some(0.0);
            } else {
                // Start with max-content width, then add proportional share of excess
819
                let weight_factor = if total_weight > 0.0 {
819
                    max_width.max(1.0) / total_weight
                } else {
                    // If all columns have 0 max_width, distribute equally
                    1.0 / num_non_collapsed.max(1) as f32
                };
819
                let final_width = max_width + (excess_width * weight_factor);
819
                col.computed_width = Some(final_width);
            }
        }
    } else if available_width >= total_min_width {
        // Case 2: Between min and max - interpolate proportionally
        // Avoid division by zero if min == max
        let scale = if total_max_width > total_min_width {
            (available_width - total_min_width) / (total_max_width - total_min_width)
        } else {
            0.0 // If min == max, just use min width
        };
        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
            if table_ctx.collapsed_columns.contains(&col_idx) {
                col.computed_width = Some(0.0);
            } else {
                let interpolated = col.min_width + (col.max_width - col.min_width) * scale;
                col.computed_width = Some(interpolated);
            }
        }
    } else {
        // Case 3: Not enough space - columns must not shrink below their
        // min-content width (CSS 2.1 §17.5.2). Floor each column at min_width;
        // the table overflows its containing block instead of squeezing content.
        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
            if table_ctx.collapsed_columns.contains(&col_idx) {
                col.computed_width = Some(0.0);
            } else {
                col.computed_width = Some(col.min_width);
            }
        }
    }
333
    Ok(())
333
}
/// Distribute a multi-column cell's width across the columns it spans
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
11
fn distribute_cell_width_across_columns(
11
    columns: &mut [TableColumnInfo],
11
    start_col: usize,
11
    colspan: usize,
11
    cell_min_width: f32,
11
    cell_max_width: f32,
11
    collapsed_columns: &std::collections::HashSet<usize>,
11
) {
11
    let end_col = start_col + colspan;
11
    if end_col > columns.len() {
3
        return;
8
    }
    // Calculate current total of spanned non-collapsed columns
8
    let current_min_total: f32 = columns[start_col..end_col]
8
        .iter()
8
        .enumerate()
14
        .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
8
        .map(|(_, c)| c.min_width)
8
        .sum();
8
    let current_max_total: f32 = columns[start_col..end_col]
8
        .iter()
8
        .enumerate()
14
        .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
8
        .map(|(_, c)| c.max_width)
8
        .sum();
    // Count non-collapsed columns in the span
8
    let num_visible_cols = (start_col..end_col)
14
        .filter(|idx| !collapsed_columns.contains(idx))
8
        .count();
8
    if num_visible_cols == 0 {
2
        return; // All spanned columns are collapsed
6
    }
    // Only distribute if the cell needs more space than currently available
6
    if cell_min_width > current_min_total {
3
        let extra_min = cell_min_width - current_min_total;
3
        let per_col = extra_min / num_visible_cols as f32;
6
        for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
6
            if !collapsed_columns.contains(&(start_col + idx)) {
5
                col.min_width += per_col;
5
            }
        }
3
    }
6
    if cell_max_width > current_max_total {
1
        let extra_max = cell_max_width - current_max_total;
1
        let per_col = extra_max / num_visible_cols as f32;
2
        for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
2
            if !collapsed_columns.contains(&(start_col + idx)) {
2
                col.max_width += per_col;
2
            }
        }
5
    }
11
}
/// Layout a cell with its computed column width to determine its content height
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
846
fn layout_cell_for_height<T: ParsedFontTrait>(
846
    ctx: &mut LayoutContext<'_, T>,
846
    tree: &mut LayoutTree,
846
    text_cache: &mut TextLayoutCache,
846
    cell_index: usize,
846
    cell_width: f32,
846
    constraints: &LayoutConstraints<'_>,
846
) -> Result<f32> {
846
    let cell_node = tree.get(LayoutNodeId::new(cell_index)).ok_or(LayoutError::InvalidTree)?;
846
    let cell_dom_id = cell_node.dom_node_id.ok_or(LayoutError::InvalidTree)?;
    // Check if cell has text content directly in DOM (not in LayoutTree)
    // Text nodes are intentionally not included in LayoutTree per CSS spec,
    // but we need to measure them for table cell height calculation.
846
    let has_text_children = cell_dom_id
846
        .az_children(&ctx.styled_dom.node_hierarchy.as_container())
846
        .any(|child_id| {
792
            let node_data = &ctx.styled_dom.node_data.as_container()[child_id];
792
            matches!(node_data.get_node_type(), NodeType::Text(_))
792
        });
846
    debug_table_layout!(
846
        ctx,
846
        "layout_cell_for_height: cell_index={}, has_text_children={}",
        cell_index,
        has_text_children
    );
    // Get padding and border to calculate content width
846
    let cell_node = tree.get(LayoutNodeId::new(cell_index)).ok_or(LayoutError::InvalidTree)?;
846
    let cell_bp = cell_node.box_props.unpack();
846
    let padding = &cell_bp.padding;
846
    let border = &cell_bp.border;
846
    let writing_mode = constraints.writing_mode;
    // cell_width is the border-box width (includes padding/border from column
    // width calculation) but layout functions need content-box width
846
    let content_width = cell_width
846
        - padding.cross_start(writing_mode)
846
        - padding.cross_end(writing_mode)
846
        - border.cross_start(writing_mode)
846
        - border.cross_end(writing_mode);
846
    debug_table_layout!(
846
        ctx,
846
        "Cell width: border_box={:.2}, content_box={:.2}",
        cell_width,
        content_width
    );
846
    let content_height = if has_text_children {
        // Cell contains text - use IFC to measure it
270
        debug_table_layout!(ctx, "Using IFC to measure text content");
270
        let cell_constraints = LayoutConstraints {
270
            available_size: LogicalSize {
270
                width: content_width, // Use content width, not border-box width
270
                height: f32::INFINITY,
270
            },
270
            writing_mode: constraints.writing_mode,
270
            writing_mode_ctx: constraints.writing_mode_ctx,
270
            bfc_state: None,
270
            text_align: constraints.text_align,
270
            containing_block_size: constraints.containing_block_size,
270
            // Use definite width for final cell layout!
270
            // This replaces any previous MinContent/MaxContent measurement.
270
            available_width_type: Text3AvailableSpace::Definite(content_width),
270
            fragmentainer: None,
270
        };
270
        let output = layout_ifc(ctx, text_cache, tree, cell_index, &cell_constraints)?;
        // The cell now owns the authoritative IFC result. Clear any duplicate
        // inline_layout_result from text children that was set during the cell's
        // prior BFC Pass 1 (which ran before layout_cell_for_height).
270
        let cell_children: Vec<usize> = tree.children(cell_index).to_vec();
540
        for child_idx in cell_children {
270
            if let Some(warm) = tree.warm_mut(LayoutNodeId::new(child_idx)) {
270
                warm.inline_layout_result = None;
270
            }
        }
270
        debug_table_layout!(
270
            ctx,
270
            "IFC returned height={:.2}",
            output.overflow_size.height
        );
270
        output.overflow_size.height
    } else {
        // Cell contains block-level children or is empty - use regular layout
576
        debug_table_layout!(ctx, "Using regular layout for block children");
576
        let cell_constraints = LayoutConstraints {
576
            available_size: LogicalSize {
576
                width: content_width, // Use content width, not border-box width
576
                height: f32::INFINITY,
576
            },
576
            writing_mode: constraints.writing_mode,
576
            writing_mode_ctx: constraints.writing_mode_ctx,
576
            bfc_state: None,
576
            text_align: constraints.text_align,
576
            containing_block_size: constraints.containing_block_size,
576
            // Use Definite width for final cell layout!
576
            available_width_type: Text3AvailableSpace::Definite(content_width),
576
            fragmentainer: None,
576
        };
576
        let mut temp_positions: super::PositionVec = Vec::new();
576
        let mut temp_scrollbar_reflow = false;
576
        let mut temp_float_cache = HashMap::new();
576
        crate::solver3::cache::calculate_layout_for_subtree(
576
            ctx,
576
            tree,
576
            text_cache,
576
            cell_index,
576
            LogicalPosition::zero(),
576
            cell_constraints.available_size,
576
            &mut temp_positions,
576
            &mut temp_scrollbar_reflow,
576
            &mut temp_float_cache,
            // PerformLayout: final table cell layout with definite width
576
            crate::solver3::cache::ComputeMode::PerformLayout,
        )?;
576
        let cell_node = tree.get(LayoutNodeId::new(cell_index)).ok_or(LayoutError::InvalidTree)?;
576
        cell_node.used_size.unwrap_or_default().height
    };
    // Add padding and border to get the total height
846
    let cell_node = tree.get(LayoutNodeId::new(cell_index)).ok_or(LayoutError::InvalidTree)?;
846
    let cell_bp = cell_node.box_props.unpack();
846
    let padding = &cell_bp.padding;
846
    let border = &cell_bp.border;
846
    let writing_mode = constraints.writing_mode;
846
    let total_height = content_height
846
        + padding.main_start(writing_mode)
846
        + padding.main_end(writing_mode)
846
        + border.main_start(writing_mode)
846
        + border.main_end(writing_mode);
846
    debug_table_layout!(
846
        ctx,
846
        "Cell total height: cell_index={}, content={:.2}, padding/border={:.2}, total={:.2}",
        cell_index,
        content_height,
846
        padding.main_start(writing_mode)
846
            + padding.main_end(writing_mode)
846
            + border.main_start(writing_mode)
846
            + border.main_end(writing_mode),
        total_height
    );
846
    Ok(total_height)
846
}
// or bottom of content edge if no such line box exists
// +spec:box-model:b64fa0 - Cell baseline is first in-flow line box or bottom of content edge
// +spec:overflow:3fa86f - Table cell baseline: first in-flow line box or bottom of content edge; scrolling boxes treated as at origin
// +spec:inline-formatting-context:c4a20d - cell baseline: first in-flow line box or bottom of content edge
// +spec:inline-formatting-context:17a9c1 - vertical-align baseline/top/bottom/middle for table cells
2703
fn compute_cell_baseline(cell_index: usize, tree: &LayoutTree) -> f32 {
2703
    let Some(cell_node) = tree.get(LayoutNodeId::new(cell_index)) else {
1
        return 0.0;
    };
2702
    let cell_bp = cell_node.box_props.unpack();
    // +spec:inline-formatting-context:27be38 - cell baseline is first in-flow line box or bottom of content edge
    // Check if the cell has inline layout (first in-flow line box)
2702
    if let Some(warm_node) = tree.warm(LayoutNodeId::new(cell_index)) {
2702
        if let Some(ref cached_layout) = warm_node.inline_layout_result {
            // (d6h) Materialized: sentinel-safe first-line baseline.
1548
            let inline_result = cached_layout.materialized();
            // The baseline is the ascent of the first item from the top of the cell
1548
            if let Some(first_item) = inline_result.items.first() {
1548
                let (item_ascent, _) = text3::cache::get_item_vertical_metrics_approx(&first_item.item);
1548
                let padding_top = cell_bp.padding.top;
1548
                let border_top = cell_bp.border.top;
1548
                return padding_top + border_top + first_item.position.y + item_ascent;
            }
1154
        }
    }
    // Check children for first in-flow line box
1154
    let children = tree.children(cell_index);
1190
    for &child_idx in children {
1044
        if child_idx < tree.nodes.len() {
1044
            if let Some(child_warm) = tree.warm(LayoutNodeId::new(child_idx)) {
1044
                if child_warm.inline_layout_result.is_some() {
1008
                    let child_baseline = compute_cell_baseline(child_idx, tree);
1008
                    let padding_top = cell_bp.padding.top;
1008
                    let border_top = cell_bp.border.top;
1008
                    return padding_top + border_top + child_baseline;
36
                }
            }
        }
    }
    // No line box found: baseline is the bottom of the content edge
146
    let used_size = cell_node.used_size.unwrap_or_default();
146
    let padding_bottom = cell_bp.padding.bottom;
146
    let border_bottom = cell_bp.border.bottom;
146
    used_size.height - padding_bottom - border_bottom
2703
}
/// +spec:box-model:72b495 - Table row height = max of computed height and MIN required by cells; baseline alignment
// +spec:display-property:728144 - Table height algorithm: row heights from cell content, rowspan distribution, vertical-align in cells (top/middle/bottom/baseline, sub/super/text-top/text-bottom/length/percentage fall back to baseline), cell baseline computation, and horizontal alignment via text-align
// +spec:positioning:3eaadd - Table height algorithms (§17.5.3): row height = max of cell heights/MIN,
//   rowspan distribution, vertical-align in table cells, cell baseline definition
/// Calculate row heights based on cell content after column widths are determined
// +spec:inline-formatting-context:87b90d - Table height algorithms: row height = max(computed height, cell heights, MIN); vertical-align in cells (baseline/top/middle/bottom, sub/super/etc. fall back to baseline)
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
333
fn calculate_row_heights<T: ParsedFontTrait>(
333
    table_ctx: &mut TableLayoutContext,
333
    tree: &mut LayoutTree,
333
    text_cache: &mut TextLayoutCache,
333
    ctx: &mut LayoutContext<'_, T>,
333
    constraints: &LayoutConstraints<'_>,
333
) -> Result<()> {
333
    debug_table_layout!(
333
        ctx,
333
        "calculate_row_heights: num_rows={}, available_size={:?}",
        table_ctx.num_rows,
        constraints.available_size
    );
    // +spec:inline-formatting-context:a7c7a0 - row height = max of computed height, cell heights, and MIN; vertical-align per cell
    // Initialize row heights and baselines
333
    table_ctx.row_heights = vec![0.0; table_ctx.num_rows];
333
    table_ctx.row_baselines = vec![0.0; table_ctx.num_rows];
    // CSS 2.2 Section 17.6: Set collapsed rows to height 0
333
    for &row_idx in &table_ctx.collapsed_rows {
        if row_idx < table_ctx.row_heights.len() {
            table_ctx.row_heights[row_idx] = 0.0;
        }
    }
    // required by content; 'height' property can influence row height but does not
    // increase cell box height
    // First pass: Calculate heights for cells that don't span multiple rows
1179
    for cell_info in &table_ctx.cells {
        // Skip cells in collapsed rows
846
        if table_ctx.collapsed_rows.contains(&cell_info.row) {
            continue;
846
        }
        // Get the cell's width (sum of column widths if colspan > 1)
846
        let mut cell_width = 0.0;
846
        for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
846
            if let Some(col) = table_ctx.columns.get(col_idx) {
846
                if let Some(width) = col.computed_width {
846
                    cell_width += width;
846
                }
            }
        }
846
        debug_table_layout!(
846
            ctx,
846
            "Cell layout: node_index={}, row={}, col={}, width={:.2}",
            cell_info.node_index,
            cell_info.row,
            cell_info.column,
            cell_width
        );
        // Layout the cell to get its height
846
        let cell_height = layout_cell_for_height(
846
            ctx,
846
            tree,
846
            text_cache,
846
            cell_info.node_index,
846
            cell_width,
846
            constraints,
        )?;
846
        debug_table_layout!(
846
            ctx,
846
            "Cell height calculated: node_index={}, height={:.2}",
            cell_info.node_index,
            cell_height
        );
        //   row height = max of all single-span cell heights in the row
846
        if cell_info.rowspan == 1 {
846
            let current_height = table_ctx.row_heights[cell_info.row];
846
            table_ctx.row_heights[cell_info.row] = current_height.max(cell_height);
846
        }
        // +spec:box-model:073652 - Table height: baseline-aligned cells establish row baseline, then top/bottom/middle cells positioned
        // The baseline of a cell is the baseline of its first line box (from inline layout)
        // or the bottom of the content box if no inline content.
846
        if cell_info.rowspan == 1 {
846
            let cell_baseline = compute_cell_baseline(cell_info.node_index, tree);
846
            let current_baseline = table_ctx.row_baselines[cell_info.row];
846
            table_ctx.row_baselines[cell_info.row] = current_baseline.max(cell_baseline);
846
        }
    }
    // involved must be great enough to encompass the cell spanning the rows
    // Second pass: Handle cells that span multiple rows (rowspan > 1)
1179
    for cell_info in &table_ctx.cells {
        // Skip cells that start in collapsed rows
846
        if table_ctx.collapsed_rows.contains(&cell_info.row) {
            continue;
846
        }
846
        if cell_info.rowspan > 1 {
            // Get the cell's width
            let mut cell_width = 0.0;
            for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
                if let Some(col) = table_ctx.columns.get(col_idx) {
                    if let Some(width) = col.computed_width {
                        cell_width += width;
                    }
                }
            }
            // Layout the cell to get its height
            let cell_height = layout_cell_for_height(
                ctx,
                tree,
                text_cache,
                cell_info.node_index,
                cell_width,
                constraints,
            )?;
            // Calculate the current total height of spanned rows (excluding collapsed rows)
            // Clamp to the actual row count: a rowspan extending past the last
            // row would slice row_heights out of bounds (panic on e.g. a
            // rowspan="2" cell in a single-row table).
            let end_row = (cell_info.row + cell_info.rowspan).min(table_ctx.row_heights.len());
            let current_total: f32 = table_ctx.row_heights[cell_info.row..end_row]
                .iter()
                .enumerate()
                .filter(|(idx, _)| !table_ctx.collapsed_rows.contains(&(cell_info.row + idx)))
                .map(|(_, height)| height)
                .sum();
            // If the cell needs more height, distribute extra height across
            // non-collapsed spanned rows
            if cell_height > current_total {
                let extra_height = cell_height - current_total;
                // Count non-collapsed rows in span
                let non_collapsed_rows = (cell_info.row..end_row)
                    .filter(|row_idx| !table_ctx.collapsed_rows.contains(row_idx))
                    .count();
                if non_collapsed_rows > 0 {
                    let per_row = extra_height / non_collapsed_rows as f32;
                    for row_idx in cell_info.row..end_row {
                        if !table_ctx.collapsed_rows.contains(&row_idx) {
                            table_ctx.row_heights[row_idx] += per_row;
                        }
                    }
                }
            }
846
        }
    }
    // CSS 2.2 Section 17.6: Final pass - ensure collapsed rows have height 0
333
    for &row_idx in &table_ctx.collapsed_rows {
        if row_idx < table_ctx.row_heights.len() {
            table_ctx.row_heights[row_idx] = 0.0;
        }
    }
    //   visible content, the row has zero height and v-spacing on only one side
    // +spec:table-layout:7370dc - empty-cells:hide in separated borders model
    // +spec:box-model:1e9cf1 - empty-cells:hide rows get zero height with v-spacing on only one side
    // +spec:overflow:a44925 - CSS 2.2 §17.6.1.1: empty-cells:hide suppresses borders/backgrounds; all-hidden rows get zero height
    // +spec:table-layout:dc8bc3 - separated borders model: border-spacing, empty-cells, row zero-height
333
    if table_ctx.border_collapse == StyleBorderCollapse::Separate {
306
        for row_idx in 0..table_ctx.num_rows {
306
            if table_ctx.collapsed_rows.contains(&row_idx) {
                continue;
306
            }
            // Collect cells in this row
306
            let row_cells: Vec<usize> = table_ctx
306
                .cells
306
                .iter()
783
                .filter(|c| c.row == row_idx && c.rowspan == 1)
306
                .map(|c| c.node_index)
306
                .collect();
306
            if row_cells.is_empty() {
                continue;
306
            }
            // +spec:box-model:0ab9b0 - empty-cells:hide suppresses borders/backgrounds, row gets zero height if all cells hidden+empty
            // Check if ALL cells in this row have empty-cells:hide and are empty
306
            let all_hidden_empty = row_cells.iter().all(|&cell_idx| {
306
                tree.get(LayoutNodeId::new(cell_idx)).is_none_or(|cell_node| {
306
                    let ec = get_empty_cells_property(ctx, cell_node);
306
                    ec == StyleEmptyCells::Hide && is_cell_empty(tree, cell_idx)
306
                })
306
            });
306
            if all_hidden_empty {
                table_ctx.row_heights[row_idx] = 0.0;
                table_ctx.hidden_empty_rows.insert(row_idx);
306
            }
        }
36
    }
333
    Ok(())
333
}
/// Position all cells in the table grid with calculated widths and heights
#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
333
fn position_table_cells<T: ParsedFontTrait>(
333
    table_ctx: &TableLayoutContext,
333
    tree: &mut LayoutTree,
333
    ctx: &mut LayoutContext<'_, T>,
333
    table_index: usize,
333
    constraints: &LayoutConstraints<'_>,
333
) -> Result<BTreeMap<usize, LogicalPosition>> {
333
    debug_log!(ctx, "Positioning table cells in grid");
333
    let mut positions = BTreeMap::new();
    // +spec:box-model:54e86a - Separated borders model: individual cell borders, border-spacing between cells, empty-cells handling
    //   rows, columns, row groups, column groups cannot have borders (UA must ignore border props);
    //   row/column/rowgroup/colgroup backgrounds are invisible in border-spacing area (table bg shows through);
    //   distance from table edge to edge-cell border = table padding + border-spacing
    //   (table padding is already accounted for by the containing block; h_spacing is the border-spacing)
    // Get border spacing values if border-collapse is separate
333
    let (h_spacing, v_spacing) = if table_ctx.border_collapse == StyleBorderCollapse::Separate {
297
        let styled_dom = ctx.styled_dom;
        // Anonymous table wrapper boxes have no dom_node_id; without a styled
        // node we cannot resolve font-relative border-spacing units, so fall
        // back to zero spacing rather than panicking.
297
        if let Some(table_id) = tree.nodes[table_index].dom_node_id {
297
            let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
297
            let spacing_context = ResolutionContext {
297
                vertical_writing_mode: false,
297
                element_font_size: get_element_font_size(styled_dom, table_id, table_state),
297
                parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
297
                root_font_size: get_root_font_size(styled_dom, table_state),
297
                containing_block_size: PhysicalSize::new(0.0, 0.0),
297
                element_size: None,
297
                viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
297
            };
297
            let h = table_ctx
297
                .border_spacing
297
                .horizontal
297
                .resolve_with_context(&spacing_context, PropertyContext::Other)
297
                .max(0.0);
297
            let v = table_ctx
297
                .border_spacing
297
                .vertical
297
                .resolve_with_context(&spacing_context, PropertyContext::Other)
297
                .max(0.0);
297
            (h, v)
        } else {
            (0.0, 0.0)
        }
    } else {
36
        (0.0, 0.0)
    };
333
    debug_log!(
333
        ctx,
333
        "Border spacing: h={:.2}, v={:.2}",
        h_spacing,
        v_spacing
    );
    // Calculate cumulative column positions (x-offsets) with spacing
333
    let mut col_positions = vec![0.0; table_ctx.columns.len()];
333
    let mut x_offset = h_spacing; // Start with spacing on the left
819
    for (i, col) in table_ctx.columns.iter().enumerate() {
819
        col_positions[i] = x_offset;
819
        if let Some(width) = col.computed_width {
            // Collapsed columns: gutters on either side collapse (width is 0, skip spacing)
819
            if table_ctx.collapsed_columns.contains(&i) {
                // No width, no gutter added
819
            } else {
819
                x_offset += width + h_spacing; // Add spacing between columns
819
            }
        }
    }
    // Calculate cumulative row positions (y-offsets) with spacing
333
    let mut row_positions = vec![0.0; table_ctx.num_rows];
333
    let mut y_offset = v_spacing; // Start with spacing on the top
351
    for (i, &height) in table_ctx.row_heights.iter().enumerate() {
351
        row_positions[i] = y_offset;
        // Collapsed rows: gutters on either side collapse (height is 0, skip spacing)
351
        if table_ctx.collapsed_rows.contains(&i) {
            // No height, no gutter added
351
        } else if table_ctx.hidden_empty_rows.contains(&i) {
            // Hidden-empty row: zero height, only one side of spacing
            // (we already added spacing before this row, so skip the spacing after)
            y_offset += height; // height is 0.0
351
        } else {
351
            y_offset += height + v_spacing; // Add spacing between rows
351
        }
    }
    // Store row positions and sizes so paint_element_background can paint row backgrounds.
    // Row width = sum of column widths + spacing. Row height from row_heights.
    {
819
        let total_col_width: f32 = table_ctx.columns.iter().map(|c| c.computed_width.unwrap_or(0.0)).sum::<f32>()
333
            + h_spacing * (table_ctx.columns.len().max(1) - 1) as f32
333
            + h_spacing * 2.0; // border-spacing on left+right edges
351
        for (i, &row_y) in row_positions.iter().enumerate() {
351
            if let Some(&row_node_idx) = table_ctx.row_node_indices.get(i) {
351
                let row_height = table_ctx.row_heights.get(i).copied().unwrap_or(0.0);
351
                if let Some(row_node) = tree.get_mut(LayoutNodeId::new(row_node_idx)) {
351
                    row_node.used_size = Some(LogicalSize {
351
                        width: total_col_width,
351
                        height: row_height,
351
                    });
351
                }
                // Don't add to `positions` map (feeds position_bfc_child_descendants,
                // would double-offset cells). The display list computes row paint
                // rects from the row's cell children.
            }
        }
    }
    // Position each cell
1179
    for cell_info in &table_ctx.cells {
846
        let precomputed_cell_baseline = compute_cell_baseline(cell_info.node_index, tree);
846
        let cell_node = tree
846
            .get_mut(LayoutNodeId::new(cell_info.node_index))
846
            .ok_or(LayoutError::InvalidTree)?;
        // Calculate cell position
846
        let x = col_positions.get(cell_info.column).copied().unwrap_or(0.0);
846
        let y = row_positions.get(cell_info.row).copied().unwrap_or(0.0);
        // Calculate cell size (sum of spanned columns/rows)
846
        let mut width = 0.0;
846
        debug_info!(
846
            ctx,
846
            "[position_table_cells] Cell {}: calculating width from cols {}..{}",
            cell_info.node_index,
            cell_info.column,
846
            cell_info.column + cell_info.colspan
        );
846
        for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
846
            if let Some(col) = table_ctx.columns.get(col_idx) {
846
                debug_info!(
846
                    ctx,
846
                    "[position_table_cells]   Col {}: computed_width={:?}",
                    col_idx,
                    col.computed_width
                );
846
                if let Some(col_width) = col.computed_width {
846
                    width += col_width;
                    // Add spacing between spanned columns (but not after the last one)
846
                    if col_idx < cell_info.column + cell_info.colspan - 1 {
                        width += h_spacing;
846
                    }
                } else {
                    debug_info!(
                        ctx,
                        "[position_table_cells]   WARN:  Col {} has NO computed_width!",
                        col_idx
                    );
                }
            } else {
                debug_info!(
                    ctx,
                    "[position_table_cells]   WARN:  Col {} not found in table_ctx.columns!",
                    col_idx
                );
            }
        }
846
        let mut height = 0.0;
846
        let end_row = cell_info.row + cell_info.rowspan;
846
        for row_idx in cell_info.row..end_row {
846
            if let Some(&row_height) = table_ctx.row_heights.get(row_idx) {
846
                height += row_height;
                // Add spacing between spanned rows (but not after the last one)
846
                if row_idx < end_row - 1 {
                    height += v_spacing;
846
                }
            }
        }
        // Update cell's used size and position
846
        let writing_mode = constraints.writing_mode;
        // Table layout works in main/cross axes, must convert back to logical width/height
846
        debug_info!(
846
            ctx,
846
            "[position_table_cells] Cell {}: BEFORE from_main_cross: width={}, height={}, \
846
             writing_mode={:?}",
            cell_info.node_index,
            width,
            height,
            writing_mode
        );
846
        cell_node.used_size = Some(LogicalSize::from_main_cross(height, width, writing_mode));
846
        debug_info!(
846
            ctx,
846
            "[position_table_cells] Cell {}: AFTER from_main_cross: used_size={:?}",
            cell_info.node_index,
            cell_node.used_size
        );
846
        debug_info!(
846
            ctx,
846
            "[position_table_cells] Cell {}: setting used_size to {}x{} (row_heights={:?})",
            cell_info.node_index,
            width,
            height,
            table_ctx.row_heights
        );
        // Save hot fields needed for vertical alignment before dropping the mutable borrow
846
        let cell_dom_node_id = cell_node.dom_node_id;
846
        let cell_box_props = cell_node.box_props.unpack();
846
        drop(cell_node);
        // +spec:inline-formatting-context:20e8e8 - table cell vertical-align alignment order (baseline first, then top, then bottom/middle)
        // receive extra top or bottom padding; vertical-align determines alignment
        // +spec:inline-formatting-context:4545e8 - vertical-align on table cells maps to align-content: top→start, bottom→end, middle→center
        // +spec:inline-formatting-context:e216be - vertical-align on table cells (baseline, middle, top, bottom)
        // +spec:positioning:156e49 - table cell vertical-align ordering and extra padding per CSS 2.2 §17.5.3
        // Apply vertical-align to cell content if it has inline layout
        // We need to compute the y_offset using immutable borrows first, then apply it mutably.
846
        let vertical_align_adjustment = if let Some(warm_node) = tree.warm(LayoutNodeId::new(cell_info.node_index)) {
846
            if let Some(ref cached_layout) = warm_node.inline_layout_result {
                // (d6h) Materialized: sentinel-safe content measurement.
270
                let inline_result = cached_layout.materialized();
                // Get vertical-align property from styled_dom
270
                let vertical_align = if let Some(dom_id) = cell_dom_node_id {
270
                    let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
270
                    match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
261
                        MultiValue::Exact(v) => v,
9
                        _ => StyleVerticalAlign::Baseline,
                    }
                } else {
                    StyleVerticalAlign::Baseline
                };
                // Calculate content height from inline layout bounds
270
                let content_bounds = inline_result.bounds();
270
                let content_height = content_bounds.height;
                // Get padding and border to calculate content-box height
                // height is border-box, but vertical alignment should be within content-box
270
                let padding = &cell_box_props.padding;
270
                let border = &cell_box_props.border;
270
                let content_box_height = height
270
                    - padding.main_start(writing_mode)
270
                    - padding.main_end(writing_mode)
270
                    - border.main_start(writing_mode)
270
                    - border.main_end(writing_mode);
                // top: top of cell box aligned with top of first row it spans
                // bottom: bottom of cell box aligned with bottom of last row it spans
                // middle: center of cell aligned with center of rows it spans
                //   the cell is aligned at the baseline instead
270
                let y_offset = match vertical_align {
                    StyleVerticalAlign::Top => 0.0,
261
                    StyleVerticalAlign::Middle => (content_box_height - content_height) * 0.5,
                    StyleVerticalAlign::Bottom => content_box_height - content_height,
                    // align with the row baseline. cell_baseline = distance from top of cell box
                    // to cell's baseline; row_baseline = distance from top of row to row's baseline
                    StyleVerticalAlign::Baseline
                    | StyleVerticalAlign::Sub
                    | StyleVerticalAlign::Superscript
                    | StyleVerticalAlign::TextTop
                    | StyleVerticalAlign::TextBottom
                    | StyleVerticalAlign::Percentage(_)
                    | StyleVerticalAlign::Length(_) => {
9
                        let row_baseline = table_ctx.row_baselines.get(cell_info.row).copied().unwrap_or(0.0);
9
                        (row_baseline - precomputed_cell_baseline).max(0.0)
                    }
                };
270
                debug_info!(
270
                    ctx,
270
                    "[position_table_cells] Cell {}: vertical-align={:?}, border_box_height={}, \
270
                     content_box_height={}, content_height={}, y_offset={}",
                    cell_info.node_index,
                    vertical_align,
                    height,
                    content_box_height,
                    content_height,
                    y_offset
                );
270
                if y_offset.abs() > 0.01 {
27
                    Some((y_offset, cached_layout.available_width, cached_layout.has_floats))
                } else {
243
                    None
                }
            } else {
576
                None
            }
        } else {
            None
        };
        // Apply the vertical alignment adjustment (requires mutable borrow)
846
        if let Some((y_offset, available_width, has_floats)) = vertical_align_adjustment {
27
            if let Some(warm_mut) = tree.warm_mut(LayoutNodeId::new(cell_info.node_index)) {
27
                if let Some(ref cached_layout) = warm_mut.inline_layout_result {
                    use std::sync::Arc;
                    use crate::text3::cache::{PositionedItem, UnifiedLayout};
                    // (d6h) Materialize the retirement sentinel before
                    // adjusting: reading the stored items raw fed EMPTY
                    // back into the rebuilt cache entry (found via
                    // caret_scroll_glide under the d7 default flip).
27
                    let source_items: Vec<PositionedItem> = if cached_layout.layout.items.is_empty()
27
                        && cached_layout
27
                            .dense
27
                            .as_deref()
27
                            .is_some_and(|d| !d.clusters.is_empty())
                    {
27
                        cached_layout
27
                            .dense
27
                            .as_deref()
27
                            .map(text3::dense::DenseText::to_unified_items)
27
                            .unwrap_or_default()
                    } else {
                        cached_layout.layout.items.clone()
                    };
27
                    let adjusted_items: Vec<PositionedItem> = source_items
27
                        .into_iter()
27
                        .map(|item| PositionedItem {
243
                            item: item.item,
243
                            position: text3::cache::Point {
243
                                x: item.position.x,
243
                                y: item.position.y + y_offset,
243
                            },
243
                            line_index: item.line_index,
243
                        })
27
                        .collect();
27
                    let adjusted_layout = UnifiedLayout {
27
                        items: adjusted_items,
27
                        overflow: cached_layout.layout.overflow.clone(),
27
                    };
                    // Keep the same constraint type from the cached layout
27
                    let mut cil = CachedInlineLayout::new(
27
                        Arc::new(adjusted_layout),
27
                        available_width,
27
                        has_floats,
                    );
                    // LineShift preserves content; carry the hash so Phase 2d
                    // can still validly fast-path this layout (#11).
27
                    cil.inline_content_hash = cached_layout.inline_content_hash;
27
                    warm_mut.inline_layout_result = Some(Box::new(cil));
                    // Vertical-align adjustment changed item positions
                    // within this cell's IFC — patched passes must re-emit.
27
                    ctx.reflowed_ifcs.insert(cell_info.node_index);
                }
            }
819
        }
        // +spec:inline-formatting-context:4545e8 - vertical-align on a table cell
        // centers/bottom-aligns its *content* within the cell box. The block above
        // only handles cells whose content is direct text (they carry an
        // `inline_layout_result`). A cell whose content is block-level
        // (`<td><p>…</p></td>`, `<td><div>…</div></td>`) has none, so its children
        // stayed top-aligned regardless of `vertical-align`. Shift the cell's
        // in-flow block children by the same offset so the UA default
        // `vertical-align: middle` actually centers block content, like browsers.
846
        let cell_has_inline = tree
846
            .warm(LayoutNodeId::new(cell_info.node_index))
846
            .is_some_and(|w| w.inline_layout_result.is_some());
846
        if !cell_has_inline {
576
            let vertical_align = cell_dom_node_id.map_or(StyleVerticalAlign::Baseline, |dom_id| {
576
                let node_state =
576
                    ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
576
                match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
504
                    MultiValue::Exact(v) => v,
72
                    _ => StyleVerticalAlign::Baseline,
                }
576
            });
            // Only middle/bottom reposition block content; top and baseline leave it
            // at the content-box top (the default block position).
576
            let factor = match vertical_align {
504
                StyleVerticalAlign::Middle => 0.5,
                StyleVerticalAlign::Bottom => 1.0,
72
                _ => 0.0,
            };
576
            if factor > 0.0 {
504
                let children: Vec<usize> = tree.children(cell_info.node_index).to_vec();
                // Natural content height = furthest in-flow child bottom edge,
                // measured from the cell content-box top (relative_position is
                // relative to the parent content box).
504
                let mut content_height = 0.0f32;
504
                let mut inflow: Vec<usize> = Vec::new();
1008
                for &c in &children {
504
                    let dom_id = tree.get(LayoutNodeId::new(c)).and_then(|n| n.dom_node_id);
504
                    if matches!(
504
                        get_position_type(ctx.styled_dom, dom_id),
                        LayoutPosition::Absolute | LayoutPosition::Fixed
                    ) {
                        continue; // out-of-flow children are unaffected by vertical-align
504
                    }
504
                    let top = tree
504
                        .warm(LayoutNodeId::new(c))
504
                        .and_then(|w| w.relative_position)
504
                        .map_or(0.0, |p| p.y);
504
                    let h = tree.get(LayoutNodeId::new(c)).and_then(|n| n.used_size).map_or(0.0, |s| s.height);
504
                    content_height = content_height.max(top + h);
504
                    inflow.push(c);
                }
504
                let content_box_height = height
504
                    - cell_box_props.padding.main_start(writing_mode)
504
                    - cell_box_props.padding.main_end(writing_mode)
504
                    - cell_box_props.border.main_start(writing_mode)
504
                    - cell_box_props.border.main_end(writing_mode);
504
                let y_offset = (content_box_height - content_height) * factor;
504
                if y_offset > 0.01 {
1008
                    for &c in &inflow {
504
                        if let Some(w) = tree.warm_mut(LayoutNodeId::new(c)) {
504
                            if let Some(pos) = w.relative_position.as_mut() {
504
                                pos.y += y_offset;
504
                            }
                        }
                    }
                }
72
            }
270
        }
        // Store position relative to table origin
846
        let position = LogicalPosition::from_main_cross(y, x, writing_mode);
        // Insert position into map so cache module can position the cell
846
        positions.insert(cell_info.node_index, position);
846
        debug_log!(
846
            ctx,
846
            "Cell at row={}, col={}: pos=({:.2}, {:.2}), size=({:.2}x{:.2})",
            cell_info.row,
            cell_info.column,
            x,
            y,
            width,
            height
        );
    }
333
    Ok(positions)
333
}
/// Gathers all inline content for `text3`, recursively laying out `inline-block` children
/// to determine their size and baseline before passing them to the text engine.
///
/// This function also assigns IFC membership to all participating nodes:
/// - The IFC root gets an `ifc_id` assigned
/// - Each text/inline child gets `ifc_membership` set with a reference back to the IFC root
///
/// This mapping enables efficient cursor hit-testing: when a text node is clicked,
/// we can find its parent IFC's `inline_layout_result` via `ifc_membership.ifc_root_layout_index`.
// +spec:display-property:63a38b - inline box boundaries and out-of-flow elements are ignored for text adjacency (white space, line-breaking, text-transform)
45191
fn collect_and_measure_inline_content<T: ParsedFontTrait>(
45191
    ctx: &mut LayoutContext<'_, T>,
45191
    text_cache: &mut TextLayoutCache,
45191
    tree: &mut LayoutTree,
45191
    ifc_root_index: usize,
45191
    constraints: &LayoutConstraints<'_>,
45191
) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
    use crate::solver3::layout_tree::{IfcId, IfcMembership};
    use crate::text3::cache::InlineContent;
45191
    let mut content = Vec::new();
45191
    let mut child_map = HashMap::new();
45191
    collect_and_measure_inline_content_impl(
45191
        ctx,
45191
        text_cache,
45191
        tree,
45191
        ifc_root_index,
45191
        constraints,
45191
        &mut content,
45191
        &mut child_map,
    )?;
    // O3-render: a split-preview PART displays only its byte slice of the
    // node's content (both parts collect the same full content; the range
    // partitions it — part 1 `[0, at)`, part 2 `[at, ∞)`).
45191
    if let Some((start, end)) = tree
45191
        .cold(LayoutNodeId::new(ifc_root_index))
45191
        .and_then(|c| c.preview_byte_range)
18
    {
18
        content = slice_inline_content_by_bytes(content, start as usize, end as usize);
45173
    }
45191
    Ok((content, child_map))
45191
}
/// Byte-slice inline content (flat text bytes; Text runs cut at char
/// boundaries, non-text items kept when their position falls inside the
/// range) — the read-side twin of the structural split's partition rule.
18
fn slice_inline_content_by_bytes(
18
    content: Vec<InlineContent>,
18
    start: usize,
18
    end: usize,
18
) -> Vec<InlineContent> {
18
    let mut out = Vec::new();
18
    let mut consumed = 0_usize;
36
    for item in content {
18
        match item {
18
            InlineContent::Text(mut run) => {
18
                let len = run.text.len();
18
                let item_start = consumed;
18
                let item_end = consumed + len;
18
                consumed = item_end;
18
                if item_end <= start || item_start >= end {
                    continue;
18
                }
18
                let cut_from = start.saturating_sub(item_start).min(len);
18
                let cut_to = (end - item_start).min(len);
18
                let cut_from = (0..=cut_from)
18
                    .rev()
18
                    .find(|&c| run.text.is_char_boundary(c))
18
                    .unwrap_or(0);
18
                let cut_to = (cut_to..=len)
18
                    .find(|&c| run.text.is_char_boundary(c))
18
                    .unwrap_or(len);
18
                if cut_from != 0 || cut_to != len {
18
                    run.text = Arc::from(&run.text[cut_from..cut_to]);
18
                    run.logical_start_byte = 0;
18
                }
18
                out.push(InlineContent::Text(run));
            }
            other => {
                if consumed >= start && consumed < end {
                    out.push(other);
                }
            }
        }
    }
18
    out
18
}
#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
45191
fn collect_and_measure_inline_content_impl<T: ParsedFontTrait>(
45191
    ctx: &mut LayoutContext<'_, T>,
45191
    text_cache: &mut TextLayoutCache,
45191
    tree: &mut LayoutTree,
45191
    ifc_root_index: usize,
45191
    constraints: &LayoutConstraints<'_>,
45191
    content: &mut Vec<InlineContent>,
45191
    child_map: &mut HashMap<ContentIndex, usize>,
45191
) -> Result<()> {
    use crate::solver3::layout_tree::{IfcId, IfcMembership};
45191
    debug_ifc_layout!(
43967
        ctx,
43967
        "collect_and_measure_inline_content: node_index={}",
        ifc_root_index
    );
    // Generate a unique IFC ID for this inline formatting context
45191
    let ifc_id = IfcId::unique();
    // Store IFC ID on the IFC root node
45191
    if let Some(cold_node) = tree.cold_mut(LayoutNodeId::new(ifc_root_index)) {
45191
        cold_node.ifc_id = Some(ifc_id);
45191
    }
    // [g129/g130 az-web-lift] `content` and `child_map` are now caller-provided out-params
    // (the by-value `(Vec, HashMap)` return mis-lifted its len on the web backend). The caller
    // passes them in EMPTY; this body fills them exactly as before. `child_map` maps the
    // `ContentIndex` used by text3 back to the `LayoutNode` index.
    // Track the current run index for IFC membership assignment
45191
    let mut current_run_index: u32 = 0;
    // [g134/g135 az-web-lift DIAG] out-param pointer + tree validity at _impl entry. The early Err is
    // the `tree.get(ifc_root_index).ok_or(InvalidTree)?` at 6449/6706 (no other `?` before the first
    // content push) — capture whether `tree` is valid (nodes.len) and tree.get(idx) actually works.
    #[cfg(feature = "web_lift")]
    unsafe {
        crate::az_mark((0x60690) as u32, (content as *const _ as usize as u32) as u32);
        crate::az_mark((0x60694) as u32, (ifc_root_index as u32 | 0xC0DE0000u32) as u32);
        crate::az_mark((0x606A8) as u32, (tree.nodes.len() as u32) as u32);
        crate::az_mark((0x606AC) as u32, (tree.get(ifc_root_index).is_some() as u32 | 0xC0DE0000u32) as u32);
        crate::az_mark((0x606B0) as u32, (tree.root as u32) as u32);
        // [g147 az-web-lift DIAG] CALLEE-side tree ptr + nodes.len indexed by ifc_root_index
        // (0x60940+ = nodes.len, 0x60960+ = tree ptr). Pair with layout_ifc's 0x60900+/0x60920+.
        let slot = (ifc_root_index & 7) * 4;
        crate::az_mark(((0x60940 + slot)) as u32, (tree.nodes.len() as u32) as u32);
        crate::az_mark(((0x60960 + slot)) as u32, ((&*tree as *const LayoutTree as usize) as u32) as u32);
    }
45191
    let ifc_root_node = tree.get(LayoutNodeId::new(ifc_root_index)).ok_or(LayoutError::InvalidTree)?;
    // [g135] reached past the 6449 tree.get.
    #[cfg(feature = "web_lift")]
    unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6449u32) as u32); }
    // Check if this is an anonymous IFC wrapper (has no DOM ID)
45191
    let is_anonymous = ifc_root_node.dom_node_id.is_none();
    // Get the DOM node ID of the IFC root, or find it from parent/children for anonymous boxes
    // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit properties from their enclosing box
45191
    let ifc_root_dom_id = if let Some(id) = ifc_root_node.dom_node_id { id } else {
        // Anonymous box - get DOM ID from parent or first child with DOM ID
111
        let parent_dom_id = ifc_root_node
111
            .parent
111
            .and_then(|p| tree.get(LayoutNodeId::new(p)))
111
            .and_then(|n| n.dom_node_id);
111
        if let Some(id) = parent_dom_id {
111
            id
        } else {
            // Try to find DOM ID from first child
            if let Some(id) = tree.children(ifc_root_index)
                .iter()
                .filter_map(|&child_idx| tree.get(LayoutNodeId::new(child_idx))).find_map(|n| n.dom_node_id) { id } else {
                debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
                return Ok(());
            }
        }
    };
    // Collect children to avoid holding an immutable borrow during iteration
45191
    let children: Vec<_> = tree.children(ifc_root_index).to_vec();
45191
    drop(ifc_root_node);
45191
    debug_ifc_layout!(
43967
        ctx,
43967
        "Node {} has {} layout children, is_anonymous={}",
        ifc_root_index,
43967
        children.len(),
        is_anonymous
    );
    // For anonymous IFC wrappers, we collect content from layout tree children
    // For regular IFC roots, we also check DOM children for text nodes
45191
    if is_anonymous {
        // Anonymous IFC wrapper - iterate over layout tree children and collect their content
124
        for (item_idx, &child_index) in children.iter().enumerate() {
124
            let content_index = ContentIndex {
124
                run_index: ifc_root_index as u32,
124
                item_index: item_idx as u32,
124
            };
124
            let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
124
            let Some(dom_id) = child_node.dom_node_id else {
                debug_warning!(
                    ctx,
                    "Anonymous IFC child at index {} has no DOM ID",
                    child_index
                );
                continue;
            };
124
            let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
            // Check if this is a text node
124
            if let NodeType::Text(ref text_content) = node_data.get_node_type() {
115
                debug_info!(
110
                    ctx,
110
                    "[collect_and_measure_inline_content] OK: Found text node (DOM {:?}) in anonymous wrapper: '{}'",
                    dom_id,
110
                    text_content.as_str()
                );
                // Get style from the TEXT NODE itself (dom_id), not the IFC root
                // This ensures inline styles like color: #666666 are applied to the text
115
                let style = crate::solver3::getters::get_style_properties_cached(
115
                        &mut ctx.style_cache,
115
                        ctx.styled_dom,
115
                        dom_id,
115
                        ctx.system_style.as_ref(),
115
                        PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
                    );
115
                let text_items = split_text_for_whitespace(
115
                    ctx.styled_dom,
115
                    dom_id,
115
                    text_content.as_str(),
115
                    &style,
                );
115
                content.extend(text_items);
115
                child_map.insert(content_index, child_index);
                // Set IFC membership on the text node - drop child_node borrow first
115
                drop(child_node);
115
                if let Some(warm_mut) = tree.warm_mut(LayoutNodeId::new(child_index)) {
115
                    warm_mut.ifc_membership = Some(IfcMembership {
115
                        ifc_id,
115
                        ifc_root_layout_index: ifc_root_index,
115
                        run_index: current_run_index,
115
                    });
115
                }
115
                current_run_index += 1;
115
                continue;
9
            }
            // A <br> forces a hard line break inside this IFC (see UA css: <br>
            // is inline). Without this it would collect as an empty inline span
            // and never break the line.
9
            if matches!(node_data.get_node_type(), NodeType::Br) {
                content.push(InlineContent::LineBreak(InlineBreak {
                    break_type: BreakType::Hard,
                    clear: ClearType::None,
                    content_index: content.len(),
                }));
                continue;
9
            }
            // +spec:positioning:17239f - abspos elements are taken out of flow and must
            // not contribute their content to this IFC (laid out independently).
9
            if matches!(
9
                get_position_type(ctx.styled_dom, Some(dom_id)),
                LayoutPosition::Absolute | LayoutPosition::Fixed
            ) {
                continue;
9
            }
            // Non-text inline child - add as shape for inline-block
9
            let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
9
            if display == LayoutDisplay::Inline {
                // Regular inline element - collect its text children
9
                let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
9
                collect_inline_span_recursive(
9
                    ctx,
9
                    tree,
9
                    dom_id,
9
                    &span_style,
9
                    content,
9
                    &children,
9
                    constraints,
                )?;
            } else {
                // +spec:display-property:a37a9a - atomic inline-level boxes treated as neutral characters in bidi reordering
                // This is an atomic inline-level box (e.g., inline-block, image).
                // We must determine its size and baseline before passing it to text3.
                // The intrinsic sizing pass has already calculated its preferred size.
                let intrinsic_size = tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
                let box_props = child_node.box_props.unpack();
                let styled_node_state = ctx
                    .styled_dom
                    .styled_nodes
                    .as_container()
                    .get(dom_id)
                    .map(|n| n.styled_node_state)
                    .unwrap_or_default();
                // Calculate tentative border-box size based on CSS properties
                let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
                    ctx.styled_dom,
                    Some(dom_id),
                    &constraints.containing_block_size,
                    intrinsic_size,
                    &box_props,
                    &ctx.viewport_size,
                )?;
                let writing_mode = get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state)
                    .unwrap_or_default();
                // Determine content-box size for laying out children
                let content_box_size = box_props.inner_size(tentative_size, writing_mode);
                // To find its height and baseline, we must lay out its contents.
                let child_wm_ctx = super::geometry::WritingModeContext::new(
                    writing_mode,
                    get_direction_property(ctx.styled_dom, dom_id, &styled_node_state)
                        .unwrap_or_default(),
                    get_text_orientation_property(ctx.styled_dom, dom_id, &styled_node_state)
                        .unwrap_or_default(),
                );
                let child_constraints = LayoutConstraints {
                    available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
                    writing_mode,
                    writing_mode_ctx: child_wm_ctx,
                    bfc_state: None,
                    text_align: TextAlign::Start,
                    containing_block_size: constraints.containing_block_size,
                    available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
                    fragmentainer: None,
                };
                // Drop the immutable borrow before calling layout_formatting_context
                drop(child_node);
                // Recursively lay out the inline-block to get its final height and baseline.
                let mut empty_float_cache = HashMap::new();
                let layout_result = layout_formatting_context(
                    ctx,
                    tree,
                    text_cache,
                    child_index,
                    &child_constraints,
                    &mut empty_float_cache,
                )?;
                let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
                // Replaced elements (image / VirtualView) have no flow content, so the
                // measured content_height is 0 — treat their auto height like an
                // explicit height (CSS/intrinsic-resolved tentative_size).
                let is_replaced_atomic = {
                    let nd = &ctx.styled_dom.node_data.as_container()[dom_id];
                    matches!(nd.get_node_type(), NodeType::Image(_)) || nd.is_virtual_view_node()
                };
                // Determine final border-box height
                let final_height = match css_height.unwrap_or_default() {
                    LayoutHeight::Auto if !is_replaced_atomic => {
                        let content_height = layout_result.output.overflow_size.height;
                        content_height
                            + box_props.padding.main_sum(writing_mode)
                            + box_props.border.main_sum(writing_mode)
                    }
                    _ => tentative_size.height,
                };
                let final_size = LogicalSize::new(tentative_size.width, final_height);
                // Update the node in the tree with its now-known used size.
                tree.get_mut(LayoutNodeId::new(child_index)).unwrap().used_size = Some(final_size);
                // CSS 2.2 § 10.8.1: inline-block baseline fallback
                // If overflow is not 'visible', use bottom margin edge as baseline
                let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
                let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
                let overflow_is_visible = matches!(
                    (overflow_x, overflow_y),
                    (LayoutOverflow::Visible, LayoutOverflow::Visible)
                );
                let baseline_offset = if overflow_is_visible {
                    layout_result.output.baseline.unwrap_or(final_height)
                } else {
                    final_height
                };
                // +spec:box-model:66ad24 - inline-axis margins, borders, padding respected for inline-level boxes (no collapsing)
                // The margin-box size is used so text3 positions inline-blocks with proper spacing
                let margin = &box_props.margin;
                let margin_box_width = final_size.width + margin.left + margin.right;
                let margin_box_height = final_size.height + margin.top + margin.bottom;
                // For inline-block shapes, text3 uses the content array index as run_index
                // and always item_index=0 for objects. We must match this when inserting into child_map.
                let shape_content_index = ContentIndex {
                    run_index: content.len() as u32,
                    item_index: 0,
                };
                content.push(InlineContent::Shape(InlineShape {
                    shape_def: ShapeDefinition::Rectangle {
                        size: crate::text3::cache::Size {
                            // Use margin-box size for positioning in inline flow
                            width: margin_box_width,
                            height: margin_box_height,
                        },
                        corner_radius: None,
                    },
                    fill: None,
                    stroke: None,
                    // Adjust baseline offset by top margin
                    baseline_offset: baseline_offset + margin.top,
                    alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
                    source_node_id: Some(dom_id),
                }));
                child_map.insert(shape_content_index, child_index);
            }
        }
111
        return Ok(());
45080
    }
    // Regular (non-anonymous) IFC root - check for list markers and use DOM traversal
    // Check if this IFC root OR its parent is a list-item and needs a marker
    // Case 1: IFC root itself is list-item (e.g., <li> with display: list-item)
    // Case 2: IFC root's parent is list-item (e.g., <li><text>...</text></li>)
45080
    let ifc_root_node = tree.get(LayoutNodeId::new(ifc_root_index)).ok_or(LayoutError::InvalidTree)?;
    // [g135] reached past the 6706 tree.get.
    #[cfg(feature = "web_lift")]
    unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6706u32) as u32); }
45080
    let mut list_item_dom_id: Option<NodeId> = None;
    // Check IFC root itself
45080
    if let Some(dom_id) = ifc_root_node.dom_node_id {
        use crate::solver3::getters::get_display_property;
45080
        if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(dom_id)) {
            use LayoutDisplay;
45080
            if display == LayoutDisplay::ListItem {
200
                debug_ifc_layout!(ctx, "IFC root NodeId({:?}) is list-item", dom_id);
200
                list_item_dom_id = Some(dom_id);
44880
            }
        }
    }
    // Check IFC root's parent
45080
    if list_item_dom_id.is_none() {
44880
        if let Some(parent_idx) = ifc_root_node.parent {
44761
            if let Some(parent_node) = tree.get(LayoutNodeId::new(parent_idx)) {
44761
                if let Some(parent_dom_id) = parent_node.dom_node_id {
                    use crate::solver3::getters::get_display_property;
44759
                    if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(parent_dom_id)) {
                        use LayoutDisplay;
44759
                        if display == LayoutDisplay::ListItem {
                            debug_ifc_layout!(
                                ctx,
                                "IFC root parent NodeId({:?}) is list-item",
                                parent_dom_id
                            );
                            list_item_dom_id = Some(parent_dom_id);
44759
                        }
                    }
2
                }
            }
119
        }
200
    }
    // If we found a list-item, generate markers
45080
    if let Some(list_dom_id) = list_item_dom_id {
200
        debug_ifc_layout!(
144
            ctx,
144
            "Found list-item (NodeId({:?})), generating marker",
            list_dom_id
        );
        // Find the layout node index for the list-item DOM node
200
        let list_item_layout_idx = tree
200
            .nodes
200
            .iter()
200
            .enumerate()
1630
            .find(|(idx, node)| {
1630
                node.dom_node_id == Some(list_dom_id) && tree.warm(LayoutNodeId::new(*idx)).and_then(|w| w.pseudo_element).is_none()
1630
            })
200
            .map(|(idx, _)| idx);
200
        if let Some(list_idx) = list_item_layout_idx {
            // Per CSS spec, the ::marker pseudo-element is the first child of the list-item
            // Find the ::marker pseudo-element in the list-item's children
200
            let marker_idx = tree.children(list_idx)
200
                .iter()
200
                .find(|&&child_idx| {
200
                    tree.warm(LayoutNodeId::new(child_idx))
200
                        .is_some_and(|w| w.pseudo_element == Some(PseudoElement::Marker))
200
                })
200
                .copied();
200
            if let Some(marker_idx) = marker_idx {
200
                debug_ifc_layout!(ctx, "Found ::marker pseudo-element at index {}", marker_idx);
                // Get the DOM ID for style resolution (marker references the same DOM node as
                // list-item)
200
                let list_dom_id_for_style = tree
200
                    .get(LayoutNodeId::new(marker_idx))
200
                    .and_then(|n| n.dom_node_id)
200
                    .unwrap_or(list_dom_id);
                // Get list-style-position to determine marker positioning
                // Default is 'outside' per CSS Lists Module Level 3
200
                let list_style_position =
200
                    get_list_style_position(ctx.styled_dom, Some(list_dom_id));
200
                let position_outside =
200
                    matches!(list_style_position, StyleListStylePosition::Outside);
200
                debug_ifc_layout!(
144
                    ctx,
144
                    "List marker list-style-position: {:?} (outside={})",
                    list_style_position,
                    position_outside
                );
                // Generate marker text segments - font fallback happens during shaping
200
                let base_style =
200
                    crate::solver3::getters::get_style_properties_cached(
200
                        &mut ctx.style_cache,
200
                        ctx.styled_dom,
200
                        list_dom_id_for_style,
200
                        ctx.system_style.as_ref(),
200
                        PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
                    );
200
                let marker_segments = generate_list_marker_segments(
200
                    tree,
200
                    ctx.styled_dom,
200
                    marker_idx, // Pass the marker index, not the list-item index
200
                    ctx.counters,
200
                    base_style,
200
                    ctx.debug_messages,
                );
200
                debug_ifc_layout!(
144
                    ctx,
144
                    "Generated {} list marker segments",
144
                    marker_segments.len()
                );
                // Add markers as InlineContent::Marker with position information
                // Outside markers will be positioned in the padding gutter by the layout engine
400
                for segment in marker_segments {
200
                    content.push(InlineContent::Marker {
200
                        run: segment,
200
                        position_outside,
200
                    });
200
                }
            } else {
                debug_ifc_layout!(
                    ctx,
                    "WARNING: List-item at index {} has no ::marker pseudo-element",
                    list_idx
                );
            }
        }
44880
    }
45080
    drop(ifc_root_node);
    // IMPORTANT: We need to traverse the DOM, not just the layout tree!
    //
    // According to CSS spec, a block container with inline-level children establishes
    // an IFC and should collect ALL inline content, including text nodes.
    // Text nodes exist in the DOM but might not have their own layout tree nodes.
    // Debug: Check what the node_hierarchy says about this node
45080
    let node_hier_item = &ctx.styled_dom.node_hierarchy.as_container()[ifc_root_dom_id];
45080
    debug_info!(
43858
        ctx,
43858
        "[collect_and_measure_inline_content] DEBUG: node_hier_item.first_child={:?}, \
43858
         last_child={:?}",
43858
        node_hier_item.first_child_id(ifc_root_dom_id),
43858
        node_hier_item.last_child_id()
    );
45080
    let ifc_root_node_data = &ctx.styled_dom.node_data.as_container()[ifc_root_dom_id];
    // SPECIAL CASE: If the IFC root itself is a text node (leaf node),
    // add its text content directly instead of iterating over children
45080
    if let NodeType::Text(ref text_content) = ifc_root_node_data.get_node_type() {
5393
        let style = crate::solver3::getters::get_style_properties_cached(
5393
                        &mut ctx.style_cache,
5393
                        ctx.styled_dom,
5393
                        ifc_root_dom_id,
5393
                        ctx.system_style.as_ref(),
5393
                        PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
                    );
5393
        let text_items = split_text_for_whitespace(
5393
            ctx.styled_dom,
5393
            ifc_root_dom_id,
5393
            text_content.as_str(),
5393
            &style,
        );
5393
        content.extend(text_items);
5393
        return Ok(());
39687
    }
39687
    let _ifc_root_node_type = match ifc_root_node_data.get_node_type() {
6298
        NodeType::Div => "Div",
        NodeType::Text(_) => "Text",
67
        NodeType::Body => "Body",
33322
        _ => "Other",
    };
    // [g138 az-web-lift] Collect `dom_children` HERE — immediately before the loop, AFTER the
    // get_node_type() calls above. Those calls were corrupting the `dom_children` Vec's stack-slot
    // header (g137 PROVED: `dom_children.len()` reads 1 right after `.collect()` but 0 in the loop's
    // `0..len` range a few calls later — the recurring SP-leak / stack-address mis-lift). With NO call
    // between this `.collect()` and the loop, the header survives the range evaluation + first index.
39687
    let dom_children: Vec<NodeId> = ifc_root_dom_id
39687
        .az_children(&ctx.styled_dom.node_hierarchy.as_container())
39687
        .collect();
    // [g139 az-web-lift] The loop's `dom_children.len()` read MIS-LIFTS to 0 even though the in-memory
    // value is 1 (g138: the volatile marker reads 1 but the loop's `0..len` range reads 0 with NOTHING
    // between — the optimizer's SROA'd len read is mis-tracked by the lift; only a FORCED/volatile read is
    // correct; same Vec-len mis-lift class as the original sret bug, here on std `collect()` which can't be
    // out-param'd). Read len via a volatile round-trip (guaranteed-correct, like the marker) and index via
    // get_unchecked (the index's bounds-check len read mis-lifts the same way; len is valid → sound).
    // [g195 — collect_and_measure_inline_content_impl is DEAD on the web lift (NOT lifted for hello-world
    // OR web-nested-text; both lay out via measure_intrinsic_widths + layout_flow instead). So this g139
    // Vec-len workaround never executes on the web lift → it's irrelevant/deletable (kept: harmless, and
    // unverified-dead for other layouts). The cron's "collect_and_measure Vec-len" target is a DEAD PATH.]
    #[cfg(feature = "web_lift")]
    let dom_children_len = unsafe {
        crate::az_mark((0x606B4) as u32, (dom_children.len() as u32) as u32);
        crate::az_mark((0x606A4) as u32, (0x0000_6863u32) as u32);
        crate::az_mark_read(0x606B4) as usize
    };
    #[cfg(not(feature = "web_lift"))]
39687
    let dom_children_len = dom_children.len();
40454
    for item_idx in 0..dom_children_len {
40454
        let dom_child_id = unsafe { *dom_children.get_unchecked(item_idx) };
40454
        let content_index = ContentIndex {
40454
            run_index: ifc_root_index as u32,
40454
            item_index: item_idx as u32,
40454
        };
40454
        let node_data = &ctx.styled_dom.node_data.as_container()[dom_child_id];
        // [g136] loop body entered; capture the FIRST child's node_type (does it read as Text?).
        #[cfg(feature = "web_lift")]
        unsafe {
            if item_idx == 0 {
                crate::az_mark((0x606B8) as u32, (match node_data.get_node_type() {
                    NodeType::Text(_) => 0xC0DE_7E70u32,
                    NodeType::Div => 0xC0DE_D11Fu32,
                    NodeType::Body => 0xC0DE_B0D1u32,
                    _ => 0xC0DE_0000u32,
                }) as u32);
            }
            crate::az_mark((0x606A4) as u32, (0x0000_6896u32) as u32);
        }
        // Check if this is a text node
40454
        if let NodeType::Text(ref text_content) = node_data.get_node_type() {
39668
            debug_info!(
38264
                ctx,
38264
                "[collect_and_measure_inline_content] OK: Found text node (DOM child {:?}): '{}'",
                dom_child_id,
38264
                text_content.as_str()
            );
            // Get style from the TEXT NODE itself (dom_child_id), not the IFC root
            // This ensures inline styles like color: #666666 are applied to the text
            // Uses split_text_for_whitespace to correctly handle white-space: pre with \n
39668
            let style = crate::solver3::getters::get_style_properties_cached(
39668
                        &mut ctx.style_cache,
39668
                        ctx.styled_dom,
39668
                        dom_child_id,
39668
                        ctx.system_style.as_ref(),
39668
                        PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
                    );
39668
            let text_items = split_text_for_whitespace(
39668
                ctx.styled_dom,
39668
                dom_child_id,
39668
                text_content.as_str(),
39668
                &style,
            );
39668
            content.extend(text_items);
            // [g136] TEXT branch taken + pushed; content.len now.
            #[cfg(feature = "web_lift")]
            unsafe {
                crate::az_mark((0x606A4) as u32, (0x0000_6905u32) as u32);
                crate::az_mark((0x606BC) as u32, (content.len() as u32) as u32);
            }
            // Set IFC membership on the text node's layout node (if it exists)
            // Text nodes may or may not have their own layout tree entry depending on
            // whether they're wrapped in an anonymous IFC wrapper
39668
            if let Some(&layout_idx) = tree.dom_to_layout.get(&dom_child_id).and_then(|v| v.first()) {
39668
                if let Some(warm_mut) = tree.warm_mut(layout_idx) {
39668
                    warm_mut.ifc_membership = Some(IfcMembership {
39668
                        ifc_id,
39668
                        ifc_root_layout_index: ifc_root_index,
39668
                        run_index: current_run_index,
39668
                    });
39668
                }
            }
39668
            current_run_index += 1;
39668
            continue;
786
        }
        // A <br> forces a hard line break inside this IFC (see UA css: <br> is
        // inline). It needs no layout node of its own — just emit the break.
786
        if matches!(node_data.get_node_type(), NodeType::Br) {
2
            content.push(InlineContent::LineBreak(InlineBreak {
2
                break_type: BreakType::Hard,
2
                clear: ClearType::None,
2
                content_index: content.len(),
2
            }));
2
            continue;
784
        }
        // For non-text nodes, find their corresponding layout tree node
784
        let child_index = children
784
            .iter()
2938
            .find(|&&idx| {
2938
                tree.get(LayoutNodeId::new(idx))
2938
                    .and_then(|n| n.dom_node_id)
2938
                    .is_some_and(|id| id == dom_child_id)
2938
            })
784
            .copied();
784
        let Some(child_index) = child_index else {
            debug_info!(
                ctx,
                "[collect_and_measure_inline_content] WARN: DOM child {:?} has no layout node",
                dom_child_id
            );
            continue;
        };
        // [g136] NON-TEXT branch taken (text child mis-classified?) — reached tree.get(child_index).
        #[cfg(feature = "web_lift")]
        unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6942u32) as u32); }
784
        let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
        // At this point we have a non-text DOM child with a layout node
784
        let dom_id = child_node.dom_node_id.unwrap();
        // +spec:positioning:17239f - abspos elements are taken out of flow
        // An out-of-flow child (position:absolute/fixed) is removed from normal flow
        // entirely: neither its box nor its (recursively) flattened text may participate
        // in this containing block's inline formatting context. It is laid out
        // independently by `process_out_of_flow_children`; contributing its content here
        // would double-render it at the static position. (Same predicate as
        // process_out_of_flow_children.)
784
        if matches!(
784
            get_position_type(ctx.styled_dom, Some(dom_id)),
            LayoutPosition::Absolute | LayoutPosition::Fixed
        ) {
            continue;
784
        }
784
        let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
784
        if display != LayoutDisplay::Inline {
            // This is an atomic inline-level box (e.g., inline-block, image).
            // We must determine its size and baseline before passing it to text3.
            // The intrinsic sizing pass has already calculated its preferred size.
86
            let intrinsic_size = tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
86
            let box_props = child_node.box_props.unpack();
86
            let styled_node_state = ctx
86
                .styled_dom
86
                .styled_nodes
86
                .as_container()
86
                .get(dom_id)
86
                .map(|n| n.styled_node_state)
86
                .unwrap_or_default();
            // Calculate tentative border-box size based on CSS properties
            // This correctly handles explicit width/height, box-sizing, and constraints
86
            let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
86
                ctx.styled_dom,
86
                Some(dom_id),
86
                &constraints.containing_block_size,
86
                intrinsic_size,
86
                &box_props,
86
                &ctx.viewport_size,
            )?;
86
            let writing_mode =
86
                get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
            // Determine content-box size for laying out children
86
            let content_box_size = box_props.inner_size(tentative_size, writing_mode);
86
            debug_info!(
86
                ctx,
86
                "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
86
                 tentative_border_box={:?}, content_box={:?}",
                dom_id,
                tentative_size,
                content_box_size
            );
            // To find its height and baseline, we must lay out its contents.
86
            let child_wm_ctx = super::geometry::WritingModeContext::new(
86
                writing_mode,
86
                get_direction_property(ctx.styled_dom, dom_id, &styled_node_state)
86
                    .unwrap_or_default(),
86
                get_text_orientation_property(ctx.styled_dom, dom_id, &styled_node_state)
86
                    .unwrap_or_default(),
            );
86
            let child_constraints = LayoutConstraints {
86
                available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
86
                writing_mode,
86
                writing_mode_ctx: child_wm_ctx,
86
                // Inline-blocks establish a new BFC, so no state is passed in.
86
                bfc_state: None,
86
                // Does not affect size/baseline of the container.
86
                text_align: TextAlign::Start,
86
                containing_block_size: constraints.containing_block_size,
86
                available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
86
                fragmentainer: None,
86
            };
            // Drop the immutable borrow before calling layout_formatting_context
86
            drop(child_node);
            // Recursively lay out the inline-block to get its final height and baseline.
            // Note: This does not affect its final position, only its dimensions.
86
            let mut empty_float_cache = HashMap::new();
86
            let layout_result = layout_formatting_context(
86
                ctx,
86
                tree,
86
                text_cache,
86
                child_index,
86
                &child_constraints,
86
                &mut empty_float_cache,
            )?;
86
            let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
            // Replaced elements (image / VirtualView) have no flow content, so the
            // measured content_height is 0 — treat their auto height like an explicit
            // height (use the CSS/intrinsic-resolved tentative_size). Fixes 0-height
            // images / VirtualViews laid out as atomic inline-blocks.
86
            let is_replaced_atomic = {
86
                let nd = &ctx.styled_dom.node_data.as_container()[dom_id];
86
                matches!(nd.get_node_type(), NodeType::Image(_)) || nd.is_virtual_view_node()
            };
            // Determine final border-box height
86
            let final_height = match css_height.clone().unwrap_or_default() {
76
                LayoutHeight::Auto if !is_replaced_atomic => {
                    // For auto height, add padding and border to the content height
76
                    let content_height = layout_result.output.overflow_size.height;
76
                    content_height
76
                        + box_props.padding.main_sum(writing_mode)
76
                        + box_props.border.main_sum(writing_mode)
                }
                // Explicit height (calculate_used_size_for_node gave the border-box
                // height), OR a replaced element's auto height (intrinsic/CSS-resolved).
10
                _ => tentative_size.height,
            };
86
            debug_info!(
86
                ctx,
86
                "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
86
                 layout_content_height={}, css_height={:?}, final_border_box_height={}",
                dom_id,
                layout_result.output.overflow_size.height,
                css_height,
                final_height
            );
86
            let final_size = LogicalSize::new(tentative_size.width, final_height);
            // Update the node in the tree with its now-known used size.
86
            tree.get_mut(LayoutNodeId::new(child_index)).unwrap().used_size = Some(final_size);
            // CSS 2.2 § 10.8.1: For inline-block elements, the baseline is the baseline of the
            // last line box in the normal flow, unless it has either no in-flow line boxes or
            // if its 'overflow' property has a computed value other than 'visible', in which
            // case the baseline is the bottom margin edge.
            //
            // `layout_result.output.baseline` returns the Y-position of the baseline measured
            // from the TOP of the content box. But `get_item_vertical_metrics` expects
            // `baseline_offset` to be the distance from the BOTTOM to the baseline.
            //
            // Conversion: baseline_offset_from_bottom = height - baseline_from_top
            //
            // If no baseline is found (e.g., the inline-block has no text), or if
            // overflow is not 'visible', we fall back to the bottom margin edge
            // (baseline_offset = 0, meaning baseline at bottom).
86
            let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
86
            let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
86
            let overflow_is_visible = matches!(
86
                (overflow_x, overflow_y),
                (LayoutOverflow::Visible, LayoutOverflow::Visible)
            );
86
            let baseline_from_top = layout_result.output.baseline;
86
            let baseline_offset = match baseline_from_top {
                Some(baseline_y) if overflow_is_visible => {
                    // baseline_y is measured from top of content box
                    // We need to add padding and border to get the position within the border-box
                    let content_box_top = box_props.padding.top + box_props.border.top;
                    let baseline_from_border_box_top = baseline_y + content_box_top;
                    // Convert to distance from bottom
                    (final_height - baseline_from_border_box_top).max(0.0)
                }
                _ => {
                    // No baseline found or overflow != visible - use bottom margin edge
86
                    0.0
                }
            };
86
            debug_info!(
86
                ctx,
86
                "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
86
                 baseline_from_top={:?}, final_height={}, baseline_offset_from_bottom={}",
                dom_id,
                baseline_from_top,
                final_height,
                baseline_offset
            );
            // Get margins for inline-block positioning
            // For inline-blocks, we need to include margins in the shape size
            // so that text3 positions them correctly with spacing
86
            let margin = &box_props.margin;
86
            let margin_box_width = final_size.width + margin.left + margin.right;
86
            let margin_box_height = final_size.height + margin.top + margin.bottom;
            // For inline-block shapes, text3 uses the content array index as run_index
            // and always item_index=0 for objects. We must match this when inserting into child_map.
86
            let shape_content_index = ContentIndex {
86
                run_index: content.len() as u32,
86
                item_index: 0,
86
            };
            // the box used for alignment is the margin box" - using margin_box_width/height here
86
            content.push(InlineContent::Shape(InlineShape {
86
                shape_def: ShapeDefinition::Rectangle {
86
                    size: crate::text3::cache::Size {
86
                        // Use margin-box size for positioning in inline flow
86
                        width: margin_box_width,
86
                        height: margin_box_height,
86
                    },
86
                    corner_radius: None,
86
                },
86
                fill: None,
86
                stroke: None,
86
                // Adjust baseline offset by top margin
86
                baseline_offset: baseline_offset + margin.top,
86
                alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
86
                source_node_id: Some(dom_id),
86
            }));
86
            child_map.insert(shape_content_index, child_index);
698
        } else if matches!(
698
            ctx.styled_dom.node_data.as_container()[dom_id].get_node_type(),
            NodeType::Image(_)
        ) {
            // +spec:replaced-elements:31a782 - replaced elements (img) not rendered purely by CSS box concepts
            // Images are replaced elements - they have intrinsic dimensions
            // and CSS width/height can constrain them
            // Re-get child_node since we dropped it earlier for the inline-block case
            let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
            let box_props = child_node.box_props.unpack();
            // Get intrinsic size from the image data or fall back to layout node
            let intrinsic_size = tree.warm(LayoutNodeId::new(child_index))
                .and_then(|w| w.intrinsic_sizes)
                .unwrap_or_else(|| IntrinsicSizes {
                    max_content_width: 50.0,
                    max_content_height: 50.0,
                    ..Default::default()
                });
            // Get styled node state for CSS property lookup
            let styled_node_state = ctx
                .styled_dom
                .styled_nodes
                .as_container()
                .get(dom_id)
                .map(|n| n.styled_node_state)
                .unwrap_or_default();
            // Calculate the used size respecting CSS width/height constraints
            let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
                ctx.styled_dom,
                Some(dom_id),
                &constraints.containing_block_size,
                intrinsic_size,
                &box_props,
                &ctx.viewport_size,
            )?;
            // Drop immutable borrow before mutable access
            drop(child_node);
            // Set the used_size on the layout node so paint_rect works correctly
            let final_size = LogicalSize::new(tentative_size.width, tentative_size.height);
            tree.get_mut(LayoutNodeId::new(child_index)).unwrap().used_size = Some(final_size);
            // Calculate display size for text3 (this is what text3 uses for positioning)
            let display_width = if final_size.width > 0.0 { 
                Some(final_size.width) 
            } else { 
                None 
            };
            let display_height = if final_size.height > 0.0 { 
                Some(final_size.height) 
            } else { 
                None 
            };
            content.push(InlineContent::Image(InlineImage {
                // Snapshot the NODE, not the ImageRef: paint resolves the live
                // content (overlay→DOM) at display-list build, so a runtime
                // image swap repaints without rebuilding this IFC. (The old
                // `Ref` snapshot froze the ImageRef here — inline `<img>`
                // swaps stayed invisible until an unrelated full relayout.)
                source: ImageSource::Node(dom_id),
                intrinsic_size: crate::text3::cache::Size {
                    width: intrinsic_size.max_content_width,
                    height: intrinsic_size.max_content_height,
                },
                display_size: if display_width.is_some() || display_height.is_some() {
                    Some(crate::text3::cache::Size {
                        width: display_width.unwrap_or(intrinsic_size.max_content_width),
                        height: display_height.unwrap_or(intrinsic_size.max_content_height),
                    })
                } else {
                    None
                },
                // Images are bottom-aligned with the baseline by default
                baseline_offset: 0.0,
                alignment: text3::cache::VerticalAlign::Baseline,
                object_fit: ObjectFit::Fill,
            }));
            // For images, text3 uses the content array index as run_index
            // and always item_index=0 for objects. We must match this.
            let image_content_index = ContentIndex {
                run_index: (content.len() - 1) as u32,  // -1 because we just pushed
                item_index: 0,
            };
            child_map.insert(image_content_index, child_index);
        } else {
            // This is a regular inline box (display: inline) - e.g., <span>, <em>, <strong>
            //
            // According to CSS Inline-3 spec §2, inline boxes are "transparent" wrappers
            // We must recursively collect their text children with inherited style
698
            debug_info!(
395
                ctx,
395
                "[collect_and_measure_inline_content] Found inline span (DOM {:?}), recursing",
                dom_id
            );
698
            let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
698
            collect_inline_span_recursive(
698
                ctx,
698
                tree,
698
                dom_id,
698
                &span_style,
698
                content,
698
                &children,
698
                constraints,
            )?;
        }
    }
    // [g134 az-web-lift DIAG] _impl reached its FINAL return; content.len as _impl sees it.
    #[cfg(feature = "web_lift")]
    unsafe {
        crate::az_mark((0x60698) as u32, (content.len() as u32) as u32);
        crate::az_mark((0x6069C) as u32, (0xC0DE069Cu32) as u32);
    }
39687
    Ok(())
45191
}
// +spec:display-property:c05c53 - inlinifying boxes can't contain block-level boxes; children are recursively inlinified
// it recursively inlinifies all of its in-flow children, so that no block-level descendants
// break up the inline formatting context in which it participates.
// +spec:display-property:aee879 - recursively inlinifies in-flow children of inline boxes
/// Recursively collects inline content from an inline span (display: inline) element.
///
/// According to CSS Inline Layout Module Level 3 §2:
///
/// "Inline boxes are transparent wrappers that wrap their content."
///
/// They don't create a new formatting context - their children participate in the
/// same IFC as the parent. This function processes:
///
/// - Text nodes: collected with the span's inherited style
/// - Nested inline spans: recursively descended
/// - Inline-blocks, images: measured and added as shapes
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
743
fn collect_inline_span_recursive<T: ParsedFontTrait>(
743
    ctx: &mut LayoutContext<'_, T>,
743
    tree: &mut LayoutTree,
743
    span_dom_id: NodeId,
743
    span_style: &StyleProperties,
743
    content: &mut Vec<InlineContent>,
743
    parent_children: &[usize], // Layout tree children of parent IFC
743
    constraints: &LayoutConstraints<'_>,
743
) -> Result<()> {
743
    debug_info!(
440
        ctx,
440
        "[collect_inline_span_recursive] Processing inline span {:?}",
        span_dom_id
    );
    // Get DOM children of this span
743
    let span_dom_children: Vec<NodeId> = span_dom_id
743
        .az_children(&ctx.styled_dom.node_hierarchy.as_container())
743
        .collect();
743
    debug_info!(
440
        ctx,
440
        "[collect_inline_span_recursive] Span has {} DOM children",
440
        span_dom_children.len()
    );
    // +spec:box-model:b7428d - empty inline boxes still have margins, padding, borders, line-height
    // +spec:box-model:cc79a4 - empty inline elements still have margins, padding, borders and line height
743
    if span_dom_children.is_empty() {
        let node_state = &ctx.styled_dom.styled_nodes.as_container()[span_dom_id].styled_node_state;
        let font_size = get_element_font_size(ctx.styled_dom, span_dom_id, node_state);
        let line_height_value = crate::solver3::getters::get_line_height_value(
            ctx.styled_dom, span_dom_id, node_state
        );
        let line_height = line_height_value
            .map_or(text3::cache::LineHeight::Normal, |v| {
                // Absolute px line-heights are stored as a negative normalized
                // value; a positive value is a unitless multiplier of font-size.
                let n = v.inner.normalized();
                let px = if n < 0.0 { -n } else { n * font_size };
                text3::cache::LineHeight::Px(px)
            });
        let cb_width = constraints.containing_block_size.main(constraints.writing_mode);
        let padding_top = get_css_padding_top(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let padding_bottom = get_css_padding_bottom(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let padding_left = crate::solver3::getters::get_css_padding_left(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let padding_right = crate::solver3::getters::get_css_padding_right(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let border_top = get_css_border_top_width(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let border_bottom = get_css_border_bottom_width(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let border_left = crate::solver3::getters::get_css_border_left_width(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let border_right = crate::solver3::getters::get_css_border_right_width(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let margin_left = crate::solver3::getters::get_css_margin_left(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let margin_right = crate::solver3::getters::get_css_margin_right(ctx.styled_dom, span_dom_id, node_state)
            .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
        let resolved_line_height = line_height.resolve(font_size, 0.0, 0.0, 0.0, 0);
        let total_height = resolved_line_height + padding_top + padding_bottom + border_top + border_bottom;
        let total_width = margin_left + padding_left + border_left
            + border_right + padding_right + margin_right;
        content.push(InlineContent::Shape(InlineShape {
            shape_def: ShapeDefinition::Rectangle {
                size: crate::text3::cache::Size {
                    width: total_width,
                    height: total_height,
                },
                corner_radius: None,
            },
            fill: None,
            stroke: None,
            baseline_offset: 0.0,
            alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, span_dom_id),
            source_node_id: Some(span_dom_id),
        }));
        return Ok(());
743
    }
1486
    for &child_dom_id in &span_dom_children {
743
        let node_data = &ctx.styled_dom.node_data.as_container()[child_dom_id];
        // CASE 1: Text node - collect with span's style
743
        if let NodeType::Text(ref text_content) = node_data.get_node_type() {
707
            debug_info!(
404
                ctx,
404
                "[collect_inline_span_recursive] ✓ Found text in span: '{}'",
404
                text_content.as_str()
            );
707
            let text_items = split_text_for_whitespace(
707
                ctx.styled_dom,
707
                child_dom_id,
707
                text_content.as_str(),
707
                &Arc::new(span_style.clone()),
            );
707
            content.extend(text_items);
707
            continue;
36
        }
        // CASE 1b: <br> inside an inline span forces a hard line break.
36
        if matches!(node_data.get_node_type(), NodeType::Br) {
            content.push(InlineContent::LineBreak(InlineBreak {
                break_type: BreakType::Hard,
                clear: ClearType::None,
                content_index: content.len(),
            }));
            continue;
36
        }
        // +spec:positioning:17239f - abspos elements are taken out of flow: an
        // out-of-flow descendant of an in-flow inline span must not contribute its
        // content to the enclosing IFC (it is laid out independently).
36
        if matches!(
36
            get_position_type(ctx.styled_dom, Some(child_dom_id)),
            LayoutPosition::Absolute | LayoutPosition::Fixed
        ) {
            continue;
36
        }
        // CASE 2: Element node - check its display type
36
        let child_display =
36
            get_display_property(ctx.styled_dom, Some(child_dom_id)).unwrap_or_default();
        // Find the corresponding layout tree node
36
        let child_index = parent_children
36
            .iter()
36
            .find(|&&idx| {
36
                tree.get(LayoutNodeId::new(idx))
36
                    .and_then(|n| n.dom_node_id)
36
                    .is_some_and(|id| id == child_dom_id)
36
            })
36
            .copied();
36
        match child_display {
            LayoutDisplay::Inline => {
                // Nested inline span - recurse with child's style
36
                debug_info!(
36
                    ctx,
36
                    "[collect_inline_span_recursive] Found nested inline span {:?}",
                    child_dom_id
                );
36
                let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
36
                collect_inline_span_recursive(
36
                    ctx,
36
                    tree,
36
                    child_dom_id,
36
                    &child_style,
36
                    content,
36
                    parent_children,
36
                    constraints,
                )?;
            }
            LayoutDisplay::InlineBlock => {
                // Inline-block inside span - measure and add as shape
                let Some(child_index) = child_index else {
                    debug_info!(
                        ctx,
                        "[collect_inline_span_recursive] WARNING: inline-block {:?} has no layout \
                         node",
                        child_dom_id
                    );
                    continue;
                };
                let child_node = tree.get(LayoutNodeId::new(child_index)).ok_or(LayoutError::InvalidTree)?;
                let intrinsic_size = tree.warm(LayoutNodeId::new(child_index)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
                let width = intrinsic_size.max_content_width;
                let styled_node_state = ctx
                    .styled_dom
                    .styled_nodes
                    .as_container()
                    .get(child_dom_id)
                    .map(|n| n.styled_node_state)
                    .unwrap_or_default();
                let writing_mode =
                    get_writing_mode(ctx.styled_dom, child_dom_id, &styled_node_state)
                        .unwrap_or_default();
                let child_wm_ctx = super::geometry::WritingModeContext::new(
                    writing_mode,
                    get_direction_property(ctx.styled_dom, child_dom_id, &styled_node_state)
                        .unwrap_or_default(),
                    get_text_orientation_property(ctx.styled_dom, child_dom_id, &styled_node_state)
                        .unwrap_or_default(),
                );
                let child_constraints = LayoutConstraints {
                    available_size: LogicalSize::new(width, f32::INFINITY),
                    writing_mode,
                    writing_mode_ctx: child_wm_ctx,
                    bfc_state: None,
                    text_align: TextAlign::Start,
                    containing_block_size: constraints.containing_block_size,
                    available_width_type: Text3AvailableSpace::Definite(width),
                    fragmentainer: None,
                };
                drop(child_node);
                let mut empty_float_cache = HashMap::new();
                let layout_result = layout_formatting_context(
                    ctx,
                    tree,
                    &mut TextLayoutCache::default(),
                    child_index,
                    &child_constraints,
                    &mut empty_float_cache,
                )?;
                let final_height = layout_result.output.overflow_size.height;
                let final_size = LogicalSize::new(width, final_height);
                tree.get_mut(LayoutNodeId::new(child_index)).unwrap().used_size = Some(final_size);
                // CSS 2.2 § 10.8.1: inline-block baseline fallback
                let overflow_x = get_overflow_x(ctx.styled_dom, child_dom_id, &styled_node_state).unwrap_or_default();
                let overflow_y = get_overflow_y(ctx.styled_dom, child_dom_id, &styled_node_state).unwrap_or_default();
                let overflow_is_visible = matches!(
                    (overflow_x, overflow_y),
                    (LayoutOverflow::Visible, LayoutOverflow::Visible)
                );
                let baseline_offset = if overflow_is_visible {
                    layout_result.output.baseline.unwrap_or(final_height)
                } else {
                    final_height
                };
                content.push(InlineContent::Shape(InlineShape {
                    shape_def: ShapeDefinition::Rectangle {
                        size: crate::text3::cache::Size {
                            width,
                            height: final_height,
                        },
                        corner_radius: None,
                    },
                    fill: None,
                    stroke: None,
                    baseline_offset,
                    alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
                    source_node_id: Some(child_dom_id),
                }));
                // Note: We don't add to child_map here because this is inside a span
                debug_info!(
                    ctx,
                    "[collect_inline_span_recursive] Added inline-block shape {}x{}",
                    width,
                    final_height
                );
            }
            _ => {
                // +spec:display-property:0684c4 - block box inlinified: inner display becomes flow-root (treated as atomic inline)
                // in-flow children of an inline box are recursively inlinified so they
                // don't break the IFC. Treat them as inline spans and recurse into their
                // children to collect text and inline content.
                debug_info!(
                    ctx,
                    "[collect_inline_span_recursive] Inlinifying block-level child {:?} \
                     (display: {:?}) inside inline span per css-display-3 §2.7",
                    child_dom_id,
                    child_display
                );
                let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
                collect_inline_span_recursive(
                    ctx,
                    tree,
                    child_dom_id,
                    &child_style,
                    content,
                    parent_children,
                    constraints,
                )?;
            }
        }
    }
743
    Ok(())
743
}
/// Positions a floated child within the BFC and updates the floating context.
/// This function is fully writing-mode aware.
6
fn position_floated_child(
6
    _child_index: usize,
6
    child_margin_box_size: LogicalSize,
6
    float_type: LayoutFloat,
6
    constraints: &LayoutConstraints<'_>,
6
    _bfc_content_box: LogicalRect,
6
    current_main_offset: f32,
6
    floating_context: &mut FloatingContext,
6
) -> Result<LogicalPosition> {
6
    let wm = constraints.writing_mode;
6
    let child_main_size = child_margin_box_size.main(wm);
6
    let child_cross_size = child_margin_box_size.cross(wm);
6
    let bfc_cross_size = constraints.available_size.cross(wm);
6
    let mut placement_main_offset = current_main_offset;
    loop {
        // 1. Determine the available cross-axis space at the current
        // `placement_main_offset`.
7
        let (available_cross_start, available_cross_end) = floating_context
7
            .available_line_box_space(
7
                placement_main_offset,
7
                placement_main_offset + child_main_size,
7
                bfc_cross_size,
7
                wm,
7
            );
7
        let available_cross_width = available_cross_end - available_cross_start;
        // 2. Check if the new float can fit in the available space.
7
        if child_cross_size <= available_cross_width {
            // It fits! Determine the final position and add it to the context.
            // +spec:floats:5cfc93 - float:right positions box at cross-end, content flows on left
4
            let final_cross_pos = match float_type {
2
                LayoutFloat::Left => available_cross_start,
                // +spec:floats:5cfc93 - float:right positions box at cross-end, content flows on left
1
                LayoutFloat::Right => available_cross_end - child_cross_size,
                LayoutFloat::None => {
1
                    return Err(LayoutError::PositioningFailed);
                }
            };
3
            let final_pos =
3
                LogicalPosition::from_main_cross(placement_main_offset, final_cross_pos, wm);
3
            let new_float_box = FloatBox {
3
                kind: float_type,
3
                rect: LogicalRect::new(final_pos, child_margin_box_size),
3
                margin: EdgeSizes::default(), // TODO: Pass actual margin if this function is used
3
            };
3
            floating_context.floats.push(new_float_box);
3
            return Ok(final_pos);
3
        }
        {
            // +spec:floats:3d89d8 - shift float downward when not enough horizontal room
            // It doesn't fit. We must move the float down past an obstacle.
            // Find the lowest main-axis end of all floats that are blocking
            // the current line.
3
            let mut next_main_offset = f32::INFINITY;
5
            for existing_float in &floating_context.floats {
2
                let float_main_start = existing_float.rect.origin.main(wm);
2
                let float_main_end = float_main_start + existing_float.rect.size.main(wm);
                // Consider only floats that are above or at the current placement line.
2
                if placement_main_offset < float_main_end {
2
                    next_main_offset = next_main_offset.min(float_main_end);
2
                }
            }
3
            if next_main_offset.is_infinite() {
                // This indicates an unrecoverable state, e.g., a float wider
                // than the container.
2
                return Err(LayoutError::PositioningFailed);
1
            }
1
            placement_main_offset = next_main_offset;
        }
    }
6
}
// CSS Property Getters
/// Get the CSS `float` property for a node.
34291
fn get_float_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutFloat {
34291
    let Some(id) = dom_id else {
1
        return LayoutFloat::None;
    };
34290
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
34290
    get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None)
34291
}
34291
fn get_clear_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutClear {
34291
    let Some(id) = dom_id else {
1
        return LayoutClear::None;
    };
34290
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
34290
    get_clear(styled_dom, id, node_state).unwrap_or(LayoutClear::None)
34291
}
/// Helper to determine if scrollbars are needed.
///
/// # CSS Spec Reference
/// CSS Overflow Module Level 3 § 3: Scrollable overflow
// +spec:block-formatting-context:50d915 - overflow-x handles horizontal, overflow-y handles vertical
// +spec:box-model:63d6f2 - scrollable overflow extends beyond padding edge, needs scroll mechanism
// +spec:box-model:45b5fb - scrollbar space subtracted from content area, inserted between inner border edge and outer padding edge
// +spec:box-model:70a0a4 - UAs must start assuming no scrollbars needed, recalculate if they are
// +spec:box-model:c1b0b2 - scrollbar gutter is space between inner border edge and outer padding edge
// +spec:overflow:4f5b99 - scrollable overflow rectangle: content_size is the minimal axis-aligned rect containing scrollable overflow
// +spec:overflow:e983f4 - overflow:auto/scroll boxes must allow user to access overflowed content via scrollbars
// +spec:overflow:97c257 - relative positioning causing overflow in auto/scroll boxes must trigger scrollbar creation
345489
#[must_use] pub fn check_scrollbar_necessity(
345489
    content_size: LogicalSize,
345489
    container_size: LogicalSize,
345489
    overflow_x: OverflowBehavior,
345489
    overflow_y: OverflowBehavior,
345489
    scrollbar_width_px: f32,
345489
) -> ScrollbarRequirements {
    // Use epsilon for float comparisons to avoid showing scrollbars due to 
    // floating-point rounding errors. Without this, content that exactly fits
    // may show scrollbars due to sub-pixel differences (e.g., 299.9999 vs 300.0).
    const EPSILON: f32 = 1.0;
    // +spec:height-calculation:c5af64 - assume no scrollbars initially; only add if content overflows
    // Determine if scrolling is needed based on overflow properties.
    // +spec:overflow:30a49c - start assuming no scrollbars, recalculate if needed
    // Note: scrollbar_width_px can be 0 for overlay scrollbars (e.g. macOS),
    // but we still need to register scroll nodes so that scrolling works —
    // overlay scrollbars just don't reserve any layout space.
345489
    let mut needs_horizontal = match overflow_x {
344984
        OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
46
        OverflowBehavior::Scroll => true,
459
        OverflowBehavior::Auto => content_size.width > container_size.width + EPSILON,
    };
345489
    let mut needs_vertical = match overflow_y {
344762
        OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
232
        OverflowBehavior::Scroll => true,
495
        OverflowBehavior::Auto => content_size.height > container_size.height + EPSILON,
    };
    // +spec:box-model:c3d73f - scrollbar presence affects available content area; padding preserved at scroll end
    // +spec:overflow:d79159 - scrollbar sizing: adding a scrollbar reduces available space,
    // which may cause content to overflow, confirming the scrollbar is needed (two-pass check)
    // A classic layout problem: a vertical scrollbar can reduce horizontal space,
    // causing a horizontal scrollbar to appear, which can reduce vertical space...
    // A full solution involves a loop, but this two-pass check handles most cases.
    // Only relevant when scrollbars reserve layout space (non-overlay).
345489
    if scrollbar_width_px > 0.0 {
345476
        if needs_vertical && !needs_horizontal && overflow_x == OverflowBehavior::Auto
154
            && content_size.width > (container_size.width - scrollbar_width_px) + EPSILON {
82
                needs_horizontal = true;
345394
            }
345476
        if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto
54
            && content_size.height > (container_size.height - scrollbar_width_px) + EPSILON {
18
                needs_vertical = true;
345458
            }
13
    }
    ScrollbarRequirements {
345489
        needs_horizontal,
345489
        needs_vertical,
345489
        scrollbar_width: if needs_vertical {
454
            scrollbar_width_px
        } else {
345035
            0.0
        },
345489
        scrollbar_height: if needs_horizontal {
203
            scrollbar_width_px
        } else {
345286
            0.0
        },
        // visual_width_px is set by the caller (compute_scrollbar_info_core)
        // since this function doesn't have access to the CSS style context.
        visual_width_px: 0.0,
    }
345489
}
/// Calculates a single collapsed margin from two adjoining vertical margins.
///
/// Implements the rules from CSS 2.1 section 8.3.1:
/// - If both margins are positive, the result is the larger of the two.
/// - If both margins are negative, the result is the more negative of the two.
/// - If the margins have mixed signs, they are effectively summed.
// +spec:margin-collapsing:814a26 - vertical margins between sibling blocks collapse
55252
#[must_use] pub fn collapse_margins(a: f32, b: f32) -> f32 {
55252
    if a.is_sign_positive() && b.is_sign_positive() {
55005
        a.max(b)
247
    } else if a.is_sign_negative() && b.is_sign_negative() {
89
        a.min(b)
    } else {
158
        a + b
    }
55252
}
/// Helper function to advance the pen position with margin collapsing.
///
/// This implements CSS 2.1 margin collapsing for adjacent block-level boxes in a BFC.
///
/// - `pen` - Current main-axis position (will be modified)
/// - `last_margin_bottom` - The bottom margin of the previous in-flow element
/// - `current_margin_top` - The top margin of the current element
///
/// # Returns
///
/// The new `last_margin_bottom` value (the bottom margin of the current element)
///
/// # CSS Spec Compliance
///
/// Per CSS 2.1 Section 8.3.1 "Collapsing margins":
///
/// - Adjacent vertical margins of block boxes collapse
/// - The resulting margin width is the maximum of the adjoining margins (if both positive)
/// - Or the sum of the most positive and most negative (if signs differ)
5
fn advance_pen_with_margin_collapse(
5
    pen: &mut f32,
5
    last_margin_bottom: f32,
5
    current_margin_top: f32,
5
) -> f32 {
    // Collapse the previous element's bottom margin with current element's top margin
5
    let collapsed_margin = collapse_margins(last_margin_bottom, current_margin_top);
    // Advance pen by the collapsed margin
5
    *pen += collapsed_margin;
    // Return collapsed_margin so caller knows how much space was actually added
5
    collapsed_margin
5
}
/// Checks if an element's border or padding prevents margin collapsing.
///
/// Per CSS 2.1 Section 8.3.1:
///
/// - Border between margins prevents collapsing
/// - Padding between margins prevents collapsing
///
/// # Arguments
///
/// - `box_props` - The box properties containing border and padding
/// - `writing_mode` - The writing mode to determine main axis
/// - `check_start` - If true, check main-start (top); if false, check main-end (bottom)
///
/// # Returns
///
/// `true` if border or padding exists and prevents collapsing
// +spec:box-model:ca8ceb - margin collapsing uses block-start/block-end per writing mode
172670
fn has_margin_collapse_blocker(
172670
    box_props: &BoxProps,
172670
    writing_mode: LayoutWritingMode,
172670
    check_start: bool, // true = check top/start, false = check bottom/end
172670
) -> bool {
172670
    if check_start {
        // Check if there's border-top or padding-top
80122
        let border_start = box_props.border.main_start(writing_mode);
80122
        let padding_start = box_props.padding.main_start(writing_mode);
80122
        border_start > 0.0 || padding_start > 0.0
    } else {
        // Check if there's border-bottom or padding-bottom
92548
        let border_end = box_props.border.main_end(writing_mode);
92548
        let padding_end = box_props.padding.main_end(writing_mode);
92548
        border_end > 0.0 || padding_end > 0.0
    }
172670
}
/// Checks if an element is empty (has no content).
///
/// Per CSS 2.1 Section 8.3.1:
///
/// > If a block element has no border, padding, inline content, height, or min-height,
/// > then its top and bottom margins collapse with each other.
///
/// # Arguments
///
/// - `node` - The layout node to check
///
/// # Returns
///
/// `true` if the element is empty and its margins can collapse internally
34109
fn is_empty_block(tree: &LayoutTree, node_index: usize) -> bool {
34109
    let Some(node) = tree.get(LayoutNodeId::new(node_index)) else {
2
        return true;
    };
    // Per CSS 2.2 § 8.3.1: An empty block is one that:
    // - Has zero computed 'min-height'
    // - Has zero or 'auto' computed 'height'
    // - Has no in-flow children
    // - Has no line boxes (no text/inline content)
    // Check if node has children
34107
    if !tree.children(node_index).is_empty() {
30890
        return false;
3217
    }
    // Check if node has inline content (text)
3217
    if tree.warm(LayoutNodeId::new(node_index)).and_then(|w| w.inline_layout_result.as_ref()).is_some() {
811
        return false;
2406
    }
    // Check if node has explicit height > 0
    // CSS 2.2 § 8.3.1: Elements with explicit height are NOT empty
2406
    if let Some(size) = node.used_size {
2405
        if size.height > 0.0 {
2332
            return false;
73
        }
1
    }
    // Empty block: no children, no inline content, no height
74
    true
34109
}
/// Generates marker text for a list item marker.
///
/// This function looks up the counter value from the cache and formats it
/// according to the list-style-type property.
///
/// Per CSS Lists Module Level 3, the `::marker` pseudo-element is the first child
/// of the list-item, and references the same DOM node. Counter resolution happens
/// on the list-item (parent) node.
432
fn generate_list_marker_text(
432
    tree: &LayoutTree,
432
    styled_dom: &StyledDom,
432
    marker_index: usize,
432
    counters: &HashMap<(usize, String), i32>,
432
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
432
) -> String {
    use crate::solver3::counters::format_counter;
    // Get the marker node
432
    let Some(marker_node) = tree.get(LayoutNodeId::new(marker_index)) else {
        return String::new();
    };
    // Verify this is actually a ::marker pseudo-element
    // Per spec, markers must be pseudo-elements, not anonymous boxes
432
    let marker_pseudo = tree.warm(LayoutNodeId::new(marker_index)).and_then(|w| w.pseudo_element);
432
    let marker_anonymous_type = tree.cold(LayoutNodeId::new(marker_index)).and_then(|c| c.anonymous_type);
432
    if marker_pseudo != Some(PseudoElement::Marker) {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::warning(format!(
                "[generate_list_marker_text] WARNING: Node {marker_index} is not a ::marker pseudo-element \
                 (pseudo={marker_pseudo:?}, anonymous_type={marker_anonymous_type:?})"
            )));
        }
        // Fallback for old-style anonymous markers during transition
        if marker_anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
            return String::new();
        }
432
    }
    // Get the parent list-item node (::marker is first child of list-item)
432
    let Some(list_item_index) = marker_node.parent else {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::error(
                "[generate_list_marker_text] ERROR: Marker has no parent".to_string(),
            ));
        }
        return String::new();
    };
432
    let Some(list_item_node) = tree.get(LayoutNodeId::new(list_item_index)) else {
        return String::new();
    };
432
    let Some(list_item_dom_id) = list_item_node.dom_node_id else {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::error(
                "[generate_list_marker_text] ERROR: List-item has no DOM ID".to_string(),
            ));
        }
        return String::new();
    };
432
    if let Some(msgs) = debug_messages {
144
        msgs.push(LayoutDebugMessage::info(format!(
144
            "[generate_list_marker_text] marker_index={marker_index}, list_item_index={list_item_index}, \
144
             list_item_dom_id={list_item_dom_id:?}"
144
        )));
288
    }
    // Get list-style-type from the list-item or its container
432
    let list_container_dom_id = list_item_node.parent.and_then(|grandparent_index| {
432
        tree.get(LayoutNodeId::new(grandparent_index)).and_then(|grandparent| grandparent.dom_node_id)
432
    });
    // Try to get list-style-type from the list container first,
    // then fall back to the list-item
432
    let list_style_type = list_container_dom_id.map_or_else(|| get_list_style_type(styled_dom, Some(list_item_dom_id)), |container_id| {
432
        let container_type = get_list_style_type(styled_dom, Some(container_id));
432
        if container_type == StyleListStyleType::default() {
432
            get_list_style_type(styled_dom, Some(list_item_dom_id))
        } else {
            container_type
        }
432
    });
    // Get the counter value for "list-item" counter from the LIST-ITEM node
    // Per CSS spec, counters are scoped to elements, and the list-item counter
    // is incremented at the list-item element, not the marker pseudo-element
432
    let counter_value = counters
432
        .get(&(list_item_index, "list-item".to_string()))
432
        .copied()
432
        .unwrap_or_else(|| {
            if let Some(msgs) = debug_messages {
                msgs.push(LayoutDebugMessage::warning(format!(
                    "[generate_list_marker_text] WARNING: No counter found for list-item at index \
                     {list_item_index}, defaulting to 1"
                )));
            }
            1
        });
432
    if let Some(msgs) = debug_messages {
144
        msgs.push(LayoutDebugMessage::info(format!(
144
            "[generate_list_marker_text] counter_value={counter_value} for list_item_index={list_item_index}"
144
        )));
288
    }
    // Format the counter according to the list-style-type
432
    let marker_text = format_counter(counter_value, list_style_type);
    // For ordered lists (non-symbolic markers), add a period and space
    // For unordered lists (symbolic markers like •, ◦, ▪), just add a space
432
    if matches!(
432
        list_style_type,
        StyleListStyleType::Decimal
            | StyleListStyleType::DecimalLeadingZero
            | StyleListStyleType::LowerAlpha
            | StyleListStyleType::UpperAlpha
            | StyleListStyleType::LowerRoman
            | StyleListStyleType::UpperRoman
            | StyleListStyleType::LowerGreek
            | StyleListStyleType::UpperGreek
    ) {
        format!("{marker_text}. ")
    } else {
432
        format!("{marker_text} ")
    }
432
}
/// Generates marker text segments for a list item marker.
///
/// Simply returns a single `StyledRun` with the marker text using the `base_style`.
/// The font stack in `base_style` already includes fallbacks with 100% Unicode coverage,
/// so font resolution happens during text shaping, not here.
432
fn generate_list_marker_segments(
432
    tree: &LayoutTree,
432
    styled_dom: &StyledDom,
432
    marker_index: usize,
432
    counters: &HashMap<(usize, String), i32>,
432
    base_style: Arc<StyleProperties>,
432
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
432
) -> Vec<StyledRun> {
    // Generate the marker text
432
    let marker_text =
432
        generate_list_marker_text(tree, styled_dom, marker_index, counters, debug_messages);
432
    if marker_text.is_empty() {
        return Vec::new();
432
    }
432
    if let Some(msgs) = debug_messages {
144
        let font_families: Vec<&str> = match &base_style.font_stack {
144
            text3::cache::FontStack::Stack(selectors) => {
20592
                selectors.iter().map(|f| f.family.as_str()).collect()
            }
            text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
        };
144
        msgs.push(LayoutDebugMessage::info(format!(
144
            "[generate_list_marker_segments] Marker text: '{marker_text}' with font stack: {font_families:?}"
        )));
288
    }
    // Return single segment - font fallback happens during shaping
    // List markers are generated content, not from DOM nodes
432
    vec![StyledRun {
432
        text: Arc::from(marker_text.as_str()),
432
        style: base_style,
432
        logical_start_byte: 0,
432
        source_node_id: None,
432
    }]
432
}
/// Returns true if a character has Unicode line breaking class BK (mandatory break)
/// or NL (next line). Per CSS Text 3 §5.1, these must be treated as forced line
/// breaks regardless of the white-space property value.
#[inline]
2330168
const fn is_bk_or_nl_class(c: char) -> bool {
2330168
    matches!(c, '\u{000B}' | '\u{000C}' | '\u{0085}' | '\u{2028}' | '\u{2029}')
2330168
}
/// Splits text at all forced break points: newlines (\n, \r\n, \r) and BK/NL class chars.
/// Used for white-space modes that preserve segment breaks (pre, pre-wrap, pre-line, break-spaces).
// +spec:white-space-processing:af4e3f - each newline/segment break in text is treated as a segment break, interpreted per white-space property
148
fn split_at_forced_breaks(text: &str) -> Vec<String> {
148
    let mut segments = Vec::new();
148
    let mut current = String::new();
148
    let mut chars = text.chars().peekable();
16358
    while let Some(c) = chars.next() {
16210
        if c == '\n' {
12118
            segments.push(std::mem::take(&mut current));
14026
        } else if c == '\r' {
8
            segments.push(std::mem::take(&mut current));
8
            if chars.peek() == Some(&'\n') {
3
                chars.next();
5
            }
4084
        } else if is_bk_or_nl_class(c) {
5
            segments.push(std::mem::take(&mut current));
4079
        } else {
4079
            current.push(c);
4079
        }
    }
148
    segments.push(current);
148
    segments
148
}
/// Splits text only at BK/NL class characters (not \n which is collapsed in normal/nowrap).
/// Used for white-space: normal/nowrap where \n is collapsed to space but BK/NL chars
/// still produce forced breaks per CSS Text 3 §5.1.
140663
fn split_at_bk_nl_chars(text: &str) -> Vec<String> {
140663
    let mut segments = Vec::new();
140663
    let mut current = String::new();
2326071
    for c in text.chars() {
2326071
        if is_bk_or_nl_class(c) {
3
            segments.push(std::mem::take(&mut current));
2326068
        } else {
2326068
            current.push(c);
2326068
        }
    }
140663
    segments.push(current);
140663
    segments
140663
}
/// Returns true if the character is East Asian (CJK) for the purposes of
/// segment break transformation rules (CSS Text Level 3, §4.1.3).
821
fn is_east_asian_wide(c: char) -> bool {
821
    let cp = c as u32;
    // CJK Unified Ideographs
821
    (0x4E00..=0x9FFF).contains(&cp)
813
    || (0x3400..=0x4DBF).contains(&cp)
813
    || (0x20000..=0x2A6DF).contains(&cp)
813
    || (0xF900..=0xFAFF).contains(&cp)
    // Hiragana
813
    || (0x3040..=0x309F).contains(&cp)
    // Katakana
809
    || (0x30A0..=0x30FF).contains(&cp)
808
    || (0x31F0..=0x31FF).contains(&cp)
    // CJK Radicals / Kangxi / Ideographic Description
808
    || (0x2E80..=0x2EFF).contains(&cp)
808
    || (0x2F00..=0x2FDF).contains(&cp)
808
    || (0x2FF0..=0x2FFF).contains(&cp)
    // CJK Symbols and Punctuation
808
    || (0x3000..=0x303F).contains(&cp)
807
    || (0x3200..=0x32FF).contains(&cp)
807
    || (0x3300..=0x33FF).contains(&cp)
    // Bopomofo
807
    || (0x3100..=0x312F).contains(&cp)
    // Hangul Syllables
806
    || (0xAC00..=0xD7AF).contains(&cp)
    // Fullwidth forms
804
    || (0xFF01..=0xFF60).contains(&cp)
801
    || (0xFFE0..=0xFFE6).contains(&cp)
821
}
// +spec:block-formatting-context:b78223 - fullwidth/wide chars treated as vertical script, halfwidth as horizontal per UAX#11
803
fn is_east_asian_fullwidth_or_wide(ch: char) -> bool {
803
    let cp = ch as u32;
    // Exclude Hangul
803
    if (0x1100..=0x11FF).contains(&cp)
802
        || (0x3130..=0x318F).contains(&cp)
801
        || (0xAC00..=0xD7AF).contains(&cp)
799
        || (0xA960..=0xA97F).contains(&cp)
798
        || (0xD7B0..=0xD7FF).contains(&cp)
    {
6
        return false;
797
    }
797
    is_east_asian_wide(ch)
790
        || (0xFF61..=0xFFDC).contains(&cp)
789
        || (0xFFE8..=0xFFEE).contains(&cp)
789
        || (0xA000..=0xA4CF).contains(&cp)
803
}
/// +spec:white-space-processing:159dbf - segment breaks converted to spaces (default transform)
/// +spec:white-space-processing:79891b - segment break transform: convert to space or remove
// +spec:white-space-processing:7e9529 - Segment break transformation rules (§4.1.3): collapse consecutive breaks, remove around ZWSP/CJK, else convert to space
/// Transforms segment breaks (newlines) in text according to CSS Text Level 3 §4.1.3.
/// - If adjacent to a zero-width space (U+200B), the segment break is removed.
/// - If both adjacent chars are East Asian F/W/H (not Hangul), removed entirely.
/// - Otherwise, converted to a single space.
140681
fn apply_segment_break_transform(text: &str) -> String {
140681
    let chars: Vec<char> = text.chars().collect();
140681
    let len = chars.len();
140681
    let mut result = String::with_capacity(text.len());
140681
    let mut i = 0;
2467582
    while i < len {
2326901
        let ch = chars[i];
2326901
        if ch == '\n' || ch == '\r' {
6225
            let break_end = if ch == '\r' && i + 1 < len && chars[i + 1] == '\n' {
1
                i + 2
            } else {
6224
                i + 1
            };
            // +spec:white-space-processing:3c3680 - remove tabs/spaces around segment break before transform
            // §4.1.1: remove collapsible whitespace around segment breaks
11250
            while result.ends_with(' ') || result.ends_with('\t') {
5025
                result.pop();
5025
            }
6225
            let mut after_idx = break_end;
10451
            while after_idx < len && (chars[after_idx] == ' ' || chars[after_idx] == '\t') {
4226
                after_idx += 1;
4226
            }
6225
            let char_before = result.chars().last();
6225
            let char_after = if after_idx < len { Some(chars[after_idx]) } else { None };
            // Rule 1: adjacent to zero-width space → remove
6225
            if char_before == Some('\u{200B}') || char_after == Some('\u{200B}') {
2
                // remove segment break
2
            }
            // Rule 2: both sides East Asian F/W/H (not Hangul) → remove
6223
            else if let (Some(before), Some(after)) = (char_before, char_after) {
789
                if is_east_asian_fullwidth_or_wide(before) && is_east_asian_fullwidth_or_wide(after) {
2
                    // remove segment break
787
                } else {
787
                    result.push(' ');
787
                }
5434
            } else {
5434
                result.push(' ');
5434
            }
6225
            i = after_idx;
2320676
        } else {
2320676
            result.push(ch);
2320676
            i += 1;
2320676
        }
    }
140681
    result
140681
}
// ============================================================================
// +spec:white-space-processing:b64e38 - parser may normalize/collapse whitespace before CSS; CSS cannot restore
// +spec:display-property:1389e3 - bidi control characters per UAX #9 for Unicode bidirectional algorithm
// +spec:display-property:aad99b - inline boxes can be split into fragments due to bidi text processing
// Bidi_Control property (UAX #9). These characters are ignored during white-space processing.
2332243
const fn is_bidi_control(c: char) -> bool {
2332243
    matches!(c,
        '\u{200E}' | // LEFT-TO-RIGHT MARK
        '\u{200F}' | // RIGHT-TO-LEFT MARK
        '\u{202A}' | // LEFT-TO-RIGHT EMBEDDING
        '\u{202B}' | // RIGHT-TO-LEFT EMBEDDING
        '\u{202C}' | // POP DIRECTIONAL FORMATTING
        '\u{202D}' | // LEFT-TO-RIGHT OVERRIDE
        '\u{202E}' | // RIGHT-TO-LEFT OVERRIDE
        '\u{2066}' | // LEFT-TO-RIGHT ISOLATE
        '\u{2067}' | // RIGHT-TO-LEFT ISOLATE
        '\u{2068}' | // FIRST STRONG ISOLATE
        '\u{2069}' | // POP DIRECTIONAL ISOLATE
        '\u{061C}'   // ARABIC LETTER MARK
    )
2332243
}
/// +spec:white-space-processing:1188f6 - only spaces, tabs, and segment breaks are document white space
/// Returns true if `c` is a CSS "document white space character" per CSS Text Level 3 §4.1.
/// Only spaces (U+0020), tabs (U+0009), and segment breaks (LF, CR, FF) qualify.
/// Other Unicode whitespace (e.g. U+00A0 non-breaking space) is NOT document white space.
#[inline]
2601345
const fn is_css_document_whitespace(c: char) -> bool {
2601345
    matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')
2601345
}
// +spec:white-space-processing:efbece - white-space property controls collapsing/preserving of formatting characters for rendering
// +spec:writing-modes:b87688 - inlines laid out with bidi reordering and white-space wrapping
// +spec:writing-modes:cdd4f1 - white space trimming before bidi reordering preserves end-of-line spaces per UAX9 L1
// white space characters are processed prior to line breaking and bidi reordering
// +spec:inline-block:381c0c - white-space property: collapsing, wrapping, and forced breaks per mode
// +spec:display-property:8acfaa - Phase I white-space collapsing for each inline in an IFC, ignoring bidi controls
/// Splits text content into `InlineContent` items based on white-space CSS property.
///
/// For `white-space: pre`, `pre-wrap`, and `pre-line`, newlines (`\n`) are treated as
/// forced line breaks per CSS Text Level 3 specification:
/// <https://www.w3.org/TR/css-text-3/#white-space-property>
///
/// Additionally, Unicode characters with BK or NL line breaking class (VT, FF, NEL, LS, PS)
/// are always treated as forced line breaks regardless of the white-space value.
///
/// This function:
/// 1. Checks the white-space property of the node (or its parent for text nodes)
/// 2. If `pre`, `pre-wrap`, or `pre-line`: splits text by `\n` and inserts `InlineContent::LineBreak`
/// 3. Otherwise: returns the text as a single `InlineContent::Text`
/// 4. In ALL modes: BK/NL class chars (VT, FF, NEL, LS, PS) produce forced breaks
///
/// Returns a Vec of `InlineContent` items that correctly represent line breaks.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
140794
pub fn split_text_for_whitespace(
140794
    styled_dom: &StyledDom,
140794
    dom_id: NodeId,
140794
    text: &str,
140794
    style: &Arc<StyleProperties>,
140794
) -> Vec<InlineContent> {
    // (characters with the Bidi_Control property) as if they were not there"
    // Strip bidi control characters before white-space processing so they don't
    // interfere with collapsing (e.g. a bidi mark between two spaces).
    let text_owned;
140794
    let text: &str = if text.chars().any(is_bidi_control) {
5
        text_owned = text.chars().filter(|c| !is_bidi_control(*c)).collect::<String>();
1
        &text_owned
    } else {
140793
        text
    };
    // Get the white-space property - TEXT NODES inherit from parent!
    // We need to check the parent element's white-space, not the text node itself
140794
    let node_hierarchy = styled_dom.node_hierarchy.as_container();
140794
    let parent_id = node_hierarchy[dom_id].parent_id();
    // Try parent first, then fall back to the node itself
140794
    let white_space = parent_id.map_or(StyleWhiteSpace::Normal, |parent| {
140794
        let styled_nodes = styled_dom.styled_nodes.as_container();
140794
        let parent_state = styled_nodes
140794
            .get(parent)
140794
            .map(|n| n.styled_node_state)
140794
            .unwrap_or_default();
140794
        match get_white_space_property(styled_dom, parent, &parent_state) {
140788
            MultiValue::Exact(ws) => ws,
6
            _ => StyleWhiteSpace::Normal,
        }
140794
    });
140794
    let mut result = Vec::new();
    // +spec:white-space-processing:3a0f58 - HTML newlines normalized to U+000A, each treated as segment break
    // +spec:white-space-processing:6eb1a2 - CR (U+000D) not treated as segment break by HTML; handle if inserted via DOM
    // HTML parsers convert \r to \n during preprocessing, but \r can survive
    // via escape sequences (e.g. &#x0d;). Any remaining U+000D must be
    // treated identically to U+000A (line feed).
    let text_cr;
140794
    let text: &str = if text.contains('\r') {
2
        text_cr = text.replace("\r\n", "\n").replace('\r', "\n");
2
        &text_cr
    } else {
140792
        text
    };
    // +spec:white-space-processing:bd11da - white-space property: new lines, spaces/tabs, wrapping per value table
    // +spec:white-space-processing:b166c5 - segment breaks preserved as forced line feeds for pre/pre-wrap/break-spaces/pre-line
    // For `pre`, `pre-wrap`, `pre-line`, and `break-spaces`, newlines must be preserved as forced breaks
    // CSS Text Level 3: "Newlines in the source will be honored as forced line breaks."
140794
    match white_space {
        StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => {
            // Pre, pre-wrap, break-spaces: preserve whitespace and honor newlines
            // Split by newlines and BK/NL class chars, insert LineBreak between parts
            // Also handle tab characters (\t) by inserting InlineContent::Tab
115
            let segments = split_at_forced_breaks(text);
115
            let segment_count = segments.len();
115
            let mut content_index = 0;
2200
            for (seg_idx, segment) in segments.into_iter().enumerate() {
                // Split the segment by tab characters and insert Tab elements
2200
                let mut tab_parts = segment.split('\t').peekable();
4419
                while let Some(part) = tab_parts.next() {
2219
                    if !part.is_empty() {
2200
                        result.push(InlineContent::Text(StyledRun {
2200
                            text: Arc::from(part),
2200
                            style: Arc::clone(style),
2200
                            logical_start_byte: 0,
2200
                            source_node_id: Some(dom_id),
2200
                        }));
2200
                    }
2219
                    if tab_parts.peek().is_some() {
19
                        result.push(InlineContent::Tab { style: Arc::clone(style) });
2200
                    }
                }
2200
                if seg_idx + 1 < segment_count {
2085
                    result.push(InlineContent::LineBreak(InlineBreak {
2085
                        break_type: BreakType::Hard,
2085
                        clear: ClearType::None,
2085
                        content_index,
2085
                    }));
2085
                    content_index += 1;
2112
                }
            }
        }
        StyleWhiteSpace::PreLine => {
            // Pre-line: collapse whitespace but honor newlines and BK/NL class chars
20
            let segments = split_at_forced_breaks(text);
20
            let segment_count = segments.len();
20
            let mut content_index = 0;
49
            for (seg_idx, segment) in segments.into_iter().enumerate() {
                // Collapse only CSS document white space within the line (not all Unicode whitespace)
49
                let collapsed: String = segment
317
                    .split(|c: char| is_css_document_whitespace(c))
109
                    .filter(|s| !s.is_empty())
49
                    .collect::<Vec<_>>()
49
                    .join(" ");
49
                if !collapsed.is_empty() {
40
                    result.push(InlineContent::Text(StyledRun {
40
                        text: Arc::from(collapsed.as_str()),
40
                        style: Arc::clone(style),
40
                        logical_start_byte: 0,
40
                        source_node_id: Some(dom_id),
40
                    }));
40
                }
49
                if seg_idx + 1 < segment_count {
29
                    result.push(InlineContent::LineBreak(InlineBreak {
29
                        break_type: BreakType::Hard,
29
                        clear: ClearType::None,
29
                        content_index,
29
                    }));
29
                    content_index += 1;
29
                }
            }
        }
        StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
            // +spec:white-space-processing:adbebb - Phase I collapsing for normal/nowrap modes
            // CSS Text Level 3, Section 4.1.1 - Phase I: Collapsing and Transformation
            // https://www.w3.org/TR/css-text-3/#white-space-phase-1
            //
            // For `white-space: normal` and `nowrap`:
            // 1. Segment breaks are transformed per §4.1.3
            // 2. Any sequence of consecutive spaces/tabs is collapsed to a single space
            // 3. Leading/trailing spaces at line boundaries are handled during line layout
            //
            // are forced breaks regardless of white-space value. Split on them first,
            // then collapse whitespace within each segment.
140659
            let segments = split_at_bk_nl_chars(text);
140659
            let segment_count = segments.len();
140659
            let mut content_index = 0;
140660
            for (seg_idx, segment) in segments.into_iter().enumerate() {
140660
                let after_segment_breaks = apply_segment_break_transform(&segment);
                // Collapse document white space within this segment (normal/nowrap rules)
140660
                let collapsed: String = after_segment_breaks
140660
                    .chars()
2321821
                    .map(|c| if is_css_document_whitespace(c) { ' ' } else { c })
140660
                    .collect::<String>()
140660
                    .split(' ')
408412
                    .filter(|s| !s.is_empty())
140660
                    .collect::<Vec<_>>()
140660
                    .join(" ");
140660
                let final_text = if collapsed.is_empty() && !segment.is_empty() {
1007
                    " ".to_string()
139653
                } else if !collapsed.is_empty() {
                    // Check if original had leading/trailing document whitespace
139598
                    let had_leading = segment.chars().next().is_some_and(is_css_document_whitespace);
139598
                    let had_trailing = segment.chars().last().is_some_and(is_css_document_whitespace);
139598
                    let mut r = String::new();
139598
                    if had_leading { r.push(' '); }
139598
                    r.push_str(&collapsed);
139598
                    if had_trailing && !had_leading { r.push(' '); }
139301
                    else if had_trailing && had_leading && collapsed.is_empty() { /* already have one space */ }
139301
                    else if had_trailing { r.push(' '); }
139598
                    r
                } else {
55
                    collapsed
                };
140660
                if !final_text.is_empty() {
140605
                    result.push(InlineContent::Text(StyledRun {
140605
                        text: Arc::from(final_text.as_str()),
140605
                        style: Arc::clone(style),
140605
                        logical_start_byte: 0,
140605
                        source_node_id: Some(dom_id),
140605
                    }));
140605
                }
                // Insert forced break between segments (for BK/NL chars)
140660
                if seg_idx + 1 < segment_count {
1
                    result.push(InlineContent::LineBreak(InlineBreak {
1
                        break_type: BreakType::Hard,
1
                        clear: ClearType::None,
1
                        content_index,
1
                    }));
1
                    content_index += 1;
140659
                }
            }
        }
    }
    // +spec:white-space-processing:5e3f70 - text-transform applied after Phase I collapsing, before Phase II trimming
    // This means full-width only transforms spaces (U+0020) to U+3000 IDEOGRAPHIC SPACE
    // within preserved white space, because non-preserved spaces were already collapsed in Phase I above.
140794
    let text_transform = style.text_transform;
140794
    if text_transform != text3::cache::TextTransform::None {
108
        for item in &mut result {
54
            if let InlineContent::Text(run) = item {
54
                run.text = Arc::from(apply_text_transform(&run.text, text_transform).as_str());
54
            }
        }
140740
    }
140794
    result
140794
}
76
fn apply_text_transform(text: &str, transform: text3::cache::TextTransform) -> String {
    use crate::text3::cache::TextTransform;
76
    match transform {
5
        TextTransform::None => text.to_string(),
31
        TextTransform::Uppercase => text.to_uppercase(),
28
        TextTransform::Lowercase => text.to_lowercase(),
        TextTransform::Capitalize => {
5
            let mut result = String::with_capacity(text.len());
5
            let mut prev_is_word_boundary = true;
31
            for c in text.chars() {
31
                if prev_is_word_boundary && c.is_alphabetic() {
7
                    for uc in c.to_uppercase() {
7
                        result.push(uc);
7
                    }
7
                    prev_is_word_boundary = false;
                } else {
24
                    result.push(c);
24
                    prev_is_word_boundary = c.is_whitespace() || c.is_ascii_punctuation();
                }
            }
5
            result
        }
        TextTransform::FullWidth => {
            // Full-width transforms ASCII characters to their full-width equivalents.
            // Spaces (U+0020) become U+3000 IDEOGRAPHIC SPACE — but only those that
            // survived Phase I collapsing (i.e. preserved white space).
9
            text.chars().map(|c| match c {
1
                ' ' => '\u{3000}',  // U+0020 SPACE -> U+3000 IDEOGRAPHIC SPACE
6
                '!' ..= '~' => {
                    // ASCII printable range U+0021..U+007E -> fullwidth U+FF01..U+FF5E
3
                    char::from_u32(c as u32 - 0x0021 + 0xFF01).unwrap_or(c)
                }
5
                _ => c,
9
            }).collect()
        }
    }
76
}
// ============================================================================
// INITIAL LETTER / DROP CAPS STUB
// ============================================================================
/// Computes the geometric exclusion area for an initial letter (drop cap).
///
/// CSS Inline Layout Module Level 3, section 3:
/// The `initial-letter` property specifies styling for dropped, raised, and sunken
/// initial letters. When set, the first glyph(s) of the first line are enlarged to
/// span multiple lines, with the remaining text wrapping around them.
///
// +spec:box-model:c93797 - initial-letter alignment points determined from contents (not border-box)
///
/// # Algorithm
///
/// 1. The letter box height spans `size` lines: `height = size * line_height`.
/// 2. The letter box width is estimated using a typical capital letter aspect ratio
///    (cap-height-to-advance-width ~0.7 for Latin text). A proper implementation
///    would measure the actual glyph, but this gives a reasonable default.
/// 3. The letter is positioned at the inline-start of the first line.
/// 4. The `sink` value determines how many lines the letter drops below the
///    first baseline. When `sink == size`, this is a classic drop cap.
///    When `sink < size`, the letter rises above the first line (raised cap).
/// 5. A small gap (4px default) is added between the letter box and adjacent text.
///
/// # Parameters
/// - `initial_letter_size`: The number of lines the initial letter should span (e.g., 3.0)
/// - `initial_letter_sink`: How many lines the letter sinks below the first line
/// - `content_box_width`: Available width in the content box (for clamping)
/// - `line_height`: The computed line height for the containing block
///
/// # Returns
/// A tuple of `(letter_width, letter_height)` representing the space reserved for
/// the initial letter exclusion, or `(0.0, 0.0)` if the parameters are invalid.
///
/// The caller should use these dimensions to create a float-like exclusion at the
/// start of the block container, causing subsequent lines to wrap around the letter.
// +spec:width-calculation:7f4f68 - initial-letter-wrap exclusion area (none behavior; first/grid require glyph outlines)
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
30
#[must_use] pub fn layout_initial_letter(
30
    initial_letter_size: f32,
30
    initial_letter_sink: u32,
30
    content_box_width: f32,
30
    line_height: f32,
30
) -> (f32, f32) {
    // Estimate the letter width using a typical Latin capital letter aspect ratio.
    // The advance width of a capital letter is approximately 0.7x the cap height.
    // This is a heuristic; a full implementation would measure the actual glyph(s).
    const CAP_WIDTH_RATIO: f32 = 0.7;
    // Add a small gap between the letter box and the adjacent inline content.
    // CSS Inline Level 3 section 3.5: browsers typically add ~4px padding.
    const LETTER_GAP: f32 = 4.0;
    // Guard against degenerate values
30
    if initial_letter_size <= 0.0 || line_height <= 0.0 || content_box_width <= 0.0 {
6
        return (0.0, 0.0);
24
    }
    // +spec:overflow:dd0679 - auto-sized initial letter content box fits exactly to content; alignment props do not apply
    // +spec:width-calculation:170742 - atomic initial letters with auto block size use inline initial letter sizing
    // CSS Inline Level 3 section 3.3: The initial letter box height spans `size` lines.
24
    let letter_height = initial_letter_size * line_height;
24
    let letter_width_raw = letter_height * CAP_WIDTH_RATIO;
24
    let letter_width = (letter_width_raw + LETTER_GAP).min(content_box_width);
    // +spec:containing-block:67fd99 - block-axis positioning: size >= sink shifts by (sink-1)*line_height toward block-end
    // The actual exclusion height accounts for the sink value.
    // sink == size means the letter is fully dropped (classic drop cap).
    // sink < size means part of the letter rises above the first line (raised cap).
    // The exclusion area height is always `sink * line_height` since that's how
    // many lines of subsequent text need to wrap around the letter.
24
    let exclusion_height = (initial_letter_sink as f32) * line_height;
    // Use the larger of exclusion_height and letter_height as the actual
    // vertical space consumed. For raised caps (sink < size), the letter
    // extends above the first line but the exclusion only covers sink lines.
    // For sunken caps (sink >= size), the exclusion covers the full letter height.
24
    let effective_height = exclusion_height.max(letter_height);
24
    (letter_width, effective_height)
30
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
    use azul_core::dom::{AttributeType, Dom, IdOrClass};
    use super::*;
    use crate::{
        solver3::geometry::{MarginAuto, PackedBoxProps},
        text3::cache::{OverflowInfo, TextTransform, UnifiedLayout},
    };
    // ------------------------------------------------------------------
    // Fixtures
    // ------------------------------------------------------------------
    const HTB: LayoutWritingMode = LayoutWritingMode::HorizontalTb;
    const VRL: LayoutWritingMode = LayoutWritingMode::VerticalRl;
    fn size(w: f32, h: f32) -> LogicalSize {
        LogicalSize::new(w, h)
    }
    fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
        EdgeSizes {
            top,
            right,
            bottom,
            left,
        }
    }
    fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
        LogicalRect::new(LogicalPosition::new(x, y), size(w, h))
    }
    fn box_props(margin: EdgeSizes, border: EdgeSizes, padding: EdgeSizes) -> BoxProps {
        BoxProps {
            margin,
            padding,
            border,
            margin_auto: MarginAuto::default(),
        }
    }
    fn hot(
        parent: Option<usize>,
        used_size: Option<LogicalSize>,
        bp: &BoxProps,
    ) -> LayoutNodeHot {
        LayoutNodeHot {
            box_props: PackedBoxProps::pack(bp),
            dom_node_id: None,
            used_size,
            formatting_context: FormattingContext::Block {
                establishes_new_context: false,
            },
            parent,
        }
    }
    /// Builds a `LayoutTree` from hot nodes + per-node child lists.
    fn build_tree(
        nodes: Vec<LayoutNodeHot>,
        warm: Vec<LayoutNodeWarm>,
        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,
            cold: vec![LayoutNodeCold::default(); n],
            root: 0,
            dom_to_layout: BTreeMap::new(),
            children_arena,
            children_offsets,
            subtree_needs_intrinsic: Vec::new(),
        }
    }
    /// An inline layout result carrying `item_count` (always-empty) items.
    /// Only `items.is_empty()` is ever inspected by the functions under test.
    fn empty_inline_layout() -> CachedInlineLayout {
        CachedInlineLayout::new(
            Arc::new(UnifiedLayout {
                items: Vec::new(),
                overflow: OverflowInfo::default(),
            }),
            Text3AvailableSpace::MaxContent,
            false,
        )
    }
    fn constraints(available: LogicalSize, wm: LayoutWritingMode) -> LayoutConstraints<'static> {
        LayoutConstraints {
            available_size: available,
            writing_mode: wm,
            writing_mode_ctx: super::super::geometry::WritingModeContext::default(),
            bfc_state: None,
            text_align: TextAlign::Start,
            containing_block_size: available,
            available_width_type: Text3AvailableSpace::Definite(available.width),
            fragmentainer: None,
        }
    }
    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)
    }
    /// `body(0) > div.p(1) > text(2)` — DOM ids are the depth-first pre-order index.
    fn text_dom(text: &str, css_str: &str) -> StyledDom {
        styled(
            Dom::create_body().with_child(
                Dom::create_div()
                    .with_ids_and_classes(vec![IdOrClass::Class("p".into())].into())
                    .with_child(Dom::create_text_do_not_use_without_block_level_wrapper(text)),
            ),
            css_str,
        )
    }
    const TEXT_NODE: NodeId = NodeId::new(2);
    const DIV_NODE: NodeId = NodeId::new(1);
    fn plain_style() -> Arc<StyleProperties> {
        Arc::new(StyleProperties::default())
    }
    fn text_of(item: &InlineContent) -> Option<&str> {
        match item {
            InlineContent::Text(run) => Some(&*run.text),
            _ => None,
        }
    }
    // ==================================================================
    // OverflowBehavior (predicates)
    // ==================================================================
    const ALL_OVERFLOW: [OverflowBehavior; 5] = [
        OverflowBehavior::Visible,
        OverflowBehavior::Hidden,
        OverflowBehavior::Clip,
        OverflowBehavior::Scroll,
        OverflowBehavior::Auto,
    ];
    #[test]
    fn overflow_behavior_is_clipped_is_exhaustive_and_total() {
        assert!(!OverflowBehavior::Visible.is_clipped());
        assert!(OverflowBehavior::Hidden.is_clipped());
        assert!(OverflowBehavior::Clip.is_clipped());
        assert!(OverflowBehavior::Scroll.is_clipped());
        assert!(OverflowBehavior::Auto.is_clipped());
        // Visible is the only non-clipping value.
        assert_eq!(ALL_OVERFLOW.iter().filter(|o| o.is_clipped()).count(), 4);
    }
    #[test]
    fn overflow_behavior_is_scroll_only_for_scroll_and_auto() {
        assert!(!OverflowBehavior::Visible.is_scroll());
        assert!(!OverflowBehavior::Hidden.is_scroll());
        assert!(!OverflowBehavior::Clip.is_scroll());
        assert!(OverflowBehavior::Scroll.is_scroll());
        assert!(OverflowBehavior::Auto.is_scroll());
    }
    #[test]
    fn overflow_behavior_scroll_implies_clipped_invariant() {
        // A scrollable box always clips: is_scroll() must be a subset of is_clipped().
        for o in ALL_OVERFLOW {
            assert!(
                !o.is_scroll() || o.is_clipped(),
                "{o:?} is scroll but not clipped"
            );
        }
    }
    // ==================================================================
    // BfcLayoutResult::from_output / BfcState::new / TableLayoutContext::new
    // ==================================================================
    #[test]
    fn bfc_layout_result_from_output_preserves_output_and_nulls_escaped_margins() {
        let mut positions = BTreeMap::new();
        positions.insert(usize::MAX, LogicalPosition::new(f32::MIN, f32::MAX));
        let output = LayoutOutput {
            positions,
            overflow_size: size(f32::NAN, f32::INFINITY),
            baseline: Some(-0.0),
        };
        let res = BfcLayoutResult::from_output(output);
        assert!(res.escaped_top_margin.is_none());
        assert!(res.escaped_bottom_margin.is_none());
        assert_eq!(res.output.positions.len(), 1);
        assert!(res.output.overflow_size.width.is_nan());
        assert!(res.output.overflow_size.height.is_infinite());
        assert_eq!(res.output.baseline, Some(-0.0));
    }
    #[test]
    fn bfc_state_new_matches_default_and_starts_empty() {
        let s = BfcState::new();
        assert_eq!(s.pen, LogicalPosition::zero());
        assert!(s.floats.floats.is_empty());
        assert_eq!(s.margins.last_in_flow_margin_bottom, 0.0);
        let d = BfcState::default();
        assert_eq!(d.pen, s.pen);
        assert!(d.floats.floats.is_empty());
    }
    #[test]
    fn table_layout_context_new_starts_empty_and_separate() {
        let t = TableLayoutContext::new();
        assert!(t.columns.is_empty());
        assert!(t.cells.is_empty());
        assert_eq!(t.num_rows, 0);
        assert!(!t.use_fixed_layout);
        assert!(t.row_heights.is_empty());
        assert!(t.row_baselines.is_empty());
        assert!(matches!(t.border_collapse, StyleBorderCollapse::Separate));
        assert!(t.caption_index.is_none());
        assert!(t.collapsed_rows.is_empty());
        assert!(t.collapsed_columns.is_empty());
        assert!(t.hidden_empty_rows.is_empty());
        assert!(t.row_node_indices.is_empty());
    }
    // ==================================================================
    // FloatingContext (numeric)
    // ==================================================================
    #[test]
    fn add_float_accepts_extreme_geometry_without_panicking() {
        let mut fc = FloatingContext::default();
        fc.add_float(LayoutFloat::Left, rect(0.0, 0.0, 0.0, 0.0), EdgeSizes::default());
        fc.add_float(
            LayoutFloat::Right,
            rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX),
            edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
        );
        fc.add_float(
            LayoutFloat::None,
            rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
            edges(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0),
        );
        assert_eq!(fc.floats.len(), 3);
        // The context is a pure accumulator: nothing is normalised or rejected.
        assert!(fc.floats[2].rect.size.width.is_nan());
    }
    #[test]
    fn available_line_box_space_with_no_floats_returns_the_full_band() {
        let fc = FloatingContext::default();
        assert_eq!(fc.available_line_box_space(0.0, 0.0, 0.0, HTB), (0.0, 0.0));
        assert_eq!(
            fc.available_line_box_space(0.0, 20.0, 300.0, HTB),
            (0.0, 300.0)
        );
        // A negative BFC size is passed straight through (start > end: an empty band).
        assert_eq!(
            fc.available_line_box_space(-50.0, -10.0, -5.0, HTB),
            (0.0, -5.0)
        );
        let (s, e) = fc.available_line_box_space(f32::MIN, f32::MAX, f32::MAX, HTB);
        assert_eq!(s, 0.0);
        assert_eq!(e, f32::MAX);
    }
    #[test]
    fn available_line_box_space_subtracts_left_and_right_float_margin_boxes() {
        let mut fc = FloatingContext::default();
        // Left float: content 0..100 on the cross axis, +10px margins on each side.
        fc.add_float(
            LayoutFloat::Left,
            rect(10.0, 10.0, 100.0, 50.0),
            edges(10.0, 10.0, 10.0, 10.0),
        );
        // Right float: content box ends at 290, +10px margin -> starts at 280.
        fc.add_float(
            LayoutFloat::Right,
            rect(200.0, 10.0, 90.0, 50.0),
            edges(10.0, 10.0, 10.0, 10.0),
        );
        // Line inside the floats' main-axis band: both margin boxes are excluded.
        let (start, end) = fc.available_line_box_space(10.0, 20.0, 300.0, HTB);
        assert_eq!(start, 120.0); // 10 (origin) - 10 (margin) + 100 + 10 + 10
        assert_eq!(end, 190.0); // 200 - 10 (left margin of the right float)
        // A line entirely below both floats sees the full band again.
        assert_eq!(
            fc.available_line_box_space(1000.0, 1010.0, 300.0, HTB),
            (0.0, 300.0)
        );
    }
    #[test]
    fn available_line_box_space_main_axis_overlap_is_half_open() {
        let mut fc = FloatingContext::default();
        // Margin box spans main 0..50 exactly (no margins).
        fc.add_float(
            LayoutFloat::Left,
            rect(0.0, 0.0, 100.0, 50.0),
            EdgeSizes::default(),
        );
        // A line starting exactly at the float's bottom edge does NOT overlap.
        assert_eq!(
            fc.available_line_box_space(50.0, 70.0, 300.0, HTB),
            (0.0, 300.0)
        );
        // A zero-height line just inside the float does not overlap either
        // (main_end > float_start is false at the very top edge).
        assert_eq!(
            fc.available_line_box_space(0.0, 0.0, 300.0, HTB),
            (0.0, 300.0)
        );
        // One pixel of overlap is enough.
        assert_eq!(
            fc.available_line_box_space(49.0, 70.0, 300.0, HTB),
            (100.0, 300.0)
        );
    }
    #[test]
    fn available_line_box_space_ignores_nan_geometry_instead_of_panicking() {
        let mut fc = FloatingContext::default();
        fc.add_float(
            LayoutFloat::Left,
            rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
            EdgeSizes::default(),
        );
        // Every NaN comparison is false, so the float never registers as overlapping.
        assert_eq!(
            fc.available_line_box_space(0.0, 20.0, 300.0, HTB),
            (0.0, 300.0)
        );
        // A NaN query range is likewise inert.
        let (s, e) = fc.available_line_box_space(f32::NAN, f32::NAN, 300.0, HTB);
        assert_eq!((s, e), (0.0, 300.0));
    }
    #[test]
    fn available_line_box_space_is_writing_mode_aware() {
        let mut fc = FloatingContext::default();
        // vertical-rl: main = x, cross = y.
        fc.add_float(
            LayoutFloat::Left,
            rect(0.0, 0.0, 50.0, 100.0),
            EdgeSizes::default(),
        );
        // Same rect, read in vertical-rl: main span 0..50 (x), cross 0..100 (y).
        assert_eq!(
            fc.available_line_box_space(10.0, 20.0, 300.0, VRL),
            (100.0, 300.0)
        );
        // In horizontal-tb the very same float spans main 0..100 (y), cross 0..50 (x).
        assert_eq!(
            fc.available_line_box_space(10.0, 20.0, 300.0, HTB),
            (50.0, 300.0)
        );
    }
    #[test]
    fn clearance_offset_never_moves_content_upwards() {
        let mut fc = FloatingContext::default();
        fc.add_float(
            LayoutFloat::Left,
            rect(0.0, 0.0, 100.0, 80.0),
            edges(0.0, 0.0, 20.0, 0.0), // 20px bottom margin -> outer edge at 100
        );
        fc.add_float(
            LayoutFloat::Right,
            rect(200.0, 0.0, 100.0, 40.0),
            EdgeSizes::default(),
        );
        assert_eq!(fc.clearance_offset(LayoutClear::Left, 0.0, HTB), 100.0);
        assert_eq!(fc.clearance_offset(LayoutClear::Right, 0.0, HTB), 40.0);
        assert_eq!(fc.clearance_offset(LayoutClear::Both, 0.0, HTB), 100.0);
        // clear:none never consults the floats.
        assert_eq!(fc.clearance_offset(LayoutClear::None, 25.0, HTB), 25.0);
        // Already below every float: the pen stays where it is (no negative clearance).
        assert_eq!(fc.clearance_offset(LayoutClear::Both, 500.0, HTB), 500.0);
    }
    #[test]
    fn clearance_offset_clamps_negative_offsets_to_zero() {
        let fc = FloatingContext::default();
        // Documented consequence of `max_end_offset` starting at 0.0: with no floats
        // at all, a negative pen is still pulled up to 0 rather than passed through.
        assert_eq!(fc.clearance_offset(LayoutClear::Both, -10.0, HTB), 0.0);
        assert_eq!(fc.clearance_offset(LayoutClear::None, -10.0, HTB), 0.0);
        // Non-negative offsets are untouched.
        assert_eq!(fc.clearance_offset(LayoutClear::None, 0.0, HTB), 0.0);
    }
    #[test]
    fn clearance_offset_handles_nan_and_infinite_floats() {
        let mut fc = FloatingContext::default();
        fc.add_float(
            LayoutFloat::Left,
            rect(0.0, 0.0, 10.0, f32::INFINITY),
            EdgeSizes::default(),
        );
        assert!(fc.clearance_offset(LayoutClear::Left, 0.0, HTB).is_infinite());
        // A NaN pen short-circuits the `>` test and is returned unchanged.
        assert!(fc.clearance_offset(LayoutClear::Left, f32::NAN, HTB).is_nan());
        let mut nan_fc = FloatingContext::default();
        nan_fc.add_float(
            LayoutFloat::Left,
            rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
            EdgeSizes::default(),
        );
        // f32::max() discards NaN, so a NaN float contributes nothing.
        assert_eq!(nan_fc.clearance_offset(LayoutClear::Left, 5.0, HTB), 5.0);
    }
    #[test]
    fn clearance_offset_saturates_at_float_extremes() {
        let mut fc = FloatingContext::default();
        fc.add_float(
            LayoutFloat::Right,
            rect(f32::MAX, 0.0, f32::MAX, f32::MAX),
            edges(0.0, 0.0, f32::MAX, 0.0),
        );
        let out = fc.clearance_offset(LayoutClear::Right, f32::MIN, HTB);
        // MAX + MAX + MAX saturates to +inf rather than wrapping or panicking.
        assert!(out.is_infinite() && out.is_sign_positive());
    }
    // ==================================================================
    // position_float (numeric)
    // ==================================================================
    #[test]
    fn position_float_places_left_and_right_floats_at_the_band_edges() {
        let fc = FloatingContext::default();
        let m = edges(10.0, 10.0, 10.0, 10.0);
        let left = position_float(&fc, LayoutFloat::Left, size(100.0, 50.0), &m, 0.0, 300.0, HTB);
        assert_eq!(left.origin.x, 10.0); // cross-start + left margin
        assert_eq!(left.origin.y, 10.0); // main offset + top margin
        assert_eq!(left.size, size(100.0, 50.0)); // the border box is passed through
        let right = position_float(&fc, LayoutFloat::Right, size(100.0, 50.0), &m, 0.0, 300.0, HTB);
        // 300 - (100 + 10 + 10) + 10 => content box ends at 290, margin box at 300.
        assert_eq!(right.origin.x, 190.0);
        assert_eq!(right.origin.y, 10.0);
    }
    #[test]
    fn position_float_at_zero_size_and_zero_band() {
        let fc = FloatingContext::default();
        let r = position_float(
            &fc,
            LayoutFloat::Left,
            size(0.0, 0.0),
            &EdgeSizes::default(),
            0.0,
            0.0,
            HTB,
        );
        assert_eq!(r.origin, LogicalPosition::zero());
        assert_eq!(r.size, size(0.0, 0.0));
    }
    #[test]
    fn position_float_wider_than_the_bfc_does_not_hang() {
        let fc = FloatingContext::default();
        // No float can ever fit; the loop must bail out instead of spinning.
        let left = position_float(
            &fc,
            LayoutFloat::Left,
            size(500.0, 50.0),
            &EdgeSizes::default(),
            0.0,
            300.0,
            HTB,
        );
        assert_eq!(left.origin.x, 0.0);
        let right = position_float(
            &fc,
            LayoutFloat::Right,
            size(500.0, 50.0),
            &EdgeSizes::default(),
            0.0,
            300.0,
            HTB,
        );
        // Overflows the cross-start edge, but deterministically so.
        assert_eq!(right.origin.x, -200.0);
    }
    #[test]
    fn position_float_stacks_then_shifts_down_past_the_lowest_float() {
        let mut fc = FloatingContext::default();
        fc.add_float(
            LayoutFloat::Left,
            rect(0.0, 0.0, 100.0, 50.0),
            EdgeSizes::default(),
        );
        // Fits next to the existing float.
        let beside = position_float(
            &fc,
            LayoutFloat::Left,
            size(100.0, 50.0),
            &EdgeSizes::default(),
            0.0,
            300.0,
            HTB,
        );
        assert_eq!(beside.origin.x, 100.0);
        assert_eq!(beside.origin.y, 0.0);
        // Too wide for the remaining 200px -> must drop below the float's bottom edge.
        let below = position_float(
            &fc,
            LayoutFloat::Left,
            size(250.0, 50.0),
            &EdgeSizes::default(),
            0.0,
            300.0,
            HTB,
        );
        assert_eq!(below.origin.x, 0.0);
        assert_eq!(below.origin.y, 50.0);
    }
    #[test]
    fn position_float_terminates_on_nan_and_infinite_sizes() {
        let mut fc = FloatingContext::default();
        fc.add_float(
            LayoutFloat::Left,
            rect(0.0, 0.0, 100.0, 50.0),
            EdgeSizes::default(),
        );
        // NaN cross size: `available_width >= total_cross` is false, and no float can
        // report a NaN-overlapping band, so the loop exits on the first pass.
        let nan = position_float(
            &fc,
            LayoutFloat::Left,
            size(f32::NAN, f32::NAN),
            &EdgeSizes::default(),
            0.0,
            300.0,
            HTB,
        );
        assert!(nan.size.width.is_nan());
        assert!(nan.origin.x.is_finite());
        // Infinite size: never fits, drops past the one float, then bails out.
        let inf = position_float(
            &fc,
            LayoutFloat::Left,
            size(f32::INFINITY, f32::INFINITY),
            &EdgeSizes::default(),
            0.0,
            300.0,
            HTB,
        );
        assert_eq!(inf.origin.x, 0.0);
    }
    #[test]
    fn position_float_honours_vertical_writing_mode() {
        let fc = FloatingContext::default();
        // vertical-rl: main = x, cross = y.
        let r = position_float(
            &fc,
            LayoutFloat::Left,
            size(50.0, 100.0),
            &EdgeSizes::default(),
            20.0,
            300.0,
            VRL,
        );
        assert_eq!(r.origin.x, 20.0); // main offset lands on x
        assert_eq!(r.origin.y, 0.0); // cross-start lands on y
    }
    // ==================================================================
    // position_floated_child (numeric)
    // ==================================================================
    #[test]
    fn position_floated_child_rejects_float_none() {
        let mut fc = FloatingContext::default();
        let c = constraints(size(300.0, 300.0), HTB);
        let out = position_floated_child(
            0,
            size(10.0, 10.0),
            LayoutFloat::None,
            &c,
            rect(0.0, 0.0, 300.0, 300.0),
            0.0,
            &mut fc,
        );
        assert!(matches!(out, Err(LayoutError::PositioningFailed)));
        assert!(fc.floats.is_empty());
    }
    #[test]
    fn position_floated_child_places_and_records_the_float() {
        let mut fc = FloatingContext::default();
        let c = constraints(size(300.0, 300.0), HTB);
        let left = position_floated_child(
            0,
            size(100.0, 50.0),
            LayoutFloat::Left,
            &c,
            rect(0.0, 0.0, 300.0, 300.0),
            0.0,
            &mut fc,
        )
        .expect("fits");
        assert_eq!(left, LogicalPosition::new(0.0, 0.0));
        assert_eq!(fc.floats.len(), 1);
        let right = position_floated_child(
            1,
            size(100.0, 50.0),
            LayoutFloat::Right,
            &c,
            rect(0.0, 0.0, 300.0, 300.0),
            0.0,
            &mut fc,
        )
        .expect("fits");
        assert_eq!(right, LogicalPosition::new(200.0, 0.0));
        assert_eq!(fc.floats.len(), 2);
        // Third float is wider than the 100px gap left between them -> pushed below.
        let third = position_floated_child(
            2,
            size(150.0, 50.0),
            LayoutFloat::Left,
            &c,
            rect(0.0, 0.0, 300.0, 300.0),
            0.0,
            &mut fc,
        )
        .expect("drops to the next band");
        assert_eq!(third, LogicalPosition::new(0.0, 50.0));
    }
    #[test]
    fn position_floated_child_errors_instead_of_looping_when_nothing_can_fit() {
        let mut fc = FloatingContext::default();
        let c = constraints(size(300.0, 300.0), HTB);
        // Wider than the BFC with no float to drop past -> unrecoverable, must Err.
        let out = position_floated_child(
            0,
            size(500.0, 50.0),
            LayoutFloat::Left,
            &c,
            rect(0.0, 0.0, 300.0, 300.0),
            0.0,
            &mut fc,
        );
        assert!(matches!(out, Err(LayoutError::PositioningFailed)));
        assert!(fc.floats.is_empty());
        // Same for a NaN-sized child: it never "fits", and NaN < float_end is false.
        let nan = position_floated_child(
            0,
            size(f32::NAN, f32::NAN),
            LayoutFloat::Left,
            &c,
            rect(0.0, 0.0, 300.0, 300.0),
            0.0,
            &mut fc,
        );
        assert!(matches!(nan, Err(LayoutError::PositioningFailed)));
    }
    // ==================================================================
    // taffy translation (round-trip)
    // ==================================================================
    #[test]
    fn translate_taffy_size_round_trips_through_taffy() {
        for s in [
            size(0.0, 0.0),
            size(-0.0, 1.5),
            size(f32::MIN, f32::MAX),
            size(-1.0, -2.0),
            size(f32::INFINITY, f32::NEG_INFINITY),
            size(f32::MIN_POSITIVE, f32::EPSILON),
        ] {
            let t = translate_taffy_size(s);
            let back = translate_taffy_size_back(TaffySize {
                width: t.width.unwrap(),
                height: t.height.unwrap(),
            });
            assert_eq!(back.width.to_bits(), s.width.to_bits(), "width for {s:?}");
            assert_eq!(back.height.to_bits(), s.height.to_bits(), "height for {s:?}");
        }
    }
    #[test]
    fn translate_taffy_size_preserves_nan_without_panicking() {
        let t = translate_taffy_size(size(f32::NAN, f32::NAN));
        assert!(t.width.unwrap().is_nan());
        let back = translate_taffy_size_back(TaffySize {
            width: t.width.unwrap(),
            height: t.height.unwrap(),
        });
        assert!(back.width.is_nan() && back.height.is_nan());
    }
    #[test]
    fn translate_taffy_point_back_is_a_field_for_field_copy() {
        for (x, y) in [
            (0.0_f32, 0.0_f32),
            (f32::MIN, f32::MAX),
            (-1.5, 2.5),
            (f32::INFINITY, f32::NEG_INFINITY),
        ] {
            let p = translate_taffy_point_back(taffy::Point { x, y });
            assert_eq!(p.x.to_bits(), x.to_bits());
            assert_eq!(p.y.to_bits(), y.to_bits());
        }
        let nan = translate_taffy_point_back(taffy::Point {
            x: f32::NAN,
            y: f32::NAN,
        });
        assert!(nan.x.is_nan() && nan.y.is_nan());
    }
    // ==================================================================
    // resolve_size_metric (numeric)
    // ==================================================================
    fn resolve(metric: SizeMetric, value: f32) -> f32 {
        resolve_size_metric(metric, value, 200.0, size(1000.0, 500.0), 16.0, 10.0)
    }
    /// The unit conversions divide before multiplying, so they are only exact to
    /// within f32 rounding — compare with a tolerance rather than bit-for-bit.
    fn approx(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 0.001,
            "expected ~{expected}, got {actual}"
        );
    }
    #[test]
    fn resolve_size_metric_converts_every_unit() {
        assert_eq!(resolve(SizeMetric::Px, 42.0), 42.0);
        approx(resolve(SizeMetric::Pt, 72.0), 96.0); // 72pt == 96px
        assert_eq!(resolve(SizeMetric::Percent, 50.0), 100.0); // 50% of 200
        assert_eq!(resolve(SizeMetric::Em, 2.0), 32.0); // 2 * 16px
        assert_eq!(resolve(SizeMetric::Rem, 2.0), 20.0); // 2 * 10px root
        approx(resolve(SizeMetric::Vw, 10.0), 100.0); // 10% of 1000
        approx(resolve(SizeMetric::Vh, 10.0), 50.0); // 10% of 500
        assert_eq!(resolve(SizeMetric::Vmin, 100.0), 500.0); // smaller axis
        assert_eq!(resolve(SizeMetric::Vmax, 100.0), 1000.0); // larger axis
        assert_eq!(resolve(SizeMetric::In, 1.0), 96.0);
        approx(resolve(SizeMetric::Cm, 2.54), 96.0);
        approx(resolve(SizeMetric::Mm, 25.4), 96.0);
    }
    #[test]
    fn resolve_size_metric_at_zero_and_negative_values() {
        for m in [
            SizeMetric::Px,
            SizeMetric::Pt,
            SizeMetric::Percent,
            SizeMetric::Em,
            SizeMetric::Rem,
            SizeMetric::Vw,
            SizeMetric::Vh,
            SizeMetric::Vmin,
            SizeMetric::Vmax,
            SizeMetric::In,
            SizeMetric::Cm,
            SizeMetric::Mm,
        ] {
            assert_eq!(resolve(m, 0.0), 0.0, "{m:?} at zero");
            assert!(resolve(m, -10.0) <= 0.0, "{m:?} keeps the sign of a negative");
        }
        // Percentages of a negative containing block stay negative.
        assert_eq!(
            resolve_size_metric(SizeMetric::Percent, 50.0, -200.0, size(0.0, 0.0), 16.0, 16.0),
            -100.0
        );
    }
    #[test]
    fn resolve_size_metric_saturates_instead_of_panicking_at_the_limits() {
        let huge = resolve_size_metric(
            SizeMetric::Em,
            f32::MAX,
            0.0,
            size(0.0, 0.0),
            f32::MAX,
            16.0,
        );
        assert!(huge.is_infinite(), "MAX * MAX saturates to +inf");
        let neg = resolve_size_metric(
            SizeMetric::Percent,
            f32::MIN,
            f32::MAX,
            size(0.0, 0.0),
            16.0,
            16.0,
        );
        assert!(neg.is_infinite() && neg.is_sign_negative());
    }
    #[test]
    fn resolve_size_metric_propagates_nan_and_inf_without_panicking() {
        assert!(resolve(SizeMetric::Px, f32::NAN).is_nan());
        assert!(resolve(SizeMetric::Px, f32::INFINITY).is_infinite());
        assert!(resolve(SizeMetric::Percent, f32::NAN).is_nan());
        // 0 * inf is the classic NaN trap: it must not panic, it just yields NaN.
        assert!(resolve_size_metric(
            SizeMetric::Em,
            0.0,
            0.0,
            size(0.0, 0.0),
            f32::INFINITY,
            16.0
        )
        .is_nan());
        // f32::min/max drop NaN, so a half-NaN viewport still resolves vmin/vmax.
        let vp = size(f32::NAN, 400.0);
        assert_eq!(
            resolve_size_metric(SizeMetric::Vmin, 100.0, 0.0, vp, 16.0, 16.0),
            400.0
        );
        assert_eq!(
            resolve_size_metric(SizeMetric::Vmax, 100.0, 0.0, vp, 16.0, 16.0),
            400.0
        );
    }
    // ==================================================================
    // convert_font_style / convert_font_weight (other)
    // ==================================================================
    #[test]
    fn convert_font_style_maps_every_variant() {
        assert_eq!(
            convert_font_style(StyleFontStyle::Normal),
            crate::font_traits::FontStyle::Normal
        );
        assert_eq!(
            convert_font_style(StyleFontStyle::Italic),
            crate::font_traits::FontStyle::Italic
        );
        assert_eq!(
            convert_font_style(StyleFontStyle::Oblique),
            crate::font_traits::FontStyle::Oblique
        );
    }
    #[test]
    fn convert_font_weight_maps_every_variant_monotonically() {
        assert_eq!(convert_font_weight(StyleFontWeight::W100), FcWeight::Thin);
        assert_eq!(
            convert_font_weight(StyleFontWeight::W200),
            FcWeight::ExtraLight
        );
        assert_eq!(convert_font_weight(StyleFontWeight::W300), FcWeight::Light);
        assert_eq!(
            convert_font_weight(StyleFontWeight::Lighter),
            FcWeight::Light
        );
        assert_eq!(convert_font_weight(StyleFontWeight::Normal), FcWeight::Normal);
        assert_eq!(convert_font_weight(StyleFontWeight::W500), FcWeight::Medium);
        assert_eq!(
            convert_font_weight(StyleFontWeight::W600),
            FcWeight::SemiBold
        );
        assert_eq!(convert_font_weight(StyleFontWeight::Bold), FcWeight::Bold);
        assert_eq!(
            convert_font_weight(StyleFontWeight::W800),
            FcWeight::ExtraBold
        );
        assert_eq!(convert_font_weight(StyleFontWeight::W900), FcWeight::Black);
        assert_eq!(convert_font_weight(StyleFontWeight::Bolder), FcWeight::Black);
    }
    // ==================================================================
    // BorderInfo (other)
    // ==================================================================
    fn bi(width: f32, style: BorderStyle, source: BorderSource) -> BorderInfo {
        BorderInfo::new(width, style, ColorU::BLACK, source)
    }
    #[test]
    fn border_info_new_stores_its_arguments_verbatim() {
        let b = BorderInfo::new(f32::NAN, BorderStyle::Dotted, ColorU::RED, BorderSource::Cell);
        assert!(b.width.is_nan());
        assert_eq!(b.style, BorderStyle::Dotted);
        assert_eq!(b.color, ColorU::RED);
        assert_eq!(b.source, BorderSource::Cell);
    }
    #[test]
    fn border_style_priority_follows_the_css_ordering() {
        assert_eq!(BorderInfo::style_priority(&BorderStyle::Hidden), 255);
        assert_eq!(BorderInfo::style_priority(&BorderStyle::None), 0);
        // double > solid > dashed > dotted > ridge > outset > groove > inset
        let ordered = [
            BorderStyle::Double,
            BorderStyle::Solid,
            BorderStyle::Dashed,
            BorderStyle::Dotted,
            BorderStyle::Ridge,
            BorderStyle::Outset,
            BorderStyle::Groove,
            BorderStyle::Inset,
        ];
        for w in ordered.windows(2) {
            assert!(
                BorderInfo::style_priority(&w[0]) > BorderInfo::style_priority(&w[1]),
                "{:?} must outrank {:?}",
                w[0],
                w[1]
            );
        }
    }
    #[test]
    fn resolve_conflict_hidden_suppresses_everything() {
        let hidden = bi(1.0, BorderStyle::Hidden, BorderSource::Table);
        let fat = bi(99.0, BorderStyle::Solid, BorderSource::Cell);
        assert!(BorderInfo::resolve_conflict(&hidden, &fat).is_none());
        assert!(BorderInfo::resolve_conflict(&fat, &hidden).is_none());
    }
    #[test]
    fn resolve_conflict_none_always_loses() {
        let none = bi(50.0, BorderStyle::None, BorderSource::Cell);
        let thin = bi(1.0, BorderStyle::Solid, BorderSource::Table);
        assert!(BorderInfo::resolve_conflict(&none, &none).is_none());
        // 'none' loses even when it is much wider.
        assert_eq!(
            BorderInfo::resolve_conflict(&none, &thin).unwrap().style,
            BorderStyle::Solid
        );
        assert_eq!(
            BorderInfo::resolve_conflict(&thin, &none).unwrap().style,
            BorderStyle::Solid
        );
    }
    #[test]
    fn resolve_conflict_falls_through_width_then_style_then_source() {
        let wide = bi(5.0, BorderStyle::Dotted, BorderSource::Table);
        let narrow = bi(2.0, BorderStyle::Double, BorderSource::Cell);
        assert_eq!(BorderInfo::resolve_conflict(&wide, &narrow).unwrap().width, 5.0);
        // Same width -> style priority decides (double beats dotted).
        let a = bi(3.0, BorderStyle::Dotted, BorderSource::Cell);
        let b = bi(3.0, BorderStyle::Double, BorderSource::Table);
        assert_eq!(
            BorderInfo::resolve_conflict(&a, &b).unwrap().style,
            BorderStyle::Double
        );
        // Same width + style -> source priority decides (cell beats table).
        let t = bi(3.0, BorderStyle::Solid, BorderSource::Table);
        let c = bi(3.0, BorderStyle::Solid, BorderSource::Cell);
        assert_eq!(
            BorderInfo::resolve_conflict(&t, &c).unwrap().source,
            BorderSource::Cell
        );
        assert_eq!(
            BorderInfo::resolve_conflict(&c, &t).unwrap().source,
            BorderSource::Cell
        );
        // Fully tied -> the first (left/top) wins.
        let first = BorderInfo::new(3.0, BorderStyle::Solid, ColorU::RED, BorderSource::Row);
        let second = BorderInfo::new(3.0, BorderStyle::Solid, ColorU::GREEN, BorderSource::Row);
        assert_eq!(
            BorderInfo::resolve_conflict(&first, &second).unwrap().color,
            ColorU::RED
        );
    }
    #[test]
    fn resolve_conflict_with_nan_widths_is_deterministic() {
        // Neither `a.width > b.width` nor `b.width > a.width` holds for NaN, so the
        // algorithm must fall through to style/source rather than panic or hang.
        let nan = bi(f32::NAN, BorderStyle::Solid, BorderSource::Table);
        let ok = bi(1.0, BorderStyle::Solid, BorderSource::Cell);
        let win = BorderInfo::resolve_conflict(&nan, &ok).unwrap();
        assert_eq!(win.source, BorderSource::Cell); // source breaks the tie
        assert_eq!(win.width, 1.0);
        // Equal source too -> first argument wins, NaN width and all.
        let nan_b = bi(f32::NAN, BorderStyle::Solid, BorderSource::Table);
        let other = bi(2.0, BorderStyle::Solid, BorderSource::Table);
        assert!(BorderInfo::resolve_conflict(&nan_b, &other)
            .unwrap()
            .width
            .is_nan());
    }
    #[test]
    fn resolve_conflict_infinite_width_wins() {
        let inf = bi(f32::INFINITY, BorderStyle::Inset, BorderSource::Table);
        let solid = bi(f32::MAX, BorderStyle::Solid, BorderSource::Cell);
        assert!(BorderInfo::resolve_conflict(&inf, &solid)
            .unwrap()
            .width
            .is_infinite());
    }
    // ==================================================================
    // distribute_cell_width_across_columns (numeric)
    // ==================================================================
    fn cols(n: usize, min: f32, max: f32) -> Vec<TableColumnInfo> {
        (0..n)
            .map(|_| TableColumnInfo {
                min_width: min,
                max_width: max,
                computed_width: None,
            })
            .collect()
    }
    #[test]
    fn distribute_cell_width_spreads_the_deficit_evenly() {
        let mut c = cols(2, 10.0, 20.0);
        let collapsed = std::collections::HashSet::new();
        distribute_cell_width_across_columns(&mut c, 0, 2, 50.0, 30.0, &collapsed);
        // min: 50 needed, 20 present -> +15 each. max: 30 needed, 40 present -> untouched.
        assert_eq!(c[0].min_width, 25.0);
        assert_eq!(c[1].min_width, 25.0);
        assert_eq!(c[0].max_width, 20.0);
        assert_eq!(c[1].max_width, 20.0);
    }
    #[test]
    fn distribute_cell_width_is_a_noop_when_the_span_overruns_the_columns() {
        let mut c = cols(2, 10.0, 20.0);
        let collapsed = std::collections::HashSet::new();
        distribute_cell_width_across_columns(&mut c, 1, 5, 500.0, 500.0, &collapsed);
        assert_eq!(c[0].min_width, 10.0);
        assert_eq!(c[1].min_width, 10.0);
        // start_col past the end, and the degenerate usize::MAX start with colspan 0.
        distribute_cell_width_across_columns(&mut c, 99, 1, 500.0, 500.0, &collapsed);
        distribute_cell_width_across_columns(&mut c, usize::MAX, 0, 500.0, 500.0, &collapsed);
        assert_eq!(c[0].min_width, 10.0);
    }
    #[test]
    fn distribute_cell_width_with_zero_colspan_does_not_divide_by_zero() {
        let mut c = cols(2, 10.0, 20.0);
        let collapsed = std::collections::HashSet::new();
        distribute_cell_width_across_columns(&mut c, 0, 0, 1000.0, 1000.0, &collapsed);
        assert_eq!(c[0].min_width, 10.0);
        assert_eq!(c[1].min_width, 10.0);
        assert!(c[0].min_width.is_finite());
    }
    #[test]
    fn distribute_cell_width_skips_fully_collapsed_spans() {
        let mut c = cols(2, 10.0, 20.0);
        let collapsed: std::collections::HashSet<usize> = [0, 1].into_iter().collect();
        distribute_cell_width_across_columns(&mut c, 0, 2, 1000.0, 1000.0, &collapsed);
        assert_eq!(c[0].min_width, 10.0);
        assert_eq!(c[1].min_width, 10.0);
        // A partially collapsed span puts the whole deficit on the visible column.
        let collapsed_one: std::collections::HashSet<usize> = [0].into_iter().collect();
        distribute_cell_width_across_columns(&mut c, 0, 2, 100.0, 0.0, &collapsed_one);
        assert_eq!(c[0].min_width, 10.0, "collapsed column is untouched");
        assert_eq!(c[1].min_width, 100.0, "10 + (100 - 10) / 1");
    }
    #[test]
    fn distribute_cell_width_ignores_nan_and_saturates_on_inf() {
        let mut c = cols(2, 10.0, 20.0);
        let collapsed = std::collections::HashSet::new();
        // NaN > total is false -> no distribution, no NaN poisoning of the columns.
        distribute_cell_width_across_columns(&mut c, 0, 2, f32::NAN, f32::NAN, &collapsed);
        assert_eq!(c[0].min_width, 10.0);
        assert_eq!(c[1].max_width, 20.0);
        // Infinite demand saturates rather than panicking.
        distribute_cell_width_across_columns(
            &mut c,
            0,
            2,
            f32::INFINITY,
            f32::INFINITY,
            &collapsed,
        );
        assert!(c[0].min_width.is_infinite());
        assert!(c[1].max_width.is_infinite());
    }
    #[test]
    fn distribute_cell_width_does_not_shrink_columns() {
        let mut c = cols(2, 100.0, 200.0);
        let collapsed = std::collections::HashSet::new();
        // The cell is narrower than what the columns already provide -> no change.
        distribute_cell_width_across_columns(&mut c, 0, 2, 1.0, 1.0, &collapsed);
        assert_eq!(c[0].min_width, 100.0);
        assert_eq!(c[0].max_width, 200.0);
        // Negative demand likewise cannot pull the columns below zero.
        distribute_cell_width_across_columns(&mut c, 0, 2, -1000.0, -1000.0, &collapsed);
        assert_eq!(c[1].min_width, 100.0);
    }
    // ==================================================================
    // is_cell_empty / is_empty_block / compute_cell_baseline
    // ==================================================================
    #[test]
    fn is_cell_empty_treats_missing_and_childless_cells_as_empty() {
        let tree = build_tree(
            vec![hot(None, Some(size(10.0, 10.0)), &BoxProps::default())],
            vec![LayoutNodeWarm::default()],
            &[vec![]],
        );
        assert!(is_cell_empty(&tree, 0), "no children => empty");
        assert!(is_cell_empty(&tree, 1), "out-of-range index => empty");
        assert!(is_cell_empty(&tree, usize::MAX), "usize::MAX must not panic");
    }
    #[test]
    fn is_cell_empty_uses_the_inline_layout_when_present() {
        let bp = BoxProps::default();
        let mut warm = vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()];
        // Cell (0) has a child (1) but an inline layout with no items => empty.
        warm[0].inline_layout_result = Some(Box::new(empty_inline_layout()));
        let tree = build_tree(
            vec![
                hot(None, Some(size(10.0, 10.0)), &bp),
                hot(Some(0), Some(size(10.0, 10.0)), &bp),
            ],
            warm,
            &[vec![1], vec![]],
        );
        assert!(is_cell_empty(&tree, 0));
        // Same tree without the inline layout: children alone mean "not empty".
        let tree2 = build_tree(
            vec![
                hot(None, Some(size(10.0, 10.0)), &bp),
                hot(Some(0), Some(size(10.0, 10.0)), &bp),
            ],
            vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()],
            &[vec![1], vec![]],
        );
        assert!(!is_cell_empty(&tree2, 0));
    }
    #[test]
    fn is_empty_block_requires_no_children_no_inline_content_and_no_height() {
        let bp = BoxProps::default();
        // Missing node => vacuously empty.
        let empty_tree = build_tree(Vec::new(), Vec::new(), &[]);
        assert!(is_empty_block(&empty_tree, 0));
        assert!(is_empty_block(&empty_tree, usize::MAX));
        // No children, no used_size => empty.
        let t = build_tree(
            vec![hot(None, None, &bp)],
            vec![LayoutNodeWarm::default()],
            &[vec![]],
        );
        assert!(is_empty_block(&t, 0));
        // Zero and negative heights still count as empty (the check is `> 0.0`).
        for h in [0.0, -5.0] {
            let t = build_tree(
                vec![hot(None, Some(size(100.0, h)), &bp)],
                vec![LayoutNodeWarm::default()],
                &[vec![]],
            );
            assert!(is_empty_block(&t, 0), "height {h} must count as empty");
        }
        // A positive height makes it non-empty.
        let t = build_tree(
            vec![hot(None, Some(size(100.0, 0.5)), &bp)],
            vec![LayoutNodeWarm::default()],
            &[vec![]],
        );
        assert!(!is_empty_block(&t, 0));
        // Children make it non-empty.
        let t = build_tree(
            vec![hot(None, None, &bp), hot(Some(0), None, &bp)],
            vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()],
            &[vec![1], vec![]],
        );
        assert!(!is_empty_block(&t, 0));
        // An inline layout result makes it non-empty even if it has no items.
        let mut warm = vec![LayoutNodeWarm::default()];
        warm[0].inline_layout_result = Some(Box::new(empty_inline_layout()));
        let t = build_tree(vec![hot(None, None, &bp)], warm, &[vec![]]);
        assert!(!is_empty_block(&t, 0));
    }
    #[test]
    fn compute_cell_baseline_falls_back_to_the_content_edge() {
        let bp = box_props(
            EdgeSizes::default(),
            edges(0.0, 0.0, 2.0, 0.0), // border-bottom: 2
            edges(0.0, 0.0, 5.0, 0.0), // padding-bottom: 5
        );
        let tree = build_tree(
            vec![hot(None, Some(size(50.0, 100.0)), &bp)],
            vec![LayoutNodeWarm::default()],
            &[vec![]],
        );
        // No line box: baseline == bottom of the content edge (100 - 5 - 2).
        assert_eq!(compute_cell_baseline(0, &tree), 93.0);
        // Missing node => 0.0, never a panic.
        assert_eq!(compute_cell_baseline(usize::MAX, &tree), 0.0);
    }
    #[test]
    fn compute_cell_baseline_without_used_size_can_go_negative() {
        let bp = box_props(
            EdgeSizes::default(),
            edges(0.0, 0.0, 2.0, 0.0),
            edges(0.0, 0.0, 5.0, 0.0),
        );
        let tree = build_tree(
            vec![hot(None, None, &bp)],
            vec![LayoutNodeWarm::default()],
            &[vec![]],
        );
        // used_size defaults to 0x0, so the baseline is -(padding + border).
        assert_eq!(compute_cell_baseline(0, &tree), -7.0);
    }
    // ==================================================================
    // check_scrollbar_necessity (numeric)
    // ==================================================================
    #[test]
    fn check_scrollbar_necessity_never_scrolls_for_visible_hidden_or_clip() {
        for o in [
            OverflowBehavior::Visible,
            OverflowBehavior::Hidden,
            OverflowBehavior::Clip,
        ] {
            let r = check_scrollbar_necessity(size(9999.0, 9999.0), size(10.0, 10.0), o, o, 16.0);
            assert!(!r.needs_horizontal, "{o:?}");
            assert!(!r.needs_vertical, "{o:?}");
            assert_eq!(r.scrollbar_width, 0.0);
            assert_eq!(r.scrollbar_height, 0.0);
        }
    }
    #[test]
    fn check_scrollbar_necessity_always_scrolls_for_scroll_even_with_no_content() {
        let r = check_scrollbar_necessity(
            size(0.0, 0.0),
            size(500.0, 500.0),
            OverflowBehavior::Scroll,
            OverflowBehavior::Scroll,
            16.0,
        );
        assert!(r.needs_horizontal && r.needs_vertical);
        assert_eq!(r.scrollbar_width, 16.0);
        assert_eq!(r.scrollbar_height, 16.0);
    }
    #[test]
    fn check_scrollbar_necessity_auto_honours_the_one_pixel_epsilon() {
        let auto = OverflowBehavior::Auto;
        // Exactly at the epsilon boundary: 301 is NOT > 300 + 1.
        let r = check_scrollbar_necessity(size(301.0, 301.0), size(300.0, 300.0), auto, auto, 0.0);
        assert!(!r.needs_horizontal);
        assert!(!r.needs_vertical);
        // One ulp past the boundary triggers both.
        let r = check_scrollbar_necessity(size(302.0, 302.0), size(300.0, 300.0), auto, auto, 0.0);
        assert!(r.needs_horizontal && r.needs_vertical);
        // Overlay scrollbars: needed, but they reserve no layout space.
        assert_eq!(r.scrollbar_width, 0.0);
        assert_eq!(r.scrollbar_height, 0.0);
    }
    #[test]
    fn check_scrollbar_necessity_two_pass_adds_the_second_scrollbar() {
        let auto = OverflowBehavior::Auto;
        // Vertically overflowing; horizontally it fits exactly — until the 16px
        // vertical scrollbar eats into the width.
        let r = check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, 16.0);
        assert!(r.needs_vertical);
        assert!(r.needs_horizontal, "vertical scrollbar must force a horizontal one");
        assert_eq!(r.scrollbar_width, 16.0);
        assert_eq!(r.scrollbar_height, 16.0);
        // With overlay scrollbars (0px) the second pass is skipped.
        let r = check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, 0.0);
        assert!(r.needs_vertical);
        assert!(!r.needs_horizontal);
    }
    #[test]
    fn check_scrollbar_necessity_with_nan_and_negative_sizes_is_deterministic() {
        let auto = OverflowBehavior::Auto;
        // NaN comparisons are false => no scrollbars, no panic.
        let r = check_scrollbar_necessity(
            size(f32::NAN, f32::NAN),
            size(300.0, 300.0),
            auto,
            auto,
            16.0,
        );
        assert!(!r.needs_horizontal && !r.needs_vertical);
        // A NaN container is equally inert.
        let r = check_scrollbar_necessity(
            size(500.0, 500.0),
            size(f32::NAN, f32::NAN),
            auto,
            auto,
            16.0,
        );
        assert!(!r.needs_horizontal && !r.needs_vertical);
        // Negative container sizes: content trivially overflows, both appear.
        let r = check_scrollbar_necessity(
            size(0.0, 0.0),
            size(-100.0, -100.0),
            auto,
            auto,
            16.0,
        );
        assert!(r.needs_horizontal && r.needs_vertical);
        // Infinite content overflows anything finite.
        let r = check_scrollbar_necessity(
            size(f32::INFINITY, f32::INFINITY),
            size(300.0, 300.0),
            auto,
            auto,
            16.0,
        );
        assert!(r.needs_horizontal && r.needs_vertical);
    }
    #[test]
    fn check_scrollbar_necessity_with_negative_scrollbar_width_skips_the_second_pass() {
        let auto = OverflowBehavior::Auto;
        let r =
            check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, -16.0);
        assert!(r.needs_vertical);
        assert!(!r.needs_horizontal, "the `> 0.0` guard skips the two-pass check");
        assert_eq!(r.scrollbar_width, -16.0); // passed through verbatim
    }
    // ==================================================================
    // collapse_margins / advance_pen_with_margin_collapse (numeric)
    // ==================================================================
    #[test]
    fn collapse_margins_follows_css_2_1_section_8_3_1() {
        assert_eq!(collapse_margins(10.0, 20.0), 20.0); // both positive -> max
        assert_eq!(collapse_margins(-10.0, -20.0), -20.0); // both negative -> min
        assert_eq!(collapse_margins(20.0, -5.0), 15.0); // mixed -> sum
        assert_eq!(collapse_margins(-5.0, 20.0), 15.0);
        assert_eq!(collapse_margins(0.0, 0.0), 0.0);
        assert_eq!(collapse_margins(0.0, 10.0), 10.0);
        // +0.0 counts as positive, so a zero/negative pair is summed.
        assert_eq!(collapse_margins(0.0, -5.0), -5.0);
    }
    #[test]
    fn collapse_margins_is_commutative_for_finite_inputs() {
        let vals = [-100.0_f32, -1.0, -0.5, 0.0, 0.5, 1.0, 100.0, f32::MAX, f32::MIN];
        for a in vals {
            for b in vals {
                let ab = collapse_margins(a, b);
                let ba = collapse_margins(b, a);
                assert_eq!(ab.to_bits(), ba.to_bits(), "collapse_margins({a}, {b})");
            }
        }
    }
    #[test]
    fn collapse_margins_saturates_and_defines_nan_inf_behaviour() {
        assert!(collapse_margins(f32::MAX, -f32::MAX).abs() < 1.0); // sum, no overflow
        assert_eq!(collapse_margins(f32::INFINITY, 5.0), f32::INFINITY);
        assert_eq!(collapse_margins(f32::NEG_INFINITY, -5.0), f32::NEG_INFINITY);
        // +inf and -inf have mixed signs -> summed -> NaN (must not panic).
        assert!(collapse_margins(f32::INFINITY, f32::NEG_INFINITY).is_nan());
        // f32::max/min discard NaN, so a NaN margin is ignored rather than propagated.
        assert_eq!(collapse_margins(f32::NAN, 1.0), 1.0);
        assert_eq!(collapse_margins(1.0, f32::NAN), 1.0);
        assert_eq!(collapse_margins(-f32::NAN, -1.0), -1.0);
    }
    #[test]
    fn advance_pen_with_margin_collapse_advances_by_exactly_the_collapsed_margin() {
        let mut pen = 100.0_f32;
        let collapsed = advance_pen_with_margin_collapse(&mut pen, 10.0, 20.0);
        assert_eq!(collapsed, 20.0);
        assert_eq!(pen, 120.0);
        // Mixed signs pull the pen back up.
        let mut pen = 100.0_f32;
        let collapsed = advance_pen_with_margin_collapse(&mut pen, 30.0, -10.0);
        assert_eq!(collapsed, 20.0);
        assert_eq!(pen, 120.0);
        // Zero margins leave the pen alone.
        let mut pen = 7.5_f32;
        assert_eq!(advance_pen_with_margin_collapse(&mut pen, 0.0, 0.0), 0.0);
        assert_eq!(pen, 7.5);
    }
    #[test]
    fn advance_pen_with_margin_collapse_saturates_at_the_f32_limits() {
        let mut pen = f32::MAX;
        let collapsed = advance_pen_with_margin_collapse(&mut pen, f32::MAX, f32::MAX);
        assert_eq!(collapsed, f32::MAX);
        assert!(pen.is_infinite(), "MAX + MAX saturates to +inf, no panic");
        let mut pen = f32::NAN;
        let collapsed = advance_pen_with_margin_collapse(&mut pen, 1.0, 2.0);
        assert_eq!(collapsed, 2.0);
        assert!(pen.is_nan(), "a NaN pen stays NaN");
    }
    // ==================================================================
    // has_margin_collapse_blocker (predicate)
    // ==================================================================
    #[test]
    fn has_margin_collapse_blocker_basic_true_false() {
        let none = BoxProps::default();
        assert!(!has_margin_collapse_blocker(&none, HTB, true));
        assert!(!has_margin_collapse_blocker(&none, HTB, false));
        let top_border = box_props(
            EdgeSizes::default(),
            edges(1.0, 0.0, 0.0, 0.0),
            EdgeSizes::default(),
        );
        assert!(has_margin_collapse_blocker(&top_border, HTB, true));
        assert!(!has_margin_collapse_blocker(&top_border, HTB, false));
        let bottom_padding = box_props(
            EdgeSizes::default(),
            EdgeSizes::default(),
            edges(0.0, 0.0, 0.5, 0.0),
        );
        assert!(!has_margin_collapse_blocker(&bottom_padding, HTB, true));
        assert!(has_margin_collapse_blocker(&bottom_padding, HTB, false));
    }
    #[test]
    fn has_margin_collapse_blocker_maps_edges_per_writing_mode() {
        // In vertical writing modes the main axis is horizontal: left/right block.
        let left_border = box_props(
            EdgeSizes::default(),
            edges(0.0, 0.0, 0.0, 3.0),
            EdgeSizes::default(),
        );
        assert!(has_margin_collapse_blocker(&left_border, VRL, true));
        assert!(!has_margin_collapse_blocker(&left_border, VRL, false));
        // The same box does not block in horizontal-tb (left is a cross edge there).
        assert!(!has_margin_collapse_blocker(&left_border, HTB, true));
        assert!(!has_margin_collapse_blocker(&left_border, HTB, false));
    }
    #[test]
    fn has_margin_collapse_blocker_ignores_negative_and_nan_edges() {
        let weird = box_props(
            EdgeSizes::default(),
            edges(-5.0, 0.0, f32::NAN, 0.0),
            edges(f32::NAN, 0.0, -1.0, 0.0),
        );
        // `> 0.0` is false for both negatives and NaN -> nothing blocks, no panic.
        assert!(!has_margin_collapse_blocker(&weird, HTB, true));
        assert!(!has_margin_collapse_blocker(&weird, HTB, false));
        // Infinity does block.
        let inf = box_props(
            EdgeSizes::default(),
            edges(f32::INFINITY, 0.0, 0.0, 0.0),
            EdgeSizes::default(),
        );
        assert!(has_margin_collapse_blocker(&inf, HTB, true));
    }
    // ==================================================================
    // is_bk_or_nl_class / is_bidi_control / is_css_document_whitespace
    // ==================================================================
    #[test]
    fn is_bk_or_nl_class_covers_exactly_vt_ff_nel_ls_ps() {
        for c in ['\u{000B}', '\u{000C}', '\u{0085}', '\u{2028}', '\u{2029}'] {
            assert!(is_bk_or_nl_class(c), "{:04X} must be BK/NL", c as u32);
        }
        // LF and CR are handled separately by the callers, not by this predicate.
        for c in ['\n', '\r', ' ', '\t', 'a', '\0', '\u{200B}', char::MAX] {
            assert!(!is_bk_or_nl_class(c), "{:04X} must not be BK/NL", c as u32);
        }
    }
    #[test]
    fn is_bidi_control_covers_the_uax9_set_only() {
        for c in [
            '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}',
            '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}', '\u{061C}',
        ] {
            assert!(is_bidi_control(c), "{:04X} must be a bidi control", c as u32);
        }
        for c in ['a', ' ', '\u{200B}', '\u{2028}', '\u{202F}', '\u{2065}', char::MAX] {
            assert!(!is_bidi_control(c), "{:04X} must not be a bidi control", c as u32);
        }
    }
    #[test]
    fn is_css_document_whitespace_excludes_other_unicode_spaces() {
        for c in [' ', '\t', '\n', '\r', '\x0C'] {
            assert!(is_css_document_whitespace(c));
        }
        // NBSP, ideographic space, ZWSP, LS and VT are NOT document white space.
        for c in ['\u{00A0}', '\u{3000}', '\u{200B}', '\u{2028}', '\u{000B}', 'a'] {
            assert!(
                !is_css_document_whitespace(c),
                "{:04X} must not be document white space",
                c as u32
            );
        }
    }
    // ==================================================================
    // split_at_forced_breaks / split_at_bk_nl_chars (other)
    // ==================================================================
    #[test]
    fn split_at_forced_breaks_handles_every_newline_flavour() {
        assert_eq!(split_at_forced_breaks(""), vec![String::new()]);
        assert_eq!(split_at_forced_breaks("abc"), vec!["abc".to_string()]);
        assert_eq!(split_at_forced_breaks("a\nb"), vec!["a", "b"]);
        assert_eq!(split_at_forced_breaks("a\r\nb"), vec!["a", "b"]);
        assert_eq!(split_at_forced_breaks("a\rb"), vec!["a", "b"]);
        // A lone \r followed by another \r is two breaks, not one.
        assert_eq!(split_at_forced_breaks("a\r\rb"), vec!["a", "", "b"]);
        // Leading / trailing breaks produce empty leading / trailing segments.
        assert_eq!(split_at_forced_breaks("\na\n"), vec!["", "a", ""]);
        // BK/NL class chars break too.
        assert_eq!(split_at_forced_breaks("a\u{2028}b"), vec!["a", "b"]);
        assert_eq!(split_at_forced_breaks("a\u{000B}b"), vec!["a", "b"]);
    }
    #[test]
    fn split_at_forced_breaks_preserves_every_non_break_char() {
        let text = "héllo 🌍\u{0301}\u{200B}x";
        let segs = split_at_forced_breaks(text);
        assert_eq!(segs.len(), 1);
        assert_eq!(segs[0], text, "no char-boundary slicing damage");
        // The number of segments is always (number of breaks) + 1.
        let many = "a\nb\rc\r\nd\u{2029}e";
        assert_eq!(split_at_forced_breaks(many).len(), 5);
        assert_eq!(split_at_forced_breaks(many).concat(), "abcde");
    }
    #[test]
    fn split_at_forced_breaks_survives_a_huge_all_break_input() {
        let text = "\n".repeat(10_000);
        let segs = split_at_forced_breaks(&text);
        assert_eq!(segs.len(), 10_001);
        assert!(segs.iter().all(String::is_empty));
    }
    #[test]
    fn split_at_bk_nl_chars_ignores_lf_and_cr() {
        assert_eq!(split_at_bk_nl_chars(""), vec![String::new()]);
        // \n and \r are collapsible in normal/nowrap, so they are NOT split points.
        assert_eq!(split_at_bk_nl_chars("a\nb\rc"), vec!["a\nb\rc".to_string()]);
        // BK/NL class chars still force a break.
        assert_eq!(split_at_bk_nl_chars("a\u{0085}b"), vec!["a", "b"]);
        assert_eq!(split_at_bk_nl_chars("\u{2029}"), vec!["", ""]);
    }
    // ==================================================================
    // is_east_asian_wide / is_east_asian_fullwidth_or_wide (predicates)
    // ==================================================================
    #[test]
    fn is_east_asian_wide_matches_cjk_kana_and_hangul() {
        for c in ['中', '一', 'あ', 'カ', '。', '가', 'ㄅ', 'A'] {
            assert!(is_east_asian_wide(c), "{:04X} must be wide", c as u32);
        }
        for c in ['a', 'Z', '0', ' ', 'é', '\u{0000}', char::MAX] {
            assert!(!is_east_asian_wide(c), "{:04X} must not be wide", c as u32);
        }
    }
    #[test]
    fn is_east_asian_wide_range_boundaries_are_inclusive() {
        assert!(!is_east_asian_wide('\u{4DFF}')); // just below CJK Unified
        assert!(is_east_asian_wide('\u{4E00}')); // first CJK Unified
        assert!(is_east_asian_wide('\u{9FFF}')); // last CJK Unified
        assert!(!is_east_asian_wide('\u{A000}')); // Yi — not in this list
        assert!(!is_east_asian_wide('\u{FF00}')); // just below fullwidth forms
        assert!(is_east_asian_wide('\u{FF01}')); // first fullwidth form
        assert!(is_east_asian_wide('\u{FF60}')); // last fullwidth form
        assert!(!is_east_asian_wide('\u{FF61}')); // halfwidth forms start here
    }
    #[test]
    fn is_east_asian_fullwidth_or_wide_excludes_hangul_but_wide_does_not() {
        // The two predicates deliberately disagree on Hangul: the segment-break
        // transform must NOT drop breaks between Hangul syllables.
        assert!(is_east_asian_wide('가'));
        assert!(!is_east_asian_fullwidth_or_wide('가'));
        assert!(!is_east_asian_fullwidth_or_wide('\u{1100}')); // Hangul Jamo
        assert!(!is_east_asian_fullwidth_or_wide('\u{3130}')); // Compat Jamo
        assert!(!is_east_asian_fullwidth_or_wide('\u{A960}')); // Jamo Extended-A
        assert!(!is_east_asian_fullwidth_or_wide('\u{D7B0}')); // Jamo Extended-B
        // Han / kana still count, and the halfwidth + Yi ranges are added on top.
        assert!(is_east_asian_fullwidth_or_wide('中'));
        assert!(is_east_asian_fullwidth_or_wide('あ'));
        assert!(is_east_asian_fullwidth_or_wide('\u{FF61}'));
        assert!(is_east_asian_fullwidth_or_wide('\u{A000}'));
        assert!(!is_east_asian_fullwidth_or_wide('a'));
        assert!(!is_east_asian_fullwidth_or_wide(char::MAX));
    }
    // ==================================================================
    // apply_segment_break_transform (other)
    // ==================================================================
    #[test]
    fn apply_segment_break_transform_converts_breaks_to_a_single_space() {
        assert_eq!(apply_segment_break_transform(""), "");
        assert_eq!(apply_segment_break_transform("ab"), "ab");
        assert_eq!(apply_segment_break_transform("a\nb"), "a b");
        assert_eq!(apply_segment_break_transform("a\r\nb"), "a b");
        assert_eq!(apply_segment_break_transform("a\rb"), "a b");
        // §4.1.1: white space around the break is removed first.
        assert_eq!(apply_segment_break_transform("a \n b"), "a b");
        assert_eq!(apply_segment_break_transform("a\t\n\tb"), "a b");
        // Consecutive breaks collapse into one space.
        assert_eq!(apply_segment_break_transform("a\n\n\nb"), "a b");
    }
    #[test]
    fn apply_segment_break_transform_at_the_string_edges() {
        // No char before -> still a space; no char after -> still a space.
        assert_eq!(apply_segment_break_transform("\na"), " a");
        assert_eq!(apply_segment_break_transform("a\n"), "a ");
        assert_eq!(apply_segment_break_transform("\n"), " ");
        assert_eq!(apply_segment_break_transform("  \n  "), " ");
    }
    #[test]
    fn apply_segment_break_transform_removes_breaks_around_zwsp_and_cjk() {
        // Rule 1: adjacent to U+200B ZERO WIDTH SPACE -> the break disappears.
        assert_eq!(apply_segment_break_transform("a\u{200B}\nb"), "a\u{200B}b");
        assert_eq!(apply_segment_break_transform("a\n\u{200B}b"), "a\u{200B}b");
        // Rule 2: East Asian on both sides -> the break disappears.
        assert_eq!(apply_segment_break_transform("中\n文"), "中文");
        assert_eq!(apply_segment_break_transform("あ \n い"), "あい");
        // Only one side East Asian -> a space is kept.
        assert_eq!(apply_segment_break_transform("中\na"), "中 a");
        assert_eq!(apply_segment_break_transform("a\n中"), "a 中");
        // Hangul is excluded from rule 2, so the break becomes a space.
        assert_eq!(apply_segment_break_transform("가\n나"), "가 나");
    }
    #[test]
    fn apply_segment_break_transform_survives_pathological_input() {
        let text = "\n".repeat(5_000);
        assert_eq!(apply_segment_break_transform(&text), " ");
        // Interleaved breaks and multi-byte chars must not slice a char boundary.
        let mixed = "🌍\n🌍\n🌍";
        assert_eq!(apply_segment_break_transform(mixed), "🌍 🌍 🌍");
    }
    // ==================================================================
    // apply_text_transform (other)
    // ==================================================================
    #[test]
    fn apply_text_transform_none_is_the_identity() {
        for s in ["", "abc", "ÄÖÜ", "🌍", " \t\n"] {
            assert_eq!(apply_text_transform(s, TextTransform::None), s);
        }
    }
    #[test]
    fn apply_text_transform_case_changes_handle_growing_and_multibyte_chars() {
        assert_eq!(apply_text_transform("abc", TextTransform::Uppercase), "ABC");
        // ß uppercases to two chars — the output is longer than the input.
        assert_eq!(apply_text_transform("straße", TextTransform::Uppercase), "STRASSE");
        assert_eq!(apply_text_transform("ÄÖÜ", TextTransform::Lowercase), "äöü");
        assert_eq!(apply_text_transform("", TextTransform::Uppercase), "");
        assert_eq!(apply_text_transform("🌍", TextTransform::Uppercase), "🌍");
    }
    #[test]
    fn apply_text_transform_capitalize_uses_word_boundaries() {
        assert_eq!(
            apply_text_transform("hello world", TextTransform::Capitalize),
            "Hello World"
        );
        // A leading digit is not alphabetic and is not a boundary either.
        assert_eq!(
            apply_text_transform("1st place", TextTransform::Capitalize),
            "1st Place"
        );
        // ASCII punctuation opens a new word.
        assert_eq!(
            apply_text_transform("a-b (c)", TextTransform::Capitalize),
            "A-B (C)"
        );
        assert_eq!(apply_text_transform("élan", TextTransform::Capitalize), "Élan");
        assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
    }
    #[test]
    fn apply_text_transform_fullwidth_maps_the_ascii_block() {
        assert_eq!(apply_text_transform("a", TextTransform::FullWidth), "\u{FF41}");
        assert_eq!(apply_text_transform("!", TextTransform::FullWidth), "\u{FF01}");
        assert_eq!(apply_text_transform("~", TextTransform::FullWidth), "\u{FF5E}");
        assert_eq!(apply_text_transform(" ", TextTransform::FullWidth), "\u{3000}");
        // Chars outside U+0021..U+007E are passed through untouched.
        assert_eq!(apply_text_transform("\t\n", TextTransform::FullWidth), "\t\n");
        assert_eq!(apply_text_transform("\u{7F}", TextTransform::FullWidth), "\u{7F}");
        assert_eq!(apply_text_transform("中🌍", TextTransform::FullWidth), "中🌍");
    }
    // ==================================================================
    // layout_initial_letter (numeric)
    // ==================================================================
    #[test]
    fn layout_initial_letter_rejects_degenerate_parameters() {
        // size <= 0, line_height <= 0 or content width <= 0 => no exclusion at all.
        assert_eq!(layout_initial_letter(0.0, 3, 1000.0, 20.0), (0.0, 0.0));
        assert_eq!(layout_initial_letter(-1.0, 3, 1000.0, 20.0), (0.0, 0.0));
        assert_eq!(layout_initial_letter(3.0, 3, 1000.0, 0.0), (0.0, 0.0));
        assert_eq!(layout_initial_letter(3.0, 3, 1000.0, -20.0), (0.0, 0.0));
        assert_eq!(layout_initial_letter(3.0, 3, 0.0, 20.0), (0.0, 0.0));
        assert_eq!(layout_initial_letter(3.0, 3, -10.0, 20.0), (0.0, 0.0));
    }
    #[test]
    fn layout_initial_letter_computes_a_classic_drop_cap() {
        // 3 lines x 20px => 60px tall; 60 * 0.7 + 4px gap => 46px wide.
        assert_eq!(layout_initial_letter(3.0, 3, 1000.0, 20.0), (46.0, 60.0));
        // The width is clamped to the content box.
        assert_eq!(layout_initial_letter(3.0, 3, 10.0, 20.0), (10.0, 60.0));
        // sink == 0 (raised cap): the exclusion still covers the whole letter.
        assert_eq!(layout_initial_letter(3.0, 0, 1000.0, 20.0), (46.0, 60.0));
        // sink > size (sunken cap): the exclusion grows past the letter.
        assert_eq!(layout_initial_letter(2.0, 5, 1000.0, 20.0), (32.0, 100.0));
    }
    #[test]
    fn layout_initial_letter_exclusion_is_never_shorter_than_the_letter() {
        for size_lines in [0.5_f32, 1.0, 3.0, 100.0] {
            for sink in [0_u32, 1, 3, 100] {
                let (w, h) = layout_initial_letter(size_lines, sink, 10_000.0, 20.0);
                let letter_height = size_lines * 20.0;
                assert!(h >= letter_height, "{size_lines}/{sink}: {h} < {letter_height}");
                assert!(w > 0.0 && w.is_finite());
            }
        }
    }
    #[test]
    fn layout_initial_letter_saturates_at_extreme_sinks_and_sizes() {
        // u32::MAX lines of sink: a huge but finite exclusion, no overflow panic.
        let (w, h) = layout_initial_letter(1.0, u32::MAX, 1000.0, 20.0);
        assert!(h.is_finite() && h > 0.0);
        assert_eq!(w, 18.0); // 20 * 0.7 + 4
        // f32::MAX size: the height saturates to +inf, the width clamps to the box.
        let (w, h) = layout_initial_letter(f32::MAX, 1, 1000.0, f32::MAX);
        assert_eq!(w, 1000.0);
        assert!(h.is_infinite());
    }
    #[test]
    fn layout_initial_letter_with_nan_and_inf_produces_a_defined_result() {
        // NaN passes the `<= 0.0` guard (NaN compares false), so the maths runs.
        // f32::min/max discard NaN, so the result stays defined: the width falls back
        // to the content box and the height to the sink-derived exclusion.
        let (w, h) = layout_initial_letter(f32::NAN, 3, 1000.0, 20.0);
        assert_eq!(w, 1000.0);
        assert_eq!(h, 60.0);
        // An infinite line height clamps the width and yields an infinite exclusion.
        let (w, h) = layout_initial_letter(3.0, 3, 1000.0, f32::INFINITY);
        assert_eq!(w, 1000.0);
        assert!(h.is_infinite());
    }
    // ==================================================================
    // get_cell_spans (other)
    // ==================================================================
    fn cell_dom(attrs: Vec<AttributeType>) -> StyledDom {
        let mut cell = Dom::create_div();
        for a in attrs {
            cell = cell.with_attribute(a);
        }
        styled(Dom::create_body().with_child(cell), "")
    }
    #[test]
    fn get_cell_spans_defaults_to_one_when_unset() {
        let dom = cell_dom(Vec::new());
        assert_eq!(get_cell_spans(&dom, DIV_NODE), (1, 1));
    }
    #[test]
    fn get_cell_spans_clamps_hostile_html_values() {
        // Zero / negative / i32::MIN spans must clamp up to 1, never wrap or panic.
        for n in [0, -1, i32::MIN] {
            let dom = cell_dom(vec![AttributeType::ColSpan(n), AttributeType::RowSpan(n)]);
            assert_eq!(get_cell_spans(&dom, DIV_NODE), (1, 1), "span {n}");
        }
        // Absurd spans clamp down to the HTML limits (1000 / 65534) so the column
        // and row vectors cannot be grown into an OOM.
        let dom = cell_dom(vec![
            AttributeType::ColSpan(i32::MAX),
            AttributeType::RowSpan(i32::MAX),
        ]);
        assert_eq!(get_cell_spans(&dom, DIV_NODE), (1000, 65534));
        // In-range values pass through.
        let dom = cell_dom(vec![
            AttributeType::ColSpan(3),
            AttributeType::RowSpan(7),
        ]);
        assert_eq!(get_cell_spans(&dom, DIV_NODE), (3, 7));
    }
    // ==================================================================
    // get_float_property / get_clear_property (other)
    // ==================================================================
    #[test]
    fn get_float_and_clear_properties_default_to_none_for_a_missing_node() {
        let dom = text_dom("x", "");
        assert_eq!(get_float_property(&dom, None), LayoutFloat::None);
        assert_eq!(get_clear_property(&dom, None), LayoutClear::None);
        // An unstyled node also reports the initial values.
        assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::None);
        assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::None);
    }
    #[test]
    fn get_float_and_clear_properties_read_the_cascade() {
        let dom = text_dom("x", ".p { float: right; clear: both; }");
        assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::Right);
        assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::Both);
        let dom = text_dom("x", ".p { float: left; clear: left; }");
        assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::Left);
        assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::Left);
    }
    // ==================================================================
    // split_text_for_whitespace (other)
    // ==================================================================
    #[test]
    fn split_text_for_whitespace_collapses_runs_in_normal_mode() {
        let dom = text_dom("  a  b  ", "");
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "  a  b  ", &plain_style());
        assert_eq!(out.len(), 1);
        // Interior runs collapse to one space; the leading/trailing ones are kept
        // as a single space each (they are trimmed later, at line-layout time).
        assert_eq!(text_of(&out[0]), Some(" a b "));
    }
    #[test]
    fn split_text_for_whitespace_on_empty_and_whitespace_only_text() {
        let dom = text_dom("", "");
        assert!(split_text_for_whitespace(&dom, TEXT_NODE, "", &plain_style()).is_empty());
        // A whitespace-only node collapses to exactly one space.
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "   \t  ", &plain_style());
        assert_eq!(out.len(), 1);
        assert_eq!(text_of(&out[0]), Some(" "));
    }
    #[test]
    fn split_text_for_whitespace_honours_newlines_in_pre() {
        let dom = text_dom("a\nb", ".p { white-space: pre; }");
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\nb", &plain_style());
        assert_eq!(out.len(), 3);
        assert_eq!(text_of(&out[0]), Some("a"));
        assert!(matches!(out[1], InlineContent::LineBreak(_)));
        assert_eq!(text_of(&out[2]), Some("b"));
        // Tabs become explicit Tab items so the tab-size can be applied later.
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\tb", &plain_style());
        assert_eq!(out.len(), 3);
        assert_eq!(text_of(&out[0]), Some("a"));
        assert!(matches!(out[1], InlineContent::Tab { .. }));
        assert_eq!(text_of(&out[2]), Some("b"));
        // Whitespace is preserved verbatim in `pre`.
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "  a  ", &plain_style());
        assert_eq!(text_of(&out[0]), Some("  a  "));
    }
    #[test]
    fn split_text_for_whitespace_normalises_cr_and_crlf() {
        let dom = text_dom("x", ".p { white-space: pre; }");
        // \r\n and a bare \r must behave exactly like \n (one forced break each).
        for text in ["a\r\nb", "a\rb", "a\nb"] {
            let out = split_text_for_whitespace(&dom, TEXT_NODE, text, &plain_style());
            assert_eq!(out.len(), 3, "{text:?}");
            assert!(matches!(out[1], InlineContent::LineBreak(_)), "{text:?}");
        }
    }
    #[test]
    fn split_text_for_whitespace_forces_breaks_on_bk_nl_chars_in_every_mode() {
        // U+2028 LINE SEPARATOR is a forced break even in white-space: normal.
        let dom = text_dom("x", "");
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\u{2028}b", &plain_style());
        assert_eq!(out.len(), 3);
        assert!(matches!(out[1], InlineContent::LineBreak(_)));
        let dom = text_dom("x", ".p { white-space: pre-line; }");
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\u{000C}b", &plain_style());
        assert_eq!(out.len(), 3);
        assert!(matches!(out[1], InlineContent::LineBreak(_)));
    }
    #[test]
    fn split_text_for_whitespace_strips_bidi_controls_before_collapsing() {
        let dom = text_dom("x", "");
        // The RLM between the two spaces must not stop them from collapsing.
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "a \u{200F} b", &plain_style());
        assert_eq!(out.len(), 1);
        assert_eq!(text_of(&out[0]), Some("a b"));
    }
    #[test]
    fn split_text_for_whitespace_pre_line_collapses_spaces_but_keeps_breaks() {
        let dom = text_dom("x", ".p { white-space: pre-line; }");
        let out = split_text_for_whitespace(&dom, TEXT_NODE, "a  b\n  c  ", &plain_style());
        assert_eq!(out.len(), 3);
        assert_eq!(text_of(&out[0]), Some("a b"));
        assert!(matches!(out[1], InlineContent::LineBreak(_)));
        assert_eq!(text_of(&out[2]), Some("c"));
    }
    #[test]
    fn split_text_for_whitespace_survives_a_huge_multibyte_input() {
        let dom = text_dom("x", ".p { white-space: pre; }");
        let text = "🌍\n".repeat(2_000);
        let out = split_text_for_whitespace(&dom, TEXT_NODE, &text, &plain_style());
        // 2000 globes + 2000 breaks (the trailing empty segment emits no Text item).
        assert_eq!(out.len(), 4_000);
        assert_eq!(text_of(&out[0]), Some("🌍"));
    }
}