1
//! Compact layout property cache — three-tier numeric encoding
2
//!
3
//! Replaces BTreeMap-based CSS property lookups with cache-friendly arrays.
4
//!
5
//! - **Tier 1**: `Vec<u64>` — ALL 21 enum properties bitpacked (8 B/node)
6
//! - **Tier 2 hot**: `Vec<CompactNodeProps>` — layout-critical numeric dimensions (68 B/node)
7
//! - **Tier 2 cold**: `Vec<CompactNodePropsCold>` — paint-only properties (28 B/node)
8
//! - **Tier 2b**: `Vec<CompactTextProps>` — text/IFC properties (24 B/node)
9
//!
10
//! Non-compact properties (background, box-shadow, transform, etc.) are
11
//! resolved via the slow cascade path in `CssPropertyCache::get_property_slow()`.
12

            
13
// The `*_from_u8` decoders below intentionally give an explicit arm for the byte
14
// that maps to each enum's default (e.g. `0 => Block`) even though the `_`
15
// catch-all returns the same default — this keeps the decode table a 1:1 mirror
16
// of the `*_to_u8` encoders. clippy::match_same_arms flags those explicit arms as
17
// duplicates of `_`; merging them would drop the encoding documentation, so allow
18
// it for this codec module (false positive for the intent here).
19
#![allow(clippy::match_same_arms)]
20

            
21
use crate::props::basic::length::{FloatValue, SizeMetric};
22
use crate::props::basic::pixel::PixelValue;
23
use crate::props::layout::{
24
    display::LayoutDisplay,
25
    dimensions::{LayoutHeight, LayoutWidth, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth},
26
    flex::{
27
        LayoutAlignContent, LayoutAlignItems, LayoutAlignSelf, LayoutFlexDirection, LayoutFlexWrap,
28
        LayoutJustifyContent,
29
    },
30
    grid::{LayoutGridAutoFlow, LayoutJustifySelf, LayoutJustifyItems},
31
    overflow::LayoutOverflow,
32
    position::LayoutPosition,
33
    wrapping::{LayoutClear, LayoutWritingMode},
34
    table::StyleBorderCollapse,
35
};
36
use crate::props::layout::display::LayoutFloat;
37
use crate::props::layout::dimensions::LayoutBoxSizing;
38
use crate::props::basic::font::{StyleFontStyle, StyleFontWeight};
39
use crate::props::basic::color::ColorU;
40
use crate::props::style::{StyleTextAlign, StyleVerticalAlign, StyleVisibility, StyleWhiteSpace, StyleDirection};
41
use crate::props::style::border::BorderStyle;
42
use crate::props::property::{CssProperty, CssPropertyType};
43
use crate::css::CssPropertyValue;
44
use alloc::boxed::Box;
45
use alloc::vec::Vec;
46

            
47
// =============================================================================
48
// Sentinel Constants
49
// =============================================================================
50

            
51
/// u16 sentinel values (for resolved-px ×10 and flex ×100)
52
pub const U16_SENTINEL: u16 = 0xFFFF;
53
/// Any u16 value >= this threshold is a sentinel, not a real value
54
pub const U16_SENTINEL_THRESHOLD: u16 = 0xFFF9;
55

            
56
/// i16 sentinel values (for signed resolved-px ×10)
57
pub const I16_SENTINEL: i16 = 0x7FFF;       // 32767
58
pub const I16_AUTO: i16 = 0x7FFE;           // 32766
59
pub const I16_INHERIT: i16 = 0x7FFD;        // 32765
60
pub const I16_INITIAL: i16 = 0x7FFC;        // 32764
61
/// Any i16 value >= this threshold is a sentinel
62
pub const I16_SENTINEL_THRESHOLD: i16 = 0x7FFC; // 32764
63

            
64
/// u32 sentinel values (for dimension properties with unit info)
65
pub const U32_SENTINEL: u32 = 0xFFFF_FFFF;
66
pub const U32_AUTO: u32 = 0xFFFF_FFFE;
67
pub const U32_NONE: u32 = 0xFFFF_FFFD;
68
pub const U32_INHERIT: u32 = 0xFFFF_FFFC;
69
pub const U32_INITIAL: u32 = 0xFFFF_FFFB;
70
pub const U32_MIN_CONTENT: u32 = 0xFFFF_FFFA;
71
pub const U32_MAX_CONTENT: u32 = 0xFFFF_FFF9;
72
/// Any u32 value >= this threshold is a sentinel
73
pub const U32_SENTINEL_THRESHOLD: u32 = 0xFFFF_FFF9;
74

            
75
// =============================================================================
76
// Tier 1: u64 bitfield — ALL enum properties
77
// =============================================================================
78
//
79
// Bit layout (52 bits used, 12 spare):
80
//   [4:0]    display          5 bits  (22 variants)
81
//   [7:5]    position         3 bits  (5 variants)
82
//   [9:8]    float            2 bits  (3 variants)
83
//   [12:10]  overflow_x       3 bits  (5 variants)
84
//   [15:13]  overflow_y       3 bits  (5 variants)
85
//   [16]     box_sizing       1 bit   (2 variants)
86
//   [18:17]  flex_direction   2 bits  (4 variants)
87
//   [20:19]  flex_wrap        2 bits  (3 variants)
88
//   [23:21]  justify_content  3 bits  (8 variants)
89
//   [26:24]  align_items      3 bits  (5 variants)
90
//   [29:27]  align_content    3 bits  (6 variants)
91
//   [31:30]  writing_mode     2 bits  (3 variants)
92
//   [33:32]  clear            2 bits  (4 variants)
93
//   [37:34]  font_weight      4 bits  (11 variants)
94
//   [39:38]  font_style       2 bits  (3 variants)
95
//   [42:40]  text_align       3 bits  (6 variants)
96
//   [44:43]  visibility       2 bits  (3 variants)
97
//   [47:45]  white_space      3 bits  (6 variants)
98
//   [48]     direction        1 bit   (2 variants)
99
//   [51:49]  vertical_align   3 bits  (8 variants)
100
//   [52]     border_collapse  1 bit   (2 variants)
101
//   [63:53]  (spare / sentinel flags)
102

            
103
// Bit offsets within u64
104
pub const DISPLAY_SHIFT: u32 = 0;
105
pub const POSITION_SHIFT: u32 = 5;
106
pub const FLOAT_SHIFT: u32 = 8;
107
pub const OVERFLOW_X_SHIFT: u32 = 10;
108
pub const OVERFLOW_Y_SHIFT: u32 = 13;
109
pub const BOX_SIZING_SHIFT: u32 = 16;
110
pub const FLEX_DIRECTION_SHIFT: u32 = 17;
111
pub const FLEX_WRAP_SHIFT: u32 = 19;
112
pub const JUSTIFY_CONTENT_SHIFT: u32 = 21;
113
pub const ALIGN_ITEMS_SHIFT: u32 = 24;
114
pub const ALIGN_CONTENT_SHIFT: u32 = 27;
115
pub const WRITING_MODE_SHIFT: u32 = 30;
116
pub const CLEAR_SHIFT: u32 = 32;
117
pub const FONT_WEIGHT_SHIFT: u32 = 34;
118
pub const FONT_STYLE_SHIFT: u32 = 38;
119
pub const TEXT_ALIGN_SHIFT: u32 = 40;
120
pub const VISIBILITY_SHIFT: u32 = 43;
121
pub const WHITE_SPACE_SHIFT: u32 = 45;
122
pub const DIRECTION_SHIFT: u32 = 48;
123
pub const VERTICAL_ALIGN_SHIFT: u32 = 49;
124
pub const BORDER_COLLAPSE_SHIFT: u32 = 52;
125

            
126
// Bit masks
127
pub const DISPLAY_MASK: u64 = 0x1F;     // 5 bits
128
pub const POSITION_MASK: u64 = 0x07;    // 3 bits
129
pub const FLOAT_MASK: u64 = 0x03;       // 2 bits
130
pub const OVERFLOW_MASK: u64 = 0x07;    // 3 bits
131
pub const BOX_SIZING_MASK: u64 = 0x01;  // 1 bit
132
pub const FLEX_DIR_MASK: u64 = 0x03;    // 2 bits
133
pub const FLEX_WRAP_MASK: u64 = 0x03;   // 2 bits
134
pub const JUSTIFY_MASK: u64 = 0x07;     // 3 bits
135
pub const ALIGN_MASK: u64 = 0x07;       // 3 bits
136
pub const WRITING_MODE_MASK: u64 = 0x03;// 2 bits
137
pub const CLEAR_MASK: u64 = 0x03;       // 2 bits
138
pub const FONT_WEIGHT_MASK: u64 = 0x0F; // 4 bits
139
pub const FONT_STYLE_MASK: u64 = 0x03;  // 2 bits
140
pub const TEXT_ALIGN_MASK: u64 = 0x07;  // 3 bits
141
pub const VISIBILITY_MASK: u64 = 0x03;  // 2 bits
142
pub const WHITE_SPACE_MASK: u64 = 0x07; // 3 bits
143
pub const DIRECTION_MASK: u64 = 0x01;   // 1 bit
144
pub const VERTICAL_ALIGN_MASK: u64 = 0x07; // 3 bits
145
pub const BORDER_COLLAPSE_MASK: u64 = 0x01; // 1 bit
146

            
147
pub const ALIGN_SELF_SHIFT: u32 = 53;
148
pub const JUSTIFY_SELF_SHIFT: u32 = 56;
149
pub const GRID_AUTO_FLOW_SHIFT: u32 = 59;
150
pub const JUSTIFY_ITEMS_SHIFT: u32 = 61;
151
pub const ALIGN_SELF_MASK: u64 = 0x07;     // 3 bits
152
pub const JUSTIFY_SELF_MASK: u64 = 0x07;   // 3 bits
153
pub const GRID_AUTO_FLOW_MASK: u64 = 0x03; // 2 bits (row/col × dense)
154
pub const JUSTIFY_ITEMS_MASK: u64 = 0x03;  // 2 bits (start/center/end/stretch)
155

            
156
/// Special value stored in the spare bits [63:51] to indicate this node has
157
/// NO tier-1 data (i.e., all defaults).
158
///
159
/// 0 is a valid all-defaults encoding,
160
/// so we use bit 63 as a "tier1 populated" flag. If bit 63 is 0 and all other
161
/// bits are 0, it means "all defaults" (`Display::Block`, `Position::Static`, etc.).
162
/// We set bit 63 = 1 to mark that the node HAS been populated.
163
pub const TIER1_POPULATED_BIT: u64 = 1 << 63;
164

            
165
// =============================================================================
166
// Safe from_u8 conversion functions (no transmute!)
167
// =============================================================================
168

            
169
/// Decode display from u8. **0 = Block** (most common HTML default).
170
/// Value 31 (0x1F) = sentinel: look up in slow path for uncommon values.
171
/// Returns default (Block) on invalid input.
172
#[inline]
173
7317714
#[must_use] pub const fn layout_display_from_u8(v: u8) -> LayoutDisplay {
174
7317714
    match v {
175
2724487
        0 => LayoutDisplay::Block,        // default when bits are 0
176
1704976
        1 => LayoutDisplay::Inline,
177
615403
        2 => LayoutDisplay::InlineBlock,
178
2192695
        3 => LayoutDisplay::Flex,
179
2165
        4 => LayoutDisplay::None,
180
162
        5 => LayoutDisplay::InlineFlex,
181
6685
        6 => LayoutDisplay::Table,
182
151
        7 => LayoutDisplay::InlineTable,
183
316
        8 => LayoutDisplay::TableRowGroup,
184
525
        9 => LayoutDisplay::TableHeaderGroup,
185
152
        10 => LayoutDisplay::TableFooterGroup,
186
7873
        11 => LayoutDisplay::TableRow,
187
151
        12 => LayoutDisplay::TableColumnGroup,
188
250
        13 => LayoutDisplay::TableColumn,
189
26727
        14 => LayoutDisplay::TableCell,
190
162
        15 => LayoutDisplay::TableCaption,
191
162
        16 => LayoutDisplay::FlowRoot,
192
31072
        17 => LayoutDisplay::ListItem,
193
129
        18 => LayoutDisplay::RunIn,
194
129
        19 => LayoutDisplay::Marker,
195
1482
        20 => LayoutDisplay::Grid,
196
152
        21 => LayoutDisplay::InlineGrid,
197
319
        22 => LayoutDisplay::Contents,
198
1389
        _ => LayoutDisplay::Block, // fallback + sentinel (31)
199
    }
200
7317714
}
201

            
202
/// Encode display to u8. **0 = Block** (most common HTML default).
203
#[inline]
204
1639253
#[must_use] pub const fn layout_display_to_u8(v: LayoutDisplay) -> u8 {
205
1639253
    match v {
206
760930
        LayoutDisplay::Block => 0,         // 0 = default when bits unset
207
362638
        LayoutDisplay::Inline => 1,
208
104922
        LayoutDisplay::InlineBlock => 2,
209
248435
        LayoutDisplay::Flex => 3,
210
5566
        LayoutDisplay::None => 4,
211
134
        LayoutDisplay::InlineFlex => 5,
212
578
        LayoutDisplay::Table => 6,
213
26
        LayoutDisplay::InlineTable => 7,
214
74
        LayoutDisplay::TableRowGroup => 8,
215
86
        LayoutDisplay::TableHeaderGroup => 9,
216
14
        LayoutDisplay::TableFooterGroup => 10,
217
830
        LayoutDisplay::TableRow => 11,
218
14
        LayoutDisplay::TableColumnGroup => 12,
219
38
        LayoutDisplay::TableColumn => 13,
220
1262
        LayoutDisplay::TableCell => 14,
221
38
        LayoutDisplay::TableCaption => 15,
222
38
        LayoutDisplay::FlowRoot => 16,
223
2342
        LayoutDisplay::ListItem => 17,
224
2
        LayoutDisplay::RunIn => 18,
225
2
        LayoutDisplay::Marker => 19,
226
151214
        LayoutDisplay::Grid => 20,
227
26
        LayoutDisplay::InlineGrid => 21,
228
44
        LayoutDisplay::Contents => 22,
229
    }
230
1639253
}
231

            
232
#[inline]
233
3609224
#[must_use] pub const fn layout_position_from_u8(v: u8) -> LayoutPosition {
234
3609224
    match v {
235
3522360
        0 => LayoutPosition::Static,
236
18455
        1 => LayoutPosition::Relative,
237
65238
        2 => LayoutPosition::Absolute,
238
645
        3 => LayoutPosition::Fixed,
239
736
        4 => LayoutPosition::Sticky,
240
1790
        _ => LayoutPosition::Static,
241
    }
242
3609224
}
243

            
244
#[inline]
245
22461
#[must_use] pub const fn layout_position_to_u8(v: LayoutPosition) -> u8 {
246
22461
    match v {
247
454
        LayoutPosition::Static => 0,
248
9815
        LayoutPosition::Relative => 1,
249
11846
        LayoutPosition::Absolute => 2,
250
110
        LayoutPosition::Fixed => 3,
251
236
        LayoutPosition::Sticky => 4,
252
    }
253
22461
}
254

            
255
/// Decode float from u8. **0 = None** (CSS initial value).
256
#[inline]
257
1517100
#[must_use] pub const fn layout_float_from_u8(v: u8) -> LayoutFloat {
258
1517100
    match v {
259
1511345
        0 => LayoutFloat::None,            // default when bits unset
260
2798
        1 => LayoutFloat::Left,
261
1678
        2 => LayoutFloat::Right,
262
1279
        _ => LayoutFloat::None,
263
    }
264
1517100
}
265

            
266
/// Encode float to u8. **0 = None** (CSS initial value).
267
#[inline]
268
885
#[must_use] pub const fn layout_float_to_u8(v: LayoutFloat) -> u8 {
269
885
    match v {
270
418
        LayoutFloat::None => 0,
271
327
        LayoutFloat::Left => 1,
272
140
        LayoutFloat::Right => 2,
273
    }
274
885
}
275

            
276
/// Decode overflow from u8. **0 = Visible** (CSS initial value).
277
#[inline]
278
5599000
#[must_use] pub const fn layout_overflow_from_u8(v: u8) -> LayoutOverflow {
279
5599000
    match v {
280
5515881
        0 => LayoutOverflow::Visible,      // default when bits unset
281
49792
        1 => LayoutOverflow::Hidden,
282
19620
        2 => LayoutOverflow::Scroll,
283
9288
        3 => LayoutOverflow::Auto,
284
1057
        4 => LayoutOverflow::Clip,
285
3362
        _ => LayoutOverflow::Visible,
286
    }
287
5599000
}
288

            
289
/// Encode overflow to u8. **0 = Visible** (CSS initial value).
290
#[inline]
291
31210
#[must_use] pub const fn layout_overflow_to_u8(v: LayoutOverflow) -> u8 {
292
31210
    match v {
293
2381
        LayoutOverflow::Visible => 0,      // 0 = default when bits unset
294
22775
        LayoutOverflow::Hidden => 1,
295
4719
        LayoutOverflow::Scroll => 2,
296
1310
        LayoutOverflow::Auto => 3,
297
25
        LayoutOverflow::Clip => 4,
298
    }
299
31210
}
300

            
301
#[inline]
302
419773
#[must_use] pub const fn layout_box_sizing_from_u8(v: u8) -> LayoutBoxSizing {
303
419773
    match v {
304
309501
        0 => LayoutBoxSizing::ContentBox,
305
110018
        1 => LayoutBoxSizing::BorderBox,
306
254
        _ => LayoutBoxSizing::ContentBox,
307
    }
308
419773
}
309

            
310
#[inline]
311
135295
#[must_use] pub const fn layout_box_sizing_to_u8(v: LayoutBoxSizing) -> u8 {
312
135295
    match v {
313
454
        LayoutBoxSizing::ContentBox => 0,
314
134841
        LayoutBoxSizing::BorderBox => 1,
315
    }
316
135295
}
317

            
318
#[inline]
319
210663
#[must_use] pub const fn layout_flex_direction_from_u8(v: u8) -> LayoutFlexDirection {
320
210663
    match v {
321
165419
        0 => LayoutFlexDirection::Row,
322
1044
        1 => LayoutFlexDirection::RowReverse,
323
42926
        2 => LayoutFlexDirection::Column,
324
1022
        3 => LayoutFlexDirection::ColumnReverse,
325
252
        _ => LayoutFlexDirection::Row,
326
    }
327
210663
}
328

            
329
#[inline]
330
251175
#[must_use] pub const fn layout_flex_direction_to_u8(v: LayoutFlexDirection) -> u8 {
331
251175
    match v {
332
174766
        LayoutFlexDirection::Row => 0,
333
2
        LayoutFlexDirection::RowReverse => 1,
334
76399
        LayoutFlexDirection::Column => 2,
335
8
        LayoutFlexDirection::ColumnReverse => 3,
336
    }
337
251175
}
338

            
339
/// 0 = `NoWrap` (CSS initial value for flex-wrap)
340
#[inline]
341
142903
#[must_use] pub const fn layout_flex_wrap_from_u8(v: u8) -> LayoutFlexWrap {
342
142903
    match v {
343
139493
        0 => LayoutFlexWrap::NoWrap,       // CSS initial
344
1044
        1 => LayoutFlexWrap::Wrap,
345
1006
        2 => LayoutFlexWrap::WrapReverse,
346
1360
        _ => LayoutFlexWrap::NoWrap,
347
    }
348
142903
}
349

            
350
#[inline]
351
3625
#[must_use] pub const fn layout_flex_wrap_to_u8(v: LayoutFlexWrap) -> u8 {
352
3625
    match v {
353
406
        LayoutFlexWrap::NoWrap => 0,
354
3211
        LayoutFlexWrap::Wrap => 1,
355
8
        LayoutFlexWrap::WrapReverse => 2,
356
    }
357
3625
}
358

            
359
#[inline]
360
142936
#[must_use] pub const fn layout_justify_content_from_u8(v: u8) -> LayoutJustifyContent {
361
142936
    match v {
362
128348
        0 => LayoutJustifyContent::FlexStart,
363
523
        1 => LayoutJustifyContent::FlexEnd,
364
486
        2 => LayoutJustifyContent::Start,
365
490
        3 => LayoutJustifyContent::End,
366
11176
        4 => LayoutJustifyContent::Center,
367
586
        5 => LayoutJustifyContent::SpaceBetween,
368
571
        6 => LayoutJustifyContent::SpaceAround,
369
508
        7 => LayoutJustifyContent::SpaceEvenly,
370
248
        _ => LayoutJustifyContent::FlexStart,
371
    }
372
142936
}
373

            
374
#[inline]
375
71571
#[must_use] pub const fn layout_justify_content_to_u8(v: LayoutJustifyContent) -> u8 {
376
71571
    match v {
377
26
        LayoutJustifyContent::FlexStart => 0,
378
2
        LayoutJustifyContent::FlexEnd => 1,
379
382
        LayoutJustifyContent::Start => 2,
380
2
        LayoutJustifyContent::End => 3,
381
71062
        LayoutJustifyContent::Center => 4,
382
87
        LayoutJustifyContent::SpaceBetween => 5,
383
2
        LayoutJustifyContent::SpaceAround => 6,
384
8
        LayoutJustifyContent::SpaceEvenly => 7,
385
    }
386
71571
}
387

            
388
#[inline]
389
142903
#[must_use] pub const fn layout_align_items_from_u8(v: u8) -> LayoutAlignItems {
390
142903
    match v {
391
86386
        0 => LayoutAlignItems::Stretch,
392
43303
        1 => LayoutAlignItems::Center,
393
10422
        2 => LayoutAlignItems::Start,
394
499
        3 => LayoutAlignItems::End,
395
483
        4 => LayoutAlignItems::Baseline,
396
1810
        _ => LayoutAlignItems::Stretch,
397
    }
398
142903
}
399

            
400
#[inline]
401
202737
#[must_use] pub const fn layout_align_items_to_u8(v: LayoutAlignItems) -> u8 {
402
202737
    match v {
403
682
        LayoutAlignItems::Stretch => 0,
404
179207
        LayoutAlignItems::Center => 1,
405
22826
        LayoutAlignItems::Start => 2,
406
14
        LayoutAlignItems::End => 3,
407
8
        LayoutAlignItems::Baseline => 4,
408
    }
409
202737
}
410

            
411
#[inline]
412
6987
#[must_use] pub const fn layout_align_self_to_u8(v: LayoutAlignSelf) -> u8 {
413
6987
    match v {
414
3
        LayoutAlignSelf::Auto => 0,
415
134
        LayoutAlignSelf::Stretch => 1,
416
3194
        LayoutAlignSelf::Center => 2,
417
3638
        LayoutAlignSelf::Start => 3,
418
14
        LayoutAlignSelf::End => 4,
419
4
        LayoutAlignSelf::Baseline => 5,
420
    }
421
6987
}
422

            
423
#[inline]
424
138803
#[must_use] pub const fn layout_align_self_from_u8(v: u8) -> LayoutAlignSelf {
425
138803
    match v {
426
138525
        0 => LayoutAlignSelf::Auto,
427
12
        1 => LayoutAlignSelf::Stretch,
428
1
        2 => LayoutAlignSelf::Center,
429
1
        3 => LayoutAlignSelf::Start,
430
12
        4 => LayoutAlignSelf::End,
431
2
        5 => LayoutAlignSelf::Baseline,
432
250
        _ => LayoutAlignSelf::Auto,
433
    }
434
138803
}
435

            
436
#[inline]
437
13
#[must_use] pub const fn layout_justify_self_to_u8(v: LayoutJustifySelf) -> u8 {
438
13
    match v {
439
3
        LayoutJustifySelf::Auto => 0,
440
2
        LayoutJustifySelf::Start => 1,
441
2
        LayoutJustifySelf::End => 2,
442
2
        LayoutJustifySelf::Center => 3,
443
4
        LayoutJustifySelf::Stretch => 4,
444
    }
