1
//! Types and methods used to describe the style of an application.
2
//!
3
//! This module defines the core CSS data model:
4
//!
5
//! - [`Css`] contains one or more [`Stylesheet`]s, each holding [`CssRuleBlock`]s.
6
//! - A [`CssRuleBlock`] pairs a [`CssPath`] (selector) with [`CssDeclaration`]s (properties).
7
//! - [`CssPropertyValue<T>`] wraps individual property values with CSS keywords
8
//!   (`auto`, `inherit`, `initial`, etc.).
9
//! - [`BoxOrStatic<T>`] is a smart-pointer enum for heap-allocated or static CSS values.
10
//! - [`NodeTypeTag`] enumerates all recognized HTML/SVG element types for selector matching.
11
use alloc::{string::String, vec::Vec};
12
use core::fmt;
13

            
14
use crate::{
15
    corety::OptionString,
16
    dynamic_selector::DynamicSelectorVec,
17
    props::property::{CssProperty, CssPropertyType},
18
    AzString,
19
};
20

            
21
/// Css stylesheet - contains a parsed CSS stylesheet in "rule blocks",
22
/// i.e. blocks of key-value pairs associated with a selector path.
23
///
24
/// Layer separation (UA / system / author / inline / runtime) is encoded
25
/// per-rule via `CssRuleBlock::priority`; see [`rule_priority`] for the
26
/// slot allocation. There is no separate `Stylesheet` wrapper — to merge
27
/// two CSS sources, concatenate their `rules` and re-sort.
28
#[derive(Debug, Default, PartialEq, Clone)]
29
#[repr(C)]
30
pub struct Css {
31
    /// All rule blocks, in source order. Sort by `(priority, specificity)`
32
    /// via `sort_by_specificity` to put them in cascade order.
33
    pub rules: CssRuleBlockVec,
34
    /// Every `@keyframes` block in this stylesheet, in source order. Web
35
    /// compatibility sugar: internally each named track compiles to an
36
    /// animation-function invocation (`animation` / `-azul-animation-in` /
37
    /// `-azul-animation-out` reference these by name).
38
    pub keyframes: KeyframesVec,
39
}
40

            
41
/// One stop of a `@keyframes` block (`from` = 0, `to` = 1000, `12.5%` = 125).
42
///
43
/// Permille rather than an f32 percentage so the type stays `Eq`-capable —
44
/// `Css` must remain `Eq`/`Ord` (`NodeData` carries it), and 0.1% resolution is
45
/// beyond anything a keyframe needs.
46
#[derive(Debug, Default, PartialEq, Eq, Clone)]
47
#[repr(C)]
48
pub struct KeyframeStop {
49
    /// 0..=1000 position of this stop.
50
    pub permille: u16,
51
    /// The property values at this stop.
52
    pub props: crate::props::property::CssPropertyVec,
53
}
54

            
55

            
56
/// A named `@keyframes` block: `@keyframes flyOutRight { from {..} to {..} }`.
57
#[derive(Debug, Default, PartialEq, Eq, Clone)]
58
#[repr(C)]
59
pub struct Keyframes {
60
    pub name: AzString,
61
    /// Sorted by `permille` ascending at parse time.
62
    pub stops: KeyframeStopVec,
63
}
64

            
65

            
66
crate::impl_vec!(
67
    KeyframeStop,
68
    KeyframeStopVec,
69
    KeyframeStopVecDestructor,
70
    KeyframeStopVecDestructorType,
71
    KeyframeStopVecSlice,
72
    OptionKeyframeStop
73
);
74
crate::impl_vec_mut!(KeyframeStop, KeyframeStopVec);
75
crate::impl_vec_debug!(KeyframeStop, KeyframeStopVec);
76
crate::impl_vec_clone!(KeyframeStop, KeyframeStopVec, KeyframeStopVecDestructor);
77
crate::impl_vec_partialeq!(KeyframeStop, KeyframeStopVec);
78
crate::impl_vec_eq!(KeyframeStop, KeyframeStopVec);
79
crate::impl_option!(
80
    KeyframeStop,
81
    OptionKeyframeStop,
82
    copy = false,
83
    [Debug, Clone, PartialEq, Eq]
84
);
85

            
86
crate::impl_vec!(
87
    Keyframes,
88
    KeyframesVec,
89
    KeyframesVecDestructor,
90
    KeyframesVecDestructorType,
91
    KeyframesVecSlice,
92
    OptionKeyframes
93
);
94
crate::impl_vec_mut!(Keyframes, KeyframesVec);
95
crate::impl_vec_debug!(Keyframes, KeyframesVec);
96
crate::impl_vec_clone!(Keyframes, KeyframesVec, KeyframesVecDestructor);
97
crate::impl_vec_partialeq!(Keyframes, KeyframesVec);
98
crate::impl_vec_eq!(Keyframes, KeyframesVec);
99
crate::impl_option!(
100
    Keyframes,
101
    OptionKeyframes,
102
    copy = false,
103
    [Debug, Clone, PartialEq, Eq]
104
);
105

            
106
impl_option!(
107
    Css,
108
    OptionCss,
109
    copy = false,
110
    [Debug, Clone, PartialEq, Eq, PartialOrd]
111
);
112

            
113
impl_vec!(Css, CssVec, CssVecDestructor, CssVecDestructorType, CssVecSlice, OptionCss);
114
impl_vec_mut!(Css, CssVec);
115
impl_vec_debug!(Css, CssVec);
116
impl_vec_partialord!(Css, CssVec);
117
impl_vec_clone!(Css, CssVec, CssVecDestructor);
118
impl_vec_partialeq!(Css, CssVec);
119

            
120
impl_vec!(CssRuleBlock, CssRuleBlockVec, CssRuleBlockVecDestructor, CssRuleBlockVecDestructorType, CssRuleBlockVecSlice, OptionCssRuleBlock);
121
impl_vec_mut!(CssRuleBlock, CssRuleBlockVec);
122
impl_vec_debug!(CssRuleBlock, CssRuleBlockVec);
123
impl_vec_partialord!(CssRuleBlock, CssRuleBlockVec);
124
impl_vec_clone!(CssRuleBlock, CssRuleBlockVec, CssRuleBlockVecDestructor);
125
impl_vec_partialeq!(CssRuleBlock, CssRuleBlockVec);
126

            
127
impl Css {
128
    /// The viewport-size thresholds (widths, heights, logical px) at which
129
    /// any `@media (min-/max-width/height)` rule in this stylesheet can flip.
130
    /// Sorted, deduplicated by bit pattern. Used by the engine's resize
131
    /// decision instead of a hardcoded breakpoint list.
132
    #[must_use]
133
109
    pub fn viewport_breakpoints(&self) -> (Vec<f32>, Vec<f32>) {
134
109
        let mut w = Vec::new();
135
109
        let mut h = Vec::new();
136
376
        for rule in self.rules.as_ref() {
137
376
            crate::dynamic_selector::collect_viewport_thresholds(
138
376
                rule.conditions.as_ref(),
139
376
                &mut w,
140
376
                &mut h,
141
376
            );
142
376
        }
143
110
        w.sort_by_key(|v| v.to_bits());
144
110
        w.dedup_by_key(|v| v.to_bits());
145
109
        h.sort_by_key(|v| v.to_bits());
146
109
        h.dedup_by_key(|v| v.to_bits());
147
109
        (w, h)
148
109
    }
149

            
150
66035
    #[must_use] pub fn is_empty(&self) -> bool {
151
66035
        self.rules.as_ref().is_empty()
152
66035
    }
153

            
154
10824
    #[must_use] pub fn new(rules: Vec<CssRuleBlock>) -> Self {
155
10824
        Self {
156
10824
            rules: rules.into(),
157
10824
            keyframes: KeyframesVec::from_const_slice(&[]),
158
10824
        }
159
10824
    }
160

            
161
    #[cfg(feature = "parser")]
162
    // takes the owned C-ABI `AzString` by value by FFI ownership-transfer convention,
163
    // even though only a string slice is read here.
164
    #[allow(clippy::needless_pass_by_value)]
165
6108
    #[must_use] pub fn from_string(s: AzString) -> Self {
166
6108
        crate::parser2::new_from_str(s.as_str()).0
167
6108
    }
168

            
169
    /// Parse inline-style CSS (bare properties, pseudo blocks, @-rule blocks)
170
    /// and return a `Css` whose rules carry `rule_priority::INLINE`.
171
    ///
172
    /// Wraps the input in `* { ... }` so the main CSS parser can handle bare
173
    /// properties at the top level. Pseudo and at-rule blocks like
174
    /// `:hover { color: red; }` or `@os(linux) { font-size: 14px; }` work
175
    /// directly via CSS nesting.
176
    #[cfg(feature = "parser")]
177
42515
    #[must_use] pub fn parse_inline(style: &str) -> Self {
178
        use alloc::string::ToString;
179
42515
        let mut wrapped = String::with_capacity(style.len() + 6);
180
42515
        wrapped.push_str("* {\n");
181
42515
        wrapped.push_str(style);
182
42515
        wrapped.push_str("\n}");
183
42515
        let (mut css, _warnings) = crate::parser2::new_from_str(&wrapped);
184
        // A `}` in `style` closes the `* {` wrapper early, so the remainder is parsed as
185
        // a free-standing rule with a caller-controlled selector (selector injection).
186
        // Every rule an inline style produces MUST stay rooted at the `*` wrapper, so
187
        // drop any that isn't Global-rooted. Legitimate pseudo/at-rule nesting stays a
188
        // child of `*` (still Global-rooted per push_front_scope), so it is kept.
189
58799
        css.rules.retain(|rule| {
190
1
            matches!(
191
58419
                rule.path.selectors.as_ref().first(),
192
                None | Some(CssPathSelector::Global)
193
            )
194
58419
        });
195
58799
        for rule in css.rules.as_mut() {
196
58418
            rule.priority = rule_priority::INLINE;
197
58418
        }
198
42515
        css
199
42515
    }
200

            
201
    #[cfg(feature = "parser")]
202
    // takes the owned C-ABI `AzString` by value by FFI ownership-transfer convention,
203
    // even though only a string slice is read here.
204
    #[allow(clippy::needless_pass_by_value)]
205
22
    #[must_use] pub fn from_string_with_warnings(
206
22
        s: AzString,
207
22
    ) -> (Self, Vec<crate::parser2::CssParseWarnMsgOwned>) {
208
22
        let (css, warnings) = crate::parser2::new_from_str(s.as_str());
209
        (
210
22
            css,
211
22
            warnings
212
22
                .into_iter()
213
22
                .map(|w| crate::parser2::CssParseWarnMsgOwned {
214
3
                    warning: w.warning.to_contained(),
215
3
                    location: w.location,
216
3
                })
217
22
                .collect(),
218
        )
219
22
    }
220
}
221

            
222
impl From<Vec<CssRuleBlock>> for Css {
223
1
    fn from(rules: Vec<CssRuleBlock>) -> Self {
224
1
        Self {
225
1
            rules: rules.into(),
226
1
            keyframes: KeyframesVec::from_const_slice(&[]),
227
1
        }
228
1
    }
229
}
230

            
231
// NodeData derives Eq + Ord and carries `Css` as its inline style. Provide
232
// length-based ordering so the derives keep working — the same pattern the
233
// previous `CssPropertyWithConditionsVec` used.
234
impl Eq for Css {}
235
// PartialOrd delegates to the length-based Ord so the two agree (the derived
236
// field-wise PartialOrd diverged from this manual Ord).
237
impl PartialOrd for Css {
238
1
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
239
1
        Some(self.cmp(other))
240
1
    }
241
}
242
impl Ord for Css {
243
4
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
244
4
        self.rules.as_ref().len().cmp(&other.rules.as_ref().len())
245
4
    }
246
}
247
impl Eq for CssRuleBlock {}
248
impl Ord for CssRuleBlock {
249
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
250
        // Match the existing PartialOrd: path first, then declarations.
251
        // Priority is intentionally not in the sort key — it's a layer label,
252
        // not a comparison primitive for callers.
253
        self.path.cmp(&other.path).then_with(|| self.declarations.cmp(&other.declarations))
254
    }
255
}
256

            
257
/// Convert a flat list of `CssPropertyWithConditions` (the legacy inline-CSS form)
258
/// into a `Css`. Each property becomes a single-declaration `CssRuleBlock` with
259
/// `priority = INLINE`, an empty path (implicitly `:scope` — applies to the node it
260
/// lives on), and the original conditions intact.
261
///
262
/// This bridge lets widget code that built `&[CssPropertyWithConditions]` arrays
263
/// keep working through `.into()` while the storage on `NodeData` is the unified
264
/// `Css` type.
265
impl From<crate::dynamic_selector::CssPropertyWithConditionsVec> for Css {
266
1060971
    fn from(props: crate::dynamic_selector::CssPropertyWithConditionsVec) -> Self {
267
        // Build via an explicit push loop rather than `.into_iter().map(|p| CssRuleBlock {
268
        // declarations: vec![...], ... }).collect()`. On the web/remill lift, constructing a
269
        // complex struct with nested Vecs *inside* a mapped+collected closure drops every
270
        // element (AzButton's inline container style came back with 0 rules even though the
271
        // source Vec had props), whereas the identical construction in a plain loop body lifts
272
        // correctly — same pattern `NodeData::add_css_property` already relies on. Native
273
        // behavior is byte-identical.
274
1060971
        let owned = props.into_library_owned_vec();
275
1060971
        let mut rules: Vec<CssRuleBlock> = Vec::with_capacity(owned.len());
276
10935810
        for p in owned {
277
9874839
            rules.push(CssRuleBlock {
278
9874839
                path: CssPath { selectors: Vec::new().into() },
279
9874839
                declarations: alloc::vec![CssDeclaration::Static(p.property)].into(),
280
9874839
                conditions: p.apply_if,
281
9874839
                priority: rule_priority::INLINE,
282
9874839
            });
283
9874839
        }
284
1060971
        Self { rules: rules.into(), keyframes: KeyframesVec::from_const_slice(&[]) }
285
1060971
    }
286
}
287

            
288
/// Contains one parsed `key: value` pair, static or dynamic
289
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
290
#[repr(C, u8)]
291
pub enum CssDeclaration {
292
    /// Static key-value pair, such as `width: 500px`
293
    Static(CssProperty),
294
    /// Dynamic key-value pair with default value, such as `width: [[ my_id | 500px ]]`
295
    Dynamic(DynamicCssProperty),
296
}
297

            
298
impl_option!(
299
    CssDeclaration,
300
    OptionCssDeclaration,
301
    copy = false,
302
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
303
);
304

            
305
impl CssDeclaration {
306
13
    #[must_use] pub const fn new_static(prop: CssProperty) -> Self {
307
13
        Self::Static(prop)
308
13
    }
309

            
310
14
    #[must_use] pub const fn new_dynamic(prop: DynamicCssProperty) -> Self {
311
14
        Self::Dynamic(prop)
312
14
    }
313

            
314
    /// Returns the type of the property (i.e. the CSS key as a typed enum)
315
3
    #[must_use] pub const fn get_type(&self) -> CssPropertyType {
316
        use self::CssDeclaration::{Static, Dynamic};
317
3
        match self {
318
2
            Static(s) => s.get_type(),
319
1
            Dynamic(d) => d.default_value.get_type(),
320
        }
321
3
    }
322

            
323
    /// Determines if the property will be inherited (applied to the children)
324
    /// during the recursive application of the style on the DOM tree
325
4
    #[must_use] pub const fn is_inheritable(&self) -> bool {
326
        use self::CssDeclaration::{Static, Dynamic};
327
4
        match self {
328
2
            Static(s) => s.get_type().is_inheritable(),
329
2
            Dynamic(d) => d.is_inheritable(),
330
        }
331
4
    }
332

            
333
    /// Returns whether this rule affects only styling properties or layout
334
    /// properties (that could trigger a re-layout)
335
4
    #[must_use] pub const fn can_trigger_relayout(&self) -> bool {
336
        use self::CssDeclaration::{Static, Dynamic};
337
4
        match self {
338
2
            Static(s) => s.get_type().can_trigger_relayout(),
339
2
            Dynamic(d) => d.can_trigger_relayout(),
340
        }
341
4
    }
342

            
343
17
    #[must_use] pub fn to_str(&self) -> String {
344
        use self::CssDeclaration::{Static, Dynamic};
345
17
        match self {
346
8
            Static(s) => format!("{s:?}"),
347
9
            Dynamic(d) => format!("var(--{}, {:?})", d.dynamic_id, d.default_value),
348
        }
349
17
    }
350
}
351

            
352
/// A `DynamicCssProperty` is a type of css property that can be changed on possibly
353
/// every frame by the Rust code - for example to implement an `On::Hover` behaviour.
354
///
355
/// The syntax for such a property looks like this:
356
///
357
/// ```no_run,ignore
358
/// #my_div {
359
///    padding: var(--my_dynamic_property_id, 400px);
360
/// }
361
/// ```
362
///
363
/// Azul will register a dynamic property with the key "`my_dynamic_property_id`"
364
/// and the default value of 400px. If the property gets overridden during one frame,
365
/// the overridden property takes precedence.
366
///
367
/// At runtime the style is immutable (which is a performance optimization - if we
368
/// can assume that the property never changes at runtime), we can do some optimizations on it.
369
/// Dynamic style properties can also be used for animations and conditional styles
370
/// (i.e. `hover`, `focus`, etc.), thereby leading to cleaner code, since all of these
371
/// special cases now use one single API.
372
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
373
#[repr(C)]
374
pub struct DynamicCssProperty {
375
    /// The stringified ID of this property, i.e. the `"my_id"` in `width: var(--my_id, 500px)`.
376
    pub dynamic_id: AzString,
377
    /// Default values for this properties - one single value can control multiple properties!
378
    pub default_value: CssProperty,
379
}
380

            
381
/// A value that is either heap-allocated (parsed at runtime) or a compile-time
382
/// static reference.
383
///
384
/// Used to reduce enum size for large CSS property payloads
385
/// by storing them behind a pointer instead of inline.
386
///
387
/// - Size: 1 (tag) + 7 (padding) + 8 (pointer) = **16 bytes** on 64-bit
388
/// - `Static` variant: no allocation, just a `*const T` pointer to static data
389
/// - `Boxed` variant: heap-allocated via `Box::into_raw`, freed on Drop
390
#[repr(C, u8)]
391
pub enum BoxOrStatic<T> {
392
    /// Heap-allocated (parsed at runtime). Owned — freed on Drop.
393
    Boxed(*mut T),
394
    /// Compile-time constant (e.g. from `const` CSS defaults). Not freed.
395
    Static(*const T),
396
}
397

            
398
impl<T> BoxOrStatic<T> {
399
    /// Allocate `value` on the heap and return a `Boxed` variant.
400
    #[inline]
401
294032
    pub fn heap(value: T) -> Self {
402
294032
        Self::Boxed(Box::into_raw(Box::new(value)))
403
294032
    }
404

            
405
    /// Return a reference to the inner value.
406
    ///
407
    /// # Safety invariant
408
    /// The inner pointer must be non-null. This is guaranteed by [`heap`](Self::heap)
409
    /// and the `Static` constructor (which should always point to valid data).
410
    #[inline]
411
1070885
    #[must_use] pub fn as_ref(&self) -> &T {
412
1070885
        match self {
413
1070518
            Self::Boxed(ptr) => unsafe {
414
1070518
                debug_assert!(!ptr.is_null(), "BoxOrStatic::Boxed contained a null pointer");
415
1070518
                &**ptr
416
            },
417
367
            Self::Static(ptr) => unsafe {
418
367
                debug_assert!(!ptr.is_null(), "BoxOrStatic::Static contained a null pointer");
419
367
                &**ptr
420
            },
421
        }
422
1070885
    }
