1
//! User-Agent Default Stylesheet for Azul
2
//!
3
//! This module provides the default CSS styling that browsers apply to HTML elements
4
//! before any author stylesheets are processed. It ensures consistent baseline behavior
5
//! across all applications.
6
//!
7
//! The user-agent stylesheet serves several critical functions:
8
//!
9
//! 1. **Prevents Layout Collapse**: Ensures root elements (`<html>`, `<body>`) have default
10
//!    dimensions so that percentage-based child sizing can work correctly.
11
//!
12
//! 2. **Establishes Display Types**: Defines the default `display` property for all HTML elements
13
//!    (e.g., `<div>` is `block`, `<span>` is `inline`).
14
//!
15
//! 3. **Provides Baseline Typography**: Sets reasonable defaults for font sizes, margins, and text
16
//!    styling for headings, paragraphs, and other text elements.
17
//!
18
//! 4. **Normalizes Browser Behavior**: Incorporates principles from normalize.css to provide
19
//!    consistent rendering across different platforms.
20
//!
21
//! # Licensing
22
//!
23
//! Based on principles from [normalize.css](https://github.com/necolas/normalize.css)
24
//! (MIT License, Copyright Nicolas Gallagher and Jonathan Neal).
25
//! This is NOT a direct copy but incorporates its principles and approach.
26
//!
27
//! # References
28
//!
29
//! - CSS 2.1 Specification: https://www.w3.org/TR/CSS21/
30
//! - HTML Living Standard: https://html.spec.whatwg.org/
31
//! - normalize.css: https://necolas.github.io/normalize.css/
32

            
33
use azul_css::{
34
    css::CssPropertyValue,
35
    dynamic_selector::{
36
        CssPropertyWithConditions,
37
        DynamicSelector, DynamicSelectorContext, OsCondition, ThemeCondition,
38
    },
39
    props::{
40
        basic::{
41
            font::StyleFontWeight, pixel::PixelValue, ColorU,
42
            StyleFontSize,
43
        },
44
        layout::{
45
            dimensions::{LayoutHeight, LayoutWidth},
46
            display::LayoutDisplay,
47
            fragmentation::{BreakInside, PageBreak},
48
            spacing::{
49
                LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight, LayoutMarginTop,
50
                LayoutPaddingBottom, LayoutPaddingInlineEnd, LayoutPaddingInlineStart,
51
                LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop,
52
            },
53
        },
54
        property::{CssProperty, CssPropertyType},
55
        style::{
56
            border::{
57
                BorderStyle,
58
                LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, LayoutBorderTopWidth,
59
                StyleBorderBottomColor, StyleBorderBottomStyle,
60
                StyleBorderLeftColor, StyleBorderLeftStyle,
61
                StyleBorderRightColor, StyleBorderRightStyle,
62
                StyleBorderTopColor, StyleBorderTopStyle,
63
            },
64
            content::CounterReset,
65
            effects::StyleCursor,
66
            lists::StyleListStyleType,
67
            scrollbar::{
68
                LayoutScrollbarWidth, ScrollbarColorCustom, ScrollbarFadeDelay,
69
                ScrollbarFadeDuration, ScrollbarVisibilityMode, StyleScrollbarColor,
70
            },
71
            text::StyleTextDecoration,
72
            StyleTextAlign, StyleVerticalAlign,
73
        },
74
    },
75
};
76

            
77
use crate::dom::NodeType;
78

            
79
/// 100% width
80
static WIDTH_100_PERCENT: CssProperty = CssProperty::Width(CssPropertyValue::Exact(
81
    LayoutWidth::Px(PixelValue::const_percent(100)),
82
));
83

            
84
/// 100% height
85
static HEIGHT_100_PERCENT: CssProperty = CssProperty::Height(CssPropertyValue::Exact(
86
    LayoutHeight::Px(PixelValue::const_percent(100)),
87
));
88

            
89
/// display: block
90
static DISPLAY_BLOCK: CssProperty =
91
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::Block));
92
static OVERFLOW_X_AUTO: CssProperty = CssProperty::OverflowX(CssPropertyValue::Exact(
93
    azul_css::props::layout::LayoutOverflow::Auto,
94
));
95
static OVERFLOW_Y_AUTO: CssProperty = CssProperty::OverflowY(CssPropertyValue::Exact(
96
    azul_css::props::layout::LayoutOverflow::Auto,
97
));
98

            
99
/// display: inline
100
static DISPLAY_INLINE: CssProperty =
101
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::Inline));
102

            
103
/// display: inline-block
104
static DISPLAY_INLINE_BLOCK: CssProperty =
105
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::InlineBlock));
106

            
107
/// display: none
108
static DISPLAY_NONE: CssProperty =
109
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::None));
110

            
111
/// break-before: page (the canonical `<pagebreak/>` element)
112
static BREAK_BEFORE_PAGE: CssProperty = CssProperty::BreakBefore(CssPropertyValue::Exact(
113
    PageBreak::Page,
114
));
115

            
116
/// display: table
117
static DISPLAY_TABLE: CssProperty =
118
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::Table));
119

            
120
/// display: table-row
121
static DISPLAY_TABLE_ROW: CssProperty =
122
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableRow));
123

            
124
/// display: table-cell
125
static DISPLAY_TABLE_CELL: CssProperty =
126
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableCell));
127

            
128
/// display: table-header-group
129
static DISPLAY_TABLE_HEADER_GROUP: CssProperty =
130
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableHeaderGroup));
131

            
132
/// display: table-row-group
133
static DISPLAY_TABLE_ROW_GROUP: CssProperty =
134
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableRowGroup));
135

            
136
/// display: table-footer-group
137
static DISPLAY_TABLE_FOOTER_GROUP: CssProperty =
138
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableFooterGroup));
139

            
140
/// display: table-caption
141
static DISPLAY_TABLE_CAPTION: CssProperty =
142
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableCaption));
143

            
144
/// display: table-column-group
145
static DISPLAY_TABLE_COLUMN_GROUP: CssProperty =
146
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableColumnGroup));
147

            
148
/// display: table-column
149
static DISPLAY_TABLE_COLUMN: CssProperty =
150
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableColumn));
151

            
152
/// display: list-item
153
static DISPLAY_LIST_ITEM: CssProperty =
154
    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::ListItem));
155

            
156
/// cursor: pointer (for clickable elements like buttons, links)
157
static CURSOR_POINTER: CssProperty =
158
    CssProperty::Cursor(CssPropertyValue::Exact(StyleCursor::Pointer));
159

            
160
/// cursor: text (for selectable text elements)
161
static CURSOR_TEXT: CssProperty =
162
    CssProperty::Cursor(CssPropertyValue::Exact(StyleCursor::Text));
163

            
164
/// margin-top: 0
165
static MARGIN_TOP_ZERO: CssProperty =
166
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
167
        inner: PixelValue::const_px(0),
168
    }));
169

            
170
/// margin-bottom: 0
171
static MARGIN_BOTTOM_ZERO: CssProperty =
172
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
173
        inner: PixelValue::const_px(0),
174
    }));
175

            
176
/// margin-left: 0
177
static MARGIN_LEFT_ZERO: CssProperty =
178
    CssProperty::MarginLeft(CssPropertyValue::Exact(LayoutMarginLeft {
179
        inner: PixelValue::const_px(0),
180
    }));
181

            
182
/// margin-right: 0
183
static MARGIN_RIGHT_ZERO: CssProperty =
184
    CssProperty::MarginRight(CssPropertyValue::Exact(LayoutMarginRight {
185
        inner: PixelValue::const_px(0),
186
    }));
187

            
188
// Chrome User-Agent Stylesheet: body { margin: 8px; }
189
/// margin-top: 8px (Chrome UA default for body)
190
static MARGIN_TOP_8PX: CssProperty =
191
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
192
        inner: PixelValue::const_px(8),
193
    }));
194

            
195
/// margin-bottom: 8px (Chrome UA default for body)
196
static MARGIN_BOTTOM_8PX: CssProperty =
197
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
198
        inner: PixelValue::const_px(8),
199
    }));
200

            
201
/// margin-left: 8px (Chrome UA default for body)
202
static MARGIN_LEFT_8PX: CssProperty =
203
    CssProperty::MarginLeft(CssPropertyValue::Exact(LayoutMarginLeft {
204
        inner: PixelValue::const_px(8),
205
    }));
206

            
207
/// margin-right: 8px (Chrome UA default for body)
208
static MARGIN_RIGHT_8PX: CssProperty =
209
    CssProperty::MarginRight(CssPropertyValue::Exact(LayoutMarginRight {
210
        inner: PixelValue::const_px(8),
211
    }));
212

            
213
/// font-size: 2em (for H1)
214
static FONT_SIZE_2EM: CssProperty = CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
215
    inner: PixelValue::const_em(2),
216
}));
217

            
218
/// font-size: 1.5em (for H2)
219
static FONT_SIZE_1_5EM: CssProperty =
220
    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
221
        inner: PixelValue::const_em_fractional(1, 5),
222
    }));
223

            
224
/// font-size: 1.17em (for H3)
225
static FONT_SIZE_1_17EM: CssProperty =
226
    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
227
        inner: PixelValue::const_em_fractional(1, 17),
228
    }));
229

            
230
/// font-size: 1em (for H4)
231
static FONT_SIZE_1EM: CssProperty = CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
232
    inner: PixelValue::const_em(1),
233
}));
234

            
235
/// font-size: 0.83em (for H5)
236
static FONT_SIZE_0_83EM: CssProperty =
237
    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
238
        inner: PixelValue::const_em_fractional(0, 83),
239
    }));
240

            
241
/// font-size: 0.67em (for H6)
242
static FONT_SIZE_0_67EM: CssProperty =
243
    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
244
        inner: PixelValue::const_em_fractional(0, 67),
245
    }));
246

            
247
/// margin-top: 1em (for P)
248
static MARGIN_TOP_1EM: CssProperty =
249
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
250
        inner: PixelValue::const_em(1),
251
    }));
252

            
253
/// margin-bottom: 1em (for P)
254
static MARGIN_BOTTOM_1EM: CssProperty =
255
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
256
        inner: PixelValue::const_em(1),
257
    }));
258

            
259
/// margin-top: 0.67em (for H1)
260
static MARGIN_TOP_0_67EM: CssProperty =
261
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
262
        inner: PixelValue::const_em_fractional(0, 67),
263
    }));
264

            
265
/// margin-bottom: 0.67em (for H1)
266
static MARGIN_BOTTOM_0_67EM: CssProperty =
267
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
268
        inner: PixelValue::const_em_fractional(0, 67),
269
    }));
270

            
271
/// margin-top: 0.83em (for H2)
272
static MARGIN_TOP_0_83EM: CssProperty =
273
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
274
        inner: PixelValue::const_em_fractional(0, 83),
275
    }));
276

            
277
/// margin-bottom: 0.83em (for H2)
278
static MARGIN_BOTTOM_0_83EM: CssProperty =
279
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
280
        inner: PixelValue::const_em_fractional(0, 83),
281
    }));
282

            
283
/// margin-top: 1.33em (for H4)
284
static MARGIN_TOP_1_33EM: CssProperty =
285
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
286
        inner: PixelValue::const_em_fractional(1, 33),
287
    }));
288

            
289
/// margin-bottom: 1.33em (for H4)
290
static MARGIN_BOTTOM_1_33EM: CssProperty =
291
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
292
        inner: PixelValue::const_em_fractional(1, 33),
293
    }));
294

            
295
/// margin-top: 1.67em (for H5)
296
static MARGIN_TOP_1_67EM: CssProperty =
297
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
298
        inner: PixelValue::const_em_fractional(1, 67),
299
    }));
300

            
301
/// margin-bottom: 1.67em (for H5)
302
static MARGIN_BOTTOM_1_67EM: CssProperty =
303
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
304
        inner: PixelValue::const_em_fractional(1, 67),
305
    }));
306

            
307
/// margin-top: 2.33em (for H6)
308
static MARGIN_TOP_2_33EM: CssProperty =
309
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
310
        inner: PixelValue::const_em_fractional(2, 33),
311
    }));
312

            
313
/// margin-bottom: 2.33em (for H6)
314
static MARGIN_BOTTOM_2_33EM: CssProperty =
315
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
316
        inner: PixelValue::const_em_fractional(2, 33),
317
    }));
318

            
319
/// font-weight: bold (for headings)
320
static FONT_WEIGHT_BOLD: CssProperty =
321
    CssProperty::FontWeight(CssPropertyValue::Exact(StyleFontWeight::Bold));
322

            
323
/// font-weight: bolder
324
static FONT_WEIGHT_BOLDER: CssProperty =
325
    CssProperty::FontWeight(CssPropertyValue::Exact(StyleFontWeight::Bolder));
326

            
327
// Table cell padding - Chrome UA CSS default: 1px
328
static PADDING_TOP_1PX: CssProperty =
329
    CssProperty::PaddingTop(CssPropertyValue::Exact(LayoutPaddingTop {
330
        inner: PixelValue::const_px(1),
331
    }));
332

            
333
static PADDING_BOTTOM_1PX: CssProperty =
334
    CssProperty::PaddingBottom(CssPropertyValue::Exact(LayoutPaddingBottom {
335
        inner: PixelValue::const_px(1),
336
    }));