445
13
}
446

            
447
#[inline]
448
138803
#[must_use] pub const fn layout_justify_self_from_u8(v: u8) -> LayoutJustifySelf {
449
138803
    match v {
450
138547
        0 => LayoutJustifySelf::Auto,
451
1
        1 => LayoutJustifySelf::Start,
452
1
        2 => LayoutJustifySelf::End,
453
1
        3 => LayoutJustifySelf::Center,
454
2
        4 => LayoutJustifySelf::Stretch,
455
251
        _ => LayoutJustifySelf::Auto,
456
    }
457
138803
}
458

            
459
// Tier1 uses 0 as the "unset" sentinel for every enum. For justify-items
460
// the CSS default is `normal` which behaves as `stretch` on grid items,
461
// so 0 must decode to Stretch (not Start). Getting this wrong leaves
462
// every unset grid container reporting justify-items: Start, which
463
// forces taffy to content-size items instead of stretching them across
464
// their column tracks — exactly the calc.c regression.
465
#[inline]
466
12
#[must_use] pub const fn layout_justify_items_to_u8(v: LayoutJustifyItems) -> u8 {
467
12
    match v {
468
4
        LayoutJustifyItems::Stretch => 0,
469
2
        LayoutJustifyItems::Start => 1,
470
2
        LayoutJustifyItems::End => 2,
471
4
        LayoutJustifyItems::Center => 3,
472
    }
473
12
}
474

            
475
#[inline]
476
138804
#[must_use] pub const fn layout_justify_items_from_u8(v: u8) -> LayoutJustifyItems {
477
138804
    match v {
478
138548
        0 => LayoutJustifyItems::Stretch,
479
1
        1 => LayoutJustifyItems::Start,
480
1
        2 => LayoutJustifyItems::End,
481
2
        3 => LayoutJustifyItems::Center,
482
252
        _ => LayoutJustifyItems::Stretch,
483
    }
484
138804
}
485

            
486
#[inline]
487
11
#[must_use] pub const fn layout_grid_auto_flow_to_u8(v: LayoutGridAutoFlow) -> u8 {
488
11
    match v {
489
3
        LayoutGridAutoFlow::Row => 0,
490
2
        LayoutGridAutoFlow::Column => 1,
491
2
        LayoutGridAutoFlow::RowDense => 2,
492
4
        LayoutGridAutoFlow::ColumnDense => 3,
493
    }
494
11
}
495

            
496
#[inline]
497
291
#[must_use] pub const fn layout_grid_auto_flow_from_u8(v: u8) -> LayoutGridAutoFlow {
498
291
    match v {
499
35
        0 => LayoutGridAutoFlow::Row,
500
1
        1 => LayoutGridAutoFlow::Column,
501
1
        2 => LayoutGridAutoFlow::RowDense,
502
2
        3 => LayoutGridAutoFlow::ColumnDense,
503
252
        _ => LayoutGridAutoFlow::Row,
504
    }
505
291
}
506

            
507
#[inline]
508
142903
#[must_use] pub const fn layout_align_content_from_u8(v: u8) -> LayoutAlignContent {
509
142903
    match v {
510
139061
        0 => LayoutAlignContent::Stretch,
511
496
        1 => LayoutAlignContent::Center,
512
493
        2 => LayoutAlignContent::Start,
513
533
        3 => LayoutAlignContent::End,
514
483
        4 => LayoutAlignContent::SpaceBetween,
515
532
        5 => LayoutAlignContent::SpaceAround,
516
1305
        _ => LayoutAlignContent::Stretch,
517
    }
518
142903
}
519

            
520
#[inline]
521
423
#[must_use] pub const fn layout_align_content_to_u8(v: LayoutAlignContent) -> u8 {
522
423
    match v {
523
406
        LayoutAlignContent::Stretch => 0,
524
2
        LayoutAlignContent::Center => 1,
525
2
        LayoutAlignContent::Start => 2,
526
3
        LayoutAlignContent::End => 3,
527
2
        LayoutAlignContent::SpaceBetween => 4,
528
8
        LayoutAlignContent::SpaceAround => 5,
529
    }
530
423
}
531

            
532
#[inline]
533
1021011
#[must_use] pub const fn layout_writing_mode_from_u8(v: u8) -> LayoutWritingMode {
534
1021011
    match v {
535
1017438
        0 => LayoutWritingMode::HorizontalTb,
536
1279
        1 => LayoutWritingMode::VerticalRl,
537
1024
        2 => LayoutWritingMode::VerticalLr,
538
1270
        _ => LayoutWritingMode::HorizontalTb,
539
    }
540
1021011
}
541

            
542
#[inline]
543
477
#[must_use] pub const fn layout_writing_mode_to_u8(v: LayoutWritingMode) -> u8 {
544
477
    match v {
545
406
        LayoutWritingMode::HorizontalTb => 0,
546
51
        LayoutWritingMode::VerticalRl => 1,
547
20
        LayoutWritingMode::VerticalLr => 2,
548
    }
549
477
}
550

            
551
#[inline]
552
52780
#[must_use] pub const fn layout_clear_from_u8(v: u8) -> LayoutClear {
553
52780
    match v {
554
49383
        0 => LayoutClear::None,
555
1066
        1 => LayoutClear::Left,
556
1010
        2 => LayoutClear::Right,
557
1069
        3 => LayoutClear::Both,
558
252
        _ => LayoutClear::None,
559
    }
560
52780
}
561

            
562
#[inline]
563
503
#[must_use] pub const fn layout_clear_to_u8(v: LayoutClear) -> u8 {
564
503
    match v {
565
406
        LayoutClear::None => 0,
566
50
        LayoutClear::Left => 1,
567
14
        LayoutClear::Right => 2,
568
33
        LayoutClear::Both => 3,
569
    }
570
503
}
571

            
572
#[inline]
573
/// 0 = Normal/400 (CSS initial value for font-weight)
574
163374
#[must_use] pub const fn style_font_weight_from_u8(v: u8) -> StyleFontWeight {
575
163374
    match v {
576
158540
        0 => StyleFontWeight::Normal,     // CSS initial (400)
577
261
        1 => StyleFontWeight::W100,
578
273
        2 => StyleFontWeight::W200,
579
251
        3 => StyleFontWeight::W300,
580
245
        4 => StyleFontWeight::W500,
581
250
        5 => StyleFontWeight::W600,
582
986
        6 => StyleFontWeight::Bold,       // 700
583
253
        7 => StyleFontWeight::W800,
584
245
        8 => StyleFontWeight::W900,
585
267
        9 => StyleFontWeight::Lighter,
586
289
        10 => StyleFontWeight::Bolder,
587
1514
        _ => StyleFontWeight::Normal,
588
    }
589
163374
}
590

            
591
#[inline]
592
/// 0 = Normal/400 (CSS initial value for font-weight)
593
1857
#[must_use] pub const fn style_font_weight_to_u8(v: StyleFontWeight) -> u8 {
594
1857
    match v {
595
406
        StyleFontWeight::Normal => 0,     // CSS initial (400)
596
2
        StyleFontWeight::W100 => 1,
597
2
        StyleFontWeight::W200 => 2,
598
2
        StyleFontWeight::W300 => 3,
599
2
        StyleFontWeight::W500 => 4,
600
2
        StyleFontWeight::W600 => 5,
601
1403
        StyleFontWeight::Bold => 6,       // 700
602
2
        StyleFontWeight::W800 => 7,
603
2
        StyleFontWeight::W900 => 8,
604
2
        StyleFontWeight::Lighter => 9,
605
32
        StyleFontWeight::Bolder => 10,
606
    }
607
1857
}
608

            
609
#[inline]
610
163374
#[must_use] pub const fn style_font_style_from_u8(v: u8) -> StyleFontStyle {
611
163374
    match v {
612
159953
        0 => StyleFontStyle::Normal,
613
1123
        1 => StyleFontStyle::Italic,
614
1011
        2 => StyleFontStyle::Oblique,
615
1287
        _ => StyleFontStyle::Normal,
616
    }
617
163374
}
618

            
619
#[inline]
620
441
#[must_use] pub const fn style_font_style_to_u8(v: StyleFontStyle) -> u8 {
621
441
    match v {
622
406
        StyleFontStyle::Normal => 0,
623
27
        StyleFontStyle::Italic => 1,
624
8
        StyleFontStyle::Oblique => 2,
625
    }
626
441
}
627

            
628
#[inline]
629
865218
#[must_use] pub const fn style_text_align_from_u8(v: u8) -> StyleTextAlign {
630
    // Code 0 must decode to the CSS-initial value so an un-written tier1 field (all
631
    // zero) yields `start`, not physical `left` — otherwise direction:rtl never
632
    // right-aligns (start→right needs the value to actually be `start`). `left`
633
    // therefore takes code 4 (swapped with `start`).
634
865218
    match v {
635
753020
        0 => StyleTextAlign::Start,
636
108345
        1 => StyleTextAlign::Center,
637
552
        2 => StyleTextAlign::Right,
638
864
        3 => StyleTextAlign::Justify,
639
667
        4 => StyleTextAlign::Left,
640
529
        5 => StyleTextAlign::End,
641
1241
        _ => StyleTextAlign::Start,
642
    }
643
865218
}
644

            
645
#[inline]
646
87903
#[must_use] pub const fn style_text_align_to_u8(v: StyleTextAlign) -> u8 {
647
    // `start` encodes to 0 (the CSS-initial / zero-baseline code); `left` takes 4.
648
87903
    match v {
649
404
        StyleTextAlign::Start => 0,
650
84315
        StyleTextAlign::Center => 1,
651
158
        StyleTextAlign::Right => 2,
652
14
        StyleTextAlign::Justify => 3,
653
3004
        StyleTextAlign::Left => 4,
654
8
        StyleTextAlign::End => 5,
655
    }
656
87903
}
657

            
658
#[inline]
659
2493504
#[must_use] pub const fn style_visibility_from_u8(v: u8) -> StyleVisibility {
660
2493504
    match v {
661
2490175
        0 => StyleVisibility::Visible,
662
1000
        1 => StyleVisibility::Hidden,
663
1070
        2 => StyleVisibility::Collapse,
664
1259
        _ => StyleVisibility::Visible,
665
    }
666
2493504
}
667

            
668
#[inline]
669
437
#[must_use] pub const fn style_visibility_to_u8(v: StyleVisibility) -> u8 {
670
437
    match v {
671
406
        StyleVisibility::Visible => 0,
672
23
        StyleVisibility::Hidden => 1,
673
8
        StyleVisibility::Collapse => 2,
674
    }
675
437
}
676

            
677
#[inline]
678
643898
#[must_use] pub const fn style_white_space_from_u8(v: u8) -> StyleWhiteSpace {
679
643898
    match v {
680
639319
        0 => StyleWhiteSpace::Normal,
681
950
        1 => StyleWhiteSpace::Pre,
682
657
        2 => StyleWhiteSpace::Nowrap,
683
555
        3 => StyleWhiteSpace::PreWrap,
684
626
        4 => StyleWhiteSpace::PreLine,
685
502
        5 => StyleWhiteSpace::BreakSpaces,
686
1289
        _ => StyleWhiteSpace::Normal,
687
    }
688
643898
}
689

            
690
#[inline]
691
4035
#[must_use] pub const fn style_white_space_to_u8(v: StyleWhiteSpace) -> u8 {
692
4035
    match v {
693
514
        StyleWhiteSpace::Normal => 0,
694
783
        StyleWhiteSpace::Pre => 1,
695
170
        StyleWhiteSpace::Nowrap => 2,
696
2486
        StyleWhiteSpace::PreWrap => 3,
697
62
        StyleWhiteSpace::PreLine => 4,
698
20
        StyleWhiteSpace::BreakSpaces => 5,
699
    }
700
4035
}
701

            
702
#[inline]
703
646076
#[must_use] pub const fn style_direction_from_u8(v: u8) -> StyleDirection {
704
646076
    match v {
705
643617
        0 => StyleDirection::Ltr,
706
2205
        1 => StyleDirection::Rtl,
707
254
        _ => StyleDirection::Ltr,
708
    }
709
646076
}
710

            
711
#[inline]
712
451
#[must_use] pub const fn style_direction_to_u8(v: StyleDirection) -> u8 {
713
451
    match v {
714
406
        StyleDirection::Ltr => 0,
715
45
        StyleDirection::Rtl => 1,
716
    }
717
451
}
718

            
719
#[inline]
720
4397
#[must_use] pub const fn style_vertical_align_from_u8(v: u8) -> StyleVerticalAlign {
721
4397
    match v {
722
545
        0 => StyleVerticalAlign::Baseline,
723
490
        1 => StyleVerticalAlign::Top,
724
497
        2 => StyleVerticalAlign::Middle,
725
535
        3 => StyleVerticalAlign::Bottom,
726
538
        4 => StyleVerticalAlign::Sub,
727
504
        5 => StyleVerticalAlign::Superscript,
728
530
        6 => StyleVerticalAlign::TextTop,
729
510
        7 => StyleVerticalAlign::TextBottom,
730
248
        _ => StyleVerticalAlign::Baseline,
731
    }
732
4397
}
733

            
734
#[inline]
735
2299
#[must_use] pub const fn style_vertical_align_to_u8(v: StyleVerticalAlign) -> u8 {
736
2299
    match v {
737
405
        StyleVerticalAlign::Baseline => 0,
738
2
        StyleVerticalAlign::Top => 1,
739
1839
        StyleVerticalAlign::Middle => 2,
740
2
        StyleVerticalAlign::Bottom => 3,
741
14
        StyleVerticalAlign::Sub => 4,
742
14
        StyleVerticalAlign::Superscript => 5,
743
2
        StyleVerticalAlign::TextTop => 6,
744
8
        StyleVerticalAlign::TextBottom => 7,
745
        // Percentage/Length cannot be stored in the 3-bit compact cache field;
746
        // fall back to 0 (Baseline). Callers must use the slow cascade path
747
        // for vertical-align values with length/percentage units.
748
13
        StyleVerticalAlign::Percentage(_) | StyleVerticalAlign::Length(_) => 0,
749
    }
750
2299
}
751

            
752
#[inline]
753
6240
#[must_use] pub const fn border_collapse_from_u8(v: u8) -> StyleBorderCollapse {
754
6240
    match v {
755
3718
        0 => StyleBorderCollapse::Separate,
756
2268
        1 => StyleBorderCollapse::Collapse,
757
254
        _ => StyleBorderCollapse::Separate,
758
    }
759
6240
}
760

            
761
#[inline]
762
503
#[must_use] pub const fn border_collapse_to_u8(v: StyleBorderCollapse) -> u8 {
763
503
    match v {
764
405
        StyleBorderCollapse::Separate => 0,
765
98
        StyleBorderCollapse::Collapse => 1,
766
    }
767
503
}
768

            
769
#[inline]
770
3295757
#[must_use] pub const fn border_style_from_u8(v: u8) -> BorderStyle {
771
3295757
    match v {
772
2862654
        0 => BorderStyle::None,
773
285839
        1 => BorderStyle::Solid,
774
12280
        2 => BorderStyle::Double,
775
12280
        3 => BorderStyle::Dotted,
776
12280
        4 => BorderStyle::Dashed,
777
12240
        5 => BorderStyle::Hidden,
778
12192
        6 => BorderStyle::Groove,
779
12193
        7 => BorderStyle::Ridge,
780
12192
        8 => BorderStyle::Inset,
781
12194
        9 => BorderStyle::Outset,
782
49413
        _ => BorderStyle::None,
783
    }
784
3295757
}
785

            
786
#[inline]
787
620226
#[must_use] pub const fn border_style_to_u8(v: BorderStyle) -> u8 {
788
620226
    match v {
789
5527
        BorderStyle::None => 0,
790
569353
        BorderStyle::Solid => 1,
791
4089
        BorderStyle::Double => 2,
792
4049
        BorderStyle::Dotted => 3,
793
4049
        BorderStyle::Dashed => 4,
794
4049
        BorderStyle::Hidden => 5,
795
4001
        BorderStyle::Groove => 6,
796
4001
        BorderStyle::Ridge => 7,
797
17105
        BorderStyle::Inset => 8,
798
4003
        BorderStyle::Outset => 9,
799
    }
800
620226
}
801

            
802
/// Encode 4 border styles into a u16: [3:0]=top, [7:4]=right, [11:8]=bottom, [15:12]=left
803
#[inline]
804
10382
#[must_use] pub const fn encode_border_styles_packed(top: BorderStyle, right: BorderStyle, bottom: BorderStyle, left: BorderStyle) -> u16 {
805
10382
    (border_style_to_u8(top) as u16)
806
10382
        | ((border_style_to_u8(right) as u16) << 4)
807
10382
        | ((border_style_to_u8(bottom) as u16) << 8)
808
10382
        | ((border_style_to_u8(left) as u16) << 12)
809
10382
}
810

            
811
/// Decode border-top-style from packed u16
812
#[inline]
813
856647
#[must_use] pub const fn decode_border_top_style(packed: u16) -> BorderStyle {
814
856647
    border_style_from_u8((packed & 0x0F) as u8)
815
856647
}
816

            
817
/// Decode border-right-style from packed u16
818
#[inline]
819
791109
#[must_use] pub const fn decode_border_right_style(packed: u16) -> BorderStyle {
820
791109
    border_style_from_u8(((packed >> 4) & 0x0F) as u8)
821
791109
}
822

            
823
/// Decode border-bottom-style from packed u16
824
#[inline]
825
791109
#[must_use] pub const fn decode_border_bottom_style(packed: u16) -> BorderStyle {
826
791109
    border_style_from_u8(((packed >> 8) & 0x0F) as u8)
827
791109
}
828

            
829
/// Decode border-left-style from packed u16
830
#[inline]
831
856646
#[must_use] pub const fn decode_border_left_style(packed: u16) -> BorderStyle {
832
856646
    border_style_from_u8(((packed >> 12) & 0x0F) as u8)
833
856646
}
834

            
835
/// Encode a `ColorU` as u32 (0xRRGGBBAA). Returns 0 for sentinel/unset.
836
#[inline]
837
634437
#[must_use] pub const fn encode_color_u32(c: &ColorU) -> u32 {
838
634437
    ((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | (c.a as u32)
839
634437
}
840

            
841
/// Decode a u32 back to `ColorU`. Returns `None` if sentinel (`0x00000000`).
842
///
843
/// **Limitation:** `rgba(0,0,0,0)` (fully transparent black) also encodes as
844
/// `0x00000000` and will be decoded as `None` (unset). This is acceptable
845
/// because fully transparent black is visually indistinguishable from unset.
846
#[inline]
847
633
#[must_use] pub const fn decode_color_u32(v: u32) -> Option<ColorU> {
848
633
    if v == 0 { return None; }
849
628
    Some(ColorU {
850
628
        r: ((v >> 24) & 0xFF) as u8,
851
628
        g: ((v >> 16) & 0xFF) as u8,
852
628
        b: ((v >> 8) & 0xFF) as u8,
853
628
        a: (v & 0xFF) as u8,
854
628
    })
855
633
}
856

            
857
// =============================================================================
858
// Tier 1: Encode / Decode
859
// =============================================================================
860

            
861
/// Pack all 21 enum properties into a single u64.
862
#[inline]
863
409
#[must_use] pub const fn encode_tier1(
864
409
    display: LayoutDisplay,
865
409
    position: LayoutPosition,
866
409
    float: LayoutFloat,
867
409
    overflow_x: LayoutOverflow,
868
409
    overflow_y: LayoutOverflow,
869
409
    box_sizing: LayoutBoxSizing,
870
409
    flex_direction: LayoutFlexDirection,
871
409
    flex_wrap: LayoutFlexWrap,
872
409
    justify_content: LayoutJustifyContent,
873
409
    align_items: LayoutAlignItems,
874
409
    align_content: LayoutAlignContent,
875
409
    writing_mode: LayoutWritingMode,
876
409
    clear: LayoutClear,
877
409
    font_weight: StyleFontWeight,
878
409
    font_style: StyleFontStyle,
879
409
    text_align: StyleTextAlign,
880
409
    visibility: StyleVisibility,
881
409
    white_space: StyleWhiteSpace,
882
409
    direction: StyleDirection,
883
409
    vertical_align: StyleVerticalAlign,
884
409
    border_collapse: StyleBorderCollapse,
885
409
) -> u64 {
886
409
    let mut v: u64 = TIER1_POPULATED_BIT;
887
409
    v |= (layout_display_to_u8(display) as u64) << DISPLAY_SHIFT;
888
409
    v |= (layout_position_to_u8(position) as u64) << POSITION_SHIFT;
889
409
    v |= (layout_float_to_u8(float) as u64) << FLOAT_SHIFT;
890
409
    v |= (layout_overflow_to_u8(overflow_x) as u64) << OVERFLOW_X_SHIFT;
891
409
    v |= (layout_overflow_to_u8(overflow_y) as u64) << OVERFLOW_Y_SHIFT;
892
409
    v |= (layout_box_sizing_to_u8(box_sizing) as u64) << BOX_SIZING_SHIFT;
893
409
    v |= (layout_flex_direction_to_u8(flex_direction) as u64) << FLEX_DIRECTION_SHIFT;
894
409
    v |= (layout_flex_wrap_to_u8(flex_wrap) as u64) << FLEX_WRAP_SHIFT;
895
409
    v |= (layout_justify_content_to_u8(justify_content) as u64) << JUSTIFY_CONTENT_SHIFT;
896
409
    v |= (layout_align_items_to_u8(align_items) as u64) << ALIGN_ITEMS_SHIFT;
897
409
    v |= (layout_align_content_to_u8(align_content) as u64) << ALIGN_CONTENT_SHIFT;
898
409
    v |= (layout_writing_mode_to_u8(writing_mode) as u64) << WRITING_MODE_SHIFT;
899
409
    v |= (layout_clear_to_u8(clear) as u64) << CLEAR_SHIFT;
900
409
    v |= (style_font_weight_to_u8(font_weight) as u64) << FONT_WEIGHT_SHIFT;
901
409
    v |= (style_font_style_to_u8(font_style) as u64) << FONT_STYLE_SHIFT;
902
409
    v |= (style_text_align_to_u8(text_align) as u64) << TEXT_ALIGN_SHIFT;
903
409
    v |= (style_visibility_to_u8(visibility) as u64) << VISIBILITY_SHIFT;
904
409
    v |= (style_white_space_to_u8(white_space) as u64) << WHITE_SPACE_SHIFT;
905
409
    v |= (style_direction_to_u8(direction) as u64) << DIRECTION_SHIFT;
906
409
    v |= (style_vertical_align_to_u8(vertical_align) as u64) << VERTICAL_ALIGN_SHIFT;
907
409
    v |= (border_collapse_to_u8(border_collapse) as u64) << BORDER_COLLAPSE_SHIFT;
908
409
    v
909
409
}
910

            
911
// Decode individual enum properties from a Tier 1 u64.
912
// Each function is `#[inline]` for zero-cost extraction.
913

            
914
#[inline]
915
7317455
#[must_use] pub const fn decode_display(t1: u64) -> LayoutDisplay {
916
7317455
    layout_display_from_u8(((t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8)
917
7317455
}
918

            
919
#[inline]
920
3608967
#[must_use] pub const fn decode_position(t1: u64) -> LayoutPosition {
921
3608967
    layout_position_from_u8(((t1 >> POSITION_SHIFT) & POSITION_MASK) as u8)
922
3608967
}
923

            
924
#[inline]
925
1516843
#[must_use] pub const fn decode_float(t1: u64) -> LayoutFloat {
926
1516843
    layout_float_from_u8(((t1 >> FLOAT_SHIFT) & FLOAT_MASK) as u8)
927
1516843
}
928

            
929
#[inline]
930
2885419
#[must_use] pub const fn decode_overflow_x(t1: u64) -> LayoutOverflow {
931
2885419
    layout_overflow_from_u8(((t1 >> OVERFLOW_X_SHIFT) & OVERFLOW_MASK) as u8)