423

            
424
    /// Return a mutable reference to the inner value (only for Boxed).
425
    ///
426
    /// # Panics
427
    ///
428
    /// Panics if called on a `Static` variant: static values are immutable
429
    /// and cannot hand out a `&mut`.
430
    #[inline]
431
3
    pub fn as_mut(&mut self) -> &mut T {
432
3
        match self {
433
2
            Self::Boxed(ptr) => unsafe { &mut **ptr },
434
1
            Self::Static(_) => panic!("Cannot mutate a static BoxOrStatic value"),
435
        }
436
2
    }
437

            
438
    /// Consume self and return the inner value.
439
    #[inline]
440
4
    #[must_use] pub fn into_inner(self) -> T where T: Clone {
441
        // Clone the inner value, then let `self` drop normally so `Drop` frees the
442
        // heap box (for the Boxed variant). The old `mem::forget(self)` LEAKED that
443
        // box on every call — the clone is an independent value, so there is no
444
        // double-free to guard against.
445
4
        self.as_ref().clone()
446
4
    }
447
}
448

            
449
impl<T> Drop for BoxOrStatic<T> {
450
795588
    fn drop(&mut self) {
451
795588
        if let Self::Boxed(ptr) = self {
452
772012
            if !ptr.is_null() {
453
772012
                unsafe { drop(Box::from_raw(*ptr)); }
454
772012
                *ptr = core::ptr::null_mut();
455
772012
            }
456
23576
        }
457
795588
    }
458
}
459

            
460
impl<T: Clone> Clone for BoxOrStatic<T> {
461
20026
    fn clone(&self) -> Self {
462
20026
        match self {
463
3890
            Self::Boxed(ptr) => {
464
3890
                let val = unsafe { &**ptr }.clone();
465
3890
                Self::Boxed(Box::into_raw(Box::new(val)))
466
            }
467
16136
            Self::Static(ptr) => Self::Static(*ptr),
468
        }
469
20026
    }
470
}
471

            
472
impl<T: fmt::Debug> fmt::Debug for BoxOrStatic<T> {
473
148318
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474
148318
        self.as_ref().fmt(f)
475
148318
    }
476
}
477

            
478
impl<T: fmt::Display> fmt::Display for BoxOrStatic<T> {
479
27192
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480
27192
        self.as_ref().fmt(f)
481
27192
    }
482
}
483

            
484
impl<T: PartialEq> PartialEq for BoxOrStatic<T> {
485
26094
    fn eq(&self, other: &Self) -> bool {
486
26094
        self.as_ref() == other.as_ref()
487
26094
    }
488
}
489

            
490
impl<T: Eq> Eq for BoxOrStatic<T> {}
491

            
492
impl<T: core::hash::Hash> core::hash::Hash for BoxOrStatic<T> {
493
177461
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
494
177461
        self.as_ref().hash(state);
495
177461
    }
496
}
497

            
498
impl<T: PartialOrd> PartialOrd for BoxOrStatic<T> {
499
2
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
500
2
        self.as_ref().partial_cmp(other.as_ref())
501
2
    }
502
}
503

            
504
impl<T: Ord> Ord for BoxOrStatic<T> {
505
1
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
506
1
        self.as_ref().cmp(other.as_ref())
507
1
    }
508
}
509

            
510
impl<T> core::ops::Deref for BoxOrStatic<T> {
511
    type Target = T;
512
    #[inline]
513
646883
    fn deref(&self) -> &T {
514
646883
        self.as_ref()
515
646883
    }
516
}
517

            
518
impl<T: Default> Default for BoxOrStatic<T> {
519
2
    fn default() -> Self {
520
2
        Self::heap(T::default())
521
2
    }
522
}
523

            
524
impl<T: PrintAsCssValue> PrintAsCssValue for BoxOrStatic<T> {
525
    fn print_as_css_value(&self) -> String {
526
        self.as_ref().print_as_css_value()
527
    }
528
}
529

            
530
// Safety: BoxOrStatic<T> is Send if T is Send
531
unsafe impl<T: Send + 'static> Send for BoxOrStatic<T> {}
532
// Safety: BoxOrStatic<T> is Sync if T is Sync
533
unsafe impl<T: Sync + 'static> Sync for BoxOrStatic<T> {}
534

            
535
/// Type alias: `BoxOrStatic<StyleBoxShadow>` — used by codegen for FFI monomorphization.
536
pub type BoxOrStaticStyleBoxShadow = BoxOrStatic<crate::props::style::box_shadow::StyleBoxShadow>;
537

            
538
/// Type alias: `BoxOrStatic<AzString>` — used by `NodeType::Text` and `NodeType::Icon`.
539
pub type BoxOrStaticString = BoxOrStatic<AzString>;
540

            
541
/// A CSS property value that may be an explicit value or a CSS-wide keyword
542
/// (`auto`, `none`, `initial`, `inherit`, `revert`, `unset`).
543
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
544
#[repr(C, u8)] // necessary for ABI stability
545
pub enum CssPropertyValue<T> {
546
    Auto,
547
    None,
548
    Initial,
549
    Inherit,
550
    Revert,
551
    Unset,
552
    Exact(T),
553
}
554

            
555
/// Trait for types that can format themselves as a CSS property value string.
556
pub trait PrintAsCssValue {
557
    fn print_as_css_value(&self) -> String;
558
}
559

            
560
impl<T: PrintAsCssValue> CssPropertyValue<T> {
561
3514
    pub fn get_css_value_fmt(&self) -> String {
562
3514
        match self {
563
559
            Self::Auto => "auto".to_string(),
564
559
            Self::None => "none".to_string(),
565
825
            Self::Initial => "initial".to_string(),
566
745
            Self::Inherit => "inherit".to_string(),
567
1
            Self::Revert => "revert".to_string(),
568
1
            Self::Unset => "unset".to_string(),
569
824
            Self::Exact(e) => e.print_as_css_value(),
570
        }
571
3514
    }
572
}
573

            
574
impl<T: fmt::Display> fmt::Display for CssPropertyValue<T> {
575
26
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576
        use self::CssPropertyValue::{Auto, None, Initial, Inherit, Revert, Unset, Exact};
577
26
        match self {
578
3
            Auto => write!(f, "auto"),
579
3
            None => write!(f, "none"),
580
3
            Initial => write!(f, "initial"),
581
3
            Inherit => write!(f, "inherit"),
582
3
            Revert => write!(f, "revert"),
583
3
            Unset => write!(f, "unset"),
584
8
            Exact(e) => write!(f, "{e}"),
585
        }
586
26
    }
587
}
588

            
589
impl<T> From<T> for CssPropertyValue<T> {
590
483647
    fn from(c: T) -> Self {
591
483647
        Self::Exact(c)
592
483647
    }
593
}
594

            
595
impl<T> CssPropertyValue<T> {
596
    /// Transforms a `CssPropertyValue<T>` into a `CssPropertyValue<U>` by applying a mapping
597
    /// function
598
    #[inline]
599
9
    pub fn map_property<F: Fn(T) -> U, U>(self, map_fn: F) -> CssPropertyValue<U> {
600
9
        match self {
601
3
            Self::Exact(c) => CssPropertyValue::Exact(map_fn(c)),
602
1
            Self::Auto => CssPropertyValue::Auto,
603
1
            Self::None => CssPropertyValue::None,
604
1
            Self::Initial => CssPropertyValue::Initial,
605
1
            Self::Inherit => CssPropertyValue::Inherit,
606
1
            Self::Revert => CssPropertyValue::Revert,
607
1
            Self::Unset => CssPropertyValue::Unset,
608
        }
609
9
    }
610

            
611
    #[inline]
612
15313398
    pub const fn get_property(&self) -> Option<&T> {
613
15313398
        match self {
614
15313206
            Self::Exact(c) => Some(c),
615
192
            _ => None,
616
        }
617
15313398
    }
618

            
619
    #[inline]
620
7
    pub fn get_property_owned(self) -> Option<T> {
621
7
        match self {
622
1
            Self::Exact(c) => Some(c),
623
6
            _ => None,
624
        }
625
7
    }
626

            
627
    #[inline]
628
115689
    pub const fn is_auto(&self) -> bool {
629
115689
        matches!(self, Self::Auto)
630
115689
    }
631

            
632
    #[inline]
633
9
    pub const fn is_none(&self) -> bool {
634
9
        matches!(self, Self::None)
635
9
    }
636

            
637
    #[inline]
638
3940
    pub const fn is_initial(&self) -> bool {
639
3940
        matches!(self, Self::Initial)
640
3940
    }
641

            
642
    #[inline]
643
11
    pub const fn is_inherit(&self) -> bool {
644
11
        matches!(self, Self::Inherit)
645
11
    }
646

            
647
    #[inline]
648
10
    pub const fn is_revert(&self) -> bool {
649
10
        matches!(self, Self::Revert)
650
10
    }
651

            
652
    #[inline]
653
12
    pub const fn is_unset(&self) -> bool {
654
12
        matches!(self, Self::Unset)
655
12
    }
656
}
657

            
658
impl<T: Default> CssPropertyValue<T> {
659
    #[inline]
660
557133
    pub fn get_property_or_default(self) -> Option<T> {
661
557133
        match self {
662
14
            Self::Auto | Self::Initial => Some(T::default()),
663
557084
            Self::Exact(c) => Some(c),
664
8
            Self::None
665
15
            | Self::Inherit
666
6
            | Self::Revert
667
35
            | Self::Unset => None,
668
        }
669
557133
    }
670
}
671

            
672
impl<T: Default> Default for CssPropertyValue<T> {
673
    #[inline]
674
2
    fn default() -> Self {
675
2
        Self::Exact(T::default())
676
2
    }
677
}
678

            
679
impl DynamicCssProperty {
680
7
    #[must_use] pub const fn is_inheritable(&self) -> bool {
681
        // Dynamic style properties should not be inheritable,
682
        // since that could lead to bugs - you set a property in Rust, suddenly
683
        // the wrong UI component starts to react because it was inherited.
684
7
        false
685
7
    }
686

            
687
6
    #[must_use] pub const fn can_trigger_relayout(&self) -> bool {
688
6
        self.default_value.get_type().can_trigger_relayout()
689
6
    }
690
}
691

            
692
/// Layer priority for `CssRuleBlock`. Lower numbers cascade first;
693
/// higher numbers override earlier layers at the same specificity.
694
///
695
/// `u8` leaves 256 slots, so a new layer can be inserted between any
696
/// two existing slots without renumbering consumers. The gaps between
697
/// named slots are intentional — fill them with custom intermediate
698
/// layers if/when `@layer` lands.
699
pub mod rule_priority {
700
    /// User-Agent / framework defaults. Widget code that emits its
701
    /// own default CSS uses this. Lowest priority — anything else
702
    /// overrides it.
703
    pub const UA: u8 = 0;
704

            
705
    /// Stylesheets the host system reports (system fonts, theme CSS
706
    /// derived from `SystemStyle`). One step above UA so they win
707
    /// against framework defaults but lose against anything the app
708
    /// author writes.
709
    pub const SYSTEM: u8 = 10;
710

            
711
    /// Default for parser-produced rules: the app author's CSS.
712
    /// Everything coming out of `Css::from_string` lives here.
713
    pub const AUTHOR: u8 = 20;
714

            
715
    /// Inline `style="..."` / `NodeData::set_css(...)` rules — used
716
    /// once the inline-vs-component unification (separate plan) folds
717
    /// inline storage into the same Vec.
718
    pub const INLINE: u8 = 30;
719

            
720
    /// Reserved for direct-rule runtime overrides.
721
    ///
722
    /// Today the
723
    /// `prop_cache` handles runtime overrides via
724
    /// `user_overridden_properties`; this slot is reserved so a
725
    /// future "push a `CssRuleBlock` at runtime" path stays above
726
    /// inline. Used only when a callback writes a full rule, not a
727
    /// single property.
728
    pub const RUNTIME: u8 = 50;
729
}
730

            
731
/// One block of rules that applies a bunch of rules to a "path" in the style, i.e.
732
/// `div#myid.myclass -> { ("justify-content", "center") }`
733
///
734
/// The `conditions` field contains @media/@lang/etc. conditions that must ALL be
735
/// satisfied for this rule block to apply (from enclosing @-rule blocks).
736
#[derive(Debug, Default, Clone, PartialEq)]
737
#[repr(C)]
738
pub struct CssRuleBlock {
739
    /// The css path (full selector) of the style ruleset
740
    pub path: CssPath,
741
    /// `"justify-content: center"` =>
742
    /// `CssDeclaration::Static(CssProperty::JustifyContent(LayoutJustifyContent::Center))`
743
    pub declarations: CssDeclarationVec,
744
    /// Conditions from enclosing @-rules (@media, @lang, etc.) that must ALL be
745
    /// satisfied for this rule block to apply. Empty = unconditional.
746
    pub conditions: DynamicSelectorVec,
747
    /// Layer priority. See [`rule_priority`] for slot allocation.
748
    /// `0` = UA / framework, `20` = author CSS (default), higher = wins.
749
    /// Sort key combined with selector specificity in `sort_by_specificity`.
750
    pub priority: u8,
751
}
752

            
753
impl_option!(
754
    CssRuleBlock,
755
    OptionCssRuleBlock,
756
    copy = false,
757
    [Debug, Clone, PartialEq, Eq, PartialOrd]
758
);
759

            
760
impl PartialOrd for CssRuleBlock {
761
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
762
        // Compare by path and declarations only, conditions are not ordered
763
        match self.path.partial_cmp(&other.path) {
764
            Some(core::cmp::Ordering::Equal) => self.declarations.partial_cmp(&other.declarations),
765
            ord => ord,
766
        }
767
    }
768
}
769

            
770
impl_vec!(CssDeclaration, CssDeclarationVec, CssDeclarationVecDestructor, CssDeclarationVecDestructorType, CssDeclarationVecSlice, OptionCssDeclaration);
771
impl_vec_mut!(CssDeclaration, CssDeclarationVec);
772
impl_vec_debug!(CssDeclaration, CssDeclarationVec);
773
impl_vec_partialord!(CssDeclaration, CssDeclarationVec);
774
impl_vec_ord!(CssDeclaration, CssDeclarationVec);
775
impl_vec_clone!(
776
    CssDeclaration,
777
    CssDeclarationVec,
778
    CssDeclarationVecDestructor
779
);
780
impl_vec_partialeq!(CssDeclaration, CssDeclarationVec);
781
impl_vec_eq!(CssDeclaration, CssDeclarationVec);
782
impl_vec_hash!(CssDeclaration, CssDeclarationVec);
783

            
784
impl CssRuleBlock {
785
10019
    #[must_use] pub fn new(path: CssPath, declarations: Vec<CssDeclaration>) -> Self {
786
10019
        Self {
787
10019
            path,
788
10019
            declarations: declarations.into(),
789
10019
            conditions: DynamicSelectorVec::from_const_slice(&[]),
790
10019
            priority: rule_priority::AUTHOR,
791
10019
        }
792
10019
    }
793

            
794
2
    #[must_use] pub fn with_conditions(
795
2
        path: CssPath,
796
2
        declarations: Vec<CssDeclaration>,
797
2
        conditions: Vec<crate::dynamic_selector::DynamicSelector>,
798
2
    ) -> Self {
799
2
        Self {
800
2
            path,
801
2
            declarations: declarations.into(),
802
2
            conditions: conditions.into(),
803
2
            priority: rule_priority::AUTHOR,
804
2
        }
805
2
    }
