Skip to main content

azul_core/
ua_css.rs

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
33use 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
77use crate::dom::NodeType;
78
79/// 100% width
80static WIDTH_100_PERCENT: CssProperty = CssProperty::Width(CssPropertyValue::Exact(
81    LayoutWidth::Px(PixelValue::const_percent(100)),
82));
83
84/// 100% height
85static HEIGHT_100_PERCENT: CssProperty = CssProperty::Height(CssPropertyValue::Exact(
86    LayoutHeight::Px(PixelValue::const_percent(100)),
87));
88
89/// display: block
90static DISPLAY_BLOCK: CssProperty =
91    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::Block));
92
93/// display: inline
94static DISPLAY_INLINE: CssProperty =
95    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::Inline));
96
97/// display: inline-block
98static DISPLAY_INLINE_BLOCK: CssProperty =
99    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::InlineBlock));
100
101/// display: none
102static DISPLAY_NONE: CssProperty =
103    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::None));
104
105/// display: table
106static DISPLAY_TABLE: CssProperty =
107    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::Table));
108
109/// display: table-row
110static DISPLAY_TABLE_ROW: CssProperty =
111    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableRow));
112
113/// display: table-cell
114static DISPLAY_TABLE_CELL: CssProperty =
115    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableCell));
116
117/// display: table-header-group
118static DISPLAY_TABLE_HEADER_GROUP: CssProperty =
119    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableHeaderGroup));
120
121/// display: table-row-group
122static DISPLAY_TABLE_ROW_GROUP: CssProperty =
123    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableRowGroup));
124
125/// display: table-footer-group
126static DISPLAY_TABLE_FOOTER_GROUP: CssProperty =
127    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableFooterGroup));
128
129/// display: table-caption
130static DISPLAY_TABLE_CAPTION: CssProperty =
131    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableCaption));
132
133/// display: table-column-group
134static DISPLAY_TABLE_COLUMN_GROUP: CssProperty =
135    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableColumnGroup));
136
137/// display: table-column
138static DISPLAY_TABLE_COLUMN: CssProperty =
139    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::TableColumn));
140
141/// display: list-item
142static DISPLAY_LIST_ITEM: CssProperty =
143    CssProperty::Display(CssPropertyValue::Exact(LayoutDisplay::ListItem));
144
145/// cursor: pointer (for clickable elements like buttons, links)
146static CURSOR_POINTER: CssProperty =
147    CssProperty::Cursor(CssPropertyValue::Exact(StyleCursor::Pointer));
148
149/// cursor: text (for selectable text elements)
150static CURSOR_TEXT: CssProperty =
151    CssProperty::Cursor(CssPropertyValue::Exact(StyleCursor::Text));
152
153/// margin-top: 0
154static MARGIN_TOP_ZERO: CssProperty =
155    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
156        inner: PixelValue::const_px(0),
157    }));
158
159/// margin-bottom: 0
160static MARGIN_BOTTOM_ZERO: CssProperty =
161    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
162        inner: PixelValue::const_px(0),
163    }));
164
165/// margin-left: 0
166static MARGIN_LEFT_ZERO: CssProperty =
167    CssProperty::MarginLeft(CssPropertyValue::Exact(LayoutMarginLeft {
168        inner: PixelValue::const_px(0),
169    }));
170
171/// margin-right: 0
172static MARGIN_RIGHT_ZERO: CssProperty =
173    CssProperty::MarginRight(CssPropertyValue::Exact(LayoutMarginRight {
174        inner: PixelValue::const_px(0),
175    }));
176
177// Chrome User-Agent Stylesheet: body { margin: 8px; }
178/// margin-top: 8px (Chrome UA default for body)
179static MARGIN_TOP_8PX: CssProperty =
180    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
181        inner: PixelValue::const_px(8),
182    }));
183
184/// margin-bottom: 8px (Chrome UA default for body)
185static MARGIN_BOTTOM_8PX: CssProperty =
186    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
187        inner: PixelValue::const_px(8),
188    }));
189
190/// margin-left: 8px (Chrome UA default for body)
191static MARGIN_LEFT_8PX: CssProperty =
192    CssProperty::MarginLeft(CssPropertyValue::Exact(LayoutMarginLeft {
193        inner: PixelValue::const_px(8),
194    }));
195
196/// margin-right: 8px (Chrome UA default for body)
197static MARGIN_RIGHT_8PX: CssProperty =
198    CssProperty::MarginRight(CssPropertyValue::Exact(LayoutMarginRight {
199        inner: PixelValue::const_px(8),
200    }));
201
202/// font-size: 2em (for H1)
203static FONT_SIZE_2EM: CssProperty = CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
204    inner: PixelValue::const_em(2),
205}));
206
207/// font-size: 1.5em (for H2)
208static FONT_SIZE_1_5EM: CssProperty =
209    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
210        inner: PixelValue::const_em_fractional(1, 5),
211    }));
212
213/// font-size: 1.17em (for H3)
214static FONT_SIZE_1_17EM: CssProperty =
215    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
216        inner: PixelValue::const_em_fractional(1, 17),
217    }));
218
219/// font-size: 1em (for H4)
220static FONT_SIZE_1EM: CssProperty = CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
221    inner: PixelValue::const_em(1),
222}));
223
224/// font-size: 0.83em (for H5)
225static FONT_SIZE_0_83EM: CssProperty =
226    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
227        inner: PixelValue::const_em_fractional(0, 83),
228    }));
229
230/// font-size: 0.67em (for H6)
231static FONT_SIZE_0_67EM: CssProperty =
232    CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
233        inner: PixelValue::const_em_fractional(0, 67),
234    }));
235
236/// margin-top: 1em (for P)
237static MARGIN_TOP_1EM: CssProperty =
238    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
239        inner: PixelValue::const_em(1),
240    }));
241
242/// margin-bottom: 1em (for P)
243static MARGIN_BOTTOM_1EM: CssProperty =
244    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
245        inner: PixelValue::const_em(1),
246    }));
247
248/// margin-top: 0.67em (for H1)
249static MARGIN_TOP_0_67EM: CssProperty =
250    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
251        inner: PixelValue::const_em_fractional(0, 67),
252    }));
253
254/// margin-bottom: 0.67em (for H1)
255static MARGIN_BOTTOM_0_67EM: CssProperty =
256    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
257        inner: PixelValue::const_em_fractional(0, 67),
258    }));
259
260/// margin-top: 0.83em (for H2)
261static MARGIN_TOP_0_83EM: CssProperty =
262    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
263        inner: PixelValue::const_em_fractional(0, 83),
264    }));
265
266/// margin-bottom: 0.83em (for H2)
267static MARGIN_BOTTOM_0_83EM: CssProperty =
268    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
269        inner: PixelValue::const_em_fractional(0, 83),
270    }));
271
272/// margin-top: 1.33em (for H4)
273static MARGIN_TOP_1_33EM: CssProperty =
274    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
275        inner: PixelValue::const_em_fractional(1, 33),
276    }));
277
278/// margin-bottom: 1.33em (for H4)
279static MARGIN_BOTTOM_1_33EM: CssProperty =
280    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
281        inner: PixelValue::const_em_fractional(1, 33),
282    }));
283
284/// margin-top: 1.67em (for H5)
285static MARGIN_TOP_1_67EM: CssProperty =
286    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
287        inner: PixelValue::const_em_fractional(1, 67),
288    }));
289
290/// margin-bottom: 1.67em (for H5)
291static MARGIN_BOTTOM_1_67EM: CssProperty =
292    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
293        inner: PixelValue::const_em_fractional(1, 67),
294    }));
295
296/// margin-top: 2.33em (for H6)
297static MARGIN_TOP_2_33EM: CssProperty =
298    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
299        inner: PixelValue::const_em_fractional(2, 33),
300    }));
301
302/// margin-bottom: 2.33em (for H6)
303static MARGIN_BOTTOM_2_33EM: CssProperty =
304    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
305        inner: PixelValue::const_em_fractional(2, 33),
306    }));
307
308/// font-weight: bold (for headings)
309static FONT_WEIGHT_BOLD: CssProperty =
310    CssProperty::FontWeight(CssPropertyValue::Exact(StyleFontWeight::Bold));
311
312/// font-weight: bolder
313static FONT_WEIGHT_BOLDER: CssProperty =
314    CssProperty::FontWeight(CssPropertyValue::Exact(StyleFontWeight::Bolder));
315
316// Table cell padding - Chrome UA CSS default: 1px
317static PADDING_TOP_1PX: CssProperty =
318    CssProperty::PaddingTop(CssPropertyValue::Exact(LayoutPaddingTop {
319        inner: PixelValue::const_px(1),
320    }));
321
322static PADDING_BOTTOM_1PX: CssProperty =
323    CssProperty::PaddingBottom(CssPropertyValue::Exact(LayoutPaddingBottom {
324        inner: PixelValue::const_px(1),
325    }));
326
327static PADDING_LEFT_1PX: CssProperty =
328    CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
329        inner: PixelValue::const_px(1),
330    }));
331
332static PADDING_RIGHT_1PX: CssProperty =
333    CssProperty::PaddingRight(CssPropertyValue::Exact(LayoutPaddingRight {
334        inner: PixelValue::const_px(1),
335    }));
336
337/// text-align: center (for th elements)
338static TEXT_ALIGN_CENTER: CssProperty =
339    CssProperty::TextAlign(CssPropertyValue::Exact(StyleTextAlign::Center));
340
341/// vertical-align: middle (for table elements)
342static VERTICAL_ALIGN_MIDDLE: CssProperty =
343    CssProperty::VerticalAlign(CssPropertyValue::Exact(StyleVerticalAlign::Middle));
344
345/// list-style-type: disc (default for <ul>)
346static LIST_STYLE_TYPE_DISC: CssProperty =
347    CssProperty::ListStyleType(CssPropertyValue::Exact(StyleListStyleType::Disc));
348
349/// list-style-type: decimal (default for <ol>)
350static LIST_STYLE_TYPE_DECIMAL: CssProperty =
351    CssProperty::ListStyleType(CssPropertyValue::Exact(StyleListStyleType::Decimal));
352
353// --- HR Element Defaults ---
354// Per HTML spec, <hr> renders as a horizontal line with inset border style
355
356/// margin-top: 0.5em (for hr)
357static MARGIN_TOP_0_5EM: CssProperty =
358    CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
359        inner: PixelValue::const_em_fractional(0, 5),
360    }));
361
362/// margin-bottom: 0.5em (for hr)
363static MARGIN_BOTTOM_0_5EM: CssProperty =
364    CssProperty::MarginBottom(CssPropertyValue::Exact(LayoutMarginBottom {
365        inner: PixelValue::const_em_fractional(0, 5),
366    }));
367
368/// border-top-style: inset (for hr - default browser style)
369static BORDER_TOP_STYLE_INSET: CssProperty =
370    CssProperty::BorderTopStyle(CssPropertyValue::Exact(StyleBorderTopStyle {
371        inner: BorderStyle::Inset,
372    }));
373
374/// border-top-width: 1px (for hr)
375static BORDER_TOP_WIDTH_1PX: CssProperty =
376    CssProperty::BorderTopWidth(CssPropertyValue::Exact(LayoutBorderTopWidth {
377        inner: PixelValue::const_px(1),
378    }));
379
380/// border-top-color: gray (for hr - default visible color)
381static BORDER_TOP_COLOR_GRAY: CssProperty =
382    CssProperty::BorderTopColor(CssPropertyValue::Exact(StyleBorderTopColor {
383        inner: ColorU {
384            r: 128,
385            g: 128,
386            b: 128,
387            a: 255,
388        },
389    }));
390
391/// height: 0 (for hr - the line comes from the border, not height)
392static HEIGHT_ZERO: CssProperty = CssProperty::Height(CssPropertyValue::Exact(LayoutHeight::Px(
393    PixelValue::const_px(0),
394)));
395
396/// counter-reset: list-item 0 (default for <ul>, <ol>)
397/// Per CSS Lists Module Level 3, list containers automatically reset the list-item counter
398static COUNTER_RESET_LIST_ITEM: CssProperty =
399    CssProperty::CounterReset(CssPropertyValue::Exact(CounterReset::list_item()));
400
401// CSS Fragmentation (Page Breaking) Properties
402//
403// Per CSS Fragmentation Level 3 and paged media best practices,
404// certain elements should avoid page breaks inside them
405
406/// break-inside: avoid
407/// Used for elements that should not be split across page boundaries
408/// Applied to: h1-h6, table, thead, tbody, tfoot, figure, figcaption
409static BREAK_INSIDE_AVOID: CssProperty = CssProperty::break_inside(BreakInside::Avoid);
410
411/// break-after: avoid
412/// Avoids a page break after the element (useful for headings)
413static BREAK_AFTER_AVOID: CssProperty = CssProperty::break_after(PageBreak::Avoid);
414
415/// padding-inline-start: 40px (default for <li>)
416///
417/// Creates space for list markers in the inline-start direction (left in LTR, right in RTL)
418/// padding-inline-start: 40px for list items per CSS Lists Module Level 3
419/// Applied to <li> items to create gutter space for `::marker` pseudo-elements
420///
421/// NOTE: This should be on the list items, not the container, because:
422///
423/// 1. `::marker` pseudo-elements are children of <li>, not <ul>/<ol>
424/// 2. The marker needs to be positioned relative to the list item's content box
425/// 3. Padding on <li> creates space between the marker and the text content
426///    TODO: Change to `PaddingInlineStart` once logical property resolution is implemented
427static PADDING_INLINE_START_40PX: CssProperty =
428    CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
429        inner: PixelValue::const_px(40),
430    }));
431
432/// Text decoration: underline - used for <a> and <u> elements
433static TEXT_DECORATION_UNDERLINE: CssProperty = CssProperty::TextDecoration(
434    CssPropertyValue::Exact(StyleTextDecoration::Underline),
435);
436
437// --- Button Element Defaults ---
438// Per browser UA CSS, <button> has padding, border, and a system font size.
439// These ensure a button is visible even without author CSS.
440
441/// font-size: 13px (standard button font size on macOS/Linux)
442static FONT_SIZE_13PX: CssProperty = CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
443    inner: PixelValue::const_px(13),
444}));
445
446/// padding-top: 5px (button)
447static PADDING_TOP_5PX: CssProperty =
448    CssProperty::PaddingTop(CssPropertyValue::Exact(LayoutPaddingTop {
449        inner: PixelValue::const_px(5),
450    }));
451
452/// padding-bottom: 5px (button)
453static PADDING_BOTTOM_5PX: CssProperty =
454    CssProperty::PaddingBottom(CssPropertyValue::Exact(LayoutPaddingBottom {
455        inner: PixelValue::const_px(5),
456    }));
457
458/// padding-left: 10px (button)
459static PADDING_LEFT_10PX: CssProperty =
460    CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
461        inner: PixelValue::const_px(10),
462    }));
463
464/// padding-right: 10px (button)
465static PADDING_RIGHT_10PX: CssProperty =
466    CssProperty::PaddingRight(CssPropertyValue::Exact(LayoutPaddingRight {
467        inner: PixelValue::const_px(10),
468    }));
469
470/// Border color for button: #c8c8c8 (light gray)
471static BUTTON_BORDER_COLOR: ColorU = ColorU { r: 200, g: 200, b: 200, a: 255 };
472
473static BUTTON_BORDER_TOP_COLOR: CssProperty =
474    CssProperty::BorderTopColor(CssPropertyValue::Exact(StyleBorderTopColor {
475        inner: BUTTON_BORDER_COLOR,
476    }));
477static BUTTON_BORDER_BOTTOM_COLOR: CssProperty =
478    CssProperty::BorderBottomColor(CssPropertyValue::Exact(StyleBorderBottomColor {
479        inner: BUTTON_BORDER_COLOR,
480    }));
481static BUTTON_BORDER_LEFT_COLOR: CssProperty =
482    CssProperty::BorderLeftColor(CssPropertyValue::Exact(StyleBorderLeftColor {
483        inner: BUTTON_BORDER_COLOR,
484    }));
485static BUTTON_BORDER_RIGHT_COLOR: CssProperty =
486    CssProperty::BorderRightColor(CssPropertyValue::Exact(StyleBorderRightColor {
487        inner: BUTTON_BORDER_COLOR,
488    }));
489
490static BUTTON_BORDER_TOP_STYLE: CssProperty =
491    CssProperty::BorderTopStyle(CssPropertyValue::Exact(StyleBorderTopStyle {
492        inner: BorderStyle::Solid,
493    }));
494static BUTTON_BORDER_BOTTOM_STYLE: CssProperty =
495    CssProperty::BorderBottomStyle(CssPropertyValue::Exact(StyleBorderBottomStyle {
496        inner: BorderStyle::Solid,
497    }));
498static BUTTON_BORDER_LEFT_STYLE: CssProperty =
499    CssProperty::BorderLeftStyle(CssPropertyValue::Exact(StyleBorderLeftStyle {
500        inner: BorderStyle::Solid,
501    }));
502static BUTTON_BORDER_RIGHT_STYLE: CssProperty =
503    CssProperty::BorderRightStyle(CssPropertyValue::Exact(StyleBorderRightStyle {
504        inner: BorderStyle::Solid,
505    }));
506
507static BUTTON_BORDER_TOP_WIDTH: CssProperty =
508    CssProperty::BorderTopWidth(CssPropertyValue::Exact(LayoutBorderTopWidth {
509        inner: PixelValue::const_px(1),
510    }));
511static BUTTON_BORDER_BOTTOM_WIDTH: CssProperty =
512    CssProperty::BorderBottomWidth(CssPropertyValue::Exact(LayoutBorderBottomWidth {
513        inner: PixelValue::const_px(1),
514    }));
515static BUTTON_BORDER_LEFT_WIDTH: CssProperty =
516    CssProperty::BorderLeftWidth(CssPropertyValue::Exact(LayoutBorderLeftWidth {
517        inner: PixelValue::const_px(1),
518    }));
519static BUTTON_BORDER_RIGHT_WIDTH: CssProperty =
520    CssProperty::BorderRightWidth(CssPropertyValue::Exact(LayoutBorderRightWidth {
521        inner: PixelValue::const_px(1),
522    }));
523
524/// Returns the default user-agent CSS property value for a given node type and property.
525///
526/// This function provides the baseline styling that should be applied before any author
527/// styles. It ensures that elements have sensible defaults that prevent layout issues.
528///
529/// # Arguments
530///
531/// * `node_type` - The type of DOM node (e.g., `Body`, `H1`, `Div`)
532/// * `property_type` - The specific CSS property to query (e.g., `Width`, `Display`)
533///
534/// # Returns
535///
536/// `Some(CssProperty)` if a default value is defined for this combination, otherwise `None`.
537// Exhaustive (node-type, property-type) → default-value lookup table: many
538// element types share a default (e.g. all block elements → DISPLAY_BLOCK). One
539// arm per (NT, PT) case is intentional for readability; merging into giant
540// or-patterns would collapse the UA stylesheet table.
541#[allow(clippy::match_same_arms)]
542#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
543#[must_use] pub fn get_ua_property(
544    node_type: &NodeType,
545    property_type: CssPropertyType,
546) -> Option<&'static CssProperty> {
547    use CssPropertyType as PT;
548    use NodeType as NT;
549
550    
551
552    match (node_type, property_type) {
553        // Body Element - CRITICAL for preventing layout collapse
554        (NT::Body, PT::Display) => Some(&DISPLAY_BLOCK),
555        // NOTE: Body does NOT have width: 100% in standard UA CSS - it inherits from ICB
556        // (NT::Body, PT::Height) => Some(&HEIGHT_100_PERCENT),
557        (NT::Body, PT::MarginTop) => Some(&MARGIN_TOP_8PX),
558        (NT::Body, PT::MarginBottom) => Some(&MARGIN_BOTTOM_8PX),
559        (NT::Body, PT::MarginLeft) => Some(&MARGIN_LEFT_8PX),
560        (NT::Body, PT::MarginRight) => Some(&MARGIN_RIGHT_8PX),
561
562        // Block-level Elements
563        // NOTE: Do NOT set width: 100% here! Block elements have width: auto by default
564        // in CSS spec. width: auto for blocks means "fill available width" but it's NOT
565        // the same as width: 100%. The difference is critical for flexbox: width: auto
566        // allows flex-grow/flex-shrink to control sizing, while width: 100% prevents it.
567        (NT::Div, PT::Display) => Some(&DISPLAY_BLOCK),
568        (NT::P, PT::Display) => Some(&DISPLAY_BLOCK),
569        // REMOVED - blocks have width: auto by default
570        // (NT::Div, PT::Width) => Some(&WIDTH_100_PERCENT),
571        // REMOVED - blocks have width: auto by default
572        // (NT::P, PT::Width) => Some(&WIDTH_100_PERCENT),
573        (NT::P, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
574        (NT::P, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
575        (NT::Main, PT::Display) => Some(&DISPLAY_BLOCK),
576        (NT::Header, PT::Display) => Some(&DISPLAY_BLOCK),
577        (NT::Footer, PT::Display) => Some(&DISPLAY_BLOCK),
578        (NT::Section, PT::Display) => Some(&DISPLAY_BLOCK),
579        (NT::Article, PT::Display) => Some(&DISPLAY_BLOCK),
580        (NT::Aside, PT::Display) => Some(&DISPLAY_BLOCK),
581        (NT::Nav, PT::Display) => Some(&DISPLAY_BLOCK),
582
583        // Headings - Chrome UA CSS values
584        // Per CSS Fragmentation Level 3: headings should avoid page breaks inside
585        // and after them (to keep heading with following content)
586        (NT::H1, PT::Display) => Some(&DISPLAY_BLOCK),
587        (NT::H1, PT::FontSize) => Some(&FONT_SIZE_2EM),
588        (NT::H1, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
589        (NT::H1, PT::MarginTop) => Some(&MARGIN_TOP_0_67EM),
590        (NT::H1, PT::MarginBottom) => Some(&MARGIN_BOTTOM_0_67EM),
591        (NT::H1, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
592        (NT::H1, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
593
594        (NT::H2, PT::Display) => Some(&DISPLAY_BLOCK),
595        (NT::H2, PT::FontSize) => Some(&FONT_SIZE_1_5EM),
596        (NT::H2, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
597        (NT::H2, PT::MarginTop) => Some(&MARGIN_TOP_0_83EM),
598        (NT::H2, PT::MarginBottom) => Some(&MARGIN_BOTTOM_0_83EM),
599        (NT::H2, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
600        (NT::H2, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
601
602        (NT::H3, PT::Display) => Some(&DISPLAY_BLOCK),
603        (NT::H3, PT::FontSize) => Some(&FONT_SIZE_1_17EM),
604        (NT::H3, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
605        (NT::H3, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
606        (NT::H3, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
607        (NT::H3, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
608        (NT::H3, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
609
610        (NT::H4, PT::Display) => Some(&DISPLAY_BLOCK),
611        (NT::H4, PT::FontSize) => Some(&FONT_SIZE_1EM),
612        (NT::H4, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
613        (NT::H4, PT::MarginTop) => Some(&MARGIN_TOP_1_33EM),
614        (NT::H4, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1_33EM),
615        (NT::H4, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
616        (NT::H4, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
617
618        (NT::H5, PT::Display) => Some(&DISPLAY_BLOCK),
619        (NT::H5, PT::FontSize) => Some(&FONT_SIZE_0_83EM),
620        (NT::H5, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
621        (NT::H5, PT::MarginTop) => Some(&MARGIN_TOP_1_67EM),
622        (NT::H5, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1_67EM),
623        (NT::H5, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
624        (NT::H5, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
625
626        (NT::H6, PT::Display) => Some(&DISPLAY_BLOCK),
627        (NT::H6, PT::FontSize) => Some(&FONT_SIZE_0_67EM),
628        (NT::H6, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
629        (NT::H6, PT::MarginTop) => Some(&MARGIN_TOP_2_33EM),
630        (NT::H6, PT::MarginBottom) => Some(&MARGIN_BOTTOM_2_33EM),
631        (NT::H6, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
632        (NT::H6, PT::BreakAfter) => Some(&BREAK_AFTER_AVOID),
633
634        // Lists - padding on container creates gutter for markers
635        (NT::Ul, PT::Display) => Some(&DISPLAY_BLOCK),
636        (NT::Ul, PT::ListStyleType) => Some(&LIST_STYLE_TYPE_DISC),
637        (NT::Ul, PT::CounterReset) => Some(&COUNTER_RESET_LIST_ITEM),
638        (NT::Ul, PT::PaddingLeft) => Some(&PADDING_INLINE_START_40PX),
639        (NT::Ul, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
640        (NT::Ul, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
641        (NT::Ol, PT::Display) => Some(&DISPLAY_BLOCK),
642        (NT::Ol, PT::ListStyleType) => Some(&LIST_STYLE_TYPE_DECIMAL),
643        (NT::Ol, PT::CounterReset) => Some(&COUNTER_RESET_LIST_ITEM),
644        (NT::Ol, PT::PaddingLeft) => Some(&PADDING_INLINE_START_40PX),
645        (NT::Ol, PT::MarginTop) => Some(&MARGIN_TOP_1EM),
646        (NT::Ol, PT::MarginBottom) => Some(&MARGIN_BOTTOM_1EM),
647        (NT::Li, PT::Display) => Some(&DISPLAY_LIST_ITEM),
648        (NT::Dl, PT::Display) => Some(&DISPLAY_BLOCK),
649        (NT::Dt, PT::Display) => Some(&DISPLAY_BLOCK),
650        (NT::Dd, PT::Display) => Some(&DISPLAY_BLOCK),
651
652        // Inline Elements
653        (NT::Span, PT::Display) => Some(&DISPLAY_INLINE),
654        (NT::A, PT::Display) => Some(&DISPLAY_INLINE),
655        (NT::A, PT::TextDecoration) => Some(&TEXT_DECORATION_UNDERLINE),
656        (NT::Strong, PT::Display) => Some(&DISPLAY_INLINE),
657        (NT::Strong, PT::FontWeight) => Some(&FONT_WEIGHT_BOLDER),
658        (NT::Em, PT::Display) => Some(&DISPLAY_INLINE),
659        (NT::B, PT::Display) => Some(&DISPLAY_INLINE),
660        (NT::B, PT::FontWeight) => Some(&FONT_WEIGHT_BOLDER),
661        (NT::I, PT::Display) => Some(&DISPLAY_INLINE),
662        (NT::U, PT::Display) => Some(&DISPLAY_INLINE),
663        (NT::U, PT::TextDecoration) => Some(&TEXT_DECORATION_UNDERLINE),
664        (NT::Small, PT::Display) => Some(&DISPLAY_INLINE),
665        (NT::Code, PT::Display) => Some(&DISPLAY_INLINE),
666        (NT::Kbd, PT::Display) => Some(&DISPLAY_INLINE),
667        (NT::Samp, PT::Display) => Some(&DISPLAY_INLINE),
668        (NT::Sub, PT::Display) => Some(&DISPLAY_INLINE),
669        (NT::Sup, PT::Display) => Some(&DISPLAY_INLINE),
670
671        // Text Content
672        (NT::Pre, PT::Display) => Some(&DISPLAY_BLOCK),
673        (NT::BlockQuote, PT::Display) => Some(&DISPLAY_BLOCK),
674        (NT::Hr, PT::Display) => Some(&DISPLAY_BLOCK),
675        (NT::Hr, PT::Width) => Some(&WIDTH_100_PERCENT),
676        (NT::Hr, PT::Height) => Some(&HEIGHT_ZERO),
677        (NT::Hr, PT::MarginTop) => Some(&MARGIN_TOP_0_5EM),
678        (NT::Hr, PT::MarginBottom) => Some(&MARGIN_BOTTOM_0_5EM),
679        (NT::Hr, PT::BorderTopStyle) => Some(&BORDER_TOP_STYLE_INSET),
680        (NT::Hr, PT::BorderTopWidth) => Some(&BORDER_TOP_WIDTH_1PX),
681        (NT::Hr, PT::BorderTopColor) => Some(&BORDER_TOP_COLOR_GRAY),
682
683        // Table Elements
684        // Per CSS Fragmentation Level 3: table ROWS should avoid breaks inside
685        // Tables themselves should NOT have break-inside: avoid (they can span pages)
686        (NT::Table, PT::Display) => Some(&DISPLAY_TABLE),
687        // NOTE: Removed break-inside: avoid from Table - tables CAN break across pages
688        (NT::THead, PT::Display) => Some(&DISPLAY_TABLE_HEADER_GROUP),
689        (NT::THead, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
690        (NT::THead, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
691        (NT::TBody, PT::Display) => Some(&DISPLAY_TABLE_ROW_GROUP),
692        (NT::TBody, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
693        // NOTE: Removed break-inside: avoid from TBody - tbody CAN break across pages
694        (NT::TFoot, PT::Display) => Some(&DISPLAY_TABLE_FOOTER_GROUP),
695        (NT::TFoot, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
696        (NT::TFoot, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
697        (NT::Tr, PT::Display) => Some(&DISPLAY_TABLE_ROW),
698        (NT::Tr, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
699        (NT::Tr, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
700        (NT::Th, PT::Display) => Some(&DISPLAY_TABLE_CELL),
701        (NT::Th, PT::TextAlign) => Some(&TEXT_ALIGN_CENTER),
702        (NT::Th, PT::FontWeight) => Some(&FONT_WEIGHT_BOLD),
703        (NT::Th, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
704        (NT::Th, PT::PaddingTop) => Some(&PADDING_TOP_1PX),
705        (NT::Th, PT::PaddingBottom) => Some(&PADDING_BOTTOM_1PX),
706        (NT::Th, PT::PaddingLeft) => Some(&PADDING_LEFT_1PX),
707        (NT::Th, PT::PaddingRight) => Some(&PADDING_RIGHT_1PX),
708        (NT::Td, PT::Display) => Some(&DISPLAY_TABLE_CELL),
709        (NT::Td, PT::VerticalAlign) => Some(&VERTICAL_ALIGN_MIDDLE),
710        (NT::Td, PT::PaddingTop) => Some(&PADDING_TOP_1PX),
711        (NT::Td, PT::PaddingBottom) => Some(&PADDING_BOTTOM_1PX),
712        (NT::Td, PT::PaddingLeft) => Some(&PADDING_LEFT_1PX),
713        (NT::Td, PT::PaddingRight) => Some(&PADDING_RIGHT_1PX),
714
715        // Form Elements
716        (NT::Form, PT::Display) => Some(&DISPLAY_BLOCK),
717        (NT::Input, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
718        (NT::Button, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
719        (NT::Button, PT::Cursor) => Some(&CURSOR_POINTER),
720        (NT::Button, PT::FontSize) => Some(&FONT_SIZE_13PX),
721        (NT::Button, PT::PaddingTop) => Some(&PADDING_TOP_5PX),
722        (NT::Button, PT::PaddingBottom) => Some(&PADDING_BOTTOM_5PX),
723        (NT::Button, PT::PaddingLeft) => Some(&PADDING_LEFT_10PX),
724        (NT::Button, PT::PaddingRight) => Some(&PADDING_RIGHT_10PX),
725        (NT::Button, PT::BorderTopWidth) => Some(&BUTTON_BORDER_TOP_WIDTH),
726        (NT::Button, PT::BorderBottomWidth) => Some(&BUTTON_BORDER_BOTTOM_WIDTH),
727        (NT::Button, PT::BorderLeftWidth) => Some(&BUTTON_BORDER_LEFT_WIDTH),
728        (NT::Button, PT::BorderRightWidth) => Some(&BUTTON_BORDER_RIGHT_WIDTH),
729        (NT::Button, PT::BorderTopStyle) => Some(&BUTTON_BORDER_TOP_STYLE),
730        (NT::Button, PT::BorderBottomStyle) => Some(&BUTTON_BORDER_BOTTOM_STYLE),
731        (NT::Button, PT::BorderLeftStyle) => Some(&BUTTON_BORDER_LEFT_STYLE),
732        (NT::Button, PT::BorderRightStyle) => Some(&BUTTON_BORDER_RIGHT_STYLE),
733        (NT::Button, PT::BorderTopColor) => Some(&BUTTON_BORDER_TOP_COLOR),
734        (NT::Button, PT::BorderBottomColor) => Some(&BUTTON_BORDER_BOTTOM_COLOR),
735        (NT::Button, PT::BorderLeftColor) => Some(&BUTTON_BORDER_LEFT_COLOR),
736        (NT::Button, PT::BorderRightColor) => Some(&BUTTON_BORDER_RIGHT_COLOR),
737        // Text nodes get I-beam cursor for text selection
738        // The cursor resolution algorithm ensures that explicit cursor properties
739        // on parent elements (e.g., cursor:pointer on button) take precedence
740        (NT::Text(_), PT::Cursor) => Some(&CURSOR_TEXT),
741        (NT::Select, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
742        (NT::TextArea, PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
743        // TextArea gets I-beam cursor since it's an editable text field
744        (NT::TextArea, PT::Cursor) => Some(&CURSOR_TEXT),
745        (NT::Label, PT::Display) => Some(&DISPLAY_INLINE),
746        // Hidden Elements
747        (NT::Head, PT::Display) => Some(&DISPLAY_NONE),
748        (NT::Title, PT::Display) => Some(&DISPLAY_NONE),
749        (NT::Script, PT::Display) => Some(&DISPLAY_NONE),
750        (NT::Style, PT::Display) => Some(&DISPLAY_NONE),
751        (NT::Link, PT::Display) => Some(&DISPLAY_NONE),
752
753        // Special Elements
754        // <br> is an inline-level element that forces a line break WITHIN the
755        // inline formatting context (HTML §4.5.28). Giving it `display: block`
756        // made `<p>text<br>more</p>` split into three stacked block boxes (an
757        // extra empty <br> box between two anonymous paragraphs), over-advancing
758        // vertically and, inside a table cell, dropping the line after the break.
759        // As inline it is turned into a hard `LineBreak` by the IFC collectors.
760        (NT::Br, PT::Display) => Some(&DISPLAY_INLINE),
761        // Images are replaced elements - inline-block so they respect width/height
762        (NT::Image(_), PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
763
764        // Media Elements
765        (NT::Video, PT::Display) => Some(&DISPLAY_INLINE),
766        (NT::Audio, PT::Display) => Some(&DISPLAY_INLINE),
767        (NT::Canvas, PT::Display) => Some(&DISPLAY_INLINE),
768        (NT::Svg, PT::Display) => Some(&DISPLAY_INLINE),
769        // VirtualView is a block-level replaced element (like div) — must be block
770        // so it participates in flex layout (flex-grow, etc.)
771        (NT::VirtualView, PT::Display) => Some(&DISPLAY_BLOCK),
772
773        // Icon Elements - inline-block so they have width/height but flow inline
774        (NT::Icon(_), PT::Display) => Some(&DISPLAY_INLINE_BLOCK),
775
776        (NT::SelectOption, PT::Display) => Some(&DISPLAY_NONE),
777        (NT::OptGroup, PT::Display) => Some(&DISPLAY_NONE),
778
779        // Other Inline Elements
780        (NT::Abbr, PT::Display) => Some(&DISPLAY_INLINE),
781        (NT::Cite, PT::Display) => Some(&DISPLAY_INLINE),
782        (NT::Del, PT::Display) => Some(&DISPLAY_INLINE),
783        (NT::Ins, PT::Display) => Some(&DISPLAY_INLINE),
784        (NT::Mark, PT::Display) => Some(&DISPLAY_INLINE),
785        (NT::Q, PT::Display) => Some(&DISPLAY_INLINE),
786        (NT::Dfn, PT::Display) => Some(&DISPLAY_INLINE),
787        (NT::Var, PT::Display) => Some(&DISPLAY_INLINE),
788        (NT::Time, PT::Display) => Some(&DISPLAY_INLINE),
789        (NT::Data, PT::Display) => Some(&DISPLAY_INLINE),
790        (NT::Wbr, PT::Display) => Some(&DISPLAY_INLINE),
791        (NT::Bdi, PT::Display) => Some(&DISPLAY_INLINE),
792        (NT::Bdo, PT::Display) => Some(&DISPLAY_INLINE),
793        (NT::Rp, PT::Display) => Some(&DISPLAY_INLINE),
794        (NT::Rt, PT::Display) => Some(&DISPLAY_INLINE),
795        (NT::Rtc, PT::Display) => Some(&DISPLAY_INLINE),
796        (NT::Ruby, PT::Display) => Some(&DISPLAY_INLINE),
797
798        // Block Container Elements
799        // Per CSS Fragmentation Level 3: figures should avoid page breaks inside
800        (NT::FieldSet, PT::Display) => Some(&DISPLAY_BLOCK),
801        (NT::Figure, PT::Display) => Some(&DISPLAY_BLOCK),
802        (NT::Figure, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
803        (NT::FigCaption, PT::Display) => Some(&DISPLAY_BLOCK),
804        (NT::FigCaption, PT::BreakInside) => Some(&BREAK_INSIDE_AVOID),
805        (NT::Details, PT::Display) => Some(&DISPLAY_BLOCK),
806        (NT::Summary, PT::Display) => Some(&DISPLAY_BLOCK),
807        (NT::Dialog, PT::Display) => Some(&DISPLAY_BLOCK),
808
809        // Table Caption
810        (NT::Caption, PT::Display) => Some(&DISPLAY_TABLE_CAPTION),
811        (NT::ColGroup, PT::Display) => Some(&DISPLAY_TABLE_COLUMN_GROUP),
812        (NT::Col, PT::Display) => Some(&DISPLAY_TABLE_COLUMN),
813
814        // Legacy/Deprecated Elements
815        (NT::Menu, PT::Display) => Some(&DISPLAY_BLOCK),
816        (NT::Dir, PT::Display) => Some(&DISPLAY_BLOCK),
817
818        // Html (root) Element
819        //
820        // In browsers, the viewport itself provides scrolling when <html> overflows.
821        // Since Azul has no separate viewport scroll mechanism, we set `height: 100%`
822        // on the <html> element so it fills the Initial Containing Block (the viewport).
823        // This constrains child elements like <body> to the viewport height, enabling
824        // overflow:scroll on <body> to create scrollable content areas.
825        //
826        // Without this, <html> has height:auto and grows to fit all content,
827        // making container_size == content_size, which results in a useless 100% scrollbar.
828        (NT::Html, PT::Display) => Some(&DISPLAY_BLOCK),
829        // ⚠ DIAG (2026-06-02, REVERT): the lifted get_ua_property jump table mis-dispatches
830        // (Text/Button, Height) → THIS (Html, Height) arm → children wrongly get height:100%
831        // → fill parent (600) instead of content. Commenting it out tests whether removing the
832        // ONLY HEIGHT_100_PERCENT producer makes the children auto-height (confirms the chain).
833        // REAL fix = the node_type jump-table dispatch/table-mirror in the lift, not this.
834        // (NT::Html, PT::Height) => Some(&HEIGHT_100_PERCENT),
835
836        // Universal fallback for display property
837        // Per CSS spec, unknown/custom elements should default to inline
838        // Text nodes will be filtered out before this function is called
839        (_, PT::Display) => Some(&DISPLAY_INLINE),
840
841        // No default defined for other combinations
842        _ => None,
843    }
844}
845
846// ============================================================================
847// UA Scrollbar Defaults — individual CssPropertyWithConditions
848// ============================================================================
849//
850// These rules define the default scrollbar appearance per OS and theme,
851// using the same `@os` / `@theme` condition system as author CSS.
852// Each entry is a single CSS property (scrollbar-color or scrollbar-width)
853// with its conditions.  Rules are evaluated first-match-wins per property type.
854//
855// Conceptually equivalent to:
856//
857//   @os macos                { scrollbar-width: thin; }
858//   @os ios                  { scrollbar-width: thin; }
859//   @os android              { scrollbar-width: thin; }
860//   /* default */            { scrollbar-width: auto; }
861//
862//   @os macos                { -azul-scrollbar-visibility: when-scrolling; }
863//   @os ios                  { -azul-scrollbar-visibility: when-scrolling; }
864//   @os android              { -azul-scrollbar-visibility: when-scrolling; }
865//   /* default */            { -azul-scrollbar-visibility: always; }
866//
867//   @os macos                { -azul-scrollbar-fade-delay: 500ms; }
868//   @os ios                  { -azul-scrollbar-fade-delay: 500ms; }
869//   @os android              { -azul-scrollbar-fade-delay: 300ms; }
870//   /* default */            { -azul-scrollbar-fade-delay: 0; }
871//
872//   @os macos                { -azul-scrollbar-fade-duration: 200ms; }
873//   @os ios                  { -azul-scrollbar-fade-duration: 200ms; }
874//   @os android              { -azul-scrollbar-fade-duration: 150ms; }
875//   /* default */            { -azul-scrollbar-fade-duration: 0; }
876//
877//   @os macos @theme dark    { scrollbar-color: rgba(180,180,180,0.78) rgba(40,40,40,0.31); }
878//   @os macos @theme light   { scrollbar-color: rgba(80,80,80,0.78) rgba(200,200,200,0.31); }
879//   @os windows @theme dark  { scrollbar-color: #6e6e6e #202020; }
880//   @os windows @theme light { scrollbar-color: #828282 #f1f1f1; }
881//   @os ios @theme dark      { scrollbar-color: rgba(255,255,255,0.4) transparent; }
882//   @os ios @theme light     { scrollbar-color: rgba(0,0,0,0.4) transparent; }
883//   @os android @theme dark  { scrollbar-color: rgba(255,255,255,0.3) transparent; }
884//   @os android @theme light { scrollbar-color: rgba(0,0,0,0.3) transparent; }
885//   @theme dark              { scrollbar-color: #646464 #2d2d2d; }
886//   /* default */            { scrollbar-color: #c1c1c1 #f1f1f1; }
887
888/// Helper to create a const `scrollbar-color` `CssProperty`.
889const fn scrollbar_color(thumb: ColorU, track: ColorU) -> CssProperty {
890    CssProperty::ScrollbarColor(CssPropertyValue::Exact(
891        StyleScrollbarColor::Custom(ScrollbarColorCustom { thumb, track }),
892    ))
893}
894
895/// Helper to create a const `scrollbar-width` `CssProperty`.
896const fn scrollbar_width(w: LayoutScrollbarWidth) -> CssProperty {
897    CssProperty::ScrollbarWidth(CssPropertyValue::Exact(w))
898}
899
900/// Helper to create a const `-azul-scrollbar-visibility` `CssProperty`.
901const fn scrollbar_visibility(v: ScrollbarVisibilityMode) -> CssProperty {
902    CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(v))
903}
904
905/// Helper to create a const `-azul-scrollbar-fade-delay` `CssProperty`.
906const fn scrollbar_fade_delay(ms: u32) -> CssProperty {
907    CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(ScrollbarFadeDelay::new(ms)))
908}
909
910/// Helper to create a const `-azul-scrollbar-fade-duration` `CssProperty`.
911const fn scrollbar_fade_duration(ms: u32) -> CssProperty {
912    CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(ScrollbarFadeDuration::new(ms)))
913}
914
915/// UA scrollbar CSS properties with `@os` / `@theme` conditions.
916///
917/// Ordered most-specific first.  The evaluation function picks the
918/// first matching entry for each property type (`scrollbar-color`,
919/// `scrollbar-width`, `-azul-scrollbar-visibility`,
920/// `-azul-scrollbar-fade-delay`, `-azul-scrollbar-fade-duration`).
921pub(crate) static UA_SCROLLBAR_CSS: &[CssPropertyWithConditions] = &[
922    // ── scrollbar-width per OS ──────────────────────────────────────────
923    // macOS → thin (overlay)
924    CssPropertyWithConditions::with_single_condition(
925        scrollbar_width(LayoutScrollbarWidth::Thin),
926        &[DynamicSelector::Os(OsCondition::MacOS)],
927    ),
928    // iOS → thin
929    CssPropertyWithConditions::with_single_condition(
930        scrollbar_width(LayoutScrollbarWidth::Thin),
931        &[DynamicSelector::Os(OsCondition::IOS)],
932    ),
933    // Android → thin
934    CssPropertyWithConditions::with_single_condition(
935        scrollbar_width(LayoutScrollbarWidth::Thin),
936        &[DynamicSelector::Os(OsCondition::Android)],
937    ),
938    // default → auto (classic)
939    CssPropertyWithConditions::simple(
940        scrollbar_width(LayoutScrollbarWidth::Auto),
941    ),
942
943    // ── scrollbar-visibility per OS ─────────────────────────────────────
944    // macOS → overlay (show only when scrolling)
945    CssPropertyWithConditions::with_single_condition(
946        scrollbar_visibility(ScrollbarVisibilityMode::WhenScrolling),
947        &[DynamicSelector::Os(OsCondition::MacOS)],
948    ),
949    // iOS → overlay
950    CssPropertyWithConditions::with_single_condition(
951        scrollbar_visibility(ScrollbarVisibilityMode::WhenScrolling),
952        &[DynamicSelector::Os(OsCondition::IOS)],
953    ),
954    // Android → overlay
955    CssPropertyWithConditions::with_single_condition(
956        scrollbar_visibility(ScrollbarVisibilityMode::WhenScrolling),
957        &[DynamicSelector::Os(OsCondition::Android)],
958    ),
959    // default → always visible (classic)
960    CssPropertyWithConditions::simple(
961        scrollbar_visibility(ScrollbarVisibilityMode::Always),
962    ),
963
964    // ── scrollbar-fade-delay per OS ─────────────────────────────────────
965    CssPropertyWithConditions::with_single_condition(
966        scrollbar_fade_delay(500),
967        &[DynamicSelector::Os(OsCondition::MacOS)],
968    ),
969    CssPropertyWithConditions::with_single_condition(
970        scrollbar_fade_delay(500),
971        &[DynamicSelector::Os(OsCondition::IOS)],
972    ),
973    CssPropertyWithConditions::with_single_condition(
974        scrollbar_fade_delay(300),
975        &[DynamicSelector::Os(OsCondition::Android)],
976    ),
977    // default → 0 (no fade)
978    CssPropertyWithConditions::simple(
979        scrollbar_fade_delay(0),
980    ),
981
982    // ── scrollbar-fade-duration per OS ──────────────────────────────────
983    CssPropertyWithConditions::with_single_condition(
984        scrollbar_fade_duration(200),
985        &[DynamicSelector::Os(OsCondition::MacOS)],
986    ),
987    CssPropertyWithConditions::with_single_condition(
988        scrollbar_fade_duration(200),
989        &[DynamicSelector::Os(OsCondition::IOS)],
990    ),
991    CssPropertyWithConditions::with_single_condition(
992        scrollbar_fade_duration(150),
993        &[DynamicSelector::Os(OsCondition::Android)],
994    ),
995    // default → 0 (instant)
996    CssPropertyWithConditions::simple(
997        scrollbar_fade_duration(0),
998    ),
999
1000    // ── scrollbar-color per OS + theme ──────────────────────────────────
1001    // macOS dark: light grey thumb on dark semi-transparent track
1002    CssPropertyWithConditions::with_single_condition(
1003        scrollbar_color(
1004            ColorU { r: 180, g: 180, b: 180, a: 200 },
1005            ColorU { r: 40, g: 40, b: 40, a: 80 },
1006        ),
1007        &[DynamicSelector::Os(OsCondition::MacOS), DynamicSelector::Theme(ThemeCondition::Dark)],
1008    ),
1009    // macOS light: dark grey thumb on light semi-transparent track
1010    CssPropertyWithConditions::with_single_condition(
1011        scrollbar_color(
1012            ColorU { r: 80, g: 80, b: 80, a: 200 },
1013            ColorU { r: 200, g: 200, b: 200, a: 80 },
1014        ),
1015        &[DynamicSelector::Os(OsCondition::MacOS), DynamicSelector::Theme(ThemeCondition::Light)],
1016    ),
1017    // Windows dark
1018    CssPropertyWithConditions::with_single_condition(
1019        scrollbar_color(
1020            ColorU { r: 110, g: 110, b: 110, a: 255 },
1021            ColorU { r: 32, g: 32, b: 32, a: 255 },
1022        ),
1023        &[DynamicSelector::Os(OsCondition::Windows), DynamicSelector::Theme(ThemeCondition::Dark)],
1024    ),
1025    // Windows light
1026    CssPropertyWithConditions::with_single_condition(
1027        scrollbar_color(
1028            ColorU { r: 130, g: 130, b: 130, a: 255 },
1029            ColorU { r: 241, g: 241, b: 241, a: 255 },
1030        ),
1031        &[DynamicSelector::Os(OsCondition::Windows), DynamicSelector::Theme(ThemeCondition::Light)],
1032    ),
1033    // iOS dark
1034    CssPropertyWithConditions::with_single_condition(
1035        scrollbar_color(
1036            ColorU { r: 255, g: 255, b: 255, a: 100 },
1037            ColorU::TRANSPARENT,
1038        ),
1039        &[DynamicSelector::Os(OsCondition::IOS), DynamicSelector::Theme(ThemeCondition::Dark)],
1040    ),
1041    // iOS light
1042    CssPropertyWithConditions::with_single_condition(
1043        scrollbar_color(
1044            ColorU { r: 0, g: 0, b: 0, a: 100 },
1045            ColorU::TRANSPARENT,
1046        ),
1047        &[DynamicSelector::Os(OsCondition::IOS), DynamicSelector::Theme(ThemeCondition::Light)],
1048    ),
1049    // Android dark
1050    CssPropertyWithConditions::with_single_condition(
1051        scrollbar_color(
1052            ColorU { r: 255, g: 255, b: 255, a: 77 },
1053            ColorU::TRANSPARENT,
1054        ),
1055        &[DynamicSelector::Os(OsCondition::Android), DynamicSelector::Theme(ThemeCondition::Dark)],
1056    ),
1057    // Android light
1058    CssPropertyWithConditions::with_single_condition(
1059        scrollbar_color(
1060            ColorU { r: 0, g: 0, b: 0, a: 77 },
1061            ColorU::TRANSPARENT,
1062        ),
1063        &[DynamicSelector::Os(OsCondition::Android), DynamicSelector::Theme(ThemeCondition::Light)],
1064    ),
1065    // Linux / unknown dark fallback
1066    CssPropertyWithConditions::with_single_condition(
1067        scrollbar_color(
1068            ColorU { r: 100, g: 100, b: 100, a: 255 },
1069            ColorU { r: 45, g: 45, b: 45, a: 255 },
1070        ),
1071        &[DynamicSelector::Theme(ThemeCondition::Dark)],
1072    ),
1073    // Unconditional fallback (classic light)
1074    CssPropertyWithConditions::simple(
1075        scrollbar_color(
1076            ColorU { r: 193, g: 193, b: 193, a: 255 },
1077            ColorU { r: 241, g: 241, b: 241, a: 255 },
1078        ),
1079    ),
1080];
1081
1082/// Resolved UA scrollbar defaults after evaluating conditions.
1083///
1084/// All fields are guaranteed to resolve because `UA_SCROLLBAR_CSS`
1085/// contains unconditional fallback entries for every property type.
1086#[derive(Debug, Copy, Clone)]
1087pub struct ResolvedUaScrollbar {
1088    pub color: StyleScrollbarColor,
1089    pub width: LayoutScrollbarWidth,
1090    pub visibility: ScrollbarVisibilityMode,
1091    pub fade_delay: ScrollbarFadeDelay,
1092    pub fade_duration: ScrollbarFadeDuration,
1093}
1094
1095/// Evaluate UA scrollbar CSS rules against a `DynamicSelectorContext`.
1096///
1097/// Iterates `UA_SCROLLBAR_CSS` and picks the first matching entry per
1098/// property type.  Unconditional fallback entries in the table guarantee
1099/// that every field resolves.
1100#[must_use] pub fn evaluate_ua_scrollbar_css(ctx: &DynamicSelectorContext) -> ResolvedUaScrollbar {
1101    let mut color: Option<StyleScrollbarColor> = None;
1102    let mut width: Option<LayoutScrollbarWidth> = None;
1103    let mut visibility: Option<ScrollbarVisibilityMode> = None;
1104    let mut fade_delay: Option<ScrollbarFadeDelay> = None;
1105    let mut fade_duration: Option<ScrollbarFadeDuration> = None;
1106
1107    for prop in UA_SCROLLBAR_CSS {
1108        if !prop.matches(ctx) {
1109            continue;
1110        }
1111        match &prop.property {
1112            CssProperty::ScrollbarColor(CssPropertyValue::Exact(c)) => {
1113                if color.is_none() {
1114                    color = Some(*c);
1115                }
1116            }
1117            CssProperty::ScrollbarWidth(CssPropertyValue::Exact(w)) => {
1118                if width.is_none() {
1119                    width = Some(*w);
1120                }
1121            }
1122            CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(v)) => {
1123                if visibility.is_none() {
1124                    visibility = Some(*v);
1125                }
1126            }
1127            CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(d)) => {
1128                if fade_delay.is_none() {
1129                    fade_delay = Some(*d);
1130                }
1131            }
1132            CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(d)) => {
1133                if fade_duration.is_none() {
1134                    fade_duration = Some(*d);
1135                }
1136            }
1137            _ => {}
1138        }
1139        if color.is_some() && width.is_some() && visibility.is_some()
1140            && fade_delay.is_some() && fade_duration.is_some()
1141        {
1142            break;
1143        }
1144    }
1145
1146    // Unconditional `simple` entries in UA_SCROLLBAR_CSS guarantee all
1147    // fields resolve; these defaults match those entries as a safety net.
1148    ResolvedUaScrollbar {
1149        color: color.unwrap_or(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1150            thumb: ColorU { r: 193, g: 193, b: 193, a: 255 },
1151            track: ColorU { r: 241, g: 241, b: 241, a: 255 },
1152        })),
1153        width: width.unwrap_or(LayoutScrollbarWidth::Auto),
1154        visibility: visibility.unwrap_or(ScrollbarVisibilityMode::Always),
1155        fade_delay: fade_delay.unwrap_or(ScrollbarFadeDelay { ms: 0 }),
1156        fade_duration: fade_duration.unwrap_or(ScrollbarFadeDuration { ms: 0 }),
1157    }
1158}
1159
1160#[cfg(test)]
1161mod autotest_generated {
1162    use alloc::{string::String, vec, vec::Vec};
1163
1164    use azul_css::{corety::AzString, css::BoxOrStatic, props::basic::length::SizeMetric};
1165
1166    use super::*;
1167    use crate::resources::{ImageRef, RawImageFormat};
1168
1169    // ------------------------------------------------------------------
1170    // Constructors / helpers
1171    // ------------------------------------------------------------------
1172
1173    fn text_node(s: &str) -> NodeType {
1174        NodeType::Text(BoxOrStatic::heap(AzString::from(s)))
1175    }
1176
1177    fn icon_node(s: &str) -> NodeType {
1178        NodeType::Icon(BoxOrStatic::heap(AzString::from(s)))
1179    }
1180
1181    fn image_node() -> NodeType {
1182        NodeType::Image(BoxOrStatic::heap(ImageRef::null_image(
1183            1,
1184            1,
1185            RawImageFormat::RGBA8,
1186            Vec::new(),
1187        )))
1188    }
1189
1190    /// Broad (not literally exhaustive) sample of `NodeType`, covering every
1191    /// variant that has an arm in `get_ua_property` plus a spread of variants
1192    /// that have none, so the catch-all arms get exercised too.
1193    fn sample_node_types() -> Vec<NodeType> {
1194        use crate::dom::NodeType as NT;
1195        vec![
1196            // matched arms
1197            NT::Html, NT::Head, NT::Body, NT::Div, NT::P, NT::Main, NT::Header,
1198            NT::Footer, NT::Section, NT::Article, NT::Aside, NT::Nav,
1199            NT::H1, NT::H2, NT::H3, NT::H4, NT::H5, NT::H6,
1200            NT::Ul, NT::Ol, NT::Li, NT::Dl, NT::Dt, NT::Dd,
1201            NT::Span, NT::A, NT::Strong, NT::Em, NT::B, NT::I, NT::U, NT::Small,
1202            NT::Code, NT::Kbd, NT::Samp, NT::Sub, NT::Sup,
1203            NT::Pre, NT::BlockQuote, NT::Hr,
1204            NT::Table, NT::THead, NT::TBody, NT::TFoot, NT::Tr, NT::Th, NT::Td,
1205            NT::Caption, NT::ColGroup, NT::Col,
1206            NT::Form, NT::Input, NT::Button, NT::Select, NT::TextArea, NT::Label,
1207            NT::Title, NT::Script, NT::Style, NT::Link,
1208            NT::Br, NT::Video, NT::Audio, NT::Canvas, NT::Svg, NT::VirtualView,
1209            NT::SelectOption, NT::OptGroup,
1210            NT::Abbr, NT::Cite, NT::Del, NT::Ins, NT::Mark, NT::Q, NT::Dfn,
1211            NT::Var, NT::Time, NT::Data, NT::Wbr, NT::Bdi, NT::Bdo,
1212            NT::Rp, NT::Rt, NT::Rtc, NT::Ruby,
1213            NT::FieldSet, NT::Figure, NT::FigCaption, NT::Details, NT::Summary,
1214            NT::Dialog, NT::Menu, NT::Dir,
1215            // unmatched arms (must fall through to the catch-alls)
1216            NT::Address, NT::Legend, NT::Output, NT::Progress, NT::Meter,
1217            NT::DataList, NT::MenuItem, NT::S, NT::Big, NT::Acronym,
1218            NT::Object, NT::Param, NT::Embed, NT::Source, NT::Track, NT::Map,
1219            NT::Area, NT::Meta, NT::Base, NT::Before, NT::After, NT::Marker,
1220            NT::Placeholder, NT::SvgG, NT::SvgPath, NT::SvgRect,
1221            NT::SvgText(AzString::from("svg-text")),
1222            // payload-carrying variants
1223            text_node(""),
1224            text_node("hello"),
1225            icon_node("home"),
1226            image_node(),
1227        ]
1228    }
1229
1230    fn all_os() -> Vec<OsCondition> {
1231        vec![
1232            OsCondition::Any,
1233            OsCondition::Apple,
1234            OsCondition::MacOS,
1235            OsCondition::IOS,
1236            OsCondition::Linux,
1237            OsCondition::Windows,
1238            OsCondition::Android,
1239            OsCondition::Web,
1240        ]
1241    }
1242
1243    fn all_themes() -> Vec<ThemeCondition> {
1244        vec![
1245            ThemeCondition::Light,
1246            ThemeCondition::Dark,
1247            ThemeCondition::Custom(AzString::from("neon")),
1248            ThemeCondition::SystemPreferred,
1249        ]
1250    }
1251
1252    fn ctx(os: OsCondition, theme: ThemeCondition) -> DynamicSelectorContext {
1253        DynamicSelectorContext {
1254            os,
1255            theme,
1256            ..DynamicSelectorContext::default()
1257        }
1258    }
1259
1260    const CLASSIC_LIGHT_THUMB: ColorU = ColorU { r: 193, g: 193, b: 193, a: 255 };
1261    const CLASSIC_LIGHT_TRACK: ColorU = ColorU { r: 241, g: 241, b: 241, a: 255 };
1262
1263    fn custom_color(thumb: ColorU, track: ColorU) -> StyleScrollbarColor {
1264        StyleScrollbarColor::Custom(ScrollbarColorCustom { thumb, track })
1265    }
1266
1267    /// Extract the `(thumb, track)` pair, panicking if the property is not a
1268    /// `Custom` scrollbar color.
1269    fn unwrap_custom(c: StyleScrollbarColor) -> (ColorU, ColorU) {
1270        match c {
1271            StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
1272            StyleScrollbarColor::Auto => panic!("expected a Custom scrollbar color, got Auto"),
1273        }
1274    }
1275
1276    fn display_of(nt: &NodeType) -> LayoutDisplay {
1277        match get_ua_property(nt, CssPropertyType::Display) {
1278            Some(CssProperty::Display(CssPropertyValue::Exact(d))) => *d,
1279            other => panic!("{nt:?}: expected an exact display value, got {other:?}"),
1280        }
1281    }
1282
1283    fn font_size_em(nt: &NodeType) -> f32 {
1284        match get_ua_property(nt, CssPropertyType::FontSize) {
1285            Some(CssProperty::FontSize(CssPropertyValue::Exact(fs))) => {
1286                assert_eq!(fs.inner.metric, SizeMetric::Em, "{nt:?}: font-size must be em-relative");
1287                fs.inner.number.get()
1288            }
1289            other => panic!("{nt:?}: expected an exact em font-size, got {other:?}"),
1290        }
1291    }
1292
1293    // ==================================================================
1294    // get_ua_property — table-wide invariants
1295    // ==================================================================
1296
1297    /// The single most important invariant of the lookup table: the property
1298    /// that comes back must be *the property that was asked for*. A copy-paste
1299    /// slip in the ~200-arm table (e.g. `(H1, MarginBottom) => &MARGIN_TOP_...`)
1300    /// would silently mis-style elements; nothing else in the codebase checks it.
1301    #[test]
1302    fn returned_property_always_has_the_requested_type() {
1303        for nt in sample_node_types() {
1304            for pt in CssPropertyType::ALL {
1305                if let Some(prop) = get_ua_property(&nt, *pt) {
1306                    assert_eq!(
1307                        prop.get_type(),
1308                        *pt,
1309                        "get_ua_property({nt:?}, {pt:?}) returned a {:?} property",
1310                        prop.get_type()
1311                    );
1312                }
1313            }
1314        }
1315    }
1316
1317    #[test]
1318    fn full_cross_product_never_panics_and_is_deterministic() {
1319        for nt in sample_node_types() {
1320            for pt in CssPropertyType::ALL {
1321                let a = get_ua_property(&nt, *pt);
1322                let b = get_ua_property(&nt, *pt);
1323                match (a, b) {
1324                    (Some(a), Some(b)) => assert!(
1325                        core::ptr::eq(a, b),
1326                        "{nt:?}/{pt:?}: repeated lookups must hand back the same static"
1327                    ),
1328                    (None, None) => {}
1329                    _ => panic!("{nt:?}/{pt:?}: lookup is not deterministic"),
1330                }
1331            }
1332        }
1333    }
1334
1335    /// Documented contract: the `(_, Display)` catch-all means *every* node type
1336    /// resolves a display value, so layout never sees a node without one.
1337    #[test]
1338    fn display_resolves_for_every_node_type() {
1339        for nt in sample_node_types() {
1340            assert!(
1341                get_ua_property(&nt, CssPropertyType::Display).is_some(),
1342                "{nt:?} has no default display"
1343            );
1344        }
1345    }
1346
1347    #[test]
1348    fn unknown_elements_default_to_inline_display() {
1349        // Per CSS spec, unknown/custom elements are inline.
1350        for nt in [NodeType::Address, NodeType::Legend, NodeType::Meter, NodeType::SvgPath] {
1351            assert_eq!(display_of(&nt), LayoutDisplay::Inline, "{nt:?}");
1352        }
1353    }
1354
1355    /// `cursor` is deliberately defined for exactly three node types; anything
1356    /// else must return `None` so the cursor-resolution walk can inherit.
1357    #[test]
1358    fn cursor_default_exists_only_for_button_textarea_and_text() {
1359        for nt in sample_node_types() {
1360            let has_cursor = get_ua_property(&nt, CssPropertyType::Cursor).is_some();
1361            let expected = matches!(nt, NodeType::Button | NodeType::TextArea | NodeType::Text(_));
1362            assert_eq!(has_cursor, expected, "{nt:?}: unexpected cursor default");
1363        }
1364    }
1365
1366    // ==================================================================
1367    // get_ua_property — payload-carrying node types (unicode / huge / empty)
1368    // ==================================================================
1369
1370    #[test]
1371    fn text_node_defaults_are_independent_of_the_payload() {
1372        let huge = "🦀".repeat(100_000);
1373        let payloads: Vec<String> = vec![
1374            String::new(),
1375            "\0".into(),
1376            "\u{202E}\u{200B}\u{FEFF}".into(), // RTL override, ZWSP, BOM
1377            "مرحبا بالعالم".into(),
1378            "🇩🇪👨‍👩‍👧‍👦".into(),
1379            "\u{FFFD}".into(),
1380            huge,
1381        ];
1382
1383        for p in payloads {
1384            let nt = text_node(&p);
1385            assert_eq!(
1386                display_of(&nt),
1387                LayoutDisplay::Inline,
1388                "text node display must not depend on its content"
1389            );
1390            assert_eq!(
1391                get_ua_property(&nt, CssPropertyType::Cursor),
1392                Some(&CURSOR_TEXT),
1393                "text node cursor must not depend on its content"
1394            );
1395            // Text nodes define no box properties of their own.
1396            assert!(get_ua_property(&nt, CssPropertyType::Width).is_none());
1397            assert!(get_ua_property(&nt, CssPropertyType::Height).is_none());
1398            assert!(get_ua_property(&nt, CssPropertyType::MarginTop).is_none());
1399        }
1400    }
1401
1402    #[test]
1403    fn icon_and_image_nodes_are_inline_block_regardless_of_payload() {
1404        let huge_name = "x".repeat(50_000);
1405        let names: [&str; 4] = ["", "home", "🏠", huge_name.as_str()];
1406        for name in names {
1407            assert_eq!(display_of(&icon_node(name)), LayoutDisplay::InlineBlock, "icon {name:?}");
1408        }
1409        assert_eq!(display_of(&image_node()), LayoutDisplay::InlineBlock);
1410    }
1411
1412    // ==================================================================
1413    // get_ua_property — specific, load-bearing defaults
1414    // ==================================================================
1415
1416    /// Regression guard for the 2026-06-02 DIAG revert documented in the table:
1417    /// `(Html, Height) => HEIGHT_100_PERCENT` is commented out on purpose. If it
1418    /// comes back without the jump-table dispatch fix, children wrongly inherit
1419    /// `height: 100%`.
1420    #[test]
1421    fn html_has_no_default_height() {
1422        assert_eq!(get_ua_property(&NodeType::Html, CssPropertyType::Display), Some(&DISPLAY_BLOCK));
1423        assert!(
1424            get_ua_property(&NodeType::Html, CssPropertyType::Height).is_none(),
1425            "the (Html, Height) arm is intentionally disabled — see the DIAG note"
1426        );
1427    }
1428
1429    /// `body { margin: 8px }` (Chrome UA), and crucially *no* width/height:
1430    /// giving body a size would break percentage sizing of its children.
1431    #[test]
1432    fn body_has_8px_margins_and_no_intrinsic_size() {
1433        assert_eq!(display_of(&NodeType::Body), LayoutDisplay::Block);
1434        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginTop), Some(&MARGIN_TOP_8PX));
1435        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginBottom), Some(&MARGIN_BOTTOM_8PX));
1436        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginLeft), Some(&MARGIN_LEFT_8PX));
1437        assert_eq!(get_ua_property(&NodeType::Body, CssPropertyType::MarginRight), Some(&MARGIN_RIGHT_8PX));
1438        assert!(get_ua_property(&NodeType::Body, CssPropertyType::Width).is_none());
1439        assert!(get_ua_property(&NodeType::Body, CssPropertyType::Height).is_none());
1440    }
1441
1442    /// Block elements must have `width: auto`, not `width: 100%` — the comment in
1443    /// the table calls this out as critical for flexbox (100% defeats flex-grow).
1444    #[test]
1445    fn block_elements_have_no_default_width() {
1446        for nt in [NodeType::Div, NodeType::P, NodeType::Section, NodeType::Main, NodeType::VirtualView] {
1447            assert_eq!(display_of(&nt), LayoutDisplay::Block, "{nt:?}");
1448            assert!(
1449                get_ua_property(&nt, CssPropertyType::Width).is_none(),
1450                "{nt:?} must be width:auto so it can flex-grow"
1451            );
1452        }
1453    }
1454
1455    #[test]
1456    fn div_defines_only_a_display_default() {
1457        for pt in CssPropertyType::ALL {
1458            let got = get_ua_property(&NodeType::Div, *pt);
1459            if *pt == CssPropertyType::Display {
1460                assert!(got.is_some());
1461            } else {
1462                assert!(got.is_none(), "Div should not define a UA default for {pt:?}");
1463            }
1464        }
1465    }
1466
1467    #[test]
1468    fn metadata_elements_are_display_none() {
1469        for nt in [NodeType::Head, NodeType::Title, NodeType::Script, NodeType::Style, NodeType::Link] {
1470            assert_eq!(display_of(&nt), LayoutDisplay::None, "{nt:?} must not render");
1471        }
1472    }
1473
1474    #[test]
1475    fn heading_font_sizes_are_strictly_decreasing() {
1476        let sizes: Vec<f32> = [NodeType::H1, NodeType::H2, NodeType::H3, NodeType::H4, NodeType::H5, NodeType::H6]
1477            .iter()
1478            .map(font_size_em)
1479            .collect();
1480
1481        // Chrome UA values — also verifies `const_em_fractional(1, 5)` really
1482        // encodes 1.5 (and not 1.05), which the digit-count encoding makes subtle.
1483        let expected = [2.0_f32, 1.5, 1.17, 1.0, 0.83, 0.67];
1484        for (i, (got, want)) in sizes.iter().zip(expected.iter()).enumerate() {
1485            assert!(
1486                (got - want).abs() < 1e-4,
1487                "H{} font-size: got {got}em, want {want}em",
1488                i + 1
1489            );
1490        }
1491        for w in sizes.windows(2) {
1492            assert!(w[0] > w[1], "heading font sizes must strictly decrease, got {sizes:?}");
1493        }
1494    }
1495
1496    #[test]
1497    fn headings_are_bold_blocks_that_avoid_page_breaks() {
1498        for nt in [NodeType::H1, NodeType::H2, NodeType::H3, NodeType::H4, NodeType::H5, NodeType::H6] {
1499            assert_eq!(display_of(&nt), LayoutDisplay::Block, "{nt:?}");
1500            assert_eq!(
1501                get_ua_property(&nt, CssPropertyType::FontWeight),
1502                Some(&FONT_WEIGHT_BOLD),
1503                "{nt:?}"
1504            );
1505            assert_eq!(
1506                get_ua_property(&nt, CssPropertyType::BreakInside),
1507                Some(&BREAK_INSIDE_AVOID),
1508                "{nt:?}"
1509            );
1510            assert_eq!(
1511                get_ua_property(&nt, CssPropertyType::BreakAfter),
1512                Some(&BREAK_AFTER_AVOID),
1513                "{nt:?}"
1514            );
1515            // Both margins must exist and be em-relative (they scale with font-size).
1516            for pt in [CssPropertyType::MarginTop, CssPropertyType::MarginBottom] {
1517                assert!(get_ua_property(&nt, pt).is_some(), "{nt:?} is missing {pt:?}");
1518            }
1519        }
1520    }
1521
1522    /// Tables *can* break across pages; their rows/headers/footers cannot. The
1523    /// table comments say so explicitly, so lock the asymmetry in.
1524    #[test]
1525    fn tables_may_break_across_pages_but_rows_may_not() {
1526        assert!(get_ua_property(&NodeType::Table, CssPropertyType::BreakInside).is_none());
1527        assert!(get_ua_property(&NodeType::TBody, CssPropertyType::BreakInside).is_none());
1528        for nt in [NodeType::THead, NodeType::TFoot, NodeType::Tr] {
1529            assert_eq!(
1530                get_ua_property(&nt, CssPropertyType::BreakInside),
1531                Some(&BREAK_INSIDE_AVOID),
1532                "{nt:?}"
1533            );
1534        }
1535    }
1536
1537    #[test]
1538    fn table_display_types_are_not_crossed() {
1539        assert_eq!(display_of(&NodeType::Table), LayoutDisplay::Table);
1540        assert_eq!(display_of(&NodeType::THead), LayoutDisplay::TableHeaderGroup);
1541        assert_eq!(display_of(&NodeType::TBody), LayoutDisplay::TableRowGroup);
1542        assert_eq!(display_of(&NodeType::TFoot), LayoutDisplay::TableFooterGroup);
1543        assert_eq!(display_of(&NodeType::Tr), LayoutDisplay::TableRow);
1544        assert_eq!(display_of(&NodeType::Th), LayoutDisplay::TableCell);
1545        assert_eq!(display_of(&NodeType::Td), LayoutDisplay::TableCell);
1546        assert_eq!(display_of(&NodeType::Caption), LayoutDisplay::TableCaption);
1547        assert_eq!(display_of(&NodeType::ColGroup), LayoutDisplay::TableColumnGroup);
1548        assert_eq!(display_of(&NodeType::Col), LayoutDisplay::TableColumn);
1549    }
1550
1551    #[test]
1552    fn table_cells_have_1px_padding_on_all_four_sides() {
1553        for nt in [NodeType::Th, NodeType::Td] {
1554            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingTop), Some(&PADDING_TOP_1PX), "{nt:?}");
1555            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingBottom), Some(&PADDING_BOTTOM_1PX), "{nt:?}");
1556            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingLeft), Some(&PADDING_LEFT_1PX), "{nt:?}");
1557            assert_eq!(get_ua_property(&nt, CssPropertyType::PaddingRight), Some(&PADDING_RIGHT_1PX), "{nt:?}");
1558            assert_eq!(get_ua_property(&nt, CssPropertyType::VerticalAlign), Some(&VERTICAL_ALIGN_MIDDLE), "{nt:?}");
1559        }
1560        // Only <th> is centered + bold.
1561        assert_eq!(get_ua_property(&NodeType::Th, CssPropertyType::TextAlign), Some(&TEXT_ALIGN_CENTER));
1562        assert_eq!(get_ua_property(&NodeType::Th, CssPropertyType::FontWeight), Some(&FONT_WEIGHT_BOLD));
1563        assert!(get_ua_property(&NodeType::Td, CssPropertyType::TextAlign).is_none());
1564        assert!(get_ua_property(&NodeType::Td, CssPropertyType::FontWeight).is_none());
1565    }
1566
1567    /// A button's border is symmetric. Crossed sides (e.g. `BorderLeftWidth`
1568    /// answered with the *top* static) would render an asymmetric button, so
1569    /// check that each side carries the value the table promises.
1570    #[test]
1571    fn button_border_is_symmetric_on_all_four_sides() {
1572        let widths = [
1573            (CssPropertyType::BorderTopWidth, &BUTTON_BORDER_TOP_WIDTH),
1574            (CssPropertyType::BorderBottomWidth, &BUTTON_BORDER_BOTTOM_WIDTH),
1575            (CssPropertyType::BorderLeftWidth, &BUTTON_BORDER_LEFT_WIDTH),
1576            (CssPropertyType::BorderRightWidth, &BUTTON_BORDER_RIGHT_WIDTH),
1577        ];
1578        for (pt, want) in widths {
1579            assert_eq!(get_ua_property(&NodeType::Button, pt), Some(want), "{pt:?}");
1580        }
1581
1582        let styles = [
1583            (CssPropertyType::BorderTopStyle, &BUTTON_BORDER_TOP_STYLE),
1584            (CssPropertyType::BorderBottomStyle, &BUTTON_BORDER_BOTTOM_STYLE),
1585            (CssPropertyType::BorderLeftStyle, &BUTTON_BORDER_LEFT_STYLE),
1586            (CssPropertyType::BorderRightStyle, &BUTTON_BORDER_RIGHT_STYLE),
1587        ];
1588        for (pt, want) in styles {
1589            assert_eq!(get_ua_property(&NodeType::Button, pt), Some(want), "{pt:?}");
1590        }
1591
1592        let colors = [
1593            (CssPropertyType::BorderTopColor, &BUTTON_BORDER_TOP_COLOR),
1594            (CssPropertyType::BorderBottomColor, &BUTTON_BORDER_BOTTOM_COLOR),
1595            (CssPropertyType::BorderLeftColor, &BUTTON_BORDER_LEFT_COLOR),
1596            (CssPropertyType::BorderRightColor, &BUTTON_BORDER_RIGHT_COLOR),
1597        ];
1598        for (pt, want) in colors {
1599            assert_eq!(get_ua_property(&NodeType::Button, pt), Some(want), "{pt:?}");
1600        }
1601
1602        assert_eq!(display_of(&NodeType::Button), LayoutDisplay::InlineBlock);
1603        assert_eq!(get_ua_property(&NodeType::Button, CssPropertyType::Cursor), Some(&CURSOR_POINTER));
1604    }
1605
1606    /// `<hr>` draws its line from the *border*, not from a height — height must
1607    /// be exactly 0px, and the width exactly 100%.
1608    #[test]
1609    fn hr_line_comes_from_the_border_not_from_height() {
1610        match get_ua_property(&NodeType::Hr, CssPropertyType::Height) {
1611            Some(CssProperty::Height(CssPropertyValue::Exact(LayoutHeight::Px(pv)))) => {
1612                assert_eq!(pv.metric, SizeMetric::Px);
1613                assert!((pv.number.get() - 0.0).abs() < 1e-6, "hr height must be 0px");
1614            }
1615            other => panic!("hr height: {other:?}"),
1616        }
1617        match get_ua_property(&NodeType::Hr, CssPropertyType::Width) {
1618            Some(CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(pv)))) => {
1619                assert_eq!(pv.metric, SizeMetric::Percent);
1620                assert!((pv.number.get() - 100.0).abs() < 1e-4, "hr width must be 100%");
1621            }
1622            other => panic!("hr width: {other:?}"),
1623        }
1624        assert_eq!(get_ua_property(&NodeType::Hr, CssPropertyType::BorderTopStyle), Some(&BORDER_TOP_STYLE_INSET));
1625        assert_eq!(get_ua_property(&NodeType::Hr, CssPropertyType::BorderTopWidth), Some(&BORDER_TOP_WIDTH_1PX));
1626        assert_eq!(get_ua_property(&NodeType::Hr, CssPropertyType::BorderTopColor), Some(&BORDER_TOP_COLOR_GRAY));
1627    }
1628
1629    #[test]
1630    fn list_containers_reset_the_counter_and_reserve_marker_space() {
1631        for (nt, marker) in [
1632            (NodeType::Ul, &LIST_STYLE_TYPE_DISC),
1633            (NodeType::Ol, &LIST_STYLE_TYPE_DECIMAL),
1634        ] {
1635            assert_eq!(display_of(&nt), LayoutDisplay::Block, "{nt:?}");
1636            assert_eq!(get_ua_property(&nt, CssPropertyType::ListStyleType), Some(marker), "{nt:?}");
1637            assert_eq!(
1638                get_ua_property(&nt, CssPropertyType::CounterReset),
1639                Some(&COUNTER_RESET_LIST_ITEM),
1640                "{nt:?} must reset the list-item counter"
1641            );
1642            assert_eq!(
1643                get_ua_property(&nt, CssPropertyType::PaddingLeft),
1644                Some(&PADDING_INLINE_START_40PX),
1645                "{nt:?}"
1646            );
1647        }
1648        assert_eq!(display_of(&NodeType::Li), LayoutDisplay::ListItem);
1649    }
1650
1651    #[test]
1652    fn inline_emphasis_and_link_defaults() {
1653        assert_eq!(get_ua_property(&NodeType::A, CssPropertyType::TextDecoration), Some(&TEXT_DECORATION_UNDERLINE));
1654        assert_eq!(get_ua_property(&NodeType::U, CssPropertyType::TextDecoration), Some(&TEXT_DECORATION_UNDERLINE));
1655        assert_eq!(get_ua_property(&NodeType::Strong, CssPropertyType::FontWeight), Some(&FONT_WEIGHT_BOLDER));
1656        assert_eq!(get_ua_property(&NodeType::B, CssPropertyType::FontWeight), Some(&FONT_WEIGHT_BOLDER));
1657        // <em>/<i> are italic via font-style, which the UA table does not define.
1658        assert!(get_ua_property(&NodeType::Em, CssPropertyType::FontWeight).is_none());
1659        assert!(get_ua_property(&NodeType::I, CssPropertyType::FontWeight).is_none());
1660    }
1661
1662    // ==================================================================
1663    // const scrollbar helpers — numeric round-trips / boundaries
1664    // ==================================================================
1665
1666    #[test]
1667    fn scrollbar_fade_delay_round_trips_every_boundary() {
1668        for ms in [0_u32, 1, 2, 299, 300, 500, u32::from(u16::MAX), i32::MAX as u32, u32::MAX - 1, u32::MAX] {
1669            match scrollbar_fade_delay(ms) {
1670                CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(d)) => {
1671                    assert_eq!(d.ms, ms, "fade-delay must round-trip losslessly");
1672                }
1673                other => panic!("scrollbar_fade_delay({ms}) built a {other:?}"),
1674            }
1675        }
1676    }
1677
1678    #[test]
1679    fn scrollbar_fade_duration_round_trips_every_boundary() {
1680        for ms in [0_u32, 1, 150, 200, u32::from(u16::MAX), i32::MAX as u32, u32::MAX - 1, u32::MAX] {
1681            match scrollbar_fade_duration(ms) {
1682                CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(d)) => {
1683                    assert_eq!(d.ms, ms, "fade-duration must round-trip losslessly");
1684                }
1685                other => panic!("scrollbar_fade_duration({ms}) built a {other:?}"),
1686            }
1687        }
1688    }
1689
1690    /// `u32::MAX` in a `const` item: if either helper ever grew an arithmetic
1691    /// conversion (ms → ns, ms → seconds), this fails to *compile* rather than
1692    /// silently wrapping in release and panicking in debug.
1693    #[test]
1694    fn scrollbar_fade_helpers_are_const_evaluable_at_u32_max() {
1695        const MAX_DELAY: CssProperty = scrollbar_fade_delay(u32::MAX);
1696        const MAX_DURATION: CssProperty = scrollbar_fade_duration(u32::MAX);
1697        const ZERO_DELAY: CssProperty = scrollbar_fade_delay(0);
1698
1699        assert_eq!(MAX_DELAY, scrollbar_fade_delay(u32::MAX));
1700        assert_eq!(MAX_DURATION, scrollbar_fade_duration(u32::MAX));
1701        assert_eq!(ZERO_DELAY, scrollbar_fade_delay(0));
1702    }
1703
1704    /// The two helpers take the same `u32` and differ only in the wrapper type —
1705    /// exactly the shape a copy-paste bug likes. Assert they stay distinct.
1706    #[test]
1707    fn fade_delay_and_fade_duration_produce_distinct_property_types() {
1708        assert_eq!(scrollbar_fade_delay(42).get_type(), CssPropertyType::ScrollbarFadeDelay);
1709        assert_eq!(scrollbar_fade_duration(42).get_type(), CssPropertyType::ScrollbarFadeDuration);
1710        assert_ne!(scrollbar_fade_delay(42), scrollbar_fade_duration(42));
1711    }
1712
1713    /// A `0` delay means "never fades" (per the `ScrollbarFadeDelay` docs), so it
1714    /// must be stored as a literal zero, not as a sentinel.
1715    #[test]
1716    fn zero_fade_delay_and_duration_are_literal_zero() {
1717        assert_eq!(
1718            scrollbar_fade_delay(0),
1719            CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(ScrollbarFadeDelay::ZERO))
1720        );
1721        assert_eq!(
1722            scrollbar_fade_duration(0),
1723            CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(ScrollbarFadeDuration::ZERO))
1724        );
1725    }
1726
1727    #[test]
1728    fn scrollbar_color_never_swaps_thumb_and_track() {
1729        let cases = [
1730            (ColorU { r: 1, g: 2, b: 3, a: 4 }, ColorU { r: 5, g: 6, b: 7, a: 8 }),
1731            (ColorU { r: 0, g: 0, b: 0, a: 0 }, ColorU { r: 255, g: 255, b: 255, a: 255 }),
1732            (ColorU { r: 255, g: 255, b: 255, a: 255 }, ColorU::TRANSPARENT),
1733            (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
1734        ];
1735        for (thumb, track) in cases {
1736            match scrollbar_color(thumb, track) {
1737                CssProperty::ScrollbarColor(CssPropertyValue::Exact(StyleScrollbarColor::Custom(c))) => {
1738                    assert_eq!(c.thumb, thumb, "thumb was not preserved");
1739                    assert_eq!(c.track, track, "track was not preserved (arguments swapped?)");
1740                }
1741                other => panic!("scrollbar_color built a {other:?}"),
1742            }
1743        }
1744    }
1745
1746    #[test]
1747    fn scrollbar_width_and_visibility_round_trip_every_variant() {
1748        for w in [LayoutScrollbarWidth::Auto, LayoutScrollbarWidth::Thin, LayoutScrollbarWidth::None] {
1749            match scrollbar_width(w) {
1750                CssProperty::ScrollbarWidth(CssPropertyValue::Exact(got)) => assert_eq!(got, w),
1751                other => panic!("scrollbar_width({w:?}) built a {other:?}"),
1752            }
1753        }
1754        for v in [
1755            ScrollbarVisibilityMode::Always,
1756            ScrollbarVisibilityMode::WhenScrolling,
1757            ScrollbarVisibilityMode::Auto,
1758        ] {
1759            match scrollbar_visibility(v) {
1760                CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(got)) => assert_eq!(got, v),
1761                other => panic!("scrollbar_visibility({v:?}) built a {other:?}"),
1762            }
1763        }
1764    }
1765
1766    // ==================================================================
1767    // UA_SCROLLBAR_CSS — table shape invariants
1768    // ==================================================================
1769
1770    /// `evaluate_ua_scrollbar_css` matches on exactly five property kinds and
1771    /// silently drops everything else via `_ => {}`. A sixth property added to
1772    /// the table would therefore never take effect — fail loudly here instead.
1773    #[test]
1774    fn table_contains_only_property_kinds_the_evaluator_understands() {
1775        let understood = [
1776            CssPropertyType::ScrollbarColor,
1777            CssPropertyType::ScrollbarWidth,
1778            CssPropertyType::ScrollbarVisibility,
1779            CssPropertyType::ScrollbarFadeDelay,
1780            CssPropertyType::ScrollbarFadeDuration,
1781        ];
1782        for (i, entry) in UA_SCROLLBAR_CSS.iter().enumerate() {
1783            let ty = entry.property.get_type();
1784            assert!(
1785                understood.contains(&ty),
1786                "UA_SCROLLBAR_CSS[{i}] is a {ty:?}, which evaluate_ua_scrollbar_css ignores"
1787            );
1788        }
1789    }
1790
1791    /// The evaluator only reads `CssPropertyValue::Exact`; an `Auto`/`Inherit`
1792    /// entry would be skipped without a trace.
1793    #[test]
1794    fn every_table_entry_carries_an_exact_value() {
1795        for (i, entry) in UA_SCROLLBAR_CSS.iter().enumerate() {
1796            let is_exact = matches!(
1797                &entry.property,
1798                CssProperty::ScrollbarColor(CssPropertyValue::Exact(_))
1799                    | CssProperty::ScrollbarWidth(CssPropertyValue::Exact(_))
1800                    | CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(_))
1801                    | CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(_))
1802                    | CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(_))
1803            );
1804            assert!(is_exact, "UA_SCROLLBAR_CSS[{i}] is not an Exact value: {:?}", entry.property);
1805        }
1806    }
1807
1808    /// The documented guarantee ("unconditional fallback entries … guarantee that
1809    /// every field resolves") plus the ordering rule it depends on: under
1810    /// first-match-wins, an unconditional entry that is *not* last for its
1811    /// property type would make every rule after it dead code.
1812    #[test]
1813    fn each_property_type_has_exactly_one_unconditional_entry_and_it_is_last() {
1814        for ty in [
1815            CssPropertyType::ScrollbarColor,
1816            CssPropertyType::ScrollbarWidth,
1817            CssPropertyType::ScrollbarVisibility,
1818            CssPropertyType::ScrollbarFadeDelay,
1819            CssPropertyType::ScrollbarFadeDuration,
1820        ] {
1821            let of_type: Vec<&CssPropertyWithConditions> = UA_SCROLLBAR_CSS
1822                .iter()
1823                .filter(|e| e.property.get_type() == ty)
1824                .collect();
1825            assert!(!of_type.is_empty(), "{ty:?} has no entry at all");
1826
1827            let unconditional: Vec<usize> = of_type
1828                .iter()
1829                .enumerate()
1830                .filter(|(_, e)| e.apply_if.as_slice().is_empty())
1831                .map(|(i, _)| i)
1832                .collect();
1833
1834            assert_eq!(
1835                unconditional.len(),
1836                1,
1837                "{ty:?} must have exactly one unconditional fallback, found {}",
1838                unconditional.len()
1839            );
1840            assert_eq!(
1841                unconditional[0],
1842                of_type.len() - 1,
1843                "{ty:?}: the unconditional fallback must come last, otherwise the \
1844                 {} rule(s) after it are dead under first-match-wins",
1845                of_type.len() - 1 - unconditional[0]
1846            );
1847        }
1848    }
1849
1850    // ==================================================================
1851    // evaluate_ua_scrollbar_css
1852    // ==================================================================
1853
1854    #[test]
1855    fn default_context_resolves_to_the_classic_light_scrollbar() {
1856        let r = evaluate_ua_scrollbar_css(&DynamicSelectorContext::default());
1857        assert_eq!(r.width, LayoutScrollbarWidth::Auto);
1858        assert_eq!(r.visibility, ScrollbarVisibilityMode::Always);
1859        assert_eq!(r.fade_delay.ms, 0);
1860        assert_eq!(r.fade_duration.ms, 0);
1861        assert_eq!(unwrap_custom(r.color), (CLASSIC_LIGHT_THUMB, CLASSIC_LIGHT_TRACK));
1862    }
1863
1864    #[test]
1865    fn per_os_and_theme_defaults_are_what_the_table_promises() {
1866        let cases: Vec<(OsCondition, ThemeCondition, LayoutScrollbarWidth, ScrollbarVisibilityMode, u32, u32, StyleScrollbarColor)> = vec![
1867            (
1868                OsCondition::MacOS, ThemeCondition::Dark,
1869                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
1870                custom_color(ColorU { r: 180, g: 180, b: 180, a: 200 }, ColorU { r: 40, g: 40, b: 40, a: 80 }),
1871            ),
1872            (
1873                OsCondition::MacOS, ThemeCondition::Light,
1874                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
1875                custom_color(ColorU { r: 80, g: 80, b: 80, a: 200 }, ColorU { r: 200, g: 200, b: 200, a: 80 }),
1876            ),
1877            (
1878                OsCondition::Windows, ThemeCondition::Dark,
1879                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
1880                custom_color(ColorU { r: 110, g: 110, b: 110, a: 255 }, ColorU { r: 32, g: 32, b: 32, a: 255 }),
1881            ),
1882            (
1883                OsCondition::Windows, ThemeCondition::Light,
1884                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
1885                custom_color(ColorU { r: 130, g: 130, b: 130, a: 255 }, ColorU { r: 241, g: 241, b: 241, a: 255 }),
1886            ),
1887            (
1888                OsCondition::IOS, ThemeCondition::Dark,
1889                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
1890                custom_color(ColorU { r: 255, g: 255, b: 255, a: 100 }, ColorU::TRANSPARENT),
1891            ),
1892            (
1893                OsCondition::IOS, ThemeCondition::Light,
1894                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 500, 200,
1895                custom_color(ColorU { r: 0, g: 0, b: 0, a: 100 }, ColorU::TRANSPARENT),
1896            ),
1897            (
1898                OsCondition::Android, ThemeCondition::Dark,
1899                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 300, 150,
1900                custom_color(ColorU { r: 255, g: 255, b: 255, a: 77 }, ColorU::TRANSPARENT),
1901            ),
1902            (
1903                OsCondition::Android, ThemeCondition::Light,
1904                LayoutScrollbarWidth::Thin, ScrollbarVisibilityMode::WhenScrolling, 300, 150,
1905                custom_color(ColorU { r: 0, g: 0, b: 0, a: 77 }, ColorU::TRANSPARENT),
1906            ),
1907            (
1908                // Linux has no OS-specific colour rule: dark falls through to the
1909                // generic dark entry.
1910                OsCondition::Linux, ThemeCondition::Dark,
1911                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
1912                custom_color(ColorU { r: 100, g: 100, b: 100, a: 255 }, ColorU { r: 45, g: 45, b: 45, a: 255 }),
1913            ),
1914            (
1915                OsCondition::Linux, ThemeCondition::Light,
1916                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
1917                custom_color(CLASSIC_LIGHT_THUMB, CLASSIC_LIGHT_TRACK),
1918            ),
1919            (
1920                OsCondition::Web, ThemeCondition::Dark,
1921                LayoutScrollbarWidth::Auto, ScrollbarVisibilityMode::Always, 0, 0,
1922                custom_color(ColorU { r: 100, g: 100, b: 100, a: 255 }, ColorU { r: 45, g: 45, b: 45, a: 255 }),
1923            ),
1924        ];
1925
1926        for (os, theme, width, visibility, delay, duration, color) in cases {
1927            let r = evaluate_ua_scrollbar_css(&ctx(os, theme.clone()));
1928            assert_eq!(r.width, width, "{os:?}/{theme:?}: width");
1929            assert_eq!(r.visibility, visibility, "{os:?}/{theme:?}: visibility");
1930            assert_eq!(r.fade_delay.ms, delay, "{os:?}/{theme:?}: fade-delay");
1931            assert_eq!(r.fade_duration.ms, duration, "{os:?}/{theme:?}: fade-duration");
1932            assert_eq!(r.color, color, "{os:?}/{theme:?}: color");
1933        }
1934    }
1935
1936    /// `match_theme` compares by equality (except when the *condition* is
1937    /// `SystemPreferred`), so a context theme of `Custom(..)` / `SystemPreferred`
1938    /// matches no `@theme` rule at all — every such context must still resolve a
1939    /// colour, via the unconditional fallback.
1940    #[test]
1941    fn unrecognised_context_themes_fall_back_instead_of_failing() {
1942        for theme in [ThemeCondition::Custom(AzString::from("")), ThemeCondition::Custom(AzString::from("🎨")), ThemeCondition::SystemPreferred] {
1943            // OS-conditioned properties still apply — only the theme rules miss.
1944            let r = evaluate_ua_scrollbar_css(&ctx(OsCondition::MacOS, theme.clone()));
1945            assert_eq!(r.width, LayoutScrollbarWidth::Thin, "{theme:?}");
1946            assert_eq!(r.visibility, ScrollbarVisibilityMode::WhenScrolling, "{theme:?}");
1947            assert_eq!(
1948                unwrap_custom(r.color),
1949                (CLASSIC_LIGHT_THUMB, CLASSIC_LIGHT_TRACK),
1950                "{theme:?}: must fall back to the unconditional colour"
1951            );
1952        }
1953    }
1954
1955    /// `OsCondition::Apple` is condition-side sugar (it *matches* MacOS/IOS); as a
1956    /// *context* value it equals neither, so an `Apple` context gets the generic
1957    /// defaults. `DynamicSelectorContext::from_system_style` never produces it, so
1958    /// this pins down the (slightly surprising) behaviour rather than blessing it.
1959    #[test]
1960    fn apple_as_a_context_os_matches_no_macos_or_ios_rule() {
1961        let r = evaluate_ua_scrollbar_css(&ctx(OsCondition::Apple, ThemeCondition::Dark));
1962        assert_eq!(r.width, LayoutScrollbarWidth::Auto);
1963        assert_eq!(r.visibility, ScrollbarVisibilityMode::Always);
1964        assert_eq!(r.fade_delay.ms, 0);
1965        assert_eq!(r.fade_duration.ms, 0);
1966    }
1967
1968    /// Overlay scrollbars are a package deal: `thin` ⇔ `when-scrolling` ⇔ a
1969    /// non-zero fade delay ⇔ a non-zero fade duration. A per-OS rule added to one
1970    /// group but forgotten in another would produce an overlay scrollbar that
1971    /// never fades (or a classic one that does).
1972    #[test]
1973    fn overlay_scrollbar_fields_stay_consistent_across_every_os_and_theme() {
1974        for os in all_os() {
1975            for theme in all_themes() {
1976                let r = evaluate_ua_scrollbar_css(&ctx(os, theme.clone()));
1977                let thin = r.width == LayoutScrollbarWidth::Thin;
1978                let overlay = r.visibility == ScrollbarVisibilityMode::WhenScrolling;
1979
1980                assert_eq!(thin, overlay, "{os:?}/{theme:?}: thin/when-scrolling disagree");
1981                assert_eq!(
1982                    overlay,
1983                    r.fade_delay.ms > 0,
1984                    "{os:?}/{theme:?}: an overlay scrollbar needs a fade delay"
1985                );
1986                assert_eq!(
1987                    overlay,
1988                    r.fade_duration.ms > 0,
1989                    "{os:?}/{theme:?}: an overlay scrollbar needs a fade duration"
1990                );
1991                // The table only ever supplies Custom colours.
1992                assert!(
1993                    matches!(r.color, StyleScrollbarColor::Custom(_)),
1994                    "{os:?}/{theme:?}: colour resolved to Auto"
1995                );
1996            }
1997        }
1998    }
1999
2000    /// The evaluator `break`s early once all five fields are filled. Cross-check
2001    /// it against a straight first-match-wins scan with no early exit: the two
2002    /// must agree for every context, or the optimisation changed the semantics.
2003    #[test]
2004    fn early_break_does_not_change_the_first_match_result() {
2005        for os in all_os() {
2006            for theme in all_themes() {
2007                let c = ctx(os, theme.clone());
2008                let got = evaluate_ua_scrollbar_css(&c);
2009
2010                let mut want_color = None;
2011                let mut want_width = None;
2012                let mut want_vis = None;
2013                let mut want_delay = None;
2014                let mut want_dur = None;
2015                for entry in UA_SCROLLBAR_CSS.iter().filter(|e| e.matches(&c)) {
2016                    match &entry.property {
2017                        CssProperty::ScrollbarColor(CssPropertyValue::Exact(v)) => {
2018                            if want_color.is_none() {
2019                                want_color = Some(*v);
2020                            }
2021                        }
2022                        CssProperty::ScrollbarWidth(CssPropertyValue::Exact(v)) => {
2023                            if want_width.is_none() {
2024                                want_width = Some(*v);
2025                            }
2026                        }
2027                        CssProperty::ScrollbarVisibility(CssPropertyValue::Exact(v)) => {
2028                            if want_vis.is_none() {
2029                                want_vis = Some(*v);
2030                            }
2031                        }
2032                        CssProperty::ScrollbarFadeDelay(CssPropertyValue::Exact(v)) => {
2033                            if want_delay.is_none() {
2034                                want_delay = Some(*v);
2035                            }
2036                        }
2037                        CssProperty::ScrollbarFadeDuration(CssPropertyValue::Exact(v)) => {
2038                            if want_dur.is_none() {
2039                                want_dur = Some(*v);
2040                            }
2041                        }
2042                        _ => {}
2043                    }
2044                }
2045
2046                let label = alloc::format!("{os:?}/{theme:?}");
2047                assert_eq!(Some(got.color), want_color, "{label}: color");
2048                assert_eq!(Some(got.width), want_width, "{label}: width");
2049                assert_eq!(Some(got.visibility), want_vis, "{label}: visibility");
2050                assert_eq!(Some(got.fade_delay), want_delay, "{label}: fade-delay");
2051                assert_eq!(Some(got.fade_duration), want_dur, "{label}: fade-duration");
2052            }
2053        }
2054    }
2055
2056    /// Degenerate / hostile context values (NaN, infinities, empty and huge
2057    /// strings) must not panic, and every field must still resolve.
2058    #[test]
2059    fn degenerate_context_values_do_not_panic() {
2060        let hostile = [
2061            (f32::NAN, f32::NAN),
2062            (0.0, 0.0),
2063            (-0.0, -1.0),
2064            (f32::INFINITY, f32::NEG_INFINITY),
2065            (f32::MAX, f32::MIN),
2066            (f32::MIN_POSITIVE, f32::EPSILON),
2067        ];
2068
2069        for (w, h) in hostile {
2070            let c = DynamicSelectorContext {
2071                os: OsCondition::MacOS,
2072                theme: ThemeCondition::Dark,
2073                de_version: u32::MAX,
2074                viewport_width: w,
2075                viewport_height: h,
2076                container_width: h,
2077                container_height: w,
2078                language: AzString::from(""),
2079                ..DynamicSelectorContext::default()
2080            };
2081            let r = evaluate_ua_scrollbar_css(&c);
2082            // macOS/dark rules are OS+theme-only, so viewport garbage cannot
2083            // perturb them.
2084            assert_eq!(r.width, LayoutScrollbarWidth::Thin, "viewport {w}x{h}");
2085            assert_eq!(r.fade_delay.ms, 500, "viewport {w}x{h}");
2086            assert!(matches!(r.color, StyleScrollbarColor::Custom(_)), "viewport {w}x{h}");
2087        }
2088    }
2089
2090    #[test]
2091    fn evaluate_is_deterministic() {
2092        for os in all_os() {
2093            for theme in all_themes() {
2094                let c = ctx(os, theme.clone());
2095                let a = evaluate_ua_scrollbar_css(&c);
2096                let b = evaluate_ua_scrollbar_css(&c);
2097                assert_eq!(a.color, b.color, "{os:?}/{theme:?}");
2098                assert_eq!(a.width, b.width, "{os:?}/{theme:?}");
2099                assert_eq!(a.visibility, b.visibility, "{os:?}/{theme:?}");
2100                assert_eq!(a.fade_delay, b.fade_delay, "{os:?}/{theme:?}");
2101                assert_eq!(a.fade_duration, b.fade_duration, "{os:?}/{theme:?}");
2102            }
2103        }
2104    }
2105}