932
2885419
}
933

            
934
#[inline]
935
2713324
#[must_use] pub const fn decode_overflow_y(t1: u64) -> LayoutOverflow {
936
2713324
    layout_overflow_from_u8(((t1 >> OVERFLOW_Y_SHIFT) & OVERFLOW_MASK) as u8)
937
2713324
}
938

            
939
#[inline]
940
419516
#[must_use] pub const fn decode_box_sizing(t1: u64) -> LayoutBoxSizing {
941
419516
    layout_box_sizing_from_u8(((t1 >> BOX_SIZING_SHIFT) & BOX_SIZING_MASK) as u8)
942
419516
}
943

            
944
#[inline]
945
210406
#[must_use] pub const fn decode_flex_direction(t1: u64) -> LayoutFlexDirection {
946
210406
    layout_flex_direction_from_u8(((t1 >> FLEX_DIRECTION_SHIFT) & FLEX_DIR_MASK) as u8)
947
210406
}
948

            
949
#[inline]
950
142646
#[must_use] pub const fn decode_flex_wrap(t1: u64) -> LayoutFlexWrap {
951
142646
    layout_flex_wrap_from_u8(((t1 >> FLEX_WRAP_SHIFT) & FLEX_WRAP_MASK) as u8)
952
142646
}
953

            
954
#[inline]
955
4134
#[must_use] pub const fn decode_justify_content(t1: u64) -> LayoutJustifyContent {
956
4134
    layout_justify_content_from_u8(((t1 >> JUSTIFY_CONTENT_SHIFT) & JUSTIFY_MASK) as u8)
957
4134
}
958

            
959
#[inline]
960
142646
#[must_use] pub const fn decode_align_items(t1: u64) -> LayoutAlignItems {
961
142646
    layout_align_items_from_u8(((t1 >> ALIGN_ITEMS_SHIFT) & ALIGN_MASK) as u8)
962
142646
}
963

            
964
#[inline]
965
142646
#[must_use] pub const fn decode_align_content(t1: u64) -> LayoutAlignContent {
966
142646
    layout_align_content_from_u8(((t1 >> ALIGN_CONTENT_SHIFT) & ALIGN_MASK) as u8)
967
142646
}
968

            
969
#[inline]
970
1020754
#[must_use] pub const fn decode_writing_mode(t1: u64) -> LayoutWritingMode {
971
1020754
    layout_writing_mode_from_u8(((t1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8)
972
1020754
}
973

            
974
#[inline]
975
52523
#[must_use] pub const fn decode_clear(t1: u64) -> LayoutClear {
976
52523
    layout_clear_from_u8(((t1 >> CLEAR_SHIFT) & CLEAR_MASK) as u8)
977
52523
}
978

            
979
#[inline]
980
163117
#[must_use] pub const fn decode_font_weight(t1: u64) -> StyleFontWeight {
981
163117
    style_font_weight_from_u8(((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8)
982
163117
}
983

            
984
#[inline]
985
163117
#[must_use] pub const fn decode_font_style(t1: u64) -> StyleFontStyle {
986
163117
    style_font_style_from_u8(((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8)
987
163117
}
988

            
989
#[inline]
990
864961
#[must_use] pub const fn decode_text_align(t1: u64) -> StyleTextAlign {
991
864961
    style_text_align_from_u8(((t1 >> TEXT_ALIGN_SHIFT) & TEXT_ALIGN_MASK) as u8)
992
864961
}
993

            
994
#[inline]
995
2493247
#[must_use] pub const fn decode_visibility(t1: u64) -> StyleVisibility {
996
2493247
    style_visibility_from_u8(((t1 >> VISIBILITY_SHIFT) & VISIBILITY_MASK) as u8)
997
2493247
}
998

            
999
#[inline]
643641
#[must_use] pub const fn decode_white_space(t1: u64) -> StyleWhiteSpace {
643641
    style_white_space_from_u8(((t1 >> WHITE_SPACE_SHIFT) & WHITE_SPACE_MASK) as u8)
643641
}
#[inline]
645819
#[must_use] pub const fn decode_direction(t1: u64) -> StyleDirection {
645819
    style_direction_from_u8(((t1 >> DIRECTION_SHIFT) & DIRECTION_MASK) as u8)
645819
}
#[inline]
4135
#[must_use] pub const fn decode_vertical_align(t1: u64) -> StyleVerticalAlign {
4135
    style_vertical_align_from_u8(((t1 >> VERTICAL_ALIGN_SHIFT) & VERTICAL_ALIGN_MASK) as u8)
4135
}
#[inline]
5983
#[must_use] pub const fn decode_border_collapse(t1: u64) -> StyleBorderCollapse {
5983
    border_collapse_from_u8(((t1 >> BORDER_COLLAPSE_SHIFT) & BORDER_COLLAPSE_MASK) as u8)
5983
}
/// Returns true if the tier1 u64 was actually populated by `encode_tier1`.
#[inline]
#[cfg(test)]
4102
#[must_use] pub const fn tier1_is_populated(t1: u64) -> bool {
4102
    (t1 & TIER1_POPULATED_BIT) != 0
4102
}
// =============================================================================
// Tier 2: CompactNodeProps — numeric dimensions (64 bytes/node)
// =============================================================================
/// u32 encoding for dimension properties (width, height, min-*, max-*, flex-basis, font-size).
///
/// Layout: `[3:0] SizeMetric (4 bits) | [31:4] signed fixed-point ×1000 (28 bits)`
///
/// This matches `FloatValue`'s internal representation (isize × 1000).
/// Range: ±134,217.727 at 0.001 precision (28-bit signed = ±2^27 = ±134,217,728 / 1000).
///
/// Sentinel values use the top of the u32 range (0xFFFFFFF9..0xFFFFFFFF).
///
/// Encode a `PixelValue` into u32 with `SizeMetric`. Returns `U32_SENTINEL` if out of range.
#[inline]
677457
#[must_use] pub fn encode_pixel_value_u32(pv: &PixelValue) -> u32 {
677457
    let metric = u32::from(size_metric_to_u8(pv.metric));
677457
    let raw = pv.number.number; // already × 1000 (FloatValue internal repr)
    // 28-bit signed range: -134_217_728 ..= +134_217_727
677457
    if !(-134_217_728..=134_217_727).contains(&raw) {
504
        return U32_SENTINEL; // overflow → tier 3
676953
    }
    // Pack: low 4 bits = metric, upper 28 bits = value (as unsigned offset)
    // raw is range-checked to 28 bits above; reinterpret its low 32 bits for packing.
676953
    let value_bits = i32::try_from(raw).unwrap_or(0).cast_unsigned() << 4;
676953
    let packed = value_bits | metric;
    // A legitimate small NEGATIVE value with a high metric nibble (e.g. -1 in
    // vh/vmin/vmax packs to 0xFFFF_FFF9/FA/FB) lands in the reserved sentinel band
    // [U32_SENTINEL_THRESHOLD, U32_SENTINEL] and decode would misread it as an unset
    // sentinel. Escape to tier 3 so the real value is stored losslessly instead.
676953
    if packed >= U32_SENTINEL_THRESHOLD {
66
        return U32_SENTINEL;
676887
    }
676887
    packed
677457
}
/// Decode a u32 back to `PixelValue`. Returns None for sentinel values.
#[inline]
1190109
#[must_use] pub const fn decode_pixel_value_u32(encoded: u32) -> Option<PixelValue> {
1190109
    if encoded >= U32_SENTINEL_THRESHOLD {
148
        return None; // sentinel
1189961
    }
1189961
    let metric = size_metric_from_u8((encoded & 0xF) as u8);
    // Cast to i32 FIRST, then arithmetic right-shift to preserve sign bit
1189961
    let value_bits = encoded.cast_signed() >> 4;
1189961
    let raw = value_bits as isize; // × 1000
1189961
    Some(PixelValue {
1189961
        metric,
1189961
        number: FloatValue { number: raw },
1189961
    })
1190109
}
/// Encode an i16 resolved px value (×10). Returns `I16_SENTINEL` if out of range.
/// Range: -3276.8 ..= +3276.3 px at 0.1px precision.
#[inline]
2153111
#[must_use] pub fn encode_resolved_px_i16(px: f32) -> i16 {
2153111
    let scaled = crate::cast::f32_to_i32((px * 10.0).round());
2153111
    if scaled < -32768 || scaled > i32::from(I16_SENTINEL_THRESHOLD) - 1 {
358
        return I16_SENTINEL; // overflow or too large → tier 3
2152753
    }
2152753
    i16::try_from(scaled).unwrap_or(I16_SENTINEL)
2153111
}
/// Decode an i16 back to resolved px. Returns None for sentinel values.
#[inline]
275229
#[must_use] pub fn decode_resolved_px_i16(v: i16) -> Option<f32> {
275229
    if v >= I16_SENTINEL_THRESHOLD {
64790
        return None;
210439
    }
210439
    Some(f32::from(v) / 10.0)
275229
}
/// Encode a u16 flex value (×100). Returns `U16_SENTINEL` if out of range.
/// Range: 0.00 ..= 655.27 at 0.01 precision.
#[inline]
755792
#[must_use] pub fn encode_flex_u16(value: f32) -> u16 {
755792
    let scaled = crate::cast::f32_to_i32((value * 100.0).round());
755792
    if scaled < 0 || scaled >= i32::from(U16_SENTINEL_THRESHOLD) {
49
        return U16_SENTINEL;
755743
    }
755743
    u16::try_from(scaled).unwrap_or(U16_SENTINEL)
755792
}
/// Decode a u16 flex value back to f32. Returns None for sentinel values.
#[inline]
342569
#[must_use] pub fn decode_flex_u16(v: u16) -> Option<f32> {
342569
    if v >= U16_SENTINEL_THRESHOLD {
7
        return None;
342562
    }
342562
    Some(f32::from(v) / 100.0)
342569
}
/// `SizeMetric` → u8 (4 bits, 12 variants)
#[inline]
677469
#[must_use] pub const fn size_metric_to_u8(m: SizeMetric) -> u8 {
677469
    match m {
632323
        SizeMetric::Px => 0,
54
        SizeMetric::Pt => 1,
1662
        SizeMetric::Em => 2,
74
        SizeMetric::Rem => 3,
14
        SizeMetric::In => 4,
14
        SizeMetric::Cm => 5,
14
        SizeMetric::Mm => 6,
42895
        SizeMetric::Percent => 7,
314
        SizeMetric::Vw => 8,
35
        SizeMetric::Vh => 9,
35
        SizeMetric::Vmin => 10,
35
        SizeMetric::Vmax => 11,
    }
677469
}
/// u8 → `SizeMetric`
#[inline]
1190205
#[must_use] pub const fn size_metric_from_u8(v: u8) -> SizeMetric {
1190205
    match v {
1166093
        0 => SizeMetric::Px,
47
        1 => SizeMetric::Pt,
443
        2 => SizeMetric::Em,
67
        3 => SizeMetric::Rem,
7
        4 => SizeMetric::In,
7
        5 => SizeMetric::Cm,
7
        6 => SizeMetric::Mm,
23224
        7 => SizeMetric::Percent,
44
        8 => SizeMetric::Vw,
6
        9 => SizeMetric::Vh,
6
        10 => SizeMetric::Vmin,
6
        11 => SizeMetric::Vmax,
248
        _ => SizeMetric::Px,
    }
1190205
}
/// Layout-hot compact numeric properties for a single node (68 bytes).
/// Only fields accessed during the constraint-solving loop.
/// All dimensions use MSB-sentinel encoding.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct CompactNodeProps {
    // --- Dimensions needing unit (u32 MSB-sentinel) ---
    pub width: u32,
    pub height: u32,
    pub min_width: u32,
    pub max_width: u32,
    pub min_height: u32,
    pub max_height: u32,
    pub flex_basis: u32,
    pub font_size: u32,
    // --- Resolved px values (i16 MSB-sentinel, ×10) ---
    pub padding_top: i16,
    pub padding_right: i16,
    pub padding_bottom: i16,
    pub padding_left: i16,
    pub margin_top: i16,
    pub margin_right: i16,
    pub margin_bottom: i16,
    pub margin_left: i16,
    pub border_top_width: i16,
    pub border_right_width: i16,
    pub border_bottom_width: i16,
    pub border_left_width: i16,
    pub top: i16,
    pub right: i16,
    pub bottom: i16,
    pub left: i16,
    // --- Flex (u16 MSB-sentinel, ×100) ---
    pub flex_grow: u16,
    pub flex_shrink: u16,
    // --- Gap (i16 px×10, 0 = default) ---
    pub row_gap: i16,
    pub column_gap: i16,
}
/// Paint-cold compact properties for a single node.
/// Only accessed during display list generation, table layout, or text shaping.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct CompactNodePropsCold {
    // --- Border colors (u32 RGBA as 0xRRGGBBAA, 0 = unset sentinel) ---
    pub border_top_color: u32,
    pub border_right_color: u32,
    pub border_bottom_color: u32,
    pub border_left_color: u32,
    // --- Border radii (i16 px × 10, I16_SENTINEL = unset/default = 0) ---
    pub border_top_left_radius: i16,
    pub border_top_right_radius: i16,
    pub border_bottom_left_radius: i16,
    pub border_bottom_right_radius: i16,
    // --- Other ---
    pub z_index: i16,   // range ±32764, sentinel = 0x7FFF
    /// Border styles packed: [3:0]=top, [7:4]=right, [11:8]=bottom, [15:12]=left
    pub border_styles_packed: u16,
    pub border_spacing_h: i16,
    pub border_spacing_v: i16,
    pub tab_size: i16,
    /// Grid column start (`I16_AUTO` = auto, positive = line number, negative = span)
    pub grid_col_start: i16,
    /// Grid column end
    pub grid_col_end: i16,
    /// Grid row start
    pub grid_row_start: i16,
    /// Grid row end
    pub grid_row_end: i16,
    // --- GPU / hot paint props ---
    /// Opacity × 254 (0 = fully transparent, 254 = opaque). 255 = unset/default (= 1.0).
    pub opacity: u8,
    /// Bitflags for properties that are usually unset. Lets the getter
    /// short-circuit without a cascade walk when the value is the default.
    ///
    /// bit 0: `has_transform`                (slow-walk only when set)
    /// bit 1: `has_transform_origin`
    /// bit 2: `has_box_shadow`
    /// bit 3: `has_text_decoration`          (slow-walk only when set)
    /// bits 4-5: `scrollbar_gutter` (0 = auto default, 1 = stable, 2 = both-edges, 3 = mirror)
    /// bit 6: `has_background`                (slow-walk only when set; ≈ negative fast path)
    /// bit 7: `has_clip_path`                 (slow-walk only when set)
    pub hot_flags: u8,
    /// Second byte of flags for rarely-set properties.
    ///
    /// bit 0: `has_any_scrollbar_css`
    ///        OR of all -azul-scrollbar-* / scrollbar-color / scrollbar-width props.
    ///        When clear, `get_scrollbar_style` can skip 8 cascade walks and use
    ///        the UA-default result.
    /// bit 1: `has_counter`      (counter-reset OR counter-increment)
    /// bit 2: `has_break`        (break-before OR break-after)
    /// bit 3: `has_text_orientation`
    /// bit 4: `has_text_shadow`
    /// bit 5: `has_backdrop_filter`
    /// bit 6: `has_filter`
    /// bit 7: `has_mix_blend_mode`
    pub extra_flags: u8,
}
pub const OPACITY_SENTINEL: u8 = 255;
pub const HOT_FLAG_HAS_TRANSFORM: u8 = 1 << 0;
pub const HOT_FLAG_HAS_TRANSFORM_ORIGIN: u8 = 1 << 1;
pub const HOT_FLAG_HAS_BOX_SHADOW: u8 = 1 << 2;
pub const HOT_FLAG_HAS_TEXT_DECORATION: u8 = 1 << 3;
pub const HOT_FLAG_SCROLLBAR_GUTTER_SHIFT: u8 = 4;
pub const HOT_FLAG_SCROLLBAR_GUTTER_MASK: u8 = 0b0011_0000;
pub const HOT_FLAG_HAS_BACKGROUND: u8 = 1 << 6;
pub const HOT_FLAG_HAS_CLIP_PATH: u8 = 1 << 7;
pub const EXTRA_FLAG_HAS_SCROLLBAR_CSS: u8 = 1 << 0;
pub const EXTRA_FLAG_HAS_COUNTER: u8 = 1 << 1;
pub const EXTRA_FLAG_HAS_BREAK: u8 = 1 << 2;
pub const EXTRA_FLAG_HAS_TEXT_ORIENTATION: u8 = 1 << 3;
pub const EXTRA_FLAG_HAS_TEXT_SHADOW: u8 = 1 << 4;
pub const EXTRA_FLAG_HAS_BACKDROP_FILTER: u8 = 1 << 5;
pub const EXTRA_FLAG_HAS_FILTER: u8 = 1 << 6;
pub const EXTRA_FLAG_HAS_MIX_BLEND_MODE: u8 = 1 << 7;
// ---- DOM-level rare text prop flags (stored on CompactLayoutCache) ----
// Each bit = "some node in this DOM declared this property".
// When clear, cascade walks for that prop anywhere in the DOM
// necessarily return None → callers can skip the walk and use
// the default value. Eliminates ~N × IFC-count walks per layout
// in typical pages where these props are never declared.
pub const DOM_HAS_SHAPE_INSIDE: u32 = 1 << 0;
pub const DOM_HAS_SHAPE_OUTSIDE: u32 = 1 << 1;
pub const DOM_HAS_TEXT_JUSTIFY: u32 = 1 << 2;
pub const DOM_HAS_TEXT_INDENT: u32 = 1 << 3;
pub const DOM_HAS_COLUMN_COUNT: u32 = 1 << 4;
pub const DOM_HAS_COLUMN_GAP: u32 = 1 << 5;
pub const DOM_HAS_INITIAL_LETTER: u32 = 1 << 6;
pub const DOM_HAS_INITIAL_LETTER_ALIGN: u32 = 1 << 7;
pub const DOM_HAS_LINE_CLAMP: u32 = 1 << 8;
pub const DOM_HAS_HANGING_PUNCTUATION: u32 = 1 << 9;
pub const DOM_HAS_TEXT_COMBINE_UPRIGHT: u32 = 1 << 10;
pub const DOM_HAS_EXCLUSION_MARGIN: u32 = 1 << 11;
pub const DOM_HAS_HYPHENATION_LANGUAGE: u32 = 1 << 12;
pub const DOM_HAS_UNICODE_BIDI: u32 = 1 << 13;
pub const DOM_HAS_TEXT_BOX_TRIM: u32 = 1 << 14;
pub const DOM_HAS_HYPHENS: u32 = 1 << 15;
pub const DOM_HAS_WORD_BREAK: u32 = 1 << 16;
pub const DOM_HAS_OVERFLOW_WRAP: u32 = 1 << 17;
pub const DOM_HAS_LINE_BREAK: u32 = 1 << 18;
pub const DOM_HAS_TEXT_ALIGN_LAST: u32 = 1 << 19;
pub const DOM_HAS_LINE_HEIGHT: u32 = 1 << 20;
pub const DOM_HAS_COLUMN_WIDTH: u32 = 1 << 21;
pub const DOM_HAS_SHAPE_MARGIN: u32 = 1 << 22;
pub const SCROLLBAR_GUTTER_AUTO: u8 = 0;
pub const SCROLLBAR_GUTTER_STABLE: u8 = 1;
pub const SCROLLBAR_GUTTER_BOTH_EDGES: u8 = 2;
pub const SCROLLBAR_GUTTER_MIRROR: u8 = 3;
impl Default for CompactNodeProps {
145021
    fn default() -> Self {
145021
        Self {
145021
            // All dimensions default to Auto
145021
            width: U32_AUTO,
145021
            height: U32_AUTO,
145021
            min_width: U32_AUTO,
145021
            max_width: U32_NONE,
145021
            min_height: U32_AUTO,
145021
            max_height: U32_NONE,
145021
            flex_basis: U32_AUTO,
145021
            font_size: U32_INITIAL,
145021
            // All resolved px default to 0
145021
            padding_top: 0,
145021
            padding_right: 0,
145021
            padding_bottom: 0,
145021
            padding_left: 0,
145021
            margin_top: 0,
145021
            margin_right: 0,
145021
            margin_bottom: 0,
145021
            margin_left: 0,
145021
            border_top_width: 0,
145021
            border_right_width: 0,
145021
            border_bottom_width: 0,
145021
            border_left_width: 0,
145021
            top: I16_AUTO,
145021
            right: I16_AUTO,
145021
            bottom: I16_AUTO,
145021
            left: I16_AUTO,
145021
            // Flex defaults
145021
            flex_grow: 0,
145021
            flex_shrink: encode_flex_u16(1.0), // CSS default: flex-shrink: 1
145021

            
145021
            // Gap defaults
145021
            row_gap: 0,
145021
            column_gap: 0,
145021
        }
145021
    }
}
impl Default for CompactNodePropsCold {
144769
    fn default() -> Self {
144769
        Self {
144769
            // Border colors default to 0 (sentinel/unset)
144769
            border_top_color: 0,
144769
            border_right_color: 0,
144769
            border_bottom_color: 0,
144769
            border_left_color: 0,
144769
            // Border radii: I16_SENTINEL means "no rounded corner" (skip slow walk)
144769
            border_top_left_radius: I16_SENTINEL,
144769
            border_top_right_radius: I16_SENTINEL,
144769
            border_bottom_left_radius: I16_SENTINEL,
144769
            border_bottom_right_radius: I16_SENTINEL,
144769
            // Other
144769
            z_index: I16_AUTO,
144769
            border_styles_packed: 0, // all BorderStyle::None
144769
            border_spacing_h: 0,
144769
            border_spacing_v: 0,
144769
            tab_size: I16_SENTINEL, // default is 8em, needs resolution → sentinel
144769
            grid_col_start: I16_AUTO,
144769
            grid_col_end: I16_AUTO,
144769
            grid_row_start: I16_AUTO,
144769
            grid_row_end: I16_AUTO,
144769
            opacity: OPACITY_SENTINEL,
144769
            hot_flags: 0,
144769
            extra_flags: 0,
144769
        }
144769
    }
}
// =============================================================================
// Tier 2b: CompactTextProps — IFC/text properties (24 bytes/node)
// =============================================================================
/// Compact text/IFC properties for a single node (24 bytes).
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct CompactTextProps {
    pub text_color: u32,       // RGBA as 0xRRGGBBAA (0 = transparent/unset)
    pub font_family_hash: u64, // FxHash of font-family list (0 = sentinel/unset)
    /// Split scale by SIGN (parser convention: negative normalized =
    /// absolute px): negative = -px x 10 (line-height: 40px -> -400),
    /// positive = unitless multiple x 1000 (1.2 / 120% -> 1200).
    /// `I16_SENTINEL` = unset ("normal").
    pub line_height: i16,
    pub letter_spacing: i16,   // px × 10
    pub word_spacing: i16,     // px × 10
    pub text_indent: i16,      // px × 10
}
impl Default for CompactTextProps {
144769
    fn default() -> Self {
144769
        Self {
144769
            text_color: 0,
144769
            font_family_hash: 0,
144769
            line_height: I16_SENTINEL, // "normal" → sentinel
144769
            letter_spacing: 0,
144769
            word_spacing: 0,
144769
            text_indent: 0,
144769
        }
144769
    }
}
// =============================================================================
// Tier 3: Overflow map — rare/complex properties
// =============================================================================
// Overflow properties that couldn't fit in Tier 1/2 encoding.
// Contains the original `CssProperty` values for properties that:
// - Have `calc()` expressions
// - Exceed the numeric range of compact encoding
// - Are rare CSS properties (grid, transforms, etc.)
// =============================================================================
// CompactLayoutCache — the top-level container
// =============================================================================
/// Three-tier compact layout property cache.
///
/// Allocated once per restyle, indexed by node index (same as `NodeId`).
/// Provides O(1) array-indexed access to all layout properties.
///
/// Non-compact properties (background, box-shadow, transform, etc.) are
/// resolved via the slow cascade path in `CssPropertyCache::get_property_slow()`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactLayoutCache {
    /// Whether ANY layout-relevant pixel value in this document uses a
    /// viewport-relative unit (vw/vh/vmin/vmax), detected while the build
    /// loop extracts those very values (text tier + box dimensions — the set
    /// that feeds inline collection and measurement).
    ///
    /// Consumers use it to SKIP viewport-based cache invalidation for the
    /// overwhelmingly common document that never mentions a viewport unit:
    /// solver3's inline-collection fingerprint folded `ctx.viewport_size`
    /// into EVERY IFC's key "because vw/vh", so every resize invalidated
    /// every collection (552 re-collections ≈ 19.6 ms per resize on big.md)
    /// to protect a feature the document did not use. False negatives are
    /// impossible for the covered set (the flag is set from the same values
    /// the cache encodes); a property OUTSIDE the covered set cannot reach
    /// inline collection, which is the only consumer.
    pub uses_viewport_units: bool,
    /// Tier 1: ALL enum properties bitpacked into u64 per node (8 B/node)
    pub tier1_enums: Vec<u64>,
    /// Tier 2 hot: Layout-critical numeric dimensions per node (68 B/node)
    pub tier2_dims: Vec<CompactNodeProps>,
    /// Tier 2 cold: Paint-only properties per node (28 B/node)
    pub tier2_cold: Vec<CompactNodePropsCold>,
    /// Tier 2b: Text/IFC properties per node (24 B/node)
    pub tier2b_text: Vec<CompactTextProps>,
    /// Indices of nodes whose `font_family_hash` changed since the last frame.
    ///
    /// Enables **per-node** font dirty tracking instead of the global all-or-nothing
    /// `font_stacks_hash` XOR approach. When this list is non-empty, only the
    /// font chains for these specific nodes need to be re-resolved, avoiding O(N)
    /// re-resolution when a single node's `font-family` changes.
    ///
    /// Populated during `build_compact_cache()` by comparing each node's
    /// `font_family_hash` against `prev_font_hashes`.
    pub font_dirty_nodes: Vec<usize>,
    /// Previous frame's per-node `font_family_hash` values.
    ///
    /// Stored after each compact cache build so that the next build can detect
    /// which specific nodes' font-family changed (rather than relying on a
    /// collision-prone global XOR hash).
    pub prev_font_hashes: Vec<u64>,
    /// Reverse map: `font_family_hash` (u64) → actual `StyleFontFamilyVec`.
    ///
    /// Populated during `build_compact_cache()` as a byproduct of hash computation.
    /// Consumers use this to look up font family names from the compact cache hash
    /// without going through `get_property_slow()` (which fails for inherited values
    /// on text nodes).
    pub font_hash_to_families: alloc::collections::BTreeMap<u64, crate::props::basic::font::StyleFontFamilyVec>,
    /// True when any node's inline style carries a NON-pseudo dynamic
    /// condition (viewport/@media, theme, OS, container...). The window uses
    /// this to decide whether a `DynamicSelectorContext` change (a resize
    /// crossing a breakpoint, a theme flip) requires rebuilding this cache:
    /// for the overwhelmingly common condition-free DOM the answer is a
    /// single bool read.
    pub has_dynamic_conditions: bool,
    /// Bitfield tracking which rare text props are declared *anywhere* in the DOM.
    /// Built once during `build_compact_cache_with_inheritance`. When a bit is
    /// clear, callers (e.g. `translate_to_text3_constraints`) can skip the
    /// cascade walk for that property — its slow path would always return
    /// `None` and fall back to the default. See `DOM_HAS_*` constants.
    pub dom_declared_flags: u32,
    /// Viewport WIDTH thresholds harvested from every inline conditional
    /// property in the DOM — where a `ViewportWidth` condition can flip.
    /// Stored as `f32::to_bits` (this struct derives `Eq`); sorted +
    /// deduped. Together with the author stylesheet's
    /// `Css::viewport_breakpoints`, these replace the old hardcoded
    /// breakpoint guess list in the engine's resize decision (a widget
    /// breakpoint like the ribbon's 720px was invisible to that list, so
    /// shrinking onto the mobile layout never regenerated).
    pub inline_viewport_w: Vec<u32>,
    /// Viewport HEIGHT thresholds, same contract as `inline_viewport_w`.
    pub inline_viewport_h: Vec<u32>,
}
impl CompactLayoutCache {
    /// Create an empty cache (no nodes).
3
    #[must_use] pub const fn empty() -> Self {
3
        Self {
3
            uses_viewport_units: false,
3
            tier1_enums: Vec::new(),
3
            tier2_dims: Vec::new(),
3
            tier2_cold: Vec::new(),
3
            tier2b_text: Vec::new(),
3
            font_dirty_nodes: Vec::new(),
3
            prev_font_hashes: Vec::new(),
3
            font_hash_to_families: alloc::collections::BTreeMap::new(),
3
            dom_declared_flags: 0,
3
            has_dynamic_conditions: false,
3
            inline_viewport_w: Vec::new(),
3
            inline_viewport_h: Vec::new(),
3
        }
3
    }
    /// Create a cache pre-allocated for `node_count` nodes, filled with defaults.