806
}
807

            
808
/// A group of CSS path selectors, used during selector matching.
809
pub type CssContentGroup<'a> = Vec<&'a CssPathSelector>;
810

            
811
/// Signifies the type of a DOM node without carrying any associated data
812
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
813
#[repr(C)]
814
pub enum NodeTypeTag {
815
    // Document structure
816
    Html,
817
    Head,
818
    Body,
819

            
820
    // Block-level elements
821
    Div,
822
    P,
823
    Article,
824
    Section,
825
    Nav,
826
    Aside,
827
    Header,
828
    Footer,
829
    Main,
830
    Figure,
831
    FigCaption,
832

            
833
    // Headings
834
    H1,
835
    H2,
836
    H3,
837
    H4,
838
    H5,
839
    H6,
840

            
841
    // Inline text
842
    Br,
843
    Hr,
844
    Pre,
845
    BlockQuote,
846
    Address,
847
    Details,
848
    Summary,
849
    Dialog,
850

            
851
    // Lists
852
    Ul,
853
    Ol,
854
    Li,
855
    Dl,
856
    Dt,
857
    Dd,
858
    Menu,
859
    MenuItem,
860
    Dir,
861

            
862
    // Tables
863
    Table,
864
    Caption,
865
    THead,
866
    TBody,
867
    TFoot,
868
    Tr,
869
    Th,
870
    Td,
871
    ColGroup,
872
    Col,
873

            
874
    // Forms
875
    Form,
876
    FieldSet,
877
    Legend,
878
    Label,
879
    Input,
880
    Button,
881
    Select,
882
    OptGroup,
883
    SelectOption,
884
    TextArea,
885
    Output,
886
    Progress,
887
    Meter,
888
    DataList,
889

            
890
    // Inline elements
891
    Span,
892
    A,
893
    Em,
894
    Strong,
895
    B,
896
    I,
897
    U,
898
    S,
899
    Mark,
900
    Del,
901
    Ins,
902
    Code,
903
    Samp,
904
    Kbd,
905
    Var,
906
    Cite,
907
    Dfn,
908
    Abbr,
909
    Acronym,
910
    Q,
911
    Time,
912
    Sub,
913
    Sup,
914
    Small,
915
    Big,
916
    Bdo,
917
    Bdi,
918
    Wbr,
919
    Ruby,
920
    Rt,
921
    Rtc,
922
    Rp,
923
    Data,
924

            
925
    // Embedded content
926
    Canvas,
927
    Object,
928
    Param,
929
    Embed,
930
    Audio,
931
    Video,
932
    Source,
933
    Track,
934
    Map,
935
    Area,
936
    Svg,
937
    /// SVG `<path>` element.
938
    SvgPath,
939
    /// SVG `<circle>` element.
940
    SvgCircle,
941
    /// SVG `<rect>` element.
942
    SvgRect,
943
    /// SVG `<ellipse>` element.
944
    SvgEllipse,
945
    /// SVG `<line>` element.
946
    SvgLine,
947
    /// SVG `<polygon>` element.
948
    SvgPolygon,
949
    /// SVG `<polyline>` element.
950
    SvgPolyline,
951
    /// SVG `<g>` group element.
952
    SvgG,
953

            
954
    // SVG container elements
955
    /// SVG `<defs>` element.
956
    SvgDefs,
957
    /// SVG `<symbol>` element.
958
    SvgSymbol,
959
    /// SVG `<use>` element.
960
    SvgUse,
961
    /// SVG `<switch>` element.
962
    SvgSwitch,
963

            
964
    // SVG text elements
965
    /// SVG `<text>` element.
966
    SvgText,
967
    /// SVG `<tspan>` element.
968
    SvgTspan,
969
    /// SVG `<textPath>` element.
970
    SvgTextPath,
971

            
972
    // SVG paint server elements
973
    /// SVG `<linearGradient>` element.
974
    SvgLinearGradient,
975
    /// SVG `<radialGradient>` element.
976
    SvgRadialGradient,
977
    /// SVG `<stop>` element.
978
    SvgStop,
979
    /// SVG `<pattern>` element.
980
    SvgPattern,
981

            
982
    // SVG clipping/masking elements
983
    /// SVG `<clipPath>` element.
984
    SvgClipPathElement,
985
    /// SVG `<mask>` element.
986
    SvgMask,
987

            
988
    // SVG filter elements
989
    /// SVG `<filter>` element.
990
    SvgFilter,
991
    /// SVG `<feBlend>` element.
992
    SvgFeBlend,
993
    /// SVG `<feColorMatrix>` element.
994
    SvgFeColorMatrix,
995
    /// SVG `<feComponentTransfer>` element.
996
    SvgFeComponentTransfer,
997
    /// SVG `<feComposite>` element.
998
    SvgFeComposite,
999
    /// SVG `<feConvolveMatrix>` element.
    SvgFeConvolveMatrix,
    /// SVG `<feDiffuseLighting>` element.
    SvgFeDiffuseLighting,
    /// SVG `<feDisplacementMap>` element.
    SvgFeDisplacementMap,
    /// SVG `<feDistantLight>` element.
    SvgFeDistantLight,
    /// SVG `<feDropShadow>` element.
    SvgFeDropShadow,
    /// SVG `<feFlood>` element.
    SvgFeFlood,
    /// SVG `<feFuncR>` element.
    SvgFeFuncR,
    /// SVG `<feFuncG>` element.
    SvgFeFuncG,
    /// SVG `<feFuncB>` element.
    SvgFeFuncB,
    /// SVG `<feFuncA>` element.
    SvgFeFuncA,
    /// SVG `<feGaussianBlur>` element.
    SvgFeGaussianBlur,
    /// SVG `<feImage>` element.
    SvgFeImage,
    /// SVG `<feMerge>` element.
    SvgFeMerge,
    /// SVG `<feMergeNode>` element.
    SvgFeMergeNode,
    /// SVG `<feMorphology>` element.
    SvgFeMorphology,
    /// SVG `<feOffset>` element.
    SvgFeOffset,
    /// SVG `<fePointLight>` element.
    SvgFePointLight,
    /// SVG `<feSpecularLighting>` element.
    SvgFeSpecularLighting,
    /// SVG `<feSpotLight>` element.
    SvgFeSpotLight,
    /// SVG `<feTile>` element.
    SvgFeTile,
    /// SVG `<feTurbulence>` element.
    SvgFeTurbulence,
    // SVG marker/image elements
    /// SVG `<marker>` element.
    SvgMarker,
    /// SVG `<image>` element.
    SvgImage,
    /// SVG `<foreignObject>` element.
    SvgForeignObject,
    // SVG descriptive elements
    /// SVG `<title>` element.
    SvgTitle,
    /// SVG `<desc>` element.
    SvgDesc,
    /// SVG `<metadata>` element.
    SvgMetadata,
    /// SVG `<a>` element.
    SvgA,
    /// SVG `<view>` element.
    SvgView,
    /// SVG `<style>` element.
    SvgStyle,
    /// SVG `<script>` element.
    SvgScript,
    // SVG animation elements
    /// SVG `<animate>` element.
    SvgAnimate,
    /// SVG `<animateMotion>` element.
    SvgAnimateMotion,
    /// SVG `<animateTransform>` element.
    SvgAnimateTransform,
    /// SVG `<set>` element.
    SvgSet,
    /// SVG `<mpath>` element.
    SvgMpath,
    // Metadata
    Title,
    Meta,
    Link,
    Script,
    Style,
    Base,
    // Special
    Text,
    Img,
    VirtualView,
    /// Icon element - resolved to actual content by `IconProvider`
    Icon,
    /// Invisible probe — `NodeType::GeolocationProbe`. Zero-size in
    /// layout, skipped in the display list. CSS tag: `geolocation-probe`.
    GeolocationProbe,
    // Pseudo-elements
    Before,
    After,
    Marker,
    Placeholder,
    /// THE canonical page-break element (`<pagebreak/>` /
    /// `Dom::create_page_break()`): an empty block with UA
    /// `break-before: page`. CSS tag: `pagebreak`.
    PageBreak,
}
/// Error returned when a CSS tag name string cannot be mapped to a [`NodeTypeTag`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NodeTypeTagParseError<'a> {
    Invalid(&'a str),
}
impl fmt::Display for NodeTypeTagParseError<'_> {
18
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18
        match &self {
18
            NodeTypeTagParseError::Invalid(e) => write!(f, "Invalid node type: {e}"),
        }
18
    }
}
/// Owned version of [`NodeTypeTagParseError`] for storage across lifetime boundaries.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum NodeTypeTagParseErrorOwned {
    Invalid(AzString),
}
impl NodeTypeTagParseError<'_> {
9
    #[must_use] pub fn to_contained(&self) -> NodeTypeTagParseErrorOwned {
9
        match self {
9
            NodeTypeTagParseError::Invalid(s) => NodeTypeTagParseErrorOwned::Invalid((*s).to_string().into()),
        }
