1
//! Builder function to convert CssPropertyCache → CompactLayoutCache.
2
//!
3
//! Called once after restyle + apply_ua_css + compute_inherited_values.
4
//! Uses typed getters on CssPropertyCache (which cascade through all sources)
5
//! to resolve each property for the "normal" state (all pseudo-states = false).
6

            
7
use crate::dom::{NodeData, NodeId};
8
use crate::prop_cache::CssPropertyCache;
9

            
10
use crate::styled_dom::StyledNodeState;
11
// wildcard import: this module is the consumer of the whole compact_cache codec
12
// (encode/decode helpers + sentinel consts); enumerating them is unmaintainable.
13
#[allow(clippy::wildcard_imports)]
14
use azul_css::compact_cache::*;
15
use azul_css::css::CssPropertyValue;
16
use azul_css::props::property::CssProperty;
17
use azul_css::props::basic::length::SizeMetric;
18
use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
19
use azul_css::props::layout::flex::LayoutFlexBasis;
20
use azul_css::props::layout::position::LayoutZIndex;
21
use core::hash::{Hash, Hasher};
22
use alloc::vec::Vec;
23
use crate::hash::DefaultHasher;
24

            
25
impl CssPropertyCache {
26
    /// Build a `CompactLayoutCache` from the current property cache state.
27
    ///
28
    /// Must be called after `restyle()`, `apply_ua_css()`, and `compute_inherited_values()`.
29
    /// Resolves all layout-relevant properties for every node in the "normal" state
30
    /// (no hover/active/focus) and encodes them into compact arrays.
31
    ///
32
    /// Tier 1/2/2b provide fast-path access for layout-hot properties.
33
    /// Non-compact properties (background, transform, box-shadow, etc.) are
34
    /// resolved via the slow cascade path in `get_property_slow()`.
35
    ///
36
    /// `prev_font_hashes` is the per-node font hash array from the previous frame.
37
    /// When non-empty, each node's new `font_family_hash` is compared against the
38
    /// previous value, and differing nodes are recorded in `font_dirty_nodes`.
39
    /// On the first build (empty slice), ALL text nodes are marked dirty.
40
    // fixed-point encoders: z-index and line-height (%×10) are range-checked
41
    // against the i16 sentinel threshold before the deliberate narrowing cast.
42
    #[allow(clippy::cast_possible_truncation)]
43
    #[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
44
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
45
8
    pub fn build_compact_cache(
46
8
        &self,
47
8
        node_data: &[NodeData],
48
8
        prev_font_hashes: &[u64],
49
8
    ) -> CompactLayoutCache {
50
8
        let node_count = self.node_count;
51
8
        let default_state = StyledNodeState::default();
52
8
        let mut result = CompactLayoutCache::with_capacity(node_count);
53

            
54
19
        for (i, nd) in node_data.iter().enumerate().take(node_count) {
55
19
            let node_id = NodeId::new(i);
56

            
57
            // =====================================================================
58
            // Tier 1: Encode all 20 enum properties into u64
59
            // =====================================================================
60

            
61
19
            let display = self
62
19
                .get_display(nd, &node_id, &default_state)
63
19
                .and_then(|v| v.get_property().copied())
64
19
                .unwrap_or_default();
65
19
            let position = self
66
19
                .get_position(nd, &node_id, &default_state)
67
19
                .and_then(|v| v.get_property().copied())
68
19
                .unwrap_or_default();
69
19
            let float = self
70
19
                .get_float(nd, &node_id, &default_state)
71
19
                .and_then(|v| v.get_property().copied())
72
19
                .unwrap_or_default();
73
19
            let overflow_x = self
74
19
                .get_overflow_x(nd, &node_id, &default_state)
75
19
                .and_then(|v| v.get_property().copied())
76
19
                .unwrap_or_default();
77
19
            let overflow_y = self
78
19
                .get_overflow_y(nd, &node_id, &default_state)
79
19
                .and_then(|v| v.get_property().copied())
80
19
                .unwrap_or_default();
81
19
            let box_sizing = self
82
19
                .get_box_sizing(nd, &node_id, &default_state)
83
19
                .and_then(|v| v.get_property().copied())
84
19
                .unwrap_or_default();
85
19
            let flex_direction = self
86
19
                .get_flex_direction(nd, &node_id, &default_state)
87
19
                .and_then(|v| v.get_property().copied())
88
19
                .unwrap_or_default();
89
19
            let flex_wrap = self
90
19
                .get_flex_wrap(nd, &node_id, &default_state)
91
19
                .and_then(|v| v.get_property().copied())
92
19
                .unwrap_or_default();
93
19
            let justify_content = self
94
19
                .get_justify_content(nd, &node_id, &default_state)
95
19
                .and_then(|v| v.get_property().copied())
96
19
                .unwrap_or_default();
97
19
            let align_items = self
98
19
                .get_align_items(nd, &node_id, &default_state)
99
19
                .and_then(|v| v.get_property().copied())
100
19
                .unwrap_or_default();
101
19
            let align_content = self
102
19
                .get_align_content(nd, &node_id, &default_state)
103
19
                .and_then(|v| v.get_property().copied())
104
19
                .unwrap_or_default();
105
19
            let writing_mode = self
106
19
                .get_writing_mode(nd, &node_id, &default_state)
107
19
                .and_then(|v| v.get_property().copied())
108
19
                .unwrap_or_default();
109
19
            let clear = self
110
19
                .get_clear(nd, &node_id, &default_state)
111
19
                .and_then(|v| v.get_property().copied())
112
19
                .unwrap_or_default();
113
19
            let font_weight = self
114
19
                .get_font_weight(nd, &node_id, &default_state)
115
19
                .and_then(|v| v.get_property().copied())
116
19
                .unwrap_or_default();
117
19
            let font_style = self
118
19
                .get_font_style(nd, &node_id, &default_state)
119
19
                .and_then(|v| v.get_property().copied())
120
19
                .unwrap_or_default();
121
19
            let text_align = self
122
19
                .get_text_align(nd, &node_id, &default_state)
123
19
                .and_then(|v| v.get_property().copied())
124
19
                .unwrap_or_default();
125
19
            let visibility = self
126
19
                .get_visibility(nd, &node_id, &default_state)
127
19
                .and_then(|v| v.get_property().copied())
128
19
                .unwrap_or_default();
129
19
            let white_space = self
130
19
                .get_white_space(nd, &node_id, &default_state)
131
19
                .and_then(|v| v.get_property().copied())
132
19
                .unwrap_or_default();
133
19
            let direction = self
134
19
                .get_direction(nd, &node_id, &default_state)
135
19
                .and_then(|v| v.get_property().copied())
136
19
                .unwrap_or_default();
137
19
            let vertical_align = self
138
19
                .get_vertical_align(nd, &node_id, &default_state)
139
19
                .and_then(|v| v.get_property().copied())
140
19
                .unwrap_or_default();
141

            
142
19
            let border_collapse = self
143
19
                .get_border_collapse(nd, &node_id, &default_state)
144
19
                .and_then(|v| v.get_property().copied())
145
19
                .unwrap_or_default();
146

            
147
19
            result.tier1_enums[i] = encode_tier1(
148
19
                display,
149
19
                position,
150
19
                float,
151
19
                overflow_x,
152
19
                overflow_y,
153
19
                box_sizing,
154
19
                flex_direction,
155
19
                flex_wrap,
156
19
                justify_content,
157
19
                align_items,
158
19
                align_content,
159
19
                writing_mode,
160
19
                clear,
161
19
                font_weight,
162
19
                font_style,
163
19
                text_align,
164
19
                visibility,
165
19
                white_space,
166
19
                direction,
167
19
                vertical_align,
168
19
                border_collapse,
169
19
            );
170

            
171
            // =====================================================================
172
            // Tier 2: Encode numeric dimension properties
173
            // =====================================================================
174

            
175
            // Width/Height are enums: Auto | Px(PixelValue) | MinContent | MaxContent | Calc
176
19
            if let Some(val) = self.get_width(nd, &node_id, &default_state) {
177
                result.tier2_dims[i].width = encode_layout_width(val);
178
19
            }
179
19
            if let Some(val) = self.get_height(nd, &node_id, &default_state) {
180
                result.tier2_dims[i].height = encode_layout_height(val);
181
19
            }
182

            
183
            // Min/Max Width/Height are simple PixelValue wrappers
184
19
            if let Some(val) = self.get_min_width(nd, &node_id, &default_state) {
185
                result.tier2_dims[i].min_width = encode_pixel_prop(val);
186
19
            }
187
19
            if let Some(val) = self.get_max_width(nd, &node_id, &default_state) {
188
                result.tier2_dims[i].max_width = encode_pixel_prop(val);
189
19
            }
190
19
            if let Some(val) = self.get_min_height(nd, &node_id, &default_state) {
191
                result.tier2_dims[i].min_height = encode_pixel_prop(val);
192
19
            }
193
19
            if let Some(val) = self.get_max_height(nd, &node_id, &default_state) {
194
                result.tier2_dims[i].max_height = encode_pixel_prop(val);
195
19
            }
196

            
197
            // Flex basis (enum: Auto | Exact(PixelValue))
198
19
            if let Some(val) = self.get_flex_basis(nd, &node_id, &default_state) {
199
                result.tier2_dims[i].flex_basis = encode_flex_basis(val);
200
19
            }
201

            
202
            // Font size
203
19
            if let Some(val) = self.get_font_size(nd, &node_id, &default_state) {
204
                result.tier2_dims[i].font_size = encode_pixel_prop(val);
205
19
            }
206

            
207
            // Padding (i16 × 10 resolved px)
208
19
            if let Some(val) = self.get_padding_top(nd, &node_id, &default_state) {
209
                result.tier2_dims[i].padding_top = encode_css_pixel_as_i16(val);
210
19
            }
211
19
            if let Some(val) = self.get_padding_right(nd, &node_id, &default_state) {
212
                result.tier2_dims[i].padding_right = encode_css_pixel_as_i16(val);
213
19
            }
214
19
            if let Some(val) = self.get_padding_bottom(nd, &node_id, &default_state) {
215
                result.tier2_dims[i].padding_bottom = encode_css_pixel_as_i16(val);
216
19
            }
217
19
            if let Some(val) = self.get_padding_left(nd, &node_id, &default_state) {
218
                result.tier2_dims[i].padding_left = encode_css_pixel_as_i16(val);
219
19
            }
220

            
221
            // Margin (i16, auto is special)
222
19
            if let Some(val) = self.get_margin_top(nd, &node_id, &default_state) {
223
                result.tier2_dims[i].margin_top = encode_margin_i16(val);
224
19
            }
225
19
            if let Some(val) = self.get_margin_right(nd, &node_id, &default_state) {
226
                result.tier2_dims[i].margin_right = encode_margin_i16(val);
227
19
            }
228
19
            if let Some(val) = self.get_margin_bottom(nd, &node_id, &default_state) {
229
                result.tier2_dims[i].margin_bottom = encode_margin_i16(val);
230
19
            }
231
19
            if let Some(val) = self.get_margin_left(nd, &node_id, &default_state) {
232
                result.tier2_dims[i].margin_left = encode_margin_i16(val);
233
19
            }
234

            
235
            // Border widths (i16 × 10 resolved px)
236
19
            if let Some(val) = self.get_border_top_width(nd, &node_id, &default_state) {
237
                result.tier2_dims[i].border_top_width = encode_css_pixel_as_i16(val);
238
19
            }
239
19
            if let Some(val) = self.get_border_right_width(nd, &node_id, &default_state) {
240
                result.tier2_dims[i].border_right_width = encode_css_pixel_as_i16(val);
241
19
            }
242
19
            if let Some(val) = self.get_border_bottom_width(nd, &node_id, &default_state) {
243
                result.tier2_dims[i].border_bottom_width = encode_css_pixel_as_i16(val);
244
19
            }
245
19
            if let Some(val) = self.get_border_left_width(nd, &node_id, &default_state) {
246
                result.tier2_dims[i].border_left_width = encode_css_pixel_as_i16(val);
247
19
            }
248

            
249
            // Position offsets (top/right/bottom/left)
250
19
            if let Some(val) = self.get_top(nd, &node_id, &default_state) {
251
                result.tier2_dims[i].top = encode_css_pixel_as_i16(val);
252
19
            }
253
19
            if let Some(val) = self.get_right(nd, &node_id, &default_state) {
254
                result.tier2_dims[i].right = encode_css_pixel_as_i16(val);
255
19
            }
256
19
            if let Some(val) = self.get_bottom(nd, &node_id, &default_state) {
257
                result.tier2_dims[i].bottom = encode_css_pixel_as_i16(val);
258
19
            }
259
19
            if let Some(val) = self.get_left(nd, &node_id, &default_state) {
260
                result.tier2_dims[i].left = encode_css_pixel_as_i16(val);
261
19
            }
262

            
263
            // Flex grow/shrink (u16 × 100)
264
19
            if let Some(val) = self.get_flex_grow(nd, &node_id, &default_state) {
265
                if let Some(exact) = val.get_property() {
266
                    result.tier2_dims[i].flex_grow = encode_flex_u16(exact.inner.get());
267
                }
268
19
            }
269
19
            if let Some(val) = self.get_flex_shrink(nd, &node_id, &default_state) {
270
                if let Some(exact) = val.get_property() {
271
                    result.tier2_dims[i].flex_shrink = encode_flex_u16(exact.inner.get());
272
                }
273
19
            }
274

            
275
            // =====================================================================
276
            // Tier 2 cold: Paint-only properties
277
            // =====================================================================
278

            
279
            // Z-index
280
19
            if let Some(val) = self.get_z_index(nd, &node_id, &default_state) {
281
                if let Some(exact) = val.get_property() {
282
                    match exact {
283
                        LayoutZIndex::Auto => result.tier2_cold[i].z_index = I16_AUTO,
284
                        LayoutZIndex::Integer(z) => {
285
                            // Two-sided, like the line-height encoder: a large NEGATIVE z
286
                            // used to fall through to `*z as i16` and WRAP positive
287
                            // (-40000 -> +25536). Escape both out-of-range ends to the
288
                            // sentinel (tier 3) so the real value is preserved.
289
                            result.tier2_cold[i].z_index =
290
                                if *z >= -32768 && *z < i32::from(I16_SENTINEL_THRESHOLD) {
291
                                    *z as i16
292
                                } else {
293
                                    I16_SENTINEL
294
                                };
295
                        }
296
                    }
297
                }
298
19
            }
299

            
300
            // Border styles (packed into u16)
301
            {
302
19
                let bts = self.get_border_top_style(nd, &node_id, &default_state)
303
19
                    .and_then(|v| v.get_property().copied())
304
19
                    .map(|v| v.inner)
305
19
                    .unwrap_or_default();
306
19
                let brs = self.get_border_right_style(nd, &node_id, &default_state)
307
19
                    .and_then(|v| v.get_property().copied())
308
19
                    .map(|v| v.inner)
309
19
                    .unwrap_or_default();
310
19
                let bbs = self.get_border_bottom_style(nd, &node_id, &default_state)
311
19
                    .and_then(|v| v.get_property().copied())
312
19
                    .map(|v| v.inner)
313
19
                    .unwrap_or_default();
314
19
                let bls = self.get_border_left_style(nd, &node_id, &default_state)
315
19
                    .and_then(|v| v.get_property().copied())
316
19
                    .map(|v| v.inner)
317
19
                    .unwrap_or_default();
318
19
                result.tier2_cold[i].border_styles_packed =
319
19
                    encode_border_styles_packed(bts, brs, bbs, bls);
320
            }
321

            
322
            // Border colors (ColorU → u32 as 0xRRGGBBAA)
323
19
            if let Some(val) = self.get_border_top_color(nd, &node_id, &default_state) {
324
                if let Some(color) = val.get_property() {
325
                    result.tier2_cold[i].border_top_color = encode_color_u32(&color.inner);
326
                }
327
19
            }
328
19
            if let Some(val) = self.get_border_right_color(nd, &node_id, &default_state) {
329
                if let Some(color) = val.get_property() {
330
                    result.tier2_cold[i].border_right_color = encode_color_u32(&color.inner);
331
                }
332
19
            }
333
19
            if let Some(val) = self.get_border_bottom_color(nd, &node_id, &default_state) {
334
                if let Some(color) = val.get_property() {
335
                    result.tier2_cold[i].border_bottom_color = encode_color_u32(&color.inner);
336
                }
337
19
            }
338
19
            if let Some(val) = self.get_border_left_color(nd, &node_id, &default_state) {
339
                if let Some(color) = val.get_property() {
340
                    result.tier2_cold[i].border_left_color = encode_color_u32(&color.inner);
341
                }
342
19
            }
343

            
344
            // Border spacing (two PixelValue → i16 × 10 resolved px)
345
19
            if let Some(val) = self.get_border_spacing(nd, &node_id, &default_state) {
346
                if let Some(spacing) = val.get_property() {
347
                    if spacing.horizontal.metric == SizeMetric::Px {
348
                        result.tier2_cold[i].border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
349
                    }
350
                    if spacing.vertical.metric == SizeMetric::Px {
351
                        result.tier2_cold[i].border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
352
                    }
353
                }
354
19
            }
355

            
356
            // Tab size (PixelValue → i16 × 10 resolved px)
357
19
            if let Some(val) = self.get_tab_size(nd, &node_id, &default_state) {
358
                result.tier2_cold[i].tab_size = encode_css_pixel_as_i16(val);
359
19
            }
360

            
361
            // =====================================================================
362
            // Tier 2b: Text properties
363
            // =====================================================================
364

            
365
            // Text color (ColorU → u32 as 0xRRGGBBAA)
366
19
            if let Some(val) = self.get_text_color(nd, &node_id, &default_state) {
367
                if let Some(color) = val.get_property() {
368
                    let c = &color.inner;
369
                    result.tier2b_text[i].text_color =
370
                        (u32::from(c.r) << 24) | (u32::from(c.g) << 16) | (u32::from(c.b) << 8) | u32::from(c.a);
371
                }
372
19
            }
373

            
374
            // Font-family (hash the whole StyleFontFamilyVec for fast comparison)
375
19
            if let Some(val) = self.get_font_family(nd, &node_id, &default_state) {
376
                if let Some(families) = val.get_property() {
377
                    let mut hasher = DefaultHasher::new();
378
                    families.hash(&mut hasher);
379
                    let h = hasher.finish();
380
                    let h = if h == 0 { 1 } else { h };
381
                    result.tier2b_text[i].font_family_hash = h;
382
                    result.font_hash_to_families.insert(h, families.clone());
383
                }
384
19
            }
385

            
386
            // Line-height. Parser convention: a NEGATIVE normalized() is an
387
            // ABSOLUTE pixel line-height, a positive one a unitless multiple
388
            // (or percentage) of font-size. The two need different i16
389
            // scales:
390
            //  - positive: multiple × 1000 (120% -> 1200; range up to ~32x)
391
            //  - negative: -px × 10 (line-height: 40px -> -400; ±3276.7px)
392
            // The old single ×1000 scale overflowed i16 for any absolute
393
            // line-height above 32.76px, stored the SENTINEL, and the getter
394
            // decoded that as "line-height: normal" - `line-height: 40px`
395
            // was silently dropped on every normal-state node.
396
19
            if let Some(val) = self.get_line_height(nd, &node_id, &default_state) {
397
                if let Some(lh) = val.get_property() {
398
                    let n = lh.inner.normalized();
399
                    let stored = if n < 0.0 {
400
                        // Absolute px: clamp to the representable range
401
                        // instead of falling to the sentinel ("normal").
402
                        ((n * 10.0).round() as i32).max(-32768)
403
                    } else {
404
                        (n * 1000.0).round() as i32
405
                    };
406
                    if stored >= -32768 && stored < i32::from(I16_SENTINEL_THRESHOLD) {
407
                        result.tier2b_text[i].line_height = stored as i16;
408
                    } else {
409
                        result.tier2b_text[i].line_height = I16_SENTINEL;
410
                    }
411
                }
412
19
            }
413

            
414
            // Letter-spacing (PixelValue wrapper → i16 × 10 resolved px)
415
19
            if let Some(val) = self.get_letter_spacing(nd, &node_id, &default_state) {
416
                result.tier2b_text[i].letter_spacing = encode_css_pixel_as_i16(val);
417
19
            }
418

            
419
            // Word-spacing (PixelValue wrapper → i16 × 10 resolved px)
420
19
            if let Some(val) = self.get_word_spacing(nd, &node_id, &default_state) {
421
                result.tier2b_text[i].word_spacing = encode_css_pixel_as_i16(val);
422
19
            }
423

            
424
            // Text-indent (PixelValue wrapper → i16 × 10 resolved px)
425
19
            if let Some(val) = self.get_text_indent(nd, &node_id, &default_state) {
426
                result.tier2b_text[i].text_indent = encode_css_pixel_as_i16(val);
427
19
            }
428
        }
429

            
430
        // =====================================================================
431
        // Per-node font dirty tracking (P4)
432
        // Compare each node's font_family_hash against the previous frame's hash.
433
        // Nodes whose hash changed are recorded in font_dirty_nodes for
434
        // incremental font chain re-resolution instead of all-or-nothing.
435
        // =====================================================================
436
8
        result.font_dirty_nodes.clear();
437
21
        for i in 0..node_count {
438
21
            let new_hash = result.tier2b_text[i].font_family_hash;
439
21
            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
440
21
            if new_hash != old_hash {
441
4
                result.font_dirty_nodes.push(i);
442
17
            }
443
        }
444
        // Save current hashes as prev_font_hashes for next frame comparison
445
8
        result.prev_font_hashes = result.tier2b_text.iter().map(|t| t.font_family_hash).collect();
446

            
447
8
        result
448
8
    }
449

            
450
    /// Build compact cache with inheritance in a single pass.
451
    ///
452
    /// Replaces the separate `compute_inherited_values()` + `build_compact_cache()` calls.
453
    /// For each node (in DOM index order, which is pre-order = parents before children):
454
    ///   1. Copy parent's compact values for INHERITABLE properties
455
    ///   2. Apply this node's CSS properties on top (from `css_props` + inline + UA)
456
    ///   3. Write directly to compact arrays
457
    ///
458
    /// This eliminates 50K Vec clones from `compute_inherited_values` and
459
    /// avoids re-reading properties from 5 separate data structures.
460
37230
    pub fn build_compact_cache_with_inheritance(
461
37230
        &self,
462
37230
        node_data: &[NodeData],
463
37230
        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
464
37230
        prev_font_hashes: &[u64],
465
37230
    ) -> CompactLayoutCache {
466
37230
        self.build_compact_cache_with_inheritance_debug(node_data, node_hierarchy, prev_font_hashes, &mut None)
467
37230
    }
468

            
469
    /// Same as `build_compact_cache_with_inheritance` but with optional debug logging.
470
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
471
37233
    pub fn build_compact_cache_with_inheritance_debug(
472
37233
        &self,
473
37233
        node_data: &[NodeData],
474
37233
        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
475
37233
        prev_font_hashes: &[u64],
476
37233
        debug_messages: &mut Option<Vec<azul_css::LayoutDebugMessage>>,
477
37233
    ) -> CompactLayoutCache {
478
        // Inheritable tier1 CSS fields (font-weight/style, text-align, visibility,
479
        // white-space, direction, border-collapse). Copied from parent in Step 1.
480
        const INHERITABLE_TIER1_MASK: u64 =
481
            (FONT_WEIGHT_MASK << FONT_WEIGHT_SHIFT)
482
            | (FONT_STYLE_MASK << FONT_STYLE_SHIFT)
483
            | (TEXT_ALIGN_MASK << TEXT_ALIGN_SHIFT)
484
            | (VISIBILITY_MASK << VISIBILITY_SHIFT)
485
            | (WHITE_SPACE_MASK << WHITE_SPACE_SHIFT)
486
            | (DIRECTION_MASK << DIRECTION_SHIFT)
487
            | (BORDER_COLLAPSE_MASK << BORDER_COLLAPSE_SHIFT);
488

            
489
37233
        let node_count = self.node_count;
490
37233
        let default_state = StyledNodeState::default();
491
37233
        let mut result = CompactLayoutCache::with_capacity(node_count);
492

            
493
        // Pre-encode global CSS properties (from `*` rules) into compact form.
494
        // These are applied as baseline for every node before inheritance.
495
37233
        let mut global_tier1: u64 = 0;
496
37233
        let mut global_dims = CompactNodeProps::default();
497
37233
        let mut global_cold = CompactNodePropsCold::default();
498
37233
        let mut global_text = CompactTextProps::default();
499
37233
        let has_global = !self.global_css_props.is_empty();
500

            
501
37233
        if has_global {
502
            use azul_css::props::property::CssProperty;
503

            
504
16401
            for prop in &self.global_css_props {
505
14528
                result.uses_viewport_units |= css_property_uses_viewport_units(prop);
506
                // Apply each global property to the pre-encoded compact values
507
                macro_rules! global_tier1_enum {
508
                    ($variant:ident, $shift:ident, $mask:ident, $encoder:ident) => {
509
                        if let CssProperty::$variant(v) = prop {
510
                            if let Some(exact) = v.get_property() {
511
                                let encoded = u64::from($encoder(*exact));
512
                                let shifted_mask = $mask << $shift;
513
                                global_tier1 = (global_tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
514
                            }
515
                        }
516
                    };
517
                }
518

            
519
14528
                global_tier1_enum!(Display, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8);
520
14528
                global_tier1_enum!(Position, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8);
521
14528
                global_tier1_enum!(Float, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8);
522
14528
                global_tier1_enum!(OverflowX, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
523
14528
                global_tier1_enum!(OverflowY, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
524
14528
                global_tier1_enum!(BoxSizing, BOX_SIZING_SHIFT, BOX_SIZING_MASK, layout_box_sizing_to_u8);
525
14528
                global_tier1_enum!(FlexDirection, FLEX_DIRECTION_SHIFT, FLEX_DIR_MASK, layout_flex_direction_to_u8);
526
14528
                global_tier1_enum!(FlexWrap, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8);
527
14528
                global_tier1_enum!(JustifyContent, JUSTIFY_CONTENT_SHIFT, JUSTIFY_MASK, layout_justify_content_to_u8);
528
14528
                global_tier1_enum!(AlignItems, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8);
529
14528
                global_tier1_enum!(AlignContent, ALIGN_CONTENT_SHIFT, ALIGN_MASK, layout_align_content_to_u8);
530
14528
                global_tier1_enum!(Clear, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8);
531
14528
                global_tier1_enum!(Visibility, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8);
532
14528
                global_tier1_enum!(WritingMode, WRITING_MODE_SHIFT, WRITING_MODE_MASK, layout_writing_mode_to_u8);
533
14528
                global_tier1_enum!(FontWeight, FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, style_font_weight_to_u8);
534
14528
                global_tier1_enum!(FontStyle, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8);
535
14528
                global_tier1_enum!(TextAlign, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8);
536
14528
                global_tier1_enum!(WhiteSpace, WHITE_SPACE_SHIFT, WHITE_SPACE_MASK, style_white_space_to_u8);
537
14528
                global_tier1_enum!(Direction, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8);
538
14528
                global_tier1_enum!(VerticalAlign, VERTICAL_ALIGN_SHIFT, VERTICAL_ALIGN_MASK, style_vertical_align_to_u8);
539
14528
                global_tier1_enum!(BorderCollapse, BORDER_COLLAPSE_SHIFT, BORDER_COLLAPSE_MASK, border_collapse_to_u8);
540

            
541
                // Tier 2 dims
542
14528
                match prop {
543
1766
                    CssProperty::PaddingTop(v) => { global_dims.padding_top = encode_css_pixel_as_i16(v); }
544
1765
                    CssProperty::PaddingRight(v) => { global_dims.padding_right = encode_css_pixel_as_i16(v); }
545
1765
                    CssProperty::PaddingBottom(v) => { global_dims.padding_bottom = encode_css_pixel_as_i16(v); }
546
1765
                    CssProperty::PaddingLeft(v) => { global_dims.padding_left = encode_css_pixel_as_i16(v); }
547
1831
                    CssProperty::MarginTop(v) => { global_dims.margin_top = encode_margin_i16(v); }
548
1831
                    CssProperty::MarginRight(v) => { global_dims.margin_right = encode_margin_i16(v); }
549
1831
                    CssProperty::MarginBottom(v) => { global_dims.margin_bottom = encode_margin_i16(v); }
550
1831
                    CssProperty::MarginLeft(v) => { global_dims.margin_left = encode_margin_i16(v); }
551
                    CssProperty::Width(v) => { global_dims.width = encode_layout_width(v); }
552
                    CssProperty::Height(v) => { global_dims.height = encode_layout_height(v); }
553
                    CssProperty::FontSize(v) => { global_dims.font_size = encode_pixel_prop(v); }
554
                    CssProperty::BorderTopWidth(v) => { global_dims.border_top_width = encode_css_pixel_as_i16(v); }
555
                    CssProperty::BorderRightWidth(v) => { global_dims.border_right_width = encode_css_pixel_as_i16(v); }
556
                    CssProperty::BorderBottomWidth(v) => { global_dims.border_bottom_width = encode_css_pixel_as_i16(v); }
557
                    CssProperty::BorderLeftWidth(v) => { global_dims.border_left_width = encode_css_pixel_as_i16(v); }
558
143
                    _ => {}
559
                }
560
            }
561

            
562
1873
            if global_tier1 != 0 {
563
132
                global_tier1 |= TIER1_POPULATED_BIT;
564
1741
            }
565
35360
        }
566

            
567
        // Helper: push debug message if debug_messages is Some
568
        macro_rules! cascade_debug {
569
            ($($arg:tt)*) => {
570
                if let Some(ref mut msgs) = debug_messages {
571
                    msgs.push(azul_css::LayoutDebugMessage::css_getter(format!($($arg)*)));
572
                }
573
            };
574
        }
575

            
576
934124
        for i in 0..node_count {
577
934124
            let node_id = NodeId::new(i);
578
934124
            let nd = &node_data[i];
579

            
580
            // Step 0: Apply UA CSS defaults first (lowest priority).
581
            // Then global `*` rules override UA (higher priority).
582
            // Then per-node CSS (Step 3) overrides both.
583
            //
584
            // CSS cascade priority: UA < author `*` < author specific < inline
585

            
586
            // Step 1: Inherit from parent's COMPACT values (not computed_values)
587
            // Parent index is always < i in pre-order arena, so already computed.
588
            //
589
            // Step 1: Inherit ONLY inheritable CSS properties from parent.
590
            // Non-inheritable fields (display, position, float, overflow, box-sizing,
591
            // flex-*, clear, vertical-align, writing-mode) stay at 0 (CSS initial value).
592
            // They get set by UA CSS (Step 2) and author CSS (Step 3).
593
934124
            let parent_id = node_hierarchy[i].parent_id();
594
934124
            if let Some(pid) = parent_id {
595
896959
                let pi = pid.index();
596

            
597
                // AUDIT: inheritance assumes a PRE-ORDER arena, i.e. a node's
598
                // parent is always stored at a lower index (`pi < i`) and has
599
                // therefore already been fully cascaded. A forward reference
600
                // (`pi >= i`) would silently inherit that parent's still-default
601
                // (all-zero) values, and an out-of-bounds `pi >= node_count`
602
                // would panic. Guard against both: assert the pre-order
603
                // invariant in debug builds, and skip inheritance (treat the
604
                // node as a root) for any malformed reference in release builds.
605
896959
                debug_assert!(
606
                    pi < i,
607
                    "compact cascade: non-pre-order arena — node {i}'s parent {pi} \
608
                     is not stored before it; inheritance would read default values",
609
                );
610
896959
                if pi < i {
611
896959
                // Copy only inheritable tier1 fields from parent
612
896959
                result.tier1_enums[i] = result.tier1_enums[pi] & INHERITABLE_TIER1_MASK;
613
896959

            
614
896959
                // Inheritable tier2: font_size
615
896959
                result.tier2_dims[i].font_size = result.tier2_dims[pi].font_size;
616
896959

            
617
896959
                // Inheritable tier2_cold: border_spacing, tab_size
618
896959
                result.tier2_cold[i].border_spacing_h = result.tier2_cold[pi].border_spacing_h;
619
896959
                result.tier2_cold[i].border_spacing_v = result.tier2_cold[pi].border_spacing_v;
620
896959
                result.tier2_cold[i].tab_size = result.tier2_cold[pi].tab_size;
621
896959

            
622
896959
                // Inheritable tier2b: all text properties
623
896959
                result.tier2b_text[i] = result.tier2b_text[pi];
624
896959
                }
625
37165
            }
626

            
627
            {
628
934124
                let d = &result.tier2_dims[i];
629
934124
                cascade_debug!("node[{}] {:?} after-inherit: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={} w={} h={}",
630
                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
631
                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right, d.width, d.height);
632
            }
633

            
634
            // Step 2: Apply UA CSS defaults for this node type directly to compact values.
635
            // UA defaults have lowest cascade priority — overridden by author CSS below.
636
934124
            apply_ua_css_to_compact(
637
934124
                &nd.node_type,
638
934124
                &mut result.tier1_enums[i],
639
934124
                &mut result.tier2_dims[i],
640
934124
                &mut result.tier2_cold[i],
641
934124
                &mut result.tier2b_text[i],
642
934124
                &mut result.font_hash_to_families,
643
            );
644

            
645
            {
646
934124
                let d = &result.tier2_dims[i];
647
934124
                cascade_debug!("node[{}] {:?} after-UA: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
648
                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
649
                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
650
            }
651

            
652
            // Step 2.5: Apply global `*` author CSS (overrides UA, overridden by specific rules)
653
            // Apply each `*` rule property individually (not bulk-assign) so we only
654
            // override properties the `*` rule actually set, preserving UA CSS for others.
655
            //
656
            // Per CSS spec, `*` matches all ELEMENTS. Text nodes are not elements —
657
            // they must only inherit from their parent. Without this check, `* { color: #666 }`
658
            // would overwrite the inherited `color: red` on a Text child of `<p>`,
659
            // even though `<p>` correctly got red from `p { color: red }`.
660
934124
            if !nd.is_text_node() {
661
710456
                for prop in &self.global_css_props {
662
76650
                    // (flag already accumulated in the has_global pre-pass)
663
76650
                    apply_css_property_to_compact(
664
76650
                        prop,
665
76650
                        &mut result.tier1_enums[i],
666
76650
                        &mut result.tier2_dims[i],
667
76650
                        &mut result.tier2_cold[i],
668
76650
                        &mut result.tier2b_text[i],
669
76650
                        &mut result.font_hash_to_families,
670
76650
                    );
671
76650
                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
672
76650
                }
673
300318
            }
674

            
675
            {
676
934124
                let d = &result.tier2_dims[i];
677
934124
                cascade_debug!("node[{}] {:?} after-global-star: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
678
                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
679
                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
680
934124
                let n_props = self.css_props.get_slice(i).len();
681
934124
                let n_inline = nd.style.iter_inline_properties().count();
682
934124
                cascade_debug!("node[{}] css_props={} entries, inline={} entries", i, n_props, n_inline);
683
939824
                for prop in self.css_props.get_slice(i) {
684
350057
                    cascade_debug!("node[{}]   css_prop: state={:?} type={:?}", i, prop.state, prop.prop_type);
685
                }
686
            }
687

            
688
            // Step 3: Apply this node's CSS properties directly to compact values.
689
            // Per-node author CSS has higher specificity than global `*`.
690

            
691
            // Scan css_props (stylesheet rules, sorted by (state, prop_type))
692
            // Typically 5-15 entries per node. Only Normal state matters for layout.
693
939824
            for prop in self.css_props.get_slice(i) {
694
350057
                if prop.state != azul_css::dynamic_selector::PseudoStateType::Normal { continue; }
695
349196
                result.uses_viewport_units |= css_property_uses_viewport_units(&prop.property);
696
349196
                apply_css_property_to_compact(
697
349196
                    &prop.property,
698
349196
                    &mut result.tier1_enums[i],
699
349196
                    &mut result.tier2_dims[i],
700
349196
                    &mut result.tier2_cold[i],
701
349196
                    &mut result.tier2b_text[i],
702
349196
                    &mut result.font_hash_to_families,
703
                );
704
349196
                update_dom_declared_flags(&prop.property, &mut result.dom_declared_flags);
705
            }
706

            
707
            {
708
934124
                let d = &result.tier2_dims[i];
709
934124
                cascade_debug!("node[{}] {:?} after-css-props: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
710
                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
711
                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
712
            }
713

            
714
            // Scan inline CSS (node_data.style — typically 0-3 properties).
715
            // Inline CSS has highest specificity — applied last to override stylesheet.
716
5094610
            for (prop, conds) in nd.style.iter_inline_properties() {
717
                // Apply when the conditions hold for the RESTING state:
718
                // pseudo-state conditions must be Normal, and every other
719
                // condition (viewport/@media, theme, OS...) is evaluated
720
                // against the window's dynamic context — the same rule
721
                // get_property_slow applies, so the fast path and the slow
722
                // path cannot disagree about a conditional property. A
723
                // non-pseudo condition also flags the cache, so the window
724
                // knows a context change requires a rebuild.
725
5082106
                let is_normal = conds.as_slice().is_empty()
726
346374
                    || conds.as_slice().iter().all(|c| match c {
727
342744
                        azul_css::dynamic_selector::DynamicSelector::PseudoState(s) => {
728
342744
                            *s == azul_css::dynamic_selector::PseudoStateType::Normal
729
                        }
730
3630
                        non_pseudo => {
731
3630
                            result.has_dynamic_conditions = true;
732
                            // Harvest the thresholds this condition can flip
733
                            // at — the resize decision regenerates when the
734
                            // window crosses one (dedup/sort happens once,
735
                            // after the node loop).
736
3630
                            {
737
3630
                                let mut w = Vec::new();
738
3630
                                let mut h = Vec::new();
739
3630
                                azul_css::dynamic_selector::collect_viewport_thresholds(
740
3630
                                    core::slice::from_ref(non_pseudo),
741
3630
                                    &mut w,
742
3630
                                    &mut h,
743
3630
                                );
744
3630
                                result
745
3630
                                    .inline_viewport_w
746
3630
                                    .extend(w.into_iter().map(f32::to_bits));
747
3630
                                result
748
3630
                                    .inline_viewport_h
749
3630
                                    .extend(h.into_iter().map(f32::to_bits));
750
3630
                            }
751
3630
                            self.dynamic_context
752
3630
                                .as_deref()
753
3630
                                .is_some_and(|ctx| non_pseudo.matches(ctx))
754
                        }
755
346374
                    });
756
5082106
                if !is_normal { continue; }
757
4736194
                result.uses_viewport_units |= css_property_uses_viewport_units(prop);
758
                // Layout-critical props dispatched via single-variant `if let` (direct discriminant
759
                // COMPARES, no indirect jump). apply_css_property_to_compact's ~100-arm `match` lowers
760
                // to a jump table that remill mis-lifts (never reaches the right arm) — same class as the
761
                // CssProperty::clone bug. With the conversion-clone fix the prop discriminant is now
762
                // correct, so these compares match and apply the value; everything else falls back.
763
                // (CssProperty is imported at module top.)
764
4736194
                if let CssProperty::Width(v) = prop {
765
109610
                    result.tier2_dims[i].width = encode_layout_width(v);
766
4626587
                } else if let CssProperty::Height(v) = prop {
767
134233
                    result.tier2_dims[i].height = encode_layout_height(v);
768
4492351
                } else if let CssProperty::FlexGrow(v) = prop {
769
367917
                    if let Some(e) = v.get_property() {
770
367917
                        result.tier2_dims[i].flex_grow = encode_flex_u16(e.inner.get());
771
367917
                    }
772
4124434
                } else if let CssProperty::Display(v) = prop {
773
235873
                    if let Some(e) = v.get_property() {
774
235873
                        let enc = u64::from(layout_display_to_u8(*e));
775
235873
                        let m = DISPLAY_MASK;
776
235873
                        let s = DISPLAY_SHIFT;
777
235873
                        result.tier1_enums[i] = (result.tier1_enums[i] & !(m << s)) | ((enc & m) << s);
778
235873
                    }
779
3888561
                } else {
780
3888561
                    apply_css_property_to_compact(
781
3888561
                        prop,
782
3888561
                        &mut result.tier1_enums[i],
783
3888561
                        &mut result.tier2_dims[i],
784
3888561
                        &mut result.tier2_cold[i],
785
3888561
                        &mut result.tier2b_text[i],
786
3888561
                        &mut result.font_hash_to_families,
787
3888561
                    );
788
3888561
                }
789
4736194
                update_dom_declared_flags(prop, &mut result.dom_declared_flags);
790
            }
791

            
792
            // Step 4b: user-overridden properties (runtime patches via
793
            // `set_css_property` / `restyle_user_property`). The resolver
794
            // consults this layer FIRST, so the compact cache must apply it
795
            // LAST — the cache is a projection of the same cascade and the
796
            // two must agree. Without this step a rebuilt cache resurrected
797
            // the pre-patch value: `restyle_user_property` rebuilds the cache
798
            // right after recording the override, and the layout fast path
799
            // then read the stale display/geometry the patch had just
800
            // changed. Same dispatch shape as the inline loop above (the
801
            // single-variant `if let`s exist for the remill lift, see there).
802
934124
            if let Some(user_props) = self.user_overridden_properties.get(i) {
803
1802
                for (_, prop) in user_props {
804
203
                    result.uses_viewport_units |= css_property_uses_viewport_units(prop);
805
203
                    if let CssProperty::Width(v) = prop {
806
126
                        result.tier2_dims[i].width = encode_layout_width(v);
807
137
                    } else if let CssProperty::Height(v) = prop {
808
                        result.tier2_dims[i].height = encode_layout_height(v);
809
77
                    } else if let CssProperty::FlexGrow(v) = prop {
810
                        if let Some(e) = v.get_property() {
811
                            result.tier2_dims[i].flex_grow = encode_flex_u16(e.inner.get());
812
                        }
813
77
                    } else if let CssProperty::Display(v) = prop {
814
13
                        if let Some(e) = v.get_property() {
815
13
                            let enc = u64::from(layout_display_to_u8(*e));
816
13
                            let m = DISPLAY_MASK;
817
13
                            let s = DISPLAY_SHIFT;
818
13
                            result.tier1_enums[i] =
819
13
                                (result.tier1_enums[i] & !(m << s)) | ((enc & m) << s);
820
13
                        }
821
64
                    } else {
822
64
                        apply_css_property_to_compact(
823
64
                            prop,
824
64
                            &mut result.tier1_enums[i],
825
64
                            &mut result.tier2_dims[i],
826
64
                            &mut result.tier2_cold[i],
827
64
                            &mut result.tier2b_text[i],
828
64
                            &mut result.font_hash_to_families,
829
64
                        );
830
64
                    }
831
203
                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
832
                }
833
932525
            }
834

            
835
            // Resolve font-size from em/percent/pt/etc. to px.
836
            // CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
837
            // Pre-order traversal guarantees parent's font_size is already resolved.
838
934124
            resolve_font_size_to_px(
839
934124
                &mut result.tier2_dims,
840
934124
                i,
841
934124
                parent_id,
842
            );
843

            
844
            // Set populated bit
845
934124
            if result.tier1_enums[i] != 0 {
846
652569
                result.tier1_enums[i] |= TIER1_POPULATED_BIT;
847
657572
            }
848
        }
849

            
850
        // Font dirty tracking.
851
        // When prev_font_hashes is empty (first build for this DOM), mark ALL
852
        // text nodes dirty to force font resolution. Without this, a DOM with
853
        // no explicit font-family (all hashes 0) would compare 0==0 and skip
854
        // resolution, even though font-weight/font-style may differ from the
855
        // cached chains of a previous DOM.
856
37233
        result.font_dirty_nodes.clear();
857
37233
        let first_build = prev_font_hashes.is_empty();
858
934124
        for i in 0..node_count {
859
934124
            let new_hash = result.tier2b_text[i].font_family_hash;
860
934124
            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
861
934124
            if first_build || new_hash != old_hash {
862
686626
                result.font_dirty_nodes.push(i);
863
687583
            }
864
        }
865
37233
        result.prev_font_hashes = result.tier2b_text.iter().map(|t| t.font_family_hash).collect();
866

            
867
        // Normalize the harvested viewport thresholds once (pushed raw per
868
        // node above): sorted + deduped by bit pattern.
869
37233
        result.inline_viewport_w.sort_unstable();
870
37233
        result.inline_viewport_w.dedup();
871
37233
        result.inline_viewport_h.sort_unstable();
872
37233
        result.inline_viewport_h.dedup();
873

            
874
37233
        result
875
37233
    }
876
}
877

            
878
// =============================================================================
879
// Helpers extracted from build_compact_cache_with_inheritance_debug
880
// =============================================================================
881

            
882
/// Apply UA CSS defaults for a node type directly to compact values.
883
/// UA defaults have lowest cascade priority — overridden by author CSS.
884
934141
fn apply_ua_css_to_compact(
885
934141
    node_type: &crate::dom::NodeType,
886
934141
    tier1: &mut u64,
887
934141
    dims: &mut CompactNodeProps,
888
934141
    cold: &mut CompactNodePropsCold,
889
934141
    text: &mut CompactTextProps,
890
934141
    font_hash_map: &mut alloc::collections::BTreeMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
891
934141
) {
892
    use azul_css::props::property::CssPropertyType as PT2;
893
    const UA_PROPERTY_TYPES: &[PT2] = &[
894
        // Tier1 enum properties
895
        PT2::Display, PT2::Position, PT2::Float, PT2::Clear,
896
        PT2::OverflowX, PT2::OverflowY, PT2::BoxSizing,
897
        PT2::FlexDirection, PT2::FlexWrap, PT2::JustifyContent,
898
        PT2::AlignItems, PT2::AlignContent, PT2::WritingMode,
899
        PT2::FontWeight, PT2::FontStyle, PT2::TextAlign,
900
        PT2::Visibility, PT2::WhiteSpace, PT2::Direction,
901
        PT2::VerticalAlign, PT2::BorderCollapse,
902
        // Tier2 dimension properties
903
        PT2::Width, PT2::Height, PT2::FontSize,
904
        PT2::MarginTop, PT2::MarginBottom, PT2::MarginLeft, PT2::MarginRight,
905
        PT2::PaddingTop, PT2::PaddingBottom, PT2::PaddingLeft, PT2::PaddingRight,
906
        PT2::BorderTopWidth, PT2::BorderTopStyle, PT2::BorderTopColor,
907
        PT2::BorderRightWidth, PT2::BorderRightStyle, PT2::BorderRightColor,
908
        PT2::BorderBottomWidth, PT2::BorderBottomStyle, PT2::BorderBottomColor,
909
        PT2::BorderLeftWidth, PT2::BorderLeftStyle, PT2::BorderLeftColor,
910
        // Text properties
911
        PT2::TextColor, PT2::LineHeight, PT2::LetterSpacing, PT2::WordSpacing,
912
        PT2::TextDecoration, PT2::Cursor, PT2::ListStyleType,
913
        // Counters: the UA sheet resets `list-item` on <ol>/<ul> so each list
914
        // restarts numbering. Without these here the has_counter fast-path bit
915
        // stays unset for list containers, compute_counters skips the reset, and
916
        // the list-item counter runs globally (a <ul> then <ol> numbered 1,2 then
917
        // 3,4 instead of restarting at 1).
918
        PT2::CounterReset, PT2::CounterIncrement,
919
    ];
920
50443614
    for pt in UA_PROPERTY_TYPES {
921
49509473
        if let Some(ua_prop) = crate::ua_css::get_ua_property(node_type, *pt) {
922
2365224
            apply_css_property_to_compact(ua_prop, tier1, dims, cold, text, font_hash_map);
923
47144249
        }
924
    }
925
934141
}
926

            
927
/// Resolve a node's font-size from relative units (em, %, rem, pt) to absolute px.
928
/// CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
929
/// Pre-order traversal guarantees parent's `font_size` is already resolved.
930
934136
fn resolve_font_size_to_px(
931
934136
    tier2_dims: &mut [CompactNodeProps],
932
934136
    node_idx: usize,
933
934136
    parent_id: Option<NodeId>,
934
934136
) {
935
934136
    let raw_fs = tier2_dims[node_idx].font_size;
936
934136
    if raw_fs == U32_SENTINEL || raw_fs >= U32_SENTINEL_THRESHOLD {
937
214621
        return;
938
719515
    }
939
719515
    let pv = match decode_pixel_value_u32(raw_fs) {
940
719515
        Some(pv) if pv.metric != SizeMetric::Px => pv,
941
719171
        _ => return,
942
    };
943

            
944
    // AUDIT: pre-order arena assumed — the parent's font-size is already
945
    // resolved to px only when `pid < node_idx`. Use checked `get` so an
946
    // out-of-bounds parent ref cannot panic, and require `pid < node_idx` so a
947
    // forward reference falls back to the 16px CSS initial value instead of
948
    // reading an unresolved (still em/%) parent value.
949
344
    let parent_font_size_px = parent_id
950
344
        .map_or(16.0, |pid| {
951
292
            let pi = pid.index();
952
292
            debug_assert!(
953
                pi < node_idx,
954
                "compact font-size resolve: non-pre-order arena — node {node_idx}'s \
955
                 parent {pi} font-size is not yet resolved",
956
            );
957
292
            if pi < node_idx {
958
292
                tier2_dims
959
292
                    .get(pi)
960
292
                    .and_then(|p| decode_pixel_value_u32(p.font_size))
961
292
                    .map_or(16.0, |ppv| ppv.number.get())
962
            } else {
963
                16.0
964
            }
965
292
        });
966

            
967
344
    let resolved_px = match pv.metric {
968
291
        SizeMetric::Em => pv.number.get() * parent_font_size_px,
969
39
        SizeMetric::Percent => pv.number.get() / 100.0 * parent_font_size_px,
970
        SizeMetric::Rem => {
971
            // rem = the ROOT element's font size. For the root itself that is circular,
972
            // so CSS resolves root rem against the 16px INITIAL value (Selectors/Values:
973
            // "when specified on the root element, rem refers to the initial value").
974
            // tier2_dims[0] IS the root's slot, but while resolving the root it still
975
            // holds the root's own unresolved raw rem — so `html { font-size: 2rem }`
976
            // computed 2*2 = 4px instead of 2*16 = 32px.
977
2
            let rem_base = if parent_id.is_none() {
978
1
                16.0
979
            } else {
980
1
                tier2_dims
981
1
                    .first()
982
1
                    .and_then(|r| decode_pixel_value_u32(r.font_size))
983
1
                    .map_or(16.0, |rpv| rpv.number.get())
984
            };
985
2
            rem_base * pv.number.get()
986
        }
987
1
        SizeMetric::Pt => pv.number.get() * 96.0 / 72.0,
988
11
        _ => pv.number.get(),
989
    };
990
344
    tier2_dims[node_idx].font_size =
991
344
        encode_pixel_value_u32(&azul_css::props::basic::pixel::PixelValue::px(resolved_px));
992
934136
}
993

            
994
/// Does this property's value use a viewport-relative unit (vw/vh/vmin/vmax)?
995
///
996
/// Feeds `CompactLayoutCache::uses_viewport_units` from the property loops of
997
/// `build_compact_cache_with_inheritance` — one call per (node, property), on
998
/// data the loops are already iterating. See that field's docs for what the
999
/// flag buys (solver3 skips per-resize invalidation of every inline collection
/// for the overwhelming majority of documents that never mention a viewport
/// unit).
///
/// Coverage = the pixel-carrying properties the compact cache itself encodes,
/// which is a superset of what inline collection/measurement reads (the only
/// consumer). `calc()` widths/heights are flagged CONSERVATIVELY without
/// walking the AST — a false positive merely keeps the old always-invalidate
/// behaviour.
5100121
fn css_property_uses_viewport_units(prop: &CssProperty) -> bool {
    use azul_css::props::basic::length::SizeMetric;
    use azul_css::props::basic::pixel::PixelValue;
1745428
    const fn pv(p: &PixelValue) -> bool {
1745428
        matches!(p.metric, SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax)
1745428
    }
1460134
    fn inner<T: HasInnerPixelValue>(v: &CssPropertyValue<T>) -> bool {
1460013
        matches!(v, CssPropertyValue::Exact(x) if pv(&x.get_inner_pixel()))
1460134
    }
    use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
    use azul_css::props::layout::flex::LayoutFlexBasis;
5100121
    match prop {
122458
        CssProperty::Width(v) => matches!(v, CssPropertyValue::Exact(w) if match w {
122415
            LayoutWidth::Px(p) | LayoutWidth::FitContent(p) => pv(p),
19
            LayoutWidth::Calc(_) => true,
            _ => false,
19
        }),
151351
        CssProperty::Height(v) => matches!(v, CssPropertyValue::Exact(h) if match h {
151351
            LayoutHeight::Px(p) | LayoutHeight::FitContent(p) => pv(p),
            LayoutHeight::Calc(_) => true,
            _ => false,
        }),
11660
        CssProperty::FlexBasis(v) => matches!(v, CssPropertyValue::Exact(LayoutFlexBasis::Exact(p)) if pv(p)),
33041
        CssProperty::MinWidth(v) => inner(v),
877
        CssProperty::MaxWidth(v) => inner(v),
3036
        CssProperty::MinHeight(v) => inner(v),
539
        CssProperty::MaxHeight(v) => inner(v),
222668
        CssProperty::FontSize(v) => inner(v),
168056
        CssProperty::PaddingTop(v) => inner(v),
138960
        CssProperty::PaddingRight(v) => inner(v),
134714
        CssProperty::PaddingBottom(v) => inner(v),
139034
        CssProperty::PaddingLeft(v) => inner(v),
40705
        CssProperty::MarginTop(v) => inner(v),
17803
        CssProperty::MarginRight(v) => inner(v),
69163
        CssProperty::MarginBottom(v) => inner(v),
49307
        CssProperty::MarginLeft(v) => inner(v),
98378
        CssProperty::BorderTopWidth(v) => inner(v),
110148
        CssProperty::BorderRightWidth(v) => inner(v),
124833
        CssProperty::BorderBottomWidth(v) => inner(v),
87268
        CssProperty::BorderLeftWidth(v) => inner(v),
10035
        CssProperty::Top(v) => inner(v),
998
        CssProperty::Right(v) => inner(v),
690
        CssProperty::Bottom(v) => inner(v),
9826
        CssProperty::Left(v) => inner(v),
33
        CssProperty::LetterSpacing(v) => inner(v),
11
        CssProperty::WordSpacing(v) => inner(v),
11
        CssProperty::TextIndent(v) => inner(v),
        CssProperty::TabSize(v) => inner(v),
3354518
        _ => false,
    }
5100121
}
// =============================================================================
// Direct CssProperty → compact field writer
// =============================================================================
/// Apply a single `CssProperty` directly to the compact representation.
/// Called once per property per node — replaces the old 56+ getter approach.
#[inline]
// The scrollbar-* and counter-* arms have identical bodies
// (`if v.get_property().is_some() { flags |= … }`) but each variant wraps a
// DIFFERENT value type (StyleBackgroundContentValue, LayoutScrollbarWidthValue,
// StyleScrollbarColorValue, CounterResetValue, CounterIncrementValue, …), so an
// or-pattern binding `v` cannot be expressed across them.
#[allow(clippy::match_same_arms)]
// fixed-point encoders: z-index / line-height are range-checked before the
// narrowing cast, and opacity is clamped to [0,1] then scaled to [0,254] (u8).
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
6679741
fn apply_css_property_to_compact(
6679741
    prop: &CssProperty,
6679741
    tier1: &mut u64,
6679741
    dims: &mut CompactNodeProps,
6679741
    cold: &mut CompactNodePropsCold,
6679741
    text: &mut CompactTextProps,
6679741
    font_hash_map: &mut alloc::collections::BTreeMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
6679741
) {
    macro_rules! set_tier1 {
        ($v:expr, $shift:expr, $mask:expr, $encoder:ident) => {
            if let Some(exact) = $v.get_property() {
                let encoded = u64::from($encoder(*exact));
                let shifted_mask = $mask << $shift;
                *tier1 = (*tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
            }
        };
    }
6679741
    match prop {
        // Tier 1 enums
984164
        CssProperty::Display(v) => set_tier1!(v, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8),
20109
        CssProperty::Position(v) => set_tier1!(v, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8),
429
        CssProperty::Float(v) => set_tier1!(v, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8),
13461
        CssProperty::OverflowX(v) => set_tier1!(v, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8),
13841
        CssProperty::OverflowY(v) => set_tier1!(v, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8),
        // +spec:overflow:17654b - overflow-block / overflow-inline resolve to
        // the physical axis through the writing mode. Application is in
        // declaration order (a later physical declaration overwrites the
        // same tier1 slot and vice versa), which is exactly CSS's
        // equal-specificity last-wins rule for logical/physical pairs. The
        // writing mode is read from tier1 AT THIS POINT: the inherited value
        // is already present (inheritance runs first), so only the exotic
        // "writing-mode declared AFTER a logical overflow on the SAME node"
        // ordering maps against the pre-declaration mode.
        CssProperty::OverflowBlock(v) => {
            if let Some(val) = v.get_property() {
                let wm_bits = ((*tier1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8;
                let vertical = wm_bits == layout_writing_mode_to_u8(
                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl,
                ) || wm_bits == layout_writing_mode_to_u8(
                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr,
                );
                let shift = if vertical { OVERFLOW_X_SHIFT } else { OVERFLOW_Y_SHIFT };
                let enc = u64::from(layout_overflow_to_u8(*val));
                *tier1 = (*tier1 & !(OVERFLOW_MASK << shift)) | ((enc & OVERFLOW_MASK) << shift);
            }
        }
11
        CssProperty::OverflowInline(v) => {
11
            if let Some(val) = v.get_property() {
11
                let wm_bits = ((*tier1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8;
11
                let vertical = wm_bits == layout_writing_mode_to_u8(
11
                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl,
11
                ) || wm_bits == layout_writing_mode_to_u8(
11
                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr,
11
                );
11
                let shift = if vertical { OVERFLOW_Y_SHIFT } else { OVERFLOW_X_SHIFT };
11
                let enc = u64::from(layout_overflow_to_u8(*val));
11
                *tier1 = (*tier1 & !(OVERFLOW_MASK << shift)) | ((enc & OVERFLOW_MASK) << shift);
            }
        }
123519
        CssProperty::BoxSizing(v) => set_tier1!(v, BOX_SIZING_SHIFT, BOX_SIZING_MASK, layout_box_sizing_to_u8),
226932
        CssProperty::FlexDirection(v) => set_tier1!(v, FLEX_DIRECTION_SHIFT, FLEX_DIR_MASK, layout_flex_direction_to_u8),
2923
        CssProperty::FlexWrap(v) => set_tier1!(v, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8),
65216
        CssProperty::JustifyContent(v) => set_tier1!(v, JUSTIFY_CONTENT_SHIFT, JUSTIFY_MASK, layout_justify_content_to_u8),
185457
        CssProperty::AlignItems(v) => set_tier1!(v, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8),
        CssProperty::AlignContent(v) => set_tier1!(v, ALIGN_CONTENT_SHIFT, ALIGN_MASK, layout_align_content_to_u8),
33
        CssProperty::WritingMode(v) => set_tier1!(v, WRITING_MODE_SHIFT, WRITING_MODE_MASK, layout_writing_mode_to_u8),
77
        CssProperty::Clear(v) => set_tier1!(v, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8),
1301
        CssProperty::FontWeight(v) => set_tier1!(v, FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, style_font_weight_to_u8),
22
        CssProperty::FontStyle(v) => set_tier1!(v, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8),
80190
        CssProperty::TextAlign(v) => set_tier1!(v, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8),
19
        CssProperty::Visibility(v) => set_tier1!(v, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8),
3311
        CssProperty::WhiteSpace(v) => set_tier1!(v, WHITE_SPACE_SHIFT, WHITE_SPACE_MASK, style_white_space_to_u8),
33
        CssProperty::Direction(v) => set_tier1!(v, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8),
1705
        CssProperty::VerticalAlign(v) => set_tier1!(v, VERTICAL_ALIGN_SHIFT, VERTICAL_ALIGN_MASK, style_vertical_align_to_u8),
45
        CssProperty::BorderCollapse(v) => set_tier1!(v, BORDER_COLLAPSE_SHIFT, BORDER_COLLAPSE_MASK, border_collapse_to_u8),
6391
        CssProperty::AlignSelf(v) => set_tier1!(v, ALIGN_SELF_SHIFT, ALIGN_SELF_MASK, layout_align_self_to_u8),
        CssProperty::JustifySelf(v) => set_tier1!(v, JUSTIFY_SELF_SHIFT, JUSTIFY_SELF_MASK, layout_justify_self_to_u8),
        CssProperty::GridAutoFlow(v) => set_tier1!(v, GRID_AUTO_FLOW_SHIFT, GRID_AUTO_FLOW_MASK, layout_grid_auto_flow_to_u8),
        CssProperty::JustifyItems(v) => set_tier1!(v, JUSTIFY_ITEMS_SHIFT, JUSTIFY_ITEMS_MASK, layout_justify_items_to_u8),
        // Tier 2 dimensions
12723
        CssProperty::Width(v) => { dims.width = encode_layout_width(v); }
17118
        CssProperty::Height(v) => { dims.height = encode_layout_height(v); }
33041
        CssProperty::MinWidth(v) => { dims.min_width = encode_pixel_prop(v); }
877
        CssProperty::MaxWidth(v) => { dims.max_width = encode_pixel_prop(v); }
3036
        CssProperty::MinHeight(v) => { dims.min_height = encode_pixel_prop(v); }
539
        CssProperty::MaxHeight(v) => { dims.max_height = encode_pixel_prop(v); }
11660
        CssProperty::FlexBasis(v) => { dims.flex_basis = encode_flex_basis(v); }
259800
        CssProperty::FontSize(v) => { dims.font_size = encode_pixel_prop(v); }
212520
        CssProperty::PaddingTop(v) => { dims.padding_top = encode_css_pixel_as_i16(v); }
183424
        CssProperty::PaddingRight(v) => { dims.padding_right = encode_css_pixel_as_i16(v); }
179178
        CssProperty::PaddingBottom(v) => { dims.padding_bottom = encode_css_pixel_as_i16(v); }
184180
        CssProperty::PaddingLeft(v) => { dims.padding_left = encode_css_pixel_as_i16(v); }
263512
        CssProperty::MarginTop(v) => { dims.margin_top = encode_margin_i16(v); }
44306
        CssProperty::MarginRight(v) => { dims.margin_right = encode_margin_i16(v); }
291970
        CssProperty::MarginBottom(v) => { dims.margin_bottom = encode_margin_i16(v); }
75810
        CssProperty::MarginLeft(v) => { dims.margin_left = encode_margin_i16(v); }
134524
        CssProperty::BorderTopWidth(v) => { dims.border_top_width = encode_css_pixel_as_i16(v); }
146294
        CssProperty::BorderRightWidth(v) => { dims.border_right_width = encode_css_pixel_as_i16(v); }
160979
        CssProperty::BorderBottomWidth(v) => { dims.border_bottom_width = encode_css_pixel_as_i16(v); }
123414
        CssProperty::BorderLeftWidth(v) => { dims.border_left_width = encode_css_pixel_as_i16(v); }
10035
        CssProperty::Top(v) => { dims.top = encode_css_pixel_as_i16(v); }
998
        CssProperty::Right(v) => { dims.right = encode_css_pixel_as_i16(v); }
690
        CssProperty::Bottom(v) => { dims.bottom = encode_css_pixel_as_i16(v); }
9826
        CssProperty::Left(v) => { dims.left = encode_css_pixel_as_i16(v); }
1417
        CssProperty::FlexGrow(v) => {
1417
            if let Some(exact) = v.get_property() {
1417
                dims.flex_grow = encode_flex_u16(exact.inner.get());
1417
            }
        }
128338
        CssProperty::FlexShrink(v) => {
128338
            if let Some(exact) = v.get_property() {
128338
                dims.flex_shrink = encode_flex_u16(exact.inner.get());
128338
            }
        }
11
        CssProperty::RowGap(v) => {
11
            if let Some(g) = v.get_property() {
11
                if g.inner.metric == SizeMetric::Px {
11
                    dims.row_gap = encode_resolved_px_i16(g.inner.number.get());
11
                }
            }
        }
11
        CssProperty::ColumnGap(v) => {
11
            if let Some(g) = v.get_property() {
11
                if g.inner.metric == SizeMetric::Px {
11
                    dims.column_gap = encode_resolved_px_i16(g.inner.number.get());
11
                }
            }
        }
2
        CssProperty::Gap(v) => {
2
            if let Some(g) = v.get_property() {
2
                if g.inner.metric == SizeMetric::Px {
1
                    let enc = encode_resolved_px_i16(g.inner.number.get());
1
                    dims.row_gap = enc;
1
                    dims.column_gap = enc;
1
                }
            }
        }
        // Grid placement (compact encoding for common Auto/Line cases)
56
        CssProperty::GridColumn(v) => {
56
            if let Some(gp) = v.get_property() {
56
                cold.grid_col_start = encode_grid_line(&gp.grid_start);
56
                cold.grid_col_end = encode_grid_line(&gp.grid_end);
56
            }
        }
55
        CssProperty::GridRow(v) => {
55
            if let Some(gp) = v.get_property() {
55
                cold.grid_row_start = encode_grid_line(&gp.grid_start);
55
                cold.grid_row_end = encode_grid_line(&gp.grid_end);
55
            }
        }
        // Tier 2 cold
128
        CssProperty::ZIndex(v) => {
128
            if let Some(exact) = v.get_property() {
117
                match exact {
1
                    LayoutZIndex::Auto => cold.z_index = I16_AUTO,
116
                    LayoutZIndex::Integer(z) => {
                        // Two-sided (see the tier2_cold path above): a large negative z
                        // used to wrap positive via `*z as i16`. Escape both ends.
116
                        cold.z_index = if *z >= -32768 && *z < i32::from(I16_SENTINEL_THRESHOLD) {
76
                            *z as i16
                        } else {
40
                            I16_SENTINEL
                        };
                    }
                }
11
            }
        }
125847
        CssProperty::BorderTopStyle(v) => {
125847
            if let Some(exact) = v.get_property() {
125781
                let bs = u16::from(border_style_to_u8(exact.inner));
125781
                cold.border_styles_packed = (cold.border_styles_packed & !0x000F) | bs;
125781
            }
        }
137615
        CssProperty::BorderRightStyle(v) => {
137615
            if let Some(exact) = v.get_property() {
137604
                let bs = u16::from(border_style_to_u8(exact.inner));
137604
                cold.border_styles_packed = (cold.border_styles_packed & !0x00F0) | (bs << 4);
137604
            }
        }
152300
        CssProperty::BorderBottomStyle(v) => {
152300
            if let Some(exact) = v.get_property() {
152289
                let bs = u16::from(border_style_to_u8(exact.inner));
152289
                cold.border_styles_packed = (cold.border_styles_packed & !0x0F00) | (bs << 8);
152289
            }
        }
114735
        CssProperty::BorderLeftStyle(v) => {
114735
            if let Some(exact) = v.get_property() {
114724
                let bs = u16::from(border_style_to_u8(exact.inner));
114724
                cold.border_styles_packed = (cold.border_styles_packed & !0xF000) | (bs << 12);
114724
            }
        }
131609
        CssProperty::BorderTopColor(v) => {
131609
            if let Some(c) = v.get_property() { cold.border_top_color = encode_color_u32(&c.inner); }
        }
167975
        CssProperty::BorderRightColor(v) => {
167975
            if let Some(c) = v.get_property() { cold.border_right_color = encode_color_u32(&c.inner); }
        }
160968
        CssProperty::BorderBottomColor(v) => {
160968
            if let Some(c) = v.get_property() { cold.border_bottom_color = encode_color_u32(&c.inner); }
        }
120499
        CssProperty::BorderLeftColor(v) => {
120499
            if let Some(c) = v.get_property() { cold.border_left_color = encode_color_u32(&c.inner); }
        }
11
        CssProperty::BorderSpacing(v) => {
11
            if let Some(spacing) = v.get_property() {
11
                if spacing.horizontal.metric == SizeMetric::Px {
11
                    cold.border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
11
                }
11
                if spacing.vertical.metric == SizeMetric::Px {
11
                    cold.border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
11
                }
            }
        }
        CssProperty::TabSize(v) => { cold.tab_size = encode_css_pixel_as_i16(v); }
        // Tier 2b text
218017
        CssProperty::TextColor(v) => {
218017
            if let Some(color) = v.get_property() {
218017
                let c = &color.inner;
218017
                text.text_color = (u32::from(c.r) << 24) | (u32::from(c.g) << 16) | (u32::from(c.b) << 8) | u32::from(c.a);
218017
            }
        }
14816
        CssProperty::FontFamily(v) => {
14816
            if let Some(families) = v.get_property() {
14816
                let mut hasher = DefaultHasher::new();
14816
                families.hash(&mut hasher);
14816
                let h = hasher.finish();
14816
                let h = if h == 0 { 1 } else { h };
14816
                text.font_family_hash = h;
14816
                font_hash_map.insert(h, families.clone());
            }
        }
790
        CssProperty::LineHeight(v) => {
790
            if let Some(lh) = v.get_property() {
                // Split scale by SIGN (see the builder's line-height pre-pass
                // and compact_cache.rs field doc): negative normalized =
                // absolute px, stored as -px x 10; positive = multiple,
                // stored x 1000. A single x1000 scale overflowed i16 for any
                // absolute line-height above 32.76px and silently became
                // "normal" via the sentinel.
790
                let n = lh.inner.normalized();
790
                let stored = if n < 0.0 {
175
                    ((n * 10.0).round() as i32).max(-32768)
                } else {
615
                    (n * 1000.0).round() as i32
                };
790
                if stored >= -32768 && stored < i32::from(I16_SENTINEL_THRESHOLD) {
789
                    text.line_height = stored as i16;
789
                } else {
1
                    text.line_height = I16_SENTINEL;
1
                }
            }
        }
33
        CssProperty::LetterSpacing(v) => { text.letter_spacing = encode_css_pixel_as_i16(v); }
11
        CssProperty::WordSpacing(v) => { text.word_spacing = encode_css_pixel_as_i16(v); }
11
        CssProperty::TextIndent(v) => { text.text_indent = encode_css_pixel_as_i16(v); }
        // Border radii (cold): encode px × 10 into i16; sentinel stays = unset/0
47421
        CssProperty::BorderTopLeftRadius(v) => {
47421
            if let Some(exact) = v.get_property() {
47421
                if exact.inner.metric == SizeMetric::Px {
47410
                    cold.border_top_left_radius = encode_resolved_px_i16(exact.inner.number.get());
47410
                }
            }
        }
47421
        CssProperty::BorderTopRightRadius(v) => {
47421
            if let Some(exact) = v.get_property() {
47421
                if exact.inner.metric == SizeMetric::Px {
47410
                    cold.border_top_right_radius = encode_resolved_px_i16(exact.inner.number.get());
47410
                }
            }
        }
47762
        CssProperty::BorderBottomLeftRadius(v) => {
47762
            if let Some(exact) = v.get_property() {
47762
                if exact.inner.metric == SizeMetric::Px {
47751
                    cold.border_bottom_left_radius = encode_resolved_px_i16(exact.inner.number.get());
47751
                }
            }
        }
47762
        CssProperty::BorderBottomRightRadius(v) => {
47762
            if let Some(exact) = v.get_property() {
47762
                if exact.inner.metric == SizeMetric::Px {
47751
                    cold.border_bottom_right_radius = encode_resolved_px_i16(exact.inner.number.get());
47751
                }
            }
        }
        // Opacity: encode as 0-254, 255 = sentinel (unset/default = 1.0)
9784
        CssProperty::Opacity(v) => {
9784
            if let Some(exact) = v.get_property() {
9784
                let o = exact.inner.normalized().clamp(0.0, 1.0);
9784
                let byte = (o * 254.0).round() as u8;
9784
                // byte is in [0, 254], never collides with OPACITY_SENTINEL=255
9784
                cold.opacity = byte;
9784
            }
        }
        // has-flags: set bit whenever property is set (regardless of value).
        // Getter uses this as a fast "is the default" bail-out.
585
        CssProperty::Transform(v) => {
585
            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM; }
        }
        CssProperty::TransformOrigin(v) => {
            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM_ORIGIN; }
        }
        // All four shadow sides wrap the same StyleBoxShadowValue and set the
        // single has-box-shadow bit.
495
        CssProperty::BoxShadowTop(v)
495
        | CssProperty::BoxShadowBottom(v)
495
        | CssProperty::BoxShadowLeft(v)
495
        | CssProperty::BoxShadowRight(v) => {
1980
            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW; }
        }
816
        CssProperty::TextDecoration(v) => {
816
            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TEXT_DECORATION; }
        }
3
        CssProperty::ScrollbarGutter(v) => {
3
            if let Some(exact) = v.get_property() {
                use azul_css::props::layout::overflow::StyleScrollbarGutter;
2
                let bits: u8 = match exact {
1
                    StyleScrollbarGutter::Auto => SCROLLBAR_GUTTER_AUTO,
1
                    StyleScrollbarGutter::Stable => SCROLLBAR_GUTTER_STABLE,
                    StyleScrollbarGutter::StableBothEdges => SCROLLBAR_GUTTER_BOTH_EDGES,
                };
2
                cold.hot_flags = (cold.hot_flags & !HOT_FLAG_SCROLLBAR_GUTTER_MASK)
2
                    | ((bits << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT) & HOT_FLAG_SCROLLBAR_GUTTER_MASK);
1
            }
        }
172648
        CssProperty::BackgroundContent(v) => {
172648
            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BACKGROUND; }
        }
        CssProperty::ClipPath(v) => {
            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_CLIP_PATH; }
        }
        // Any scrollbar customisation sets the single `has_any_scrollbar_css`
        // bit. When unset, get_scrollbar_style can bail to UA defaults without
        // doing 8 cascade walks.
        CssProperty::ScrollbarTrack(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarThumb(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarButton(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarCorner(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarWidth(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarColor(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarVisibility(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarFadeDelay(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        CssProperty::ScrollbarFadeDuration(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
        }
        // Rare paint/layout props with dedicated fast-path bits.
682
        CssProperty::CounterReset(v) => {
682
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER; }
        }
        CssProperty::CounterIncrement(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER; }
        }
        // Both break-before/after wrap PageBreakValue and set the has-break bit.
11
        CssProperty::BreakBefore(v) | CssProperty::BreakAfter(v) => {
11
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BREAK; }
        }
        CssProperty::TextOrientation(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_ORIENTATION; }
        }
        CssProperty::TextShadow(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_SHADOW; }
        }
        CssProperty::BackdropFilter(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BACKDROP_FILTER; }
        }
11
        CssProperty::Filter(v) => {
11
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_FILTER; }
        }
        CssProperty::MixBlendMode(v) => {
            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_MIX_BLEND_MODE; }
        }
        // Non-compact properties (background, etc.) — handled by get_property_slow fallback
737957
        _ => {}
    }
6679741
}
/// OR the DOM-level declared-flag for rarely-set text properties. Called once
/// per property per node so that when a flag bit is clear, callers
/// (e.g. `translate_to_text3_constraints`) can skip the cascade walk and use
/// the default value — the slow walk would never find a declaration anyway.
5162257
const fn update_dom_declared_flags(prop: &CssProperty, flags: &mut u32) {
    // Only mark if the property value is actually "set" (not Auto/Initial/etc.).
    // Using `get_property().is_some()` mirrors the pattern used elsewhere in
    // this builder for has-X bits.
5162257
    match prop {
        CssProperty::ShapeInside(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_INSIDE; }
        CssProperty::ShapeOutside(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_OUTSIDE; }
        CssProperty::TextJustify(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_JUSTIFY; }
17
        CssProperty::TextIndent(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_INDENT; }
        CssProperty::ColumnCount(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_COUNT; }
11
        CssProperty::ColumnGap(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_GAP; }
        CssProperty::ColumnWidth(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_WIDTH; }
        CssProperty::InitialLetter(v) => if v.get_property().is_some() { *flags |= DOM_HAS_INITIAL_LETTER; }
        CssProperty::InitialLetterAlign(v) => if v.get_property().is_some() { *flags |= DOM_HAS_INITIAL_LETTER_ALIGN; }
        CssProperty::LineClamp(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_CLAMP; }
        CssProperty::HangingPunctuation(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HANGING_PUNCTUATION; }
        CssProperty::TextCombineUpright(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_COMBINE_UPRIGHT; }
        CssProperty::ExclusionMargin(v) => if v.get_property().is_some() { *flags |= DOM_HAS_EXCLUSION_MARGIN; }
        CssProperty::ShapeMargin(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_MARGIN; }
        CssProperty::HyphenationLanguage(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HYPHENATION_LANGUAGE; }
        CssProperty::UnicodeBidi(v) => if v.get_property().is_some() { *flags |= DOM_HAS_UNICODE_BIDI; }
22
        CssProperty::TextBoxTrim(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_BOX_TRIM; }
        CssProperty::Hyphens(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HYPHENS; }
        CssProperty::WordBreak(v) => if v.get_property().is_some() { *flags |= DOM_HAS_WORD_BREAK; }
11
        CssProperty::OverflowWrap(v) => if v.get_property().is_some() { *flags |= DOM_HAS_OVERFLOW_WRAP; }
        CssProperty::LineBreak(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_BREAK; }
        CssProperty::TextAlignLast(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_ALIGN_LAST; }
791
        CssProperty::LineHeight(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_HEIGHT; }
5161405
        _ => {}
    }
5162257
}
// =============================================================================
// Helper encoders for dimension properties
// =============================================================================
/// Encode a `GridLine` into i16: `Auto=I16_AUTO`, Line(n)=n, Span(n)=-(n).
/// Named lines fall back to `I16_SENTINEL` (not compact-encodable).
// const fn: the `n as i16` casts are guarded by explicit +/-32000 range checks.
#[allow(clippy::cast_possible_truncation)]
251
const fn encode_grid_line(line: &azul_css::props::layout::grid::GridLine) -> i16 {
    use azul_css::props::layout::grid::GridLine;
251
    match line {
1
        GridLine::Auto => I16_AUTO,
17
        GridLine::Line(n) => {
17
            if *n >= -32000 && *n <= 32000 { *n as i16 } else { I16_SENTINEL }
        }
12
        GridLine::Span(n) => {
12
            if *n >= 1 && *n <= 32000 { -(*n as i16) } else { I16_SENTINEL }
        }
221
        GridLine::Named(_) => I16_SENTINEL,
    }
251
}
/// Encode a `CssPropertyValue`<LayoutWidth> into u32 compact form.
273847
fn encode_layout_width<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
273847
    match val {
273813
        CssPropertyValue::Exact(w) => w.encode_compact_u32(),
24
        CssPropertyValue::Auto => U32_AUTO,
1
        CssPropertyValue::Initial => U32_INITIAL,
1
        CssPropertyValue::Inherit => U32_INHERIT,
2
        CssPropertyValue::None => U32_NONE,
6
        _ => U32_SENTINEL,
    }
273847
}
/// Encode a `CssPropertyValue`<LayoutHeight> into u32 compact form.
151359
fn encode_layout_height<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
151359
    encode_layout_width(val)
151359
}
/// Trait for types that can be encoded as compact u32 dimension values.
/// Implemented for `LayoutWidth`, `LayoutHeight` (which are Auto|Px|MinContent|MaxContent|Calc enums).
trait LayoutWidthLike {
    fn encode_compact_u32(&self) -> u32;
}
impl LayoutWidthLike for LayoutWidth {
122462
    fn encode_compact_u32(&self) -> u32 {
122462
        match self {
3
            Self::Auto => U32_AUTO,
122433
            Self::Px(pv) => encode_pixel_value_u32(pv),
3
            Self::MinContent => U32_MIN_CONTENT,
3
            Self::MaxContent => U32_MAX_CONTENT,
            // FitContent/Calc are not compact-encodable → overflow to tier 3.
20
            Self::FitContent(_) | Self::Calc(_) => U32_SENTINEL,
        }
122462
    }
}
impl LayoutWidthLike for LayoutHeight {
151351
    fn encode_compact_u32(&self) -> u32 {
151351
        match self {
            Self::Auto => U32_AUTO,
151351
            Self::Px(pv) => encode_pixel_value_u32(pv),
            Self::MinContent => U32_MIN_CONTENT,
            Self::MaxContent => U32_MAX_CONTENT,
            // FitContent/Calc are not compact-encodable → overflow to tier 3.
            Self::FitContent(_) | Self::Calc(_) => U32_SENTINEL,
        }
151351
    }
}
/// Encode a `CssPropertyValue` wrapping a simple `PixelValue` struct (`LayoutMinWidth`, etc.)
297308
fn encode_pixel_prop<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> u32 {
297308
    match val {
297302
        CssPropertyValue::Exact(inner) => encode_pixel_value_u32(&inner.get_inner_pixel()),
1
        CssPropertyValue::Auto => U32_AUTO,
1
        CssPropertyValue::Initial => U32_INITIAL,
1
        CssPropertyValue::Inherit => U32_INHERIT,
1
        CssPropertyValue::None => U32_NONE,
2
        _ => U32_SENTINEL,
    }
297308
}
/// Trait for dimension structs wrapping `inner: PixelValue`.
trait HasInnerPixelValue {
    fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue;
}
macro_rules! impl_has_inner_pixel {
    ($($ty:ty),*) => {
        $(
            impl HasInnerPixelValue for $ty {
3793329
                fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue {
3793329
                    self.inner
3793329
                }
            }
        )*
    };
}
impl_has_inner_pixel!(
    azul_css::props::layout::dimensions::LayoutMinWidth,
    azul_css::props::layout::dimensions::LayoutMaxWidth,
    azul_css::props::layout::dimensions::LayoutMinHeight,
    azul_css::props::layout::dimensions::LayoutMaxHeight,
    azul_css::props::basic::font::StyleFontSize,
    azul_css::props::layout::spacing::LayoutPaddingTop,
    azul_css::props::layout::spacing::LayoutPaddingRight,
    azul_css::props::layout::spacing::LayoutPaddingBottom,
    azul_css::props::layout::spacing::LayoutPaddingLeft,
    azul_css::props::layout::spacing::LayoutMarginTop,
    azul_css::props::layout::spacing::LayoutMarginRight,
    azul_css::props::layout::spacing::LayoutMarginBottom,
    azul_css::props::layout::spacing::LayoutMarginLeft,
    azul_css::props::style::border::LayoutBorderTopWidth,
    azul_css::props::style::border::LayoutBorderRightWidth,
    azul_css::props::style::border::LayoutBorderBottomWidth,
    azul_css::props::style::border::LayoutBorderLeftWidth,
    azul_css::props::layout::position::LayoutTop,
    azul_css::props::layout::position::LayoutRight,
    azul_css::props::layout::position::LayoutInsetBottom,
    azul_css::props::layout::position::LayoutLeft,
    azul_css::props::style::text::StyleLetterSpacing,
    azul_css::props::style::text::StyleWordSpacing,
    azul_css::props::style::text::StyleTextIndent,
    azul_css::props::style::text::StyleTabSize
);
/// Encode a `CssPropertyValue`<T> where T wraps a `PixelValue`, as i16 (×10 resolved px).
/// Delegates to the canonical `azul_css::compact_cache::encode_css_pixel_as_i16`.
2036142
fn encode_css_pixel_as_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
2036142
    let mapped = match val {
2036014
        CssPropertyValue::Exact(inner) => CssPropertyValue::Exact(inner.get_inner_pixel()),
68
        CssPropertyValue::Auto => CssPropertyValue::Auto,
1
        CssPropertyValue::Initial => CssPropertyValue::Initial,
1
        CssPropertyValue::Inherit => CssPropertyValue::Inherit,
56
        CssPropertyValue::None => CssPropertyValue::None,
2
        _ => return I16_SENTINEL,
    };
2036140
    azul_css::compact_cache::encode_css_pixel_as_i16(&mapped)
2036142
}
/// Encode margin: same as `encode_css_pixel_as_i16` but Auto is a distinct value.
682929
fn encode_margin_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
682929
    encode_css_pixel_as_i16(val)
682929
}
/// Encode `CssPropertyValue`<LayoutFlexBasis> — `LayoutFlexBasis` is Auto | Exact(PixelValue).
11670
fn encode_flex_basis(val: &CssPropertyValue<LayoutFlexBasis>) -> u32 {
11670
    match val {
11653
        CssPropertyValue::Exact(fb) => match fb {
1
            LayoutFlexBasis::Auto => U32_AUTO,
11652
            LayoutFlexBasis::Exact(pv) => encode_pixel_value_u32(pv),
        },
12
        CssPropertyValue::Auto => U32_AUTO,
1
        CssPropertyValue::Initial => U32_INITIAL,
1
        CssPropertyValue::Inherit => U32_INHERIT,
1
        CssPropertyValue::None => U32_NONE,
2
        _ => U32_SENTINEL,
    }
11670
}
#[cfg(test)]
mod audit_tests {
    use super::resolve_font_size_to_px;
    use crate::dom::NodeId;
    use azul_css::compact_cache::{
        decode_pixel_value_u32, encode_pixel_value_u32, CompactNodeProps,
    };
    use azul_css::props::basic::pixel::PixelValue;
    // Happy path: an `em` font-size resolves against a valid (pre-order) parent.
    #[test]
1
    fn resolve_font_size_em_from_parent() {
1
        let mut dims = vec![CompactNodeProps::default(); 2];
1
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
1
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(2.0));
1
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
1
        let pv = decode_pixel_value_u32(dims[1].font_size).unwrap();
1
        assert!((pv.number.get() - 40.0).abs() < 0.01, "got {}", pv.number.get());
1
    }
    // Root `em` (no parent) uses the 16px CSS initial value.
    #[test]
1
    fn resolve_font_size_root_em_uses_default() {
1
        let mut dims = vec![CompactNodeProps::default()];
1
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::em(2.0));
1
        resolve_font_size_to_px(&mut dims, 0, None);
1
        let pv = decode_pixel_value_u32(dims[0].font_size).unwrap();
1
        assert!((pv.number.get() - 32.0).abs() < 0.01, "got {}", pv.number.get());
1
    }
    // A `rem` value reads the root (index 0) via the `.first()` guard without
    // panicking (previously indexed `tier2_dims[0]` directly).
    #[test]