71980
    #[must_use] pub fn with_capacity(node_count: usize) -> Self {
71980
        Self {
71980
            uses_viewport_units: false,
71980
            tier1_enums: vec![0u64; node_count],
71980
            tier2_dims: vec![CompactNodeProps::default(); node_count],
71980
            tier2_cold: vec![CompactNodePropsCold::default(); node_count],
71980
            tier2b_text: vec![CompactTextProps::default(); node_count],
71980
            font_dirty_nodes: Vec::new(),
71980
            prev_font_hashes: vec![0u64; node_count],
71980
            font_hash_to_families: alloc::collections::BTreeMap::new(),
71980
            dom_declared_flags: 0,
71980
            has_dynamic_conditions: false,
71980
            inline_viewport_w: Vec::new(),
71980
            inline_viewport_h: Vec::new(),
71980
        }
71980
    }
    /// Number of nodes in this cache.
    #[inline]
17
    #[must_use] pub const fn node_count(&self) -> usize {
17
        self.tier1_enums.len()
17
    }
    // -- Tier 1 getters (enum properties) --
    #[inline]
7313324
    #[must_use] pub fn get_display(&self, node_idx: usize) -> LayoutDisplay {
7313324
        decode_display(self.tier1_enums[node_idx])
7313324
    }
    #[inline]
3604835
    #[must_use] pub fn get_position(&self, node_idx: usize) -> LayoutPosition {
3604835
        decode_position(self.tier1_enums[node_idx])
3604835
    }
    #[inline]
1512712
    #[must_use] pub fn get_float(&self, node_idx: usize) -> LayoutFloat {
1512712
        decode_float(self.tier1_enums[node_idx])
1512712
    }
    #[inline]
2881288
    #[must_use] pub fn get_overflow_x(&self, node_idx: usize) -> LayoutOverflow {
2881288
        decode_overflow_x(self.tier1_enums[node_idx])
2881288
    }
    #[inline]
2709193
    #[must_use] pub fn get_overflow_y(&self, node_idx: usize) -> LayoutOverflow {
2709193
        decode_overflow_y(self.tier1_enums[node_idx])
2709193
    }
    #[inline]
415385
    #[must_use] pub fn get_box_sizing(&self, node_idx: usize) -> LayoutBoxSizing {
415385
        decode_box_sizing(self.tier1_enums[node_idx])
415385
    }
    #[inline]
206275
    #[must_use] pub fn get_flex_direction(&self, node_idx: usize) -> LayoutFlexDirection {
206275
        decode_flex_direction(self.tier1_enums[node_idx])
206275
    }
    #[inline]
138515
    #[must_use] pub fn get_flex_wrap(&self, node_idx: usize) -> LayoutFlexWrap {
138515
        decode_flex_wrap(self.tier1_enums[node_idx])
138515
    }
    #[inline]
3
    #[must_use] pub fn get_justify_content(&self, node_idx: usize) -> LayoutJustifyContent {
3
        decode_justify_content(self.tier1_enums[node_idx])
3
    }
    #[inline]
138515
    #[must_use] pub fn get_align_items(&self, node_idx: usize) -> LayoutAlignItems {
138515
        decode_align_items(self.tier1_enums[node_idx])
138515
    }
    #[inline]
138515
    #[must_use] pub fn get_align_content(&self, node_idx: usize) -> LayoutAlignContent {
138515
        decode_align_content(self.tier1_enums[node_idx])
138515
    }
    #[inline]
1016623
    #[must_use] pub fn get_writing_mode(&self, node_idx: usize) -> LayoutWritingMode {
1016623
        decode_writing_mode(self.tier1_enums[node_idx])
1016623
    }
    #[inline]
48392
    #[must_use] pub fn get_clear(&self, node_idx: usize) -> LayoutClear {
48392
        decode_clear(self.tier1_enums[node_idx])
48392
    }
    #[inline]
158986
    #[must_use] pub fn get_font_weight(&self, node_idx: usize) -> StyleFontWeight {
158986
        decode_font_weight(self.tier1_enums[node_idx])
158986
    }
    #[inline]
158986
    #[must_use] pub fn get_font_style(&self, node_idx: usize) -> StyleFontStyle {
158986
        decode_font_style(self.tier1_enums[node_idx])
158986
    }
    #[inline]
860830
    #[must_use] pub fn get_text_align(&self, node_idx: usize) -> StyleTextAlign {
860830
        decode_text_align(self.tier1_enums[node_idx])
860830
    }
    #[inline]
2489116
    #[must_use] pub fn get_visibility(&self, node_idx: usize) -> StyleVisibility {
2489116
        decode_visibility(self.tier1_enums[node_idx])
2489116
    }
    #[inline]
639510
    #[must_use] pub fn get_white_space(&self, node_idx: usize) -> StyleWhiteSpace {
639510
        decode_white_space(self.tier1_enums[node_idx])
639510
    }
    #[inline]
641688
    #[must_use] pub fn get_direction(&self, node_idx: usize) -> StyleDirection {
641688
        decode_direction(self.tier1_enums[node_idx])
641688
    }
    #[inline]
3
    #[must_use] pub fn get_vertical_align(&self, node_idx: usize) -> StyleVerticalAlign {
3
        decode_vertical_align(self.tier1_enums[node_idx])
3
    }
    #[inline]
1851
    #[must_use] pub fn get_border_collapse(&self, node_idx: usize) -> StyleBorderCollapse {
1851
        decode_border_collapse(self.tier1_enums[node_idx])
1851
    }
    // -- Tier 2 getters (numeric dimensions) --
    /// Get width as encoded u32 (use `decode_pixel_value_u32` or check sentinel).
    #[inline]
1052289
    #[must_use] pub fn get_width_raw(&self, node_idx: usize) -> u32 {
1052289
        self.tier2_dims[node_idx].width
1052289
    }
    #[inline]
1107857
    #[must_use] pub fn get_height_raw(&self, node_idx: usize) -> u32 {
1107857
        self.tier2_dims[node_idx].height
1107857
    }
    #[inline]
650642
    #[must_use] pub fn get_min_width_raw(&self, node_idx: usize) -> u32 {
650642
        self.tier2_dims[node_idx].min_width
650642
    }
    #[inline]
434943
    #[must_use] pub fn get_max_width_raw(&self, node_idx: usize) -> u32 {
434943
        self.tier2_dims[node_idx].max_width
434943
    }
    #[inline]
650543
    #[must_use] pub fn get_min_height_raw(&self, node_idx: usize) -> u32 {
650543
        self.tier2_dims[node_idx].min_height
650543
    }
    #[inline]
434844
    #[must_use] pub fn get_max_height_raw(&self, node_idx: usize) -> u32 {
434844
        self.tier2_dims[node_idx].max_height
434844
    }
    #[inline]
69889
    #[must_use] pub fn get_font_size_raw(&self, node_idx: usize) -> u32 {
69889
        self.tier2_dims[node_idx].font_size
69889
    }
    #[inline]
138515
    #[must_use] pub fn get_flex_basis_raw(&self, node_idx: usize) -> u32 {
138515
        self.tier2_dims[node_idx].flex_basis
138515
    }
    /// Get padding-top as resolved px. Returns None if sentinel (needs slow path).
    #[inline]
5
    #[must_use] pub fn get_padding_top(&self, node_idx: usize) -> Option<f32> {
5
        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_top)
5
    }
    #[inline]
3
    #[must_use] pub fn get_padding_right(&self, node_idx: usize) -> Option<f32> {
3
        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_right)
3
    }
    #[inline]
3
    #[must_use] pub fn get_padding_bottom(&self, node_idx: usize) -> Option<f32> {
3
        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_bottom)
3
    }
    #[inline]
3
    #[must_use] pub fn get_padding_left(&self, node_idx: usize) -> Option<f32> {
3
        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_left)
3
    }
    #[inline]
3
    #[must_use] pub fn get_margin_top(&self, node_idx: usize) -> Option<f32> {
3
        let v = self.tier2_dims[node_idx].margin_top;
3
        if v == I16_AUTO { return None; } // Auto for margin is special
3
        decode_resolved_px_i16(v)
3
    }
    #[inline]
    #[must_use] pub fn get_margin_right(&self, node_idx: usize) -> Option<f32> {
        let v = self.tier2_dims[node_idx].margin_right;
        if v == I16_AUTO { return None; }
        decode_resolved_px_i16(v)
    }
    #[inline]
    #[must_use] pub fn get_margin_bottom(&self, node_idx: usize) -> Option<f32> {
        let v = self.tier2_dims[node_idx].margin_bottom;
        if v == I16_AUTO { return None; }
        decode_resolved_px_i16(v)
    }
    #[inline]
3
    #[must_use] pub fn get_margin_left(&self, node_idx: usize) -> Option<f32> {
3
        let v = self.tier2_dims[node_idx].margin_left;
3
        if v == I16_AUTO { return None; }
3
        decode_resolved_px_i16(v)
3
    }
    /// Check if margin is Auto (important for centering logic).
    #[inline]
3
    #[must_use] pub fn is_margin_top_auto(&self, node_idx: usize) -> bool {
3
        self.tier2_dims[node_idx].margin_top == I16_AUTO
3
    }
    #[inline]
3
    #[must_use] pub fn is_margin_right_auto(&self, node_idx: usize) -> bool {
3
        self.tier2_dims[node_idx].margin_right == I16_AUTO
3
    }
    #[inline]
3
    #[must_use] pub fn is_margin_bottom_auto(&self, node_idx: usize) -> bool {
3
        self.tier2_dims[node_idx].margin_bottom == I16_AUTO
3
    }
    #[inline]
3
    #[must_use] pub fn is_margin_left_auto(&self, node_idx: usize) -> bool {
3
        self.tier2_dims[node_idx].margin_left == I16_AUTO
3
    }
    #[inline]
3
    #[must_use] pub fn get_border_top_width(&self, node_idx: usize) -> Option<f32> {
3
        decode_resolved_px_i16(self.tier2_dims[node_idx].border_top_width)
3
    }
    #[inline]
    #[must_use] pub fn get_border_right_width(&self, node_idx: usize) -> Option<f32> {
        decode_resolved_px_i16(self.tier2_dims[node_idx].border_right_width)
    }
    #[inline]
    #[must_use] pub fn get_border_bottom_width(&self, node_idx: usize) -> Option<f32> {
        decode_resolved_px_i16(self.tier2_dims[node_idx].border_bottom_width)
    }
    #[inline]
3
    #[must_use] pub fn get_border_left_width(&self, node_idx: usize) -> Option<f32> {
3
        decode_resolved_px_i16(self.tier2_dims[node_idx].border_left_width)
3
    }
    // -- Raw i16 getters for macro fast paths --
    #[inline]
443619
    #[must_use] pub fn get_padding_top_raw(&self, node_idx: usize) -> i16 {
443619
        self.tier2_dims[node_idx].padding_top
443619
    }
    #[inline]
443531
    #[must_use] pub fn get_padding_right_raw(&self, node_idx: usize) -> i16 {
443531
        self.tier2_dims[node_idx].padding_right
443531
    }
    #[inline]
443619
    #[must_use] pub fn get_padding_bottom_raw(&self, node_idx: usize) -> i16 {
443619
        self.tier2_dims[node_idx].padding_bottom
443619
    }
    #[inline]
443531
    #[must_use] pub fn get_padding_left_raw(&self, node_idx: usize) -> i16 {
443531
        self.tier2_dims[node_idx].padding_left
443531
    }
    #[inline]
374220
    #[must_use] pub fn get_margin_top_raw(&self, node_idx: usize) -> i16 {
374220
        self.tier2_dims[node_idx].margin_top
374220
    }
    #[inline]
374220
    #[must_use] pub fn get_margin_right_raw(&self, node_idx: usize) -> i16 {
374220
        self.tier2_dims[node_idx].margin_right
374220
    }
    #[inline]
374220
    #[must_use] pub fn get_margin_bottom_raw(&self, node_idx: usize) -> i16 {
374220
        self.tier2_dims[node_idx].margin_bottom
374220
    }
    #[inline]
374220
    #[must_use] pub fn get_margin_left_raw(&self, node_idx: usize) -> i16 {
374220
        self.tier2_dims[node_idx].margin_left
374220
    }
    #[inline]
919699
    #[must_use] pub fn get_border_top_width_raw(&self, node_idx: usize) -> i16 {
919699
        self.tier2_dims[node_idx].border_top_width
919699
    }
    #[inline]
919611
    #[must_use] pub fn get_border_right_width_raw(&self, node_idx: usize) -> i16 {
919611
        self.tier2_dims[node_idx].border_right_width
919611
    }
    #[inline]
919699
    #[must_use] pub fn get_border_bottom_width_raw(&self, node_idx: usize) -> i16 {
919699
        self.tier2_dims[node_idx].border_bottom_width
919699
    }
    #[inline]
919611
    #[must_use] pub fn get_border_left_width_raw(&self, node_idx: usize) -> i16 {
919611
        self.tier2_dims[node_idx].border_left_width
919611
    }
    #[inline]
148448
    #[must_use] pub fn get_top(&self, node_idx: usize) -> i16 {
148448
        self.tier2_dims[node_idx].top
148448
    }
    #[inline]
148448
    #[must_use] pub fn get_right(&self, node_idx: usize) -> i16 {
148448
        self.tier2_dims[node_idx].right
148448
    }
    #[inline]
148448
    #[must_use] pub fn get_bottom(&self, node_idx: usize) -> i16 {
148448
        self.tier2_dims[node_idx].bottom
148448
    }
    #[inline]
148448
    #[must_use] pub fn get_left(&self, node_idx: usize) -> i16 {
148448
        self.tier2_dims[node_idx].left
148448
    }
    #[inline]
138515
    #[must_use] pub fn get_flex_grow(&self, node_idx: usize) -> Option<f32> {
138515
        decode_flex_u16(self.tier2_dims[node_idx].flex_grow)
138515
    }
    #[inline]
138515
    #[must_use] pub fn get_flex_shrink(&self, node_idx: usize) -> Option<f32> {
138515
        decode_flex_u16(self.tier2_dims[node_idx].flex_shrink)
138515
    }
    #[inline]
1638565
    #[must_use] pub fn get_z_index(&self, node_idx: usize) -> i16 {
1638565
        self.tier2_cold[node_idx].z_index
1638565
    }
    // -- Border colors (u32 RGBA) — cold tier --
    #[inline]
545397
    #[must_use] pub fn get_border_top_color_raw(&self, node_idx: usize) -> u32 {
545397
        self.tier2_cold[node_idx].border_top_color
545397
    }
    #[inline]
545391
    #[must_use] pub fn get_border_right_color_raw(&self, node_idx: usize) -> u32 {
545391
        self.tier2_cold[node_idx].border_right_color
545391
    }
    #[inline]
545391
    #[must_use] pub fn get_border_bottom_color_raw(&self, node_idx: usize) -> u32 {
545391
        self.tier2_cold[node_idx].border_bottom_color
545391
    }
    #[inline]
545391
    #[must_use] pub fn get_border_left_color_raw(&self, node_idx: usize) -> u32 {
545391
        self.tier2_cold[node_idx].border_left_color
545391
    }
    // -- Border styles (packed u16) — cold tier --
    #[inline]
3
    #[must_use] pub fn get_border_styles_packed(&self, node_idx: usize) -> u16 {
3
        self.tier2_cold[node_idx].border_styles_packed
3
    }
    #[inline]
781103
    #[must_use] pub fn get_border_top_style(&self, node_idx: usize) -> BorderStyle {
781103
        decode_border_top_style(self.tier2_cold[node_idx].border_styles_packed)
781103
    }
    #[inline]
781102
    #[must_use] pub fn get_border_right_style(&self, node_idx: usize) -> BorderStyle {
781102
        decode_border_right_style(self.tier2_cold[node_idx].border_styles_packed)
781102
    }
    #[inline]
781102
    #[must_use] pub fn get_border_bottom_style(&self, node_idx: usize) -> BorderStyle {
781102
        decode_border_bottom_style(self.tier2_cold[node_idx].border_styles_packed)
781102
    }
    #[inline]
781102
    #[must_use] pub fn get_border_left_style(&self, node_idx: usize) -> BorderStyle {
781102
        decode_border_left_style(self.tier2_cold[node_idx].border_styles_packed)
781102
    }
    // -- Border spacing — cold tier --
    #[inline]
421
    #[must_use] pub fn get_border_spacing_h_raw(&self, node_idx: usize) -> i16 {
421
        self.tier2_cold[node_idx].border_spacing_h
421
    }
    #[inline]
421
    #[must_use] pub fn get_border_spacing_v_raw(&self, node_idx: usize) -> i16 {
421
        self.tier2_cold[node_idx].border_spacing_v
421
    }
    // -- Tab size — cold tier --
    #[inline]
69886
    #[must_use] pub fn get_tab_size_raw(&self, node_idx: usize) -> i16 {
69886
        self.tier2_cold[node_idx].tab_size
69886
    }
    // -- Border radii — cold tier (i16 px × 10, I16_SENTINEL = unset = 0) --
    #[inline]
2607212
    #[must_use] pub fn get_border_top_left_radius_raw(&self, node_idx: usize) -> i16 {
2607212
        self.tier2_cold[node_idx].border_top_left_radius
2607212
    }
    #[inline]
2607209
    #[must_use] pub fn get_border_top_right_radius_raw(&self, node_idx: usize) -> i16 {
2607209
        self.tier2_cold[node_idx].border_top_right_radius
2607209
    }
    #[inline]
2607209
    #[must_use] pub fn get_border_bottom_left_radius_raw(&self, node_idx: usize) -> i16 {
2607209
        self.tier2_cold[node_idx].border_bottom_left_radius
2607209
    }
    #[inline]
2607209
    #[must_use] pub fn get_border_bottom_right_radius_raw(&self, node_idx: usize) -> i16 {
2607209
        self.tier2_cold[node_idx].border_bottom_right_radius
2607209
    }
    // -- Opacity / transform / hot flags --
    /// Raw opacity byte. `OPACITY_SENTINEL` (255) = unset (default = 1.0).
    /// Otherwise value / 254.0 yields the opacity in [0.0, 1.0].
    #[inline]
2120611
    #[must_use] pub fn get_opacity_raw(&self, node_idx: usize) -> u8 {
2120611
        self.tier2_cold[node_idx].opacity
2120611
    }
    #[inline]
3
    #[must_use] pub fn get_hot_flags(&self, node_idx: usize) -> u8 {
3
        self.tier2_cold[node_idx].hot_flags
3
    }
    #[inline]
2105366
    #[must_use] pub fn has_transform(&self, node_idx: usize) -> bool {
2105366
        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM != 0
2105366
    }
    #[inline]
9
    #[must_use] pub fn has_transform_origin(&self, node_idx: usize) -> bool {
9
        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM_ORIGIN != 0
9
    }
    #[inline]
1900677
    #[must_use] pub fn has_box_shadow(&self, node_idx: usize) -> bool {
1900677
        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BOX_SHADOW != 0