337

            
338
static PADDING_LEFT_1PX: CssProperty =
339
    CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
340
        inner: PixelValue::const_px(1),
341
    }));
342

            
343
static PADDING_RIGHT_1PX: CssProperty =
344
    CssProperty::PaddingRight(CssPropertyValue::Exact(LayoutPaddingRight {
345
        inner: PixelValue::const_px(1),
346
    }));
347

            
348
/// text-align: center (for th elements)
349
static TEXT_ALIGN_CENTER: CssProperty =
350
    CssProperty::TextAlign(CssPropertyValue::Exact(StyleTextAlign::Center));
351

            
352
/// vertical-align: middle (for table elements)
353
static VERTICAL_ALIGN_MIDDLE: CssProperty =
354
    CssProperty::VerticalAlign(CssPropertyValue::Exact(StyleVerticalAlign::Middle));
355

            
356
/// list-style-type: disc (default for <ul>)
357
static LIST_STYLE_TYPE_DISC: CssProperty =
358
    CssProperty::ListStyleType(CssPropertyValue::Exact(StyleListStyleType::Disc));
359

            
360
/// list-style-type: decimal (default for <ol>)
361
static LIST_STYLE_TYPE_DECIMAL: CssProperty =
362
    CssProperty::ListStyleType(CssPropertyValue::Exact(StyleListStyleType::Decimal));
363

            
364
// --- HR Element Defaults ---
365
// Per HTML spec, <hr> renders as a horizontal line with inset border style
366

            
367
/// margin-top: 0.5em (for hr)
368
static MARGIN_TOP_0_5EM: CssProperty =
369
    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
370
        inner: PixelValue::const_em_fractional(0, 5),
371
    }));
372

            
373
/// margin-bottom: 0.5em (for hr)
374
static MARGIN_BOTTOM_0_5EM: CssProperty =
375
    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
376
        inner: PixelValue::const_em_fractional(0, 5),
377
    }));
378

            
379
/// border-top-style: inset (for hr - default browser style)
380
static BORDER_TOP_STYLE_INSET: CssProperty =
381
    CssProperty::BorderTopStyle(CssPropertyValue::Exact(StyleBorderTopStyle {
382
        inner: BorderStyle::Inset,
383
    }));
384

            
385
/// border-top-width: 1px (for hr)
386
static BORDER_TOP_WIDTH_1PX: CssProperty =
387
    CssProperty::BorderTopWidth(CssPropertyValue::Exact(LayoutBorderTopWidth {
388
        inner: PixelValue::const_px(1),
389
    }));
390

            
391
/// border-top-color: gray (for hr - default visible color)
392
static BORDER_TOP_COLOR_GRAY: CssProperty =
393
    CssProperty::BorderTopColor(CssPropertyValue::Exact(StyleBorderTopColor {
394
        inner: ColorU {
395
            r: 128,
396
            g: 128,
397
            b: 128,
398
            a: 255,
399
        },
400
    }));
401

            
402
/// height: 0 (for hr - the line comes from the border, not height)
403
static HEIGHT_ZERO: CssProperty = CssProperty::Height(CssPropertyValue::Exact(LayoutHeight::Px(
404
    PixelValue::const_px(0),
405
)));
406

            
407
/// counter-reset: list-item 0 (default for <ul>, <ol>)
408
/// Per CSS Lists Module Level 3, list containers automatically reset the list-item counter
409
static COUNTER_RESET_LIST_ITEM: CssProperty =
410
    CssProperty::CounterReset(CssPropertyValue::Exact(CounterReset::list_item()));
411

            
412
// CSS Fragmentation (Page Breaking) Properties
413
//
414
// Per CSS Fragmentation Level 3 and paged media best practices,
415
// certain elements should avoid page breaks inside them
416

            
417
/// break-inside: avoid
418
/// Used for elements that should not be split across page boundaries
419
/// Applied to: h1-h6, table, thead, tbody, tfoot, figure, figcaption
420
static BREAK_INSIDE_AVOID: CssProperty = CssProperty::break_inside(BreakInside::Avoid);
421

            
422
/// break-after: avoid
423
/// Avoids a page break after the element (useful for headings)
424
static BREAK_AFTER_AVOID: CssProperty = CssProperty::break_after(PageBreak::Avoid);
425

            
426
/// padding-inline-start: 40px (default for <li>)
427
///
428
/// Creates space for list markers in the inline-start direction (left in LTR, right in RTL)
429
/// padding-inline-start: 40px for list items per CSS Lists Module Level 3
430
/// Applied to <li> items to create gutter space for `::marker` pseudo-elements
431
///
432
/// NOTE: This should be on the list items, not the container, because:
433
///
434
/// 1. `::marker` pseudo-elements are children of <li>, not <ul>/<ol>
435
/// 2. The marker needs to be positioned relative to the list item's content box
436
/// 3. Padding on <li> creates space between the marker and the text content
437
///    TODO: Change to `PaddingInlineStart` once logical property resolution is implemented
438
static PADDING_INLINE_START_40PX: CssProperty =
439
    CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
440
        inner: PixelValue::const_px(40),
441
    }));
442

            
443
/// Text decoration: underline - used for <a> and <u> elements
444
static TEXT_DECORATION_UNDERLINE: CssProperty = CssProperty::TextDecoration(
445
    CssPropertyValue::Exact(StyleTextDecoration::Underline),
446
);
447

            
448
// --- Button Element Defaults ---
449
// Per browser UA CSS, <button> has padding, border, and a system font size.
450
// These ensure a button is visible even without author CSS.
451

            
452
/// font-size: 13px (standard button font size on macOS/Linux)
453
static FONT_SIZE_13PX: CssProperty = CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
454
    inner: PixelValue::const_px(13),
455
}));
456

            
457
/// padding-top: 5px (button)
458
static PADDING_TOP_5PX: CssProperty =
459
    CssProperty::PaddingTop(CssPropertyValue::Exact(LayoutPaddingTop {
460
        inner: PixelValue::const_px(5),
461
    }));
462

            
463
/// padding-bottom: 5px (button)
464
static PADDING_BOTTOM_5PX: CssProperty =
465
    CssProperty::PaddingBottom(CssPropertyValue::Exact(LayoutPaddingBottom {
466
        inner: PixelValue::const_px(5),
467
    }));
468

            
469
/// padding-left: 10px (button)
470
static PADDING_LEFT_10PX: CssProperty =
471
    CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
472
        inner: PixelValue::const_px(10),
473
    }));
474

            
475
/// padding-right: 10px (button)
476
static PADDING_RIGHT_10PX: CssProperty =
477
    CssProperty::PaddingRight(CssPropertyValue::Exact(LayoutPaddingRight {
478
        inner: PixelValue::const_px(10),
479
    }));
480

            
481
/// Border color for button: #c8c8c8 (light gray)
482
static BUTTON_BORDER_COLOR: ColorU = ColorU { r: 200, g: 200, b: 200, a: 255 };
483

            
484
static BUTTON_BORDER_TOP_COLOR: CssProperty =
485
    CssProperty::BorderTopColor(CssPropertyValue::Exact(StyleBorderTopColor {
486
        inner: BUTTON_BORDER_COLOR,
487
    }));
488
static BUTTON_BORDER_BOTTOM_COLOR: CssProperty =
489
    CssProperty::BorderBottomColor(CssPropertyValue::Exact(StyleBorderBottomColor {
490
        inner: BUTTON_BORDER_COLOR,
491
    }));
492
static BUTTON_BORDER_LEFT_COLOR: CssProperty =
493
    CssProperty::BorderLeftColor(CssPropertyValue::Exact(StyleBorderLeftColor {
494
        inner: BUTTON_BORDER_COLOR,
495
    }));
496
static BUTTON_BORDER_RIGHT_COLOR: CssProperty =
497
    CssProperty::BorderRightColor(CssPropertyValue::Exact(StyleBorderRightColor {
498
        inner: BUTTON_BORDER_COLOR,
499
    }));
500

            
501
static BUTTON_BORDER_TOP_STYLE: CssProperty =
502
    CssProperty::BorderTopStyle(CssPropertyValue::Exact(StyleBorderTopStyle {
503
        inner: BorderStyle::Solid,
504
    }));
505
static BUTTON_BORDER_BOTTOM_STYLE: CssProperty =
506
    CssProperty::BorderBottomStyle(CssPropertyValue::Exact(StyleBorderBottomStyle {
507
        inner: BorderStyle::Solid,
508
    }));
509
static BUTTON_BORDER_LEFT_STYLE: CssProperty =
510
    CssProperty::BorderLeftStyle(CssPropertyValue::Exact(StyleBorderLeftStyle {
511
        inner: BorderStyle::Solid,
512
    }));
513
static BUTTON_BORDER_RIGHT_STYLE: CssProperty =
514
    CssProperty::BorderRightStyle(CssPropertyValue::Exact(StyleBorderRightStyle {
515
        inner: BorderStyle::Solid,
516
    }));
517

            
518
static BUTTON_BORDER_TOP_WIDTH: CssProperty =
519
    CssProperty::BorderTopWidth(CssPropertyValue::Exact(LayoutBorderTopWidth {
520
        inner: PixelValue::const_px(1),
521
    }));
522
static BUTTON_BORDER_BOTTOM_WIDTH: CssProperty =
523
    CssProperty::BorderBottomWidth(CssPropertyValue::Exact(LayoutBorderBottomWidth {
524
        inner: PixelValue::const_px(1),
525
    }));
526
static BUTTON_BORDER_LEFT_WIDTH: CssProperty =
527
    CssProperty::BorderLeftWidth(CssPropertyValue::Exact(LayoutBorderLeftWidth {
528
        inner: PixelValue::const_px(1),
529
    }));
530
static BUTTON_BORDER_RIGHT_WIDTH: CssProperty =
531
    CssProperty::BorderRightWidth(CssPropertyValue::Exact(LayoutBorderRightWidth {
532
        inner: PixelValue::const_px(1),
533
    }));
534

            
535
/// Returns the default user-agent CSS property value for a given node type and property.
536
///
537
/// This function provides the baseline styling that should be applied before any author
538
/// styles. It ensures that elements have sensible defaults that prevent layout issues.
539
///
540
/// # Arguments
541
///
542
/// * `node_type` - The type of DOM node (e.g., `Body`, `H1`, `Div`)
543
/// * `property_type` - The specific CSS property to query (e.g., `Width`, `Display`)
544
///
545
/// # Returns
546
///
547
/// `Some(CssProperty)` if a default value is defined for this combination, otherwise `None`.
548
// Exhaustive (node-type, property-type) → default-value lookup table: many
549
// element types share a default (e.g. all block elements → DISPLAY_BLOCK). One
550
// arm per (NT, PT) case is intentional for readability; merging into giant
551
// or-patterns would collapse the UA stylesheet table.
552
#[allow(clippy::match_same_arms)]
553
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
554
76473917
#[must_use] pub fn get_ua_property(
555
76473917
    node_type: &NodeType,
556
76473917
    property_type: CssPropertyType,
557
76473917
) -> Option<&'static CssProperty> {
558
    use CssPropertyType as PT;
559
    use NodeType as NT;
560

            
561
    