1
    fn resolve_font_size_rem_reads_root() {
1
        let mut dims = vec![CompactNodeProps::default(); 2];
1
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(10.0)); // root
1
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::rem(3.0)); // child rem
1
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
1
        let pv = decode_pixel_value_u32(dims[1].font_size).unwrap();
1
        assert!((pv.number.get() - 30.0).abs() < 0.01, "got {}", pv.number.get());
1
    }
}
// =============================================================================
// Adversarial unit tests (autotest)
//
// Inline module: the encoders below (`encode_grid_line`, `encode_layout_width`,
// `encode_pixel_prop`, `encode_css_pixel_as_i16`, `encode_margin_i16`,
// `encode_flex_basis`, `apply_css_property_to_compact`, `apply_ua_css_to_compact`,
// `update_dom_declared_flags`, `resolve_font_size_to_px`) are all private, so they
// can only be exercised from inside this module.
//
// Focus: overflow / saturation / sentinel-aliasing / round-trip fidelity, i.e. the
// places where a fixed-point codec silently turns one CSS value into a different
// one instead of panicking.
// =============================================================================
#[cfg(test)]
#[allow(
    clippy::float_cmp,
    clippy::unreadable_literal,
    clippy::too_many_lines,
    clippy::cast_lossless
)]
mod autotest_generated {
    use super::*;
    use alloc::collections::BTreeMap;
    use crate::dom::NodeType;
    use crate::styled_dom::NodeHierarchyItem;
    use azul_css::props::basic::color::ColorU;
    use azul_css::props::basic::font::{StyleFontFamily, StyleFontFamilyVec};
    use azul_css::props::basic::length::{FloatValue, PercentageValue};
    use azul_css::props::basic::pixel::PixelValue;
    use azul_css::props::layout::dimensions::LayoutMinWidth;
    use azul_css::props::layout::display::LayoutDisplay;
    use azul_css::props::layout::flex::{LayoutFlexGrow, LayoutFlexShrink};
    use azul_css::props::layout::grid::{GridLine, GridPlacement, LayoutGap, NamedGridLine};
    use azul_css::props::layout::overflow::StyleScrollbarGutter;
    use azul_css::props::layout::position::LayoutPosition;
    use azul_css::props::layout::spacing::{LayoutMarginTop, LayoutPaddingTop};
    use azul_css::props::layout::table::StyleBorderCollapse;
    use azul_css::props::style::border::{BorderStyle, StyleBorderTopStyle};
    use azul_css::props::style::effects::StyleOpacity;
    use azul_css::props::style::text::{
        StyleLineHeight, StyleTextColor, StyleTextDecoration, StyleTextIndent,
    };
    // -------------------------------------------------------------------------
    // Fixtures
    // -------------------------------------------------------------------------
    /// The four compact output slots + the font reverse-map, as one value, so a
    /// test can snapshot "everything the writer could have touched".
    struct Sink {
        tier1: u64,
        dims: CompactNodeProps,
        cold: CompactNodePropsCold,
        text: CompactTextProps,
        fonts: BTreeMap<u64, StyleFontFamilyVec>,
    }
    impl Sink {
        fn new() -> Self {
            Self {
                tier1: 0,
                dims: CompactNodeProps::default(),
                cold: CompactNodePropsCold::default(),
                text: CompactTextProps::default(),
                fonts: BTreeMap::new(),
            }
        }
        fn apply(&mut self, prop: &CssProperty) {
            apply_css_property_to_compact(
                prop,
                &mut self.tier1,
                &mut self.dims,
                &mut self.cold,
                &mut self.text,
                &mut self.fonts,
            );
        }
        fn ua(&mut self, node_type: &NodeType) {
            apply_ua_css_to_compact(
                node_type,
                &mut self.tier1,
                &mut self.dims,
                &mut self.cold,
                &mut self.text,
                &mut self.fonts,
            );
        }
        fn snapshot(&self) -> (u64, CompactNodeProps, CompactNodePropsCold, CompactTextProps) {
            (self.tier1, self.dims, self.cold, self.text)
        }
    }
    fn div_nodes(n: usize) -> Vec<NodeData> {
        (0..n).map(|_| NodeData::create_node(NodeType::Div)).collect()
    }
    /// Pre-order chain: node 0 is the root, node `i` is the child of node `i-1`.
    /// `NodeHierarchyItem` uses 1-based encoding (0 = None, n = `NodeId(n-1)`).
    fn linear_hierarchy(n: usize) -> Vec<NodeHierarchyItem> {
        (0..n)
            .map(|i| NodeHierarchyItem {
                parent: i, // i == 0 -> None; i > 0 -> NodeId(i-1)
                previous_sibling: 0,
                next_sibling: 0,
                last_child: if i + 1 < n { i + 2 } else { 0 },
            })
            .collect()
    }
    fn padding(px: f32) -> CssPropertyValue<LayoutPaddingTop> {
        CssPropertyValue::Exact(LayoutPaddingTop { inner: PixelValue::px(px) })
    }
    // -------------------------------------------------------------------------
    // encode_grid_line
    // -------------------------------------------------------------------------
    #[test]
    fn grid_line_auto_and_named_map_to_their_sentinels() {
        assert_eq!(encode_grid_line(&GridLine::Auto), I16_AUTO);
        let named = GridLine::Named(NamedGridLine {
            grid_line_name: "sidebar".into(),
            span_count: 0,
        });
        assert_eq!(encode_grid_line(&named), I16_SENTINEL);
    }
    #[test]
    fn grid_line_number_boundaries_saturate_instead_of_truncating() {
        assert_eq!(encode_grid_line(&GridLine::Line(0)), 0);
        assert_eq!(encode_grid_line(&GridLine::Line(1)), 1);
        assert_eq!(encode_grid_line(&GridLine::Line(-1)), -1);
        assert_eq!(encode_grid_line(&GridLine::Line(32_000)), 32_000);
        assert_eq!(encode_grid_line(&GridLine::Line(-32_000)), -32_000);
        // One past the guarded range: must become the sentinel, never a wrapped i16.
        assert_eq!(encode_grid_line(&GridLine::Line(32_001)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Line(-32_001)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Line(i32::MAX)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Line(i32::MIN)), I16_SENTINEL);
    }
    #[test]
    fn grid_line_span_boundaries_and_nonsense_spans() {
        assert_eq!(encode_grid_line(&GridLine::Span(1)), -1);
        assert_eq!(encode_grid_line(&GridLine::Span(32_000)), -32_000);
        // `span 0` / negative spans are not representable -> sentinel, NOT 0 (which
        // would silently mean "grid line 0").
        assert_eq!(encode_grid_line(&GridLine::Span(0)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Span(-1)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Span(32_001)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Span(i32::MAX)), I16_SENTINEL);
        assert_eq!(encode_grid_line(&GridLine::Span(i32::MIN)), I16_SENTINEL);
    }
    #[test]
    fn grid_line_in_range_values_never_alias_the_sentinel_band() {
        // A real line number that lands on >= I16_SENTINEL_THRESHOLD would decode
        // as "auto" / "overflow" and move the item to a different grid cell.
        for n in [-32_000i32, -1_000, -1, 0, 1, 1_000, 32_000] {
            let e = encode_grid_line(&GridLine::Line(n));
            assert!(
                e < I16_SENTINEL_THRESHOLD,
                "Line({n}) encoded into the sentinel band as {e}"
            );
        }
        for n in [1i32, 2, 1_000, 32_000] {
            let e = encode_grid_line(&GridLine::Span(n));
            assert!(e < 0, "Span({n}) must encode as a negative value, got {e}");
            assert!(
                e < I16_SENTINEL_THRESHOLD,
                "Span({n}) encoded into the sentinel band as {e}"
            );
        }
    }
    // -------------------------------------------------------------------------
    // encode_layout_width / encode_layout_height
    // -------------------------------------------------------------------------
    #[test]
    fn layout_width_keywords_map_to_distinct_sentinels() {
        let auto: CssPropertyValue<LayoutWidth> = CssPropertyValue::Auto;
        let none: CssPropertyValue<LayoutWidth> = CssPropertyValue::None;
        let initial: CssPropertyValue<LayoutWidth> = CssPropertyValue::Initial;
        let inherit: CssPropertyValue<LayoutWidth> = CssPropertyValue::Inherit;
        assert_eq!(encode_layout_width(&auto), U32_AUTO);
        assert_eq!(encode_layout_width(&none), U32_NONE);
        assert_eq!(encode_layout_width(&initial), U32_INITIAL);
        assert_eq!(encode_layout_width(&inherit), U32_INHERIT);
    }
    #[test]
    fn layout_width_revert_and_unset_fall_back_to_the_overflow_sentinel() {
        // `revert` / `unset` have no compact slot. They must land on U32_SENTINEL
        // (= "ask the slow path"), never on a *semantic* sentinel like AUTO.
        let revert: CssPropertyValue<LayoutWidth> = CssPropertyValue::Revert;
        let unset: CssPropertyValue<LayoutWidth> = CssPropertyValue::Unset;
        assert_eq!(encode_layout_width(&revert), U32_SENTINEL);
        assert_eq!(encode_layout_width(&unset), U32_SENTINEL);
        assert_eq!(encode_layout_height(&revert), U32_SENTINEL);
        assert_eq!(encode_layout_height(&unset), U32_SENTINEL);
    }
    #[test]
    fn layout_width_exact_keyword_variants() {
        assert_eq!(
            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Auto)),
            U32_AUTO
        );
        assert_eq!(
            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::MinContent)),
            U32_MIN_CONTENT
        );
        assert_eq!(
            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::MaxContent)),
            U32_MAX_CONTENT
        );
        // fit-content() is not compact-encodable -> tier 3
        assert_eq!(
            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::FitContent(
                PixelValue::px(10.0)
            ))),
            U32_SENTINEL
        );
    }
    #[test]
    fn layout_width_px_round_trips() {
        for px in [0.0f32, 0.5, 1.0, 100.0, 1234.567, -50.0] {
            let enc =
                encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(px))));
            let dec = decode_pixel_value_u32(enc)
                .expect("an in-range px value must not encode to a sentinel");
            assert_eq!(dec.metric, SizeMetric::Px);
            assert!(
                (dec.number.get() - px).abs() < 0.002,
                "round-trip of {px}px produced {}px",
                dec.number.get()
            );
        }
    }
    #[test]
    fn layout_width_extreme_values_saturate_to_the_overflow_sentinel() {
        // Past the 28-bit fixed-point range the encoder must bail to tier 3 rather
        // than wrapping the low bits into a small (and plausible-looking) width.
        for px in [
            1.0e9f32,
            -1.0e9,
            f32::MAX,
            f32::MIN,
            f32::INFINITY,
            f32::NEG_INFINITY,
        ] {
            let enc =
                encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(px))));
            assert_eq!(
                enc, U32_SENTINEL,
                "width {px}px should overflow to U32_SENTINEL, got {enc:#x}"
            );
        }
    }
    #[test]
    fn layout_width_nan_degrades_to_zero_without_panicking() {
        // `NaN as isize` saturates to 0, so a NaN width becomes 0px — deterministic
        // and finite, which is what the layout solver needs.
        let enc = encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(
            f32::NAN,
        ))));
        let dec = decode_pixel_value_u32(enc).expect("NaN must degrade to a value, not a sentinel");
        assert!(dec.number.get().is_finite());
        assert_eq!(dec.number.get(), 0.0);
    }
    #[test]
    fn layout_height_never_diverges_from_layout_width() {
        let vals = [
            CssPropertyValue::Exact(LayoutWidth::Auto),
            CssPropertyValue::Exact(LayoutWidth::MinContent),
            CssPropertyValue::Exact(LayoutWidth::MaxContent),
            CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(42.0))),
            CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(1.0e9))),
            CssPropertyValue::Unset,
        ];
        for v in &vals {
            assert_eq!(encode_layout_width(v), encode_layout_height(v));
        }
    }
    // -------------------------------------------------------------------------
    // encode_pixel_prop
    // -------------------------------------------------------------------------
    #[test]
    fn pixel_prop_keywords_map_to_distinct_sentinels() {
        let auto: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Auto;
        let none: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::None;
        let initial: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Initial;
        let inherit: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Inherit;
        let revert: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Revert;
        let unset: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Unset;
        assert_eq!(encode_pixel_prop(&auto), U32_AUTO);
        assert_eq!(encode_pixel_prop(&none), U32_NONE);
        assert_eq!(encode_pixel_prop(&initial), U32_INITIAL);
        assert_eq!(encode_pixel_prop(&inherit), U32_INHERIT);
        assert_eq!(encode_pixel_prop(&revert), U32_SENTINEL);
        assert_eq!(encode_pixel_prop(&unset), U32_SENTINEL);
    }
    #[test]
    fn pixel_prop_round_trips_value_and_metric() {
        for pv in [
            PixelValue::px(50.0),
            PixelValue::em(1.5),
            PixelValue::percent(80.0),
            PixelValue::pt(12.0),
            PixelValue::rem(2.0),
        ] {
            let enc = encode_pixel_prop(&CssPropertyValue::Exact(LayoutMinWidth { inner: pv }));
            let dec = decode_pixel_value_u32(enc).expect("must round-trip");
            assert_eq!(dec.metric, pv.metric, "metric lost in the round-trip");
            assert!(
                (dec.number.get() - pv.number.get()).abs() < 0.002,
                "value lost in the round-trip: {} -> {}",
                pv.number.get(),
                dec.number.get()
            );
        }
    }
    #[test]
    fn pixel_prop_overflow_saturates() {
        let enc = encode_pixel_prop(&CssPropertyValue::Exact(LayoutMinWidth {
            inner: PixelValue::px(1.0e9),
        }));
        assert_eq!(enc, U32_SENTINEL);
    }
    #[test]
    fn pixel_prop_exact_value_never_aliases_a_semantic_sentinel() {
        // INVARIANT: an `Exact` length may overflow to U32_SENTINEL (= "slow path"),
        // but must never collide with a sentinel that means something *else*
        // (auto / none / inherit / initial / min-content / max-content) — that turns
        // a length into a different keyword with no way to tell.
        //
        // `encode_pixel_value_u32` packs `value << 4 | metric`. For the raw
        // fixed-point value -1 (i.e. -0.001) the value bits are 0xFFFF_FFF0, so any
        // metric whose code is >= 9 (vh = 9, vmin = 10, vmax = 11) ORs straight into
        // the sentinel band:
        //     -0.001vh   -> 0xFFFF_FFF9 == U32_MAX_CONTENT
        //     -0.001vmin -> 0xFFFF_FFFA == U32_MIN_CONTENT
        //     -0.001vmax -> 0xFFFF_FFFB == U32_INITIAL
        for metric in [SizeMetric::Vh, SizeMetric::Vmin, SizeMetric::Vmax] {
            let pv = PixelValue::from_metric(metric, -0.001);
            let enc = encode_pixel_prop(&CssPropertyValue::Exact(LayoutMinWidth { inner: pv }));
            assert!(
                enc == U32_SENTINEL || enc < U32_SENTINEL_THRESHOLD,
                "an Exact viewport length encoded to {enc:#x}, which aliases a semantic sentinel",
            );
        }
    }
    // -------------------------------------------------------------------------
    // encode_css_pixel_as_i16 / encode_margin_i16
    // -------------------------------------------------------------------------
    #[test]
    fn css_pixel_i16_scales_by_ten() {
        assert_eq!(encode_css_pixel_as_i16(&padding(0.0)), 0);
        assert_eq!(encode_css_pixel_as_i16(&padding(10.5)), 105);
        assert_eq!(encode_css_pixel_as_i16(&padding(-10.5)), -105);
    }
    #[test]
    fn css_pixel_i16_boundaries() {
        // 3276.3px is the largest representable value (one below the sentinel band)
        assert_eq!(encode_css_pixel_as_i16(&padding(3276.3)), 32_763);
        // one tick further must saturate, NOT alias I16_INITIAL (32764)
        assert_eq!(encode_css_pixel_as_i16(&padding(3276.4)), I16_SENTINEL);
        // and the negative end
        assert_eq!(encode_css_pixel_as_i16(&padding(-3276.8)), -32_768);
        assert_eq!(encode_css_pixel_as_i16(&padding(-3276.9)), I16_SENTINEL);
    }
    #[test]
    fn css_pixel_i16_non_px_units_need_the_slow_path() {
        let em = CssPropertyValue::Exact(LayoutPaddingTop { inner: PixelValue::em(2.0) });
        let pct = CssPropertyValue::Exact(LayoutPaddingTop {
            inner: PixelValue::percent(50.0),
        });
        assert_eq!(encode_css_pixel_as_i16(&em), I16_SENTINEL);
        assert_eq!(encode_css_pixel_as_i16(&pct), I16_SENTINEL);
    }
    #[test]
    fn css_pixel_i16_keywords_are_distinguishable() {
        let auto: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Auto;
        let initial: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Initial;
        let inherit: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Inherit;
        let none: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::None;
        let revert: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Revert;
        let unset: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Unset;
        assert_eq!(encode_css_pixel_as_i16(&auto), I16_AUTO);
        assert_eq!(encode_css_pixel_as_i16(&initial), I16_INITIAL);
        assert_eq!(encode_css_pixel_as_i16(&inherit), I16_INHERIT);
        // none / revert / unset have no dedicated slot -> generic sentinel
        assert_eq!(encode_css_pixel_as_i16(&none), I16_SENTINEL);
        assert_eq!(encode_css_pixel_as_i16(&revert), I16_SENTINEL);
        assert_eq!(encode_css_pixel_as_i16(&unset), I16_SENTINEL);
    }
    #[test]
    fn css_pixel_i16_nan_and_infinity_are_safe() {
        assert_eq!(encode_css_pixel_as_i16(&padding(f32::NAN)), 0);
        assert_eq!(encode_css_pixel_as_i16(&padding(f32::INFINITY)), I16_SENTINEL);
        assert_eq!(
            encode_css_pixel_as_i16(&padding(f32::NEG_INFINITY)),
            I16_SENTINEL
        );
        assert_eq!(encode_css_pixel_as_i16(&padding(f32::MAX)), I16_SENTINEL);
        assert_eq!(encode_css_pixel_as_i16(&padding(f32::MIN)), I16_SENTINEL);
    }
    #[test]
    fn css_pixel_i16_exact_value_never_aliases_a_keyword_sentinel() {
        // The i16 encoder range-checks *both* ends before narrowing, so — unlike the
        // u32 path — an Exact px value can never be mistaken for auto/inherit/initial.
        for px in [
            -3276.8f32, -100.0, -0.1, 0.0, 0.1, 100.0, 3276.3, 1.0e9, -1.0e9,
        ] {
            let e = encode_css_pixel_as_i16(&padding(px));
            assert!(
                e != I16_AUTO && e != I16_INHERIT && e != I16_INITIAL,
                "{px}px aliased a keyword sentinel ({e})"
            );
        }
    }
    #[test]
    fn margin_i16_keeps_auto_and_otherwise_matches_the_pixel_encoder() {
        let auto: CssPropertyValue<LayoutMarginTop> = CssPropertyValue::Auto;
        assert_eq!(encode_margin_i16(&auto), I16_AUTO);
        for px in [-50.0f32, 0.0, 12.5, 3276.3, 5.0e9, f32::NAN] {
            let m = CssPropertyValue::Exact(LayoutMarginTop { inner: PixelValue::px(px) });
            assert_eq!(encode_margin_i16(&m), encode_css_pixel_as_i16(&padding(px)));
        }
    }
    // -------------------------------------------------------------------------
    // encode_flex_basis
    // -------------------------------------------------------------------------
    #[test]
    fn flex_basis_all_variants() {
        assert_eq!(
            encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Auto)),
            U32_AUTO
        );
        let enc = encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Exact(
            PixelValue::px(120.0),
        )));
        let dec = decode_pixel_value_u32(enc).expect("px flex-basis must round-trip");
        assert!((dec.number.get() - 120.0).abs() < 0.002);
        assert_eq!(encode_flex_basis(&CssPropertyValue::Auto), U32_AUTO);
        assert_eq!(encode_flex_basis(&CssPropertyValue::None), U32_NONE);
        assert_eq!(encode_flex_basis(&CssPropertyValue::Initial), U32_INITIAL);
        assert_eq!(encode_flex_basis(&CssPropertyValue::Inherit), U32_INHERIT);
        assert_eq!(encode_flex_basis(&CssPropertyValue::Revert), U32_SENTINEL);
        assert_eq!(encode_flex_basis(&CssPropertyValue::Unset), U32_SENTINEL);
    }
    #[test]
    fn flex_basis_overflow_saturates() {
        assert_eq!(
            encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Exact(
                PixelValue::px(1.0e9)
            ))),
            U32_SENTINEL
        );
        assert_eq!(
            encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Exact(
                PixelValue::px(f32::INFINITY)
            ))),
            U32_SENTINEL
        );
    }
    // -------------------------------------------------------------------------
    // update_dom_declared_flags
    // -------------------------------------------------------------------------
    fn text_indent_prop() -> CssProperty {
        CssProperty::TextIndent(CssPropertyValue::Exact(StyleTextIndent::default()))
    }
    fn line_height_prop(pct: f32) -> CssProperty {
        CssProperty::LineHeight(CssPropertyValue::Exact(StyleLineHeight {
            inner: PercentageValue::new(pct),
        }))
    }
    #[test]
    fn dom_flags_set_the_right_bit_from_zero() {
        let mut flags = 0u32;
        update_dom_declared_flags(&text_indent_prop(), &mut flags);
        assert_eq!(flags, DOM_HAS_TEXT_INDENT);
        let mut flags2 = 0u32;
        update_dom_declared_flags(&line_height_prop(150.0), &mut flags2);
        assert_eq!(flags2, DOM_HAS_LINE_HEIGHT);
    }
    #[test]
    fn dom_flags_only_ever_or_never_clear() {
        // Starting from all-ones, the function must not clear a single bit.
        let mut flags = u32::MAX;
        update_dom_declared_flags(&text_indent_prop(), &mut flags);
        update_dom_declared_flags(&line_height_prop(150.0), &mut flags);
        update_dom_declared_flags(
            &CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::px(10.0))),
            &mut flags,
        );
        assert_eq!(flags, u32::MAX);
    }
    #[test]
    fn dom_flags_accumulate_and_are_idempotent() {
        let mut flags = 0u32;
        update_dom_declared_flags(&text_indent_prop(), &mut flags);
        update_dom_declared_flags(&line_height_prop(150.0), &mut flags);
        let after_two = flags;
        assert_eq!(after_two, DOM_HAS_TEXT_INDENT | DOM_HAS_LINE_HEIGHT);
        // re-applying the same properties must be a no-op
        update_dom_declared_flags(&text_indent_prop(), &mut flags);
        update_dom_declared_flags(&line_height_prop(150.0), &mut flags);
        assert_eq!(flags, after_two);
    }
    #[test]
    fn dom_flags_are_not_set_for_a_valueless_property() {
        // `line-height: initial` / `text-indent: auto` carry no Exact payload, so the
        // "declared" fast-path bit must stay clear (the slow walk would find nothing).
        let mut flags = 0u32;
        update_dom_declared_flags(&CssProperty::LineHeight(CssPropertyValue::Initial), &mut flags);
        update_dom_declared_flags(&CssProperty::TextIndent(CssPropertyValue::Auto), &mut flags);
        update_dom_declared_flags(&CssProperty::TextIndent(CssPropertyValue::Unset), &mut flags);
        assert_eq!(flags, 0);
    }
    #[test]
    fn dom_flags_ignore_unrelated_properties() {
        let mut flags = 0u32;
        update_dom_declared_flags(
            &CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::px(10.0))),
            &mut flags,
        );
        update_dom_declared_flags(
            &CssProperty::ZIndex(CssPropertyValue::Exact(LayoutZIndex::Integer(3))),
            &mut flags,
        );
        assert_eq!(flags, 0);
    }
    // -------------------------------------------------------------------------
    // apply_css_property_to_compact — tier 1 bitfield
    // -------------------------------------------------------------------------
    #[test]
    fn apply_tier1_fields_do_not_bleed_into_each_other() {
        let mut s = Sink::new();
        s.apply(&CssProperty::Display(CssPropertyValue::Exact(
            LayoutDisplay::InlineBlock,
        )));
        s.apply(&CssProperty::Position(CssPropertyValue::Exact(
            LayoutPosition::Absolute,
        )));
        // border-collapse lives at bit 52, i.e. at the far end of the bitfield
        s.apply(&CssProperty::BorderCollapse(CssPropertyValue::Exact(
            StyleBorderCollapse::Collapse,
        )));
        assert_eq!(
            (s.tier1 >> DISPLAY_SHIFT) & DISPLAY_MASK,
            u64::from(layout_display_to_u8(LayoutDisplay::InlineBlock))
        );
        assert_eq!(
            (s.tier1 >> POSITION_SHIFT) & POSITION_MASK,
            u64::from(layout_position_to_u8(LayoutPosition::Absolute))
        );
        assert_eq!(
            (s.tier1 >> BORDER_COLLAPSE_SHIFT) & BORDER_COLLAPSE_MASK,
            u64::from(border_collapse_to_u8(StyleBorderCollapse::Collapse))
        );
        let known = (DISPLAY_MASK << DISPLAY_SHIFT)
            | (POSITION_MASK << POSITION_SHIFT)
            | (BORDER_COLLAPSE_MASK << BORDER_COLLAPSE_SHIFT);
        assert_eq!(
            s.tier1 & !known,
            0,
            "tier1 = {:#x} has bits set outside the three fields that were written",
            s.tier1
        );
    }
    #[test]
    fn apply_tier1_overwrite_clears_only_its_own_field() {
        // Hostile starting state: every bit set. The clear-then-set in `set_tier1!`
        // must wipe exactly the display field and leave every neighbour intact.
        let mut s = Sink::new();
        s.tier1 = u64::MAX;
        s.apply(&CssProperty::Display(CssPropertyValue::Exact(
            LayoutDisplay::Block,
        )));
        assert_eq!(
            (s.tier1 >> DISPLAY_SHIFT) & DISPLAY_MASK,
            u64::from(layout_display_to_u8(LayoutDisplay::Block))
        );
        let others = !(DISPLAY_MASK << DISPLAY_SHIFT);
        assert_eq!(
            s.tier1 & others,
            u64::MAX & others,
            "neighbouring tier-1 fields were clobbered"
        );
    }
    #[test]
    fn apply_tier1_ignores_a_valueless_property() {
        let mut s = Sink::new();
        s.apply(&CssProperty::Display(CssPropertyValue::Inherit));
        assert_eq!(s.tier1, 0, "`display: inherit` has no Exact payload to encode");
    }
    // -------------------------------------------------------------------------
    // apply_css_property_to_compact — tier 2 dims
    // -------------------------------------------------------------------------
    #[test]
    fn apply_width_round_trips_and_touches_nothing_else() {
        let mut s = Sink::new();
        let before_cold = s.cold;
        let before_text = s.text;
        s.apply(&CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(
            PixelValue::px(320.0),
        ))));
        let dec = decode_pixel_value_u32(s.dims.width).expect("width must round-trip");
        assert!((dec.number.get() - 320.0).abs() < 0.002);
        assert_eq!(s.tier1, 0, "a tier-2 property must not touch the tier-1 bitfield");
        assert_eq!(s.cold, before_cold, "a tier-2 property must not touch tier-2 cold");
        assert_eq!(s.text, before_text, "a tier-2 property must not touch tier-2b text");
        assert!(s.fonts.is_empty());
    }
    #[test]
    fn apply_flex_grow_saturates_and_rejects_negatives() {
        let mut s = Sink::new();
        s.apply(&CssProperty::FlexGrow(CssPropertyValue::Exact(LayoutFlexGrow {
            inner: FloatValue::new(2.5),
        })));
        assert_eq!(s.dims.flex_grow, 250);
        // A negative flex-grow must not wrap around into a huge positive u16.
        let mut neg = Sink::new();
        neg.apply(&CssProperty::FlexGrow(CssPropertyValue::Exact(LayoutFlexGrow {
            inner: FloatValue::new(-1.0),
        })));
        assert_eq!(neg.dims.flex_grow, U16_SENTINEL);
        // ...and neither must an absurdly large one.
        let mut big = Sink::new();
        big.apply(&CssProperty::FlexGrow(CssPropertyValue::Exact(LayoutFlexGrow {
            inner: FloatValue::new(1.0e9),
        })));
        assert_eq!(big.dims.flex_grow, U16_SENTINEL);
        // NaN degrades to 0 rather than to a wrapped value.
        let mut nan = Sink::new();
        nan.apply(&CssProperty::FlexShrink(CssPropertyValue::Exact(
            LayoutFlexShrink { inner: FloatValue::new(f32::NAN) },
        )));
        assert_eq!(nan.dims.flex_shrink, 0);
    }
    #[test]
    fn apply_gap_px_sets_both_axes_and_ignores_unresolvable_units() {
        let mut s = Sink::new();
        s.apply(&CssProperty::Gap(CssPropertyValue::Exact(LayoutGap {
            inner: PixelValue::px(8.0),
        })));
        assert_eq!(s.dims.row_gap, 80);
        assert_eq!(s.dims.column_gap, 80);
        // An `em` gap cannot be resolved without a font context — it must be left
        // untouched (so the slow path can handle it), not silently encoded as 2px.
        let mut em = Sink::new();
        em.apply(&CssProperty::Gap(CssPropertyValue::Exact(LayoutGap {
            inner: PixelValue::em(2.0),
        })));
        assert_eq!(em.dims.row_gap, 0);
        assert_eq!(em.dims.column_gap, 0);
    }
    // -------------------------------------------------------------------------
    // apply_css_property_to_compact — tier 2 cold
    // -------------------------------------------------------------------------
    #[test]
    fn apply_z_index_auto_and_in_range_values() {
        let mut s = Sink::new();
        s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(LayoutZIndex::Auto)));
        assert_eq!(s.cold.z_index, I16_AUTO);
        s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
            LayoutZIndex::Integer(100),
        )));
        assert_eq!(s.cold.z_index, 100);
        // last value below the sentinel band
        s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
            LayoutZIndex::Integer(32_763),
        )));
        assert_eq!(s.cold.z_index, 32_763);
    }
    #[test]
    fn apply_z_index_large_positive_saturates() {
        for z in [32_764i32, 100_000, i32::MAX] {
            let mut s = Sink::new();
            s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
                LayoutZIndex::Integer(z),
            )));
            assert_eq!(s.cold.z_index, I16_SENTINEL, "z-index {z} should saturate");
        }
    }
    #[test]
    fn apply_z_index_large_negative_must_not_wrap_positive() {
        // The encoder range-checks only the UPPER bound:
        //     if *z >= I16_SENTINEL_THRESHOLD { I16_SENTINEL } else { *z as i16 }
        // so a large negative z-index truncates instead of saturating, e.g.
        //     z-index: -40000  ->  -40000 as i16  ==  +25536
        // which flips the node from the very back of the stacking context to the
        // front. Compare with the line-height encoder, which *does* check
        // `pct_x10 >= -32768` before narrowing.
        for z in [-32_769i32, -40_000, -99_999, i32::MIN] {
            let mut s = Sink::new();
            s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
                LayoutZIndex::Integer(z),
            )));
            assert!(
                s.cold.z_index < 0 || s.cold.z_index == I16_SENTINEL,
                "z-index {z} encoded to {}: a negative z-index must stay negative (or \
                 saturate to the sentinel), it must never wrap to a positive value",
                s.cold.z_index,
            );
        }
    }
    #[test]
    fn apply_border_styles_pack_into_independent_nibbles() {
        let mut s = Sink::new();
        s.apply(&CssProperty::BorderTopStyle(CssPropertyValue::Exact(
            StyleBorderTopStyle { inner: BorderStyle::Solid },
        )));
        assert_eq!(
            s.cold.border_styles_packed & 0x000F,
            u16::from(border_style_to_u8(BorderStyle::Solid))
        );
        assert_eq!(
            s.cold.border_styles_packed & 0xFFF0,
            0,
            "the top-style nibble leaked into the other three sides"
        );
        // Re-applying must REPLACE the nibble, not OR into it: Solid(1) | Double(2)
        // would be Dotted(3), a different border style entirely.
        s.apply(&CssProperty::BorderTopStyle(CssPropertyValue::Exact(
            StyleBorderTopStyle { inner: BorderStyle::Double },
        )));
        assert_eq!(
            s.cold.border_styles_packed & 0x000F,
            u16::from(border_style_to_u8(BorderStyle::Double))
        );
    }
    #[test]
    fn apply_opacity_clamps_into_the_0_254_range() {
        for (pct, expected) in [
            (-1.0e9f32, 0u8),
            (-100.0, 0),
            (0.0, 0),
            (50.0, 127),
            (100.0, 254),
            (500.0, 254),
            (1.0e9, 254),
        ] {
            let mut s = Sink::new();
            s.apply(&CssProperty::Opacity(CssPropertyValue::Exact(StyleOpacity {
                inner: PercentageValue::new(pct),
            })));
            assert_eq!(s.cold.opacity, expected, "opacity: {pct}%");
            assert_ne!(
                s.cold.opacity, OPACITY_SENTINEL,
                "an explicitly set opacity must never encode as the 'unset' sentinel"
            );
        }
    }
    #[test]
    fn apply_grid_column_encodes_both_lines() {
        let mut s = Sink::new();
        s.apply(&CssProperty::GridColumn(CssPropertyValue::Exact(GridPlacement {
            grid_start: GridLine::Line(2),
            grid_end: GridLine::Span(3),
        })));
        assert_eq!(s.cold.grid_col_start, 2);
        assert_eq!(s.cold.grid_col_end, -3);
        // grid-row must be untouched by a grid-column declaration
        assert_eq!(s.cold.grid_row_start, I16_AUTO);
        assert_eq!(s.cold.grid_row_end, I16_AUTO);
    }
    #[test]
    fn apply_hot_flags_or_in_without_clobbering_each_other() {
        let mut s = Sink::new();
        s.apply(&CssProperty::TextDecoration(CssPropertyValue::Exact(
            StyleTextDecoration::Underline,
        )));
        assert_eq!(
            s.cold.hot_flags & HOT_FLAG_HAS_TEXT_DECORATION,
            HOT_FLAG_HAS_TEXT_DECORATION
        );
        // scrollbar-gutter writes a 2-bit *field* into the same byte; it must not
        // wipe the has-* bits around it.
        s.apply(&CssProperty::ScrollbarGutter(CssPropertyValue::Exact(
            StyleScrollbarGutter::Stable,
        )));
        assert_eq!(
            (s.cold.hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK) >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT,
            SCROLLBAR_GUTTER_STABLE
        );
        assert_eq!(
            s.cold.hot_flags & HOT_FLAG_HAS_TEXT_DECORATION,
            HOT_FLAG_HAS_TEXT_DECORATION,
            "scrollbar-gutter cleared the has-text-decoration bit"
        );
        // ...and replacing the gutter value must clear the old bits, not OR into them
        s.apply(&CssProperty::ScrollbarGutter(CssPropertyValue::Exact(
            StyleScrollbarGutter::Auto,
        )));
        assert_eq!(
            (s.cold.hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK) >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT,
            SCROLLBAR_GUTTER_AUTO
        );
        assert_eq!(
            s.cold.hot_flags & HOT_FLAG_HAS_TEXT_DECORATION,
            HOT_FLAG_HAS_TEXT_DECORATION
        );
    }
    #[test]
    fn apply_valueless_property_does_not_set_a_has_flag() {
        // The has-* bits exist so the getter can skip the cascade walk. A property
        // with no Exact payload must leave them clear, or every node pays for a walk
        // that would find nothing.
        let mut s = Sink::new();
        s.apply(&CssProperty::TextDecoration(CssPropertyValue::Initial));
        s.apply(&CssProperty::ScrollbarGutter(CssPropertyValue::Unset));
        assert_eq!(s.cold.hot_flags, 0);
    }
    // -------------------------------------------------------------------------
    // apply_css_property_to_compact — tier 2b text
    // -------------------------------------------------------------------------
    #[test]
    fn apply_text_color_packs_rgba_big_endian() {
        let mut s = Sink::new();
        s.apply(&CssProperty::TextColor(CssPropertyValue::Exact(StyleTextColor {
            inner: ColorU { r: 0x12, g: 0x34, b: 0x56, a: 0x78 },
        })));
        assert_eq!(s.text.text_color, 0x1234_5678);
        // Documented limitation: rgba(0,0,0,0) is indistinguishable from "unset".
        let mut transparent = Sink::new();
        transparent.apply(&CssProperty::TextColor(CssPropertyValue::Exact(
            StyleTextColor { inner: ColorU { r: 0, g: 0, b: 0, a: 0 } },
        )));
        assert_eq!(transparent.text.text_color, 0);
    }
    #[test]
    fn apply_line_height_round_trips_and_saturates_at_both_ends() {
        let mut s = Sink::new();
        s.apply(&line_height_prop(120.0));
        assert_eq!(s.text.line_height, 1200, "120% must encode as % x 10");
        // Absurd values must saturate - no wrap-around. The two signs land
        // differently by design: a huge POSITIVE (unitless multiple) falls to
        // the sentinel ("normal" - a 10^7x multiple is meaningless), while a
        // huge NEGATIVE (= absolute px per the parser convention) CLAMPS to
        // the largest representable px (-32768 = 3276.8px) instead of being
        // silently reinterpreted as "normal".
        let mut big = Sink::new();
        big.apply(&line_height_prop(1.0e9f32));
        assert_eq!(
            big.text.line_height, I16_SENTINEL,
            "a huge unitless multiple saturates to the sentinel"
        );
        let mut neg = Sink::new();
        neg.apply(&line_height_prop(-1.0e9f32));
        assert_eq!(
            neg.text.line_height, -32768,
            "a huge absolute px line-height clamps instead of dropping to normal"
        );
        // The split scale itself: 48px (normalized -48) stores as -480 and
        // decodes back to 48px - the old x1000 scale overflowed at 32.76px.
        let mut px48 = Sink::new();
        px48.apply(&line_height_prop(-4800.0));
        assert_eq!(px48.text.line_height, -480, "line-height: 48px stores as -px x 10");
    }
    #[test]
    fn apply_font_family_hash_is_nonzero_stable_and_registered() {
        let arial = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("Arial".into())]);
        let mut s = Sink::new();
        s.apply(&CssProperty::FontFamily(CssPropertyValue::Exact(arial.clone())));
        let h = s.text.font_family_hash;
        assert_ne!(
            h, 0,
            "0 is the 'unset' sentinel — a set font-family must never hash to it"
        );
        assert!(
            s.fonts.contains_key(&h),
            "the hash must be registered in the reverse map, or consumers cannot resolve it"
        );
        // Same input -> same hash (the whole dirty-tracking scheme depends on this).
        let mut same = Sink::new();
        same.apply(&CssProperty::FontFamily(CssPropertyValue::Exact(arial)));
        assert_eq!(same.text.font_family_hash, h);
        // Different input -> different hash.
        let mut other = Sink::new();
        other.apply(&CssProperty::FontFamily(CssPropertyValue::Exact(
            StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("Times".into())]),
        )));
        assert_ne!(other.text.font_family_hash, h);
    }
    // -------------------------------------------------------------------------
    // apply_ua_css_to_compact
    // -------------------------------------------------------------------------
    #[test]
    fn ua_css_is_idempotent_for_every_representative_node_type() {
        let nodes = [
            NodeData::create_node(NodeType::Html),
            NodeData::create_node(NodeType::Body),
            NodeData::create_node(NodeType::Div),
            NodeData::create_node(NodeType::P),
            NodeData::create_node(NodeType::Br),
            NodeData::create_text_do_not_use_without_block_level_wrapper("hello"),
        ];
        for nd in &nodes {
            let mut s = Sink::new();
            s.ua(&nd.node_type);
            let once = s.snapshot();
            s.ua(&nd.node_type);
            assert_eq!(
                s.snapshot(),
                once,
                "applying UA CSS twice must be a no-op the second time"
            );
        }
    }
    #[test]
    fn ua_css_never_touches_the_tier1_populated_bit() {
        // Bit 63 is owned by the builder, not by the UA stylesheet.
        for nt in [NodeType::Html, NodeType::Body, NodeType::Div, NodeType::P] {
            let mut s = Sink::new();
            s.ua(&nt);
            assert_eq!(s.tier1 & TIER1_POPULATED_BIT, 0);
        }
    }
    #[test]
    fn ua_css_survives_a_hostile_pre_filled_sink() {
        // Every bit set / every numeric field at an extreme: the writer must still
        // only touch its own fields and must not panic on the sentinel inputs.
        let mut s = Sink::new();
        s.tier1 = u64::MAX;
        s.dims.width = U32_SENTINEL;
        s.dims.font_size = U32_SENTINEL;
        s.dims.flex_grow = U16_SENTINEL;
        s.cold.z_index = i16::MIN;
        s.cold.opacity = OPACITY_SENTINEL;
        s.text.line_height = i16::MIN;
        s.ua(&NodeType::Div);
        assert_eq!(
            s.tier1 & TIER1_POPULATED_BIT,
            TIER1_POPULATED_BIT,
            "UA CSS must not clear bits it does not own"
        );
    }
    // -------------------------------------------------------------------------
    // build_compact_cache
    // -------------------------------------------------------------------------
    #[test]
    fn build_compact_cache_handles_zero_nodes() {
        let cache = CssPropertyCache::empty(0);
        let r = cache.build_compact_cache(&[], &[]);
        assert_eq!(r.node_count(), 0);
        assert!(r.tier2_dims.is_empty());
        assert!(r.font_dirty_nodes.is_empty());
        assert!(r.prev_font_hashes.is_empty());
    }
    #[test]
    fn build_compact_cache_tolerates_a_mismatched_prev_font_hash_slice() {
        let cache = CssPropertyCache::empty(3);
        let nodes = div_nodes(3);
        // longer than node_count, shorter than node_count, and empty — none may panic
        for prev in [vec![1u64, 2, 3, 4, 5, 6], vec![7u64], Vec::new()] {
            let r = cache.build_compact_cache(&nodes, &prev);
            assert_eq!(r.prev_font_hashes.len(), 3);
            assert_eq!(r.node_count(), 3);
        }
    }
    #[test]
    fn build_compact_cache_tolerates_short_node_data() {
        // node_count claims 4 but only 2 NodeDatas are supplied: the trailing nodes
        // must keep their defaults instead of indexing out of bounds.
        let cache = CssPropertyCache::empty(4);
        let r = cache.build_compact_cache(&div_nodes(2), &[]);
        assert_eq!(r.node_count(), 4);
        assert_eq!(r.tier2_dims.len(), 4);
        assert_eq!(r.tier2_cold.len(), 4);
        assert_eq!(r.tier2b_text.len(), 4);
        assert_eq!(r.prev_font_hashes.len(), 4);
    }
    #[test]
    fn build_compact_cache_honours_node_count_over_node_data_len() {
        let cache = CssPropertyCache::empty(2);
        let r = cache.build_compact_cache(&div_nodes(5), &[]);
        assert_eq!(r.node_count(), 2);
    }
    #[test]
    fn build_compact_cache_rebuild_with_unchanged_fonts_is_not_dirty() {
        let cache = CssPropertyCache::empty(3);
        let nodes = div_nodes(3);
        let first = cache.build_compact_cache(&nodes, &[]);
        let second = cache.build_compact_cache(&nodes, &first.prev_font_hashes);
        assert!(
            second.font_dirty_nodes.is_empty(),
            "a rebuild with identical font hashes must not re-resolve any font chain"
        );
    }
    // -------------------------------------------------------------------------
    // build_compact_cache_with_inheritance{,_debug}
    // -------------------------------------------------------------------------
    #[test]
    fn build_with_inheritance_handles_zero_nodes() {
        let cache = CssPropertyCache::empty(0);
        let r = cache.build_compact_cache_with_inheritance(&[], &[], &[]);
        assert_eq!(r.node_count(), 0);
        let mut msgs = None;
        let r2 = cache.build_compact_cache_with_inheritance_debug(&[], &[], &[], &mut msgs);
        assert_eq!(r2.node_count(), 0);
        assert!(msgs.is_none());
    }
    #[test]
    fn build_with_inheritance_propagates_font_size_down_the_chain() {
        let n = 3;
        let cache = CssPropertyCache::empty(n);
        let r = cache.build_compact_cache_with_inheritance(
            &div_nodes(n),
            &linear_hierarchy(n),
            &[],
        );
        assert_eq!(r.node_count(), n);
        // font-size is inheritable: property-less children must match the root exactly.
        assert_eq!(r.tier2_dims[1].font_size, r.tier2_dims[0].font_size);
        assert_eq!(r.tier2_dims[2].font_size, r.tier2_dims[0].font_size);
    }
    #[test]
    fn build_with_inheritance_marks_all_nodes_dirty_on_the_first_build() {
        let n = 3;
        let cache = CssPropertyCache::empty(n);
        let nodes = div_nodes(n);
        let hierarchy = linear_hierarchy(n);
        // Empty prev_font_hashes == first build for this DOM -> force ALL nodes dirty.
        let first = cache.build_compact_cache_with_inheritance(&nodes, &hierarchy, &[]);
        assert_eq!(first.font_dirty_nodes, vec![0, 1, 2]);
        // Second build with the previous hashes -> nothing changed, nothing dirty.
        let second =
            cache.build_compact_cache_with_inheritance(&nodes, &hierarchy, &first.prev_font_hashes);
        assert!(second.font_dirty_nodes.is_empty());
    }
    #[test]
    fn build_with_inheritance_global_star_rules_skip_text_nodes() {
        // Per CSS, `*` matches ELEMENTS. A text node is not an element — it may only
        // inherit from its parent, otherwise `* { padding: 5px }` would overwrite the
        // value a text node inherited from `<p>`.
        let mut cache = CssPropertyCache::empty(2);
        cache
            .global_css_props
            .push(CssProperty::PaddingTop(padding(5.0)));
        let nodes = vec![
            NodeData::create_node(NodeType::Div),
            NodeData::create_text_do_not_use_without_block_level_wrapper("hi"),
        ];
        let r = cache.build_compact_cache_with_inheritance(&nodes, &linear_hierarchy(2), &[]);
        assert_eq!(
            r.tier2_dims[0].padding_top, 50,
            "the `*` rule must apply to the element"
        );
        assert_ne!(
            r.tier2_dims[1].padding_top, 50,
            "the `*` rule must NOT apply to a text node"
        );
    }
    #[test]
    fn build_with_inheritance_debug_messages_are_opt_in() {
        let n = 2;
        let cache = CssPropertyCache::empty(n);
        let nodes = div_nodes(n);
        let hierarchy = linear_hierarchy(n);
        let mut on = Some(Vec::new());
        let _ = cache.build_compact_cache_with_inheritance_debug(&nodes, &hierarchy, &[], &mut on);
        assert!(
            !on.expect("still Some").is_empty(),
            "debug logging must emit at least one cascade message"
        );
        let mut off = None;
        let _ = cache.build_compact_cache_with_inheritance_debug(&nodes, &hierarchy, &[], &mut off);
        assert!(off.is_none(), "a None sink must stay None");
    }
    // -------------------------------------------------------------------------
    // resolve_font_size_to_px
    // -------------------------------------------------------------------------
    #[test]
    fn resolve_font_size_percent_uses_the_parent() {
        let mut dims = vec![CompactNodeProps::default(); 2];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::percent(50.0));
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
        let pv = decode_pixel_value_u32(dims[1].font_size).expect("must resolve to px");
        assert_eq!(pv.metric, SizeMetric::Px);
        assert!((pv.number.get() - 10.0).abs() < 0.01, "got {}", pv.number.get());
    }
    #[test]
    fn resolve_font_size_pt_converts_to_px() {
        let mut dims = vec![CompactNodeProps::default()];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::pt(12.0));
        resolve_font_size_to_px(&mut dims, 0, None);
        let pv = decode_pixel_value_u32(dims[0].font_size).expect("must resolve to px");
        assert!(
            (pv.number.get() - 16.0).abs() < 0.01,
            "12pt should be 16px, got {}",
            pv.number.get()
        );
    }
    #[test]
    fn resolve_font_size_leaves_absolute_and_sentinel_values_alone() {
        // an already-px value must not be re-scaled by the parent
        let mut dims = vec![CompactNodeProps::default(); 2];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::px(13.0));
        let before = dims[1].font_size;
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
        assert_eq!(dims[1].font_size, before);
        // an explicit sentinel must survive untouched
        let mut sent = vec![CompactNodeProps::default(); 2];
        sent[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
        sent[1].font_size = U32_SENTINEL;
        resolve_font_size_to_px(&mut sent, 1, Some(NodeId::new(0)));
        assert_eq!(sent[1].font_size, U32_SENTINEL);
        // ...as must the CSS-initial default (which also sits above the threshold)
        let mut def = vec![CompactNodeProps::default(); 2];
        assert_eq!(def[1].font_size, U32_INITIAL);
        resolve_font_size_to_px(&mut def, 1, Some(NodeId::new(0)));
        assert_eq!(def[1].font_size, U32_INITIAL);
    }
    #[test]
    fn resolve_font_size_negative_em_is_deterministic() {
        let mut dims = vec![CompactNodeProps::default(); 2];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(-2.0));
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
        let pv = decode_pixel_value_u32(dims[1].font_size).expect("must stay decodable");
        assert!(
            (pv.number.get() + 40.0).abs() < 0.01,
            "-2em of 20px should be -40px, got {}",
            pv.number.get()
        );
    }
    #[test]
    fn resolve_font_size_overflow_saturates_instead_of_wrapping() {
        let mut dims = vec![CompactNodeProps::default(); 2];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
        // 100_000em x 20px = 2_000_000px, past the 28-bit fixed-point range
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(100_000.0));
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
        assert_eq!(
            dims[1].font_size, U32_SENTINEL,
            "an overflowing font-size must land on the tier-3 sentinel, not wrap"
        );
    }
    #[test]
    fn resolve_font_size_nan_em_degrades_to_zero() {
        let mut dims = vec![CompactNodeProps::default(); 2];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(f32::NAN));
        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
        let pv = decode_pixel_value_u32(dims[1].font_size).expect("must stay decodable");
        assert!(pv.number.get().is_finite(), "a NaN font-size must not propagate");
        assert_eq!(pv.number.get(), 0.0);
    }
    #[test]
    fn resolve_font_size_root_rem_uses_the_16px_initial_value() {
        // For the ROOT node, `tier2_dims.first()` IS the node itself — and at this
        // point its font-size is still the *unresolved* rem value. The Rem arm then
        // multiplies the rem factor by itself:
        //     html { font-size: 2rem }  ->  2 * 2 = 4px   (should be 2 * 16 = 32px)
        // Every other unit handles the no-parent case correctly via `map_or(16.0, ..)`.
        let mut dims = vec![CompactNodeProps::default()];
        dims[0].font_size = encode_pixel_value_u32(&PixelValue::rem(2.0));
        resolve_font_size_to_px(&mut dims, 0, None);
        let pv = decode_pixel_value_u32(dims[0].font_size).expect("must resolve to px");
        assert!(
            (pv.number.get() - 32.0).abs() < 0.01,
            "root `font-size: 2rem` should resolve against the 16px initial value (= 32px), \
             got {}px",
            pv.number.get()
        );
    }
}