9
    }
}
impl NodeTypeTagParseErrorOwned {
16
    #[must_use] pub fn to_shared(&self) -> NodeTypeTagParseError<'_> {
16
        match self {
16
            Self::Invalid(s) => NodeTypeTagParseError::Invalid(s),
        }
16
    }
}
/// Parses the node type from a CSS string such as `"div"` => `NodeTypeTag::Div`
impl NodeTypeTag {
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
    /// # Errors
    ///
    /// Returns an error if `css_key` is not a recognized HTML node-type tag.
97560
    pub fn from_str(css_key: &str) -> Result<Self, NodeTypeTagParseError<'_>> {
97560
        match css_key {
            // Document structure
97560
            "html" => Ok(Self::Html),
95970
            "head" => Ok(Self::Head),
95961
            "body" => Ok(Self::Body),
            // Block-level elements
90683
            "div" => Ok(Self::Div),
4798
            "p" => Ok(Self::P),
3382
            "article" => Ok(Self::Article),
3373
            "section" => Ok(Self::Section),
3364
            "nav" => Ok(Self::Nav),
3355
            "aside" => Ok(Self::Aside),
3346
            "header" => Ok(Self::Header),
3337
            "footer" => Ok(Self::Footer),
3328
            "main" => Ok(Self::Main),
3319
            "figure" => Ok(Self::Figure),
3310
            "figcaption" => Ok(Self::FigCaption),
            // Headings
3301
            "h1" => Ok(Self::H1),
2613
            "h2" => Ok(Self::H2),
2429
            "h3" => Ok(Self::H3),
2389
            "h4" => Ok(Self::H4),
2380
            "h5" => Ok(Self::H5),
2371
            "h6" => Ok(Self::H6),
            // Inline text
2362
            "br" => Ok(Self::Br),
2353
            "hr" => Ok(Self::Hr),
2320
            "pre" => Ok(Self::Pre),
2239
            "blockquote" => Ok(Self::BlockQuote),
2206
            "address" => Ok(Self::Address),
2197
            "details" => Ok(Self::Details),
2188
            "summary" => Ok(Self::Summary),
2179
            "dialog" => Ok(Self::Dialog),
            // Lists
2170
            "ul" => Ok(Self::Ul),
1482
            "ol" => Ok(Self::Ol),
1442
            "li" => Ok(Self::Li),
1402
            "dl" => Ok(Self::Dl),
1393
            "dt" => Ok(Self::Dt),
1384
            "dd" => Ok(Self::Dd),
1375
            "menu" => Ok(Self::Menu),
1366
            "menuitem" => Ok(Self::MenuItem),
1357
            "dir" => Ok(Self::Dir),
            // Tables
1348
            "table" => Ok(Self::Table),
1320
            "caption" => Ok(Self::Caption),
1311
            "thead" => Ok(Self::THead),
1295
            "tbody" => Ok(Self::TBody),
1279
            "tfoot" => Ok(Self::TFoot),
1270
            "tr" => Ok(Self::Tr),
1254
            "th" => Ok(Self::Th),
1226
            "td" => Ok(Self::Td),
1210
            "colgroup" => Ok(Self::ColGroup),
1201
            "col" => Ok(Self::Col),
            // Forms
1192
            "form" => Ok(Self::Form),
1176
            "fieldset" => Ok(Self::FieldSet),
1167
            "legend" => Ok(Self::Legend),
1158
            "label" => Ok(Self::Label),
1149
            "input" => Ok(Self::Input),
1126
            "button" => Ok(Self::Button),
1110
            "select" => Ok(Self::Select),
1101
            "optgroup" => Ok(Self::OptGroup),
1092
            "option" => Ok(Self::SelectOption),
1083
            "textarea" => Ok(Self::TextArea),
1074
            "output" => Ok(Self::Output),
1065
            "progress" => Ok(Self::Progress),
1056
            "meter" => Ok(Self::Meter),
1047
            "datalist" => Ok(Self::DataList),
            // Inline elements
1038
            "span" => Ok(Self::Span),
950
            "a" => Ok(Self::A),
895
            "em" => Ok(Self::Em),
862
            "strong" => Ok(Self::Strong),
829
            "b" => Ok(Self::B),
819
            "i" => Ok(Self::I),
810
            "u" => Ok(Self::U),
801
            "s" => Ok(Self::S),
792
            "mark" => Ok(Self::Mark),
783
            "del" => Ok(Self::Del),
774
            "ins" => Ok(Self::Ins),
765
            "code" => Ok(Self::Code),
732
            "samp" => Ok(Self::Samp),
723
            "kbd" => Ok(Self::Kbd),
714
            "var" => Ok(Self::Var),
705
            "cite" => Ok(Self::Cite),
696
            "dfn" => Ok(Self::Dfn),
687
            "abbr" => Ok(Self::Abbr),
678
            "acronym" => Ok(Self::Acronym),
669
            "q" => Ok(Self::Q),
660
            "time" => Ok(Self::Time),
651
            "sub" => Ok(Self::Sub),
642
            "sup" => Ok(Self::Sup),
633
            "small" => Ok(Self::Small),
624
            "big" => Ok(Self::Big),
615
            "bdo" => Ok(Self::Bdo),
606
            "bdi" => Ok(Self::Bdi),
597
            "wbr" => Ok(Self::Wbr),
588
            "ruby" => Ok(Self::Ruby),
579
            "rt" => Ok(Self::Rt),
570
            "rtc" => Ok(Self::Rtc),
561
            "rp" => Ok(Self::Rp),
552
            "data" => Ok(Self::Data),
            // Embedded content
543
            "canvas" => Ok(Self::Canvas),
534
            "object" => Ok(Self::Object),
525
            "param" => Ok(Self::Param),
516
            "embed" => Ok(Self::Embed),
507
            "audio" => Ok(Self::Audio),
498
            "video" => Ok(Self::Video),
489
            "source" => Ok(Self::Source),
480
            "track" => Ok(Self::Track),
471
            "map" => Ok(Self::Map),
462
            "area" => Ok(Self::Area),
453
            "svg" => Ok(Self::Svg),
            // SVG shape elements
444
            "path" => Ok(Self::SvgPath),
442
            "circle" => Ok(Self::SvgCircle),
440
            "rect" => Ok(Self::SvgRect),
438
            "ellipse" => Ok(Self::SvgEllipse),
436
            "line" => Ok(Self::SvgLine),
434
            "polygon" => Ok(Self::SvgPolygon),
432
            "polyline" => Ok(Self::SvgPolyline),
430
            "g" => Ok(Self::SvgG),
            // SVG container elements
428
            "defs" => Ok(Self::SvgDefs),
426
            "symbol" => Ok(Self::SvgSymbol),
424
            "use" => Ok(Self::SvgUse),
422
            "switch" => Ok(Self::SvgSwitch),
            // SVG text elements
420
            "svg:text" => Ok(Self::SvgText),
418
            "tspan" => Ok(Self::SvgTspan),
416
            "textpath" => Ok(Self::SvgTextPath),
            // SVG paint server elements
414
            "lineargradient" => Ok(Self::SvgLinearGradient),
412
            "radialgradient" => Ok(Self::SvgRadialGradient),
410
            "stop" => Ok(Self::SvgStop),
408
            "pattern" => Ok(Self::SvgPattern),
            // SVG clipping/masking elements
406
            "clippath" => Ok(Self::SvgClipPathElement),
404
            "mask" => Ok(Self::SvgMask),
            // SVG filter elements
402
            "filter" => Ok(Self::SvgFilter),
400
            "feblend" => Ok(Self::SvgFeBlend),
398
            "fecolormatrix" => Ok(Self::SvgFeColorMatrix),
396
            "fecomponenttransfer" => Ok(Self::SvgFeComponentTransfer),
394
            "fecomposite" => Ok(Self::SvgFeComposite),
392
            "feconvolvematrix" => Ok(Self::SvgFeConvolveMatrix),
390
            "fediffuselighting" => Ok(Self::SvgFeDiffuseLighting),
388
            "fedisplacementmap" => Ok(Self::SvgFeDisplacementMap),
386
            "fedistantlight" => Ok(Self::SvgFeDistantLight),
384
            "fedropshadow" => Ok(Self::SvgFeDropShadow),
382
            "feflood" => Ok(Self::SvgFeFlood),
380
            "fefuncr" => Ok(Self::SvgFeFuncR),
378
            "fefuncg" => Ok(Self::SvgFeFuncG),
376
            "fefuncb" => Ok(Self::SvgFeFuncB),
374
            "fefunca" => Ok(Self::SvgFeFuncA),
372
            "fegaussianblur" => Ok(Self::SvgFeGaussianBlur),
370
            "feimage" => Ok(Self::SvgFeImage),
368
            "femerge" => Ok(Self::SvgFeMerge),
366
            "femergenode" => Ok(Self::SvgFeMergeNode),
364
            "femorphology" => Ok(Self::SvgFeMorphology),
362
            "feoffset" => Ok(Self::SvgFeOffset),
360
            "fepointlight" => Ok(Self::SvgFePointLight),
358
            "fespecularlighting" => Ok(Self::SvgFeSpecularLighting),
356
            "fespotlight" => Ok(Self::SvgFeSpotLight),
354
            "fetile" => Ok(Self::SvgFeTile),
352
            "feturbulence" => Ok(Self::SvgFeTurbulence),
            // SVG marker/image elements
350
            "image" | "svg:image" => Ok(Self::SvgImage),
346
            "svg:marker" => Ok(Self::SvgMarker),
344
            "foreignobject" => Ok(Self::SvgForeignObject),
            // SVG descriptive elements
342
            "svg:title" => Ok(Self::SvgTitle),
340
            "svg:a" => Ok(Self::SvgA),
338
            "svg:style" => Ok(Self::SvgStyle),
336
            "svg:script" => Ok(Self::SvgScript),
334
            "desc" => Ok(Self::SvgDesc),
332
            "metadata" => Ok(Self::SvgMetadata),
330
            "view" => Ok(Self::SvgView),
            // SVG animation elements
328
            "animate" => Ok(Self::SvgAnimate),
326
            "animatemotion" => Ok(Self::SvgAnimateMotion),
324
            "animatetransform" => Ok(Self::SvgAnimateTransform),
322
            "set" => Ok(Self::SvgSet),
320
            "mpath" => Ok(Self::SvgMpath),
            // Metadata
318
            "title" => Ok(Self::Title),
309
            "meta" => Ok(Self::Meta),
300
            "link" => Ok(Self::Link),
291
            "script" => Ok(Self::Script),
282
            "style" => Ok(Self::Style),
273
            "base" => Ok(Self::Base),
            // Special
264
            "text" => Ok(Self::Text), // Display emits "text"; from_str must accept it back
262
            "img" => Ok(Self::Img),
246
            "virtual-view" | "iframe" => Ok(Self::VirtualView),
235
            "icon" => Ok(Self::Icon),
233
            "geolocation-probe" => Ok(Self::GeolocationProbe),
231
            "pagebreak" => Ok(Self::PageBreak),
            // Pseudo-elements (usually prefixed with ::)
231
            "before" | "::before" => Ok(Self::Before),
213
            "after" | "::after" => Ok(Self::After),
195
            "marker" | "::marker" => Ok(Self::Marker),
177
            "placeholder" | "::placeholder" => Ok(Self::Placeholder),
159
            other => Err(NodeTypeTagParseError::Invalid(other)),
        }
97560
    }
}
impl fmt::Display for NodeTypeTag {
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
290073
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290073
        match self {
            // Document structure
2411
            Self::Html => write!(f, "html"),
11
            Self::Head => write!(f, "head"),
6731
            Self::Body => write!(f, "body"),
            // Block elements
92631
            Self::Div => write!(f, "div"),
59339
            Self::P => write!(f, "p"),
11
            Self::Article => write!(f, "article"),
11
            Self::Section => write!(f, "section"),
11
            Self::Nav => write!(f, "nav"),
11
            Self::Aside => write!(f, "aside"),
11
            Self::Header => write!(f, "header"),
11
            Self::Footer => write!(f, "footer"),
11
            Self::Main => write!(f, "main"),
11
            Self::Figure => write!(f, "figure"),
11
            Self::FigCaption => write!(f, "figcaption"),
            // Headings
563
            Self::H1 => write!(f, "h1"),
23
            Self::H2 => write!(f, "h2"),
23
            Self::H3 => write!(f, "h3"),
23
            Self::H4 => write!(f, "h4"),
23
            Self::H5 => write!(f, "h5"),
23
            Self::H6 => write!(f, "h6"),
            // Text formatting
11
            Self::Br => write!(f, "br"),
11
            Self::Hr => write!(f, "hr"),
11
            Self::Pre => write!(f, "pre"),
11
            Self::BlockQuote => write!(f, "blockquote"),
11
            Self::Address => write!(f, "address"),
11
            Self::Details => write!(f, "details"),
11
            Self::Summary => write!(f, "summary"),
11
            Self::Dialog => write!(f, "dialog"),
            // List elements
587
            Self::Ul => write!(f, "ul"),
11
            Self::Ol => write!(f, "ol"),
1703
            Self::Li => write!(f, "li"),
11
            Self::Dl => write!(f, "dl"),
11
            Self::Dt => write!(f, "dt"),
11
            Self::Dd => write!(f, "dd"),
11
            Self::Menu => write!(f, "menu"),
11
            Self::MenuItem => write!(f, "menuitem"),
11
            Self::Dir => write!(f, "dir"),
            // Table elements
407
            Self::Table => write!(f, "table"),
11
            Self::Caption => write!(f, "caption"),
23
            Self::THead => write!(f, "thead"),
11
            Self::TBody => write!(f, "tbody"),
11
            Self::TFoot => write!(f, "tfoot"),
419
            Self::Tr => write!(f, "tr"),
35
            Self::Th => write!(f, "th"),
1031
            Self::Td => write!(f, "td"),
11
            Self::ColGroup => write!(f, "colgroup"),
11
            Self::Col => write!(f, "col"),
            // Form elements
11
            Self::Form => write!(f, "form"),
11
            Self::FieldSet => write!(f, "fieldset"),
11
            Self::Legend => write!(f, "legend"),
11
            Self::Label => write!(f, "label"),
23
            Self::Input => write!(f, "input"),
19535
            Self::Button => write!(f, "button"),
11
            Self::Select => write!(f, "select"),
11
            Self::OptGroup => write!(f, "optgroup"),
11
            Self::SelectOption => write!(f, "option"),
11
            Self::TextArea => write!(f, "textarea"),
11
            Self::Output => write!(f, "output"),
11
            Self::Progress => write!(f, "progress"),
11
            Self::Meter => write!(f, "meter"),
11
            Self::DataList => write!(f, "datalist"),
            // Inline elements
647
            Self::Span => write!(f, "span"),
599
            Self::A => write!(f, "a"),
11
            Self::Em => write!(f, "em"),
11
            Self::Strong => write!(f, "strong"),
11
            Self::B => write!(f, "b"),
11
            Self::I => write!(f, "i"),
11
            Self::U => write!(f, "u"),
11
            Self::S => write!(f, "s"),
11
            Self::Mark => write!(f, "mark"),
11
            Self::Del => write!(f, "del"),
11
            Self::Ins => write!(f, "ins"),
11
            Self::Code => write!(f, "code"),
11
            Self::Samp => write!(f, "samp"),
11
            Self::Kbd => write!(f, "kbd"),
11
            Self::Var => write!(f, "var"),
11
            Self::Cite => write!(f, "cite"),
11
            Self::Dfn => write!(f, "dfn"),
11
            Self::Abbr => write!(f, "abbr"),
11
            Self::Acronym => write!(f, "acronym"),
11
            Self::Q => write!(f, "q"),
11
            Self::Time => write!(f, "time"),
11
            Self::Sub => write!(f, "sub"),
11
            Self::Sup => write!(f, "sup"),
11
            Self::Small => write!(f, "small"),
11
            Self::Big => write!(f, "big"),
11
            Self::Bdo => write!(f, "bdo"),
11
            Self::Bdi => write!(f, "bdi"),
11
            Self::Wbr => write!(f, "wbr"),
11
            Self::Ruby => write!(f, "ruby"),
11
            Self::Rt => write!(f, "rt"),
11
            Self::Rtc => write!(f, "rtc"),
11
            Self::Rp => write!(f, "rp"),
11
            Self::Data => write!(f, "data"),
            // Embedded content
11
            Self::Canvas => write!(f, "canvas"),
11
            Self::Object => write!(f, "object"),
11
            Self::Param => write!(f, "param"),
11
            Self::Embed => write!(f, "embed"),
11
            Self::Audio => write!(f, "audio"),
11
            Self::Video => write!(f, "video"),
11
            Self::Source => write!(f, "source"),
11
            Self::Track => write!(f, "track"),
11
            Self::Map => write!(f, "map"),
11
            Self::Area => write!(f, "area"),
11
            Self::Svg => write!(f, "svg"),
4
            Self::SvgPath => write!(f, "path"),
4
            Self::SvgCircle => write!(f, "circle"),
4
            Self::SvgRect => write!(f, "rect"),
4
            Self::SvgEllipse => write!(f, "ellipse"),
4
            Self::SvgLine => write!(f, "line"),
4
            Self::SvgPolygon => write!(f, "polygon"),
4
            Self::SvgPolyline => write!(f, "polyline"),
4
            Self::SvgG => write!(f, "g"),
            // SVG container elements
4
            Self::SvgDefs => write!(f, "defs"),
4
            Self::SvgSymbol => write!(f, "symbol"),
4
            Self::SvgUse => write!(f, "use"),
4
            Self::SvgSwitch => write!(f, "switch"),
            // SVG text elements
4
            Self::SvgText => write!(f, "svg:text"),
4
            Self::SvgTspan => write!(f, "tspan"),
4
            Self::SvgTextPath => write!(f, "textpath"),
            // SVG paint server elements
4
            Self::SvgLinearGradient => write!(f, "lineargradient"),
4
            Self::SvgRadialGradient => write!(f, "radialgradient"),
4
            Self::SvgStop => write!(f, "stop"),
4
            Self::SvgPattern => write!(f, "pattern"),
            // SVG clipping/masking elements
4
            Self::SvgClipPathElement => write!(f, "clippath"),
4
            Self::SvgMask => write!(f, "mask"),
            // SVG filter elements
4
            Self::SvgFilter => write!(f, "filter"),
4
            Self::SvgFeBlend => write!(f, "feblend"),
4
            Self::SvgFeColorMatrix => write!(f, "fecolormatrix"),
4
            Self::SvgFeComponentTransfer => write!(f, "fecomponenttransfer"),
4
            Self::SvgFeComposite => write!(f, "fecomposite"),
4
            Self::SvgFeConvolveMatrix => write!(f, "feconvolvematrix"),
4
            Self::SvgFeDiffuseLighting => write!(f, "fediffuselighting"),
4
            Self::SvgFeDisplacementMap => write!(f, "fedisplacementmap"),
4
            Self::SvgFeDistantLight => write!(f, "fedistantlight"),
4
            Self::SvgFeDropShadow => write!(f, "fedropshadow"),
4
            Self::SvgFeFlood => write!(f, "feflood"),
4
            Self::SvgFeFuncR => write!(f, "fefuncr"),
4
            Self::SvgFeFuncG => write!(f, "fefuncg"),
4
            Self::SvgFeFuncB => write!(f, "fefuncb"),
4
            Self::SvgFeFuncA => write!(f, "fefunca"),
4
            Self::SvgFeGaussianBlur => write!(f, "fegaussianblur"),
4
            Self::SvgFeImage => write!(f, "feimage"),
4
            Self::SvgFeMerge => write!(f, "femerge"),
4
            Self::SvgFeMergeNode => write!(f, "femergenode"),
4
            Self::SvgFeMorphology => write!(f, "femorphology"),
4
            Self::SvgFeOffset => write!(f, "feoffset"),
4
            Self::SvgFePointLight => write!(f, "fepointlight"),
4
            Self::SvgFeSpecularLighting => write!(f, "fespecularlighting"),
4
            Self::SvgFeSpotLight => write!(f, "fespotlight"),
4
            Self::SvgFeTile => write!(f, "fetile"),
4
            Self::SvgFeTurbulence => write!(f, "feturbulence"),
            // SVG marker/image elements
4
            Self::SvgMarker => write!(f, "svg:marker"),
5
            Self::SvgImage => write!(f, "svg:image"),
4
            Self::SvgForeignObject => write!(f, "foreignobject"),
            // SVG descriptive elements
4
            Self::SvgTitle => write!(f, "svg:title"),
4
            Self::SvgDesc => write!(f, "desc"),
4
            Self::SvgMetadata => write!(f, "metadata"),
4
            Self::SvgA => write!(f, "svg:a"),
4
            Self::SvgView => write!(f, "view"),
4
            Self::SvgStyle => write!(f, "svg:style"),
4
            Self::SvgScript => write!(f, "svg:script"),
            // SVG animation elements
4
            Self::SvgAnimate => write!(f, "animate"),
4
            Self::SvgAnimateMotion => write!(f, "animatemotion"),
4
            Self::SvgAnimateTransform => write!(f, "animatetransform"),
4
            Self::SvgSet => write!(f, "set"),
4
            Self::SvgMpath => write!(f, "mpath"),
            // Metadata
11
            Self::Title => write!(f, "title"),
11
            Self::Meta => write!(f, "meta"),
11
            Self::Link => write!(f, "link"),
11
            Self::Script => write!(f, "script"),
11
            Self::Style => write!(f, "style"),
11
            Self::Base => write!(f, "base"),
            // Content elements
82253
            Self::Text => write!(f, "text"),
59
            Self::Img => write!(f, "img"),
11
            Self::VirtualView => write!(f, "virtual-view"),
19648
            Self::Icon => write!(f, "icon"),
4
            Self::GeolocationProbe => write!(f, "geolocation-probe"),
12
            Self::PageBreak => write!(f, "pagebreak"),
            // Pseudo-elements
12
            Self::Before => write!(f, "::before"),
12
            Self::After => write!(f, "::after"),
12
            Self::Marker => write!(f, "::marker"),
12
            Self::Placeholder => write!(f, "::placeholder"),
        }
290073
    }
}
/// Represents a full CSS path (i.e. the "div#id.class" selector belonging to
///  a CSS "content group" (the following key-value block)).
///
/// ```no_run,ignore
/// "#div > .my_class:focus" ==
/// [
///   CssPathSelector::Type(NodeTypeTag::Div),
///   CssPathSelector::PseudoSelector(CssPathPseudoSelector::LimitChildren),
///   CssPathSelector::Class("my_class"),
///   CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus),
/// ]
#[derive(Clone, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
#[repr(C)]
pub struct CssPath {
    pub selectors: CssPathSelectorVec,
}
impl_vec!(CssPathSelector, CssPathSelectorVec, CssPathSelectorVecDestructor, CssPathSelectorVecDestructorType, CssPathSelectorVecSlice, OptionCssPathSelector);
impl_vec_debug!(CssPathSelector, CssPathSelectorVec);
impl_vec_partialord!(CssPathSelector, CssPathSelectorVec);
impl_vec_ord!(CssPathSelector, CssPathSelectorVec);
impl_vec_clone!(
    CssPathSelector,
    CssPathSelectorVec,
    CssPathSelectorVecDestructor
);
impl_vec_partialeq!(CssPathSelector, CssPathSelectorVec);
impl_vec_eq!(CssPathSelector, CssPathSelectorVec);
impl_vec_hash!(CssPathSelector, CssPathSelectorVec);
impl CssPath {
10118
    #[must_use] pub fn new(selectors: Vec<CssPathSelector>) -> Self {
10118
        Self {
10118
            selectors: selectors.into(),
10118
        }
10118
    }
    /// Prepend a `Root` scope selector (`push_front`) confining this rule to the owner
    /// node `start` (whose subtree spans the inclusive flat ids `[start, end]`).
    /// Two cases (#47 leak fix + descendant-selector support):
    ///
    /// - A **bare `*` rule** (the `parse_inline` wrapper for a `with_css`/`set_css`
    ///   bare-declaration string) is scoped **node-only** (`[start, start]`):
    ///   inline-style semantics — it applies to the OWNER only and must not leak to
    ///   descendants or siblings. `[Root([s,s]), Global]` matches `s` only.
    /// - A rule with a **real selector** (`.menu-item`, `div`, a descendant chain —
    ///   from `add_component_css` / a component stylesheet) is scoped to the whole
    ///   **subtree** (`[start, end]`), so its selectors match within the owner's
    ///   subtree (e.g. a menu container's `.menu-item` children). `[Root([s,e]),
    ///   Class(x)]` matches any node in `[s,e]` that also matches `.x`.
14
    pub fn push_front_scope(&mut self, start: usize, end: usize) {
14
        self.push_front_scope_for(start, end, true);
14
    }
    /// Like [`Self::push_front_scope`], but the CALLER decides whether a bare
    /// `[Global]` path is a bare-declaration wrapper (scope it NODE-ONLY,
    /// `[start, start]`) or a real stylesheet `* { ... }` rule (scope it to
    /// the whole subtree).
    ///
    /// The two are indistinguishable by path shape - `parse_inline` wraps
    /// selector-less declarations in `*`, and an author stylesheet's
    /// universal rule IS `*` - but they differ by RULE PRIORITY
    /// (`rule_priority::INLINE` vs `AUTHOR`), which only the caller holding
    /// the `CssRuleBlock` can see. Treating every bare global as a wrapper
    /// scoped the classic `* { margin: 0 }` reset of a mounted document to
    /// the mount root alone: the UA body margin survived on every child and
    /// each reftest page rendered shifted by 8px against the browser.
23726
    pub fn push_front_scope_for(
23726
        &mut self,
23726
        start: usize,
23726
        end: usize,
23726
        node_only_bare_global: bool,
23726
    ) {
23726
        let is_bare_global = self.selectors.as_ref().len() == 1
23218
            && matches!(self.selectors.as_ref().first(), Some(CssPathSelector::Global));
23726
        let range = if is_bare_global && node_only_bare_global {
7573
            CssScopeRange { start, end: start }
        } else {
16153
            CssScopeRange { start, end }
        };
23726
        let mut selectors = Vec::with_capacity(self.selectors.as_ref().len() + 1);
23726
        selectors.push(CssPathSelector::Root(range));
23726
        selectors.extend(self.selectors.as_ref().iter().cloned());
23726
        self.selectors = selectors.into();
23726
    }
}
impl fmt::Display for CssPath {
67
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75
        for selector in self.selectors.as_ref() {
54
            write!(f, "{selector}")?;
        }
67
        Ok(())
67
    }
}
impl fmt::Debug for CssPath {
15
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15
        write!(f, "{self}")
15
    }
}
/// Inclusive range of flat `NodeId`s describing a node's subtree `[start, end]`
/// (`end = start + estimated_total_children`, since the flat arena lays subtrees
/// out contiguously).
///
/// Carried by [`CssPathSelector::Root`] to scope inline css to
/// a subtree, and is the unit of future parallel per-subtree cascading.
/// `repr(C)` for FFI / api.json codegen.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct CssScopeRange {
    /// First flat `NodeId` of the subtree (the owning node itself).
    pub start: usize,
    /// Last flat `NodeId` of the subtree, inclusive (`start` for a leaf).
    pub end: usize,
}
impl CssScopeRange {
    /// True if `node` (a flat `NodeId` index) is inside this subtree range.
    #[inline]
232896
    #[must_use] pub const fn contains(&self, node: usize) -> bool {
232896
        self.start <= node && node <= self.end
232896
    }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C, u8)]
#[derive(Default)]
pub enum CssPathSelector {
    /// Represents the `*` selector
    #[default]
    Global,
    /// Scope marker carrying a node's **subtree range** `[start, end]` (inclusive
    /// flat `NodeId`s; `end = start + estimated_total_children`). Matches a node
    /// iff `start <= node <= end`. Synthesized at flatten time and `push_front`-ed
    /// onto every inline (`with_css`/`set_css`) rule's path, so the rule compounds
    /// with the `parse_inline` `*` wrapper (`[Root(s,e), Global, …]`) and is scoped
    /// to that node's subtree instead of leaking to the whole tree (#47). Because
    /// the flat arena lays subtrees out contiguously, this range is also the unit
    /// of future parallel per-subtree cascading.
    Root(CssScopeRange),
    /// `div`, `p`, etc.
    Type(NodeTypeTag),
    /// `.something`
    Class(AzString),
    /// `#something`
    Id(AzString),
    /// `:something`
    PseudoSelector(CssPathPseudoSelector),
    /// `[attr]`, `[attr="value"]`, `[attr~="value"]`, etc.
    Attribute(CssAttributeSelector),
    /// Represents the `>` selector (direct child)
    DirectChildren,
    /// Represents the ` ` selector (descendant)
    Children,
    /// Represents the `+` selector (adjacent sibling)
    AdjacentSibling,
    /// Represents the `~` selector (general sibling)
    GeneralSibling,
}
/// Attribute selector (`[attr]`, `[attr="value"]`, ...).
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct CssAttributeSelector {
    pub name: AzString,
    pub op: AttributeMatchOp,
    pub value: OptionString,
}
impl Default for CssAttributeSelector {
3
    fn default() -> Self {
3
        Self {
3
            name: AzString::default(),
3
            op: AttributeMatchOp::Exists,
3
            value: OptionString::None,
3
        }
3
    }
}
/// Operator that compares an attribute value against a target string.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
#[derive(Default)]
pub enum AttributeMatchOp {
    /// `[attr]` — attribute is present (any value).
    #[default]
    Exists,
    /// `[attr="value"]` — attribute equals value exactly.
    Eq,
    /// `[attr~="value"]` — value is one of the whitespace-separated words.
    Includes,
    /// `[attr|="value"]` — value equals exactly OR begins with value followed by `-`.
    DashMatch,
    /// `[attr^="value"]` — value starts with the given prefix.
    Prefix,
    /// `[attr$="value"]` — value ends with the given suffix.
    Suffix,
    /// `[attr*="value"]` — value contains the given substring.
    Substring,
}
impl_option!(
    CssPathSelector,
    OptionCssPathSelector,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl fmt::Display for CssPathSelector {
71
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::CssPathSelector::{Global, Root, Type, Class, Id, PseudoSelector, Attribute, DirectChildren, Children, AdjacentSibling, GeneralSibling};
71
        match &self {
1
            Global => write!(f, "*"),
3
            Root(r) => write!(f, ":root({}..={})", r.start, r.end),
4
            Type(n) => write!(f, "{n}"),
48
            Class(c) => write!(f, ".{c}"),
6
            Id(i) => write!(f, "#{i}"),
4
            PseudoSelector(p) => write!(f, ":{p}"),
1
            Attribute(a) => write!(f, "{a}"),
1
            DirectChildren => write!(f, ">"),
1
            Children => write!(f, " "),
1
            AdjacentSibling => write!(f, "+"),
1
            GeneralSibling => write!(f, "~"),
        }
71
    }
}
impl fmt::Display for CssAttributeSelector {
17
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17
        match (&self.op, self.value.as_ref()) {
3
            (AttributeMatchOp::Exists, _) => write!(f, "[{}]", self.name),
13
            (op, Some(v)) => write!(f, "[{}{}=\"{}\"]", self.name, op.symbol_prefix(), v),
1
            (op, None) => write!(f, "[{}{}=\"\"]", self.name, op.symbol_prefix()),
        }
17
    }
}
impl AttributeMatchOp {
    /// Returns the prefix character for the `=` operator (e.g. `~` for `~=`).
    /// `Eq` returns `""`, `Exists` is unused (no `=` printed at all).
21
    #[must_use] pub const fn symbol_prefix(&self) -> &'static str {
21
        match self {
11
            Self::Exists | Self::Eq => "",
2
            Self::Includes => "~",
2
            Self::DashMatch => "|",
2
            Self::Prefix => "^",
2
            Self::Suffix => "$",
2
            Self::Substring => "*",
        }
21
    }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C, u8)]