562

            
563
76473917
    match (node_type, property_type) {
564
        // Body Element - CRITICAL for preventing layout collapse
565
35973
        (NT::Body, PT::Display) => Some(&DISPLAY_BLOCK),
566
        // NOTE: Body does NOT have width: 100% in standard UA CSS - it inherits from ICB
567
        // (NT::Body, PT::Height) => Some(&HEIGHT_100_PERCENT),
568
33450
        (NT::Body, PT::MarginTop) => Some(&MARGIN_TOP_8PX),
569
33431
        (NT::Body, PT::MarginBottom) => Some(&MARGIN_BOTTOM_8PX),
570
33431
        (NT::Body, PT::MarginLeft) => Some(&MARGIN_LEFT_8PX),
571
33431
        (NT::Body, PT::MarginRight) => Some(&MARGIN_RIGHT_8PX),
572

            
573
        // Block-level Elements
574
        // NOTE: Do NOT set width: 100% here! Block elements have width: auto by default
575
        // in CSS spec. width: auto for blocks means "fill available width" but it's NOT
576
        // the same as width: 100%. The difference is critical for flexbox: width: auto
577
        // allows flex-grow/flex-shrink to control sizing, while width: 100% prevents it.
578
445018
        (NT::Div, PT::Display) => Some(&DISPLAY_BLOCK),
579
313109
        (NT::P, PT::Display) => Some(&DISPLAY_BLOCK),
580
        // REMOVED - blocks have width: auto by default
581
        // (NT::Div, PT::Width) => Some(&WIDTH_100_PERCENT),
582
        // REMOVED - blocks have width: auto by default
583
        // (NT::P, PT::Width) => Some(&WIDTH_100_PERCENT),
584
329858
        (NT::P, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
585
325766
        (NT::P, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
586
5
        (NT::Main, PT::Display) => Some(&DISPLAY_BLOCK),
587
4
        (NT::Header, PT::Display) => Some(&DISPLAY_BLOCK),
588
4
        (NT::Footer, PT::Display) => Some(&DISPLAY_BLOCK),
589
43
        (NT::Section, PT::Display) => Some(&DISPLAY_BLOCK),
590
42
        (NT::Article, PT::Display) => Some(&DISPLAY_BLOCK),
591
4
        (NT::Aside, PT::Display) => Some(&DISPLAY_BLOCK),
592
4
        (NT::Nav, PT::Display) => Some(&DISPLAY_BLOCK),
593

            
594
        // Headings - Chrome UA CSS values
595
        // Per CSS Fragmentation Level 3: headings should avoid page breaks inside
596
        // and after them (to keep heading with following content)
597
1493
        (NT::H1, PT::Display) => Some(&DISPLAY_BLOCK),
598
876
        (NT::H1, PT::FontSize) => Some(&FONT_SIZE_2EM),
599
1492
        (NT::H1, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
600
1440
        (NT::H1, PT::MarginTop) => Some(&MARGIN_TOP_0_67EM),
601
846
        (NT::H1, PT::MarginBottom) => Some(&MARGIN_BOTTOM_0_67EM),
602
748
        (NT::H1, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
603
748
        (NT::H1, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
604

            
605
445
        (NT::H2, PT::Display) => Some(&DISPLAY_BLOCK),
606
224
        (NT::H2, PT::FontSize) => Some(&FONT_SIZE_1_5EM),
607
444
        (NT::H2, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
608
356
        (NT::H2, PT::MarginTop) => Some(&MARGIN_TOP_0_83EM),
609
224
        (NT::H2, PT::MarginBottom) => Some(&MARGIN_BOTTOM_0_83EM),
610
224
        (NT::H2, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
611
224
        (NT::H2, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
612

            
613
49
        (NT::H3, PT::Display) => Some(&DISPLAY_BLOCK),
614
26
        (NT::H3, PT::FontSize) => Some(&FONT_SIZE_1_17EM),
615
48
        (NT::H3, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
616
26
        (NT::H3, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
617
26
        (NT::H3, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
618
26
        (NT::H3, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
619
26
        (NT::H3, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
620

            
621
5
        (NT::H4, PT::Display) => Some(&DISPLAY_BLOCK),
622
4
        (NT::H4, PT::FontSize) => Some(&FONT_SIZE_1EM),
623
4
        (NT::H4, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
624
4
        (NT::H4, PT::MarginTop) => Some(&MARGIN_TOP_1_33EM),
625
4
        (NT::H4, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1_33EM),
626
4
        (NT::H4, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
627
4
        (NT::H4, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
628

            
629
5
        (NT::H5, PT::Display) => Some(&DISPLAY_BLOCK),
630
4
        (NT::H5, PT::FontSize) => Some(&FONT_SIZE_0_83EM),
631
4
        (NT::H5, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
632
4
        (NT::H5, PT::MarginTop) => Some(&MARGIN_TOP_1_67EM),
633
4
        (NT::H5, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1_67EM),
634
4
        (NT::H5, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
635
4
        (NT::H5, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
636

            
637
5
        (NT::H6, PT::Display) => Some(&DISPLAY_BLOCK),
638
4
        (NT::H6, PT::FontSize) => Some(&FONT_SIZE_0_67EM),
639
4
        (NT::H6, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
640
4
        (NT::H6, PT::MarginTop) => Some(&MARGIN_TOP_2_33EM),
641
4
        (NT::H6, PT::MarginBottom) => Some(&MARGIN_BOTTOM_2_33EM),
642
4
        (NT::H6, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
643
4
        (NT::H6, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
644

            
645
        // Lists - padding on container creates gutter for markers
646
1369
        (NT::Ul, PT::Display) => Some(&DISPLAY_BLOCK),
647
1368
        (NT::Ul, PT::ListStyleType) => Some(&LIST_STYLE_TYPE_DISC),
648
1368
        (NT::Ul, PT::CounterReset) => Some(&COUNTER_RESET_LIST_ITEM),
649
1368
        (NT::Ul, PT::PaddingLeft) => Some(&PADDING_INLINE_START_40PX),
650
1367
        (NT::Ul, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
651
751
        (NT::Ul, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
652
5
        (NT::Ol, PT::Display) => Some(&DISPLAY_BLOCK),
653
4
        (NT::Ol, PT::ListStyleType) => Some(&LIST_STYLE_TYPE_DECIMAL),
654
4
        (NT::Ol, PT::CounterReset) => Some(&COUNTER_RESET_LIST_ITEM),
655
4
        (NT::Ol, PT::PaddingLeft) => Some(&PADDING_INLINE_START_40PX),
656
3
        (NT::Ol, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
657
3
        (NT::Ol, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
658
4075
        (NT::Li, PT::Display) => Some(&DISPLAY_LIST_ITEM),
659
4
        (NT::Dl, PT::Display) => Some(&DISPLAY_BLOCK),
660
4
        (NT::Dt, PT::Display) => Some(&DISPLAY_BLOCK),
661
4
        (NT::Dd, PT::Display) => Some(&DISPLAY_BLOCK),
662

            
663
        // Inline Elements
664
2552
        (NT::Span, PT::Display) => Some(&DISPLAY_INLINE),
665
1632
        (NT::A, PT::Display) => Some(&DISPLAY_INLINE),
666
1632
        (NT::A, PT::TextDecoration) => Some(&TEXT_DECORATION_UNDERLINE),
667
48
        (NT::Strong, PT::Display) => Some(&DISPLAY_INLINE),
668
26
        (NT::Strong, PT::FontWeight) => Some(&FONT_WEIGHT_BOLDER),
669
70
        (NT::Em, PT::Display) => Some(&DISPLAY_INLINE),
670
4
        (NT::B, PT::Display) => Some(&DISPLAY_INLINE),
671
4
        (NT::B, PT::FontWeight) => Some(&FONT_WEIGHT_BOLDER),
672
4
        (NT::I, PT::Display) => Some(&DISPLAY_INLINE),
673
4
        (NT::U, PT::Display) => Some(&DISPLAY_INLINE),
674
4
        (NT::U, PT::TextDecoration) => Some(&TEXT_DECORATION_UNDERLINE),
675
4
        (NT::Small, PT::Display) => Some(&DISPLAY_INLINE),
676
48
        (NT::Code, PT::Display) => Some(&DISPLAY_INLINE),
677
4
        (NT::Kbd, PT::Display) => Some(&DISPLAY_INLINE),
678
4
        (NT::Samp, PT::Display) => Some(&DISPLAY_INLINE),
679
4
        (NT::Sub, PT::Display) => Some(&DISPLAY_INLINE),
680
4
        (NT::Sup, PT::Display) => Some(&DISPLAY_INLINE),
681

            
682
        // Text Content
683
92
        (NT::Pre, PT::Display) => Some(&DISPLAY_BLOCK),
684
48
        (NT::BlockQuote, PT::Display) => Some(&DISPLAY_BLOCK),
685
4
        (NT::Hr, PT::Display) => Some(&DISPLAY_BLOCK),
686
4
        (NT::Hr, PT::Width) => Some(&WIDTH_100_PERCENT),
687
4
        (NT::Hr, PT::Height) => Some(&HEIGHT_ZERO),
688
3
        (NT::Hr, PT::MarginTop) => Some(&MARGIN_TOP_0_5EM),
689
3
        (NT::Hr, PT::MarginBottom) => Some(&MARGIN_BOTTOM_0_5EM),
690
4
        (NT::Hr, PT::BorderTopStyle) => Some(&BORDER_TOP_STYLE_INSET),
691
4
        (NT::Hr, PT::BorderTopWidth) => Some(&BORDER_TOP_WIDTH_1PX),
692
4
        (NT::Hr, PT::BorderTopColor) => Some(&BORDER_TOP_COLOR_GRAY),
693

            
694
        // Table Elements
695
        // Per CSS Fragmentation Level 3: table ROWS should avoid breaks inside
696
        // Tables themselves should NOT have break-inside: avoid (they can span pages)
697
819
        (NT::Table, PT::Display) => Some(&DISPLAY_TABLE),
698
        // NOTE: Removed break-inside: avoid from Table - tables CAN break across pages
699
88
        (NT::PageBreak, PT::Display) => Some(&DISPLAY_BLOCK),
700
66
        (NT::PageBreak, PT::BreakBefore) => Some(&BREAK_BEFORE_PAGE),
701
115
        (NT::THead, PT::Display) => Some(&DISPLAY_TABLE_HEADER_GROUP),
702
113
        (NT::THead, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
703
59
        (NT::THead, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
704
93
        (NT::TBody, PT::Display) => Some(&DISPLAY_TABLE_ROW_GROUP),
705
91
        (NT::TBody, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
706
        // NOTE: Removed break-inside: avoid from TBody - tbody CAN break across pages
707
5
        (NT::TFoot, PT::Display) => Some(&DISPLAY_TABLE_FOOTER_GROUP),
708
3
        (NT::TFoot, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
709
4
        (NT::TFoot, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
710
1303
        (NT::Tr, PT::Display) => Some(&DISPLAY_TABLE_ROW),
711
1301
        (NT::Tr, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
712
653
        (NT::Tr, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
713
49
        (NT::Th, PT::Display) => Some(&DISPLAY_TABLE_CELL),
714
26
        (NT::Th, PT::TextAlign) => Some(&TEXT_ALIGN_CENTER),
715
48
        (NT::Th, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
716
70
        (NT::Th, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
717
26
        (NT::Th, PT::PaddingTop) => Some(&PADDING_TOP_1PX),
718
26
        (NT::Th, PT::PaddingBottom) => Some(&PADDING_BOTTOM_1PX),
719
26
        (NT::Th, PT::PaddingLeft) => Some(&PADDING_LEFT_1PX),
720
26
        (NT::Th, PT::PaddingRight) => Some(&PADDING_RIGHT_1PX),
721
1831
        (NT::Td, PT::Display) => Some(&DISPLAY_TABLE_CELL),
722
2743
        (NT::Td, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
723
1753
        (NT::Td, PT::PaddingTop) => Some(&PADDING_TOP_1PX),
724
1753
        (NT::Td, PT::PaddingBottom) => Some(&PADDING_BOTTOM_1PX),
725
1753
        (NT::Td, PT::PaddingLeft) => Some(&PADDING_LEFT_1PX),
726
1753
        (NT::Td, PT::PaddingRight) => Some(&PADDING_RIGHT_1PX),
727

            
728
        // Form Elements
729
4
        (NT::Form, PT::Display) => Some(&DISPLAY_BLOCK),
730
554
        (NT::Input, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
731
36910
        (NT::Button, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
732
36965
        (NT::Button, PT::Cursor) => Some(&CURSOR_POINTER),
733
54409
        (NT::Button, PT::FontSize) => Some(&FONT_SIZE_13PX),
734
36952
        (NT::Button, PT::PaddingTop) => Some(&PADDING_TOP_5PX),
735
36952
        (NT::Button, PT::PaddingBottom) => Some(&PADDING_BOTTOM_5PX),
736
37007
        (NT::Button, PT::PaddingLeft) => Some(&PADDING_LEFT_10PX),
737
37007
        (NT::Button, PT::PaddingRight) => Some(&PADDING_RIGHT_10PX),
738
36964
        (NT::Button, PT::BorderTopWidth) => Some(&BUTTON_BORDER_TOP_WIDTH),
739
36150
        (NT::Button, PT::BorderBottomWidth) => Some(&BUTTON_BORDER_BOTTOM_WIDTH),
740
36150
        (NT::Button, PT::BorderLeftWidth) => Some(&BUTTON_BORDER_LEFT_WIDTH),
741
36150
        (NT::Button, PT::BorderRightWidth) => Some(&BUTTON_BORDER_RIGHT_WIDTH),
742
41254
        (NT::Button, PT::BorderTopStyle) => Some(&BUTTON_BORDER_TOP_STYLE),
743
36150
        (NT::Button, PT::BorderBottomStyle) => Some(&BUTTON_BORDER_BOTTOM_STYLE),
744
36150
        (NT::Button, PT::BorderLeftStyle) => Some(&BUTTON_BORDER_LEFT_STYLE),
745
36150
        (NT::Button, PT::BorderRightStyle) => Some(&BUTTON_BORDER_RIGHT_STYLE),
746
41254
        (NT::Button, PT::BorderTopColor) => Some(&BUTTON_BORDER_TOP_COLOR),
747
36150
        (NT::Button, PT::BorderBottomColor) => Some(&BUTTON_BORDER_BOTTOM_COLOR),
748
36150
        (NT::Button, PT::BorderLeftColor) => Some(&BUTTON_BORDER_LEFT_COLOR),
749
36150
        (NT::Button, PT::BorderRightColor) => Some(&BUTTON_BORDER_RIGHT_COLOR),
750
        // Text nodes get I-beam cursor for text selection
751
        // The cursor resolution algorithm ensures that explicit cursor properties
752
        // on parent elements (e.g., cursor:pointer on button) take precedence
753
419707
        (NT::Text(_), PT::Cursor) => Some(&CURSOR_TEXT),
754
554
        (NT::Select, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
755
1500
        (NT::TextArea, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
756
        // TextArea gets I-beam cursor since it's an editable text field
757
1500
        (NT::TextArea, PT::Cursor) => Some(&CURSOR_TEXT),
758
4
        (NT::Label, PT::Display) => Some(&DISPLAY_INLINE),
759
        // Hidden Elements
760
27
        (NT::Head, PT::Display) => Some(&DISPLAY_NONE),
761
5
        (NT::Title, PT::Display) => Some(&DISPLAY_NONE),
762
5
        (NT::Script, PT::Display) => Some(&DISPLAY_NONE),
763
5
        (NT::Style, PT::Display) => Some(&DISPLAY_NONE),
764
5
        (NT::Link, PT::Display) => Some(&DISPLAY_NONE),
765

            
766
        // Special Elements
767
        // <br> is an inline-level element that forces a line break WITHIN the
768
        // inline formatting context (HTML §4.5.28). Giving it `display: block`
769
        // made `<p>text<br>more</p>` split into three stacked block boxes (an
770
        // extra empty <br> box between two anonymous paragraphs), over-advancing
771
        // vertically and, inside a table cell, dropping the line after the break.
772
        // As inline it is turned into a hard `LineBreak` by the IFC collectors.
773
83
        (NT::Br, PT::Display) => Some(&DISPLAY_INLINE),
774
        // Images are replaced elements - inline-block so they respect width/height
775
428
        (NT::Image(_), PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
776

            
777
        // Media Elements
778
4
        (NT::Video, PT::Display) => Some(&DISPLAY_INLINE),
779
4
        (NT::Audio, PT::Display) => Some(&DISPLAY_INLINE),
780
4
        (NT::Canvas, PT::Display) => Some(&DISPLAY_INLINE),
781
422
        (NT::Svg, PT::Display) => Some(&DISPLAY_INLINE),
782
        // VirtualView is a block-level replaced element (like div) — must be block
783
        // so it participates in flex layout (flex-grow, etc.)
784
654
        (NT::VirtualView, PT::Display) => Some(&DISPLAY_BLOCK),
785
        // A VirtualView exists to virtualize scrollable content, so scrolling
786
        // is its DEFAULT: `auto` gets it a scroll id (wheel target) and — via
787
        // the virtual-size-aware necessity rule — a scrollbar exactly when
788
        // the published `virtual_scroll_size` overflows the viewport. A VV
789
        // that must NOT wheel-scroll (the map pans+zooms, the video widget)
790
        // opts out explicitly with `overflow: hidden`, which both already do.
791
588
        (NT::VirtualView, PT::OverflowX) => Some(&OVERFLOW_X_AUTO),
792
577
        (NT::VirtualView, PT::OverflowY) => Some(&OVERFLOW_Y_AUTO),
793

            
794
        // Icon Elements - inline-block so they have width/height but flow inline
795
56698
        (NT::Icon(_), PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
796

            
797
4
        (NT::SelectOption, PT::Display) => Some(&DISPLAY_NONE),
798
4
        (NT::OptGroup, PT::Display) => Some(&DISPLAY_NONE),
799

            
800
        // Other Inline Elements
801
4
        (NT::Abbr, PT::Display) => Some(&DISPLAY_INLINE),
802
4
        (NT::Cite, PT::Display) => Some(&DISPLAY_INLINE),
803
4
        (NT::Del, PT::Display) => Some(&DISPLAY_INLINE),
804
4
        (NT::Ins, PT::Display) => Some(&DISPLAY_INLINE),
805
4
        (NT::Mark, PT::Display) => Some(&DISPLAY_INLINE),
806
4
        (NT::Q, PT::Display) => Some(&DISPLAY_INLINE),
807
4
        (NT::Dfn, PT::Display) => Some(&DISPLAY_INLINE),
808
4
        (NT::Var, PT::Display) => Some(&DISPLAY_INLINE),
809
4
        (NT::Time, PT::Display) => Some(&DISPLAY_INLINE),
810
4
        (NT::Data, PT::Display) => Some(&DISPLAY_INLINE),
811
4
        (NT::Wbr, PT::Display) => Some(&DISPLAY_INLINE),
812
4
        (NT::Bdi, PT::Display) => Some(&DISPLAY_INLINE),
813
4
        (NT::Bdo, PT::Display) => Some(&DISPLAY_INLINE),
814
4
        (NT::Rp, PT::Display) => Some(&DISPLAY_INLINE),
815
4
        (NT::Rt, PT::Display) => Some(&DISPLAY_INLINE),
816
4
        (NT::Rtc, PT::Display) => Some(&DISPLAY_INLINE),
817
4
        (NT::Ruby, PT::Display) => Some(&DISPLAY_INLINE),
818

            
819
        // Block Container Elements
820
        // Per CSS Fragmentation Level 3: figures should avoid page breaks inside
821
4
        (NT::FieldSet, PT::Display) => Some(&DISPLAY_BLOCK),
822
4
        (NT::Figure, PT::Display) => Some(&DISPLAY_BLOCK),
823
3
        (NT::Figure, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
824
4
        (NT::FigCaption, PT::Display) => Some(&DISPLAY_BLOCK),
825
3
        (NT::FigCaption, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
826
4
        (NT::Details, PT::Display) => Some(&DISPLAY_BLOCK),
827
4
        (NT::Summary, PT::Display) => Some(&DISPLAY_BLOCK),
828
4
        (NT::Dialog, PT::Display) => Some(&DISPLAY_BLOCK),
829

            
830
        // Table Caption
831
5
        (NT::Caption, PT::Display) => Some(&DISPLAY_TABLE_CAPTION),
832
5
        (NT::ColGroup, PT::Display) => Some(&DISPLAY_TABLE_COLUMN_GROUP),
833
5
        (NT::Col, PT::Display) => Some(&DISPLAY_TABLE_COLUMN),
834

            
835
        // Legacy/Deprecated Elements
836
4
        (NT::Menu, PT::Display) => Some(&DISPLAY_BLOCK),
837
4
        (NT::Dir, PT::Display) => Some(&DISPLAY_BLOCK),
838

            
839
        // Html (root) Element
840
        //
841
        // In browsers, the viewport itself provides scrolling when <html> overflows.
842
        // Since Azul has no separate viewport scroll mechanism, we set `height: 100%`
843
        // on the <html> element so it fills the Initial Containing Block (the viewport).
844
        // This constrains child elements like <body> to the viewport height, enabling
845
        // overflow:scroll on <body> to create scrollable content areas.
846
        //
847
        // Without this, <html> has height:auto and grows to fit all content,
848
        // making container_size == content_size, which results in a useless 100% scrollbar.
849
7317
        (NT::Html, PT::Display) => Some(&DISPLAY_BLOCK),
850
        // ⚠ DIAG (2026-06-02, REVERT): the lifted get_ua_property jump table mis-dispatches
851
        // (Text/Button, Height) → THIS (Html, Height) arm → children wrongly get height:100%
852
        // → fill parent (600) instead of content. Commenting it out tests whether removing the
853
        // ONLY HEIGHT_100_PERCENT producer makes the children auto-height (confirms the chain).
854
        // REAL fix = the node_type jump-table dispatch/table-mirror in the lift, not this.
855
        // (NT::Html, PT::Height) => Some(&HEIGHT_100_PERCENT),
856

            
857
        // Universal fallback for display property
858
        // Per CSS spec, unknown/custom elements should default to inline
859
        // Text nodes will be filtered out before this function is called
860
542330
        (_, PT::Display) => Some(&DISPLAY_INLINE),
861

            
862
        // No default defined for other combinations
863
73091599
        _ => None,
864
    }
865
76473917
}
866

            
867
// ============================================================================
868
// UA Scrollbar Defaults — individual CssPropertyWithConditions
869
// ============================================================================
870
//
871
// These rules define the default scrollbar appearance per OS and theme,
872
// using the same `@os` / `@theme` condition system as author CSS.
873
// Each entry is a single CSS property (scrollbar-color or scrollbar-width)
874
// with its conditions.  Rules are evaluated first-match-wins per property type.
875
//
876
// Conceptually equivalent to:
877
//
878
//   @os macos                { scrollbar-width: thin; }
879
//   @os ios                  { scrollbar-width: thin; }
880
//   @os android              { scrollbar-width: thin; }
881
//   /* default */            { scrollbar-width: auto; }
882
//
883
//   @os macos                { -azul-scrollbar-visibility: when-scrolling; }
884
//   @os ios                  { -azul-scrollbar-visibility: when-scrolling; }
885
//   @os android              { -azul-scrollbar-visibility: when-scrolling; }
886
//   /* default */            { -azul-scrollbar-visibility: always; }
887
//
888
//   @os macos                { -azul-scrollbar-fade-delay: 500ms; }
889
//   @os ios                  { -azul-scrollbar-fade-delay: 500ms; }
890
//   @os android              { -azul-scrollbar-fade-delay: 300ms; }
891
//   /* default */            { -azul-scrollbar-fade-delay: 0; }
892
//
893
//   @os macos                { -azul-scrollbar-fade-duration: 200ms; }
894
//   @os ios                  { -azul-scrollbar-fade-duration: 200ms; }
895
//   @os android              { -azul-scrollbar-fade-duration: 150ms; }
896
//   /* default */            { -azul-scrollbar-fade-duration: 0; }
897
//
898
//   @os macos @theme dark    { scrollbar-color: rgba(180,180,180,0.78) rgba(40,40,40,0.31); }
899
//   @os macos @theme light   { scrollbar-color: rgba(80,80,80,0.78) rgba(200,200,200,0.31); }
900
//   @os windows @theme dark  { scrollbar-color: #6e6e6e #202020; }
901
//   @os windows @theme light { scrollbar-color: #828282 #f1f1f1; }
902
//   @os ios @theme dark      { scrollbar-color: rgba(255,255,255,0.4) transparent; }
903
//   @os ios @theme light     { scrollbar-color: rgba(0,0,0,0.4) transparent; }
904
//   @os android @theme dark  { scrollbar-color: rgba(255,255,255,0.3) transparent; }
905
//   @os android @theme light { scrollbar-color: rgba(0,0,0,0.3) transparent; }
906
//   @theme dark              { scrollbar-color: #646464 #2d2d2d; }
907
//   /* default */            { scrollbar-color: #c1c1c1 #f1f1f1; }
908

            
909
/// Helper to create a const `scrollbar-color` `CssProperty`.
910
4
const fn scrollbar_color(thumb: ColorU, track: ColorU) -> CssProperty {
911
4
    CssProperty::ScrollbarColor(CssPropertyValue::Exact(
912
4
        StyleScrollbarColor::Custom(ScrollbarColorCustom { thumb, track }),
913
4
    ))
914
4
}
915

            
916
/// Helper to create a const `scrollbar-width` `CssProperty`.
917
3
const fn scrollbar_width(w: LayoutScrollbarWidth) -> CssProperty {
918
3
    CssProperty::ScrollbarWidth(CssPropertyValue::Exact(w))
919
3
}
920

            
921
/// Helper to create a const `-azul-scrollbar-visibility` `CssProperty`.
922
3
const fn scrollbar_visibility(v: ScrollbarVisibilityMode) -> CssProperty {
923
3
    CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(v))
924
3
}
925

            
926
/// Helper to create a const `-azul-scrollbar-fade-delay` `CssProperty`.
927
15
const fn scrollbar_fade_delay(ms: u32) -> CssProperty {
928
15
    CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(ScrollbarFadeDelay::new(ms)))
929
15
}
930

            
931
/// Helper to create a const `-azul-scrollbar-fade-duration` `CssProperty`.
932
12
const fn scrollbar_fade_duration(ms: u32) -> CssProperty {
933
12
    CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(ScrollbarFadeDuration::new(ms)))
934
12
}
935

            
936
/// UA scrollbar CSS properties with `@os` / `@theme` conditions.
937
///
938
/// Ordered most-specific first.  The evaluation function picks the
939
/// first matching entry for each property type (`scrollbar-color`,
940
/// `scrollbar-width`, `-azul-scrollbar-visibility`,
941
/// `-azul-scrollbar-fade-delay`, `-azul-scrollbar-fade-duration`).
942
pub(crate) static UA_SCROLLBAR_CSS: &[CssPropertyWithConditions] = &[
943
    // ── scrollbar-width per OS ──────────────────────────────────────────
944
    // macOS → thin (overlay)
945
    CssPropertyWithConditions::with_single_condition(
946
        scrollbar_width(LayoutScrollbarWidth::Thin),
947
        &[DynamicSelector::Os(OsCondition::MacOS)],
948
    ),
949
    // iOS → thin
950
    CssPropertyWithConditions::with_single_condition(
951
        scrollbar_width(LayoutScrollbarWidth::Thin),
952
        &[DynamicSelector::Os(OsCondition::IOS)],
953
    ),
954
    // Android → thin
955
    CssPropertyWithConditions::with_single_condition(
956
        scrollbar_width(LayoutScrollbarWidth::Thin),
957
        &[DynamicSelector::Os(OsCondition::Android)],
958
    ),
959
    // default → auto (classic)
960
    CssPropertyWithConditions::simple(
961
        scrollbar_width(LayoutScrollbarWidth::Auto),
962
    ),
963

            
964
    // ── scrollbar-visibility per OS ─────────────────────────────────────
965
    // macOS → overlay (show only when scrolling)
966
    CssPropertyWithConditions::with_single_condition(
967
        scrollbar_visibility(ScrollbarVisibilityMode::WhenScrolling),
968
        &[DynamicSelector::Os(OsCondition::MacOS)],
969
    ),
970
    // iOS → overlay
971
    CssPropertyWithConditions::with_single_condition(
972
        scrollbar_visibility(ScrollbarVisibilityMode::WhenScrolling),
973
        &[DynamicSelector::Os(OsCondition::IOS)],
974
    ),
975
    // Android → overlay
976
    CssPropertyWithConditions::with_single_condition(
977
        scrollbar_visibility(ScrollbarVisibilityMode::WhenScrolling),
978
        &[DynamicSelector::Os(OsCondition::Android)],
979
    ),
980
    // default → always visible (classic)
981
    CssPropertyWithConditions::simple(
982
        scrollbar_visibility(ScrollbarVisibilityMode::Always),
983
    ),
984

            
985
    // ── scrollbar-fade-delay per OS ─────────────────────────────────────
986
    CssPropertyWithConditions::with_single_condition(
987
        scrollbar_fade_delay(500),
988
        &[DynamicSelector::Os(OsCondition::MacOS)],
989
    ),
990
    CssPropertyWithConditions::with_single_condition(
991
        scrollbar_fade_delay(500),
992
        &[DynamicSelector::Os(OsCondition::IOS)],
993
    ),
994
    CssPropertyWithConditions::with_single_condition(
995
        scrollbar_fade_delay(300),
996
        &[DynamicSelector::Os(OsCondition::Android)],
997
    ),
998
    // default → 0 (no fade)
999
    CssPropertyWithConditions::simple(
        scrollbar_fade_delay(0),
    ),
    // ── scrollbar-fade-duration per OS ──────────────────────────────────
    CssPropertyWithConditions::with_single_condition(
        scrollbar_fade_duration(200),
        &[DynamicSelector::Os(OsCondition::MacOS)],
    ),
    CssPropertyWithConditions::with_single_condition(
        scrollbar_fade_duration(200),
        &[DynamicSelector::Os(OsCondition::IOS)],
    ),
    CssPropertyWithConditions::with_single_condition(
        scrollbar_fade_duration(150),
        &[DynamicSelector::Os(OsCondition::Android)],
    ),
    // default → 0 (instant)
    CssPropertyWithConditions::simple(
        scrollbar_fade_duration(0),
    ),
    // ── scrollbar-color per OS + theme ──────────────────────────────────
    // macOS dark: light grey thumb on dark semi-transparent track
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 180, g: 180, b: 180, a: 200 },
            ColorU { r: 40, g: 40, b: 40, a: 80 },
        ),
        &[DynamicSelector::Os(OsCondition::MacOS), DynamicSelector::Theme(ThemeCondition::Dark)],
    ),
    // macOS light: dark grey thumb on light semi-transparent track
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 80, g: 80, b: 80, a: 200 },
            ColorU { r: 200, g: 200, b: 200, a: 80 },
        ),
        &[DynamicSelector::Os(OsCondition::MacOS), DynamicSelector::Theme(ThemeCondition::Light)],
    ),
    // Windows dark
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 110, g: 110, b: 110, a: 255 },
            ColorU { r: 32, g: 32, b: 32, a: 255 },
        ),
        &[DynamicSelector::Os(OsCondition::Windows), DynamicSelector::Theme(ThemeCondition::Dark)],
    ),
    // Windows light
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 130, g: 130, b: 130, a: 255 },
            ColorU { r: 241, g: 241, b: 241, a: 255 },
        ),
        &[DynamicSelector::Os(OsCondition::Windows), DynamicSelector::Theme(ThemeCondition::Light)],
    ),
    // iOS dark
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 255, g: 255, b: 255, a: 100 },
            ColorU::TRANSPARENT,
        ),
        &[DynamicSelector::Os(OsCondition::IOS), DynamicSelector::Theme(ThemeCondition::Dark)],
    ),
    // iOS light
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 0, g: 0, b: 0, a: 100 },
            ColorU::TRANSPARENT,
        ),
        &[DynamicSelector::Os(OsCondition::IOS), DynamicSelector::Theme(ThemeCondition::Light)],
    ),
    // Android dark
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 255, g: 255, b: 255, a: 77 },
            ColorU::TRANSPARENT,
        ),
        &[DynamicSelector::Os(OsCondition::Android), DynamicSelector::Theme(ThemeCondition::Dark)],
    ),
    // Android light
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 0, g: 0, b: 0, a: 77 },
            ColorU::TRANSPARENT,
        ),
        &[DynamicSelector::Os(OsCondition::Android), DynamicSelector::Theme(ThemeCondition::Light)],
    ),
    // Linux / unknown dark fallback
    CssPropertyWithConditions::with_single_condition(
        scrollbar_color(
            ColorU { r: 100, g: 100, b: 100, a: 255 },
            ColorU { r: 45, g: 45, b: 45, a: 255 },
        ),
        &[DynamicSelector::Theme(ThemeCondition::Dark)],
    ),
    // Unconditional fallback (classic light)
    CssPropertyWithConditions::simple(
        scrollbar_color(
            ColorU { r: 193, g: 193, b: 193, a: 255 },
            ColorU { r: 241, g: 241, b: 241, a: 255 },
        ),
    ),
];
/// Resolved UA scrollbar defaults after evaluating conditions.
///
/// All fields are guaranteed to resolve because `UA_SCROLLBAR_CSS`
/// contains unconditional fallback entries for every property type.
#[derive(Debug, Copy, Clone)]
pub struct ResolvedUaScrollbar {
    pub color: StyleScrollbarColor,
    pub width: LayoutScrollbarWidth,
    pub visibility: ScrollbarVisibilityMode,
    pub fade_delay: ScrollbarFadeDelay,
    pub fade_duration: ScrollbarFadeDuration,
}
/// Evaluate UA scrollbar CSS rules against a `DynamicSelectorContext`.
///
/// Iterates `UA_SCROLLBAR_CSS` and picks the first matching entry per
/// property type.  Unconditional fallback entries in the table guarantee
/// that every field resolves.
811449
#[must_use] pub fn evaluate_ua_scrollbar_css(ctx: &DynamicSelectorContext) -> ResolvedUaScrollbar {
811449
    let mut color: Option<StyleScrollbarColor> = None;
811449
    let mut width: Option<LayoutScrollbarWidth> = None;
811449
    let mut visibility: Option<ScrollbarVisibilityMode> = None;
811449
    let mut fade_delay: Option<ScrollbarFadeDelay> = None;
811449
    let mut fade_duration: Option<ScrollbarFadeDuration> = None;
21097381
    for prop in UA_SCROLLBAR_CSS {
21097381
        if !prop.matches(ctx) {
17039884
            continue;
4057497
        }
811512
        match &prop.property {
811449
            CssProperty::ScrollbarColor(CssPropertyValue::Exact(c)) => {
811449
                if color.is_none() {
811449
                    color = Some(*c);
811449
                }
            }
811512
            CssProperty::ScrollbarWidth(CssPropertyValue::Exact(w)) => {
811512
                if width.is_none() {
811449
                    width = Some(*w);
811449
                }
            }
811512
            CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(v)) => {
811512
                if visibility.is_none() {
811449
                    visibility = Some(*v);
811449
                }
            }
811512
            CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(d)) => {
811512
                if fade_delay.is_none() {
811449
                    fade_delay = Some(*d);
811449
                }
            }
811512
            CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(d)) => {
811512
                if fade_duration.is_none() {
811449
                    fade_duration = Some(*d);
811449
                }
            }
            _ => {}
        }
4057497
        if color.is_some() && width.is_some() && visibility.is_some()
811449
            && fade_delay.is_some() && fade_duration.is_some()
        {
811449
            break;
3246048
        }
    }
    // Unconditional `simple` entries in UA_SCROLLBAR_CSS guarantee all
    // fields resolve; these defaults match those entries as a safety net.
811449
    ResolvedUaScrollbar {
811449
        color: color.unwrap_or(StyleScrollbarColor::Custom(ScrollbarColorCustom {
811449
            thumb: ColorU { r: 193, g: 193, b: 193, a: 255 },
811449
            track: ColorU { r: 241, g: 241, b: 241, a: 255 },
811449
        })),
811449
        width: width.unwrap_or(LayoutScrollbarWidth::Auto),
811449
        visibility: visibility.unwrap_or(ScrollbarVisibilityMode::Always),
811449
        fade_delay: fade_delay.unwrap_or(ScrollbarFadeDelay { ms: 0 }),
811449
        fade_duration: fade_duration.unwrap_or(ScrollbarFadeDuration { ms: 0 }),
811449
    }
811449
}
#[cfg(test)]
mod autotest_generated {
    use alloc::{string::String, vec, vec::Vec};
    use azul_css::{corety::AzString, css::BoxOrStatic, props::basic::length::SizeMetric};
    use super::*;
    use crate::resources::{ImageRef, RawImageFormat};
    // ------------------------------------------------------------------
    // Constructors / helpers
    // ------------------------------------------------------------------
    fn text_node(s: &str) -> NodeType {
        NodeType::Text(BoxOrStatic::heap(AzString::from(s)))
    }
    /// A VirtualView is a scroll container BY DEFAULT: `overflow: auto` on
    /// both axes from the UA sheet, so app CSS no longer has to opt in (the
    /// virtual-size-aware necessity rule keeps bars away until the published
    /// `virtual_scroll_size` actually overflows). Opt-outs stay explicit
    /// (`overflow: hidden` — map/video); invisible-but-scrollable composes
    /// via `scrollbar-width: none`.
    #[test]
    fn virtual_view_defaults_to_overflow_auto_on_both_axes() {
        use azul_css::props::layout::LayoutOverflow;
        for pt in [CssPropertyType::OverflowX, CssPropertyType::OverflowY] {
            let got = get_ua_property(&NodeType::VirtualView, pt)
                .unwrap_or_else(|| panic!("no UA default for VirtualView {pt:?}"));
            let ok = matches!(
                got,
                CssProperty::OverflowX(CssPropertyValue::Exact(LayoutOverflow::Auto))
                    | CssProperty::OverflowY(CssPropertyValue::Exact(LayoutOverflow::Auto))
            );
            assert!(ok, "VirtualView {pt:?} UA default is not overflow:auto: {got:?}");
        }
        // And the axis-correct variant is returned for each request.
        assert!(matches!(
            get_ua_property(&NodeType::VirtualView, CssPropertyType::OverflowX),
            Some(CssProperty::OverflowX(_))
        ));
        assert!(matches!(
            get_ua_property(&NodeType::VirtualView, CssPropertyType::OverflowY),
            Some(CssProperty::OverflowY(_))
        ));
    }
    fn icon_node(s: &str) -> NodeType {
        NodeType::Icon(BoxOrStatic::heap(AzString::from(s)))
    }
    fn image_node() -> NodeType {
        NodeType::Image(BoxOrStatic::heap(ImageRef::null_image(
            1,
            1,
            RawImageFormat::RGBA8,
            Vec::new(),
        )))
    }
    /// Broad (not literally exhaustive) sample of `NodeType`, covering every
    /// variant that has an arm in `get_ua_property` plus a spread of variants
    /// that have none, so the catch-all arms get exercised too.
    fn sample_node_types() -> Vec<NodeType> {
        use crate::dom::NodeType as NT;
        vec![
            // matched arms
            NT::Html, NT::Head, NT::Body, NT::Div, NT::P, NT::Main, NT::Header,
            NT::Footer, NT::Section, NT::Article, NT::Aside, NT::Nav,
            NT::H1, NT::H2, NT::H3, NT::H4, NT::H5, NT::H6,
            NT::Ul, NT::Ol, NT::Li, NT::Dl, NT::Dt, NT::Dd,
            NT::Span, NT::A, NT::Strong, NT::Em, NT::B, NT::I, NT::U, NT::Small,
            NT::Code, NT::Kbd, NT::Samp, NT::Sub, NT::Sup,
            NT::Pre, NT::BlockQuote, NT::Hr,
            NT::Table, NT::THead, NT::TBody, NT::TFoot, NT::Tr, NT::Th, NT::Td,
            NT::Caption, NT::ColGroup, NT::Col,
            NT::Form, NT::Input, NT::Button, NT::Select, NT::TextArea, NT::Label,
            NT::Title, NT::Script, NT::Style, NT::Link,
            NT::Br, NT::Video, NT::Audio, NT::Canvas, NT::Svg, NT::VirtualView,
            NT::SelectOption, NT::OptGroup,
            NT::Abbr, NT::Cite, NT::Del, NT::Ins, NT::Mark, NT::Q, NT::Dfn,
            NT::Var, NT::Time, NT::Data, NT::Wbr, NT::Bdi, NT::Bdo,
            NT::Rp, NT::Rt, NT::Rtc, NT::Ruby,
            NT::FieldSet, NT::Figure, NT::FigCaption, NT::Details, NT::Summary,
            NT::Dialog, NT::Menu, NT::Dir,
            // unmatched arms (must fall through to the catch-alls)
            NT::Address, NT::Legend, NT::Output, NT::Progress, NT::Meter,
            NT::DataList, NT::MenuItem, NT::S, NT::Big, NT::Acronym,
            NT::Object, NT::Param, NT::Embed, NT::Source, NT::Track, NT::Map,
            NT::Area, NT::Meta, NT::Base, NT::Before, NT::After, NT::Marker,
            NT::Placeholder, NT::SvgG, NT::SvgPath, NT::SvgRect,
            NT::SvgText(AzString::from("svg-text")),
            // payload-carrying variants
            text_node(""),
            text_node("hello"),
            icon_node("home"),
            image_node(),
        ]
    }
    fn all_os() -> Vec<OsCondition> {
        vec![
            OsCondition::Any,
            OsCondition::Apple,
            OsCondition::MacOS,
            OsCondition::IOS,
            OsCondition::Linux,
            OsCondition::Windows,
            OsCondition::Android,
            OsCondition::Web,
        ]
    }
    fn all_themes() -> Vec<ThemeCondition> {
        vec![
            ThemeCondition::Light,
            ThemeCondition::Dark,
            ThemeCondition::Custom(AzString::from("neon")),
            ThemeCondition::SystemPreferred,
        ]
    }
    fn ctx(os: OsCondition, theme: ThemeCondition) -> DynamicSelectorContext {
        DynamicSelectorContext {
            os,
            theme,
            ..DynamicSelectorContext::default()
        }
    }
    const CLASSIC_LIGHT_THUMB: ColorU = ColorU { r: 193, g: 193, b: 193, a: 255 };
    const CLASSIC_LIGHT_TRACK: ColorU = ColorU { r: 241, g: 241, b: 241, a: 255 };
    fn custom_color(thumb: ColorU, track: ColorU) -> StyleScrollbarColor {
        StyleScrollbarColor::Custom(ScrollbarColorCustom { thumb, track })
    }
    /// Extract the `(thumb, track)` pair, panicking if the property is not a
    /// `Custom` scrollbar color.
    fn unwrap_custom(c: StyleScrollbarColor) -> (ColorU, ColorU) {
        match c {
            StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
            StyleScrollbarColor::Auto => panic!("expected a Custom scrollbar color, got Auto"),
        }
    }
    fn display_of(nt: &NodeType) -> LayoutDisplay {
        match get_ua_property(nt, CssPropertyType::Display) {
            Some(CssProperty::Display(CssPropertyValue::Exact(d))) => *d,
            other => panic!("{nt:?}: expected an exact display value, got {other:?}"),
        }
    }
    fn font_size_em(nt: &NodeType) -> f32 {
        match get_ua_property(nt, CssPropertyType::FontSize) {
            Some(CssProperty::FontSize(CssPropertyValue::Exact(fs))) => {
                assert_eq!(fs.inner.metric, SizeMetric::Em, "{nt:?}: font-size must be em-relative");
                fs.inner.number.get()
            }
            other => panic!("{nt:?}: expected an exact em font-size, got {other:?}"),
        }
    }
    // ==================================================================
    // get_ua_property — table-wide invariants
    // ==================================================================
    /// The single most important invariant of the lookup table: the property
    /// that comes back must be *the property that was asked for*. A copy-paste
    /// slip in the ~200-arm table (e.g. `(H1, MarginBottom) => &MARGIN_TOP_...`)
    /// would silently mis-style elements; nothing else in the codebase checks it.
    #[test]
    fn returned_property_always_has_the_requested_type() {
        for nt in sample_node_types() {
            for pt in CssPropertyType::ALL {
                if let Some(prop) = get_ua_property(&nt, *pt) {
                    assert_eq!(
                        prop.get_type(),
                        *pt,
                        "get_ua_property({nt:?}, {pt:?}) returned a {:?} property",
                        prop.get_type()
                    );
                }
            }
        }
    }
    #[test]
    fn full_cross_product_never_panics_and_is_deterministic() {
        for nt in sample_node_types() {
            for pt in CssPropertyType::ALL {
                let a = get_ua_property(&nt, *pt);
                let b = get_ua_property(&nt, *pt);
                match (a, b) {
                    (Some(a), Some(b)) => assert!(
                        core::ptr::eq(a, b),
                        "{nt:?}/{pt:?}: repeated lookups must hand back the same static"
                    ),
                    (None, None) => {}
                    _ => panic!("{nt:?}/{pt:?}: lookup is not deterministic"),
                }
            }
        }
    }
    /// Documented contract: the `(_, Display)` catch-all means *every* node type
    /// resolves a display value, so layout never sees a node without one.
    #[test]
    fn display_resolves_for_every_node_type() {
        for nt in sample_node_types() {
            assert!(
                get_ua_property(&nt, CssPropertyType::Display).is_some(),
                "{nt:?} has no default display"
            );
        }
    }
    #[test]
    fn unknown_elements_default_to_inline_display() {
        // Per CSS spec, unknown/custom elements are inline.
        for nt in [NodeType::Address, NodeType::Legend, NodeType::Meter, NodeType::SvgPath] {
            assert_eq!(display_of(&nt), LayoutDisplay::Inline, "{nt:?}");
        }
    }
    /// `cursor` is deliberately defined for exactly three node types; anything
    /// else must return `None` so the cursor-resolution walk can inherit.
    #[test]
    fn cursor_default_exists_only_for_button_textarea_and_text() {
        for nt in sample_node_types() {
            let has_cursor = get_ua_property(&nt, CssPropertyType::Cursor).is_some();
            let expected = matches!(nt, NodeType::Button | NodeType::TextArea | NodeType::Text(_));
            assert_eq!(has_cursor, expected, "{nt:?}: unexpected cursor default");
        }
    }
    // ==================================================================
    // get_ua_property — payload-carrying node types (unicode / huge / empty)
    // ==================================================================
    #[test]
    fn text_node_defaults_are_independent_of_the_payload() {
        let huge = "🦀".repeat(100_000);
        let payloads: Vec<String> = vec![
            String::new(),
            "\0".into(),
            "\u{202E}\u{200B}\u{FEFF}".into(), // RTL override, ZWSP, BOM
            "مرحبا بالعالم".into(),
            "🇩🇪👨‍👩‍👧‍👦".into(),
            "\u{FFFD}".into(),
            huge,
        ];
        for p in payloads {
            let nt = text_node(&p);
            assert_eq!(
                display_of(&nt),
                LayoutDisplay::Inline,
                "text node display must not depend on its content"
            );
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::Cursor),
                Some(&CURSOR_TEXT),
                "text node cursor must not depend on its content"
            );
            // Text nodes define no box properties of their own.
            assert!(get_ua_property(&nt, CssPropertyType::Width).is_none());
            assert!(get_ua_property(&nt, CssPropertyType::Height).is_none());
            assert!(get_ua_property(&nt, CssPropertyType::MarginTop).is_none());
        }
    }
    #[test]
    fn icon_and_image_nodes_are_inline_block_regardless_of_payload() {
        let huge_name = "x".repeat(50_000);
        let names: [&str; 4] = ["", "home", "🏠", huge_name.as_str()];
        for name in names {
            assert_eq!(display_of(&icon_node(name)), LayoutDisplay::InlineBlock, "icon {name:?}");
        }
        assert_eq!(display_of(&image_node()), LayoutDisplay::InlineBlock);
    }
    // ==================================================================
    // get_ua_property — specific, load-bearing defaults
    // ==================================================================
    /// Regression guard for the 2026-06-02 DIAG revert documented in the table:
    /// `(Html, Height) => HEIGHT_100_PERCENT` is commented out on purpose. If it
    /// comes back without the jump-table dispatch fix, children wrongly inherit
    /// `height: 100%`.
    #[test]
    fn html_has_no_default_height() {
        assert_eq!(get_ua_property(&NodeType::Html, CssPropertyType::Display), Some(&DISPLAY_BLOCK));
        assert!(
            get_ua_property(&NodeType::Html, CssPropertyType::Height).is_none(),
            "the (Html, Height) arm is intentionally disabled — see the DIAG note"
        );
    }
    /// `body { margin: 8px }` (Chrome UA), and crucially *no* width/height:
    /// giving body a size would break percentage sizing of its children.
    #[test]
    fn body_has_8px_margins_and_no_intrinsic_size() {
        assert_eq!(display_of(&NodeType::Body), LayoutDisplay::Block);
        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginTop), Some(&MARGIN_TOP_8PX));
        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginBottom), Some(&MARGIN_BOTTOM_8PX));
        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginLeft), Some(&MARGIN_LEFT_8PX));
        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginRight), Some(&MARGIN_RIGHT_8PX));
        assert!(get_ua_property(&NodeType::Body, CssPropertyType::Width).is_none());
        assert!(get_ua_property(&NodeType::Body, CssPropertyType::Height).is_none());
    }
    /// Block elements must have `width: auto`, not `width: 100%` — the comment in
    /// the table calls this out as critical for flexbox (100% defeats flex-grow).
    #[test]
    fn block_elements_have_no_default_width() {
        for nt in [NodeType::Div, NodeType::P, NodeType::Section, NodeType::Main, NodeType::VirtualView] {
            assert_eq!(display_of(&nt), LayoutDisplay::Block, "{nt:?}");
            assert!(
                get_ua_property(&nt, CssPropertyType::Width).is_none(),
                "{nt:?} must be width:auto so it can flex-grow"
            );
        }
    }
    #[test]
    fn div_defines_only_a_display_default() {
        for pt in CssPropertyType::ALL {
            let got = get_ua_property(&NodeType::Div, *pt);
            if *pt == CssPropertyType::Display {
                assert!(got.is_some());
            } else {
                assert!(got.is_none(), "Div should not define a UA default for {pt:?}");
            }
        }
    }
    #[test]
    fn metadata_elements_are_display_none() {
        for nt in [NodeType::Head, NodeType::Title, NodeType::Script, NodeType::Style, NodeType::Link] {
            assert_eq!(display_of(&nt), LayoutDisplay::None, "{nt:?} must not render");
        }
    }
    #[test]
    fn heading_font_sizes_are_strictly_decreasing() {
        let sizes: Vec<f32> = [NodeType::H1, NodeType::H2, NodeType::H3, NodeType::H4, NodeType::H5, NodeType::H6]
            .iter()
            .map(font_size_em)
            .collect();
        // Chrome UA values — also verifies `const_em_fractional(1, 5)` really
        // encodes 1.5 (and not 1.05), which the digit-count encoding makes subtle.
        let expected = [2.0_f32, 1.5, 1.17, 1.0, 0.83, 0.67];
        for (i, (got, want)) in sizes.iter().zip(expected.iter()).enumerate() {
            assert!(
                (got - want).abs() < 1e-4,
                "H{} font-size: got {got}em, want {want}em",
                i + 1
            );
        }
        for w in sizes.windows(2) {
            assert!(w[0] > w[1], "heading font sizes must strictly decrease, got {sizes:?}");
        }
    }
    #[test]
    fn headings_are_bold_blocks_that_avoid_page_breaks() {
        for nt in [NodeType::H1, NodeType::H2, NodeType::H3, NodeType::H4, NodeType::H5, NodeType::H6] {
            assert_eq!(display_of(&nt), LayoutDisplay::Block, "{nt:?}");
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::FontWeight),
                Some(&FONT_WEIGHT_BOLD),
                "{nt:?}"
            );
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::BreakInside),
                Some(&BREAK_INSIDE_AVOID),
                "{nt:?}"
            );
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::BreakAfter),
                Some(&BREAK_AFTER_AVOID),
                "{nt:?}"
            );
            // Both margins must exist and be em-relative (they scale with font-size).
            for pt in [CssPropertyType::MarginTop, CssPropertyType::MarginBottom] {
                assert!(get_ua_property(&nt, pt).is_some(), "{nt:?} is missing {pt:?}");
            }
        }
    }
    /// Tables *can* break across pages; their rows/headers/footers cannot. The
    /// table comments say so explicitly, so lock the asymmetry in.
    #[test]
    fn tables_may_break_across_pages_but_rows_may_not() {
        assert!(get_ua_property(&NodeType::Table, CssPropertyType::BreakInside).is_none());
        assert!(get_ua_property(&NodeType::TBody, CssPropertyType::BreakInside).is_none());
        for nt in [NodeType::THead, NodeType::TFoot, NodeType::Tr] {
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::BreakInside),
                Some(&BREAK_INSIDE_AVOID),
                "{nt:?}"
            );
        }
    }
    #[test]
    fn table_display_types_are_not_crossed() {
        assert_eq!(display_of(&NodeType::Table), LayoutDisplay::Table);
        assert_eq!(display_of(&NodeType::THead), LayoutDisplay::TableHeaderGroup);
        assert_eq!(display_of(&NodeType::TBody), LayoutDisplay::TableRowGroup);
        assert_eq!(display_of(&NodeType::TFoot), LayoutDisplay::TableFooterGroup);
        assert_eq!(display_of(&NodeType::Tr), LayoutDisplay::TableRow);
        assert_eq!(display_of(&NodeType::Th), LayoutDisplay::TableCell);
        assert_eq!(display_of(&NodeType::Td), LayoutDisplay::TableCell);
        assert_eq!(display_of(&NodeType::Caption), LayoutDisplay::TableCaption);
        assert_eq!(display_of(&NodeType::ColGroup), LayoutDisplay::TableColumnGroup);
        assert_eq!(display_of(&NodeType::Col), LayoutDisplay::TableColumn);
    }
    #[test]
    fn table_cells_have_1px_padding_on_all_four_sides() {
        for nt in [NodeType::Th, NodeType::Td] {
            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingTop), Some(&PADDING_TOP_1PX), "{nt:?}");
            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingBottom), Some(&PADDING_BOTTOM_1PX), "{nt:?}");
            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingLeft), Some(&PADDING_LEFT_1PX), "{nt:?}");
            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingRight), Some(&PADDING_RIGHT_1PX), "{nt:?}");
            assert_eq!(get_ua_property(&nt, CssPropertyType::VerticalAlign), Some(&VERTICAL_ALIGN_MIDDLE), "{nt:?}");
        }
        // Only <th> is centered + bold.
        assert_eq!(get_ua_property(&NodeType::Th, CssPropertyType::TextAlign), Some(&TEXT_ALIGN_CENTER));
        assert_eq!(get_ua_property(&NodeType::Th, CssPropertyType::FontWeight), Some(&FONT_WEIGHT_BOLD));
        assert!(get_ua_property(&NodeType::Td, CssPropertyType::TextAlign).is_none());
        assert!(get_ua_property(&NodeType::Td, CssPropertyType::FontWeight).is_none());
    }
    /// A button's border is symmetric. Crossed sides (e.g. `BorderLeftWidth`
    /// answered with the *top* static) would render an asymmetric button, so
    /// check that each side carries the value the table promises.
    #[test]
    fn button_border_is_symmetric_on_all_four_sides() {
        let widths = [
            (CssPropertyType::BorderTopWidth, &BUTTON_BORDER_TOP_WIDTH),
            (CssPropertyType::BorderBottomWidth, &BUTTON_BORDER_BOTTOM_WIDTH),
            (CssPropertyType::BorderLeftWidth, &BUTTON_BORDER_LEFT_WIDTH),
            (CssPropertyType::BorderRightWidth, &BUTTON_BORDER_RIGHT_WIDTH),
        ];
        for (pt, want) in widths {
            assert_eq!(get_ua_property(&NodeType::Button, pt), Some(want), "{pt:?}");
        }
        let styles = [
            (CssPropertyType::BorderTopStyle, &BUTTON_BORDER_TOP_STYLE),
            (CssPropertyType::BorderBottomStyle, &BUTTON_BORDER_BOTTOM_STYLE),
            (CssPropertyType::BorderLeftStyle, &BUTTON_BORDER_LEFT_STYLE),
            (CssPropertyType::BorderRightStyle, &BUTTON_BORDER_RIGHT_STYLE),
        ];
        for (pt, want) in styles {
            assert_eq!(get_ua_property(&NodeType::Button, pt), Some(want), "{pt:?}");
        }
        let colors = [
            (CssPropertyType::BorderTopColor, &BUTTON_BORDER_TOP_COLOR),
            (CssPropertyType::BorderBottomColor, &BUTTON_BORDER_BOTTOM_COLOR),
            (CssPropertyType::BorderLeftColor, &BUTTON_BORDER_LEFT_COLOR),
            (CssPropertyType::BorderRightColor, &BUTTON_BORDER_RIGHT_COLOR),
        ];
        for (pt, want) in colors {
            assert_eq!(get_ua_property(&NodeType::Button, pt), Some(want), "{pt:?}");
        }
        assert_eq!(display_of(&NodeType::Button), LayoutDisplay::InlineBlock);
        assert_eq!(get_ua_property(&NodeType::Button, CssPropertyType::Cursor), Some(&CURSOR_POINTER));
    }
    /// `<hr>` draws its line from the *border*, not from a height — height must
    /// be exactly 0px, and the width exactly 100%.
    #[test]
    fn hr_line_comes_from_the_border_not_from_height() {
        match get_ua_property(&NodeType::Hr, CssPropertyType::Height) {
            Some(CssProperty::Height(CssPropertyValue::Exact(LayoutHeight::Px(pv)))) => {
                assert_eq!(pv.metric, SizeMetric::Px);
                assert!((pv.number.get() - 0.0).abs() < 1e-6, "hr height must be 0px");
            }
            other => panic!("hr height: {other:?}"),
        }
        match get_ua_property(&NodeType::Hr, CssPropertyType::Width) {
            Some(CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(pv)))) => {
                assert_eq!(pv.metric, SizeMetric::Percent);
                assert!((pv.number.get() - 100.0).abs() < 1e-4, "hr width must be 100%");
            }
            other => panic!("hr width: {other:?}"),
        }
        assert_eq!(get_ua_property(&NodeType::Hr, CssPropertyType::BorderTopStyle), Some(&BORDER_TOP_STYLE_INSET));
        assert_eq!(get_ua_property(&NodeType::Hr, CssPropertyType::BorderTopWidth), Some(&BORDER_TOP_WIDTH_1PX));
        assert_eq!(get_ua_property(&NodeType::Hr, CssPropertyType::BorderTopColor), Some(&BORDER_TOP_COLOR_GRAY));
    }
    #[test]
    fn list_containers_reset_the_counter_and_reserve_marker_space() {
        for (nt, marker) in [
            (NodeType::Ul, &LIST_STYLE_TYPE_DISC),
            (NodeType::Ol, &LIST_STYLE_TYPE_DECIMAL),
        ] {
            assert_eq!(display_of(&nt), LayoutDisplay::Block, "{nt:?}");
            assert_eq!(get_ua_property(&nt, CssPropertyType::ListStyleType), Some(marker), "{nt:?}");
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::CounterReset),
                Some(&COUNTER_RESET_LIST_ITEM),
                "{nt:?} must reset the list-item counter"
            );
            assert_eq!(
                get_ua_property(&nt, CssPropertyType::PaddingLeft),
                Some(&PADDING_INLINE_START_40PX),
                "{nt:?}"
            );
        }
        assert_eq!(display_of(&NodeType::Li), LayoutDisplay::ListItem);
    }
    #[test]
    fn inline_emphasis_and_link_defaults() {
        assert_eq!(get_ua_property(&NodeType::A, CssPropertyType::TextDecoration), Some(&TEXT_DECORATION_UNDERLINE));
        assert_eq!(get_ua_property(&NodeType::U, CssPropertyType::TextDecoration), Some(&TEXT_DECORATION_UNDERLINE));
        assert_eq!(get_ua_property(&NodeType::Strong, CssPropertyType::FontWeight), Some(&FONT_WEIGHT_BOLDER));
        assert_eq!(get_ua_property(&NodeType::B, CssPropertyType::FontWeight), Some(&FONT_WEIGHT_BOLDER));
        // <em>/<i> are italic via font-style, which the UA table does not define.
        assert!(get_ua_property(&NodeType::Em, CssPropertyType::FontWeight).is_none());
        assert!(get_ua_property(&NodeType::I, CssPropertyType::FontWeight).is_none());
    }
    // ==================================================================
    // const scrollbar helpers — numeric round-trips / boundaries
    // ==================================================================
    #[test]
    fn scrollbar_fade_delay_round_trips_every_boundary() {
        for ms in [0_u32, 1, 2, 299, 300, 500, u32::from(u16::MAX), i32::MAX as u32, u32::MAX - 1, u32::MAX] {
            match scrollbar_fade_delay(ms) {
                CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(d)) => {
                    assert_eq!(d.ms, ms, "fade-delay must round-trip losslessly");
                }
                other => panic!("scrollbar_fade_delay({ms}) built a {other:?}"),
            }
        }
    }
    #[test]
    fn scrollbar_fade_duration_round_trips_every_boundary() {
        for ms in [0_u32, 1, 150, 200, u32::from(u16::MAX), i32::MAX as u32, u32::MAX - 1, u32::MAX] {
            match scrollbar_fade_duration(ms) {
                CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(d)) => {
                    assert_eq!(d.ms, ms, "fade-duration must round-trip losslessly");
                }
                other => panic!("scrollbar_fade_duration({ms}) built a {other:?}"),
            }
        }
    }
    /// `u32::MAX` in a `const` item: if either helper ever grew an arithmetic
    /// conversion (ms → ns, ms → seconds), this fails to *compile* rather than
    /// silently wrapping in release and panicking in debug.
    #[test]
    fn scrollbar_fade_helpers_are_const_evaluable_at_u32_max() {
        const MAX_DELAY: CssProperty = scrollbar_fade_delay(u32::MAX);
        const MAX_DURATION: CssProperty = scrollbar_fade_duration(u32::MAX);
        const ZERO_DELAY: CssProperty = scrollbar_fade_delay(0);
        assert_eq!(MAX_DELAY, scrollbar_fade_delay(u32::MAX));
        assert_eq!(MAX_DURATION, scrollbar_fade_duration(u32::MAX));
        assert_eq!(ZERO_DELAY, scrollbar_fade_delay(0));
    }
    /// The two helpers take the same `u32` and differ only in the wrapper type —
    /// exactly the shape a copy-paste bug likes. Assert they stay distinct.
    #[test]
    fn fade_delay_and_fade_duration_produce_distinct_property_types() {
        assert_eq!(scrollbar_fade_delay(42).get_type(), CssPropertyType::ScrollbarFadeDelay);
        assert_eq!(scrollbar_fade_duration(42).get_type(), CssPropertyType::ScrollbarFadeDuration);
        assert_ne!(scrollbar_fade_delay(42), scrollbar_fade_duration(42));
    }
    /// A `0` delay means "never fades" (per the `ScrollbarFadeDelay` docs), so it
    /// must be stored as a literal zero, not as a sentinel.
    #[test]
    fn zero_fade_delay_and_duration_are_literal_zero() {
        assert_eq!(
            scrollbar_fade_delay(0),
            CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(ScrollbarFadeDelay::ZERO))
        );
        assert_eq!(
            scrollbar_fade_duration(0),
            CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(ScrollbarFadeDuration::ZERO))
        );
    }
    #[test]
    fn scrollbar_color_never_swaps_thumb_and_track() {
        let cases = [
            (ColorU { r: 1, g: 2, b: 3, a: 4 }, ColorU { r: 5, g: 6, b: 7, a: 8 }),
            (ColorU { r: 0, g: 0, b: 0, a: 0 }, ColorU { r: 255, g: 255, b: 255, a: 255 }),
            (ColorU { r: 255, g: 255, b: 255, a: 255 }, ColorU::TRANSPARENT),
            (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
        ];
        for (thumb, track) in cases {
            match scrollbar_color(thumb, track) {
                CssProperty::ScrollbarColor(CssPropertyValue::Exact(StyleScrollbarColor::Custom(c))) => {
                    assert_eq!(c.thumb, thumb, "thumb was not preserved");
                    assert_eq!(c.track, track, "track was not preserved (arguments swapped?)");
                }
                other => panic!("scrollbar_color built a {other:?}"),
            }
        }
    }
    #[test]
    fn scrollbar_width_and_visibility_round_trip_every_variant() {
        for w in [LayoutScrollbarWidth::Auto, LayoutScrollbarWidth::Thin, LayoutScrollbarWidth::None] {
            match scrollbar_width(w) {
                CssProperty::ScrollbarWidth(CssPropertyValue::Exact(got)) => assert_eq!(got, w),
                other => panic!("scrollbar_width({w:?}) built a {other:?}"),
            }
        }
        for v in [
            ScrollbarVisibilityMode::Always,
            ScrollbarVisibilityMode::WhenScrolling,
            ScrollbarVisibilityMode::Auto,
        ] {
            match scrollbar_visibility(v) {
                CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(got)) => assert_eq!(got, v),
                other => panic!("scrollbar_visibility({v:?}) built a {other:?}"),
            }
        }
    }
    // ==================================================================
    // UA_SCROLLBAR_CSS — table shape invariants
    // ==================================================================
    /// `evaluate_ua_scrollbar_css` matches on exactly five property kinds and
    /// silently drops everything else via `_ => {}`. A sixth property added to
    /// the table would therefore never take effect — fail loudly here instead.
    #[test]
    fn table_contains_only_property_kinds_the_evaluator_understands() {
        let understood = [
            CssPropertyType::ScrollbarColor,
            CssPropertyType::ScrollbarWidth,
            CssPropertyType::ScrollbarVisibility,
            CssPropertyType::ScrollbarFadeDelay,
            CssPropertyType::ScrollbarFadeDuration,
        ];
        for (i, entry) in UA_SCROLLBAR_CSS.iter().enumerate() {
            let ty = entry.property.get_type();
            assert!(
                understood.contains(&ty),
                "UA_SCROLLBAR_CSS[{i}] is a {ty:?}, which evaluate_ua_scrollbar_css ignores"
            );
        }
    }
    /// The evaluator only reads `CssPropertyValue::Exact`; an `Auto`/`Inherit`
    /// entry would be skipped without a trace.
    #[test]
    fn every_table_entry_carries_an_exact_value() {
        for (i, entry) in UA_SCROLLBAR_CSS.iter().enumerate() {
            let is_exact = matches!(
                &entry.property,
                CssProperty::ScrollbarColor(CssPropertyValue::Exact(_))
                    | CssProperty::ScrollbarWidth(CssPropertyValue::Exact(_))
                    | CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(_))
                    | CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(_))
                    | CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(_))
            );
            assert!(is_exact, "UA_SCROLLBAR_CSS[{i}] is not an Exact value: {:?}", entry.property);
        }
    }
    /// The documented guarantee ("unconditional fallback entries … guarantee that
    /// every field resolves") plus the ordering rule it depends on: under
    /// first-match-wins, an unconditional entry that is *not* last for its
    /// property type would make every rule after it dead code.
    #[test]
    fn each_property_type_has_exactly_one_unconditional_entry_and_it_is_last() {
        for ty in [
            CssPropertyType::ScrollbarColor,
            CssPropertyType::ScrollbarWidth,
            CssPropertyType::ScrollbarVisibility,
            CssPropertyType::ScrollbarFadeDelay,
            CssPropertyType::ScrollbarFadeDuration,
        ] {
            let of_type: Vec<&CssPropertyWithConditions> = UA_SCROLLBAR_CSS
                .iter()
                .filter(|e| e.property.get_type() == ty)
                .collect();
            assert!(!of_type.is_empty(), "{ty:?} has no entry at all");
            let unconditional: Vec<usize> = of_type
                .iter()
                .enumerate()
                .filter(|(_, e)| e.apply_if.as_slice().is_empty())
                .map(|(i, _)| i)
                .collect();
            assert_eq!(
                unconditional.len(),
                1,
                "{ty:?} must have exactly one unconditional fallback, found {}",
                unconditional.len()
            );
            assert_eq!(
                unconditional[0],
                of_type.len() - 1,
                "{ty:?}: the unconditional fallback must come last, otherwise the \
                 {} rule(s) after it are dead under first-match-wins",
                of_type.len() - 1 - unconditional[0]
            );
        }
    }
    // ==================================================================
    // evaluate_ua_scrollbar_css
    // ==================================================================
    #[test]
    fn default_context_resolves_to_the_classic_light_scrollbar() {
        let r = evaluate_ua_scrollbar_css(&DynamicSelectorContext::default());
        assert_eq!(r.width, LayoutScrollbarWidth::Auto);
        assert_eq!(r.visibility, ScrollbarVisibilityMode::Always);
        assert_eq!(r.fade_delay.ms, 0);
        assert_eq!(r.fade_duration.ms, 0);
        assert_eq!(unwrap_custom(r.color), (CLASSIC_LIGHT_THUMB, CLASSIC_LIGHT_TRACK));
    }
    #[test]
    fn per_os_and_theme_defaults_are_what_the_table_promises() {
        let cases: Vec<(OsCondition, ThemeCondition, LayoutScrollbarWidth, ScrollbarVisibilityMode, u32, u32, StyleScrollbarColor)> = vec![
            (
                OsCondition::MacOS, ThemeCondition::Dark,
                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
                custom_color(ColorU { r: 180, g: 180, b: 180, a: 200 }, ColorU { r: 40, g: 40, b: 40, a: 80 }),
            ),
            (
                OsCondition::MacOS, ThemeCondition::Light,
                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
                custom_color(ColorU { r: 80, g: 80, b: 80, a: 200 }, ColorU { r: 200, g: 200, b: 200, a: 80 }),
            ),
            (
                OsCondition::Windows, ThemeCondition::Dark,
                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
                custom_color(ColorU { r: 110, g: 110, b: 110, a: 255 }, ColorU { r: 32, g: 32, b: 32, a: 255 }),
            ),
            (
                OsCondition::Windows, ThemeCondition::Light,
                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
                custom_color(ColorU { r: 130, g: 130, b: 130, a: 255 }, ColorU { r: 241, g: 241, b: 241, a: 255 }),
            ),
            (
                OsCondition::IOS, ThemeCondition::Dark,
                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
                custom_color(ColorU { r: 255, g: 255, b: 255, a: 100 }, ColorU::TRANSPARENT),
            ),
            (
                OsCondition::IOS, ThemeCondition::Light,
                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
                custom_color(ColorU { r: 0, g: 0, b: 0, a: 100 }, ColorU::TRANSPARENT),
            ),
            (
                OsCondition::Android, ThemeCondition::Dark,
                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 300, 150,
                custom_color(ColorU { r: 255, g: 255, b: 255, a: 77 }, ColorU::TRANSPARENT),
            ),
            (
                OsCondition::Android, ThemeCondition::Light,
                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 300, 150,
                custom_color(ColorU { r: 0, g: 0, b: 0, a: 77 }, ColorU::TRANSPARENT),
            ),
            (
                // Linux has no OS-specific colour rule: dark falls through to the
                // generic dark entry.
                OsCondition::Linux, ThemeCondition::Dark,
                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
                custom_color(ColorU { r: 100, g: 100, b: 100, a: 255 }, ColorU { r: 45, g: 45, b: 45, a: 255 }),
            ),
            (
                OsCondition::Linux, ThemeCondition::Light,
                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
                custom_color(CLASSIC_LIGHT_THUMB, CLASSIC_LIGHT_TRACK),
            ),
            (
                OsCondition::Web, ThemeCondition::Dark,
                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
                custom_color(ColorU { r: 100, g: 100, b: 100, a: 255 }, ColorU { r: 45, g: 45, b: 45, a: 255 }),
            ),
        ];
        for (os, theme, width, visibility, delay, duration, color) in cases {
            let r = evaluate_ua_scrollbar_css(&ctx(os, theme.clone()));
            assert_eq!(r.width, width, "{os:?}/{theme:?}: width");
            assert_eq!(r.visibility, visibility, "{os:?}/{theme:?}: visibility");
            assert_eq!(r.fade_delay.ms, delay, "{os:?}/{theme:?}: fade-delay");
            assert_eq!(r.fade_duration.ms, duration, "{os:?}/{theme:?}: fade-duration");
            assert_eq!(r.color, color, "{os:?}/{theme:?}: color");
        }
    }
    /// `match_theme` compares by equality (except when the *condition* is
    /// `SystemPreferred`), so a context theme of `Custom(..)` / `SystemPreferred`
    /// matches no `@theme` rule at all — every such context must still resolve a
    /// colour, via the unconditional fallback.
    #[test]
    fn unrecognised_context_themes_fall_back_instead_of_failing() {
        for theme in [ThemeCondition::Custom(AzString::from("")), ThemeCondition::Custom(AzString::from("🎨")), ThemeCondition::SystemPreferred] {
            // OS-conditioned properties still apply — only the theme rules miss.
            let r = evaluate_ua_scrollbar_css(&ctx(OsCondition::MacOS, theme.clone()));
            assert_eq!(r.width, LayoutScrollbarWidth::Thin, "{theme:?}");
            assert_eq!(r.visibility, ScrollbarVisibilityMode::WhenScrolling, "{theme:?}");
            assert_eq!(
                unwrap_custom(r.color),
                (CLASSIC_LIGHT_THUMB, CLASSIC_LIGHT_TRACK),
                "{theme:?}: must fall back to the unconditional colour"
            );
        }
    }
    /// `OsCondition::Apple` is condition-side sugar (it *matches* MacOS/IOS); as a
    /// *context* value it equals neither, so an `Apple` context gets the generic
    /// defaults. `DynamicSelectorContext::from_system_style` never produces it, so
    /// this pins down the (slightly surprising) behaviour rather than blessing it.
    #[test]
    fn apple_as_a_context_os_matches_no_macos_or_ios_rule() {
        let r = evaluate_ua_scrollbar_css(&ctx(OsCondition::Apple, ThemeCondition::Dark));
        assert_eq!(r.width, LayoutScrollbarWidth::Auto);
        assert_eq!(r.visibility, ScrollbarVisibilityMode::Always);
        assert_eq!(r.fade_delay.ms, 0);
        assert_eq!(r.fade_duration.ms, 0);
    }
    /// Overlay scrollbars are a package deal: `thin` ⇔ `when-scrolling` ⇔ a
    /// non-zero fade delay ⇔ a non-zero fade duration. A per-OS rule added to one
    /// group but forgotten in another would produce an overlay scrollbar that
    /// never fades (or a classic one that does).
    #[test]
    fn overlay_scrollbar_fields_stay_consistent_across_every_os_and_theme() {
        for os in all_os() {
            for theme in all_themes() {
                let r = evaluate_ua_scrollbar_css(&ctx(os, theme.clone()));
                let thin = r.width == LayoutScrollbarWidth::Thin;
                let overlay = r.visibility == ScrollbarVisibilityMode::WhenScrolling;
                assert_eq!(thin, overlay, "{os:?}/{theme:?}: thin/when-scrolling disagree");
                assert_eq!(
                    overlay,
                    r.fade_delay.ms > 0,
                    "{os:?}/{theme:?}: an overlay scrollbar needs a fade delay"
                );
                assert_eq!(
                    overlay,
                    r.fade_duration.ms > 0,
                    "{os:?}/{theme:?}: an overlay scrollbar needs a fade duration"
                );
                // The table only ever supplies Custom colours.
                assert!(
                    matches!(r.color, StyleScrollbarColor::Custom(_)),
                    "{os:?}/{theme:?}: colour resolved to Auto"
                );
            }
        }
    }
    /// The evaluator `break`s early once all five fields are filled. Cross-check
    /// it against a straight first-match-wins scan with no early exit: the two
    /// must agree for every context, or the optimisation changed the semantics.
    #[test]
    fn early_break_does_not_change_the_first_match_result() {
        for os in all_os() {
            for theme in all_themes() {
                let c = ctx(os, theme.clone());
                let got = evaluate_ua_scrollbar_css(&c);
                let mut want_color = None;
                let mut want_width = None;
                let mut want_vis = None;
                let mut want_delay = None;
                let mut want_dur = None;
                for entry in UA_SCROLLBAR_CSS.iter().filter(|e| e.matches(&c)) {
                    match &entry.property {
                        CssProperty::ScrollbarColor(CssPropertyValue::Exact(v)) => {
                            if want_color.is_none() {
                                want_color = Some(*v);
                            }
                        }
                        CssProperty::ScrollbarWidth(CssPropertyValue::Exact(v)) => {
                            if want_width.is_none() {
                                want_width = Some(*v);
                            }
                        }
                        CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(v)) => {
                            if want_vis.is_none() {
                                want_vis = Some(*v);
                            }
                        }
                        CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(v)) => {
                            if want_delay.is_none() {
                                want_delay = Some(*v);
                            }
                        }
                        CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(v)) => {
                            if want_dur.is_none() {
                                want_dur = Some(*v);
                            }
                        }
                        _ => {}
                    }
                }
                let label = alloc::format!("{os:?}/{theme:?}");
                assert_eq!(Some(got.color), want_color, "{label}: color");
                assert_eq!(Some(got.width), want_width, "{label}: width");
                assert_eq!(Some(got.visibility), want_vis, "{label}: visibility");
                assert_eq!(Some(got.fade_delay), want_delay, "{label}: fade-delay");
                assert_eq!(Some(got.fade_duration), want_dur, "{label}: fade-duration");
            }
        }
    }
    /// Degenerate / hostile context values (NaN, infinities, empty and huge
    /// strings) must not panic, and every field must still resolve.
    #[test]
    fn degenerate_context_values_do_not_panic() {
        let hostile = [
            (f32::NAN, f32::NAN),
            (0.0, 0.0),
            (-0.0, -1.0),
            (f32::INFINITY, f32::NEG_INFINITY),
            (f32::MAX, f32::MIN),
            (f32::MIN_POSITIVE, f32::EPSILON),
        ];
        for (w, h) in hostile {
            let c = DynamicSelectorContext {
                os: OsCondition::MacOS,
                theme: ThemeCondition::Dark,
                de_version: u32::MAX,
                viewport_width: w,
                viewport_height: h,
                container_width: h,
                container_height: w,
                language: AzString::from(""),
                ..DynamicSelectorContext::default()
            };
            let r = evaluate_ua_scrollbar_css(&c);
            // macOS/dark rules are OS+theme-only, so viewport garbage cannot
            // perturb them.
            assert_eq!(r.width, LayoutScrollbarWidth::Thin, "viewport {w}x{h}");
            assert_eq!(r.fade_delay.ms, 500, "viewport {w}x{h}");
            assert!(matches!(r.color, StyleScrollbarColor::Custom(_)), "viewport {w}x{h}");
        }
    }
    #[test]
    fn evaluate_is_deterministic() {
        for os in all_os() {
            for theme in all_themes() {
                let c = ctx(os, theme.clone());
                let a = evaluate_ua_scrollbar_css(&c);
                let b = evaluate_ua_scrollbar_css(&c);
                assert_eq!(a.color, b.color, "{os:?}/{theme:?}");
                assert_eq!(a.width, b.width, "{os:?}/{theme:?}");
                assert_eq!(a.visibility, b.visibility, "{os:?}/{theme:?}");
                assert_eq!(a.fade_delay, b.fade_delay, "{os:?}/{theme:?}");
                assert_eq!(a.fade_duration, b.fade_duration, "{os:?}/{theme:?}");
            }
        }
    }
}