1900677
    }
    #[inline]
69892
    #[must_use] pub fn has_text_decoration(&self, node_idx: usize) -> bool {
69892
        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TEXT_DECORATION != 0
69892
    }
    #[inline]
1475942
    #[must_use] pub fn has_background(&self, node_idx: usize) -> bool {
1475942
        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0
1475942
    }
    #[inline]
830051
    #[must_use] pub fn has_clip_path(&self, node_idx: usize) -> bool {
830051
        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_CLIP_PATH != 0
830051
    }
    #[inline]
824307
    #[must_use] pub fn has_scrollbar_css(&self, node_idx: usize) -> bool {
824307
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_SCROLLBAR_CSS != 0
824307
    }
    #[inline]
273460
    #[must_use] pub fn has_counter(&self, node_idx: usize) -> bool {
273460
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_COUNTER != 0
273460
    }
    #[inline]
1649703
    #[must_use] pub fn has_break(&self, node_idx: usize) -> bool {
1649703
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BREAK != 0
1649703
    }
    #[inline]
591294
    #[must_use] pub fn has_text_orientation(&self, node_idx: usize) -> bool {
591294
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_ORIENTATION != 0
591294
    }
    #[inline]
22
    #[must_use] pub fn has_text_shadow(&self, node_idx: usize) -> bool {
22
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_SHADOW != 0
22
    }
    #[inline]
22
    #[must_use] pub fn has_backdrop_filter(&self, node_idx: usize) -> bool {
22
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BACKDROP_FILTER != 0
22
    }
    #[inline]
22
    #[must_use] pub fn has_filter(&self, node_idx: usize) -> bool {
22
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_FILTER != 0
22
    }
    #[inline]
11
    #[must_use] pub fn has_mix_blend_mode(&self, node_idx: usize) -> bool {
11
        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_MIX_BLEND_MODE != 0
11
    }
    /// DOM-level fast-path check: returns `true` if the given flag bit is set
    /// (some node in this DOM declared the corresponding property).
    #[inline]
73
    #[must_use] pub const fn dom_declared(&self, flag: u32) -> bool {
73
        self.dom_declared_flags & flag != 0
73
    }
    /// Scrollbar-gutter: 0 = auto (default), 1 = stable, 2 = both-edges, 3 = mirror.
    #[inline]
1257018
    #[must_use] pub fn get_scrollbar_gutter_bits(&self, node_idx: usize) -> u8 {
1257018
        (self.tier2_cold[node_idx].hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK)
1257018
            >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT
1257018
    }
    // -- Tier 2b getters (text props) --
    #[inline]
69886
    #[must_use] pub fn get_text_color_raw(&self, node_idx: usize) -> u32 {
69886
        self.tier2b_text[node_idx].text_color
69886
    }
    #[inline]
4
    #[must_use] pub fn get_font_family_hash(&self, node_idx: usize) -> u64 {
4
        self.tier2b_text[node_idx].font_family_hash
4
    }
    #[inline]
69887
    #[must_use] pub fn get_line_height(&self, node_idx: usize) -> Option<f32> {
69887
        decode_resolved_px_i16(self.tier2b_text[node_idx].line_height)
69887
    }
    #[inline]
69886
    #[must_use] pub fn get_letter_spacing(&self, node_idx: usize) -> Option<f32> {
69886
        decode_resolved_px_i16(self.tier2b_text[node_idx].letter_spacing)
69886
    }
    #[inline]
69886
    #[must_use] pub fn get_word_spacing(&self, node_idx: usize) -> Option<f32> {
69886
        decode_resolved_px_i16(self.tier2b_text[node_idx].word_spacing)
69886
    }
    #[inline]
3
    #[must_use] pub fn get_text_indent(&self, node_idx: usize) -> Option<f32> {
3
        decode_resolved_px_i16(self.tier2b_text[node_idx].text_indent)
3
    }
}
// =============================================================================
// Helper: encode a CssPropertyValue<PixelValue> into i16 resolved-px
// =============================================================================
/// Resolve a `CssPropertyValue`<PixelValue> to an i16 ×10 encoding.
///
/// Only handles `Exact(px(...))` values. Everything else → sentinel.
/// For the compact cache builder, we only pre-resolve absolute pixel values.
/// Relative units (em, %, etc.) get sentinel and fall back to the slow path.
#[inline]
2311584
#[must_use] pub fn encode_css_pixel_as_i16(prop: &CssPropertyValue<PixelValue>) -> i16 {
2311584
    match prop {
2311346
        CssPropertyValue::Exact(pv) => {
2311346
            if pv.metric == SizeMetric::Px {
1879871
                encode_resolved_px_i16(pv.number.get())
            } else {
431475
                I16_SENTINEL // non-px units need resolution context → slow path
            }
        }
113
        CssPropertyValue::Auto => I16_AUTO,
21
        CssPropertyValue::Initial => I16_INITIAL,
21
        CssPropertyValue::Inherit => I16_INHERIT,
83
        _ => I16_SENTINEL,
    }
2311584
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
1
    fn test_tier1_roundtrip() {
1
        let t1 = encode_tier1(
1
            LayoutDisplay::Flex,
1
            LayoutPosition::Relative,
1
            LayoutFloat::Left,
1
            LayoutOverflow::Hidden,
1
            LayoutOverflow::Scroll,
1
            LayoutBoxSizing::BorderBox,
1
            LayoutFlexDirection::Column,
1
            LayoutFlexWrap::Wrap,
1
            LayoutJustifyContent::SpaceBetween,
1
            LayoutAlignItems::Center,
1
            LayoutAlignContent::End,
1
            LayoutWritingMode::VerticalRl,
1
            LayoutClear::Both,
1
            StyleFontWeight::Bold,
1
            StyleFontStyle::Italic,
1
            StyleTextAlign::Center,
1
            StyleVisibility::Hidden,
1
            StyleWhiteSpace::Pre,
1
            StyleDirection::Rtl,
1
            StyleVerticalAlign::Middle,
1
            StyleBorderCollapse::Collapse,
        );
1
        assert!(tier1_is_populated(t1));
1
        assert_eq!(decode_display(t1), LayoutDisplay::Flex);
1
        assert_eq!(decode_position(t1), LayoutPosition::Relative);
1
        assert_eq!(decode_float(t1), LayoutFloat::Left);
1
        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Hidden);
1
        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Scroll);
1
        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::BorderBox);
1
        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Column);
1
        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::Wrap);
1
        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::SpaceBetween);
1
        assert_eq!(decode_align_items(t1), LayoutAlignItems::Center);
1
        assert_eq!(decode_align_content(t1), LayoutAlignContent::End);
1
        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::VerticalRl);
1
        assert_eq!(decode_clear(t1), LayoutClear::Both);
1
        assert_eq!(decode_font_weight(t1), StyleFontWeight::Bold);
1
        assert_eq!(decode_font_style(t1), StyleFontStyle::Italic);
1
        assert_eq!(decode_text_align(t1), StyleTextAlign::Center);
1
        assert_eq!(decode_visibility(t1), StyleVisibility::Hidden);
1
        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Pre);
1
        assert_eq!(decode_direction(t1), StyleDirection::Rtl);
1
        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Middle);
1
        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Collapse);
1
    }
    #[test]
1
    fn test_tier1_defaults() {
1
        let t1 = encode_tier1(
1
            LayoutDisplay::Block,
1
            LayoutPosition::Static,
1
            LayoutFloat::None,
1
            LayoutOverflow::Visible,
1
            LayoutOverflow::Visible,
1
            LayoutBoxSizing::ContentBox,
1
            LayoutFlexDirection::Row,
1
            LayoutFlexWrap::NoWrap,
1
            LayoutJustifyContent::FlexStart,
1
            LayoutAlignItems::Stretch,
1
            LayoutAlignContent::Stretch,
1
            LayoutWritingMode::HorizontalTb,
1
            LayoutClear::None,
1
            StyleFontWeight::Normal,
1
            StyleFontStyle::Normal,
1
            StyleTextAlign::Left,
1
            StyleVisibility::Visible,
1
            StyleWhiteSpace::Normal,
1
            StyleDirection::Ltr,
1
            StyleVerticalAlign::Baseline,
1
            StyleBorderCollapse::Separate,
        );
1
        assert!(tier1_is_populated(t1));
1
        assert_eq!(decode_display(t1), LayoutDisplay::Block);
1
        assert_eq!(decode_position(t1), LayoutPosition::Static);
1
    }
    #[test]
1
    fn test_pixel_value_u32_roundtrip() {
1
        let pv = PixelValue::px(123.456);
1
        let encoded = encode_pixel_value_u32(&pv);
1
        assert!(encoded < U32_SENTINEL_THRESHOLD);
1
        let decoded = decode_pixel_value_u32(encoded).unwrap();
1
        assert_eq!(decoded.metric, SizeMetric::Px);
        // Check within precision (×1000)
1
        assert!((decoded.number.get() - 123.456).abs() < 0.002);
1
    }
    #[test]
1
    fn test_pixel_value_u32_percent() {
1
        let pv = PixelValue {
1
            metric: SizeMetric::Percent,
1
            number: FloatValue::new(50.0),
1
        };
1
        let encoded = encode_pixel_value_u32(&pv);
1
        let decoded = decode_pixel_value_u32(encoded).unwrap();
1
        assert_eq!(decoded.metric, SizeMetric::Percent);
1
        assert!((decoded.number.get() - 50.0).abs() < 0.002);
1
    }
    #[test]
1
    fn test_sentinel_values() {
1
        assert_eq!(decode_pixel_value_u32(U32_SENTINEL), None);
1
        assert_eq!(decode_pixel_value_u32(U32_AUTO), None);
1
        assert_eq!(decode_pixel_value_u32(U32_MIN_CONTENT), None);
1
        assert_eq!(decode_resolved_px_i16(I16_SENTINEL), None);
1
        assert_eq!(decode_resolved_px_i16(I16_AUTO), None);
1
    }
    #[test]
1
    fn test_resolved_px_i16_roundtrip() {
1
        let px = 123.4f32;
1
        let encoded = encode_resolved_px_i16(px);
1
        let decoded = decode_resolved_px_i16(encoded).unwrap();
1
        assert!((decoded - 123.4).abs() < 0.11);
        // Negative values
1
        let px = -50.7f32;
1
        let encoded = encode_resolved_px_i16(px);
1
        let decoded = decode_resolved_px_i16(encoded).unwrap();
1
        assert!((decoded - (-50.7)).abs() < 0.11);
1
    }
    #[test]
1
    fn test_flex_u16_roundtrip() {
1
        let v = 2.5f32;
1
        let encoded = encode_flex_u16(v);
1
        let decoded = decode_flex_u16(encoded).unwrap();
1
        assert!((decoded - 2.5).abs() < 0.011);
1
    }
    #[test]
1
    fn test_compact_node_props_size() {
        // 72B hot props: 8×u32 dimensions (32B) + 16×i16 box model (32B)
        // + 2×u16 flex (4B) + 1×i16 order + align/pos tier1 bits (4B).
1
        assert_eq!(size_of::<CompactNodeProps>(), 72);
1
    }
    #[test]
1
    fn test_compact_node_props_cold_size() {
        // 48B cold props: 4×u32 border colors (16B) + 4×i16 border radii (8B)
        // + 1×i16 z_index + 1×u16 border_styles_packed + 2×i16 border_spacing
        // + 1×i16 tab_size + 4×i16 grid placement (8B) + 3×u8 (opacity,
        // hot_flags, extra_flags) = 45B, padded to 48B for u32 alignment.
1
        assert_eq!(size_of::<CompactNodePropsCold>(), 48);
1
    }
    #[test]
1
    fn test_compact_text_props_size() {
1
        assert_eq!(size_of::<CompactTextProps>(), 24);
1
    }
    // ========================================================================
    // Tier1 enum 0-sentinel contract
    //
    // Tier1 packs 21 enums into a single u64. A bit run that is all zeros
    // must decode to the CSS initial value of that property, because an
    // unpopulated tier1 field is all zeros. If any encoder shifts so that
    // `0 -> something-other-than-initial`, every node that didn't explicitly
    // set the property silently gets the wrong default — which is exactly
    // how the calc.c grid stretch regression shipped (Start encoded as 0,
    // so every grid container reported justify-items: Start instead of
    // the CSS default Stretch-for-grid, collapsing the calc button grid).
    //
    // Test invariant: decoding a u8 of 0 for every enum yields the CSS
    // initial value of that property.
    // ========================================================================
    #[test]
1
    fn test_justify_items_zero_is_stretch() {
        // CSS initial for justify-items is `normal`, which on a grid item
        // behaves as `stretch`. The tier1 bit pattern 0 must round-trip to
        // Stretch so unset grid containers don't collapse their items.
1
        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
1
        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
1
    }
    #[test]
1
    fn test_tier1_enum_zero_sentinel_is_css_initial() {
1
        assert_eq!(layout_display_from_u8(0), LayoutDisplay::Block);
1
        assert_eq!(layout_position_from_u8(0), LayoutPosition::Static);
1
        assert_eq!(layout_float_from_u8(0), LayoutFloat::None);
1
        assert_eq!(layout_overflow_from_u8(0), LayoutOverflow::Visible);
1
        assert_eq!(layout_box_sizing_from_u8(0), LayoutBoxSizing::ContentBox);
1
        assert_eq!(layout_flex_direction_from_u8(0), LayoutFlexDirection::Row);
1
        assert_eq!(layout_flex_wrap_from_u8(0), LayoutFlexWrap::NoWrap);
1
        assert_eq!(layout_justify_content_from_u8(0), LayoutJustifyContent::FlexStart);
1
        assert_eq!(layout_align_items_from_u8(0), LayoutAlignItems::Stretch);
1
        assert_eq!(layout_align_content_from_u8(0), LayoutAlignContent::Stretch);
1
        assert_eq!(layout_align_self_from_u8(0), LayoutAlignSelf::Auto);
1
        assert_eq!(layout_justify_self_from_u8(0), LayoutJustifySelf::Auto);
1
        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
1
        assert_eq!(layout_grid_auto_flow_from_u8(0), LayoutGridAutoFlow::Row);
1
        assert_eq!(layout_writing_mode_from_u8(0), LayoutWritingMode::HorizontalTb);
1
        assert_eq!(layout_clear_from_u8(0), LayoutClear::None);
1
        assert_eq!(style_font_weight_from_u8(0), StyleFontWeight::Normal);
1
        assert_eq!(style_font_style_from_u8(0), StyleFontStyle::Normal);
        // text-align initial is `start`; we collapse `start` → `left` on
        // LTR runs at encode time, so the 0 slot decodes to Left.
1
        assert_eq!(style_text_align_from_u8(0), StyleTextAlign::Start);
1
        assert_eq!(style_visibility_from_u8(0), StyleVisibility::Visible);
1
        assert_eq!(style_white_space_from_u8(0), StyleWhiteSpace::Normal);
1
        assert_eq!(style_direction_from_u8(0), StyleDirection::Ltr);
1
        assert_eq!(style_vertical_align_from_u8(0), StyleVerticalAlign::Baseline);
1
        assert_eq!(border_collapse_from_u8(0), StyleBorderCollapse::Separate);
1
    }
    #[test]
1
    fn test_tier1_enum_initial_encodes_to_zero() {
        // Mirror of the above — encoding the CSS initial value must
        // produce 0, otherwise an `all-zeros` tier1 bit run would encode
        // a non-initial value and nodes without explicit properties would
        // silently inherit the wrong default.
1
        assert_eq!(layout_display_to_u8(LayoutDisplay::Block), 0);
1
        assert_eq!(layout_position_to_u8(LayoutPosition::Static), 0);
1
        assert_eq!(layout_float_to_u8(LayoutFloat::None), 0);
1
        assert_eq!(layout_overflow_to_u8(LayoutOverflow::Visible), 0);
1
        assert_eq!(layout_box_sizing_to_u8(LayoutBoxSizing::ContentBox), 0);
1
        assert_eq!(layout_flex_direction_to_u8(LayoutFlexDirection::Row), 0);
1
        assert_eq!(layout_flex_wrap_to_u8(LayoutFlexWrap::NoWrap), 0);
1
        assert_eq!(layout_justify_content_to_u8(LayoutJustifyContent::FlexStart), 0);
1
        assert_eq!(layout_align_items_to_u8(LayoutAlignItems::Stretch), 0);
1
        assert_eq!(layout_align_content_to_u8(LayoutAlignContent::Stretch), 0);
1
        assert_eq!(layout_align_self_to_u8(LayoutAlignSelf::Auto), 0);
1
        assert_eq!(layout_justify_self_to_u8(LayoutJustifySelf::Auto), 0);
1
        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
1
        assert_eq!(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::Row), 0);
1
        assert_eq!(layout_writing_mode_to_u8(LayoutWritingMode::HorizontalTb), 0);
1
        assert_eq!(layout_clear_to_u8(LayoutClear::None), 0);
1
        assert_eq!(style_font_weight_to_u8(StyleFontWeight::Normal), 0);
1
        assert_eq!(style_font_style_to_u8(StyleFontStyle::Normal), 0);
1
        assert_eq!(style_text_align_to_u8(StyleTextAlign::Left), 4);
1
        assert_eq!(style_visibility_to_u8(StyleVisibility::Visible), 0);
1
        assert_eq!(style_white_space_to_u8(StyleWhiteSpace::Normal), 0);
1
        assert_eq!(style_direction_to_u8(StyleDirection::Ltr), 0);
1
        assert_eq!(style_vertical_align_to_u8(StyleVerticalAlign::Baseline), 0);
1
        assert_eq!(border_collapse_to_u8(StyleBorderCollapse::Separate), 0);
1
    }
    // ========================================================================
    // Exhaustive round-trip: every variant of every enum must survive
    // encode → decode unchanged. Catches any reordering that maps two
    // different variants to the same u8, or any mask-width mismatch.
    // ========================================================================
    macro_rules! roundtrip_all {
        ($name:ident, $to:ident, $from:ident, [$($variant:expr),+ $(,)?]) => {
            #[test]
24
            fn $name() {
155
                for v in [$($variant),+] {
131
                    let u = $to(v);
131
                    let decoded = $from(u);
131
                    assert_eq!(decoded, v, "{:?} != {:?} (via u8 = {})", decoded, v, u);
                }
24
            }
        };
    }
    roundtrip_all!(rt_display, layout_display_to_u8, layout_display_from_u8, [
        LayoutDisplay::Block, LayoutDisplay::Inline, LayoutDisplay::InlineBlock,
        LayoutDisplay::Flex, LayoutDisplay::None, LayoutDisplay::InlineFlex,
        LayoutDisplay::Table, LayoutDisplay::InlineTable, LayoutDisplay::TableRowGroup,
        LayoutDisplay::TableHeaderGroup, LayoutDisplay::TableFooterGroup,
        LayoutDisplay::TableRow, LayoutDisplay::TableColumnGroup,
        LayoutDisplay::TableColumn, LayoutDisplay::TableCell,
        LayoutDisplay::TableCaption, LayoutDisplay::FlowRoot,
        LayoutDisplay::ListItem, LayoutDisplay::RunIn, LayoutDisplay::Marker,
        LayoutDisplay::Grid, LayoutDisplay::InlineGrid, LayoutDisplay::Contents,
    ]);
    roundtrip_all!(rt_position, layout_position_to_u8, layout_position_from_u8, [
        LayoutPosition::Static, LayoutPosition::Relative, LayoutPosition::Absolute,
        LayoutPosition::Fixed, LayoutPosition::Sticky,
    ]);
    roundtrip_all!(rt_float, layout_float_to_u8, layout_float_from_u8, [
        LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right,
    ]);
    roundtrip_all!(rt_overflow, layout_overflow_to_u8, layout_overflow_from_u8, [
        LayoutOverflow::Visible, LayoutOverflow::Hidden, LayoutOverflow::Scroll,
        LayoutOverflow::Auto, LayoutOverflow::Clip,
    ]);
    roundtrip_all!(rt_box_sizing, layout_box_sizing_to_u8, layout_box_sizing_from_u8, [
        LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox,
    ]);
    roundtrip_all!(rt_flex_direction, layout_flex_direction_to_u8, layout_flex_direction_from_u8, [
        LayoutFlexDirection::Row, LayoutFlexDirection::RowReverse,
        LayoutFlexDirection::Column, LayoutFlexDirection::ColumnReverse,
    ]);
    roundtrip_all!(rt_flex_wrap, layout_flex_wrap_to_u8, layout_flex_wrap_from_u8, [
        LayoutFlexWrap::NoWrap, LayoutFlexWrap::Wrap, LayoutFlexWrap::WrapReverse,
    ]);
    roundtrip_all!(rt_justify_content, layout_justify_content_to_u8, layout_justify_content_from_u8, [
        LayoutJustifyContent::FlexStart, LayoutJustifyContent::FlexEnd,
        LayoutJustifyContent::Start, LayoutJustifyContent::End,
        LayoutJustifyContent::Center, LayoutJustifyContent::SpaceBetween,
        LayoutJustifyContent::SpaceAround, LayoutJustifyContent::SpaceEvenly,
    ]);
    roundtrip_all!(rt_align_items, layout_align_items_to_u8, layout_align_items_from_u8, [
        LayoutAlignItems::Stretch, LayoutAlignItems::Center, LayoutAlignItems::Start,
        LayoutAlignItems::End, LayoutAlignItems::Baseline,
    ]);
    roundtrip_all!(rt_align_self, layout_align_self_to_u8, layout_align_self_from_u8, [
        LayoutAlignSelf::Auto, LayoutAlignSelf::Stretch, LayoutAlignSelf::Center,
        LayoutAlignSelf::Start, LayoutAlignSelf::End, LayoutAlignSelf::Baseline,
    ]);
    roundtrip_all!(rt_justify_self, layout_justify_self_to_u8, layout_justify_self_from_u8, [
        LayoutJustifySelf::Auto, LayoutJustifySelf::Start, LayoutJustifySelf::End,
        LayoutJustifySelf::Center, LayoutJustifySelf::Stretch,
    ]);
    roundtrip_all!(rt_justify_items, layout_justify_items_to_u8, layout_justify_items_from_u8, [
        LayoutJustifyItems::Stretch, LayoutJustifyItems::Start,
        LayoutJustifyItems::End, LayoutJustifyItems::Center,
    ]);
    roundtrip_all!(rt_grid_auto_flow, layout_grid_auto_flow_to_u8, layout_grid_auto_flow_from_u8, [
        LayoutGridAutoFlow::Row, LayoutGridAutoFlow::Column,
        LayoutGridAutoFlow::RowDense, LayoutGridAutoFlow::ColumnDense,
    ]);
    roundtrip_all!(rt_align_content, layout_align_content_to_u8, layout_align_content_from_u8, [
        LayoutAlignContent::Stretch, LayoutAlignContent::Center,
        LayoutAlignContent::Start, LayoutAlignContent::End,
        LayoutAlignContent::SpaceBetween, LayoutAlignContent::SpaceAround,
    ]);
    roundtrip_all!(rt_writing_mode, layout_writing_mode_to_u8, layout_writing_mode_from_u8, [
        LayoutWritingMode::HorizontalTb, LayoutWritingMode::VerticalRl,
        LayoutWritingMode::VerticalLr,
    ]);
    roundtrip_all!(rt_clear, layout_clear_to_u8, layout_clear_from_u8, [
        LayoutClear::None, LayoutClear::Left, LayoutClear::Right, LayoutClear::Both,
    ]);
    roundtrip_all!(rt_font_weight, style_font_weight_to_u8, style_font_weight_from_u8, [
        StyleFontWeight::Normal, StyleFontWeight::W100, StyleFontWeight::W200,
        StyleFontWeight::W300, StyleFontWeight::W500, StyleFontWeight::W600,
        StyleFontWeight::Bold, StyleFontWeight::W800, StyleFontWeight::W900,
        StyleFontWeight::Lighter, StyleFontWeight::Bolder,
    ]);
    roundtrip_all!(rt_font_style, style_font_style_to_u8, style_font_style_from_u8, [
        StyleFontStyle::Normal, StyleFontStyle::Italic, StyleFontStyle::Oblique,
    ]);
    roundtrip_all!(rt_text_align, style_text_align_to_u8, style_text_align_from_u8, [
        StyleTextAlign::Left, StyleTextAlign::Center, StyleTextAlign::Right,
        StyleTextAlign::Justify, StyleTextAlign::Start, StyleTextAlign::End,
    ]);
    roundtrip_all!(rt_visibility, style_visibility_to_u8, style_visibility_from_u8, [
        StyleVisibility::Visible, StyleVisibility::Hidden, StyleVisibility::Collapse,
    ]);
    roundtrip_all!(rt_white_space, style_white_space_to_u8, style_white_space_from_u8, [
        StyleWhiteSpace::Normal, StyleWhiteSpace::Pre, StyleWhiteSpace::Nowrap,
        StyleWhiteSpace::PreWrap, StyleWhiteSpace::PreLine, StyleWhiteSpace::BreakSpaces,
    ]);
    roundtrip_all!(rt_direction, style_direction_to_u8, style_direction_from_u8, [
        StyleDirection::Ltr, StyleDirection::Rtl,
    ]);
    roundtrip_all!(rt_vertical_align, style_vertical_align_to_u8, style_vertical_align_from_u8, [
        StyleVerticalAlign::Baseline, StyleVerticalAlign::Top, StyleVerticalAlign::Middle,
        StyleVerticalAlign::Bottom, StyleVerticalAlign::Sub, StyleVerticalAlign::Superscript,
        StyleVerticalAlign::TextTop, StyleVerticalAlign::TextBottom,
    ]);
    roundtrip_all!(rt_border_collapse, border_collapse_to_u8, border_collapse_from_u8, [
        StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse,
    ]);
    // ========================================================================
    // Bit-layout safety: every encoder must produce a u8 whose bits all
    // fit inside the mask allocated for that property in the tier1 u64.
    // If an enum grows a new variant that overflows its mask, the encoded
    // bits would leak into the next property's slot and silently corrupt
    // unrelated state.
    // ========================================================================
    #[test]