pub enum CssPathPseudoSelector {
    /// `:first`
    First,
    /// `:last`
    Last,
    /// `:nth-child`
    NthChild(CssNthChildSelector),
    /// `:hover` - mouse is over element
    Hover,
    /// `:active` - mouse is pressed and over element
    Active,
    /// `:focus` - element has received focus
    Focus,
    /// `:lang(de)` - element matches language
    Lang(AzString),
    /// `:backdrop` - window is not focused (GTK compatibility)
    Backdrop,
    /// `:dragging` - element is currently being dragged
    Dragging,
    /// `:drag-over` - a dragged element is over this drop target
    DragOver,
    /// `:root` - matches the document root element (equivalent to `html`,
    /// but with pseudo-class specificity). Structural (non-interactive).
    Root,
}
/// Selector for the `:nth-child()` CSS pseudo-class.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C, u8)]
pub enum CssNthChildSelector {
    Number(u32),
    Even,
    Odd,
    Pattern(CssNthChildPattern),
}
/// Pattern for `:nth-child(An+B)` selectors, where `pattern_repeat` is A and `offset` is B.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct CssNthChildPattern {
    pub pattern_repeat: u32,
    pub offset: u32,
}
impl fmt::Display for CssNthChildSelector {
8
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::CssNthChildSelector::{Number, Even, Odd, Pattern};
8
        match &self {
2
            Number(u) => write!(f, "{u}"),
2
            Even => write!(f, "even"),
1
            Odd => write!(f, "odd"),
3
            Pattern(p) => write!(f, "{}n + {}", p.pattern_repeat, p.offset),
        }
8
    }
}
impl fmt::Display for CssPathPseudoSelector {
21
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::CssPathPseudoSelector::{First, Last, NthChild, Hover, Active, Focus, Lang, Backdrop, Dragging, DragOver, Root};
21
        match &self {
1
            First => write!(f, "first"),
1
            Last => write!(f, "last"),
1
            NthChild(u) => write!(f, "nth-child({u})"),
4
            Hover => write!(f, "hover"),
1
            Active => write!(f, "active"),
2
            Focus => write!(f, "focus"),
8
            Lang(lang) => write!(f, "lang({})", lang.as_str()),
1
            Backdrop => write!(f, "backdrop"),
1
            Dragging => write!(f, "dragging"),
1
            DragOver => write!(f, "drag-over"),
            Root => write!(f, "root"),
        }
21
    }
}
impl Css {
    /// Creates a new, empty CSS.
47807
    #[must_use] pub fn empty() -> Self {
47807
        Self::default()
47807
    }
    /// Sort the rules by `(priority, specificity)` so they apply in cascade order.
    /// Lower-priority rules sort first; ties break by selector specificity.
    /// This preserves layer identity (UA / SYSTEM / AUTHOR / INLINE / RUNTIME)
    /// without needing a separate `Stylesheet` boundary.
17253
    pub fn sort_by_specificity(&mut self) {
45485
        self.rules.as_mut().sort_by(|a, b| {
45485
            a.priority.cmp(&b.priority)
45485
                .then_with(|| get_specificity(&a.path).cmp(&get_specificity(&b.path)))
45485
        });
17253
    }
17270
    pub fn rules(&self) -> core::slice::Iter<'_, CssRuleBlock> {
17270
        self.rules.as_ref().iter()
17270
    }
    /// Iterate `(property, conditions)` pairs as if this were a flat list of
    /// `CssPropertyWithConditions`. Each `Static` declaration yields one item,
    /// sharing the conditions of its enclosing rule. `Dynamic` declarations
    /// are skipped (matching the previous inline-CSS behaviour).
    ///
    /// Used by cascade and diff code that walks per-property to keep the
    /// flat-iteration shape after the inline-vs-component unification.
32426702
    pub fn iter_inline_properties(
32426702
        &self,
32426702
    ) -> impl Iterator<
32426702
        Item = (
32426702
            &CssProperty,
32426702
            &DynamicSelectorVec,
32426702
        ),
32426702
    > + '_ {
70728729
        self.rules.as_ref().iter().flat_map(|r| {
65920580
            r.declarations.as_ref().iter().filter_map(move |d| match d {
65920579
                CssDeclaration::Static(p) => Some((p, &r.conditions)),
1
                CssDeclaration::Dynamic(_) => None,
65920580
            })
65920578
        })