1
    fn test_encoded_u8_fits_in_tier1_mask() {
24
        fn assert_fits(name: &str, val: u8, mask: u64) {
24
            assert!(
24
                u64::from(val) & !mask == 0,
                "{name}: encoded u8 {val} overflows mask {mask:b}",
            );
24
        }
1
        assert_fits("display", layout_display_to_u8(LayoutDisplay::Contents), DISPLAY_MASK);
1
        assert_fits("position", layout_position_to_u8(LayoutPosition::Sticky), POSITION_MASK);
1
        assert_fits("float", layout_float_to_u8(LayoutFloat::Right), FLOAT_MASK);
1
        assert_fits("overflow", layout_overflow_to_u8(LayoutOverflow::Clip), OVERFLOW_MASK);
1
        assert_fits("box_sizing", layout_box_sizing_to_u8(LayoutBoxSizing::BorderBox), BOX_SIZING_MASK);
1
        assert_fits("flex_direction", layout_flex_direction_to_u8(LayoutFlexDirection::ColumnReverse), FLEX_DIR_MASK);
1
        assert_fits("flex_wrap", layout_flex_wrap_to_u8(LayoutFlexWrap::WrapReverse), FLEX_WRAP_MASK);
1
        assert_fits("justify_content", layout_justify_content_to_u8(LayoutJustifyContent::SpaceEvenly), JUSTIFY_MASK);
1
        assert_fits("align_items", layout_align_items_to_u8(LayoutAlignItems::Baseline), ALIGN_MASK);
1
        assert_fits("align_self", layout_align_self_to_u8(LayoutAlignSelf::Baseline), ALIGN_SELF_MASK);
1
        assert_fits("justify_self", layout_justify_self_to_u8(LayoutJustifySelf::Stretch), JUSTIFY_SELF_MASK);
1
        assert_fits("justify_items", layout_justify_items_to_u8(LayoutJustifyItems::Center), JUSTIFY_ITEMS_MASK);
1
        assert_fits("grid_auto_flow", layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense), GRID_AUTO_FLOW_MASK);
1
        assert_fits("align_content", layout_align_content_to_u8(LayoutAlignContent::SpaceAround), ALIGN_MASK);
1
        assert_fits("writing_mode", layout_writing_mode_to_u8(LayoutWritingMode::VerticalLr), WRITING_MODE_MASK);
1
        assert_fits("clear", layout_clear_to_u8(LayoutClear::Both), CLEAR_MASK);
1
        assert_fits("font_weight", style_font_weight_to_u8(StyleFontWeight::Bolder), FONT_WEIGHT_MASK);
1
        assert_fits("font_style", style_font_style_to_u8(StyleFontStyle::Oblique), FONT_STYLE_MASK);
1
        assert_fits("text_align", style_text_align_to_u8(StyleTextAlign::End), TEXT_ALIGN_MASK);
1
        assert_fits("visibility", style_visibility_to_u8(StyleVisibility::Collapse), VISIBILITY_MASK);
1
        assert_fits("white_space", style_white_space_to_u8(StyleWhiteSpace::BreakSpaces), WHITE_SPACE_MASK);
1
        assert_fits("direction", style_direction_to_u8(StyleDirection::Rtl), DIRECTION_MASK);
1
        assert_fits("vertical_align", style_vertical_align_to_u8(StyleVerticalAlign::TextBottom), VERTICAL_ALIGN_MASK);
1
        assert_fits("border_collapse", border_collapse_to_u8(StyleBorderCollapse::Collapse), BORDER_COLLAPSE_MASK);
1
    }
    // ========================================================================
    // Empty tier1 decodes to all-initial — this is the core contract that
    // was violated by the pre-fix justify_items encoding. An empty u64 with
    // only TIER1_POPULATED_BIT set must decode every property to its CSS
    // initial value; this is how `build_compact_cache` can leave unspecified
    // properties at 0 and still produce the correct cascade result.
    // ========================================================================
    #[test]
1
    fn test_empty_tier1_decodes_to_initial_values() {
1
        let t1 = TIER1_POPULATED_BIT; // populated, but zero content
1
        assert!(tier1_is_populated(t1));
1
        assert_eq!(decode_display(t1), LayoutDisplay::Block);
1
        assert_eq!(decode_position(t1), LayoutPosition::Static);
1
        assert_eq!(decode_float(t1), LayoutFloat::None);
1
        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Visible);
1
        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Visible);
1
        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::ContentBox);
1
        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Row);
1
        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::NoWrap);
1
        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::FlexStart);
1
        assert_eq!(decode_align_items(t1), LayoutAlignItems::Stretch);
1
        assert_eq!(decode_align_content(t1), LayoutAlignContent::Stretch);
1
        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::HorizontalTb);
1
        assert_eq!(decode_clear(t1), LayoutClear::None);
1
        assert_eq!(decode_font_weight(t1), StyleFontWeight::Normal);
1
        assert_eq!(decode_font_style(t1), StyleFontStyle::Normal);
1
        assert_eq!(decode_text_align(t1), StyleTextAlign::Start);
1
        assert_eq!(decode_visibility(t1), StyleVisibility::Visible);
1
        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Normal);
1
        assert_eq!(decode_direction(t1), StyleDirection::Ltr);
1
        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Baseline);
1
        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Separate);
1
    }
}
#[cfg(test)]
#[allow(
    clippy::float_cmp,
    clippy::unreadable_literal,
    clippy::too_many_lines,
    clippy::cast_precision_loss
)]
mod autotest_generated {
    use super::*;
    use crate::props::basic::length::PercentageValue;
    // =========================================================================
    // Shared fixtures
    // =========================================================================
    const ALL_DISPLAY: [LayoutDisplay; 23] = [
        LayoutDisplay::Block,
        LayoutDisplay::Inline,
        LayoutDisplay::InlineBlock,
        LayoutDisplay::Flex,
        LayoutDisplay::None,
        LayoutDisplay::InlineFlex,
        LayoutDisplay::Table,
        LayoutDisplay::InlineTable,
        LayoutDisplay::TableRowGroup,
        LayoutDisplay::TableHeaderGroup,
        LayoutDisplay::TableFooterGroup,
        LayoutDisplay::TableRow,
        LayoutDisplay::TableColumnGroup,
        LayoutDisplay::TableColumn,
        LayoutDisplay::TableCell,
        LayoutDisplay::TableCaption,
        LayoutDisplay::FlowRoot,
        LayoutDisplay::ListItem,
        LayoutDisplay::RunIn,
        LayoutDisplay::Marker,
        LayoutDisplay::Grid,
        LayoutDisplay::InlineGrid,
        LayoutDisplay::Contents,
    ];
    const ALL_BORDER_STYLE: [BorderStyle; 10] = [
        BorderStyle::None,
        BorderStyle::Solid,
        BorderStyle::Double,
        BorderStyle::Dotted,
        BorderStyle::Dashed,
        BorderStyle::Hidden,
        BorderStyle::Groove,
        BorderStyle::Ridge,
        BorderStyle::Inset,
        BorderStyle::Outset,
    ];
    const ALL_SIZE_METRIC: [SizeMetric; 12] = [
        SizeMetric::Px,
        SizeMetric::Pt,
        SizeMetric::Em,
        SizeMetric::Rem,
        SizeMetric::In,
        SizeMetric::Cm,
        SizeMetric::Mm,
        SizeMetric::Percent,
        SizeMetric::Vw,
        SizeMetric::Vh,
        SizeMetric::Vmin,
        SizeMetric::Vmax,
    ];
    /// Build a `PixelValue` straight from the raw fixed-point isize (×1000),
    /// bypassing the f32 constructor so boundary rows are exact.
    fn pv_raw(metric: SizeMetric, raw: isize) -> PixelValue {
        PixelValue {
            metric,
            number: FloatValue { number: raw },
        }
    }
    /// All 21 tier-1 enum properties as one struct, so a decode can be checked
    /// field-by-field against exactly what was encoded.
    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
    struct T1 {
        display: LayoutDisplay,
        position: LayoutPosition,
        float: LayoutFloat,
        overflow_x: LayoutOverflow,
        overflow_y: LayoutOverflow,
        box_sizing: LayoutBoxSizing,
        flex_direction: LayoutFlexDirection,
        flex_wrap: LayoutFlexWrap,
        justify_content: LayoutJustifyContent,
        align_items: LayoutAlignItems,
        align_content: LayoutAlignContent,
        writing_mode: LayoutWritingMode,
        clear: LayoutClear,
        font_weight: StyleFontWeight,
        font_style: StyleFontStyle,
        text_align: StyleTextAlign,
        visibility: StyleVisibility,
        white_space: StyleWhiteSpace,
        direction: StyleDirection,
        vertical_align: StyleVerticalAlign,
        border_collapse: StyleBorderCollapse,
    }
    impl T1 {
        /// Every field at its CSS initial value (the u8-0 slot).
        const fn initial() -> Self {
            Self {
                display: LayoutDisplay::Block,
                position: LayoutPosition::Static,
                float: LayoutFloat::None,
                overflow_x: LayoutOverflow::Visible,
                overflow_y: LayoutOverflow::Visible,
                box_sizing: LayoutBoxSizing::ContentBox,
                flex_direction: LayoutFlexDirection::Row,
                flex_wrap: LayoutFlexWrap::NoWrap,
                justify_content: LayoutJustifyContent::FlexStart,
                align_items: LayoutAlignItems::Stretch,
                align_content: LayoutAlignContent::Stretch,
                writing_mode: LayoutWritingMode::HorizontalTb,
                clear: LayoutClear::None,
                font_weight: StyleFontWeight::Normal,
                font_style: StyleFontStyle::Normal,
                text_align: StyleTextAlign::Start,
                visibility: StyleVisibility::Visible,
                white_space: StyleWhiteSpace::Normal,
                direction: StyleDirection::Ltr,
                vertical_align: StyleVerticalAlign::Baseline,
                border_collapse: StyleBorderCollapse::Separate,
            }
        }
        /// Every field at its highest-numbered variant — the worst case for a
        /// mask that is one bit too narrow.
        const fn saturated() -> Self {
            Self {
                display: LayoutDisplay::Contents,
                position: LayoutPosition::Sticky,
                float: LayoutFloat::Right,
                overflow_x: LayoutOverflow::Clip,
                overflow_y: LayoutOverflow::Clip,
                box_sizing: LayoutBoxSizing::BorderBox,
                flex_direction: LayoutFlexDirection::ColumnReverse,
                flex_wrap: LayoutFlexWrap::WrapReverse,
                justify_content: LayoutJustifyContent::SpaceEvenly,
                align_items: LayoutAlignItems::Baseline,
                align_content: LayoutAlignContent::SpaceAround,
                writing_mode: LayoutWritingMode::VerticalLr,
                clear: LayoutClear::Both,
                font_weight: StyleFontWeight::Bolder,
                font_style: StyleFontStyle::Oblique,
                text_align: StyleTextAlign::End,
                visibility: StyleVisibility::Collapse,
                white_space: StyleWhiteSpace::BreakSpaces,
                direction: StyleDirection::Rtl,
                vertical_align: StyleVerticalAlign::TextBottom,
                border_collapse: StyleBorderCollapse::Collapse,
            }
        }
        fn encode(self) -> u64 {
            encode_tier1(
                self.display,
                self.position,
                self.float,
                self.overflow_x,
                self.overflow_y,
                self.box_sizing,
                self.flex_direction,
                self.flex_wrap,
                self.justify_content,
                self.align_items,
                self.align_content,
                self.writing_mode,
                self.clear,
                self.font_weight,
                self.font_style,
                self.text_align,
                self.visibility,
                self.white_space,
                self.direction,
                self.vertical_align,
                self.border_collapse,
            )
        }
        fn decode(t1: u64) -> Self {
            Self {
                display: decode_display(t1),
                position: decode_position(t1),
                float: decode_float(t1),
                overflow_x: decode_overflow_x(t1),
                overflow_y: decode_overflow_y(t1),
                box_sizing: decode_box_sizing(t1),
                flex_direction: decode_flex_direction(t1),
                flex_wrap: decode_flex_wrap(t1),
                justify_content: decode_justify_content(t1),
                align_items: decode_align_items(t1),
                align_content: decode_align_content(t1),
                writing_mode: decode_writing_mode(t1),
                clear: decode_clear(t1),
                font_weight: decode_font_weight(t1),
                font_style: decode_font_style(t1),
                text_align: decode_text_align(t1),
                visibility: decode_visibility(t1),
                white_space: decode_white_space(t1),
                direction: decode_direction(t1),
                vertical_align: decode_vertical_align(t1),
                border_collapse: decode_border_collapse(t1),
            }
        }
    }
    // =========================================================================
    // u8 decoders — every byte outside the variant range must fall back to the
    // CSS initial value (the u8-0 slot), never panic, never alias a real variant
    // =========================================================================
    /// `first_invalid` = the first u8 that has no variant. Every byte from there
    /// to `u8::MAX` must decode to `initial`.
    fn assert_u8_fallback<T: PartialEq + core::fmt::Debug + Copy>(
        name: &str,
        decode: fn(u8) -> T,
        first_invalid: u8,
        initial: T,
    ) {
        for v in first_invalid..=u8::MAX {
            assert_eq!(
                decode(v),
                initial,
                "{name}: out-of-range byte {v} must fall back to the CSS initial value",
            );
        }
    }
    #[test]
    fn out_of_range_u8_falls_back_to_css_initial_for_every_enum() {
        assert_u8_fallback("display", layout_display_from_u8, 23, LayoutDisplay::Block);
        assert_u8_fallback("position", layout_position_from_u8, 5, LayoutPosition::Static);
        assert_u8_fallback("float", layout_float_from_u8, 3, LayoutFloat::None);
        assert_u8_fallback("overflow", layout_overflow_from_u8, 5, LayoutOverflow::Visible);
        assert_u8_fallback(
            "box_sizing",
            layout_box_sizing_from_u8,
            2,
            LayoutBoxSizing::ContentBox,
        );
        assert_u8_fallback(
            "flex_direction",
            layout_flex_direction_from_u8,
            4,
            LayoutFlexDirection::Row,
        );
        assert_u8_fallback("flex_wrap", layout_flex_wrap_from_u8, 3, LayoutFlexWrap::NoWrap);
        assert_u8_fallback(
            "justify_content",
            layout_justify_content_from_u8,
            8,
            LayoutJustifyContent::FlexStart,
        );
        assert_u8_fallback(
            "align_items",
            layout_align_items_from_u8,
            5,
            LayoutAlignItems::Stretch,
        );
        assert_u8_fallback("align_self", layout_align_self_from_u8, 6, LayoutAlignSelf::Auto);
        assert_u8_fallback(
            "justify_self",
            layout_justify_self_from_u8,
            5,
            LayoutJustifySelf::Auto,
        );
        assert_u8_fallback(
            "justify_items",
            layout_justify_items_from_u8,
            4,
            LayoutJustifyItems::Stretch,
        );
        assert_u8_fallback(
            "grid_auto_flow",
            layout_grid_auto_flow_from_u8,
            4,
            LayoutGridAutoFlow::Row,
        );
        assert_u8_fallback(
            "align_content",
            layout_align_content_from_u8,
            6,
            LayoutAlignContent::Stretch,
        );
        assert_u8_fallback(
            "writing_mode",
            layout_writing_mode_from_u8,
            3,
            LayoutWritingMode::HorizontalTb,
        );
        assert_u8_fallback("clear", layout_clear_from_u8, 4, LayoutClear::None);
        assert_u8_fallback(
            "font_weight",
            style_font_weight_from_u8,
            11,
            StyleFontWeight::Normal,
        );
        assert_u8_fallback("font_style", style_font_style_from_u8, 3, StyleFontStyle::Normal);
        assert_u8_fallback("text_align", style_text_align_from_u8, 6, StyleTextAlign::Start);
        assert_u8_fallback(
            "visibility",
            style_visibility_from_u8,
            3,
            StyleVisibility::Visible,
        );
        assert_u8_fallback(
            "white_space",
            style_white_space_from_u8,
            6,
            StyleWhiteSpace::Normal,
        );
        assert_u8_fallback("direction", style_direction_from_u8, 2, StyleDirection::Ltr);
        assert_u8_fallback(
            "vertical_align",
            style_vertical_align_from_u8,
            8,
            StyleVerticalAlign::Baseline,
        );
        assert_u8_fallback(
            "border_collapse",
            border_collapse_from_u8,
            2,
            StyleBorderCollapse::Separate,
        );
        assert_u8_fallback("border_style", border_style_from_u8, 10, BorderStyle::None);
        assert_u8_fallback("size_metric", size_metric_from_u8, 12, SizeMetric::Px);
    }
    #[test]
    fn display_documented_sentinel_31_decodes_to_block() {
        // The module doc promises 0x1F is the "look it up in the slow path"
        // sentinel and that decoding it yields the default rather than a
        // garbage variant. 31 is also DISPLAY_MASK, so a saturated tier1 u64
        // lands exactly here.
        assert_eq!(layout_display_from_u8(31), LayoutDisplay::Block);
        assert_eq!(layout_display_from_u8(u8::MAX), LayoutDisplay::Block);
    }
    #[test]
    fn every_variant_of_every_enum_fits_its_tier1_mask() {
        // The existing suite checks only the highest variant per enum. If a new
        // variant is inserted in the middle with a hand-written u8 above the
        // mask width, that check still passes while the encoding silently
        // corrupts the neighbouring bit run. Check the whole variant set.
        fn fits(name: &str, val: u8, mask: u64) {
            assert!(
                u64::from(val) & !mask == 0,
                "{name}: encoded u8 {val} does not fit mask {mask:#b}",
            );
        }
        for v in ALL_DISPLAY {
            fits("display", layout_display_to_u8(v), DISPLAY_MASK);
        }
        for v in [
            LayoutPosition::Static,
            LayoutPosition::Relative,
            LayoutPosition::Absolute,
            LayoutPosition::Fixed,
            LayoutPosition::Sticky,
        ] {
            fits("position", layout_position_to_u8(v), POSITION_MASK);
        }
        for v in [LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right] {
            fits("float", layout_float_to_u8(v), FLOAT_MASK);
        }
        for v in [
            LayoutOverflow::Visible,
            LayoutOverflow::Hidden,
            LayoutOverflow::Scroll,
            LayoutOverflow::Auto,
            LayoutOverflow::Clip,
        ] {
            fits("overflow", layout_overflow_to_u8(v), OVERFLOW_MASK);
        }
        for v in [LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox] {
            fits("box_sizing", layout_box_sizing_to_u8(v), BOX_SIZING_MASK);
        }
        for v in [
            LayoutFlexDirection::Row,
            LayoutFlexDirection::RowReverse,
            LayoutFlexDirection::Column,
            LayoutFlexDirection::ColumnReverse,
        ] {
            fits("flex_direction", layout_flex_direction_to_u8(v), FLEX_DIR_MASK);
        }
        for v in [
            LayoutFlexWrap::NoWrap,
            LayoutFlexWrap::Wrap,
            LayoutFlexWrap::WrapReverse,
        ] {
            fits("flex_wrap", layout_flex_wrap_to_u8(v), FLEX_WRAP_MASK);
        }
        for v in [
            LayoutJustifyContent::FlexStart,
            LayoutJustifyContent::FlexEnd,
            LayoutJustifyContent::Start,
            LayoutJustifyContent::End,
            LayoutJustifyContent::Center,
            LayoutJustifyContent::SpaceBetween,
            LayoutJustifyContent::SpaceAround,
            LayoutJustifyContent::SpaceEvenly,
        ] {
            fits("justify_content", layout_justify_content_to_u8(v), JUSTIFY_MASK);
        }
        for v in [
            LayoutAlignItems::Stretch,
            LayoutAlignItems::Center,
            LayoutAlignItems::Start,
            LayoutAlignItems::End,
            LayoutAlignItems::Baseline,
        ] {
            fits("align_items", layout_align_items_to_u8(v), ALIGN_MASK);
        }
        for v in [
            LayoutAlignSelf::Auto,
            LayoutAlignSelf::Stretch,
            LayoutAlignSelf::Center,
            LayoutAlignSelf::Start,
            LayoutAlignSelf::End,
            LayoutAlignSelf::Baseline,
        ] {
            fits("align_self", layout_align_self_to_u8(v), ALIGN_SELF_MASK);
        }
        for v in [
            LayoutJustifySelf::Auto,
            LayoutJustifySelf::Start,
            LayoutJustifySelf::End,
            LayoutJustifySelf::Center,
            LayoutJustifySelf::Stretch,
        ] {
            fits("justify_self", layout_justify_self_to_u8(v), JUSTIFY_SELF_MASK);
        }
        for v in [
            LayoutJustifyItems::Stretch,
            LayoutJustifyItems::Start,
            LayoutJustifyItems::End,
            LayoutJustifyItems::Center,
        ] {
            fits("justify_items", layout_justify_items_to_u8(v), JUSTIFY_ITEMS_MASK);
        }
        for v in [
            LayoutGridAutoFlow::Row,
            LayoutGridAutoFlow::Column,
            LayoutGridAutoFlow::RowDense,
            LayoutGridAutoFlow::ColumnDense,
        ] {
            fits("grid_auto_flow", layout_grid_auto_flow_to_u8(v), GRID_AUTO_FLOW_MASK);
        }
        for v in [
            LayoutAlignContent::Stretch,
            LayoutAlignContent::Center,
            LayoutAlignContent::Start,
            LayoutAlignContent::End,
            LayoutAlignContent::SpaceBetween,
            LayoutAlignContent::SpaceAround,
        ] {
            fits("align_content", layout_align_content_to_u8(v), ALIGN_MASK);
        }
        for v in [
            LayoutWritingMode::HorizontalTb,
            LayoutWritingMode::VerticalRl,
            LayoutWritingMode::VerticalLr,
        ] {
            fits("writing_mode", layout_writing_mode_to_u8(v), WRITING_MODE_MASK);
        }
        for v in [
            LayoutClear::None,
            LayoutClear::Left,
            LayoutClear::Right,
            LayoutClear::Both,
        ] {
            fits("clear", layout_clear_to_u8(v), CLEAR_MASK);
        }
        for v in [
            StyleFontWeight::Normal,
            StyleFontWeight::W100,
            StyleFontWeight::W200,
            StyleFontWeight::W300,
            StyleFontWeight::W500,
            StyleFontWeight::W600,
            StyleFontWeight::Bold,
            StyleFontWeight::W800,
            StyleFontWeight::W900,
            StyleFontWeight::Lighter,
            StyleFontWeight::Bolder,
        ] {
            fits("font_weight", style_font_weight_to_u8(v), FONT_WEIGHT_MASK);
        }
        for v in [
            StyleFontStyle::Normal,
            StyleFontStyle::Italic,
            StyleFontStyle::Oblique,
        ] {
            fits("font_style", style_font_style_to_u8(v), FONT_STYLE_MASK);
        }
        for v in [
            StyleTextAlign::Left,
            StyleTextAlign::Center,
            StyleTextAlign::Right,
            StyleTextAlign::Justify,
            StyleTextAlign::Start,
            StyleTextAlign::End,
        ] {
            fits("text_align", style_text_align_to_u8(v), TEXT_ALIGN_MASK);
        }
        for v in [
            StyleVisibility::Visible,
            StyleVisibility::Hidden,
            StyleVisibility::Collapse,
        ] {
            fits("visibility", style_visibility_to_u8(v), VISIBILITY_MASK);
        }
        for v in [
            StyleWhiteSpace::Normal,
            StyleWhiteSpace::Pre,
            StyleWhiteSpace::Nowrap,
            StyleWhiteSpace::PreWrap,
            StyleWhiteSpace::PreLine,
            StyleWhiteSpace::BreakSpaces,
        ] {
            fits("white_space", style_white_space_to_u8(v), WHITE_SPACE_MASK);
        }
        for v in [StyleDirection::Ltr, StyleDirection::Rtl] {
            fits("direction", style_direction_to_u8(v), DIRECTION_MASK);
        }
        for v in [
            StyleVerticalAlign::Baseline,
            StyleVerticalAlign::Top,
            StyleVerticalAlign::Middle,
            StyleVerticalAlign::Bottom,
            StyleVerticalAlign::Sub,
            StyleVerticalAlign::Superscript,
            StyleVerticalAlign::TextTop,
            StyleVerticalAlign::TextBottom,
            StyleVerticalAlign::Percentage(PercentageValue::new(50.0)),
            StyleVerticalAlign::Length(PixelValue::px(4.0)),
        ] {
            fits("vertical_align", style_vertical_align_to_u8(v), VERTICAL_ALIGN_MASK);
        }
        for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
            fits("border_collapse", border_collapse_to_u8(v), BORDER_COLLAPSE_MASK);
        }
        // border-style gets a 4-bit nibble inside the packed u16, not a tier1 mask.
        for v in ALL_BORDER_STYLE {
            fits("border_style", border_style_to_u8(v), 0x0F);
        }
        // SizeMetric gets the low 4 bits of the pixel-value u32.
        for v in ALL_SIZE_METRIC {
            fits("size_metric", size_metric_to_u8(v), 0x0F);
        }
    }
    #[test]
    fn vertical_align_percentage_and_length_collapse_to_baseline() {
        // Documented lossy fallback: the 3-bit tier1 slot cannot carry a
        // length/percentage payload, so these must encode as 0 (Baseline) and
        // the caller is expected to take the slow path. What must NOT happen is
        // an out-of-range u8 leaking into the border-collapse bit next door.
        for v in [
            StyleVerticalAlign::Percentage(PercentageValue::new(0.0)),
            StyleVerticalAlign::Percentage(PercentageValue::new(-100.0)),
            StyleVerticalAlign::Percentage(PercentageValue::new(1e9)),
            StyleVerticalAlign::Length(PixelValue::px(0.0)),
            StyleVerticalAlign::Length(PixelValue::px(-1e9)),
        ] {
            assert_eq!(style_vertical_align_to_u8(v), 0);
            assert_eq!(
                style_vertical_align_from_u8(style_vertical_align_to_u8(v)),
                StyleVerticalAlign::Baseline,
            );
        }
        // …and the collapse must not disturb the neighbouring border-collapse bit.
        let mut t = T1::initial();
        t.vertical_align = StyleVerticalAlign::Percentage(PercentageValue::new(150.0));
        t.border_collapse = StyleBorderCollapse::Collapse;
        let encoded = t.encode();
        assert_eq!(decode_vertical_align(encoded), StyleVerticalAlign::Baseline);
        assert_eq!(decode_border_collapse(encoded), StyleBorderCollapse::Collapse);
    }
    // =========================================================================
    // Tier1 u64 packing — field isolation, saturation, arbitrary input
    // =========================================================================
    #[test]
    fn tier1_each_field_at_max_does_not_leak_into_any_other_field() {
        // One field at its highest variant, all others at CSS initial. If a
        // shift/mask pair is wrong, the extra bits land in a neighbour and that
        // neighbour decodes to something other than its initial value.
        let sat = T1::saturated();
        let mut cases: Vec<(&str, T1)> = Vec::new();
        macro_rules! case {
            ($field:ident) => {{
                let mut t = T1::initial();
                t.$field = sat.$field;
                cases.push((stringify!($field), t));
            }};
        }
        case!(display);
        case!(position);
        case!(float);
        case!(overflow_x);
        case!(overflow_y);
        case!(box_sizing);
        case!(flex_direction);
        case!(flex_wrap);
        case!(justify_content);
        case!(align_items);
        case!(align_content);
        case!(writing_mode);
        case!(clear);
        case!(font_weight);
        case!(font_style);
        case!(text_align);
        case!(visibility);
        case!(white_space);
        case!(direction);
        case!(vertical_align);
        case!(border_collapse);
        assert_eq!(cases.len(), 21, "every tier1 field must be covered");
        for (name, expected) in cases {
            let decoded = T1::decode(expected.encode());
            assert_eq!(
                decoded, expected,
                "{name} at its max variant leaked into another tier1 field",
            );
        }
    }
    #[test]
    fn tier1_all_fields_saturated_roundtrips() {
        let sat = T1::saturated();
        assert_eq!(T1::decode(sat.encode()), sat);
        assert!(tier1_is_populated(sat.encode()));
    }
    #[test]
    fn tier1_encode_never_touches_the_grid_bits_or_bits_above_63() {
        // encode_tier1 owns bits [52:0] plus the populated flag at bit 63.
        // Bits [62:53] belong to align-self / justify-self / grid-auto-flow /
        // justify-items, which the cache builder ORs in separately. If
        // encode_tier1 ever spills into that window it silently rewrites a grid
        // property that it never received as an argument.
        const GRID_WINDOW: u64 = 0x3FF << 53; // bits 53..=62
        for t in [T1::initial(), T1::saturated()] {
            let encoded = t.encode();
            assert_eq!(
                encoded & GRID_WINDOW,
                0,
                "encode_tier1 wrote into the grid bit window [62:53]",
            );
            assert_eq!(encoded & TIER1_POPULATED_BIT, TIER1_POPULATED_BIT);
        }
        // The grid fields must survive being ORed on top of a saturated tier1.
        let base = T1::saturated().encode();
        let with_grid = base
            | ((u64::from(layout_align_self_to_u8(LayoutAlignSelf::Baseline))) << ALIGN_SELF_SHIFT)
            | ((u64::from(layout_justify_self_to_u8(LayoutJustifySelf::Stretch)))
                << JUSTIFY_SELF_SHIFT)
            | ((u64::from(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense)))
                << GRID_AUTO_FLOW_SHIFT)
            | ((u64::from(layout_justify_items_to_u8(LayoutJustifyItems::Center)))
                << JUSTIFY_ITEMS_SHIFT);
        assert_eq!(T1::decode(with_grid), T1::saturated());
        assert_eq!(
            layout_align_self_from_u8(((with_grid >> ALIGN_SELF_SHIFT) & ALIGN_SELF_MASK) as u8),
            LayoutAlignSelf::Baseline,
        );
        assert_eq!(
            layout_justify_self_from_u8(
                ((with_grid >> JUSTIFY_SELF_SHIFT) & JUSTIFY_SELF_MASK) as u8
            ),
            LayoutJustifySelf::Stretch,
        );
        assert_eq!(
            layout_grid_auto_flow_from_u8(
                ((with_grid >> GRID_AUTO_FLOW_SHIFT) & GRID_AUTO_FLOW_MASK) as u8
            ),
            LayoutGridAutoFlow::ColumnDense,
        );
        assert_eq!(
            layout_justify_items_from_u8(
                ((with_grid >> JUSTIFY_ITEMS_SHIFT) & JUSTIFY_ITEMS_MASK) as u8
            ),
            LayoutJustifyItems::Center,
        );
    }
    #[test]
    fn tier1_zero_is_unpopulated_but_still_decodes_to_css_initial() {
        // A `with_capacity`-allocated cache holds 0 for every node until the
        // builder fills it in. Reading such a node must be safe and yield the
        // CSS initial value, not a garbage variant.
        assert!(!tier1_is_populated(0));
        assert_eq!(T1::decode(0), T1::initial());
    }
    #[test]
    fn tier1_decodes_arbitrary_u64_deterministically_without_panic() {
        // u64::MAX means every mask reads all-ones. Each decoder must clamp to a
        // real variant (usually the initial value via the `_` arm) rather than
        // panic or produce an out-of-range discriminant.
        let m = u64::MAX;
        assert!(tier1_is_populated(m));
        assert_eq!(
            T1::decode(m),
            T1 {
                display: LayoutDisplay::Block,          // 31 → fallback
                position: LayoutPosition::Static,       // 7  → fallback
                float: LayoutFloat::None,               // 3  → fallback
                overflow_x: LayoutOverflow::Visible,    // 7  → fallback
                overflow_y: LayoutOverflow::Visible,    // 7  → fallback
                box_sizing: LayoutBoxSizing::BorderBox, // 1  → real variant
                flex_direction: LayoutFlexDirection::ColumnReverse, // 3 → real
                flex_wrap: LayoutFlexWrap::NoWrap,      // 3  → fallback
                justify_content: LayoutJustifyContent::SpaceEvenly, // 7 → real
                align_items: LayoutAlignItems::Stretch, // 7  → fallback
                align_content: LayoutAlignContent::Stretch, // 7 → fallback
                writing_mode: LayoutWritingMode::HorizontalTb, // 3 → fallback
                clear: LayoutClear::Both,               // 3  → real variant
                font_weight: StyleFontWeight::Normal,   // 15 → fallback
                font_style: StyleFontStyle::Normal,     // 3  → fallback
                text_align: StyleTextAlign::Start,      // 7  → fallback
                visibility: StyleVisibility::Visible,   // 3  → fallback
                white_space: StyleWhiteSpace::Normal,   // 7  → fallback
                direction: StyleDirection::Rtl,         // 1  → real variant
                vertical_align: StyleVerticalAlign::TextBottom, // 7 → real
                border_collapse: StyleBorderCollapse::Collapse, // 1 → real
            },
        );
        // A deterministic sweep of adversarial bit patterns: none may panic.
        let mut x: u64 = 0x9E37_79B9_7F4A_7C15;
        for _ in 0..4096 {
            let _ = T1::decode(x);
            let _ = tier1_is_populated(x);
            x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
        }
        for x in [0u64, 1, u64::MAX, 0xAAAA_AAAA_AAAA_AAAA, 0x5555_5555_5555_5555] {
            let _ = T1::decode(x);
        }
    }
    // =========================================================================
    // Packed border styles (u16, four nibbles)
    // =========================================================================
    #[test]
    fn border_styles_packed_roundtrip_for_all_10000_combinations() {
        for top in ALL_BORDER_STYLE {
            for right in ALL_BORDER_STYLE {
                for bottom in ALL_BORDER_STYLE {
                    for left in ALL_BORDER_STYLE {
                        let packed = encode_border_styles_packed(top, right, bottom, left);
                        assert_eq!(decode_border_top_style(packed), top);
                        assert_eq!(decode_border_right_style(packed), right);
                        assert_eq!(decode_border_bottom_style(packed), bottom);
                        assert_eq!(decode_border_left_style(packed), left);
                    }
                }
            }
        }
    }
    #[test]
    fn border_styles_packed_nibbles_do_not_alias() {
        // Only the top nibble set — the other three must read as None (0),
        // not pick up bits from their neighbours.
        let packed = encode_border_styles_packed(
            BorderStyle::Outset, // 9 — the widest valid nibble
            BorderStyle::None,
            BorderStyle::None,
            BorderStyle::None,
        );
        assert_eq!(packed, 0x0009);
        assert_eq!(decode_border_top_style(packed), BorderStyle::Outset);
        assert_eq!(decode_border_right_style(packed), BorderStyle::None);
        assert_eq!(decode_border_bottom_style(packed), BorderStyle::None);
        assert_eq!(decode_border_left_style(packed), BorderStyle::None);
        let packed = encode_border_styles_packed(
            BorderStyle::None,
            BorderStyle::None,
            BorderStyle::None,
            BorderStyle::Outset,
        );
        assert_eq!(packed, 0x9000);
        assert_eq!(decode_border_left_style(packed), BorderStyle::Outset);
        assert_eq!(decode_border_top_style(packed), BorderStyle::None);
    }
    #[test]
    fn border_styles_packed_decodes_garbage_u16_without_panic() {
        // Nibbles 10..=15 have no variant. `0xFFFF` (an all-ones cold-tier row,
        // e.g. from a misinitialised buffer) must decode to None everywhere.
        for packed in [0u16, 0xFFFF, 0xAAAA, 0x5555, u16::MAX / 2] {
            let _ = decode_border_top_style(packed);
            let _ = decode_border_right_style(packed);
            let _ = decode_border_bottom_style(packed);
            let _ = decode_border_left_style(packed);
        }
        assert_eq!(decode_border_top_style(0xFFFF), BorderStyle::None);
        assert_eq!(decode_border_right_style(0xFFFF), BorderStyle::None);
        assert_eq!(decode_border_bottom_style(0xFFFF), BorderStyle::None);
        assert_eq!(decode_border_left_style(0xFFFF), BorderStyle::None);
        // Exhaustive: no u16 may panic any of the four decoders.
        for packed in 0..=u16::MAX {
            let _ = decode_border_top_style(packed);
            let _ = decode_border_left_style(packed);
        }
    }
    // =========================================================================
    // Colors (u32 0xRRGGBBAA)
    // =========================================================================
    #[test]
    fn color_u32_channel_order_is_rrggbbaa() {
        let c = ColorU {
            r: 0x12,
            g: 0x34,
            b: 0x56,
            a: 0x78,
        };
        assert_eq!(encode_color_u32(&c), 0x1234_5678);
        assert_eq!(decode_color_u32(0x1234_5678), Some(c));
    }
    #[test]
    fn color_u32_roundtrips_every_boundary_channel_combination() {
        for r in [0u8, 1, 127, 254, 255] {
            for g in [0u8, 1, 127, 254, 255] {
                for b in [0u8, 1, 127, 254, 255] {
                    for a in [0u8, 1, 127, 254, 255] {
                        let c = ColorU { r, g, b, a };
                        let encoded = encode_color_u32(&c);
                        if encoded == 0 {
                            // Only fully-transparent black hits the unset sentinel.
                            assert_eq!((r, g, b, a), (0, 0, 0, 0));
                            assert_eq!(decode_color_u32(encoded), None);
                        } else {
                            assert_eq!(decode_color_u32(encoded), Some(c));
                        }
                    }
                }
            }
        }
    }
    #[test]
    fn color_u32_transparent_black_is_the_documented_unset_collision() {
        // Documented limitation, pinned so it cannot regress silently:
        // rgba(0,0,0,0) is indistinguishable from "property never set".
        let transparent_black = ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 0,
        };
        assert_eq!(encode_color_u32(&transparent_black), 0);
        assert_eq!(decode_color_u32(0), None);
        // Every other alpha-0 color must still survive the round-trip.
        let transparent_red = ColorU {
            r: 255,
            g: 0,
            b: 0,
            a: 0,
        };
        assert_eq!(encode_color_u32(&transparent_red), 0xFF00_0000);
        assert_eq!(decode_color_u32(0xFF00_0000), Some(transparent_red));
        // …including "black but only just" (alpha 1).
        let almost = ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 1,
        };
        assert_eq!(decode_color_u32(encode_color_u32(&almost)), Some(almost));
    }
    #[test]
    fn color_u32_max_decodes_to_opaque_white() {
        assert_eq!(
            decode_color_u32(u32::MAX),
            Some(ColorU {
                r: 255,
                g: 255,
                b: 255,
                a: 255
            }),
        );
    }
    // =========================================================================
    // PixelValue u32 (4-bit metric + 28-bit signed fixed-point ×1000)
    // =========================================================================
    #[test]
    fn pixel_value_u32_sentinels_all_decode_to_none_and_are_distinct() {
        let sentinels = [
            U32_SENTINEL,
            U32_AUTO,
            U32_NONE,
            U32_INHERIT,
            U32_INITIAL,
            U32_MIN_CONTENT,
            U32_MAX_CONTENT,
        ];
        for (i, a) in sentinels.iter().enumerate() {
            assert!(
                *a >= U32_SENTINEL_THRESHOLD,
                "sentinel {a:#X} sits below the threshold and would decode as a value",
            );
            assert_eq!(decode_pixel_value_u32(*a), None);
            for b in &sentinels[i + 1..] {
                assert_ne!(a, b, "two u32 sentinels share a bit pattern");
            }
        }
        // The threshold itself is the lowest reserved value.
        assert_eq!(decode_pixel_value_u32(U32_SENTINEL_THRESHOLD), None);
        // One below the threshold is still a real (negative) value.
        assert!(decode_pixel_value_u32(U32_SENTINEL_THRESHOLD - 1).is_some());
    }
    #[test]
    fn pixel_value_u32_roundtrips_at_the_28_bit_boundaries_for_every_metric() {
        // ±2^27 is the documented edge of the 28-bit signed fixed-point field.
        for metric in ALL_SIZE_METRIC {
            for raw in [0isize, 1, -1, 1000, -1000, 134_217_727, -134_217_728] {
                let pv = pv_raw(metric, raw);
                let encoded = encode_pixel_value_u32(&pv);
                // Raw -1 with a high metric nibble collides with the sentinel
                // band; that is asserted separately in the bug test below.
                if encoded >= U32_SENTINEL_THRESHOLD {
                    continue;
                }
                let decoded = decode_pixel_value_u32(encoded)
                    .unwrap_or_else(|| panic!("{metric:?} raw {raw} decoded as a sentinel"));
                assert_eq!(decoded.metric, metric, "metric nibble lost for raw {raw}");
                assert_eq!(
                    decoded.number.number(),
                    raw,
                    "{metric:?}: raw {raw} did not survive the round-trip",
                );
            }
        }
    }
    #[test]
    fn pixel_value_u32_out_of_28_bit_range_returns_sentinel() {
        for metric in ALL_SIZE_METRIC {
            for raw in [
                134_217_728isize,
                -134_217_729,
                1_000_000_000,
                -1_000_000_000,
                isize::MAX,
                isize::MIN,
            ] {
                assert_eq!(
                    encode_pixel_value_u32(&pv_raw(metric, raw)),
                    U32_SENTINEL,
                    "{metric:?}: raw {raw} is outside 28 bits and must escape to tier 3",
                );
            }
        }
    }
    #[test]
    fn pixel_value_u32_decodes_every_low_bit_pattern_without_panic() {
        // Metric nibbles 12..=15 have no SizeMetric — they must clamp to Px.
        for nibble in 12u32..16 {
            let encoded = (1u32 << 4) | nibble;
            let decoded = decode_pixel_value_u32(encoded).unwrap();
            assert_eq!(decoded.metric, SizeMetric::Px);
            assert_eq!(decoded.number.number(), 1);
        }
        // Sign extension: the top bit of the 28-bit field must arithmetic-shift.
        let neg = decode_pixel_value_u32(0x8000_0000).unwrap();
        assert_eq!(neg.metric, SizeMetric::Px);
        assert_eq!(neg.number.number(), -134_217_728);
    }
    #[test]
    fn pixel_value_u32_negative_0_001_in_vh_vmin_vmax_collides_with_sentinels() {
        // BUG (encode_pixel_value_u32 / decode_pixel_value_u32):
        //
        // raw == -1 (i.e. -0.001 of a unit) sign-extends to 0xFFFF_FFFF, and
        // `<< 4` leaves 0xFFFF_FFF0. ORing in a metric nibble >= 9 pushes the
        // word into the reserved sentinel band (>= 0xFFFF_FFF9):
        //
        //   -0.001vh   → 0xFFFF_FFF9 == U32_MAX_CONTENT
        //   -0.001vmin → 0xFFFF_FFFA == U32_MIN_CONTENT
        //   -0.001vmax → 0xFFFF_FFFB == U32_INITIAL
        //
        // So a legal (if tiny) negative viewport-relative length is written into
        // the cache as `max-content` / `min-content` / `initial`, and the decoder
        // reports None (unset) instead of the value. The encoder's range check
        // guards the 28-bit magnitude but not the sentinel band it lands in.
        //
        // Fix would be to reject any encoding that lands >= U32_SENTINEL_THRESHOLD
        // and escape to tier 3 instead.
        // FIXED (as this test's own comment prescribed): a raw -1 with a high metric
        // nibble packs into the reserved sentinel band, so the encoder now ESCAPES it to
        // U32_SENTINEL (tier 3) rather than emitting a value that decodes as a wrong,
        // aliased sentinel. decode() therefore returns None ("not in the fast cache —
        // look in tier 3"), which is the safe, non-aliasing outcome.
        for metric in [SizeMetric::Vh, SizeMetric::Vmin, SizeMetric::Vmax] {
            let pv = pv_raw(metric, -1);
            let encoded = encode_pixel_value_u32(&pv);
            assert_eq!(encoded, U32_SENTINEL, "{metric:?}: raw -1 must escape to tier 3");
            assert_eq!(
                decode_pixel_value_u32(encoded),
                None,
                "{metric:?}: an escaped value decodes as None, never an aliased sentinel",
            );
        }
    }
    // =========================================================================
    // Resolved px i16 (×10)
    // =========================================================================
    #[test]
    fn resolved_px_i16_nan_and_infinity_are_defined_and_do_not_panic() {
        // `f32 as i32` saturates: NaN → 0, +inf → i32::MAX, -inf → i32::MIN.
        assert_eq!(encode_resolved_px_i16(f32::NAN), 0);
        assert_eq!(encode_resolved_px_i16(-f32::NAN), 0);
        assert_eq!(encode_resolved_px_i16(f32::INFINITY), I16_SENTINEL);
        assert_eq!(encode_resolved_px_i16(f32::NEG_INFINITY), I16_SENTINEL);
        assert_eq!(encode_resolved_px_i16(f32::MAX), I16_SENTINEL);
        assert_eq!(encode_resolved_px_i16(f32::MIN), I16_SENTINEL);
        // Subnormals round to zero rather than blowing up.
        assert_eq!(encode_resolved_px_i16(f32::MIN_POSITIVE), 0);
        assert_eq!(encode_resolved_px_i16(-0.0), 0);
    }
    #[test]
    fn resolved_px_i16_saturates_exactly_at_the_documented_range() {
        // Doc: -3276.8 ..= +3276.3 px at 0.1px precision.
        assert_eq!(encode_resolved_px_i16(3276.3), 32763);
        assert_eq!(encode_resolved_px_i16(-3276.8), -32768);
        // One tick outside in either direction escapes to tier 3.
        assert_eq!(encode_resolved_px_i16(3276.4), I16_SENTINEL);
        assert_eq!(encode_resolved_px_i16(-3276.9), I16_SENTINEL);
        assert_eq!(encode_resolved_px_i16(1e9), I16_SENTINEL);
        assert_eq!(encode_resolved_px_i16(-1e9), I16_SENTINEL);
    }
    #[test]
    fn resolved_px_i16_sentinels_decode_to_none_and_are_distinct() {
        let sentinels = [I16_SENTINEL, I16_AUTO, I16_INHERIT, I16_INITIAL];
        for (i, a) in sentinels.iter().enumerate() {
            assert!(*a >= I16_SENTINEL_THRESHOLD);
            assert_eq!(decode_resolved_px_i16(*a), None);
            for b in &sentinels[i + 1..] {
                assert_ne!(a, b, "two i16 sentinels share a bit pattern");
            }
        }
        assert_eq!(decode_resolved_px_i16(I16_SENTINEL_THRESHOLD), None);
        assert_eq!(decode_resolved_px_i16(I16_SENTINEL_THRESHOLD - 1), Some(3276.3));
    }
    #[test]
    fn resolved_px_i16_every_non_sentinel_value_roundtrips() {
        // Exhaustive over the whole non-sentinel i16 domain: decode → encode must
        // be the identity, or a value written by one frame reads back shifted on
        // the next.
        for v in i16::MIN..I16_SENTINEL_THRESHOLD {
            let px = decode_resolved_px_i16(v)
                .unwrap_or_else(|| panic!("{v} is below the threshold but decoded as a sentinel"));
            assert_eq!(
                encode_resolved_px_i16(px),
                v,
                "i16 {v} decoded to {px} px which re-encodes to a different i16",
            );
        }
    }
    // =========================================================================
    // Flex u16 (×100)
    // =========================================================================
    #[test]
    fn flex_u16_nan_infinity_and_negatives_are_defined_and_do_not_panic() {
        assert_eq!(encode_flex_u16(f32::NAN), 0);
        assert_eq!(encode_flex_u16(f32::INFINITY), U16_SENTINEL);
        assert_eq!(encode_flex_u16(f32::NEG_INFINITY), U16_SENTINEL);
        assert_eq!(encode_flex_u16(f32::MAX), U16_SENTINEL);
        // flex-grow/shrink are non-negative; a negative escapes to tier 3.
        assert_eq!(encode_flex_u16(-1.0), U16_SENTINEL);
        assert_eq!(encode_flex_u16(-0.01), U16_SENTINEL);
        // …but -0.0 and values that round to zero clamp to 0, not to a sentinel.
        assert_eq!(encode_flex_u16(-0.0), 0);
        assert_eq!(encode_flex_u16(0.0), 0);
    }
    #[test]
    fn flex_u16_saturates_exactly_at_the_documented_range() {
        // Doc: 0.00 ..= 655.27 at 0.01 precision.
        assert_eq!(encode_flex_u16(655.27), 65527);
        assert_eq!(decode_flex_u16(65527), Some(655.27));
        // 65528 is representable but 65529 is the threshold.
        assert_eq!(encode_flex_u16(655.28), 65528);
        assert_eq!(encode_flex_u16(655.29), U16_SENTINEL);
        assert_eq!(encode_flex_u16(1e9), U16_SENTINEL);
    }
    #[test]
    fn flex_u16_sentinel_band_decodes_to_none() {
        for v in U16_SENTINEL_THRESHOLD..=u16::MAX {
            assert_eq!(decode_flex_u16(v), None, "u16 {v} is reserved and must decode as None");
        }
        assert_eq!(U16_SENTINEL, u16::MAX);
        assert!(decode_flex_u16(U16_SENTINEL_THRESHOLD - 1).is_some());
    }
    #[test]
    fn flex_u16_every_non_sentinel_value_roundtrips() {
        for v in 0..U16_SENTINEL_THRESHOLD {
            let f = decode_flex_u16(v).unwrap_or_else(|| panic!("{v} decoded as a sentinel"));
            assert_eq!(encode_flex_u16(f), v, "u16 {v} → {f} → re-encoded differently");
        }
    }
    // =========================================================================
    // encode_css_pixel_as_i16 — CssPropertyValue → i16 keyword sentinels
    // =========================================================================
    #[test]
    fn css_pixel_as_i16_maps_every_keyword_to_its_own_sentinel() {
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::Auto),
            I16_AUTO,
        );
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::Initial),
            I16_INITIAL,
        );
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::Inherit),
            I16_INHERIT,
        );
        // None / Revert / Unset have no dedicated slot → generic sentinel (slow path).
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::None),
            I16_SENTINEL,
        );
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::Revert),
            I16_SENTINEL,
        );
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::Unset),
            I16_SENTINEL,
        );
    }
    #[test]
    fn css_pixel_as_i16_only_takes_the_fast_path_for_absolute_px() {
        // Only SizeMetric::Px can be pre-resolved without layout context;
        // every relative unit must escape to the cascade.
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(12.5))),
            125,
        );
        for metric in ALL_SIZE_METRIC {
            if metric == SizeMetric::Px {
                continue;
            }
            assert_eq!(
                encode_css_pixel_as_i16(&CssPropertyValue::Exact(pv_raw(metric, 12_500))),
                I16_SENTINEL,
                "{metric:?} needs resolution context and must not be pre-resolved",
            );
        }
    }
    #[test]
    fn css_pixel_as_i16_out_of_range_px_escapes_to_the_sentinel() {
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(1e6))),
            I16_SENTINEL,
        );
        assert_eq!(
            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(-1e6))),
            I16_SENTINEL,
        );
        // A px value that happens to land on a keyword sentinel would be
        // misread as `auto`/`inherit`; the range check must exclude the band.
        for raw in [I16_SENTINEL, I16_AUTO, I16_INHERIT, I16_INITIAL] {
            let px = f32::from(raw) / 10.0;
            assert_eq!(
                encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(px))),
                I16_SENTINEL,
                "{px} px must not silently encode as the keyword sentinel {raw}",
            );
        }
    }
    // =========================================================================
    // CompactLayoutCache — constructor invariants, bounds, predicates
    // =========================================================================
    #[test]
    fn empty_cache_is_the_neutral_element() {
        let c = CompactLayoutCache::empty();
        assert_eq!(c.node_count(), 0);
        assert!(c.tier1_enums.is_empty());
        assert!(c.tier2_dims.is_empty());
        assert!(c.tier2_cold.is_empty());
        assert!(c.tier2b_text.is_empty());
        assert!(c.font_dirty_nodes.is_empty());
        assert!(c.prev_font_hashes.is_empty());
        assert!(c.font_hash_to_families.is_empty());
        assert_eq!(c.dom_declared_flags, 0);
        // with_capacity(0) must produce exactly the same thing.
        assert_eq!(CompactLayoutCache::with_capacity(0), c);
    }
    #[test]
    fn with_capacity_keeps_every_tier_the_same_length() {
        for n in [0usize, 1, 2, 3, 17, 1024] {
            let c = CompactLayoutCache::with_capacity(n);
            assert_eq!(c.node_count(), n);
            assert_eq!(c.tier1_enums.len(), n);
            assert_eq!(c.tier2_dims.len(), n);
            assert_eq!(c.tier2_cold.len(), n);
            assert_eq!(c.tier2b_text.len(), n);
            assert_eq!(c.prev_font_hashes.len(), n);
            // Dirty list starts empty regardless of node count.
            assert!(c.font_dirty_nodes.is_empty());
            assert_eq!(c.dom_declared_flags, 0);
        }
    }
    #[test]
    fn freshly_allocated_node_reads_back_every_css_initial_value() {
        // `with_capacity` zeroes tier1 and default-fills tier2. A node the
        // builder never touched must still answer every getter with the CSS
        // initial value — this is what lets the builder skip unset properties.
        let c = CompactLayoutCache::with_capacity(3);
        for i in 0..3 {
            assert_eq!(T1::decode(c.tier1_enums[i]), T1::initial());
            assert_eq!(c.get_display(i), LayoutDisplay::Block);
            assert_eq!(c.get_position(i), LayoutPosition::Static);
            assert_eq!(c.get_float(i), LayoutFloat::None);
            assert_eq!(c.get_overflow_x(i), LayoutOverflow::Visible);
            assert_eq!(c.get_overflow_y(i), LayoutOverflow::Visible);
            assert_eq!(c.get_box_sizing(i), LayoutBoxSizing::ContentBox);
            assert_eq!(c.get_flex_direction(i), LayoutFlexDirection::Row);
            assert_eq!(c.get_flex_wrap(i), LayoutFlexWrap::NoWrap);
            assert_eq!(c.get_justify_content(i), LayoutJustifyContent::FlexStart);
            assert_eq!(c.get_align_items(i), LayoutAlignItems::Stretch);
            assert_eq!(c.get_align_content(i), LayoutAlignContent::Stretch);
            assert_eq!(c.get_writing_mode(i), LayoutWritingMode::HorizontalTb);
            assert_eq!(c.get_clear(i), LayoutClear::None);
            assert_eq!(c.get_font_weight(i), StyleFontWeight::Normal);
            assert_eq!(c.get_font_style(i), StyleFontStyle::Normal);
            assert_eq!(c.get_text_align(i), StyleTextAlign::Start);
            assert_eq!(c.get_visibility(i), StyleVisibility::Visible);
            assert_eq!(c.get_white_space(i), StyleWhiteSpace::Normal);
            assert_eq!(c.get_direction(i), StyleDirection::Ltr);
            assert_eq!(c.get_vertical_align(i), StyleVerticalAlign::Baseline);
            assert_eq!(c.get_border_collapse(i), StyleBorderCollapse::Separate);
            // Dimensions: auto / none, i.e. no decodable pixel value.
            assert_eq!(c.get_width_raw(i), U32_AUTO);
            assert_eq!(c.get_height_raw(i), U32_AUTO);
            assert_eq!(c.get_min_width_raw(i), U32_AUTO);
            assert_eq!(c.get_min_height_raw(i), U32_AUTO);
            assert_eq!(c.get_max_width_raw(i), U32_NONE);
            assert_eq!(c.get_max_height_raw(i), U32_NONE);
            assert_eq!(c.get_flex_basis_raw(i), U32_AUTO);
            assert_eq!(c.get_font_size_raw(i), U32_INITIAL);
            assert_eq!(decode_pixel_value_u32(c.get_width_raw(i)), None);
            assert_eq!(decode_pixel_value_u32(c.get_font_size_raw(i)), None);
            // Box model: zeros are real values (Some), offsets are auto (raw sentinel).
            assert_eq!(c.get_padding_top(i), Some(0.0));
            assert_eq!(c.get_padding_right(i), Some(0.0));
            assert_eq!(c.get_padding_bottom(i), Some(0.0));
            assert_eq!(c.get_padding_left(i), Some(0.0));
            assert_eq!(c.get_border_top_width(i), Some(0.0));
            assert_eq!(c.get_border_left_width(i), Some(0.0));
            // margin defaults to 0, NOT auto — centering must not kick in for free.
            assert_eq!(c.get_margin_top(i), Some(0.0));
            assert_eq!(c.get_margin_left(i), Some(0.0));
            assert!(!c.is_margin_top_auto(i));
            assert!(!c.is_margin_right_auto(i));
            assert!(!c.is_margin_bottom_auto(i));
            assert!(!c.is_margin_left_auto(i));
            // …but the inset properties DO default to auto.
            assert_eq!(c.get_top(i), I16_AUTO);
            assert_eq!(c.get_right(i), I16_AUTO);
            assert_eq!(c.get_bottom(i), I16_AUTO);
            assert_eq!(c.get_left(i), I16_AUTO);
            // Flex: grow 0, shrink 1 (the CSS defaults).
            assert_eq!(c.get_flex_grow(i), Some(0.0));
            assert_eq!(c.get_flex_shrink(i), Some(1.0));
            // Cold tier.
            assert_eq!(c.get_z_index(i), I16_AUTO);
            assert_eq!(c.get_border_styles_packed(i), 0);
            assert_eq!(c.get_border_top_style(i), BorderStyle::None);
            assert_eq!(c.get_border_right_style(i), BorderStyle::None);
            assert_eq!(c.get_border_bottom_style(i), BorderStyle::None);
            assert_eq!(c.get_border_left_style(i), BorderStyle::None);
            assert_eq!(c.get_border_top_color_raw(i), 0);
            assert_eq!(decode_color_u32(c.get_border_top_color_raw(i)), None);
            assert_eq!(c.get_border_top_left_radius_raw(i), I16_SENTINEL);
            assert_eq!(c.get_tab_size_raw(i), I16_SENTINEL);
            assert_eq!(c.get_border_spacing_h_raw(i), 0);
            assert_eq!(c.get_border_spacing_v_raw(i), 0);
            assert_eq!(c.get_opacity_raw(i), OPACITY_SENTINEL);
            assert_eq!(c.get_hot_flags(i), 0);
            assert_eq!(c.get_scrollbar_gutter_bits(i), SCROLLBAR_GUTTER_AUTO);
            // Every "has this rare prop" predicate must be false on a fresh node,
            // otherwise the fast path would take a cascade walk for every node.
            assert!(!c.has_transform(i));
            assert!(!c.has_transform_origin(i));
            assert!(!c.has_box_shadow(i));
            assert!(!c.has_text_decoration(i));
            assert!(!c.has_background(i));
            assert!(!c.has_clip_path(i));
            assert!(!c.has_scrollbar_css(i));
            assert!(!c.has_counter(i));
            assert!(!c.has_break(i));
            assert!(!c.has_text_orientation(i));
            assert!(!c.has_text_shadow(i));
            assert!(!c.has_backdrop_filter(i));
            assert!(!c.has_filter(i));
            assert!(!c.has_mix_blend_mode(i));
            // Text tier.
            assert_eq!(c.get_text_color_raw(i), 0);
            assert_eq!(c.get_font_family_hash(i), 0);
            assert_eq!(c.get_line_height(i), None); // "normal" → slow path
            assert_eq!(c.get_letter_spacing(i), Some(0.0));
            assert_eq!(c.get_word_spacing(i), Some(0.0));
            assert_eq!(c.get_text_indent(i), Some(0.0));
        }
    }
    #[test]
    fn hot_flag_predicates_read_only_their_own_bit() {
        let flags = [
            ("transform", HOT_FLAG_HAS_TRANSFORM),
            ("transform_origin", HOT_FLAG_HAS_TRANSFORM_ORIGIN),
            ("box_shadow", HOT_FLAG_HAS_BOX_SHADOW),
            ("text_decoration", HOT_FLAG_HAS_TEXT_DECORATION),
            ("background", HOT_FLAG_HAS_BACKGROUND),
            ("clip_path", HOT_FLAG_HAS_CLIP_PATH),
        ];
        for (name, bit) in flags {
            let mut c = CompactLayoutCache::with_capacity(1);
            c.tier2_cold[0].hot_flags = bit;
            let observed = [
                ("transform", c.has_transform(0)),
                ("transform_origin", c.has_transform_origin(0)),
                ("box_shadow", c.has_box_shadow(0)),
                ("text_decoration", c.has_text_decoration(0)),
                ("background", c.has_background(0)),
                ("clip_path", c.has_clip_path(0)),
            ];
            for (other, is_set) in observed {
                assert_eq!(
                    is_set,
                    other == name,
                    "hot_flags = {bit:#010b}: has_{other}() should be {}",
                    other == name,
                );
            }
            // The gutter field lives in bits 4-5 and must be unaffected.
            assert_eq!(c.get_scrollbar_gutter_bits(0), SCROLLBAR_GUTTER_AUTO);
        }
        // No two hot flags may share a bit, and none may overlap the gutter field.
        let mut seen = 0u8;
        for (_, bit) in flags {
            assert_eq!(seen & bit, 0, "two hot flags share bit {bit:#010b}");
            assert_eq!(
                bit & HOT_FLAG_SCROLLBAR_GUTTER_MASK,
                0,
                "hot flag {bit:#010b} overlaps the scrollbar-gutter field",
            );
            seen |= bit;
        }
    }
    #[test]
    fn scrollbar_gutter_bits_survive_a_fully_set_hot_flags_byte() {
        for gutter in [
            SCROLLBAR_GUTTER_AUTO,
            SCROLLBAR_GUTTER_STABLE,
            SCROLLBAR_GUTTER_BOTH_EDGES,
            SCROLLBAR_GUTTER_MIRROR,
        ] {
            let mut c = CompactLayoutCache::with_capacity(1);
            // Every boolean flag set *and* a gutter value: the gutter must still
            // read back cleanly out of the middle of the byte.
            c.tier2_cold[0].hot_flags = HOT_FLAG_HAS_TRANSFORM
                | HOT_FLAG_HAS_TRANSFORM_ORIGIN
                | HOT_FLAG_HAS_BOX_SHADOW
                | HOT_FLAG_HAS_TEXT_DECORATION
                | HOT_FLAG_HAS_BACKGROUND
                | HOT_FLAG_HAS_CLIP_PATH
                | (gutter << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT);
            assert_eq!(c.get_scrollbar_gutter_bits(0), gutter);
            assert!(c.has_transform(0));
            assert!(c.has_clip_path(0));
        }
        // An all-ones byte reads the max gutter value, never something out of range.
        let mut c = CompactLayoutCache::with_capacity(1);
        c.tier2_cold[0].hot_flags = u8::MAX;
        assert_eq!(c.get_scrollbar_gutter_bits(0), SCROLLBAR_GUTTER_MIRROR);
        assert!(c.get_scrollbar_gutter_bits(0) <= 3);
    }
    #[test]
    fn extra_flag_predicates_read_only_their_own_bit() {
        let flags = [
            ("scrollbar_css", EXTRA_FLAG_HAS_SCROLLBAR_CSS),
            ("counter", EXTRA_FLAG_HAS_COUNTER),
            ("break", EXTRA_FLAG_HAS_BREAK),
            ("text_orientation", EXTRA_FLAG_HAS_TEXT_ORIENTATION),
            ("text_shadow", EXTRA_FLAG_HAS_TEXT_SHADOW),
            ("backdrop_filter", EXTRA_FLAG_HAS_BACKDROP_FILTER),
            ("filter", EXTRA_FLAG_HAS_FILTER),
            ("mix_blend_mode", EXTRA_FLAG_HAS_MIX_BLEND_MODE),
        ];
        // All 8 bits must be distinct and together cover the whole byte.
        let mut seen = 0u8;
        for (_, bit) in flags {
            assert_eq!(seen & bit, 0, "two extra flags share bit {bit:#010b}");
            seen |= bit;
        }
        assert_eq!(seen, u8::MAX);
        for (name, bit) in flags {
            let mut c = CompactLayoutCache::with_capacity(1);
            c.tier2_cold[0].extra_flags = bit;
            let observed = [
                ("scrollbar_css", c.has_scrollbar_css(0)),
                ("counter", c.has_counter(0)),
                ("break", c.has_break(0)),
                ("text_orientation", c.has_text_orientation(0)),
                ("text_shadow", c.has_text_shadow(0)),
                ("backdrop_filter", c.has_backdrop_filter(0)),
                ("filter", c.has_filter(0)),
                ("mix_blend_mode", c.has_mix_blend_mode(0)),
            ];
            for (other, is_set) in observed {
                assert_eq!(
                    is_set,
                    other == name,
                    "extra_flags = {bit:#010b}: has_{other}() should be {}",
                    other == name,
                );
            }
            // Setting an extra flag must not make any hot-flag predicate fire.
            assert!(!c.has_transform(0));
            assert!(!c.has_background(0));
        }
    }
    #[test]
    fn dom_declared_flags_are_distinct_and_queryable() {
        let flags = [
            DOM_HAS_SHAPE_INSIDE,
            DOM_HAS_SHAPE_OUTSIDE,
            DOM_HAS_TEXT_JUSTIFY,
            DOM_HAS_TEXT_INDENT,
            DOM_HAS_COLUMN_COUNT,
            DOM_HAS_COLUMN_GAP,
            DOM_HAS_INITIAL_LETTER,
            DOM_HAS_INITIAL_LETTER_ALIGN,
            DOM_HAS_LINE_CLAMP,
            DOM_HAS_HANGING_PUNCTUATION,
            DOM_HAS_TEXT_COMBINE_UPRIGHT,
            DOM_HAS_EXCLUSION_MARGIN,
            DOM_HAS_HYPHENATION_LANGUAGE,
            DOM_HAS_UNICODE_BIDI,
            DOM_HAS_TEXT_BOX_TRIM,
            DOM_HAS_HYPHENS,
            DOM_HAS_WORD_BREAK,
            DOM_HAS_OVERFLOW_WRAP,
            DOM_HAS_LINE_BREAK,
            DOM_HAS_TEXT_ALIGN_LAST,
            DOM_HAS_LINE_HEIGHT,
            DOM_HAS_COLUMN_WIDTH,
            DOM_HAS_SHAPE_MARGIN,
        ];
        let mut seen = 0u32;
        for f in flags {
            assert_eq!(f.count_ones(), 1, "{f:#X} is not a single-bit flag");
            assert_eq!(seen & f, 0, "two DOM_HAS_* flags share bit {f:#X}");
            seen |= f;
        }
        let mut c = CompactLayoutCache::empty();
        // Nothing declared: every query is false, including the degenerate ones.
        for f in flags {
            assert!(!c.dom_declared(f));
        }
        assert!(!c.dom_declared(0));
        assert!(!c.dom_declared(u32::MAX));
        // One flag declared: only that query is true.
        c.dom_declared_flags = DOM_HAS_LINE_HEIGHT;
        for f in flags {
            assert_eq!(c.dom_declared(f), f == DOM_HAS_LINE_HEIGHT);
        }
        assert!(!c.dom_declared(0), "an empty flag query must never report declared");
        assert!(c.dom_declared(u32::MAX));
        // Everything declared: every query is true.
        c.dom_declared_flags = u32::MAX;
        for f in flags {
            assert!(c.dom_declared(f));
        }
    }
    #[test]
    fn getters_at_the_last_valid_index_do_not_panic() {
        let c = CompactLayoutCache::with_capacity(4);
        let last = c.node_count() - 1;
        let _ = c.get_display(last);
        let _ = c.get_width_raw(last);
        let _ = c.get_padding_top(last);
        let _ = c.get_z_index(last);
        let _ = c.get_border_top_style(last);
        let _ = c.get_line_height(last);
        let _ = c.has_transform(last);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn tier1_getter_on_an_empty_cache_panics_rather_than_reading_oob() {
        let c = CompactLayoutCache::empty();
        let _ = c.get_display(0);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn tier2_getter_past_the_end_panics_rather_than_reading_oob() {
        let c = CompactLayoutCache::with_capacity(2);
        let _ = c.get_padding_top(2);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn cold_tier_getter_at_usize_max_panics_rather_than_wrapping() {
        // usize::MAX would wrap to a valid offset if the index were ever used in
        // pointer arithmetic without a bounds check.
        let c = CompactLayoutCache::with_capacity(1);
        let _ = c.get_z_index(usize::MAX);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn text_tier_getter_past_the_end_panics_rather_than_reading_oob() {
        let c = CompactLayoutCache::with_capacity(1);
        let _ = c.get_font_family_hash(1);
    }
    // =========================================================================
    // Struct layout — the compact cache's whole point is its byte budget
    // =========================================================================
    #[test]
    fn compact_structs_stay_within_their_documented_byte_budget() {
        // These sizes are load-bearing: the cache is sized as N × these, and the
        // module header quotes them. A field added without updating the header
        // silently doubles the per-node memory cost.
        assert_eq!(size_of::<CompactNodeProps>(), 72);
        assert_eq!(size_of::<CompactNodePropsCold>(), 48);
        assert_eq!(size_of::<CompactTextProps>(), 24);
        // Tier 1 is exactly one u64 per node.
        assert_eq!(size_of::<u64>(), 8);
        // No padding surprises from #[repr(C)] reordering.
        assert_eq!(align_of::<CompactNodeProps>(), 4);
        assert_eq!(align_of::<CompactNodePropsCold>(), 4);
        assert_eq!(align_of::<CompactTextProps>(), 8);
    }
}