32426702
    }
}
#[cfg(test)]
mod root_scope_tests {
    use super::*;
    #[test]
1
    fn scope_range_contains() {
1
        let r = CssScopeRange { start: 3, end: 7 };
1
        assert!(r.contains(3) && r.contains(5) && r.contains(7));
1
        assert!(!r.contains(2) && !r.contains(8));
        // leaf: start == end matches only itself
1
        let leaf = CssScopeRange { start: 4, end: 4 };
1
        assert!(leaf.contains(4));
1
        assert!(!leaf.contains(3) && !leaf.contains(5));
1
    }
    #[test]
1
    fn push_front_scope_compounds_with_wrapper() {
        // a bare-decl `set_css` path is `[Global]` (the parse_inline `*` wrapper) and
        // is scoped NODE-ONLY ([start, start]) so it applies to the owner only.
1
        let mut p = CssPath::new(vec![CssPathSelector::Global]);
1
        p.push_front_scope(5, 9);
1
        assert_eq!(
1
            p.selectors.as_ref(),
1
            &[
1
                CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
1
                CssPathSelector::Global
1
            ][..]
        );
        // a path with a real selector is SUBTREE-scoped ([start, end]).
1
        let subtree = CssScopeRange { start: 5, end: 9 };
1
        let mut p2 = CssPath::new(vec![
1
            CssPathSelector::Global,
1
            CssPathSelector::Children,
1
            CssPathSelector::Class("foo".to_string().into()),
        ]);
1
        p2.push_front_scope(5, 9);
1
        assert_eq!(p2.selectors.as_ref()[0], CssPathSelector::Root(subtree));
1
        assert_eq!(p2.selectors.as_ref().len(), 4);
1
    }
    #[test]
1
    fn root_display_roundtrips() {
1
        let s = CssPathSelector::Root(CssScopeRange { start: 2, end: 6 });
1
        assert_eq!(format!("{s}"), ":root(2..=6)");
1
    }
    #[test]
1
    fn parse_inline_keeps_layout_and_style_decls() {
        // set_css("width: 200px; height: 100px; background: red") must keep all
        // three declarations (layout + style) as Static props in the parsed rule.
1
        let css = Css::parse_inline("width: 200px; height: 100px; background: red");
1
        let mut types = Vec::new();
1
        for r in css.rules.as_ref() {
3
            for d in r.declarations.as_ref() {
3
                if let CssDeclaration::Static(p) = d {
3
                    types.push(alloc::format!("{:?}", p.get_type()));
3
                }
            }
        }
1
        println!("INLINE PROP TYPES: {types:?}");
1
        assert!(
3
            types.iter().any(|t| t.contains("width")),
            "width must survive parse_inline as a Static decl; got {types:?}"
        );
1
        assert!(
2
            types.iter().any(|t| t.contains("height")),
            "height must survive parse_inline; got {types:?}"
        );
1
    }
}
#[cfg(test)]
mod priority_sort_tests {
    use super::*;
    use crate::css::rule_priority;
4
    fn rule_with(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
4
        CssRuleBlock {
4
            path: CssPath { selectors: selectors.into() },
4
            declarations: Vec::new().into(),
4
            conditions: DynamicSelectorVec::from_const_slice(&[]),
4
            priority,
4
        }
4
    }
    /// Pin the (priority, specificity) sort order. Lower priority sorts first;
    /// ties break by specificity.
    #[test]
1
    fn sort_by_priority_then_specificity() {
1
        let mut css = Css::new(vec![
            // Author rule, no specificity.
1
            rule_with(rule_priority::AUTHOR, vec![CssPathSelector::Global]),
            // UA rule with high specificity — must still come BEFORE any author rule.
1
            rule_with(rule_priority::UA, vec![
1
                CssPathSelector::Id("ua-id".to_string().into()),
1
                CssPathSelector::Class("ua-class".to_string().into()),
            ]),
            // Author rule with high specificity.
1
            rule_with(rule_priority::AUTHOR, vec![
1
                CssPathSelector::Id("a-id".to_string().into()),
            ]),
            // System rule with no specificity — must sit between UA and author.
1
            rule_with(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
        ]);
1
        css.sort_by_specificity();
1
        let priorities: Vec<u8> = css.rules.as_ref().iter().map(|r| r.priority).collect();
1
        assert_eq!(
            priorities,
1
            vec![rule_priority::UA, rule_priority::SYSTEM, rule_priority::AUTHOR, rule_priority::AUTHOR],
            "rules must sort by layer first; specificity only breaks ties within a layer"
        );
        // Within author, the high-specificity #a-id comes after the * rule.
1
        let last_two_specificity: Vec<_> = css.rules.as_ref().iter()
4
            .filter(|r| r.priority == rule_priority::AUTHOR)
2
            .map(|r| get_specificity(&r.path))
1
            .collect();
1
        assert!(last_two_specificity[0] < last_two_specificity[1]);
1
    }
}
/// Returns specificity of the given css path. Further information can be found on
/// [the w3 website](http://www.w3.org/TR/selectors/#specificity).
90945
#[must_use] pub fn get_specificity(path: &CssPath) -> (usize, usize, usize, usize) {
90945
    let id_count = path
90945
        .selectors
90945
        .iter()
184833
        .filter(|x| matches!(x, CssPathSelector::Id(_)))
90945
        .count();
90945
    let class_count = path
90945
        .selectors
90945
        .iter()
184833
        .filter(|x| {
154394
            matches!(
184833
                x,
                CssPathSelector::Class(_)
                    | CssPathSelector::PseudoSelector(_)
                    | CssPathSelector::Attribute(_)
            )
184833
        })
90945
        .count();
90945
    let div_count = path
90945
        .selectors
90945
        .iter()
184833
        .filter(|x| matches!(x, CssPathSelector::Type(_)))
90945
        .count();
90945
    (id_count, class_count, div_count, path.selectors.len())
90945
}
#[cfg(test)]
#[allow(clippy::pedantic, clippy::nursery)]
mod autotest_generated {
    use core::hash::{Hash, Hasher};
    use std::collections::hash_map::DefaultHasher;
    use super::*;
    use crate::{
        dynamic_selector::DynamicSelector,
        props::{
            basic::{color::ColorU, pixel::PixelValue},
            layout::dimensions::LayoutWidth,
            style::text::StyleTextColor,
        },
    };
    // ---------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------
    /// `width` — NOT inheritable, DOES trigger relayout.
    fn prop_width(px: f32) -> CssProperty {
        CssProperty::width(LayoutWidth::Px(PixelValue::px(px)))
    }
    /// `color` — IS inheritable, does NOT trigger relayout.
    fn prop_text_color(r: u8) -> CssProperty {
        CssProperty::const_text_color(StyleTextColor {
            inner: ColorU::new(r, 0, 0, 255),
        })
    }
    fn dyn_prop(id: &str, default_value: CssProperty) -> DynamicCssProperty {
        DynamicCssProperty {
            dynamic_id: id.to_string().into(),
            default_value,
        }
    }
    fn rule_at(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
        let mut r = CssRuleBlock::new(
            CssPath::new(selectors),
            vec![CssDeclaration::Static(prop_width(1.0))],
        );
        r.priority = priority;
        r
    }
    fn hash_of<T: Hash>(t: &T) -> u64 {
        let mut h = DefaultHasher::new();
        t.hash(&mut h);
        h.finish()
    }
    /// A stand-in property payload so the generic `CssPropertyValue<T>` surface can be
    /// driven with hostile floats (NaN / ±inf) that no real CSS type would hand us.
    #[derive(Debug, Copy, Clone, PartialEq, Default)]
    struct TestVal(f32);
    impl PrintAsCssValue for TestVal {
        fn print_as_css_value(&self) -> String {
            format!("{}", self.0)
        }
    }
    impl fmt::Display for TestVal {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}", self.0)
        }
    }
    /// The six CSS-wide keyword variants (everything that is not `Exact`).
    fn keyword_values() -> Vec<CssPropertyValue<TestVal>> {
        vec![
            CssPropertyValue::Auto,
            CssPropertyValue::None,
            CssPropertyValue::Initial,
            CssPropertyValue::Inherit,
            CssPropertyValue::Revert,
            CssPropertyValue::Unset,
        ]
    }
    // =====================================================================
    // Css — constructors, predicates, getters
    // =====================================================================
    #[test]
    fn css_empty_is_the_neutral_element() {
        let e = Css::empty();
        assert!(e.is_empty());
        assert_eq!(e.rules().count(), 0);
        assert_eq!(e.iter_inline_properties().count(), 0);
        // empty() / default() / new(vec![]) must all agree.
        assert_eq!(e, Css::default());
        assert_eq!(e, Css::new(Vec::new()));
        assert_eq!(e, Css::from(Vec::<CssRuleBlock>::new()));
    }
    #[test]
    fn css_new_preserves_length_and_order() {
        let rules = vec![
            rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
            rule_at(
                rule_priority::AUTHOR,
                vec![CssPathSelector::Id("a".to_string().into())],
            ),
            rule_at(rule_priority::INLINE, vec![CssPathSelector::Children]),
        ];
        let css = Css::new(rules.clone());
        assert!(!css.is_empty());
        assert_eq!(css.rules().count(), 3);
        assert_eq!(css.rules.as_ref(), &rules[..]);
        // `rules()` and the raw vec must not disagree.
        assert_eq!(css.rules().count(), css.rules.as_ref().len());
    }
    #[test]
    fn css_new_with_many_rules_does_not_panic() {
        let rules: Vec<CssRuleBlock> = (0..10_000)
            .map(|i| rule_at((i % 256) as u8, vec![CssPathSelector::Global]))
            .collect();
        let css = Css::new(rules);
        assert_eq!(css.rules.as_ref().len(), 10_000);
        assert!(!css.is_empty());
        // Every rule carries one Static decl → one inline property each.
        assert_eq!(css.iter_inline_properties().count(), 10_000);
    }
    #[test]
    fn css_sort_by_specificity_on_empty_and_singleton_is_a_noop() {
        let mut empty = Css::empty();
        empty.sort_by_specificity();
        assert!(empty.is_empty());
        let one = rule_at(rule_priority::AUTHOR, vec![CssPathSelector::Global]);
        let mut css = Css::new(vec![one.clone()]);
        css.sort_by_specificity();
        assert_eq!(css.rules.as_ref(), &[one][..]);
    }
    #[test]
    fn css_sort_by_specificity_is_idempotent() {
        let mut css = Css::new(vec![
            rule_at(rule_priority::RUNTIME, vec![CssPathSelector::Global]),
            rule_at(
                rule_priority::UA,
                vec![
                    CssPathSelector::Id("x".to_string().into()),
                    CssPathSelector::Class("y".to_string().into()),
                ],
            ),
            rule_at(rule_priority::INLINE, vec![CssPathSelector::Global]),
            rule_at(
                rule_priority::AUTHOR,
                vec![CssPathSelector::Type(NodeTypeTag::Div)],
            ),
            rule_at(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
        ]);
        css.sort_by_specificity();
        let once = css.clone();
        css.sort_by_specificity();
        assert_eq!(css, once, "sort_by_specificity must be idempotent");
        // Layer order is the primary key, and it is monotonically non-decreasing.
        let priorities: Vec<u8> = css.rules().map(|r| r.priority).collect();
        assert_eq!(
            priorities,
            vec![
                rule_priority::UA,
                rule_priority::SYSTEM,
                rule_priority::AUTHOR,
                rule_priority::INLINE,
                rule_priority::RUNTIME,
            ]
        );
    }
    #[test]
    fn css_sort_by_specificity_keeps_equal_keys_in_source_order() {
        // sort_by is stable — two rules with the same (priority, specificity) must not swap.
        let a = CssRuleBlock::new(
            CssPath::new(vec![CssPathSelector::Global]),
            vec![CssDeclaration::Static(prop_width(1.0))],
        );
        let b = CssRuleBlock::new(
            CssPath::new(vec![CssPathSelector::Global]),
            vec![CssDeclaration::Static(prop_width(2.0))],
        );
        let mut css = Css::new(vec![a.clone(), b.clone()]);
        css.sort_by_specificity();
        assert_eq!(
            css.rules.as_ref(),
            &[a, b][..],
            "ties must keep source order (last-wins cascade depends on it)"
        );
    }
    #[test]
    fn css_iter_inline_properties_skips_dynamic_declarations() {
        let css = Css::new(vec![CssRuleBlock::with_conditions(
            CssPath::new(vec![CssPathSelector::Global]),
            vec![
                CssDeclaration::Static(prop_width(10.0)),
                CssDeclaration::Dynamic(dyn_prop("d", prop_text_color(1))),
                CssDeclaration::Static(prop_text_color(2)),
            ],
            vec![DynamicSelector::ContainerName("c".to_string().into())],
        )]);
        let collected: Vec<_> = css.iter_inline_properties().collect();
        assert_eq!(collected.len(), 2, "Dynamic declarations must be skipped");
        assert_eq!(collected[0].0.get_type(), CssPropertyType::Width);
        assert_eq!(collected[1].0.get_type(), CssPropertyType::TextColor);
        // Every yielded property shares the conditions of its enclosing rule.
        for (_, conds) in &collected {
            assert_eq!(conds.as_ref().len(), 1);
        }
    }
    #[test]
    fn css_iter_inline_properties_on_rule_without_declarations() {
        let css = Css::new(vec![CssRuleBlock::new(
            CssPath::new(vec![CssPathSelector::Global]),
            Vec::new(),
        )]);
        assert!(!css.is_empty(), "a rule with 0 declarations is still a rule");
        assert_eq!(css.iter_inline_properties().count(), 0);
    }
    #[test]
    fn css_ord_is_length_based_by_design() {
        // Documented deviation: `Ord for Css` compares rule COUNT only, so two
        // structurally different stylesheets of equal length compare Equal while
        // PartialEq reports them as different. Pinned here so the deviation is a
        // deliberate choice, not an accident.
        let a = Css::new(vec![rule_at(
            rule_priority::UA,
            vec![CssPathSelector::Global],
        )]);
        let b = Css::new(vec![rule_at(
            rule_priority::RUNTIME,
            vec![CssPathSelector::Type(NodeTypeTag::Div)],
        )]);
        assert_ne!(a, b);
        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
        assert_eq!(a.partial_cmp(&b), Some(core::cmp::Ordering::Equal));
        // …and the length ordering itself is right.
        let longer = Css::new(vec![
            rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
            rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
        ]);
        assert_eq!(a.cmp(&longer), core::cmp::Ordering::Less);
        assert_eq!(Css::empty().cmp(&a), core::cmp::Ordering::Less);
    }
    // =====================================================================
    // CssDeclaration
    // =====================================================================
    #[test]
    fn css_declaration_new_static_matches_the_wrapped_property() {
        let p = prop_width(42.0);
        let d = CssDeclaration::new_static(p.clone());
        assert_eq!(d, CssDeclaration::Static(p.clone()));
        assert_eq!(d.get_type(), p.get_type());
        assert_eq!(d.get_type(), CssPropertyType::Width);
    }
    #[test]
    fn css_declaration_new_dynamic_takes_its_type_from_the_default_value() {
        let dp = dyn_prop("my_id", prop_text_color(7));
        let d = CssDeclaration::new_dynamic(dp.clone());
        assert_eq!(d, CssDeclaration::Dynamic(dp));
        assert_eq!(
            d.get_type(),
            CssPropertyType::TextColor,
            "a Dynamic declaration's type is its default value's type"
        );
    }
    #[test]
    fn css_declaration_is_inheritable_matrix() {
        // color inherits, width does not — that is the CSS spec.
        assert!(CssDeclaration::new_static(prop_text_color(1)).is_inheritable());
        assert!(!CssDeclaration::new_static(prop_width(1.0)).is_inheritable());
        // A Dynamic declaration is NEVER inheritable, even when its default value
        // is an inheritable property. Guards the documented anti-surprise rule.
        assert!(
            !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).is_inheritable(),
            "Dynamic declarations must never inherit, even wrapping an inheritable prop"
        );
        assert!(!CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).is_inheritable());
    }
    #[test]
    fn css_declaration_can_trigger_relayout_matrix() {
        assert!(CssDeclaration::new_static(prop_width(1.0)).can_trigger_relayout());
        assert!(!CssDeclaration::new_static(prop_text_color(1)).can_trigger_relayout());
        // Dynamic delegates to the default value's type (unlike is_inheritable).
        assert!(CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).can_trigger_relayout());
        assert!(
            !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).can_trigger_relayout()
        );
    }
    #[test]
    fn css_declaration_to_str_static_is_non_empty_for_edge_floats() {
        for px in [
            0.0_f32,
            -0.0,
            f32::MIN,
            f32::MAX,
            f32::NAN,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::EPSILON,
        ] {
            let s = CssDeclaration::new_static(prop_width(px)).to_str();
            assert!(
                !s.is_empty(),
                "to_str() must render something for width: {px:?}"
            );
        }
    }
    #[test]
    fn css_declaration_to_str_dynamic_renders_var_syntax() {
        let s = CssDeclaration::new_dynamic(dyn_prop("my_id", prop_width(5.0))).to_str();
        assert!(
            s.starts_with("var(--my_id, "),
            "dynamic to_str must render CSS var() syntax, got {s:?}"
        );
        assert!(s.ends_with(')'));
    }
    #[test]
    fn css_declaration_to_str_dynamic_with_hostile_ids_does_not_panic() {
        let long = "x".repeat(100_000);
        for id in [
            "",
            "   ",
            "😀",
            "a\u{0301}\u{0301}",
            "--)",
            "\u{0}",
            "a\nb",
            long.as_str(),
        ] {
            let s = CssDeclaration::new_dynamic(dyn_prop(id, prop_width(1.0))).to_str();
            assert!(s.starts_with("var(--"));
        }
    }
    // =====================================================================
    // DynamicCssProperty
    // =====================================================================
    #[test]
    fn dynamic_css_property_is_never_inheritable() {
        for p in [
            prop_text_color(0),
            prop_width(0.0),
            prop_width(f32::NAN),
            CssProperty::const_none(CssPropertyType::FontSize),
            CssProperty::const_inherit(CssPropertyType::TextColor),
        ] {
            assert!(
                !dyn_prop("id", p).is_inheritable(),
                "DynamicCssProperty::is_inheritable is unconditionally false"
            );
        }
    }
    #[test]
    fn dynamic_css_property_relayout_follows_the_default_value_type() {
        assert!(dyn_prop("a", prop_width(1.0)).can_trigger_relayout());
        assert!(!dyn_prop("a", prop_text_color(1)).can_trigger_relayout());
        // Keyword-valued defaults keep their property type, so the answer is unchanged.
        assert!(dyn_prop("a", CssProperty::const_auto(CssPropertyType::Width)).can_trigger_relayout());
        assert!(
            !dyn_prop("a", CssProperty::const_none(CssPropertyType::TextColor))
                .can_trigger_relayout()
        );
    }
    // =====================================================================
    // BoxOrStatic
    // =====================================================================
    static STATIC_U32: u32 = 0xDEAD_BEEF;
    #[test]
    fn box_or_static_heap_round_trips_through_as_ref_and_deref() {
        let b = BoxOrStatic::heap(123_u32);
        assert_eq!(*b.as_ref(), 123);
        assert_eq!(*b, 123, "Deref must agree with as_ref");
        assert_eq!(*BoxOrStatic::heap(u32::MAX).as_ref(), u32::MAX);
        assert_eq!(*BoxOrStatic::heap(0_u32).as_ref(), 0);
        // Zero-sized payload: exercise as_ref + Deref on the ZST heap path (no
        // value to compare; Miri is the UB oracle here).
        let z = BoxOrStatic::heap(());
        let _: &() = z.as_ref();
        let () = *z;
        let big = BoxOrStatic::heap(vec![0_u8; 1_000_000]);
        assert_eq!(big.as_ref().len(), 1_000_000);
    }
    #[test]
    fn box_or_static_static_variant_reads_through_as_ref() {
        let b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
        assert_eq!(*b.as_ref(), 0xDEAD_BEEF);
        assert_eq!(*b, 0xDEAD_BEEF);
    }
    #[test]
    fn box_or_static_as_mut_mutates_the_boxed_value() {
        let mut b = BoxOrStatic::heap(1_u32);
        *b.as_mut() = 9;
        assert_eq!(*b.as_ref(), 9);
    }
    #[test]
    #[should_panic(expected = "Cannot mutate a static BoxOrStatic value")]
    fn box_or_static_as_mut_on_static_panics_as_documented() {
        let mut b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
        let _ = b.as_mut();
    }
    #[test]
    fn box_or_static_clone_is_deep_for_boxed() {
        let a = BoxOrStatic::heap(5_u32);
        let mut b = a.clone();
        *b.as_mut() = 6;
        assert_eq!(*a.as_ref(), 5, "cloning a Boxed value must not alias it");
        assert_eq!(*b.as_ref(), 6);
        assert_ne!(a, b);
    }
    #[test]
    fn box_or_static_eq_ord_hash_all_delegate_to_the_inner_value() {
        let heap = BoxOrStatic::heap(0xDEAD_BEEF_u32);
        let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
        // Same inner value, different variant → still equal / equal-hashing / equal-ordering.
        assert_eq!(heap, stat);
        assert_eq!(hash_of(&heap), hash_of(&stat));
        assert_eq!(heap.cmp(&stat), core::cmp::Ordering::Equal);
        assert_eq!(heap.partial_cmp(&stat), Some(core::cmp::Ordering::Equal));
        let smaller = BoxOrStatic::heap(1_u32);
        assert!(smaller < heap);
    }
    #[test]
    fn box_or_static_debug_and_display_render_the_inner_value() {
        let b = BoxOrStatic::heap(42_u32);
        assert_eq!(format!("{b:?}"), "42");
        assert_eq!(format!("{b}"), "42");
        let s: BoxOrStaticString = BoxOrStatic::heap(String::new().into());
        assert!(
            !format!("{s:?}").is_empty(),
            "Debug of an empty string payload is still well-formed"
        );
        // Hostile float payloads must not panic while formatting.
        for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MIN] {
            let bf = BoxOrStatic::heap(f);
            assert!(!format!("{bf:?}").is_empty());
            assert!(!format!("{bf}").is_empty());
        }
    }
    #[test]
    fn box_or_static_default_is_a_heap_allocated_default() {
        let b: BoxOrStatic<u32> = BoxOrStatic::default();
        assert_eq!(*b.as_ref(), 0);
        assert!(matches!(b, BoxOrStatic::Boxed(_)));
        let s: BoxOrStaticString = BoxOrStatic::default();
        assert_eq!(s.as_ref().as_str(), "");
    }
    #[test]
    fn box_or_static_into_inner_returns_the_payload() {
        assert_eq!(BoxOrStatic::heap(7_u32).into_inner(), 7);
        let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
        assert_eq!(stat.into_inner(), 0xDEAD_BEEF);
        let s: BoxOrStaticString = BoxOrStatic::heap("hello".to_string().into());
        assert_eq!(s.into_inner().as_str(), "hello");
    }
    #[test]
    fn box_or_static_into_inner_must_not_leak_the_box() {
        use core::sync::atomic::{AtomicIsize, Ordering};
        // Counts LIVE instances: +1 on construction, +1 on clone, -1 on drop.
        // Never touched by any other test, so parallel execution is safe.
        static LIVE: AtomicIsize = AtomicIsize::new(0);
        struct Tracked(u32);
        impl Tracked {
            fn new(v: u32) -> Self {
                LIVE.fetch_add(1, Ordering::SeqCst);
                Self(v)
            }
        }
        impl Clone for Tracked {
            fn clone(&self) -> Self {
                LIVE.fetch_add(1, Ordering::SeqCst);
                Self(self.0)
            }
        }
        impl Drop for Tracked {
            fn drop(&mut self) {
                LIVE.fetch_sub(1, Ordering::SeqCst);
            }
        }
        let boxed = BoxOrStatic::heap(Tracked::new(7));
        assert_eq!(LIVE.load(Ordering::SeqCst), 1);
        let inner = boxed.into_inner();
        assert_eq!(inner.0, 7);
        drop(inner);
        assert_eq!(
            LIVE.load(Ordering::SeqCst),
            0,
            "into_inner() on a Boxed variant leaks: it clones the payload and then \
             mem::forget(self), so `Drop for BoxOrStatic` never runs and the Box \
             (plus the T inside it) is never freed"
        );
    }
    #[test]
    #[cfg(target_pointer_width = "64")]
    fn box_or_static_is_the_documented_16_bytes() {
        assert_eq!(size_of::<BoxOrStatic<u32>>(), 16);
        assert_eq!(size_of::<BoxOrStaticString>(), 16);
    }
    // =====================================================================
    // CssPropertyValue
    // =====================================================================
    #[test]
    fn css_property_value_predicates_are_mutually_exclusive() {
        for v in keyword_values() {
            let flags = [
                v.is_auto(),
                v.is_none(),
                v.is_initial(),
                v.is_inherit(),
                v.is_revert(),
                v.is_unset(),
            ];
            assert_eq!(
                flags.iter().filter(|b| **b).count(),
                1,
                "exactly one predicate must fire for {v:?}"
            );
            assert!(
                v.get_property().is_none(),
                "a keyword variant has no property"
            );
        }
        let exact = CssPropertyValue::Exact(TestVal(1.0));
        assert!(
            !exact.is_auto()
                && !exact.is_none()
                && !exact.is_initial()
                && !exact.is_inherit()
                && !exact.is_revert()
                && !exact.is_unset(),
            "Exact must answer false to every keyword predicate"
        );
    }
    #[test]
    fn css_property_value_predicates_pick_the_right_variant() {
        assert!(CssPropertyValue::<TestVal>::Auto.is_auto());
        assert!(CssPropertyValue::<TestVal>::None.is_none());
        assert!(CssPropertyValue::<TestVal>::Initial.is_initial());
        assert!(CssPropertyValue::<TestVal>::Inherit.is_inherit());
        assert!(CssPropertyValue::<TestVal>::Revert.is_revert());
        assert!(CssPropertyValue::<TestVal>::Unset.is_unset());
    }
    #[test]
    fn css_property_value_get_property_and_get_property_owned_agree() {
        let exact = CssPropertyValue::Exact(TestVal(3.5));
        assert_eq!(exact.get_property(), Some(&TestVal(3.5)));
        assert_eq!(exact.get_property_owned(), Some(TestVal(3.5)));
        for v in keyword_values() {
            assert_eq!(v.get_property(), None);
            assert_eq!(v.get_property_owned(), None);
        }
    }
    #[test]
    fn css_property_value_get_property_or_default_substitutes_only_auto_and_initial() {
        // Documented mapping: Auto/Initial fall back to T::default(); the remaining
        // keywords stay None (the cascade resolves them elsewhere).
        assert_eq!(
            CssPropertyValue::<TestVal>::Auto.get_property_or_default(),
            Some(TestVal::default())
        );
        assert_eq!(
            CssPropertyValue::<TestVal>::Initial.get_property_or_default(),
            Some(TestVal::default())
        );
        assert_eq!(CssPropertyValue::<TestVal>::None.get_property_or_default(), None);
        assert_eq!(
            CssPropertyValue::<TestVal>::Inherit.get_property_or_default(),
            None
        );
        assert_eq!(
            CssPropertyValue::<TestVal>::Revert.get_property_or_default(),
            None
        );
        assert_eq!(CssPropertyValue::<TestVal>::Unset.get_property_or_default(), None);
        assert_eq!(
            CssPropertyValue::Exact(TestVal(9.0)).get_property_or_default(),
            Some(TestVal(9.0))
        );
    }
    #[test]
    fn css_property_value_default_is_exact_default() {
        assert_eq!(
            CssPropertyValue::<TestVal>::default(),
            CssPropertyValue::Exact(TestVal::default())
        );
        assert!(!CssPropertyValue::<TestVal>::default().is_auto());
    }
    #[test]
    fn css_property_value_from_wraps_into_exact() {
        let v: CssPropertyValue<TestVal> = TestVal(2.0).into();
        assert_eq!(v, CssPropertyValue::Exact(TestVal(2.0)));
    }
    #[test]
    fn css_property_value_keyword_serialization_is_the_css_keyword() {
        let cases: [(CssPropertyValue<TestVal>, &str); 6] = [
            (CssPropertyValue::Auto, "auto"),
            (CssPropertyValue::None, "none"),
            (CssPropertyValue::Initial, "initial"),
            (CssPropertyValue::Inherit, "inherit"),
            (CssPropertyValue::Revert, "revert"),
            (CssPropertyValue::Unset, "unset"),
        ];
        for (v, expected) in cases {
            assert_eq!(v.get_css_value_fmt(), expected);
            assert_eq!(
                format!("{v}"),
                expected,
                "Display and get_css_value_fmt must not diverge for keywords"
            );
        }
        let exact = CssPropertyValue::Exact(TestVal(1.5));
        assert_eq!(exact.get_css_value_fmt(), "1.5");
        assert_eq!(format!("{exact}"), "1.5");
    }
    #[test]
    fn css_property_value_serializes_hostile_floats_without_panicking() {
        for f in [
            f32::NAN,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::MIN,
            f32::MAX,
            -0.0,
            f32::MIN_POSITIVE,
        ] {
            let v = CssPropertyValue::Exact(TestVal(f));
            assert!(!v.get_css_value_fmt().is_empty());
            assert!(!format!("{v}").is_empty());
        }
    }
    #[test]
    fn css_property_value_map_property_preserves_keyword_variants() {
        // The mapper must never run for a keyword variant…
        for v in keyword_values() {
            let before = format!("{v}");
            let mapped: CssPropertyValue<u32> =
                v.map_property(|_| panic!("map_fn must not run on a keyword variant"));
            assert_eq!(format!("{mapped}"), before, "the keyword must survive the map");
        }
        // …and must run exactly once for Exact.
        let mapped = CssPropertyValue::Exact(TestVal(2.0)).map_property(|t| t.0 as u32);
        assert_eq!(mapped, CssPropertyValue::Exact(2_u32));
    }
    #[test]
    fn css_property_value_map_property_handles_nan_and_type_changing_maps() {
        let mapped = CssPropertyValue::Exact(TestVal(f32::NAN)).map_property(|t| t.0.is_nan());
        assert_eq!(mapped, CssPropertyValue::Exact(true));
        // f32 -> i32 saturating cast on infinity must not trap.
        let mapped = CssPropertyValue::Exact(TestVal(f32::INFINITY)).map_property(|t| t.0 as i64);
        assert_eq!(mapped, CssPropertyValue::Exact(i64::MAX));
    }
    // =====================================================================
    // CssRuleBlock
    // =====================================================================
    #[test]
    fn css_rule_block_new_defaults_to_author_priority_and_no_conditions() {
        let decls = vec![
            CssDeclaration::Static(prop_width(1.0)),
            CssDeclaration::Static(prop_text_color(2)),
        ];
        let r = CssRuleBlock::new(
            CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]),
            decls.clone(),
        );
        assert_eq!(r.priority, rule_priority::AUTHOR);
        assert!(r.conditions.as_ref().is_empty());
        assert_eq!(r.declarations.as_ref(), &decls[..]);
        assert_eq!(r.path.selectors.as_ref().len(), 1);
    }
    #[test]
    fn css_rule_block_with_conditions_keeps_every_condition() {
        let conds: Vec<DynamicSelector> = (0..1_000)
            .map(|i| DynamicSelector::ContainerName(format!("c{i}").into()))
            .collect();
        let r = CssRuleBlock::with_conditions(
            CssPath::default(),
            Vec::new(),
            conds.clone(),
        );
        assert_eq!(r.conditions.as_ref().len(), 1_000);
        assert_eq!(r.conditions.as_ref(), &conds[..]);
        assert_eq!(
            r.priority,
            rule_priority::AUTHOR,
            "with_conditions must not change the layer"
        );
        assert!(r.declarations.as_ref().is_empty());
        assert!(r.path.selectors.as_ref().is_empty());
    }
    #[test]
    fn css_rule_block_default_is_empty_and_ua_priority() {
        let r = CssRuleBlock::default();
        assert!(r.path.selectors.as_ref().is_empty());
        assert!(r.declarations.as_ref().is_empty());
        assert!(r.conditions.as_ref().is_empty());
        assert_eq!(r.priority, rule_priority::UA, "u8::default() == 0 == UA");
    }
    #[test]
    fn rule_priority_slots_are_strictly_ordered() {
        const _: () = assert!(rule_priority::UA < rule_priority::SYSTEM);
        const _: () = assert!(rule_priority::SYSTEM < rule_priority::AUTHOR);
        const _: () = assert!(rule_priority::AUTHOR < rule_priority::INLINE);
        const _: () = assert!(rule_priority::INLINE < rule_priority::RUNTIME);
    }
    // =====================================================================
    // NodeTypeTag — parse / serialize round trip
    // =====================================================================
    const ALL_TAGS: &[NodeTypeTag] = {
        use NodeTypeTag::*;
        &[
            Html, Head, Body, Div, P, Article, Section, Nav, Aside, Header, Footer, Main, Figure,
            FigCaption, H1, H2, H3, H4, H5, H6, Br, Hr, Pre, BlockQuote, Address, Details, Summary,
            Dialog, Ul, Ol, Li, Dl, Dt, Dd, Menu, MenuItem, Dir, Table, Caption, THead, TBody,
            TFoot, Tr, Th, Td, ColGroup, Col, Form, FieldSet, Legend, Label, Input, Button, Select,
            OptGroup, SelectOption, TextArea, Output, Progress, Meter, DataList, Span, A, Em,
            Strong, B, I, U, S, Mark, Del, Ins, Code, Samp, Kbd, Var, Cite, Dfn, Abbr, Acronym, Q,
            Time, Sub, Sup, Small, Big, Bdo, Bdi, Wbr, Ruby, Rt, Rtc, Rp, Data, Canvas, Object,
            Param, Embed, Audio, Video, Source, Track, Map, Area, Svg, SvgPath, SvgCircle, SvgRect,
            SvgEllipse, SvgLine, SvgPolygon, SvgPolyline, SvgG, SvgDefs, SvgSymbol, SvgUse,
            SvgSwitch, SvgText, SvgTspan, SvgTextPath, SvgLinearGradient, SvgRadialGradient,
            SvgStop, SvgPattern, SvgClipPathElement, SvgMask, SvgFilter, SvgFeBlend,
            SvgFeColorMatrix, SvgFeComponentTransfer, SvgFeComposite, SvgFeConvolveMatrix,
            SvgFeDiffuseLighting, SvgFeDisplacementMap, SvgFeDistantLight, SvgFeDropShadow,
            SvgFeFlood, SvgFeFuncR, SvgFeFuncG, SvgFeFuncB, SvgFeFuncA, SvgFeGaussianBlur,
            SvgFeImage, SvgFeMerge, SvgFeMergeNode, SvgFeMorphology, SvgFeOffset, SvgFePointLight,
            SvgFeSpecularLighting, SvgFeSpotLight, SvgFeTile, SvgFeTurbulence, SvgMarker, SvgImage,
            SvgForeignObject, SvgTitle, SvgDesc, SvgMetadata, SvgA, SvgView, SvgStyle, SvgScript,
            SvgAnimate, SvgAnimateMotion, SvgAnimateTransform, SvgSet, SvgMpath, Title, Meta, Link,
            Script, Style, Base, Text, Img, VirtualView, Icon, GeolocationProbe, Before, After,
            Marker, Placeholder,
        ]
    };
    #[test]
    fn node_type_tag_variant_list_is_complete_and_unique() {
        // Guard rail for the round-trip tests below: if a variant is added to the enum
        // without being added here, this count check fails and points at the omission.
        assert_eq!(
            ALL_TAGS.len(),
            182,
            "ALL_TAGS is out of sync with the NodeTypeTag enum"
        );
        let mut seen: Vec<NodeTypeTag> = Vec::new();
        for t in ALL_TAGS {
            assert!(!seen.contains(t), "duplicate entry in ALL_TAGS: {t:?}");
            seen.push(*t);
        }
    }
    #[test]
    fn node_type_tag_display_names_are_all_distinct() {
        let mut names: Vec<String> = ALL_TAGS.iter().map(ToString::to_string).collect();
        names.sort();
        let before = names.len();
        names.dedup();
        assert_eq!(
            names.len(),
            before,
            "two NodeTypeTag variants serialize to the same CSS tag name — \
             the string is then ambiguous on the way back in"
        );
    }
    #[test]
    fn node_type_tag_display_then_from_str_round_trips_every_variant() {
        let mut broken: Vec<(NodeTypeTag, String)> = Vec::new();
        for tag in ALL_TAGS {
            let serialized = tag.to_string();
            // Resolve to a bool first: the Result borrows `serialized`, so `serialized`
            // cannot be moved into `broken` while that borrow is still live.
            let round_trips = matches!(NodeTypeTag::from_str(&serialized), Ok(t) if t == *tag);
            if !round_trips {
                broken.push((*tag, serialized));
            }
        }
        assert!(
            broken.is_empty(),
            "from_str(Display(tag)) must yield tag back, but these variants do not \
             round-trip: {broken:?}"
        );
    }
    #[test]
    fn node_type_tag_serialize_parse_serialize_is_stable() {
        // Idempotent normalization: for every variant that parses back at all, a second
        // serialize must produce a byte-identical string.
        for tag in ALL_TAGS {
            let once = tag.to_string();
            if let Ok(parsed) = NodeTypeTag::from_str(&once) {
                assert_eq!(
                    parsed.to_string(),
                    once,
                    "serialize(parse(serialize({tag:?}))) drifted"
                );
            }
        }
    }
    #[test]
    fn node_type_tag_from_str_valid_minimal() {
        assert_eq!(NodeTypeTag::from_str("div"), Ok(NodeTypeTag::Div));
        assert_eq!(NodeTypeTag::from_str("p"), Ok(NodeTypeTag::P));
        assert_eq!(NodeTypeTag::from_str("a"), Ok(NodeTypeTag::A));
    }
    #[test]
    fn node_type_tag_from_str_accepts_documented_aliases() {
        // Two spellings, one variant — and the canonical spelling is what comes back out.
        assert_eq!(NodeTypeTag::from_str("image"), Ok(NodeTypeTag::SvgImage));
        assert_eq!(NodeTypeTag::from_str("svg:image"), Ok(NodeTypeTag::SvgImage));
        assert_eq!(NodeTypeTag::SvgImage.to_string(), "svg:image");
        assert_eq!(NodeTypeTag::from_str("iframe"), Ok(NodeTypeTag::VirtualView));
        assert_eq!(
            NodeTypeTag::from_str("virtual-view"),
            Ok(NodeTypeTag::VirtualView)
        );
        for (bare, prefixed, tag) in [
            ("before", "::before", NodeTypeTag::Before),
            ("after", "::after", NodeTypeTag::After),
            ("marker", "::marker", NodeTypeTag::Marker),
            ("placeholder", "::placeholder", NodeTypeTag::Placeholder),
        ] {
            assert_eq!(NodeTypeTag::from_str(bare), Ok(tag));
            assert_eq!(NodeTypeTag::from_str(prefixed), Ok(tag));
            assert_eq!(
                tag.to_string(),
                prefixed,
                "pseudo-elements must serialize in their `::` form"
            );
        }
    }
    #[test]
    fn node_type_tag_from_str_rejects_hostile_input_without_panicking() {
        let long = "a".repeat(1_000_000);
        let nested = "<".repeat(10_000);
        let hostile = [
            "",                       // empty
            " ",                      // whitespace only
            "   \t\n\r  ",            // whitespace only, mixed
            " div",                   // leading junk — no trimming
            "div ",                   // trailing junk
            "  div  ",                // both
            "div;garbage",            // trailing garbage
            "DIV",                    // wrong case — CSS tag matching here is case-sensitive
            "Div",
            "0",
            "-0",
            "9223372036854775807",    // i64::MAX
            "1e400",
            "NaN",
            "inf",
            "-inf",
            "\u{1F600}",              // emoji
            "e\u{0301}",              // combining mark
            "\u{0}",                  // NUL
            "\u{FEFF}div",            // BOM prefix
            "div\u{0}",
            "*",
            "::",
            ":::before",
            "<script>",
            long.as_str(),
            nested.as_str(),
        ];
        for input in hostile {
            match NodeTypeTag::from_str(input) {
                Ok(t) => panic!("{input:?} must not parse, but produced {t:?}"),
                Err(NodeTypeTagParseError::Invalid(echoed)) => {
                    assert_eq!(echoed, input, "the error must echo the input verbatim");
                }
            }
        }
    }
    #[test]
    fn node_type_tag_parse_error_display_names_the_offending_input() {
        let e = NodeTypeTagParseError::Invalid("wat");
        assert_eq!(format!("{e}"), "Invalid node type: wat");
        // Empty / unicode payloads must still format cleanly.
        assert_eq!(format!("{}", NodeTypeTagParseError::Invalid("")), "Invalid node type: ");
        assert!(format!("{}", NodeTypeTagParseError::Invalid("😀")).contains('😀'));
    }
    #[test]
    fn node_type_tag_parse_error_to_contained_to_shared_round_trips() {
        let long = "x".repeat(10_000);
        for s in ["", "   ", "div", "😀", "e\u{0301}", "\u{0}", long.as_str()] {
            let shared = NodeTypeTagParseError::Invalid(s);
            let owned = shared.to_contained();
            assert_eq!(owned, NodeTypeTagParseErrorOwned::Invalid(s.to_string().into()));
            assert_eq!(
                owned.to_shared(),
                shared,
                "to_shared(to_contained(x)) must equal x"
            );
            // …and the owned form still renders the same message.
            assert_eq!(format!("{}", owned.to_shared()), format!("{shared}"));
        }
    }
    // =====================================================================
    // CssPath / CssScopeRange
    // =====================================================================
    #[test]
    fn css_path_new_preserves_selectors_including_empty() {
        assert!(CssPath::new(Vec::new()).selectors.as_ref().is_empty());
        assert_eq!(CssPath::new(Vec::new()), CssPath::default());
        let sels = vec![
            CssPathSelector::Type(NodeTypeTag::Div),
            CssPathSelector::DirectChildren,
            CssPathSelector::Class("c".to_string().into()),
        ];
        let p = CssPath::new(sels.clone());
        assert_eq!(p.selectors.as_ref(), &sels[..]);
    }
    #[test]
    fn css_path_display_and_debug_agree_and_compose() {
        let p = CssPath::new(vec![
            CssPathSelector::Type(NodeTypeTag::Div),
            CssPathSelector::Id("id".to_string().into()),
            CssPathSelector::Class("cls".to_string().into()),
            CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
        ]);
        assert_eq!(format!("{p}"), "div#id.cls:hover");
        assert_eq!(format!("{p:?}"), format!("{p}"), "Debug delegates to Display");
        // An empty path renders as the empty string — deterministic, no panic.
        assert_eq!(format!("{}", CssPath::default()), "");
    }
    #[test]
    fn push_front_scope_scopes_a_bare_star_rule_to_the_node_only() {
        // A bare `*` path is the parse_inline wrapper for inline styles → node-only.
        let mut p = CssPath::new(vec![CssPathSelector::Global]);
        p.push_front_scope(5, 9);
        assert_eq!(
            p.selectors.as_ref(),
            &[
                CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
                CssPathSelector::Global,
            ][..],
            "inline style must not leak past the owner node (#47)"
        );
    }
    #[test]
    fn push_front_scope_scopes_a_real_selector_to_the_whole_subtree() {
        let mut p = CssPath::new(vec![CssPathSelector::Class("menu-item".to_string().into())]);
        p.push_front_scope(5, 9);
        assert_eq!(
            p.selectors.as_ref()[0],
            CssPathSelector::Root(CssScopeRange { start: 5, end: 9 })
        );
        assert_eq!(p.selectors.as_ref().len(), 2);
    }
    #[test]
    fn push_front_scope_on_an_empty_path_uses_the_subtree_range() {
        // len() != 1 → not the bare-`*` case → subtree scope.
        let mut p = CssPath::default();
        p.push_front_scope(2, 8);
        assert_eq!(
            p.selectors.as_ref(),
            &[CssPathSelector::Root(CssScopeRange { start: 2, end: 8 })][..]
        );
    }
    #[test]
    fn push_front_scope_at_numeric_boundaries_does_not_panic() {
        // 0, usize::MAX, and an INVERTED range (start > end) — the function does no
        // arithmetic, so all three must be stored verbatim without overflow.
        for (start, end) in [
            (0_usize, 0_usize),
            (0, usize::MAX),
            (usize::MAX, usize::MAX),
            (usize::MAX, 0),  // inverted: end < start
            (9, 5),           // inverted
        ] {
            let mut p = CssPath::new(vec![CssPathSelector::Class("c".to_string().into())]);
            p.push_front_scope(start, end);
            assert_eq!(
                p.selectors.as_ref()[0],
                CssPathSelector::Root(CssScopeRange { start, end })
            );
        }
        // The bare-`*` branch clamps `end` to `start`, so it can never be inverted.
        let mut g = CssPath::new(vec![CssPathSelector::Global]);
        g.push_front_scope(usize::MAX, 0);
        assert_eq!(
            g.selectors.as_ref()[0],
            CssPathSelector::Root(CssScopeRange {
                start: usize::MAX,
                end: usize::MAX
            })
        );
    }
    #[test]
    fn push_front_scope_applied_twice_stacks_root_selectors() {
        let mut p = CssPath::new(vec![CssPathSelector::Global]);
        p.push_front_scope(5, 9);
        // Now the path is [Root, Global] (len 2) → no longer the bare-`*` case, so the
        // second call scopes to the full subtree rather than node-only.
        p.push_front_scope(1, 20);
        assert_eq!(
            p.selectors.as_ref(),
            &[
                CssPathSelector::Root(CssScopeRange { start: 1, end: 20 }),
                CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
                CssPathSelector::Global,
            ][..]
        );
    }
    #[test]
    fn push_front_scope_on_a_long_path_preserves_order_and_length() {
        let sels: Vec<CssPathSelector> = (0..5_000)
            .map(|i| CssPathSelector::Class(format!("c{i}").into()))
            .collect();
        let mut p = CssPath::new(sels.clone());
        p.push_front_scope(3, 4);
        assert_eq!(p.selectors.as_ref().len(), 5_001);
        assert_eq!(
            p.selectors.as_ref()[0],
            CssPathSelector::Root(CssScopeRange { start: 3, end: 4 })
        );
        assert_eq!(&p.selectors.as_ref()[1..], &sels[..], "the tail must be untouched");
    }
    #[test]
    fn scope_range_contains_at_zero_and_usize_max() {
        let zero = CssScopeRange { start: 0, end: 0 };
        assert!(zero.contains(0));
        assert!(!zero.contains(1));
        assert!(!zero.contains(usize::MAX));
        let full = CssScopeRange {
            start: 0,
            end: usize::MAX,
        };
        assert!(full.contains(0));
        assert!(full.contains(usize::MAX));
        assert!(full.contains(usize::MAX / 2));
        let top = CssScopeRange {
            start: usize::MAX,
            end: usize::MAX,
        };
        assert!(top.contains(usize::MAX));
        assert!(!top.contains(usize::MAX - 1));
        assert!(!top.contains(0));
    }
    #[test]
    fn scope_range_inverted_contains_nothing() {
        // start > end is nonsense but must degrade to "matches nothing", not panic.
        let inverted = CssScopeRange { start: 9, end: 5 };
        for n in [0_usize, 4, 5, 7, 9, 10, usize::MAX] {
            assert!(!inverted.contains(n), "inverted range must never match {n}");
        }
    }
    // =====================================================================
    // Selector serializers
    // =====================================================================
    #[test]
    fn css_path_selector_display_covers_every_variant() {
        let cases: Vec<(CssPathSelector, String)> = vec![
            (CssPathSelector::Global, "*".to_string()),
            (
                CssPathSelector::Root(CssScopeRange { start: 2, end: 6 }),
                ":root(2..=6)".to_string(),
            ),
            (
                CssPathSelector::Type(NodeTypeTag::Div),
                "div".to_string(),
            ),
            (
                CssPathSelector::Class("c".to_string().into()),
                ".c".to_string(),
            ),
            (CssPathSelector::Id("i".to_string().into()), "#i".to_string()),
            (
                CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus),
                ":focus".to_string(),
            ),
            (
                CssPathSelector::Attribute(CssAttributeSelector::default()),
                "[]".to_string(),
            ),
            (CssPathSelector::DirectChildren, ">".to_string()),
            (CssPathSelector::Children, " ".to_string()),
            (CssPathSelector::AdjacentSibling, "+".to_string()),
            (CssPathSelector::GeneralSibling, "~".to_string()),
        ];
        for (sel, expected) in cases {
            assert_eq!(format!("{sel}"), expected);
        }
        assert_eq!(CssPathSelector::default(), CssPathSelector::Global);
    }
    #[test]
    fn css_path_selector_display_at_scope_range_boundaries() {
        let s = CssPathSelector::Root(CssScopeRange {
            start: 0,
            end: usize::MAX,
        });
        assert_eq!(format!("{s}"), format!(":root(0..={})", usize::MAX));
    }
    #[test]
    fn css_path_selector_display_with_empty_and_unicode_names() {
        // Empty class/id names produce a bare `.` / `#` — degenerate but deterministic.
        assert_eq!(
            format!("{}", CssPathSelector::Class(String::new().into())),
            "."
        );
        assert_eq!(format!("{}", CssPathSelector::Id(String::new().into())), "#");
        assert_eq!(
            format!("{}", CssPathSelector::Class("😀".to_string().into())),
            ".😀"
        );
        let long = "x".repeat(100_000);
        assert_eq!(
            format!("{}", CssPathSelector::Id(long.clone().into())).len(),
            long.len() + 1
        );
    }
    #[test]
    fn attribute_match_op_symbol_prefix_matrix() {
        assert_eq!(AttributeMatchOp::Exists.symbol_prefix(), "");
        assert_eq!(AttributeMatchOp::Eq.symbol_prefix(), "");
        assert_eq!(AttributeMatchOp::Includes.symbol_prefix(), "~");
        assert_eq!(AttributeMatchOp::DashMatch.symbol_prefix(), "|");
        assert_eq!(AttributeMatchOp::Prefix.symbol_prefix(), "^");
        assert_eq!(AttributeMatchOp::Suffix.symbol_prefix(), "$");
        assert_eq!(AttributeMatchOp::Substring.symbol_prefix(), "*");
        assert_eq!(AttributeMatchOp::default(), AttributeMatchOp::Exists);
    }
    #[test]
    fn css_attribute_selector_display_renders_each_operator() {
        let ops = [
            (AttributeMatchOp::Eq, "[a=\"v\"]"),
            (AttributeMatchOp::Includes, "[a~=\"v\"]"),
            (AttributeMatchOp::DashMatch, "[a|=\"v\"]"),
            (AttributeMatchOp::Prefix, "[a^=\"v\"]"),
            (AttributeMatchOp::Suffix, "[a$=\"v\"]"),
            (AttributeMatchOp::Substring, "[a*=\"v\"]"),
        ];
        for (op, expected) in ops {
            let sel = CssAttributeSelector {
                name: "a".to_string().into(),
                op,
                value: OptionString::Some("v".to_string().into()),
            };
            assert_eq!(format!("{sel}"), expected);
        }
    }
    #[test]
    fn css_attribute_selector_exists_ignores_any_value() {
        // `[attr]` has no right-hand side, so a stray value must be dropped, not printed.
        let sel = CssAttributeSelector {
            name: "a".to_string().into(),
            op: AttributeMatchOp::Exists,
            value: OptionString::Some("ignored".to_string().into()),
        };
        assert_eq!(format!("{sel}"), "[a]");
        assert_eq!(format!("{}", CssAttributeSelector::default()), "[]");
    }
    #[test]
    fn css_attribute_selector_missing_value_renders_an_empty_string_literal() {
        let sel = CssAttributeSelector {
            name: "a".to_string().into(),
            op: AttributeMatchOp::Eq,
            value: OptionString::None,
        };
        assert_eq!(format!("{sel}"), "[a=\"\"]");
    }
    #[test]
    fn css_attribute_selector_display_with_hostile_names_and_values_does_not_panic() {
        for (name, value) in [
            ("", ""),
            ("😀", "😀"),
            ("a\u{0301}", "e\u{0301}"),
            ("a", "has \" quote"),   // NOTE: not escaped — see report
            ("a", "]"),
            ("a", "\u{0}"),
            ("a", "\n"),
        ] {
            let sel = CssAttributeSelector {
                name: name.to_string().into(),
                op: AttributeMatchOp::Eq,
                value: OptionString::Some(value.to_string().into()),
            };
            let out = format!("{sel}");
            assert!(out.starts_with('[') && out.ends_with(']'));
        }
    }
    #[test]
    fn css_nth_child_selector_display_at_numeric_boundaries() {
        assert_eq!(format!("{}", CssNthChildSelector::Number(0)), "0");
        assert_eq!(
            format!("{}", CssNthChildSelector::Number(u32::MAX)),
            u32::MAX.to_string()
        );
        assert_eq!(format!("{}", CssNthChildSelector::Even), "even");
        assert_eq!(format!("{}", CssNthChildSelector::Odd), "odd");
        assert_eq!(
            format!(
                "{}",
                CssNthChildSelector::Pattern(CssNthChildPattern {
                    pattern_repeat: 2,
                    offset: 1,
                })
            ),
            "2n + 1"
        );
        // A=0 / A=B=u32::MAX must not overflow or panic while formatting.
        assert_eq!(
            format!(
                "{}",
                CssNthChildSelector::Pattern(CssNthChildPattern {
                    pattern_repeat: 0,
                    offset: 0,
                })
            ),
            "0n + 0"
        );
        let maxed = CssNthChildSelector::Pattern(CssNthChildPattern {
            pattern_repeat: u32::MAX,
            offset: u32::MAX,
        });
        assert_eq!(
            format!("{maxed}"),
            format!("{}n + {}", u32::MAX, u32::MAX)
        );
    }
    #[test]
    fn css_path_pseudo_selector_display_covers_every_variant() {
        let cases: Vec<(CssPathPseudoSelector, String)> = vec![
            (CssPathPseudoSelector::First, "first".to_string()),
            (CssPathPseudoSelector::Last, "last".to_string()),
            (
                CssPathPseudoSelector::NthChild(CssNthChildSelector::Even),
                "nth-child(even)".to_string(),
            ),
            (CssPathPseudoSelector::Hover, "hover".to_string()),
            (CssPathPseudoSelector::Active, "active".to_string()),
            (CssPathPseudoSelector::Focus, "focus".to_string()),
            (
                CssPathPseudoSelector::Lang("de-DE".to_string().into()),
                "lang(de-DE)".to_string(),
            ),
            (CssPathPseudoSelector::Backdrop, "backdrop".to_string()),
            (CssPathPseudoSelector::Dragging, "dragging".to_string()),
            (CssPathPseudoSelector::DragOver, "drag-over".to_string()),
        ];
        for (p, expected) in cases {
            assert_eq!(format!("{p}"), expected);
        }
    }
    #[test]
    fn css_path_pseudo_selector_lang_with_hostile_payloads_does_not_panic() {
        let long = "l".repeat(100_000);
        for lang in ["", "   ", "😀", "e\u{0301}", ")", "\u{0}", long.as_str()] {
            let p = CssPathPseudoSelector::Lang(lang.to_string().into());
            let out = format!("{p}");
            assert!(out.starts_with("lang(") && out.ends_with(')'));
        }
    }
    // =====================================================================
    // get_specificity
    // =====================================================================
    #[test]
    fn get_specificity_of_an_empty_path_is_all_zero() {
        assert_eq!(get_specificity(&CssPath::default()), (0, 0, 0, 0));
        assert_eq!(get_specificity(&CssPath::new(Vec::new())), (0, 0, 0, 0));
    }
    #[test]
    fn get_specificity_counts_ids_classes_and_types_separately() {
        let path = CssPath::new(vec![
            CssPathSelector::Id("a".to_string().into()),
            CssPathSelector::Id("b".to_string().into()),
            CssPathSelector::Class("c".to_string().into()),
            CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
            CssPathSelector::Attribute(CssAttributeSelector::default()),
            CssPathSelector::Type(NodeTypeTag::Div),
        ]);
        // pseudo-classes and attribute selectors count in the CLASS column (per W3C).
        assert_eq!(get_specificity(&path), (2, 3, 1, 6));
    }
    #[test]
    fn get_specificity_ignores_combinators_and_root_except_in_the_total() {
        let path = CssPath::new(vec![
            CssPathSelector::Root(CssScopeRange { start: 0, end: 9 }),
            CssPathSelector::Global,
            CssPathSelector::Children,
            CssPathSelector::DirectChildren,
            CssPathSelector::AdjacentSibling,
            CssPathSelector::GeneralSibling,
        ]);
        let (ids, classes, types, total) = get_specificity(&path);
        assert_eq!((ids, classes, types), (0, 0, 0));
        assert_eq!(total, 6, "the 4th field is the raw selector count");
    }
    #[test]
    fn get_specificity_orders_ids_above_classes_above_types() {
        let id = get_specificity(&CssPath::new(vec![CssPathSelector::Id("x".to_string().into())]));
        let class = get_specificity(&CssPath::new(vec![CssPathSelector::Class(
            "x".to_string().into(),
        )]));
        let ty = get_specificity(&CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]));
        let star = get_specificity(&CssPath::new(vec![CssPathSelector::Global]));
        assert!(star < ty, "* must be the weakest");
        assert!(ty < class);
        assert!(class < id);
    }
    #[test]
    fn get_specificity_on_a_huge_path_does_not_overflow_or_hang() {
        let sels: Vec<CssPathSelector> = (0..50_000)
            .map(|i| CssPathSelector::Id(format!("i{i}").into()))
            .collect();
        let path = CssPath::new(sels);
        assert_eq!(get_specificity(&path), (50_000, 0, 0, 50_000));
    }
    // =====================================================================
    // Parsers (feature = "parser")
    // =====================================================================
    #[cfg(feature = "parser")]
    fn parse(s: &str) -> Css {
        Css::from_string(s.to_string().into())
    }
    #[cfg(feature = "parser")]
    #[test]
    fn viewport_breakpoints_harvests_media_bounds() {
        let css = Css::from_string(
            "@media (max-width: 400px) { .a { width: 10px; } }\n\
             @media (min-width: 800px) { .b { width: 10px; } }\n\
             @media (max-height: 300px) { .c { width: 10px; } }\n\
             .d { width: 10px; }"
                .into(),
        );
        let (w, h) = css.viewport_breakpoints();
        assert_eq!(w, vec![400.0, 800.0]);
        assert_eq!(h, vec![300.0]);
    }
    #[test]
    fn from_string_on_empty_and_whitespace_only_input_yields_no_rules() {
        for input in ["", " ", "   ", "\t\n\r\n ", "\u{FEFF}", "\u{00A0}"] {
            let css = parse(input);
            assert!(
                css.is_empty(),
                "{input:?} must produce zero rules, got {}",
                css.rules.as_ref().len()
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_on_garbage_never_panics_and_is_deterministic() {
        let garbage = [
            "}}}}",
            "{{{{",
            "@@@@",
            ";;;;",
            "\u{0}\u{1}\u{2}",
            "div {",
            "div }",
            "} div {",
            "div { color",
            "div { color: }",
            "div { : red; }",
            "* * * * *",
            ":::::",
            "[[[[",
            "/* unterminated comment",
            "@media {",
            "url(",
            "\"unterminated",
            "div{color:red;}}}}",
        ];
        for input in garbage {
            let a = parse(input);
            let b = parse(input);
            assert_eq!(a, b, "parsing {input:?} must be deterministic");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_valid_minimal_produces_one_author_rule() {
        let css = parse("div { width: 200px; }");
        assert_eq!(css.rules.as_ref().len(), 1);
        let rule = &css.rules.as_ref()[0];
        assert_eq!(
            rule.priority,
            rule_priority::AUTHOR,
            "parser output belongs to the author layer"
        );
        assert_eq!(
            rule.path.selectors.as_ref(),
            &[CssPathSelector::Type(NodeTypeTag::Div)][..]
        );
        let props: Vec<CssPropertyType> = css
            .iter_inline_properties()
            .map(|(p, _)| p.get_type())
            .collect();
        assert_eq!(props, vec![CssPropertyType::Width]);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_handles_leading_and_trailing_junk_deterministically() {
        // Surrounding whitespace must be irrelevant.
        assert_eq!(parse("  div { width: 1px; }  "), parse("div { width: 1px; }"));
        // Trailing garbage must not eat the valid rule that precedes it.
        let with_junk = parse("div { width: 1px; } @@@ garbage");
        assert!(
            with_junk.rules().any(|r| r.path.selectors.as_ref()
                == &[CssPathSelector::Type(NodeTypeTag::Div)][..]),
            "the leading valid rule must survive trailing junk"
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_on_boundary_numbers_does_not_panic() {
        let inputs = [
            "div { width: 0px; }",
            "div { width: -0px; }",
            "div { width: -1px; }",
            "div { width: 9223372036854775807px; }",   // i64::MAX
            "div { width: 340282350000000000000000000000000000000px; }", // ~f32::MAX
            "div { width: 1e400px; }",                 // f64 overflow → inf
            "div { width: 1e-400px; }",                // f64 underflow → 0
            "div { width: NaN; }",
            "div { width: inf; }",
            "div { width: -inf; }",
            "div { width: 99999999999999999999999999px; }",
            "div { opacity: 1e309; }",
            "div { z-index: -9223372036854775808; }",  // i64::MIN
            "div { width: .....; }",
            "div { width: --5px; }",
        ];
        for input in inputs {
            let css = parse(input);
            assert!(
                css.rules.as_ref().len() <= 1,
                "{input:?} must not explode into multiple rules"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_on_unicode_input_does_not_panic() {
        let inputs = [
            "div { content: \"😀\"; }",
            ".😀 { width: 1px; }",
            "#e\u{0301} { width: 1px; }",
            "div { font-family: \"日本語\"; }",
            "div\u{0301} { width: 1px; }",
            "div { width: 1px; } /* 🎉 */",
            "\u{202E}div { width: 1px; }",  // right-to-left override
        ];
        for input in inputs {
            let css = parse(input);
            // The invariant is "no panic + stable output", not any particular rule count.
            assert_eq!(css, parse(input));
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_on_a_one_megabyte_input_terminates() {
        // 100k declarations inside a single rule ≈ 1_000_000 chars.
        let body = "width:1px;".repeat(100_000);
        assert!(body.len() >= 1_000_000);
        let css = parse(&format!("div{{{body}}}"));
        assert_eq!(css.rules.as_ref().len(), 1);
        assert!(!css.rules.as_ref()[0].declarations.as_ref().is_empty());
        // …and a single 100k-char junk token must not hang either.
        let junk = "a".repeat(100_000);
        let _ = parse(&junk);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_on_deeply_nested_blocks_does_not_stack_overflow() {
        // Run on a dedicated 64 MiB stack: a recursive-descent parser would blow the
        // default 2 MiB test stack and take the whole test binary down with it.
        let depth = 10_000;
        let mut s = String::with_capacity(depth * 8);
        for _ in 0..depth {
            s.push_str("div{");
        }
        s.push_str("width:1px;");
        for _ in 0..depth {
            s.push('}');
        }
        let handle = std::thread::Builder::new()
            .stack_size(64 * 1024 * 1024)
            .spawn(move || Css::from_string(s.into()).rules.as_ref().len())
            .expect("spawning the parser thread must succeed");
        let rule_count = handle
            .join()
            .expect("10_000 nested blocks must not panic or overflow the stack");
        assert!(rule_count <= depth + 1, "rule count must stay bounded by the nesting depth");
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_with_warnings_agrees_with_from_string() {
        for input in [
            "",
            "   ",
            "div { width: 1px; }",
            "div { not-a-property: 1; }",
            "}}} garbage {{{",
            "div { width: NaN; }",
        ] {
            let (css, _warnings) = Css::from_string_with_warnings(input.to_string().into());
            assert_eq!(
                css,
                parse(input),
                "from_string_with_warnings must parse {input:?} identically to from_string"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_with_warnings_reports_an_unknown_property() {
        let (css, warnings) =
            Css::from_string_with_warnings("div { definitely-not-a-property: 1px; }".to_string().into());
        assert!(
            !warnings.is_empty(),
            "an unknown property must surface as a warning rather than being dropped silently"
        );
        // The unknown property is a RECOVERABLE error: the parse keeps going, so the
        // stylesheet must not be torn down around it.
        assert!(css.rules.as_ref().len() <= 1);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn from_string_with_warnings_on_empty_input_has_no_rules() {
        let (css, warnings) = Css::from_string_with_warnings(String::new().into());
        assert!(css.is_empty());
        assert!(warnings.is_empty());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_marks_every_rule_as_the_inline_layer() {
        let css = Css::parse_inline("width: 200px; color: red;");
        assert!(!css.is_empty());
        for r in css.rules() {
            assert_eq!(
                r.priority,
                rule_priority::INLINE,
                "parse_inline must stamp every rule with the INLINE layer"
            );
        }
        let props: Vec<CssPropertyType> = css
            .iter_inline_properties()
            .map(|(p, _)| p.get_type())
            .collect();
        assert!(props.contains(&CssPropertyType::Width));
        assert!(props.contains(&CssPropertyType::TextColor));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_wraps_bare_declarations_in_a_star_rule() {
        let css = Css::parse_inline("width: 200px;");
        assert_eq!(css.rules.as_ref().len(), 1);
        assert_eq!(
            css.rules.as_ref()[0].path.selectors.as_ref(),
            &[CssPathSelector::Global][..],
            "the wrapper path must be exactly `*` — push_front_scope keys node-only \
             inline semantics off that shape"
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_on_empty_and_whitespace_input_does_not_panic() {
        for input in ["", " ", "\t\n", "   \r\n  "] {
            let css = Css::parse_inline(input);
            for r in css.rules() {
                assert_eq!(r.priority, rule_priority::INLINE);
                assert!(
                    r.declarations.as_ref().is_empty(),
                    "an empty inline style must not produce declarations"
                );
            }
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_on_garbage_never_panics_and_is_deterministic() {
        for input in [
            "}}}}",
            "{{{{",
            ";;;;",
            ":::",
            "color",
            "color:",
            ": red",
            "\u{0}\u{1}",
            "/* unterminated",
            "@@@",
            "width: 1px",  // no trailing semicolon
        ] {
            assert_eq!(
                Css::parse_inline(input),
                Css::parse_inline(input),
                "parse_inline({input:?}) must be deterministic"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_on_unicode_and_boundary_numbers_does_not_panic() {
        for input in [
            "content: \"😀\"",
            "font-family: \"日本語\"",
            "width: 0px",
            "width: -0px",
            "width: 9223372036854775807px",
            "width: 1e400px",
            "width: NaN",
            "width: inf",
            "opacity: 1e309",
        ] {
            let css = Css::parse_inline(input);
            for r in css.rules() {
                assert_eq!(r.priority, rule_priority::INLINE);
            }
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_on_a_one_megabyte_style_terminates() {
        let style = "width:1px;".repeat(100_000);
        assert!(style.len() >= 1_000_000);
        let css = Css::parse_inline(&style);
        assert!(!css.is_empty());
        for r in css.rules() {
            assert_eq!(r.priority, rule_priority::INLINE);
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_supports_nested_pseudo_blocks() {
        // Documented feature: `:hover { ... }` works inside an inline style via CSS nesting.
        let css = Css::parse_inline(":hover { color: red; }");
        assert!(!css.is_empty(), "a nested pseudo block must produce a rule");
        for r in css.rules() {
            assert_eq!(r.priority, rule_priority::INLINE);
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_inline_must_not_let_a_brace_escape_the_star_wrapper() {
        // parse_inline builds `* {\n<input>\n}`. A `}` inside the input closes that
        // wrapper early, so whatever follows is parsed as a TOP-LEVEL rule with an
        // attacker-chosen selector. An inline style string must never be able to define
        // rules that target other elements — every rule it produces has to stay rooted
        // at the `*` wrapper (push_front_scope also keys node-only scoping off that).
        let css = Css::parse_inline("color: red; } div { background: green;");
        for r in css.rules() {
            let first = r.path.selectors.as_ref().first();
            assert!(
                matches!(first, None | Some(CssPathSelector::Global)),
                "a `}}` in the inline style escaped the `*` wrapper and produced the \
                 free-standing rule `{}` (selector injection)",
                r.path
            );
        }
    }
}