1
//! Dynamic CSS selectors for runtime evaluation based on OS, media queries, container queries, etc.
2

            
3
use crate::corety::{AzString, OptionString};
4
use crate::props::property::CssProperty;
5

            
6
/// State flags for pseudo-classes (used in `DynamicSelectorContext`)
7
/// Note: This is a CSS-only version. See `azul_core::styled_dom::StyledNodeState` for the main type.
8
//
9
// TODO(superplan g8 item 3): unify with `azul_core::styled_dom::StyledNodeState`
10
// (core/src/styled_dom.rs:190). The two structs now carry the *identical* 10 fields
11
// (hover/active/focused/disabled/checked/focus_within/visited/backdrop/dragging/
12
// drag_over) and core already bridges them via `StyledNodeState::from_pseudo_state_flags`.
13
// `azul_css` cannot depend on `azul_core`, so the merge must land core-side (e.g. move
14
// the shared struct into `azul_css` and re-export from core, or delete one type). This is
15
// a cross-crate change touching core/, left as a TODO per group ownership.
16
#[repr(C)]
17
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
18
pub struct PseudoStateFlags {
19
    pub hover: bool,
20
    pub active: bool,
21
    pub focused: bool,
22
    pub disabled: bool,
23
    pub checked: bool,
24
    pub focus_within: bool,
25
    pub visited: bool,
26
    /// Window is not focused (equivalent to GTK :backdrop)
27
    pub backdrop: bool,
28
    /// Element is currently being dragged (:dragging)
29
    pub dragging: bool,
30
    /// A dragged element is over this drop target (:drag-over)
31
    pub drag_over: bool,
32
}
33

            
34
impl PseudoStateFlags {
35
    /// Check if a specific pseudo-state is active
36
33
    #[must_use] pub const fn has_state(&self, state: PseudoStateType) -> bool {
37
33
        match state {
38
3
            PseudoStateType::Normal => true,
39
3
            PseudoStateType::Hover => self.hover,
40
2
            PseudoStateType::Active => self.active,
41
2
            PseudoStateType::Focus => self.focused,
42
2
            PseudoStateType::Disabled => self.disabled,
43
5
            PseudoStateType::CheckedTrue => self.checked,
44
5
            PseudoStateType::CheckedFalse => !self.checked,
45
2
            PseudoStateType::FocusWithin => self.focus_within,
46
2
            PseudoStateType::Visited => self.visited,
47
2
            PseudoStateType::Backdrop => self.backdrop,
48
2
            PseudoStateType::Dragging => self.dragging,
49
3
            PseudoStateType::DragOver => self.drag_over,
50
        }
51
33
    }
52
}
53

            
54
/// Dynamic selector that is evaluated at runtime
55
/// C-compatible: Tagged union with single field
56
#[repr(C, u8)]
57
#[derive(Debug, Clone, PartialEq)]
58
pub enum DynamicSelector {
59
    /// Operating system condition
60
    Os(OsCondition) = 0,
61
    /// Operating system version (e.g. macOS 14.0, Windows 11)
62
    OsVersion(OsVersionCondition) = 1,
63
    /// Media query (print/screen)
64
    Media(MediaType) = 2,
65
    /// Viewport width min/max (for @media)
66
    ViewportWidth(MinMaxRange) = 3,
67
    /// Viewport height min/max (for @media)
68
    ViewportHeight(MinMaxRange) = 4,
69
    /// Container width min/max (for @container)
70
    ContainerWidth(MinMaxRange) = 5,
71
    /// Container height min/max (for @container)
72
    ContainerHeight(MinMaxRange) = 6,
73
    /// Container name (for named @container queries)
74
    ContainerName(AzString) = 7,
75
    /// Theme (dark/light/custom)
76
    Theme(ThemeCondition) = 8,
77
    /// Aspect Ratio (min/max for @media and @container)
78
    AspectRatio(MinMaxRange) = 9,
79
    /// Orientation (portrait/landscape)
80
    Orientation(OrientationType) = 10,
81
    /// Reduced Motion (accessibility)
82
    PrefersReducedMotion(BoolCondition) = 11,
83
    /// High Contrast (accessibility)
84
    PrefersHighContrast(BoolCondition) = 12,
85
    /// Pseudo-State (hover, active, focus, etc.)
86
    PseudoState(PseudoStateType) = 13,
87
    /// Language/Locale (for @lang("de-DE"))
88
    /// Matches BCP 47 language tags (e.g., "de", "de-DE", "en-US")
89
    Language(LanguageCondition) = 14,
90
}
91

            
92
impl_option!(
93
    DynamicSelector,
94
    OptionDynamicSelector,
95
    copy = false,
96
    [Debug, Clone, PartialEq, Eq]
97
);
98

            
99
impl_vec!(DynamicSelector, DynamicSelectorVec, DynamicSelectorVecDestructor, DynamicSelectorVecDestructorType, DynamicSelectorVecSlice, OptionDynamicSelector);
100
impl_vec_clone!(
101
    DynamicSelector,
102
    DynamicSelectorVec,
103
    DynamicSelectorVecDestructor
104
);
105
impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
106
impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
107

            
108
impl DynamicSelector {
109
    /// Stable per-variant tag (mirrors the `#[repr(C, u8)]` discriminants), used as
110
    /// the primary key for both `Ord` and `Hash` so the two stay consistent.
111
110
    const fn variant_tag(&self) -> u8 {
112
110
        match self {
113
5
            Self::Os(_) => 0,
114
6
            Self::OsVersion(_) => 1,
115
6
            Self::Media(_) => 2,
116
12
            Self::ViewportWidth(_) => 3,
117
8
            Self::ViewportHeight(_) => 4,
118
8
            Self::ContainerWidth(_) => 5,
119
6
            Self::ContainerHeight(_) => 6,
120
6
            Self::ContainerName(_) => 7,
121
6
            Self::Theme(_) => 8,
122
6
            Self::AspectRatio(_) => 9,
123
6
            Self::Orientation(_) => 10,
124
6
            Self::PrefersReducedMotion(_) => 11,
125
6
            Self::PrefersHighContrast(_) => 12,
126
18
            Self::PseudoState(_) => 13,
127
5
            Self::Language(_) => 14,
128
        }
129
110
    }
130
}
131

            
132
// `DynamicSelector` carries `f32` ranges (`MinMaxRange`), so `Eq`/`Ord`/`Hash`
133
// cannot be derived. They are implemented by hand here: every non-float payload
134
// already provides them, and the float ranges are compared/hashed by their bit
135
// pattern so the resulting order is *total* and consistent with `Hash`. (Bit
136
// comparison means NaN sentinels sort deterministically instead of being
137
// incomparable.)
138
impl Eq for DynamicSelector {}
139

            
140
impl PartialOrd for DynamicSelector {
141
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
142
        Some(self.cmp(other))
143
    }
144
}
145

            
146
impl Ord for DynamicSelector {
147
    // Order-dependent tie-break arms with identical bodies can't merge without
148
    // changing the ordering (clippy::match_same_arms false positive).
149
    #[allow(clippy::match_same_arms)]
150
21
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
151
        use core::cmp::Ordering;
152
21
        match self.variant_tag().cmp(&other.variant_tag()) {
153
6
            Ordering::Equal => {}
154
15
            non_eq => return non_eq,
155
        }
156
        // Same variant on both sides (tags are equal): compare the payloads.
157
6
        match (self, other) {
158
            (Self::Os(a), Self::Os(b)) => a.cmp(b),
159
            (Self::OsVersion(a), Self::OsVersion(b)) => a.cmp(b),
160
            (Self::Media(a), Self::Media(b)) => a.cmp(b),
161
            (Self::ContainerName(a), Self::ContainerName(b)) => a.cmp(b),
162
            (Self::Theme(a), Self::Theme(b)) => a.cmp(b),
163
            (Self::Orientation(a), Self::Orientation(b)) => a.cmp(b),
164
            (Self::PrefersReducedMotion(a), Self::PrefersReducedMotion(b)) => {
165
                a.cmp(b)
166
            }
167
            (Self::PrefersHighContrast(a), Self::PrefersHighContrast(b)) => {
168
                a.cmp(b)
169
            }
170
4
            (Self::PseudoState(a), Self::PseudoState(b)) => a.cmp(b),
171
            (Self::Language(a), Self::Language(b)) => a.cmp(b),
172
1
            (Self::ViewportWidth(a), Self::ViewportWidth(b))
173
1
            | (Self::ViewportHeight(a), Self::ViewportHeight(b))
174
            | (Self::ContainerWidth(a), Self::ContainerWidth(b))
175
            | (Self::ContainerHeight(a), Self::ContainerHeight(b))
176
            | (Self::AspectRatio(a), Self::AspectRatio(b)) => {
177
2
                (a.min.to_bits(), a.max.to_bits()).cmp(&(b.min.to_bits(), b.max.to_bits()))
178
            }
179
            // Unreachable: tags are equal, so both sides are the same variant.
180
            _ => Ordering::Equal,
181
        }
182
21
    }
183
}
184

            
185
impl core::hash::Hash for DynamicSelector {
186
    // Per-variant dispatch: each `x` is a different type, so the identical
187
    // `x.hash(state)` bodies can't merge (clippy::match_same_arms false positive).
188
    #[allow(clippy::match_same_arms)]
189
8
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
190
8
        self.variant_tag().hash(state);
191
8
        match self {
192
            Self::Os(x) => x.hash(state),
193
            Self::OsVersion(x) => x.hash(state),
194
            Self::Media(x) => x.hash(state),
195
            Self::ContainerName(x) => x.hash(state),
196
            Self::Theme(x) => x.hash(state),
197
            Self::Orientation(x) => x.hash(state),
198
            Self::PrefersReducedMotion(x) => x.hash(state),
199
            Self::PrefersHighContrast(x) => x.hash(state),
200
4
            Self::PseudoState(x) => x.hash(state),
201
            Self::Language(x) => x.hash(state),
202
3
            Self::ViewportWidth(r)
203
            | Self::ViewportHeight(r)
204
1
            | Self::ContainerWidth(r)
205
            | Self::ContainerHeight(r)
206
4
            | Self::AspectRatio(r) => {
207
4
                r.min.to_bits().hash(state);
208
4
                r.max.to_bits().hash(state);
209
4
            }
210
        }
211
8
    }
212
}
213

            
214
/// Min/Max Range for numeric conditions (C-compatible)
215
#[repr(C)]
216
#[derive(Debug, Clone, Copy)]
217
pub struct MinMaxRange {
218
    /// Minimum value (NaN = no minimum limit)
219
    pub min: f32,
220
    /// Maximum value (NaN = no maximum limit)
221
    pub max: f32,
222
}
223

            
224
// The f32 fields use NaN as the "no bound" sentinel, so equality and order compare by
225
// BIT PATTERN (via to_bits), NOT raw float `==`/`<`. Deriving them used raw float, under
226
// which a NaN-bounded range — i.e. EVERY single-sided `(min-width: …)` / `(max-width: …)`
227
// selector — was not equal to itself, breaking the `Eq` contract that `DynamicSelector`
228
// asserts, and disagreeing with `DynamicSelector::cmp` (which already orders these fields
229
// via to_bits). PartialEq and PartialOrd must move together: a to_bits PartialEq with a
230
// raw-float PartialOrd would itself be inconsistent (NaN == NaN true, partial_cmp None).
231
// The sentinel is always the canonical `f32::NAN`, so all sentinels share one bit pattern.
232
impl PartialEq for MinMaxRange {
233
314
    fn eq(&self, other: &Self) -> bool {
234
314
        self.min.to_bits() == other.min.to_bits() && self.max.to_bits() == other.max.to_bits()
235
314
    }
236
}
237

            
238
impl Eq for MinMaxRange {}
239

            
240
// NB: deliberately NO `impl Ord` — `Ord::min`/`Ord::max` take `self` by value and would
241
// shadow the inherent `min(&self)`/`max(&self)` getters in method resolution (the by-value
242
// receiver is tried before autoref to `&self`), breaking every `range.min()` call.
243
// `PartialOrd` is fine (it adds no `min`/`max` method) and gives a total, to_bits-based
244
// order consistent with `PartialEq`. `DynamicSelector::cmp` orders these fields directly,
245
// so it never needed `MinMaxRange: Ord` anyway.
246
impl PartialOrd for MinMaxRange {
247
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
248
        Some(
249
            (self.min.to_bits(), self.max.to_bits())
250
                .cmp(&(other.min.to_bits(), other.max.to_bits())),
251
        )
252
    }
253
}
254

            
255
impl MinMaxRange {
256
235
    #[must_use] pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
257
        Self {
258
235
            min: if let Some(m) = min { m } else { f32::NAN },
259
235
            max: if let Some(m) = max { m } else { f32::NAN },
260
        }
261
235
    }
262
    
263
    /// Create a range with only a minimum value (>= min)
264
26
    #[must_use] pub const fn with_min(min_val: f32) -> Self {
265
26
        Self {
266
26
            min: min_val,
267
26
            max: f32::NAN,
268
26
        }
269
26
    }
270
    
271
    /// Create a range with only a maximum value (<= max)
272
13
    #[must_use] pub const fn with_max(max_val: f32) -> Self {
273
13
        Self {
274
13
            min: f32::NAN,
275
13
            max: max_val,
276
13
        }
277
13
    }
278

            
279
24
    #[must_use] pub const fn min(&self) -> Option<f32> {
280
24
        if self.min.is_nan() {
281
6
            None
282
        } else {
283
18
            Some(self.min)
284
        }
285
24
    }
286

            
287
23
    #[must_use] pub const fn max(&self) -> Option<f32> {
288
23
        if self.max.is_nan() {
289
9
            None
290
        } else {
291
14
            Some(self.max)
292
        }
293
23
    }
294

            
295
150664
    #[must_use] pub fn matches(&self, value: f32) -> bool {
296
150664
        let min_ok = self.min.is_nan() || value >= self.min;
297
150664
        let max_ok = self.max.is_nan() || value <= self.max;
298
150664
        min_ok && max_ok
299
150664
    }
300
}
301

            
302
/// Boolean condition (C-compatible)
303
#[repr(C)]
304
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
305
pub enum BoolCondition {
306
    #[default]
307
    False,
308
    True,
309
}
310

            
311
impl From<bool> for BoolCondition {
312
2
    fn from(b: bool) -> Self {
313
2
        if b {
314
1
            Self::True
315
        } else {
316
1
            Self::False
317
        }
318
2
    }
319
}
320

            
321
impl From<BoolCondition> for bool {
322
20
    fn from(b: BoolCondition) -> Self {
323
20
        matches!(b, BoolCondition::True)
324
20
    }
325
}
326

            
327
/// Operating system condition for `@os` CSS selectors
328
#[repr(C)]
329
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
330
pub enum OsCondition {
331
    Any,
332
    Apple, // macOS + iOS
333
    MacOS,
334
    IOS,
335
    Linux,
336
    Windows,
337
    Android,
338
    Web, // WASM
339
}
340

            
341
impl_option!(
342
    OsCondition,
343
    OptionOsCondition,
344
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
345
);
346

            
347
impl OsCondition {
348
    /// Convert from `css::system::Platform`
349
597
    #[must_use] pub const fn from_system_platform(platform: &crate::system::Platform) -> Self {
350
        use crate::system::Platform;
351
597
        match platform {
352
1
            Platform::Windows => Self::Windows,
353
1
            Platform::MacOs => Self::MacOS,
354
591
            Platform::Linux(_) => Self::Linux,
355
1
            Platform::Android => Self::Android,
356
1
            Platform::Ios => Self::IOS,
357
2
            Platform::Unknown => Self::Any,
358
        }
359
597
    }
360
}
361

            
362
#[repr(C, u8)]
363
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
364
pub enum OsVersionCondition {
365
    /// Minimum version: >= specified version
366
    /// Format: `OsVersion` { os, `version_id` }
367
    Min(OsVersion),
368
    /// Maximum version: <= specified version
369
    Max(OsVersion),
370
    /// Exact version match
371
    Exact(OsVersion),
372
    /// Desktop environment (Linux only)
373
    DesktopEnvironment(LinuxDesktopEnv),
374
    /// Desktop environment with min version (e.g. `@os(linux:gnome > 40)`)
375
    DesktopEnvMin(DesktopEnvVersion),
376
    /// Desktop environment with max version
377
    DesktopEnvMax(DesktopEnvVersion),
378
    /// Desktop environment with exact version
379
    DesktopEnvExact(DesktopEnvVersion),
380
}
381

            
382
/// A desktop environment together with a numeric version (e.g. GNOME 40).
383
/// Used by `OsVersionCondition::DesktopEnv{Min,Max,Exact}` for `@os(linux:gnome > 40)` style selectors.
384
#[repr(C)]
385
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
386
pub struct DesktopEnvVersion {
387
    pub env: LinuxDesktopEnv,
388
    pub version_id: u32,
389
}
390

            
391
/// OS version with ordering - only comparable within the same OS family
392
/// 
393
/// Each OS has its own version numbering system with named versions.
394
/// Comparisons between different OS families always return false.
395
#[repr(C)]
396
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
397
pub struct OsVersion {
398
    /// Which OS family this version belongs to
399
    pub os: OsFamily,
400
    /// Numeric version ID for ordering (higher = newer)
401
    /// Each OS has its own numbering scheme starting from 0
402
    pub version_id: u32,
403
}
404

            
405
impl Default for OsVersion {
406
36755
    fn default() -> Self {
407
36755
        Self::unknown()
408
36755
    }
409
}
410

            
411
impl OsVersion {
412
51
    #[must_use] pub const fn new(os: OsFamily, version_id: u32) -> Self {
413
51
        Self { os, version_id }
414
51
    }
415
    
416
    /// Compare two versions - only meaningful within the same OS family
417
    /// Returns None if OS families don't match (comparison not meaningful)
418
48
    #[must_use] pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
419
48
        if self.os == other.os {
420
37
            Some(self.version_id.cmp(&other.version_id))
421
        } else {
422
11
            None // Cross-OS comparison not meaningful
423
        }
424
48
    }
425
    
426
    /// Check if self >= other (for Min conditions)
427
17
    #[must_use] pub fn is_at_least(&self, other: &Self) -> bool {
428
17
        self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Less)
429
17
    }
430
    
431
    /// Check if self <= other (for Max conditions)
432
13
    #[must_use] pub fn is_at_most(&self, other: &Self) -> bool {
433
13
        self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Greater)
434
13
    }
435
}
436

            
437
impl_option!(
438
    OsVersion,
439
    OptionOsVersion,
440
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
441
);
442

            
443
impl OsVersion {
444
    
445
    /// Check if self == other
446
11
    #[must_use] pub fn is_exactly(&self, other: &Self) -> bool {
447
11
        self.compare(other) == Some(core::cmp::Ordering::Equal)
448
11
    }
449
}
450

            
451
/// OS family for version comparisons
452
#[repr(C)]
453
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
454
pub enum OsFamily {
455
    Windows,
456
    MacOS,
457
    IOS,
458
    Linux,
459
    Android,
460
}
461

            
462
// ============================================================================
463
// Windows Version IDs (chronological order)
464
// ============================================================================
465

            
466
/// Windows version constants - use these in CSS like `@os(windows >= win-xp)`
467
impl OsVersion {
468
    // Windows versions (version_id = NT version * 100 + minor)
469
    pub const WIN_2000: Self = Self::new(OsFamily::Windows, 500);       // NT 5.0
470
    pub const WIN_XP: Self = Self::new(OsFamily::Windows, 501);         // NT 5.1
471
    pub const WIN_XP_64: Self = Self::new(OsFamily::Windows, 502);      // NT 5.2
472
    pub const WIN_VISTA: Self = Self::new(OsFamily::Windows, 600);      // NT 6.0
473
    pub const WIN_7: Self = Self::new(OsFamily::Windows, 601);          // NT 6.1
474
    pub const WIN_8: Self = Self::new(OsFamily::Windows, 602);          // NT 6.2
475
    pub const WIN_8_1: Self = Self::new(OsFamily::Windows, 603);        // NT 6.3
476
    pub const WIN_10: Self = Self::new(OsFamily::Windows, 1000);        // NT 10.0
477
    pub const WIN_10_1507: Self = Self::new(OsFamily::Windows, 1000);   // Initial release
478
    pub const WIN_10_1511: Self = Self::new(OsFamily::Windows, 1001);   // November Update
479
    pub const WIN_10_1607: Self = Self::new(OsFamily::Windows, 1002);   // Anniversary Update
480
    pub const WIN_10_1703: Self = Self::new(OsFamily::Windows, 1003);   // Creators Update
481
    pub const WIN_10_1709: Self = Self::new(OsFamily::Windows, 1004);   // Fall Creators Update
482
    pub const WIN_10_1803: Self = Self::new(OsFamily::Windows, 1005);   // April 2018 Update
483
    pub const WIN_10_1809: Self = Self::new(OsFamily::Windows, 1006);   // October 2018 Update
484
    pub const WIN_10_1903: Self = Self::new(OsFamily::Windows, 1007);   // May 2019 Update
485
    pub const WIN_10_1909: Self = Self::new(OsFamily::Windows, 1008);   // November 2019 Update
486
    pub const WIN_10_2004: Self = Self::new(OsFamily::Windows, 1009);   // May 2020 Update
487
    pub const WIN_10_20H2: Self = Self::new(OsFamily::Windows, 1010);   // October 2020 Update
488
    pub const WIN_10_21H1: Self = Self::new(OsFamily::Windows, 1011);   // May 2021 Update
489
    pub const WIN_10_21H2: Self = Self::new(OsFamily::Windows, 1012);   // November 2021 Update
490
    pub const WIN_10_22H2: Self = Self::new(OsFamily::Windows, 1013);   // 2022 Update
491
    pub const WIN_11: Self = Self::new(OsFamily::Windows, 1100);        // Windows 11 base
492
    pub const WIN_11_21H2: Self = Self::new(OsFamily::Windows, 1100);   // Initial release
493
    pub const WIN_11_22H2: Self = Self::new(OsFamily::Windows, 1101);   // 2022 Update
494
    pub const WIN_11_23H2: Self = Self::new(OsFamily::Windows, 1102);   // 2023 Update
495
    pub const WIN_11_24H2: Self = Self::new(OsFamily::Windows, 1103);   // 2024 Update
496
    
497
    // macOS versions (version_id = major * 100 + minor)
498
    pub const MACOS_CHEETAH: Self = Self::new(OsFamily::MacOS, 1000);       // 10.0
499
    pub const MACOS_PUMA: Self = Self::new(OsFamily::MacOS, 1001);          // 10.1
500
    pub const MACOS_JAGUAR: Self = Self::new(OsFamily::MacOS, 1002);        // 10.2
501
    pub const MACOS_PANTHER: Self = Self::new(OsFamily::MacOS, 1003);       // 10.3
502
    pub const MACOS_TIGER: Self = Self::new(OsFamily::MacOS, 1004);         // 10.4
503
    pub const MACOS_LEOPARD: Self = Self::new(OsFamily::MacOS, 1005);       // 10.5
504
    pub const MACOS_SNOW_LEOPARD: Self = Self::new(OsFamily::MacOS, 1006);  // 10.6
505
    pub const MACOS_LION: Self = Self::new(OsFamily::MacOS, 1007);          // 10.7
506
    pub const MACOS_MOUNTAIN_LION: Self = Self::new(OsFamily::MacOS, 1008); // 10.8
507
    pub const MACOS_MAVERICKS: Self = Self::new(OsFamily::MacOS, 1009);     // 10.9
508
    pub const MACOS_YOSEMITE: Self = Self::new(OsFamily::MacOS, 1010);      // 10.10
509
    pub const MACOS_EL_CAPITAN: Self = Self::new(OsFamily::MacOS, 1011);    // 10.11
510
    pub const MACOS_SIERRA: Self = Self::new(OsFamily::MacOS, 1012);        // 10.12
511
    pub const MACOS_HIGH_SIERRA: Self = Self::new(OsFamily::MacOS, 1013);   // 10.13
512
    pub const MACOS_MOJAVE: Self = Self::new(OsFamily::MacOS, 1014);        // 10.14
513
    pub const MACOS_CATALINA: Self = Self::new(OsFamily::MacOS, 1015);      // 10.15
514
    pub const MACOS_BIG_SUR: Self = Self::new(OsFamily::MacOS, 1100);       // 11.0
515
    pub const MACOS_MONTEREY: Self = Self::new(OsFamily::MacOS, 1200);      // 12.0
516
    pub const MACOS_VENTURA: Self = Self::new(OsFamily::MacOS, 1300);       // 13.0
517
    pub const MACOS_SONOMA: Self = Self::new(OsFamily::MacOS, 1400);        // 14.0
518
    pub const MACOS_SEQUOIA: Self = Self::new(OsFamily::MacOS, 1500);       // 15.0
519
    pub const MACOS_TAHOE: Self = Self::new(OsFamily::MacOS, 2600);         // 26.0
520
    
521
    // iOS versions (version_id = major * 100 + minor)
522
    pub const IOS_1: Self = Self::new(OsFamily::IOS, 100);
523
    pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
524
    pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
525
    pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
526
    pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
527
    pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
528
    pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
529
    pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
530
    pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
531
    pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
532
    pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
533
    pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
534
    pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
535
    pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
536
    pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
537
    pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
538
    pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
539
    pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
540
    
541
    // Android versions (API level as version_id)
542
    pub const ANDROID_CUPCAKE: Self = Self::new(OsFamily::Android, 3);      // 1.5
543
    pub const ANDROID_DONUT: Self = Self::new(OsFamily::Android, 4);        // 1.6
544
    pub const ANDROID_ECLAIR: Self = Self::new(OsFamily::Android, 7);       // 2.1
545
    pub const ANDROID_FROYO: Self = Self::new(OsFamily::Android, 8);        // 2.2
546
    pub const ANDROID_GINGERBREAD: Self = Self::new(OsFamily::Android, 10); // 2.3
547
    pub const ANDROID_HONEYCOMB: Self = Self::new(OsFamily::Android, 13);   // 3.2
548
    pub const ANDROID_ICE_CREAM_SANDWICH: Self = Self::new(OsFamily::Android, 15); // 4.0
549
    pub const ANDROID_JELLY_BEAN: Self = Self::new(OsFamily::Android, 18);  // 4.3
550
    pub const ANDROID_KITKAT: Self = Self::new(OsFamily::Android, 19);      // 4.4
551
    pub const ANDROID_LOLLIPOP: Self = Self::new(OsFamily::Android, 22);    // 5.1
552
    pub const ANDROID_MARSHMALLOW: Self = Self::new(OsFamily::Android, 23); // 6.0
553
    pub const ANDROID_NOUGAT: Self = Self::new(OsFamily::Android, 25);      // 7.1
554
    pub const ANDROID_OREO: Self = Self::new(OsFamily::Android, 27);        // 8.1
555
    pub const ANDROID_PIE: Self = Self::new(OsFamily::Android, 28);         // 9.0
556
    pub const ANDROID_10: Self = Self::new(OsFamily::Android, 29);          // 10
557
    pub const ANDROID_11: Self = Self::new(OsFamily::Android, 30);          // 11
558
    pub const ANDROID_12: Self = Self::new(OsFamily::Android, 31);          // 12
559
    pub const ANDROID_12L: Self = Self::new(OsFamily::Android, 32);         // 12L
560
    pub const ANDROID_13: Self = Self::new(OsFamily::Android, 33);          // 13
561
    pub const ANDROID_14: Self = Self::new(OsFamily::Android, 34);          // 14
562
    pub const ANDROID_15: Self = Self::new(OsFamily::Android, 35);          // 15
563
    
564
    // Linux kernel versions (major * 1000 + minor * 10 + patch)
565
    pub const LINUX_2_6: Self = Self::new(OsFamily::Linux, 2060);
566
    pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
567
    pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
568
    pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
569
    pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
570
    
571
    /// Unknown OS version (for when detection fails or OS is unknown)
572
946295
    #[must_use] pub const fn unknown() -> Self {
573
946295
        Self {
574
946295
            os: OsFamily::Linux, // Fallback, but version_id 0 means "unknown"
575
946295
            version_id: 0,
576
946295
        }
577
946295
    }
578
}
579

            
580
/// Parse a named or numeric OS version string
581
/// Returns None if the version string is not recognized
582
544
#[must_use] pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
583
544
    let version_str = version_str.trim().to_lowercase();
584
544
    let version_str = version_str.as_str();
585
    
586
544
    match os {
587
139
        OsFamily::Windows => parse_windows_version(version_str),
588
109
        OsFamily::MacOS => parse_macos_version(version_str),
589
94
        OsFamily::IOS => parse_ios_version(version_str),
590
95
        OsFamily::Android => parse_android_version(version_str),
591
107
        OsFamily::Linux => parse_linux_version(version_str),
592
    }
593
544
}
594

            
595
168
fn parse_windows_version(s: &str) -> Option<OsVersion> {
596
    // Strip optional "win"/"windows" prefix (allowing -, _ separators).
597
    // This collapses "11", "win11", "win-11", "windows11", "windows-11", "windows_11" to "11".
598
168
    let core = strip_os_prefix(s, &["windows", "win"]);
599
168
    match core {
600
        // Each version groups its named alias with the numeric NT version.
601
168
        "2000" | "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
602
168
        "xp" | "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
603
166
        "vista" | "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
604
164
        "7" | "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
605
164
        "8" | "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
606
164
        "8.1" | "8-1" | "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
607
162
        "10" | "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
608
161
        "11" => Some(OsVersion::WIN_11),
609
107
        _ => None,
610
    }
611
168
}
612

            
613
/// If `s` starts with any of the given prefixes, strip the prefix plus an optional
614
/// trailing `-` or `_` separator. Otherwise return `s` unchanged. Matching is
615
/// case-insensitive (callers already lowercase, this just makes the helper safe).
616
314
fn strip_os_prefix<'a>(s: &'a str, prefixes: &[&str]) -> &'a str {
617
717
    for p in prefixes {
618
469
        if let Some(rest) = s.strip_prefix(p) {
619
66
            return rest.strip_prefix(['-', '_']).unwrap_or(rest);
620
403
        }
621
    }
622
248
    s
623
314
}
624

            
625
141
fn parse_macos_version(s: &str) -> Option<OsVersion> {
626
141
    match s {
627
141
        "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
628
138
        "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
629
137
        "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
630
136
        "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
631
135
        "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
632
134
        "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
633
133
        "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
634
131
        "lion" | "10.7" => Some(OsVersion::MACOS_LION),
635
130
        "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
636
130
        "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
637
130
        "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
638
130
        "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
639
130
        "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
640
130
        "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
641
130
        "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
642
129
        "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
643
128
        "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
644
122
        "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
645
121
        "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
646
120
        "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
647
103
        "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
648
102
        "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
649
100
        _ => None,
650
    }
651
141
}
652

            
653
102
fn parse_ios_version(s: &str) -> Option<OsVersion> {
654
102
    match s {
655
102
        "1" | "1.0" => Some(OsVersion::IOS_1),
656
101
        "2" | "2.0" => Some(OsVersion::IOS_2),
657
101
        "3" | "3.0" => Some(OsVersion::IOS_3),
658
101
        "4" | "4.0" => Some(OsVersion::IOS_4),
659
101
        "5" | "5.0" => Some(OsVersion::IOS_5),
660
101
        "6" | "6.0" => Some(OsVersion::IOS_6),
661
101
        "7" | "7.0" => Some(OsVersion::IOS_7),
662
101
        "8" | "8.0" => Some(OsVersion::IOS_8),
663
101
        "9" | "9.0" => Some(OsVersion::IOS_9),
664
101
        "10" | "10.0" => Some(OsVersion::IOS_10),
665
101
        "11" | "11.0" => Some(OsVersion::IOS_11),
666
101
        "12" | "12.0" => Some(OsVersion::IOS_12),
667
101
        "13" | "13.0" => Some(OsVersion::IOS_13),
668
101
        "14" | "14.0" => Some(OsVersion::IOS_14),
669
101
        "15" | "15.0" => Some(OsVersion::IOS_15),
670
101
        "16" | "16.0" => Some(OsVersion::IOS_16),
671
101
        "17" | "17.0" => Some(OsVersion::IOS_17),
672
99
        "18" | "18.0" => Some(OsVersion::IOS_18),
673
98
        _ => None,
674
    }
675
102
}
676

            
677
112
fn parse_android_version(s: &str) -> Option<OsVersion> {
678
112
    match s {
679
112
        "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
680
112
        "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
681
112
        "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
682
112
        "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
683
112
        "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
684
112
        "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
685
112
        "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
686
112
        "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
687
112
        "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
688
111
        "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
689
111
        "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
690
111
        "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
691
111
        "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
692
111
        "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
693
111
        "10" | "q" => Some(OsVersion::ANDROID_10),
694
110
        "11" | "r" => Some(OsVersion::ANDROID_11),
695
110
        "12" | "s" => Some(OsVersion::ANDROID_12),
696
110
        "12l" | "12L" => Some(OsVersion::ANDROID_12L),
697
110
        "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
698
105
        "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
699
105
        "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
700
        _ => {
701
            // Try parsing as API level
702
104
            if let Some(api) = s.strip_prefix("api") {
703
11
                if let Ok(level) = api.trim().parse::<u32>() {
704
5
                    return Some(OsVersion::new(OsFamily::Android, level));
705
6
                }
706
93
            }
707
99
            None
708
        }
709
    }
710
112
}
711

            
712
132
fn parse_linux_version(s: &str) -> Option<OsVersion> {
713
    // Strip optional "linux" prefix so "linux6.1" / "linux-6.1" also work.
714
132
    let s = strip_os_prefix(s, &["linux"]);
715
    // Parse kernel version like "5.4", "6.0", or bare major like "5" (== "5.0").
716
132
    let mut parts = s.split('.');
717
132
    let major = parts.next()?.parse::<u32>().ok()?;
718
36
    let minor = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
719
32
    let patch = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
720
    // Checked: this string comes straight from CSS text via parse_os_at_rule_content
721
    // (`@os(linux >= 5000000)` parses fine), and there is no digit-count precondition.
722
    // Unchecked `major * 1000 + minor * 10 + patch` overflowed u32 and panicked.
723
32
    let encoded = major
724
32
        .checked_mul(1000)?
725
29
        .checked_add(minor.checked_mul(10)?)?
726
28
        .checked_add(patch)?;
727
28
    Some(OsVersion::new(OsFamily::Linux, encoded))
728
132
}
729

            
730
/// Linux desktop environment for `@os(linux:<de>)` CSS selectors.
731
///
732
/// Note: `from_system_desktop_env` currently only maps Gnome, KDE, and Other.
733
/// XFCE, Unity, Cinnamon, and MATE can be matched via CSS parsing (`@os(linux:xfce)`)
734
/// but will not be auto-detected from the system — they map to `Other` at runtime.
735
#[repr(C)]
736
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
737
pub enum LinuxDesktopEnv {
738
    Gnome,
739
    KDE,
740
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
741
    XFCE,
742
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
743
    Unity,
744
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
745
    Cinnamon,
746
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
747
    MATE,
748
    Other,
749
}
750

            
751
impl LinuxDesktopEnv {
752
    /// Convert from `css::system::DesktopEnvironment`
753
595
    #[must_use] pub const fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
754
        use crate::system::DesktopEnvironment;
755
595
        match de {
756
589
            DesktopEnvironment::Gnome => Self::Gnome,
757
2
            DesktopEnvironment::Kde => Self::KDE,
758
4
            DesktopEnvironment::Other(_) => Self::Other,
759
        }
760
595
    }
761
}
762

            
763
/// Media type for `@media` CSS selectors (screen, print, all)
764
#[repr(C)]
765
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
766
pub enum MediaType {
767
    Screen,
768
    Print,
769
    All,
770
}
771
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
772
#[repr(C, u8)]
773
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
774
pub enum ThemeCondition {
775
    Light,
776
    Dark,
777
    Custom(AzString),
778
    /// System preference
779
    SystemPreferred,
780
}
781

            
782
impl_option!(
783
    ThemeCondition,
784
    OptionThemeCondition,
785
    copy = false,
786
    [Debug, Clone, PartialEq, Eq, Hash]
787
);
788

            
789
impl ThemeCondition {
790
    /// Convert from `css::system::Theme`
791
592
    #[must_use] pub const fn from_system_theme(theme: crate::system::Theme) -> Self {
792
        use crate::system::Theme;
793
592
        match theme {
794
591
            Theme::Light => Self::Light,
795
1
            Theme::Dark => Self::Dark,
796
        }
797
592
    }
798
}
799

            
800
/// Orientation type for `@media (orientation: ...)` CSS selectors
801
#[repr(C)]
802
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
803
pub enum OrientationType {
804
    Portrait,
805
    Landscape,
806
}
807

            
808
/// Language/Locale condition for @`lang()` CSS selector
809
/// Matches BCP 47 language tags with prefix matching
810
#[repr(C, u8)]
811
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
812
pub enum LanguageCondition {
813
    /// Exact match (e.g., "de-DE" matches only "de-DE")
814
    Exact(AzString),
815
    /// Prefix match (e.g., "de" matches "de", "de-DE", "de-AT", etc.)
816
    Prefix(AzString),
817
}
818

            
819
impl LanguageCondition {
820
    /// Check if this condition matches the given language tag
821
149
    #[must_use] pub fn matches(&self, language: &str) -> bool {
822
149
        match self {
823
42
            Self::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
824
107
            Self::Prefix(prefix) => {
825
107
                let prefix_str = prefix.as_str();
826
107
                if language.len() < prefix_str.len() {
827
18
                    return false;
828
89
                }
829
                // Check if language starts with prefix (case-insensitive).
830
                // `get` (not a raw index): the byte-LENGTH guard above says nothing
831
                // about char boundaries, so a multi-byte language tag -- which `:lang()`
832
                // accepts, it is arbitrary UTF-8 -- would slice mid-character and panic.
833
                // A split inside a character is never a prefix match anyway.
834
89
                let Some(lang_prefix) = language.get(..prefix_str.len()) else {
835
2
                    return false;
836
                };
837
87
                if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
838
23
                    return false;
839
64
                }
840
                // Must be exact match or followed by '-'
841
64
                language.len() == prefix_str.len()
842
48
                    || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
843
            }
844
        }
845
149
    }
846
}
847

            
848
#[repr(C)]
849
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
850
pub enum PseudoStateType {
851
    /// No special state (corresponds to "Normal" in `NodeDataInlineCssProperty`)
852
    Normal,
853
    /// Element is being hovered (:hover)
854
    Hover,
855
    /// Element is active/being clicked (:active)
856
    Active,
857
    /// Element has focus (:focus)
858
    Focus,
859
    /// Element is disabled (:disabled)
860
    Disabled,
861
    /// Element is checked/selected (:checked)
862
    CheckedTrue,
863
    /// Element is unchecked (:not(:checked))
864
    CheckedFalse,
865
    /// Element or child has focus (:focus-within)
866
    FocusWithin,
867
    /// Link has been visited (:visited)
868
    Visited,
869
    /// Window is not focused (:backdrop) - GTK compatibility
870
    Backdrop,
871
    /// Element is currently being dragged (:dragging)
872
    Dragging,
873
    /// A dragged element is over this drop target (:drag-over)
874
    DragOver,
875
}
876

            
877
impl_option!(
878
    LinuxDesktopEnv,
879
    OptionLinuxDesktopEnv,
880
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
881
);
882

            
883
/// Default viewport width used when actual window size is not yet known.
884
pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
885
/// Default viewport height used when actual window size is not yet known.
886
pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
887

            
888
/// Context for evaluating dynamic selectors
889
///
890
/// `PartialEq` is IMPLEMENTED MANUALLY (not derived): `container_width`
891
/// / `container_height` use `f32::NAN` as the "no container" sentinel,
892
/// and derived float equality makes NaN != NaN — so two identical
893
/// contexts never compared equal, `set_dynamic_selector_context`'s
894
/// early return never fired, and EVERY context set paid a full author
895
/// restyle (~2-4 ms at document scale, measured by the
896
/// `media_restyle_cost` workbench). The manual impl compares the f32
897
/// fields by bit pattern, which treats the NaN sentinel as equal to
898
/// itself and is exactly the "did anything change" question this
899
/// equality exists to answer.
900
#[repr(C)]
901
#[derive(Debug, Clone)]
902
pub struct DynamicSelectorContext {
903
    /// Operating system info
904
    pub os: OsCondition,
905
    pub os_version: OsVersion,
906
    pub desktop_env: OptionLinuxDesktopEnv,
907
    /// Numeric version of the active desktop environment (0 = unknown).
908
    /// Used by `@os(linux:gnome > 40)` style selectors. A value of 0 never
909
    /// satisfies any DE-version constraint, so detection can be wired up
910
    /// later without breaking parsed rules.
911
    pub de_version: u32,
912

            
913
    /// Theme info
914
    pub theme: ThemeCondition,
915

            
916
    /// Media info (from `WindowState`)
917
    pub media_type: MediaType,
918
    pub viewport_width: f32,
919
    pub viewport_height: f32,
920

            
921
    /// Container info (from parent node)
922
    /// NaN = no container
923
    pub container_width: f32,
924
    pub container_height: f32,
925
    pub container_name: OptionString,
926

            
927
    /// Accessibility preferences
928
    pub prefers_reduced_motion: BoolCondition,
929
    pub prefers_high_contrast: BoolCondition,
930

            
931
    /// Orientation
932
    pub orientation: OrientationType,
933

            
934
    /// Node state (hover, active, focus, disabled, checked, `focus_within`, visited)
935
    pub pseudo_state: PseudoStateFlags,
936

            
937
    /// Language/Locale (BCP 47 tag, e.g., "en-US", "de-DE")
938
    pub language: AzString,
939

            
940
    /// Whether the window currently has focus (for :backdrop pseudo-class)
941
    /// When false, :backdrop styles should be applied
942
    pub window_focused: bool,
943
}
944

            
945
impl PartialEq for DynamicSelectorContext {
946
3252
    fn eq(&self, other: &Self) -> bool {
947
        // f32 fields by BIT pattern: the NaN "no container" sentinel must
948
        // equal itself (see the struct doc — derived float equality made
949
        // every context set pay a full restyle).
950
3252
        self.os == other.os
951
3252
            && self.os_version == other.os_version
952
3252
            && self.desktop_env == other.desktop_env
953
3252
            && self.de_version == other.de_version
954
3252
            && self.theme == other.theme
955
3252
            && self.media_type == other.media_type
956
3252
            && self.viewport_width.to_bits() == other.viewport_width.to_bits()
957
2388
            && self.viewport_height.to_bits() == other.viewport_height.to_bits()
958
2388
            && self.container_width.to_bits() == other.container_width.to_bits()
959
2388
            && self.container_height.to_bits() == other.container_height.to_bits()
960
2388
            && self.container_name == other.container_name
961
2388
            && self.prefers_reduced_motion == other.prefers_reduced_motion
962
2388
            && self.prefers_high_contrast == other.prefers_high_contrast
963
2388
            && self.orientation == other.orientation
964
2388
            && self.pseudo_state == other.pseudo_state
965
2388
            && self.language == other.language
966
2388
            && self.window_focused == other.window_focused
967
3252
    }
968
}
969

            
970
impl Default for DynamicSelectorContext {
971
831377
    fn default() -> Self {
972
831377
        Self {
973
831377
            os: OsCondition::Any,
974
831377
            os_version: OsVersion::unknown(),
975
831377
            desktop_env: OptionLinuxDesktopEnv::None,
976
831377
            de_version: 0,
977
831377
            theme: ThemeCondition::Light,
978
831377
            media_type: MediaType::Screen,
979
831377
            viewport_width: DEFAULT_VIEWPORT_WIDTH,
980
831377
            viewport_height: DEFAULT_VIEWPORT_HEIGHT,
981
831377
            container_width: f32::NAN,
982
831377
            container_height: f32::NAN,
983
831377
            container_name: OptionString::None,
984
831377
            prefers_reduced_motion: BoolCondition::False,
985
831377
            prefers_high_contrast: BoolCondition::False,
986
831377
            orientation: OrientationType::Landscape,
987
831377
            pseudo_state: PseudoStateFlags::default(),
988
831377
            language: AzString::from_const_str("en-US"),
989
831377
            window_focused: true,
990
831377
        }
991
831377
    }
992
}
993

            
994
impl DynamicSelectorContext {
995
    /// Create a context from `SystemStyle`
996
590
    #[must_use] pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
997
590
        let os = OsCondition::from_system_platform(&system_style.platform);
998
590
        let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
999
589
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
        } else {
1
            OptionLinuxDesktopEnv::None
        };
590
        let theme = ThemeCondition::from_system_theme(system_style.theme);
590
        Self {
590
            os,
590
            os_version: system_style.os_version, // Use version from SystemStyle
590
            desktop_env,
590
            de_version: 0, // TODO: wire up DE version detection in system::detect_*
590
            theme,
590
            media_type: MediaType::Screen,
590
            viewport_width: DEFAULT_VIEWPORT_WIDTH, // Will be updated with window size
590
            viewport_height: DEFAULT_VIEWPORT_HEIGHT,
590
            container_width: f32::NAN,
590
            container_height: f32::NAN,
590
            container_name: OptionString::None,
590
            prefers_reduced_motion: system_style.prefers_reduced_motion,
590
            prefers_high_contrast: system_style.prefers_high_contrast,
590
            orientation: OrientationType::Landscape,
590
            pseudo_state: PseudoStateFlags::default(),
590
            language: system_style.language.clone(),
590
            window_focused: true,
590
        }
590
    }
    /// Update viewport dimensions (e.g., on window resize)
7585
    #[must_use] pub fn with_viewport(&self, width: f32, height: f32) -> Self {
7585
        let mut ctx = self.clone();
7585
        ctx.viewport_width = width;
7585
        ctx.viewport_height = height;
7585
        ctx.orientation = if width > height {
6794
            OrientationType::Landscape
        } else {
791
            OrientationType::Portrait
        };
7585
        ctx
7585
    }
    /// Update container dimensions (for @container queries)
4
    #[must_use] pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
4
        let mut ctx = self.clone();
4
        ctx.container_width = width;
4
        ctx.container_height = height;
4
        ctx.container_name = name.into();
4
        ctx
4
    }
    /// Update pseudo-state (hover, active, focus, etc.)
5
    #[must_use] pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
5
        let mut ctx = self.clone();
5
        ctx.pseudo_state = state;
5
        ctx
5
    }
    /// Check if viewport changed significantly (for breakpoint detection)
12
    #[must_use] pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
23
        for bp in breakpoints {
16
            let self_above = self.viewport_width >= *bp;
16
            let other_above = other.viewport_width >= *bp;
16
            if self_above != other_above {
5
                return true;
11
            }
        }
7
        false
12
    }
}
impl DynamicSelector {
    /// Check if this selector matches in the given context
19172106
    #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
19172106
        match self {
18065267
            Self::Os(os) => Self::match_os(*os, ctx.os),
30
            Self::OsVersion(ver) => Self::match_os_version(ver, ctx.os_version, ctx.desktop_env, ctx.de_version),
12
            Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
150522
            Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
2
            Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
3
            Self::ContainerWidth(range) => {
3
                !ctx.container_width.is_nan() && range.matches(ctx.container_width)
            }
3
            Self::ContainerHeight(range) => {
3
                !ctx.container_height.is_nan() && range.matches(ctx.container_height)
            }
6
            Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
906028
            Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
5
            Self::AspectRatio(range) => {
5
                let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
5
                range.matches(ratio)
            }
2
            Self::Orientation(orient) => *orient == ctx.orientation,
4
            Self::PrefersReducedMotion(pref) => {
4
                bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
            }
4
            Self::PrefersHighContrast(pref) => {
4
                bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
            }
50216
            Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
2
            Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
        }
19172106
    }
18065282
    fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
18065282
        match condition {
32
            OsCondition::Any => true,
24
            OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
18065226
            _ => condition == actual,
        }
18065282
    }
52
    fn match_os_version(
52
        condition: &OsVersionCondition,
52
        actual: OsVersion,
52
        desktop_env: OptionLinuxDesktopEnv,
52
        de_version: u32,
52
    ) -> bool {
        // de_version == 0 means the runtime hasn't reported a version,
        // so any DE-version constraint fails until detection is wired up.
52
        let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
52
        match condition {
3
            OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
5
            OsVersionCondition::Min(ver) => actual.is_at_least(ver),
2
            OsVersionCondition::Max(ver) => actual.is_at_most(ver),
3
            OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
32
            OsVersionCondition::DesktopEnvMin(d) =>
32
                de_matches(&d.env) && de_version != 0 && de_version >= d.version_id,
3
            OsVersionCondition::DesktopEnvMax(d) =>
3
                de_matches(&d.env) && de_version != 0 && de_version <= d.version_id,
4
            OsVersionCondition::DesktopEnvExact(d) =>
                // `de_version != 0` like the Min/Max arms: 0 is the "unknown" sentinel,
                // so a DE-version constraint (including `= 0`) must fail until detection
                // is wired up — otherwise `@os(linux:gnome = 0)` matched every session.
4
                de_matches(&d.env) && de_version != 0 && de_version == d.version_id,
        }
52
    }
906036
    fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
906036
        match (condition, actual) {
4
            (ThemeCondition::SystemPreferred, _) => true,
906032
            _ => condition == actual,
        }
906036
    }
50233
    const fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
50233
        let node_state = &ctx.pseudo_state;
50233
        match state {
50003
            PseudoStateType::Normal => true, // Normal is always active (base state)
216
            PseudoStateType::Hover => node_state.hover,
2
            PseudoStateType::Active => node_state.active,
2
            PseudoStateType::Focus => node_state.focused,
1
            PseudoStateType::Disabled => node_state.disabled,
2
            PseudoStateType::CheckedTrue => node_state.checked,
2
            PseudoStateType::CheckedFalse => !node_state.checked,
1
            PseudoStateType::FocusWithin => node_state.focus_within,
1
            PseudoStateType::Visited => node_state.visited,
1
            PseudoStateType::Backdrop => node_state.backdrop,
1
            PseudoStateType::Dragging => node_state.dragging,
1
            PseudoStateType::DragOver => node_state.drag_over,
        }
50233
    }
}
/// Parse the content of an `@os(...)` at-rule into a list of dynamic-selector conditions.
///
/// Accepts both bare-identifier and parenthesized forms:
///
/// - `linux`                       → `[Os(Linux)]`
/// - `(linux)`                     → `[Os(Linux)]`
/// - `(linux:gnome)`               → `[Os(Linux), OsVersion(DesktopEnvironment(Gnome))]`
/// - `(windows >= win-11)`         → `[Os(Windows), OsVersion(Min(WIN_11))]`
/// - `(linux:gnome > 40)`          → `[Os(Linux), OsVersion(DesktopEnvMin{ env: Gnome, version_id: 40 })]`
/// - `(any)` / `(*)` / `(all)`     → `[]` (always-match, no conditions emitted)
///
/// Returns `None` only when the content is a parse error.
/// `Some(vec![])` means "always match" (the rule applies unconditionally).
#[cfg(feature = "parser")]
290
#[must_use] pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
290
    let trimmed = content.trim();
290
    let inner = trimmed
290
        .strip_prefix('(').and_then(|s| s.strip_suffix(')'))
290
        .unwrap_or(trimmed)
290
        .trim();
290
    let inner = inner
290
        .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
290
        .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
290
        .unwrap_or(inner)
290
        .trim();
290
    if inner.is_empty() {
51
        return None;
239
    }
    // Split off the operator + version, if any.
239
    let (subject, op_and_version) = split_op_and_version(inner);
239
    let subject = subject.trim();
    // subject is "family" or "family:de"
239
    let (family_str, de_str) = match subject.split_once(':') {
24
        Some((f, d)) => (f.trim(), Some(d.trim())),
215
        None => (subject, None),
    };
239
    let family = parse_os_family_token(family_str)?;
228
    let de = match de_str {
22
        Some(s) if !s.is_empty() => Some(parse_de_token(s)),
207
        _ => None,
    };
228
    let mut out = Vec::new();
    // Always emit the family selector, even for `Any` — `Os(Any)` is matched as
    // unconditionally true, but keeping it in the conditions list makes the rule
    // structure visible to introspection.
228
    out.push(DynamicSelector::Os(family));
228
    match (de, op_and_version) {
        // Bare DE with no version: just "is the DE this one"
9
        (Some(env), None) => {
9
            out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
9
        }
        // DE + version: emit a DesktopEnv* condition
12
        (Some(env), Some((op, ver_str))) => {
12
            let v: u32 = ver_str.parse().ok()?;
8
            let dev = DesktopEnvVersion { env, version_id: v };
8
            let cond = match op {
8
                VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
                VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
                VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
            };
8
            out.push(DynamicSelector::OsVersion(cond));
        }
        // OS family + version
79
        (None, Some((op, ver_str))) => {
79
            let os_family = match family {
15
                OsCondition::Linux => OsFamily::Linux,
45
                OsCondition::Windows => OsFamily::Windows,
15
                OsCondition::MacOS => OsFamily::MacOS,
1
                OsCondition::IOS => OsFamily::IOS,
                OsCondition::Android => OsFamily::Android,
                // Apple, Web, Any have no version line — reject.
3
                _ => return None,
            };
76
            let version = parse_os_version(os_family, ver_str)?;
75
            let cond = match op {
73
                VersionOp::Min => OsVersionCondition::Min(version),
1
                VersionOp::Max => OsVersionCondition::Max(version),
1
                VersionOp::Exact => OsVersionCondition::Exact(version),
            };
75
            out.push(DynamicSelector::OsVersion(cond));
        }
        // Family only — already pushed above (or empty for `any`).
128
        (None, None) => {}
    }
220
    Some(out)
290
}
#[cfg(feature = "parser")]
#[derive(Copy, Clone)]
enum VersionOp { Min, Max, Exact }
/// Find the first comparison operator (`>=`, `<=`, `=`, `>`, `<`) in `s` and split.
/// `>` and `<` are treated as `>=` / `<=` because version IDs are discrete integers.
#[cfg(feature = "parser")]
249
fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
    // Earliest match wins; on a tie, the longer operator wins (so ">=" beats "=" at the same position).
249
    let candidates: &[(&str, VersionOp)] = &[
249
        (">=", VersionOp::Min),
249
        ("<=", VersionOp::Max),
249
        ("=",  VersionOp::Exact),
249
        (">",  VersionOp::Min),
249
        ("<",  VersionOp::Max),
249
    ];
249
    let mut best: Option<(usize, usize, VersionOp)> = None;
1494
    for (op_str, op) in candidates {
1245
        if let Some(pos) = s.find(op_str) {
262
            let len = op_str.len();
164
            best = Some(match best {
98
                None => (pos, len, *op),
164
                Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
164
                Some(b) => b,
            });
983
        }
    }
249
    match best {
98
        Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
151
        None => (s, None),
    }
249
}
#[cfg(feature = "parser")]
258
fn parse_os_family_token(s: &str) -> Option<OsCondition> {
258
    match s.to_lowercase().as_str() {
258
        "linux" => Some(OsCondition::Linux),
166
        "windows" | "win" => Some(OsCondition::Windows),
98
        "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
60
        "ios" => Some(OsCondition::IOS),
59
        "android" => Some(OsCondition::Android),
59
        "apple" => Some(OsCondition::Apple),
51
        "web" | "wasm" => Some(OsCondition::Web),
35
        "any" | "all" | "*" => Some(OsCondition::Any),
22
        _ => None,
    }
258
}
#[cfg(feature = "parser")]
34
fn parse_de_token(s: &str) -> LinuxDesktopEnv {
34
    match s.to_lowercase().as_str() {
34
        "gnome" => LinuxDesktopEnv::Gnome,
13
        "kde" => LinuxDesktopEnv::KDE,
12
        "xfce" => LinuxDesktopEnv::XFCE,
11
        "unity" => LinuxDesktopEnv::Unity,
10
        "cinnamon" => LinuxDesktopEnv::Cinnamon,
9
        "mate" => LinuxDesktopEnv::MATE,
8
        _ => LinuxDesktopEnv::Other,
    }
34
}
// ============================================================================
// CssPropertyWithConditions - Replacement for NodeDataInlineCssProperty
// ============================================================================
/// A CSS property with optional conditions for when it should be applied.
/// This replaces `NodeDataInlineCssProperty` with a more flexible system.
///
/// If `apply_if` is empty, the property always applies.
/// If `apply_if` contains conditions, ALL conditions must be satisfied for the property to apply.
#[repr(C)]
#[derive(Debug, Clone, PartialEq)]
pub struct CssPropertyWithConditions {
    /// The actual CSS property value
    pub property: CssProperty,
    /// Conditions that must all be satisfied for this property to apply.
    /// Empty means unconditional (always apply).
    pub apply_if: DynamicSelectorVec,
}
impl_option!(
    CssPropertyWithConditions,
    OptionCssPropertyWithConditions,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd]
);
impl Eq for CssPropertyWithConditions {}
/// Collect the viewport-size thresholds at which this set can flip.
///
/// The thresholds (logical px) are the width/height bounds of
/// `ViewportWidth` / `ViewportHeight` selectors (NaN "no bound" ends are
/// skipped). The engine's resize decision regenerates the DOM when the
/// window crosses one of these, so the set of HARVESTED thresholds — not a
/// hardcoded guess list — defines where a resize must re-run the cascade.
4336
pub fn collect_viewport_thresholds(
4336
    conds: &[DynamicSelector],
4336
    widths: &mut Vec<f32>,
4336
    heights: &mut Vec<f32>,
4336
) {
8359
    for c in conds {
4023
        match c {
4022
            DynamicSelector::ViewportWidth(r) => {
4022
                if r.min.is_finite() {
37
                    widths.push(r.min);
3985
                }
4022
                if r.max.is_finite() {
3985
                    widths.push(r.max);
3985
                }
            }
1
            DynamicSelector::ViewportHeight(r) => {
1
                if r.min.is_finite() {
                    heights.push(r.min);
1
                }
1
                if r.max.is_finite() {
1
                    heights.push(r.max);
1
                }
            }
            _ => {}
        }
    }
4336
}
impl PartialOrd for CssPropertyWithConditions {
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for CssPropertyWithConditions {
4
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        // Order by the property first, then lexicographically by the full list of
        // conditions. This is consistent with the derived `PartialEq` (which compares
        // both fields) and with `Hash` below, so the type is sound to use as a
        // `BTreeMap`/`BTreeSet` key or to dedup after sorting. (The previous impl
        // compared the condition *count* only, which violated the Eq/Ord agreement.)
4
        self.property
4
            .cmp(&other.property)
4
            .then_with(|| self.apply_if.as_slice().cmp(other.apply_if.as_slice()))
4
    }
}
impl CssPropertyWithConditions {
    /// Create an unconditional property (always applies) - const version
5915240
    #[must_use] pub const fn simple(property: CssProperty) -> Self {
5915240
        Self {
5915240
            property,
5915240
            apply_if: DynamicSelectorVec::from_const_slice(&[]),
5915240
        }
5915240
    }
    /// Create a property with a single condition (const version using slice reference)
67160
    #[must_use] pub const fn with_single_condition(
67160
        property: CssProperty,
67160
        conditions: &'static [DynamicSelector],
67160
    ) -> Self {
67160
        Self {
67160
            property,
67160
            apply_if: DynamicSelectorVec::from_const_slice(conditions),
67160
        }
67160
    }
    /// Create a property with a single condition (non-const, allocates)
2
    #[must_use] pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
2
        Self {
2
            property,
2
            apply_if: DynamicSelectorVec::from_vec(vec![condition]),
2
        }
2
    }
    /// Create a property with multiple conditions (all must match)
9
    #[must_use] pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
9
        Self {
9
            property,
9
            apply_if: conditions,
9
        }
9
    }
    /// Create a property that applies only on hover (const version)
55277
    #[must_use] pub const fn on_hover(property: CssProperty) -> Self {
55277
        Self::with_single_condition(
55277
            property,
55277
            &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
        )
55277
    }
    /// Create a property that applies only when active (const version)
5276
    #[must_use] pub const fn on_active(property: CssProperty) -> Self {
5276
        Self::with_single_condition(
5276
            property,
5276
            &[DynamicSelector::PseudoState(PseudoStateType::Active)],
        )
5276
    }
    /// Create a property that applies only when focused (const version)
131
    #[must_use] pub const fn on_focus(property: CssProperty) -> Self {
131
        Self::with_single_condition(
131
            property,
131
            &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
        )
131
    }
    /// Create a property that applies only when disabled (const version)
1
    #[must_use] pub const fn when_disabled(property: CssProperty) -> Self {
1
        Self::with_single_condition(
1
            property,
1
            &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
        )
1
    }
    /// Create a property that applies only on a specific OS (non-const, needs runtime value)
1
    #[must_use] pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1
        Self::with_condition(property, DynamicSelector::Os(os))
1
    }
    /// Create a property that applies only in dark theme (const version)
2
    #[must_use] pub const fn dark_theme(property: CssProperty) -> Self {
2
        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
2
    }
    /// Create a property that applies only in light theme (const version)
1
    #[must_use] pub const fn light_theme(property: CssProperty) -> Self {
1
        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1
    }
    /// Create a property for Windows only (const version)
1
    #[must_use] pub const fn on_windows(property: CssProperty) -> Self {
1
        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1
    }
    /// Create a property for macOS only (const version)
1
    #[must_use] pub const fn on_macos(property: CssProperty) -> Self {
1
        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1
    }
    /// Create a property for Linux only (const version)
2
    #[must_use] pub const fn on_linux(property: CssProperty) -> Self {
2
        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
2
    }
    /// Check if this property matches in the given context
23483555
    #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
        // Empty conditions = always matches
23483555
        if self.apply_if.as_slice().is_empty() {
4515887
            return true;
18967668
        }
        // All conditions must match
18967668
        self.apply_if
18967668
            .as_slice()
18967668
            .iter()
19021197
            .all(|selector| selector.matches(ctx))
23483555
    }
    /// Check if this property has any conditions
9
    #[must_use] pub fn is_conditional(&self) -> bool {
9
        !self.apply_if.as_slice().is_empty()
9
    }
    /// Check if this property is a pseudo-state conditional only
    /// (hover, active, focus, etc.)
14
    #[must_use] pub fn is_pseudo_state_only(&self) -> bool {
14
        let conditions = self.apply_if.as_slice();
14
        !conditions.is_empty()
12
            && conditions
12
                .iter()
63
                .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
14
    }
    /// Check if this property affects layout (width, height, margin, etc.)
    /// 
    /// Returns `true` for layout-affecting properties like width, height, margin, padding,
    /// font-size, etc. Returns `false` for paint-only properties like color, background,
    /// box-shadow, opacity, transform, etc.
4
    #[must_use] pub const fn is_layout_affecting(&self) -> bool {
4
        self.property.get_type().can_trigger_relayout()
4
    }
}
impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
impl_vec_clone!(
    CssPropertyWithConditions,
    CssPropertyWithConditionsVec,
    CssPropertyWithConditionsVecDestructor
);
// Manual implementations for Eq and Ord (required for NodeData derives)
impl Eq for CssPropertyWithConditionsVec {}
impl Ord for CssPropertyWithConditionsVec {
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        // Lexicographic, matching the `impl_vec_partialord!` PartialOrd above and the
        // element `Ord`; previously this compared length only (inconsistent with Eq).
        self.as_slice().cmp(other.as_slice())
    }
}
impl core::hash::Hash for CssPropertyWithConditions {
4
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
4
        self.property.hash(state);
        // Hash the full set of conditions (length + each selector, via the now-`Hash`
        // `DynamicSelector`) so the hash agrees with `Eq`/`Ord` instead of colliding
        // on condition count alone.
4
        self.apply_if.as_slice().hash(state);
4
    }
}
impl core::hash::Hash for CssPropertyWithConditionsVec {
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        // Hashing the slice folds in the length as well as every element.
        self.as_slice().hash(state);
    }
}
impl CssPropertyWithConditionsVec {
    /// Parse CSS with support for selectors and nesting.
    /// 
    /// Supports:
    /// - Simple properties: `color: red;`
    /// - Pseudo-selectors: `:hover { background: blue; }`
    /// - @-rules: `@os linux { font-size: 14px; }`
    /// - Nesting: `@os linux { font-size: 14px; :hover { color: red; }}`
    /// 
    /// Examples:
    /// ```ignore
    /// // Simple inline styles
    /// CssPropertyWithConditionsVec::parse("color: red; font-size: 14px;")
    /// 
    /// // With hover state
    /// CssPropertyWithConditionsVec::parse(":hover { background: blue; }")
    /// 
    /// // OS-specific with nested hover
    /// CssPropertyWithConditionsVec::parse("@os linux { font-size: 14px; :hover { color: red; }}")
    /// ```
    #[cfg(feature = "parser")]
193
    #[must_use] pub fn parse(style: &str) -> Self {
193
        Self::parse_with_conditions(style, &[])
193
    }
    /// Internal recursive parser with inherited conditions
    #[cfg(feature = "parser")]
250
    fn parse_with_conditions(style: &str, inherited_conditions: &[DynamicSelector]) -> Self {
        use crate::props::property::{
            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
            CssPropertyType,
        };
250
        let mut props = Vec::new();
250
        let key_map = CssKeyMap::get();
250
        let style = style.trim();
250
        if style.is_empty() {
28
            return Self::from_vec(props);
222
        }
        // Tokenize into segments: properties, pseudo-selectors, and @-rules
222
        let chars = style.chars();
222
        let mut current_segment = String::new();
222
        let mut brace_depth = 0;
1071565
        for c in chars {
5261
            match c {
2305
                '{' => {
2305
                    brace_depth += 1;
2305
                    current_segment.push(c);
2305
                }
                '}' => {
2306
                    brace_depth -= 1;
2306
                    current_segment.push(c);
2306
                    if brace_depth == 0 {
                        // End of a block - process it
58
                        let segment = current_segment.trim().to_string();
58
                        current_segment.clear();
58
                        if let Some(parsed) = Self::parse_block_segment(&segment, inherited_conditions, &key_map) {
54
                            props.extend(parsed);
54
                        }
2248
                    }
                }
5261
                ';' if brace_depth == 0 => {
                    // End of a simple property
5201
                    let segment = current_segment.trim().to_string();
5201
                    current_segment.clear();
5201
                    if !segment.is_empty() {
5146
                        if let Some(parsed) = Self::parse_property_segment(&segment, inherited_conditions, &key_map) {
5105
                            props.extend(parsed);
5105
                        }
55
                    }
                }
1061531
                _ => {
1061531
                    current_segment.push(c);
1061531
                }
            }
        }
        // Handle any remaining segment (property without trailing semicolon)
222
        let remaining = current_segment.trim();
222
        if !remaining.is_empty() && !remaining.contains('{') {
28
            if let Some(parsed) = Self::parse_property_segment(remaining, inherited_conditions, &key_map) {
2
                props.extend(parsed);
26
            }
194
        }
222
        Self::from_vec(props)
250
    }
    /// Parse a block segment like `:hover { ... }` or `@os linux { ... }`
    #[cfg(feature = "parser")]
63
    fn parse_block_segment(
63
        segment: &str,
63
        inherited_conditions: &[DynamicSelector],
63
        key_map: &crate::props::property::CssKeyMap,
63
    ) -> Option<Vec<CssPropertyWithConditions>> {
        // Find the opening brace
63
        let brace_pos = segment.find('{')?;
62
        let selector = segment[..brace_pos].trim();
        // Extract content between braces (excluding the braces themselves)
62
        let content_start = brace_pos + 1;
62
        let content_end = segment.rfind('}')?;
61
        if content_end <= content_start {
3
            return None;
58
        }
58
        let content = &segment[content_start..content_end];
        // Parse selector to get conditions
58
        let mut conditions = inherited_conditions.to_vec();
58
        if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
55
            conditions.extend(new_conditions);
55
        } else {
            // Unknown selector, skip this block
3
            return None;
        }
        // Recursively parse the content with the new conditions
55
        let parsed = Self::parse_with_conditions(content, &conditions);
55
        Some(parsed.into_library_owned_vec())
63
    }
    /// Parse a selector string into `DynamicSelector` conditions
    #[cfg(feature = "parser")]
80
    fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
80
        let selector = selector.trim();
        // Handle pseudo-selectors
80
        if let Some(pseudo) = selector.strip_prefix(':') {
68
            match pseudo {
68
                "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
14
                "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
13
                "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
12
                "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
11
                "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
10
                "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::CheckedTrue)]),
9
                "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
8
                "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
7
                "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
6
                "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
5
                _ => return None,
            }
12
        }
        // Handle @-rules
12
        if let Some(rule_content) = selector.strip_prefix('@') {
4
            return Self::parse_at_rule(rule_content);
8
        }
        // Handle universal selector * (treat as unconditional)
8
        if selector == "*" {
1
            return Some(vec![]);
7
        }
        // Empty selector means unconditional
7
        if selector.is_empty() {
2
            return Some(vec![]);
5
        }
5
        None
80
    }
    /// Parse an @-rule (the content after '@') into `DynamicSelector` conditions.
    /// Handles @os, @media, @theme, @lang, @container,
    /// @prefers-reduced-motion, and @prefers-high-contrast.
    #[cfg(feature = "parser")]
33
    fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
        // @os linux                    -- bare family
        // @os(linux)                   -- family in parens
        // @os(linux:gnome)             -- family + desktop env
        // @os(windows >= win-11)       -- family + version
        // @os(linux:gnome > 40)        -- family + DE + DE version
33
        if let Some(rest) = rule_content
33
            .strip_prefix("os ")
33
            .or_else(|| if rule_content.starts_with("os(") { Some(&rule_content[2..]) } else { None })
        {
5
            if let Some(conds) = parse_os_at_rule_content(rest) {
1
                return Some(conds);
4
            }
28
        }
        // @media (min-width: 800px), etc.
32
        if let Some(rest) = rule_content.strip_prefix("media ") {
3
            let media_query = rest.trim();
3
            if let Some(media_conds) = Self::parse_media_query(media_query) {
1
                return Some(media_conds);
2
            }
29
        }
        // @theme dark, @theme light
31
        if let Some(rest) = rule_content.strip_prefix("theme ") {
3
            let theme = rest.trim();
3
            match theme {
3
                "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
2
                "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
1
                _ => return None,
            }
28
        }
        // @lang("de-DE") or @lang de-DE
28
        let lang_body = rule_content
28
            .strip_prefix("lang(")
28
            .map(|r| r.trim_end_matches(')').trim())
28
            .or_else(|| rule_content.strip_prefix("lang ").map(str::trim));
28
        if let Some(lang_str) = lang_body {
5
            let lang_str = lang_str
5
                .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
5
                .or_else(|| lang_str.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
5
                .unwrap_or(lang_str);
5
            if !lang_str.is_empty() {
3
                return Some(vec![DynamicSelector::Language(
3
                    LanguageCondition::Prefix(AzString::from(lang_str.to_string()))
3
                )]);
2
            }
23
        }
        // @container (min-width: 400px) or @container sidebar (min-width: 400px)
25
        if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
4
            let container_str = if rule_content.starts_with("container(") {
                &rule_content[9..] // keep the '(' for parsing
            } else {
4
                rule_content[10..].trim()
            };
4
            let mut conds = Vec::new();
            // Check for named container: "sidebar (min-width: 400px)"
4
            let (name_part, query_part) = if container_str.starts_with('(') {
2
                (None, container_str)
2
            } else if let Some(paren_idx) = container_str.find('(') {
1
                let name = container_str[..paren_idx].trim();
1
                if name.is_empty() {
                    (None, container_str)
                } else {
1
                    (Some(name), &container_str[paren_idx..])
                }
            } else {
1
                if !container_str.is_empty() {
1
                    return Some(vec![DynamicSelector::ContainerName(
1
                        AzString::from(container_str.to_string())
1
                    )]);
                }
                return None;
            };
3
            if let Some(name) = name_part {
1
                conds.push(DynamicSelector::ContainerName(
1
                    AzString::from(name.to_string())
1
                ));
2
            }
            // Parse (min-width: 400px) style conditions
3
            if let Some(inner) = query_part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
3
                if let Some((key, value)) = inner.split_once(':') {
2
                    let key = key.trim();
2
                    let value = value.trim();
2
                    let px_value = value.strip_suffix("px")
2
                        .and_then(|v| v.trim().parse::<f32>().ok())
2
                        .filter(|px| !px.is_nan()); // reject NaN (sentinel); keep inf (never-matching)
2
                    match key {
2
                        "min-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(px))); } }
1
                        "max-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(px))); } }
1
                        "min-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_min(px))); } }
1
                        "max-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_max(px))); } }
                        _ => {}
                    }
1
                }
            }
3
            if !conds.is_empty() {
2
                return Some(conds);
1
            }
21
        }
        // @prefers-reduced-motion or @reduced-motion
22
        if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
1
            return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
21
        }
        // @prefers-high-contrast or @high-contrast
21
        if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
1
            return Some(vec![DynamicSelector::PrefersHighContrast(BoolCondition::True)]);
20
        }
20
        None
33
    }
    /// Parse simple media query
    #[cfg(feature = "parser")]
26
    fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
26
        let query = query.trim();
        // Handle (min-width: XXXpx)
26
        if query.starts_with('(') && query.ends_with(')') {
15
            let inner = &query[1..query.len()-1];
15
            if let Some((key, value)) = inner.split_once(':') {
13
                let key = key.trim();
13
                let value = value.trim();
                // Reject only NaN, not infinity. NaN collides with MinMaxRange's NaN
                // "no bound" sentinel — `(min-width: NaN)` would silently match every
                // viewport. Infinity is a valid, meaningful bound: `(min-width: inf)`
                // creates a range no finite viewport satisfies (matches nothing), which
                // is the correct outcome, so it must be KEPT.
13
                let px_value = value.strip_suffix("px")
13
                    .and_then(|v| v.trim().parse::<f32>().ok())
13
                    .filter(|px| !px.is_nan());
13
                match key {
13
                    "min-width" => {
9
                        if let Some(px) = px_value {
4
                            return Some(vec![DynamicSelector::ViewportWidth(
4
                                MinMaxRange::with_min(px)
4
                            )]);
5
                        }
                    }
4
                    "max-width" => {
                        if let Some(px) = px_value {
                            return Some(vec![DynamicSelector::ViewportWidth(
                                MinMaxRange::with_max(px)
                            )]);
                        }
                    }
4
                    "min-height" => {
                        if let Some(px) = px_value {
                            return Some(vec![DynamicSelector::ViewportHeight(
                                MinMaxRange::with_min(px)
                            )]);
                        }
                    }
4
                    "max-height" => {
1
                        if let Some(px) = px_value {
1
                            return Some(vec![DynamicSelector::ViewportHeight(
1
                                MinMaxRange::with_max(px)
1
                            )]);
                        }
                    }
3
                    other => {
                        // Try orientation, prefers-color-scheme, prefers-reduced-motion, etc.
3
                        if let Some(sel) = Self::parse_media_feature_inline(other, value) {
                            return Some(vec![sel]);
3
                        }
                    }
                }
2
            }
11
        }
        // Handle screen, print, all
21
        match query {
21
            "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
20
            "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
19
            "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
18
            _ => None,
        }
26
    }
    /// Parse a media query feature value into a `DynamicSelector`
    /// Handles features like orientation, prefers-color-scheme, prefers-reduced-motion, etc.
    #[cfg(feature = "parser")]
16
    fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
16
        match key {
16
            "orientation" => {
3
                if value.eq_ignore_ascii_case("portrait") {
1
                    Some(DynamicSelector::Orientation(OrientationType::Portrait))
2
                } else if value.eq_ignore_ascii_case("landscape") {
                    Some(DynamicSelector::Orientation(OrientationType::Landscape))
                } else {
2
                    None
                }
            }
13
            "prefers-color-scheme" => {
2
                if value.eq_ignore_ascii_case("dark") {
1
                    Some(DynamicSelector::Theme(ThemeCondition::Dark))
1
                } else if value.eq_ignore_ascii_case("light") {
                    Some(DynamicSelector::Theme(ThemeCondition::Light))
                } else {
1
                    None
                }
            }
11
            "prefers-reduced-motion" => {
2
                if value.eq_ignore_ascii_case("reduce") {
1
                    Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
1
                } else if value.eq_ignore_ascii_case("no-preference") {
1
                    Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
                } else {
                    None
                }
            }
9
            "prefers-contrast" | "prefers-high-contrast" => {
2
                if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
1
                    Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
1
                } else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
1
                    Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
                } else {
                    None
                }
            }
7
            _ => None,
        }
16
    }
    /// Parse a simple property like "color: red"
    #[cfg(feature = "parser")]
5183
    fn parse_property_segment(
5183
        segment: &str,
5183
        inherited_conditions: &[DynamicSelector],
5183
        key_map: &crate::props::property::CssKeyMap,
5183
    ) -> Option<Vec<CssPropertyWithConditions>> {
        use crate::props::property::{
            parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
            CssPropertyType,
        };
5183
        let segment = segment.trim();
5183
        if segment.is_empty() {
2
            return None;
5181
        }
5181
        let (key, value) = segment.split_once(':')?;
5158
        let key = key.trim();
5158
        let value = value.trim();
5158
        let mut props = Vec::new();
5158
        let conditions = if inherited_conditions.is_empty() {
5150
            DynamicSelectorVec::from_const_slice(&[])
        } else {
8
            DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
        };
        // First, try to parse as a regular (non-shorthand) property
5158
        if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
5132
            if let Ok(prop) = parse_css_property(prop_type, value) {
5103
                props.push(CssPropertyWithConditions {
5103
                    property: prop,
5103
                    apply_if: conditions,
5103
                });
5103
                return Some(props);
29
            }
26
        }
        // If not found, try as a shorthand (combined) property
55
        if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
6
            if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
23
                for prop in expanded_props {
17
                    props.push(CssPropertyWithConditions {
17
                        property: prop,
17
                        apply_if: conditions.clone(),
17
                    });
17
                }
6
                return Some(props);
            }
49
        }
49
        None
5183
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
1
    fn test_inline_overflow_parse() {
1
        let style = "overflow: scroll;";
1
        let parsed = CssPropertyWithConditionsVec::parse(style);
1
        let props = parsed.into_library_owned_vec();
1
        assert!(!props.is_empty(), "Expected overflow to parse into at least 1 property");
1
    }
    #[test]
1
    fn test_inline_overflow_y_parse() {
1
        let style = "overflow-y: scroll;";
1
        let parsed = CssPropertyWithConditionsVec::parse(style);
1
        let props = parsed.into_library_owned_vec();
1
        assert!(!props.is_empty(), "Expected overflow-y to parse into at least 1 property");
1
    }
    #[test]
1
    fn test_inline_combined_style_with_overflow() {
1
        let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
1
        let parsed = CssPropertyWithConditionsVec::parse(style);
1
        let props = parsed.into_library_owned_vec();
        // padding:20px expands to 4, background:1, font-size:1, color:1, overflow:2 = 10
1
        assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
1
    }
    #[test]
1
    fn test_inline_grid_template_columns_parse() {
        use crate::props::layout::grid::GridTrackSizing;
1
        let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
1
        let parsed = CssPropertyWithConditionsVec::parse(style);
1
        let props = parsed.into_library_owned_vec();
        // Find grid-template-columns property
2
        let grid_cols = props.iter().find(|p| {
2
            matches!(p.property, CssProperty::GridTemplateColumns(_))
2
        }).expect("Expected GridTemplateColumns property");
1
        if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
1
            let template = value.get_property().expect("Expected Exact value");
1
            let tracks = template.tracks.as_ref();
1
            assert_eq!(tracks.len(), 4, "Expected 4 tracks");
4
            for (i, track) in tracks.iter().enumerate() {
4
                assert!(matches!(track, GridTrackSizing::Fixed(_)),
                    "Track {i} should be Fixed(160px), got {track:?}");
            }
        } else {
            panic!("Expected CssProperty::GridTemplateColumns");
        }
1
    }
}
#[cfg(test)]
#[allow(
    clippy::float_cmp,
    clippy::too_many_lines,
    clippy::cast_precision_loss,
    clippy::field_reassign_with_default,
    clippy::unreadable_literal
)]
mod autotest_generated {
    use core::cmp::Ordering;
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    use super::*;
    use crate::props::property::CssPropertyType;
    // ---------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------
    fn hash_of<T: Hash>(t: &T) -> u64 {
        let mut h = DefaultHasher::new();
        t.hash(&mut h);
        h.finish()
    }
    /// A paint-only property (does not trigger relayout).
    fn paint_prop() -> CssProperty {
        CssProperty::const_none(CssPropertyType::TextColor)
    }
    /// A layout-affecting property.
    fn layout_prop() -> CssProperty {
        CssProperty::const_none(CssPropertyType::Width)
    }
    /// Every `DynamicSelector` variant, in discriminant order.
    fn all_selector_variants() -> Vec<DynamicSelector> {
        vec![
            DynamicSelector::Os(OsCondition::Linux),
            DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
            DynamicSelector::Media(MediaType::Print),
            DynamicSelector::ViewportWidth(MinMaxRange::with_min(1.0)),
            DynamicSelector::ViewportHeight(MinMaxRange::with_max(2.0)),
            DynamicSelector::ContainerWidth(MinMaxRange::new(Some(1.0), Some(2.0))),
            DynamicSelector::ContainerHeight(MinMaxRange::new(None, None)),
            DynamicSelector::ContainerName(AzString::from_const_str("sidebar")),
            DynamicSelector::Theme(ThemeCondition::Dark),
            DynamicSelector::AspectRatio(MinMaxRange::with_min(0.5)),
            DynamicSelector::Orientation(OrientationType::Portrait),
            DynamicSelector::PrefersReducedMotion(BoolCondition::True),
            DynamicSelector::PrefersHighContrast(BoolCondition::False),
            DynamicSelector::PseudoState(PseudoStateType::Hover),
            DynamicSelector::Language(LanguageCondition::Prefix(AzString::from_const_str("de"))),
        ]
    }
    /// Adversarial input corpus reused across every string parser under test.
    fn nasty_strings() -> Vec<String> {
        vec![
            String::new(),
            " ".to_string(),
            "   \t\n\r  ".to_string(),
            "\0".to_string(),
            "0".to_string(),
            "-0".to_string(),
            "-1".to_string(),
            "NaN".to_string(),
            "nan".to_string(),
            "inf".to_string(),
            "-inf".to_string(),
            "infinity".to_string(),
            "1e400".to_string(),
            i64::MAX.to_string(),
            i64::MIN.to_string(),
            u32::MAX.to_string(),
            u64::MAX.to_string(),
            "9999999999999999999999999999".to_string(),
            "1.7976931348623157e308".to_string(),
            "\u{1F600}".to_string(),
            "e\u{301}\u{301}\u{301}".to_string(),
            "日本語".to_string(),
            "\u{202e}gnome".to_string(),
            "  linux  ".to_string(),
            "linux;garbage".to_string(),
            "linux)".to_string(),
            "((((".to_string(),
            "))))".to_string(),
            ">=".to_string(),
            "<=<=<=".to_string(),
            ":::::".to_string(),
            "-".to_string(),
            "_".to_string(),
            ".".to_string(),
            "..".to_string(),
            "...".to_string(),
            "1.2.3.4.5.6".to_string(),
            "%s%s%n".to_string(),
            "\\x00\\xff".to_string(),
            "a".repeat(100_000),
            "1".repeat(100_000),
            ".".repeat(10_000),
            "(".repeat(5_000),
            "🦀".repeat(10_000),
        ]
    }
    // ---------------------------------------------------------------
    // 1. PseudoStateFlags::has_state  (predicate)
    // ---------------------------------------------------------------
    #[test]
    fn has_state_default_flags_only_normal_and_checked_false() {
        let flags = PseudoStateFlags::default();
        // Normal is the base state and is always active.
        assert!(flags.has_state(PseudoStateType::Normal));
        // `checked: false` means :not(:checked) is active.
        assert!(flags.has_state(PseudoStateType::CheckedFalse));
        for state in [
            PseudoStateType::Hover,
            PseudoStateType::Active,
            PseudoStateType::Focus,
            PseudoStateType::Disabled,
            PseudoStateType::CheckedTrue,
            PseudoStateType::FocusWithin,
            PseudoStateType::Visited,
            PseudoStateType::Backdrop,
            PseudoStateType::Dragging,
            PseudoStateType::DragOver,
        ] {
            assert!(!flags.has_state(state), "{state:?} must be off by default");
        }
    }
    #[test]
    fn has_state_all_flags_set_reports_every_state_except_checked_false() {
        let flags = PseudoStateFlags {
            hover: true,
            active: true,
            focused: true,
            disabled: true,
            checked: true,
            focus_within: true,
            visited: true,
            backdrop: true,
            dragging: true,
            drag_over: true,
        };
        assert!(flags.has_state(PseudoStateType::Hover));
        assert!(flags.has_state(PseudoStateType::CheckedTrue));
        // CheckedTrue and CheckedFalse must always be mutually exclusive.
        assert!(!flags.has_state(PseudoStateType::CheckedFalse));
        assert!(flags.has_state(PseudoStateType::DragOver));
        assert!(flags.has_state(PseudoStateType::Normal));
    }
    #[test]
    fn has_state_checked_true_and_false_are_never_both_active() {
        for checked in [false, true] {
            let flags = PseudoStateFlags {
                checked,
                ..PseudoStateFlags::default()
            };
            assert_ne!(
                flags.has_state(PseudoStateType::CheckedTrue),
                flags.has_state(PseudoStateType::CheckedFalse),
                "checked={checked}: CheckedTrue/CheckedFalse must be complementary"
            );
        }
    }
    // ---------------------------------------------------------------
    // 2. DynamicSelector::variant_tag  (getter)
    // ---------------------------------------------------------------
    #[test]
    fn variant_tag_matches_declared_repr_discriminants() {
        for (expected, sel) in all_selector_variants().iter().enumerate() {
            let expected = u8::try_from(expected).expect("15 variants fit in u8");
            assert_eq!(
                sel.variant_tag(),
                expected,
                "variant_tag drifted from the #[repr(C, u8)] discriminant for {sel:?}"
            );
        }
    }
    #[test]
    fn variant_tag_is_unique_per_variant() {
        let variants = all_selector_variants();
        let mut tags: Vec<u8> = variants.iter().map(DynamicSelector::variant_tag).collect();
        tags.sort_unstable();
        tags.dedup();
        assert_eq!(tags.len(), variants.len(), "variant tags must be unique");
    }
    #[test]
    fn ord_is_keyed_on_variant_tag_first() {
        let variants = all_selector_variants();
        for w in variants.windows(2) {
            assert_eq!(
                w[0].cmp(&w[1]),
                Ordering::Less,
                "selectors must sort by variant tag: {:?} < {:?}",
                w[0],
                w[1]
            );
        }
    }
    #[test]
    fn hash_distinguishes_variants_carrying_the_same_payload() {
        // ViewportWidth / ContainerWidth carry identical payloads but must not collide,
        // because `variant_tag` is folded into the hash first.
        let range = MinMaxRange::with_min(800.0);
        let a = DynamicSelector::ViewportWidth(range);
        let b = DynamicSelector::ContainerWidth(range);
        assert_ne!(hash_of(&a), hash_of(&b));
        assert_ne!(a.cmp(&b), Ordering::Equal);
    }
    #[test]
    fn hash_and_ord_agree_for_nan_carrying_ranges() {
        // Both are implemented over the *bit pattern*, so two structurally identical
        // NaN-sentinel ranges must compare Equal and hash the same.
        let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
        let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
        assert_eq!(a.cmp(&b), Ordering::Equal);
        assert_eq!(hash_of(&a), hash_of(&b));
    }
    // RED (genuine bug): `MinMaxRange` derives `PartialEq` over raw `f32`s, but the type
    // uses NaN as the "no limit" sentinel. NaN != NaN, so a selector built by
    // `MinMaxRange::with_min`/`with_max` (i.e. every `@media (min-width: ...)` selector)
    // is not even equal to itself. `impl Eq for DynamicSelector` is therefore unsound,
    // and `Ord` (which compares bit patterns) reports `Equal` where `PartialEq` reports
    // `false` — breaking the Ord/Eq contract for BTreeMap/BTreeSet/dedup.
    #[test]
    fn nan_sentinel_range_selector_is_reflexive_under_partial_eq() {
        let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
        let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
        assert_eq!(a, b, "Eq requires reflexivity, but the NaN `max` sentinel breaks it");
    }
    // RED (same root cause, stated as the Ord/Eq contract it violates).
    #[test]
    fn ord_equal_implies_partial_eq_for_range_selectors() {
        let a = DynamicSelector::ViewportHeight(MinMaxRange::with_max(600.0));
        let b = a.clone();
        assert_eq!(a.cmp(&b), Ordering::Equal);
        assert!(
            a == b,
            "cmp() == Equal must imply == (Ord/Eq contract); NaN sentinel breaks it"
        );
    }
    // ---------------------------------------------------------------
    // 3-7. MinMaxRange constructors + getters
    // ---------------------------------------------------------------
    #[test]
    fn min_max_range_new_roundtrips_finite_values() {
        let r = MinMaxRange::new(Some(1.5), Some(9.5));
        assert_eq!(r.min(), Some(1.5));
        assert_eq!(r.max(), Some(9.5));
    }
    #[test]
    fn min_max_range_new_none_encodes_nan_sentinel() {
        let r = MinMaxRange::new(None, None);
        assert!(r.min.is_nan());
        assert!(r.max.is_nan());
        assert_eq!(r.min(), None);
        assert_eq!(r.max(), None);
    }
    #[test]
    fn min_max_range_new_nan_argument_is_indistinguishable_from_none() {
        // Documented sentinel behaviour: NaN *is* "no limit", so a caller passing
        // `Some(NAN)` gets `None` back. Assert it rather than letting it surprise.
        let r = MinMaxRange::new(Some(f32::NAN), Some(f32::NAN));
        assert_eq!(r.min(), None);
        assert_eq!(r.max(), None);
        assert!(r.matches(0.0));
        assert!(r.matches(f32::MAX));
    }
    #[test]
    fn min_max_range_with_min_and_with_max_leave_the_other_side_open() {
        let lo = MinMaxRange::with_min(-0.0);
        assert_eq!(lo.min(), Some(-0.0));
        assert_eq!(lo.max(), None);
        let hi = MinMaxRange::with_max(f32::MAX);
        assert_eq!(hi.min(), None);
        assert_eq!(hi.max(), Some(f32::MAX));
    }
    #[test]
    fn min_max_range_getters_survive_extreme_values() {
        for v in [
            0.0_f32,
            -0.0,
            f32::MIN,
            f32::MAX,
            f32::MIN_POSITIVE,
            f32::EPSILON,
            f32::INFINITY,
            f32::NEG_INFINITY,
        ] {
            let r = MinMaxRange::new(Some(v), Some(v));
            assert_eq!(r.min(), Some(v));
            assert_eq!(r.max(), Some(v));
        }
    }
    // ---------------------------------------------------------------
    // 8. MinMaxRange::matches  (numeric)
    // ---------------------------------------------------------------
    #[test]
    fn matches_zero_boundary_is_inclusive() {
        assert!(MinMaxRange::with_min(0.0).matches(0.0));
        assert!(MinMaxRange::with_max(0.0).matches(0.0));
        assert!(MinMaxRange::new(Some(0.0), Some(0.0)).matches(0.0));
        // IEEE-754: -0.0 == 0.0, so both bounds accept it.
        assert!(MinMaxRange::with_min(0.0).matches(-0.0));
        assert!(MinMaxRange::with_max(0.0).matches(-0.0));
    }
    #[test]
    fn matches_negative_values_are_ordered_correctly() {
        let r = MinMaxRange::new(Some(-10.0), Some(-1.0));
        assert!(r.matches(-10.0));
        assert!(r.matches(-5.0));
        assert!(r.matches(-1.0));
        assert!(!r.matches(-10.001));
        assert!(!r.matches(0.0));
    }
    #[test]
    fn matches_at_float_extremes_does_not_panic() {
        let open = MinMaxRange::new(None, None);
        let bounded = MinMaxRange::new(Some(f32::MIN), Some(f32::MAX));
        for v in [
            f32::MIN,
            f32::MAX,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::MIN_POSITIVE,
            -f32::MIN_POSITIVE,
        ] {
            // Open range accepts everything (both sentinels are NaN).
            assert!(open.matches(v), "open range must accept {v}");
        }
        assert!(bounded.matches(0.0));
        assert!(bounded.matches(f32::MIN));
        assert!(bounded.matches(f32::MAX));
        // Infinities fall outside a MIN..=MAX range.
        assert!(!bounded.matches(f32::INFINITY));
        assert!(!bounded.matches(f32::NEG_INFINITY));
    }
    #[test]
    fn matches_nan_value_is_rejected_by_any_real_bound() {
        // NaN compares false against everything, so any *actual* bound rejects it.
        assert!(!MinMaxRange::with_min(0.0).matches(f32::NAN));
        assert!(!MinMaxRange::with_max(0.0).matches(f32::NAN));
        assert!(!MinMaxRange::new(Some(1.0), Some(2.0)).matches(f32::NAN));
        // ...but a fully-open range has no bound to reject it.
        assert!(MinMaxRange::new(None, None).matches(f32::NAN));
    }
    #[test]
    fn matches_infinite_bounds_are_deterministic() {
        let min_inf = MinMaxRange::with_min(f32::INFINITY);
        assert!(!min_inf.matches(f32::MAX));
        assert!(min_inf.matches(f32::INFINITY));
        let max_neg_inf = MinMaxRange::with_max(f32::NEG_INFINITY);
        assert!(!max_neg_inf.matches(f32::MIN));
        assert!(max_neg_inf.matches(f32::NEG_INFINITY));
    }
    #[test]
    fn matches_inverted_range_matches_nothing() {
        let inverted = MinMaxRange::new(Some(10.0), Some(5.0));
        for v in [-1.0_f32, 0.0, 5.0, 7.5, 10.0, 1e30] {
            assert!(!inverted.matches(v), "inverted range must reject {v}");
        }
    }
    // ---------------------------------------------------------------
    // 9. OsCondition::from_system_platform  (constructor)
    // ---------------------------------------------------------------
    #[test]
    fn os_condition_from_system_platform_covers_every_platform() {
        use crate::system::{DesktopEnvironment, Platform};
        assert_eq!(
            OsCondition::from_system_platform(&Platform::Windows),
            OsCondition::Windows
        );
        assert_eq!(
            OsCondition::from_system_platform(&Platform::MacOs),
            OsCondition::MacOS
        );
        assert_eq!(
            OsCondition::from_system_platform(&Platform::Ios),
            OsCondition::IOS
        );
        assert_eq!(
            OsCondition::from_system_platform(&Platform::Android),
            OsCondition::Android
        );
        assert_eq!(
            OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Gnome)),
            OsCondition::Linux
        );
        assert_eq!(
            OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Other(
                AzString::from_const_str(""),
            ))),
            OsCondition::Linux
        );
        // Unknown degrades to `Any`, which `match_os` treats as always-true.
        assert_eq!(
            OsCondition::from_system_platform(&Platform::Unknown),
            OsCondition::Any
        );
    }
    // ---------------------------------------------------------------
    // 10-15. OsVersion constructor / compare / predicates / unknown
    // ---------------------------------------------------------------
    #[test]
    fn os_version_new_stores_fields_verbatim_at_boundaries() {
        for id in [0, 1, u32::MAX - 1, u32::MAX] {
            let v = OsVersion::new(OsFamily::Linux, id);
            assert_eq!(v.os, OsFamily::Linux);
            assert_eq!(v.version_id, id);
        }
    }
    #[test]
    fn os_version_unknown_is_the_default_and_has_id_zero() {
        let u = OsVersion::unknown();
        assert_eq!(u.version_id, 0);
        assert_eq!(u, OsVersion::default());
    }
    #[test]
    fn os_version_compare_is_none_across_families() {
        let win = OsVersion::WIN_11;
        let mac = OsVersion::MACOS_SONOMA;
        assert_eq!(win.compare(&mac), None);
        assert_eq!(mac.compare(&win), None);
        // A None comparison must make *all three* predicates false — a cross-OS
        // condition can never accidentally match.
        assert!(!win.is_at_least(&mac));
        assert!(!win.is_at_most(&mac));
        assert!(!win.is_exactly(&mac));
    }
    #[test]
    fn os_version_compare_within_family_orders_by_id() {
        assert_eq!(
            OsVersion::WIN_10.compare(&OsVersion::WIN_11),
            Some(Ordering::Less)
        );
        assert_eq!(
            OsVersion::WIN_11.compare(&OsVersion::WIN_10),
            Some(Ordering::Greater)
        );
        assert_eq!(
            OsVersion::WIN_11.compare(&OsVersion::WIN_11_21H2),
            Some(Ordering::Equal)
        );
    }
    #[test]
    fn os_version_compare_at_id_extremes() {
        let lo = OsVersion::new(OsFamily::Android, 0);
        let hi = OsVersion::new(OsFamily::Android, u32::MAX);
        assert_eq!(lo.compare(&hi), Some(Ordering::Less));
        assert_eq!(hi.compare(&lo), Some(Ordering::Greater));
        assert!(hi.is_at_least(&lo));
        assert!(!hi.is_at_most(&lo));
        assert!(lo.is_at_most(&hi));
    }
    #[test]
    fn os_version_predicates_are_reflexive_and_consistent() {
        for v in [
            OsVersion::unknown(),
            OsVersion::WIN_XP,
            OsVersion::MACOS_TAHOE,
            OsVersion::IOS_18,
            OsVersion::ANDROID_15,
            OsVersion::new(OsFamily::Linux, u32::MAX),
        ] {
            assert!(v.is_at_least(&v), "{v:?} >= itself");
            assert!(v.is_at_most(&v), "{v:?} <= itself");
            assert!(v.is_exactly(&v), "{v:?} == itself");
        }
    }
    #[test]
    fn os_version_at_least_is_the_strict_complement_of_less_than() {
        let a = OsVersion::WIN_10;
        let b = OsVersion::WIN_11;
        assert!(!a.is_at_least(&b));
        assert!(a.is_at_most(&b));
        assert!(!a.is_exactly(&b));
        assert!(b.is_at_least(&a));
        assert!(!b.is_at_most(&a));
    }
    #[test]
    fn os_version_unknown_never_satisfies_a_min_constraint_on_a_real_version() {
        // `unknown()` reports OsFamily::Linux/0 — it must not silently satisfy
        // "at least Windows 11" (different family) nor "at least Linux 6.0".
        assert!(!OsVersion::unknown().is_at_least(&OsVersion::WIN_11));
        assert!(!OsVersion::unknown().is_at_least(&OsVersion::LINUX_6_0));
    }
    // ---------------------------------------------------------------
    // 16-22. OS version parsers
    // ---------------------------------------------------------------
    #[test]
    fn parse_os_version_valid_minimal_positive_controls() {
        assert_eq!(
            parse_os_version(OsFamily::Windows, "11"),
            Some(OsVersion::WIN_11)
        );
        assert_eq!(
            parse_os_version(OsFamily::MacOS, "sonoma"),
            Some(OsVersion::MACOS_SONOMA)
        );
        assert_eq!(
            parse_os_version(OsFamily::IOS, "17.0"),
            Some(OsVersion::IOS_17)
        );
        assert_eq!(
            parse_os_version(OsFamily::Android, "tiramisu"),
            Some(OsVersion::ANDROID_13)
        );
        assert_eq!(
            parse_os_version(OsFamily::Linux, "6.0"),
            Some(OsVersion::LINUX_6_0)
        );
    }
    #[test]
    fn parse_os_version_trims_and_lowercases() {
        assert_eq!(
            parse_os_version(OsFamily::Windows, "  WIN-11  "),
            Some(OsVersion::WIN_11)
        );
        assert_eq!(
            parse_os_version(OsFamily::MacOS, "\tBIG-SUR\n"),
            Some(OsVersion::MACOS_BIG_SUR)
        );
        assert_eq!(
            parse_os_version(OsFamily::Android, " KitKat "),
            Some(OsVersion::ANDROID_KITKAT)
        );
    }
    #[test]
    fn parse_os_version_empty_and_whitespace_is_none_for_every_family() {
        for os in [
            OsFamily::Windows,
            OsFamily::MacOS,
            OsFamily::IOS,
            OsFamily::Android,
            OsFamily::Linux,
        ] {
            assert_eq!(parse_os_version(os, ""), None, "{os:?} empty");
            assert_eq!(parse_os_version(os, "   "), None, "{os:?} spaces");
            assert_eq!(parse_os_version(os, "\t\n\r"), None, "{os:?} ws");
        }
    }
    #[test]
    fn parse_os_version_garbage_and_unicode_never_panics() {
        for os in [
            OsFamily::Windows,
            OsFamily::MacOS,
            OsFamily::IOS,
            OsFamily::Android,
            OsFamily::Linux,
        ] {
            for s in nasty_strings() {
                // Only requirement: terminate, do not panic, be deterministic.
                let a = parse_os_version(os, &s);
                let b = parse_os_version(os, &s);
                assert_eq!(a, b, "{os:?} not deterministic for {s:?}");
            }
        }
    }
    #[test]
    fn parse_os_version_leading_trailing_junk_is_rejected() {
        assert_eq!(parse_os_version(OsFamily::Windows, "win-11;drop"), None);
        assert_eq!(parse_os_version(OsFamily::MacOS, "sonoma!"), None);
        assert_eq!(parse_os_version(OsFamily::IOS, "17.0.0.0"), None);
        assert_eq!(parse_os_version(OsFamily::Android, "api"), None);
        assert_eq!(parse_os_version(OsFamily::Android, "api abc"), None);
    }
    #[test]
    fn parse_windows_version_prefix_forms_all_collapse_to_the_same_version() {
        for s in [
            "11",
            "win11",
            "win-11",
            "win_11",
            "windows11",
            "windows-11",
            "windows_11",
        ] {
            assert_eq!(
                parse_windows_version(s),
                Some(OsVersion::WIN_11),
                "{s} should parse to WIN_11"
            );
        }
    }
    #[test]
    fn parse_windows_version_bare_prefix_and_separator_only_are_none() {
        assert_eq!(parse_windows_version("win"), None);
        assert_eq!(parse_windows_version("windows"), None);
        assert_eq!(parse_windows_version("win-"), None);
        assert_eq!(parse_windows_version("windows_"), None);
        assert_eq!(parse_windows_version(""), None);
    }
    #[test]
    fn parse_windows_version_nt_aliases_agree_with_names() {
        assert_eq!(parse_windows_version("xp"), parse_windows_version("nt5.1"));
        assert_eq!(parse_windows_version("vista"), parse_windows_version("6.0"));
        assert_eq!(parse_windows_version("8.1"), parse_windows_version("8-1"));
        assert_eq!(parse_windows_version("10"), Some(OsVersion::WIN_10));
    }
    #[test]
    fn parse_windows_version_boundary_numbers_are_none() {
        for s in ["0", "-0", "-1", "NaN", "inf", "99999999999999999999"] {
            assert_eq!(parse_windows_version(s), None, "{s} must not parse");
        }
    }
    #[test]
    fn parse_windows_version_huge_and_unicode_input_terminates() {
        assert_eq!(parse_windows_version(&"win".repeat(200_000)), None);
        assert_eq!(parse_windows_version(&"1".repeat(1_000_000)), None);
        assert_eq!(parse_windows_version("\u{1F600}"), None);
        assert_eq!(parse_windows_version("win-\u{1F600}"), None);
    }
    #[test]
    fn strip_os_prefix_strips_prefix_and_optional_separator() {
        assert_eq!(strip_os_prefix("win-11", &["win"]), "11");
        assert_eq!(strip_os_prefix("win_11", &["win"]), "11");
        assert_eq!(strip_os_prefix("win11", &["win"]), "11");
        // Longest prefix must be listed first; that ordering is the caller's job.
        assert_eq!(strip_os_prefix("windows-11", &["windows", "win"]), "11");
        assert_eq!(strip_os_prefix("windows-11", &["win", "windows"]), "dows-11");
    }
    #[test]
    fn strip_os_prefix_leaves_non_matching_input_untouched() {
        assert_eq!(strip_os_prefix("11", &["win"]), "11");
        assert_eq!(strip_os_prefix("", &["win"]), "");
        assert_eq!(strip_os_prefix("anything", &[]), "anything");
        assert_eq!(strip_os_prefix("\u{1F600}win", &["win"]), "\u{1F600}win");
    }
    #[test]
    fn strip_os_prefix_only_strips_one_separator() {
        assert_eq!(strip_os_prefix("win--11", &["win"]), "-11");
        assert_eq!(strip_os_prefix("win-", &["win"]), "");
        assert_eq!(strip_os_prefix("win", &["win"]), "");
    }
    #[test]
    fn strip_os_prefix_with_empty_prefix_is_identity() {
        // An empty prefix matches everything; it must still not eat a leading char
        // other than a separator, and must not panic on multibyte input.
        assert_eq!(strip_os_prefix("日本語", &[""]), "日本語");
        assert_eq!(strip_os_prefix("-日本語", &[""]), "日本語");
    }
    #[test]
    fn parse_macos_version_names_and_numbers_agree() {
        assert_eq!(parse_macos_version("cheetah"), parse_macos_version("10.0"));
        assert_eq!(parse_macos_version("big-sur"), parse_macos_version("bigsur"));
        assert_eq!(parse_macos_version("bigsur"), parse_macos_version("11.0"));
        assert_eq!(parse_macos_version("tahoe"), Some(OsVersion::MACOS_TAHOE));
        assert_eq!(
            parse_macos_version("snow-leopard"),
            parse_macos_version("snowleopard")
        );
    }
    #[test]
    fn parse_macos_version_rejects_junk_and_terminates_on_huge_input() {
        for s in ["", " ", "sonoma ", "SONOMA", "10.16", "27", "🍎"] {
            assert_eq!(parse_macos_version(s), None, "{s:?} must not parse");
        }
        assert_eq!(parse_macos_version(&"10.".repeat(100_000)), None);
    }
    #[test]
    fn parse_macos_version_is_ordered_monotonically() {
        let names = [
            "cheetah", "puma", "jaguar", "panther", "tiger", "leopard", "lion", "mojave",
            "catalina", "bigsur", "monterey", "ventura", "sonoma", "sequoia", "tahoe",
        ];
        let ids: Vec<u32> = names
            .iter()
            .map(|n| {
                parse_macos_version(n)
                    .unwrap_or_else(|| panic!("{n} must parse"))
                    .version_id
            })
            .collect();
        for w in ids.windows(2) {
            assert!(w[0] < w[1], "macOS version ids must increase: {w:?}");
        }
    }
    #[test]
    fn parse_ios_version_boundaries() {
        assert_eq!(parse_ios_version("1"), Some(OsVersion::IOS_1));
        assert_eq!(parse_ios_version("18.0"), Some(OsVersion::IOS_18));
        assert_eq!(parse_ios_version("0"), None);
        assert_eq!(parse_ios_version("19"), None);
        assert_eq!(parse_ios_version(""), None);
        assert_eq!(parse_ios_version("-1"), None);
        assert_eq!(parse_ios_version("NaN"), None);
        assert_eq!(parse_ios_version(&"9".repeat(500_000)), None);
    }
    #[test]
    fn parse_android_version_api_level_escape_hatch() {
        assert_eq!(
            parse_android_version("api34"),
            Some(OsVersion::new(OsFamily::Android, 34))
        );
        assert_eq!(
            parse_android_version("api 34"),
            Some(OsVersion::new(OsFamily::Android, 34))
        );
        assert_eq!(
            parse_android_version("api0"),
            Some(OsVersion::new(OsFamily::Android, 0))
        );
        assert_eq!(
            parse_android_version(&format!("api{}", u32::MAX)),
            Some(OsVersion::new(OsFamily::Android, u32::MAX))
        );
    }
    #[test]
    fn parse_android_version_api_level_out_of_range_is_none_not_a_panic() {
        // u32::MAX + 1 and beyond must be rejected by `parse::<u32>()`, not wrap.
        assert_eq!(parse_android_version("api4294967296"), None);
        assert_eq!(parse_android_version("api-1"), None);
        assert_eq!(parse_android_version("api+1"), Some(OsVersion::new(OsFamily::Android, 1)));
        assert_eq!(parse_android_version("apiNaN"), None);
        assert_eq!(parse_android_version(&format!("api{}", "9".repeat(100_000))), None);
    }
    #[test]
    fn parse_android_version_named_releases() {
        assert_eq!(parse_android_version("q"), Some(OsVersion::ANDROID_10));
        assert_eq!(parse_android_version("13"), parse_android_version("t"));
        assert_eq!(
            parse_android_version("13"),
            parse_android_version("tiramisu")
        );
        assert_eq!(parse_android_version("15"), Some(OsVersion::ANDROID_15));
        assert_eq!(parse_android_version(""), None);
        assert_eq!(parse_android_version("🤖"), None);
    }
    #[test]
    fn parse_linux_version_accepts_bare_major_and_prefixes() {
        assert_eq!(
            parse_linux_version("5"),
            Some(OsVersion::new(OsFamily::Linux, 5000))
        );
        assert_eq!(parse_linux_version("6.0"), Some(OsVersion::LINUX_6_0));
        assert_eq!(parse_linux_version("linux6.0"), Some(OsVersion::LINUX_6_0));
        assert_eq!(parse_linux_version("linux-6.0"), Some(OsVersion::LINUX_6_0));
        assert_eq!(parse_linux_version("linux_6.0"), Some(OsVersion::LINUX_6_0));
        assert_eq!(
            parse_linux_version("6.17.0"),
            Some(OsVersion::new(OsFamily::Linux, 6170))
        );
    }
    #[test]
    fn parse_linux_version_rejects_malformed_and_unicode() {
        for s in [
            "", " ", ".", "..", "-1", "6.-1", "6.x", "x.6", "NaN", "inf", "🐧", "linux", "linux-",
        ] {
            assert_eq!(parse_linux_version(s), None, "{s:?} must not parse");
        }
    }
    #[test]
    fn parse_linux_version_ignores_everything_past_the_patch_component() {
        // Only major/minor/patch are consumed by `split('.')`; anything after the third
        // component is silently dropped — including outright garbage. Pinning the
        // behaviour so a future tightening is a deliberate change, not a surprise.
        assert_eq!(
            parse_linux_version("6.1.2"),
            Some(OsVersion::new(OsFamily::Linux, 6012))
        );
        assert_eq!(
            parse_linux_version("6.1.2.3"),
            Some(OsVersion::new(OsFamily::Linux, 6012))
        );
        assert_eq!(
            parse_linux_version("6.0.0.0extra"),
            Some(OsVersion::LINUX_6_0),
            "a 4th component is never parsed, so trailing junk is accepted"
        );
    }
    // RED (genuine bug): `major * 1000 + minor * 10 + patch` is unchecked u32 arithmetic.
    // A major >= 4_294_968 overflows and panics in any debug/overflow-checks build, and
    // wraps silently in release. The input is attacker-reachable from CSS via
    // `@os(linux >= 5000000)`, so a stylesheet can crash the app.
    #[test]
    fn parse_linux_version_huge_major_does_not_overflow() {
        assert_eq!(
            parse_linux_version("5000000"),
            None,
            "out-of-range kernel major must be rejected, not overflow u32"
        );
    }
    // RED (same root cause, via the minor component: `minor * 10` overflows).
    #[test]
    fn parse_linux_version_huge_minor_does_not_overflow() {
        assert_eq!(
            parse_linux_version("1.999999999"),
            None,
            "out-of-range kernel minor must be rejected, not overflow u32"
        );
    }
    #[test]
    fn parse_linux_version_max_u32_component_is_rejected_by_parse_not_by_math() {
        // `u32::MAX + 1` fails `parse::<u32>()` before any multiplication happens.
        assert_eq!(parse_linux_version("4294967296"), None);
    }
    // ---------------------------------------------------------------
    // 23-24. LinuxDesktopEnv / ThemeCondition converters
    // ---------------------------------------------------------------
    #[test]
    fn linux_desktop_env_from_system_maps_unknown_des_to_other() {
        use crate::system::DesktopEnvironment;
        assert_eq!(
            LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Gnome),
            LinuxDesktopEnv::Gnome
        );
        assert_eq!(
            LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Kde),
            LinuxDesktopEnv::KDE
        );
        // XFCE/Unity/Cinnamon/MATE are parse-only: they collapse to `Other` at runtime.
        for name in ["xfce", "", "🖥", &"x".repeat(10_000)] {
            assert_eq!(
                LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Other(
                    AzString::from(name.to_string())
                )),
                LinuxDesktopEnv::Other
            );
        }
    }
    #[test]
    fn theme_condition_from_system_theme_is_total() {
        use crate::system::Theme;
        assert_eq!(
            ThemeCondition::from_system_theme(Theme::Light),
            ThemeCondition::Light
        );
        assert_eq!(
            ThemeCondition::from_system_theme(Theme::Dark),
            ThemeCondition::Dark
        );
    }
    // ---------------------------------------------------------------
    // 25. LanguageCondition::matches
    // ---------------------------------------------------------------
    #[test]
    fn language_exact_is_case_insensitive_and_strict() {
        let cond = LanguageCondition::Exact(AzString::from_const_str("de-DE"));
        assert!(cond.matches("de-DE"));
        assert!(cond.matches("DE-de"));
        assert!(!cond.matches("de"));
        assert!(!cond.matches("de-AT"));
        assert!(!cond.matches("de-DE-x"));
        assert!(!cond.matches(""));
    }
    #[test]
    fn language_prefix_matches_subtags_only_at_a_dash_boundary() {
        let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
        assert!(cond.matches("de"));
        assert!(cond.matches("de-DE"));
        assert!(cond.matches("DE-at"));
        // "den" must NOT match prefix "de" — the boundary has to be '-'.
        assert!(!cond.matches("den"));
        assert!(!cond.matches("deu"));
        assert!(!cond.matches("d"));
        assert!(!cond.matches(""));
    }
    #[test]
    fn language_empty_prefix_matches_only_the_empty_tag() {
        // An empty prefix is NOT a wildcard. `matches` is a subtag/dash-boundary
        // prefix matcher, and CSS agrees: for `[att^=val]`, "if val is the empty
        // string then the selector does not represent anything"
        // (Selectors Level 3 §6.3.2). Both `@lang()` parsers refuse to build
        // `Prefix("")` anyway, so this value is unreachable in practice.
        let cond = LanguageCondition::Prefix(AzString::from_const_str(""));
        assert!(cond.matches(""));
        assert!(!cond.matches("en-US"));
    }
    #[test]
    fn language_prefix_longer_than_input_is_false() {
        let cond = LanguageCondition::Prefix(AzString::from_const_str("de-DE-1996"));
        assert!(!cond.matches("de"));
        assert!(!cond.matches(""));
    }
    #[test]
    fn language_matches_huge_input_terminates() {
        let cond = LanguageCondition::Prefix(AzString::from_const_str("en"));
        let huge = format!("en-{}", "a".repeat(1_000_000));
        assert!(cond.matches(&huge));
        let cond_exact = LanguageCondition::Exact(AzString::from_const_str("en"));
        assert!(!cond_exact.matches(&huge));
    }
    // RED (genuine bug, PANIC): `LanguageCondition::Prefix` slices the language tag with
    // `&language[..prefix_str.len()]` — a *byte* index. When the runtime language tag is
    // non-ASCII (or merely multibyte), that index can land inside a UTF-8 code point and
    // `str` indexing panics. `@lang("de")` + a system locale reported as e.g. "日本語"
    // aborts style resolution.
    #[test]
    fn language_prefix_does_not_panic_on_multibyte_language_tag() {
        let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
        // 2-byte prefix index falls inside the 3-byte '日'.
        assert!(!cond.matches("日本語"));
    }
    // RED (same root cause, 1-byte prefix into a 2-byte char).
    #[test]
    fn language_prefix_does_not_panic_on_two_byte_language_tag() {
        let cond = LanguageCondition::Prefix(AzString::from_const_str("d"));
        assert!(!cond.matches("é"));
    }
    // ---------------------------------------------------------------
    // 26-30. DynamicSelectorContext
    // ---------------------------------------------------------------
    #[test]
    fn context_from_system_style_default_is_coherent() {
        let style = crate::system::SystemStyle::default();
        let ctx = DynamicSelectorContext::from_system_style(&style);
        // Platform::Unknown -> OsCondition::Any, and no desktop env.
        assert_eq!(ctx.os, OsCondition::Any);
        assert_eq!(ctx.desktop_env, OptionLinuxDesktopEnv::None);
        assert_eq!(ctx.de_version, 0);
        assert_eq!(ctx.theme, ThemeCondition::Light);
        assert_eq!(ctx.media_type, MediaType::Screen);
        assert_eq!(ctx.viewport_width, DEFAULT_VIEWPORT_WIDTH);
        assert_eq!(ctx.viewport_height, DEFAULT_VIEWPORT_HEIGHT);
        // "no container" is encoded as NaN, and must therefore never match a
        // @container query.
        assert!(ctx.container_width.is_nan());
        assert!(ctx.container_height.is_nan());
        assert!(ctx.window_focused);
    }
    #[test]
    fn context_from_system_style_linux_carries_the_desktop_env() {
        use crate::system::{DesktopEnvironment, Platform};
        let mut style = crate::system::SystemStyle::default();
        style.platform = Platform::Linux(DesktopEnvironment::Kde);
        let ctx = DynamicSelectorContext::from_system_style(&style);
        assert_eq!(ctx.os, OsCondition::Linux);
        assert_eq!(
            ctx.desktop_env,
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE)
        );
    }
    #[test]
    fn with_viewport_updates_orientation_and_is_deterministic_at_extremes() {
        let base = DynamicSelectorContext::default();
        assert_eq!(
            base.with_viewport(1920.0, 1080.0).orientation,
            OrientationType::Landscape
        );
        assert_eq!(
            base.with_viewport(1080.0, 1920.0).orientation,
            OrientationType::Portrait
        );
        // Square is *not* landscape (strict `>`), by construction.
        assert_eq!(
            base.with_viewport(500.0, 500.0).orientation,
            OrientationType::Portrait
        );
        // NaN comparisons are all false -> Portrait. Deterministic, no panic.
        assert_eq!(
            base.with_viewport(f32::NAN, f32::NAN).orientation,
            OrientationType::Portrait
        );
        assert_eq!(
            base.with_viewport(f32::INFINITY, f32::NEG_INFINITY).orientation,
            OrientationType::Landscape
        );
        // Zero / negative sizes must not panic.
        let z = base.with_viewport(0.0, 0.0);
        assert_eq!(z.viewport_width, 0.0);
        assert_eq!(z.orientation, OrientationType::Portrait);
        let neg = base.with_viewport(-100.0, -200.0);
        assert_eq!(neg.orientation, OrientationType::Landscape);
    }
    #[test]
    fn with_viewport_does_not_disturb_unrelated_fields() {
        let base = DynamicSelectorContext::default();
        let updated = base.with_viewport(1.0, 2.0);
        assert_eq!(updated.os, base.os);
        assert_eq!(updated.theme, base.theme);
        assert_eq!(updated.language, base.language);
        assert_eq!(updated.pseudo_state, base.pseudo_state);
    }
    #[test]
    fn with_container_stores_name_and_extreme_dimensions() {
        let base = DynamicSelectorContext::default();
        let named = base.with_container(
            f32::MAX,
            f32::NEG_INFINITY,
            Some(AzString::from_const_str("sidebar")),
        );
        assert_eq!(named.container_width, f32::MAX);
        assert_eq!(named.container_height, f32::NEG_INFINITY);
        assert_eq!(
            named.container_name.as_ref(),
            Some(&AzString::from_const_str("sidebar"))
        );
        let unnamed = base.with_container(0.0, 0.0, None);
        assert_eq!(unnamed.container_name.as_ref(), None);
    }
    #[test]
    fn with_container_nan_dimensions_stay_unmatched() {
        let ctx = DynamicSelectorContext::default().with_container(f32::NAN, f32::NAN, None);
        // NaN container size means "no container": the guard in `matches` must reject
        // even a fully-open range.
        let open = DynamicSelector::ContainerWidth(MinMaxRange::new(None, None));
        assert!(!open.matches(&ctx));
        let open_h = DynamicSelector::ContainerHeight(MinMaxRange::new(None, None));
        assert!(!open_h.matches(&ctx));
    }
    #[test]
    fn with_pseudo_state_replaces_the_whole_flag_set() {
        let base = DynamicSelectorContext::default();
        let hovered = base.with_pseudo_state(PseudoStateFlags {
            hover: true,
            ..PseudoStateFlags::default()
        });
        assert!(hovered.pseudo_state.hover);
        assert!(!hovered.pseudo_state.active);
        // Replacing again must not OR the previous state in.
        let active = hovered.with_pseudo_state(PseudoStateFlags {
            active: true,
            ..PseudoStateFlags::default()
        });
        assert!(!active.pseudo_state.hover);
        assert!(active.pseudo_state.active);
    }
    #[test]
    fn viewport_breakpoint_changed_detects_crossings_only() {
        let bps = [480.0_f32, 768.0, 1024.0];
        let base = DynamicSelectorContext::default();
        let small = base.with_viewport(320.0, 480.0);
        let medium = base.with_viewport(800.0, 600.0);
        let also_medium = base.with_viewport(900.0, 600.0);
        assert!(small.viewport_breakpoint_changed(&medium, &bps));
        assert!(medium.viewport_breakpoint_changed(&small, &bps));
        assert!(!medium.viewport_breakpoint_changed(&also_medium, &bps));
        assert!(!small.viewport_breakpoint_changed(&small, &bps));
    }
    #[test]
    fn viewport_breakpoint_changed_is_exactly_on_the_boundary() {
        let bps = [800.0_f32];
        let base = DynamicSelectorContext::default();
        // `>=` bound: 800 is "above", 799.99 is not.
        let at = base.with_viewport(800.0, 600.0);
        let just_below = base.with_viewport(799.99, 600.0);
        assert!(at.viewport_breakpoint_changed(&just_below, &bps));
        assert!(!at.viewport_breakpoint_changed(&at, &bps));
    }
    #[test]
    fn viewport_breakpoint_changed_handles_empty_and_degenerate_breakpoints() {
        let base = DynamicSelectorContext::default();
        let a = base.with_viewport(100.0, 100.0);
        let b = base.with_viewport(5000.0, 100.0);
        assert!(!a.viewport_breakpoint_changed(&b, &[]));
        // NaN breakpoints: `>=` is false on both sides -> no crossing, no panic.
        assert!(!a.viewport_breakpoint_changed(&b, &[f32::NAN]));
        // Infinite breakpoints: nothing is >= +inf, everything is >= -inf.
        assert!(!a.viewport_breakpoint_changed(&b, &[f32::INFINITY]));
        assert!(!a.viewport_breakpoint_changed(&b, &[f32::NEG_INFINITY]));
        // A NaN viewport is never "above" any breakpoint.
        let nan_vp = base.with_viewport(f32::NAN, 100.0);
        assert!(nan_vp.viewport_breakpoint_changed(&b, &[800.0]));
    }
    #[test]
    fn viewport_breakpoint_changed_with_many_breakpoints_terminates() {
        let bps: Vec<f32> = (0..100_000).map(|i| i as f32).collect();
        let base = DynamicSelectorContext::default();
        let a = base.with_viewport(0.0, 100.0);
        let b = base.with_viewport(99_999.0, 100.0);
        assert!(a.viewport_breakpoint_changed(&b, &bps));
    }
    // ---------------------------------------------------------------
    // 31-35. DynamicSelector matching
    // ---------------------------------------------------------------
    #[test]
    fn match_os_any_matches_everything() {
        for actual in [
            OsCondition::Any,
            OsCondition::Apple,
            OsCondition::MacOS,
            OsCondition::IOS,
            OsCondition::Linux,
            OsCondition::Windows,
            OsCondition::Android,
            OsCondition::Web,
        ] {
            assert!(DynamicSelector::match_os(OsCondition::Any, actual));
        }
    }
    #[test]
    fn match_os_apple_is_the_macos_ios_union() {
        assert!(DynamicSelector::match_os(
            OsCondition::Apple,
            OsCondition::MacOS
        ));
        assert!(DynamicSelector::match_os(
            OsCondition::Apple,
            OsCondition::IOS
        ));
        assert!(!DynamicSelector::match_os(
            OsCondition::Apple,
            OsCondition::Linux
        ));
        // Note the asymmetry: `Apple` as the *actual* OS does not satisfy `MacOS`.
        assert!(!DynamicSelector::match_os(
            OsCondition::MacOS,
            OsCondition::Apple
        ));
    }
    #[test]
    fn match_os_concrete_conditions_require_equality() {
        assert!(DynamicSelector::match_os(
            OsCondition::Linux,
            OsCondition::Linux
        ));
        assert!(!DynamicSelector::match_os(
            OsCondition::Linux,
            OsCondition::Windows
        ));
        // `Any` as the *actual* OS (i.e. unknown platform) must not satisfy a concrete rule.
        assert!(!DynamicSelector::match_os(
            OsCondition::Windows,
            OsCondition::Any
        ));
    }
    #[test]
    fn match_os_version_min_max_exact_at_zero_and_u32_max() {
        let zero = OsVersion::new(OsFamily::Linux, 0);
        let max = OsVersion::new(OsFamily::Linux, u32::MAX);
        let none = OptionLinuxDesktopEnv::None;
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::Min(zero),
            max,
            none,
            0
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::Min(max),
            zero,
            none,
            0
        ));
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::Max(max),
            zero,
            none,
            0
        ));
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::Exact(max),
            max,
            none,
            0
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::Exact(zero),
            max,
            none,
            0
        ));
    }
    #[test]
    fn match_os_version_cross_family_never_matches() {
        let none = OptionLinuxDesktopEnv::None;
        // A Windows rule evaluated against a macOS runtime must be false for all three ops.
        for cond in [
            OsVersionCondition::Min(OsVersion::WIN_10),
            OsVersionCondition::Max(OsVersion::WIN_10),
            OsVersionCondition::Exact(OsVersion::WIN_10),
        ] {
            assert!(
                !DynamicSelector::match_os_version(
                    &cond,
                    OsVersion::MACOS_SONOMA,
                    none,
                    0
                ),
                "{cond:?} must not match a macOS runtime"
            );
        }
    }
    #[test]
    fn match_os_version_desktop_environment_requires_the_env_to_be_present() {
        let cond = OsVersionCondition::DesktopEnvironment(LinuxDesktopEnv::Gnome);
        assert!(DynamicSelector::match_os_version(
            &cond,
            OsVersion::unknown(),
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
            0
        ));
        assert!(!DynamicSelector::match_os_version(
            &cond,
            OsVersion::unknown(),
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
            0
        ));
        assert!(!DynamicSelector::match_os_version(
            &cond,
            OsVersion::unknown(),
            OptionLinuxDesktopEnv::None,
            0
        ));
    }
    #[test]
    fn match_os_version_desktop_env_min_max_respect_the_unknown_zero_sentinel() {
        let gnome = OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome);
        let dev = DesktopEnvVersion {
            env: LinuxDesktopEnv::Gnome,
            version_id: 40,
        };
        // de_version == 0 means "not detected": Min/Max constraints must fail.
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMin(dev),
            OsVersion::unknown(),
            gnome,
            0
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMax(dev),
            OsVersion::unknown(),
            gnome,
            0
        ));
        // With a real version, the bounds are inclusive.
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMin(dev),
            OsVersion::unknown(),
            gnome,
            40
        ));
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMin(dev),
            OsVersion::unknown(),
            gnome,
            u32::MAX
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMin(dev),
            OsVersion::unknown(),
            gnome,
            39
        ));
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMax(dev),
            OsVersion::unknown(),
            gnome,
            40
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvMax(dev),
            OsVersion::unknown(),
            gnome,
            41
        ));
    }
    #[test]
    fn match_os_version_desktop_env_exact_needs_the_matching_env() {
        let dev = DesktopEnvVersion {
            env: LinuxDesktopEnv::Gnome,
            version_id: 45,
        };
        assert!(DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvExact(dev),
            OsVersion::unknown(),
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
            45
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvExact(dev),
            OsVersion::unknown(),
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
            45
        ));
        assert!(!DynamicSelector::match_os_version(
            &OsVersionCondition::DesktopEnvExact(dev),
            OsVersion::unknown(),
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
            46
        ));
    }
    // RED (genuine bug, low severity): the invariant documented directly above
    // `match_os_version` is "de_version == 0 means the runtime hasn't reported a version,
    // so any DE-version constraint fails". `DesktopEnvMin`/`DesktopEnvMax` guard on
    // `de_version != 0`, but `DesktopEnvExact` does not — so `@os(linux:gnome = 0)`
    // matches every GNOME session while DE-version detection is still unwired.
    #[test]
    fn match_os_version_desktop_env_exact_zero_fails_when_de_version_is_unknown() {
        let dev = DesktopEnvVersion {
            env: LinuxDesktopEnv::Gnome,
            version_id: 0,
        };
        assert!(
            !DynamicSelector::match_os_version(
                &OsVersionCondition::DesktopEnvExact(dev),
                OsVersion::unknown(),
                OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
                0
            ),
            "de_version == 0 is the 'unknown' sentinel; it must not satisfy an exact match"
        );
    }
    #[test]
    fn match_theme_system_preferred_is_a_wildcard() {
        for actual in [
            ThemeCondition::Light,
            ThemeCondition::Dark,
            ThemeCondition::SystemPreferred,
            ThemeCondition::Custom(AzString::from_const_str("solarized")),
        ] {
            assert!(DynamicSelector::match_theme(
                &ThemeCondition::SystemPreferred,
                &actual
            ));
        }
    }
    #[test]
    fn match_theme_custom_compares_the_name() {
        let a = ThemeCondition::Custom(AzString::from_const_str("nord"));
        let b = ThemeCondition::Custom(AzString::from_const_str("nord"));
        let c = ThemeCondition::Custom(AzString::from_const_str("Nord"));
        assert!(DynamicSelector::match_theme(&a, &b));
        // Theme names are compared case-sensitively.
        assert!(!DynamicSelector::match_theme(&a, &c));
        assert!(!DynamicSelector::match_theme(&a, &ThemeCondition::Dark));
        // `SystemPreferred` as the *actual* theme does not satisfy a concrete rule.
        assert!(!DynamicSelector::match_theme(
            &ThemeCondition::Dark,
            &ThemeCondition::SystemPreferred
        ));
    }
    #[test]
    fn match_pseudo_state_reads_through_to_the_context_flags() {
        let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
            hover: true,
            checked: true,
            ..PseudoStateFlags::default()
        });
        assert!(DynamicSelector::match_pseudo_state(
            PseudoStateType::Hover,
            &ctx
        ));
        assert!(DynamicSelector::match_pseudo_state(
            PseudoStateType::CheckedTrue,
            &ctx
        ));
        assert!(!DynamicSelector::match_pseudo_state(
            PseudoStateType::CheckedFalse,
            &ctx
        ));
        assert!(!DynamicSelector::match_pseudo_state(
            PseudoStateType::Active,
            &ctx
        ));
        assert!(DynamicSelector::match_pseudo_state(
            PseudoStateType::Normal,
            &ctx
        ));
    }
    #[test]
    fn match_pseudo_state_agrees_with_has_state_for_every_state() {
        let flags = PseudoStateFlags {
            hover: true,
            focused: true,
            visited: true,
            drag_over: true,
            ..PseudoStateFlags::default()
        };
        let ctx = DynamicSelectorContext::default().with_pseudo_state(flags);
        for state in [
            PseudoStateType::Normal,
            PseudoStateType::Hover,
            PseudoStateType::Active,
            PseudoStateType::Focus,
            PseudoStateType::Disabled,
            PseudoStateType::CheckedTrue,
            PseudoStateType::CheckedFalse,
            PseudoStateType::FocusWithin,
            PseudoStateType::Visited,
            PseudoStateType::Backdrop,
            PseudoStateType::Dragging,
            PseudoStateType::DragOver,
        ] {
            assert_eq!(
                DynamicSelector::match_pseudo_state(state, &ctx),
                flags.has_state(state),
                "match_pseudo_state and has_state disagree on {state:?}"
            );
        }
    }
    #[test]
    fn selector_matches_every_variant_against_the_default_context() {
        // Smoke: no variant may panic on the default context.
        let ctx = DynamicSelectorContext::default();
        for sel in all_selector_variants() {
            let a = sel.matches(&ctx);
            let b = sel.matches(&ctx);
            assert_eq!(a, b, "{sel:?} is not deterministic");
        }
    }
    #[test]
    fn selector_matches_media_all_is_a_wildcard() {
        let ctx = DynamicSelectorContext::default();
        assert_eq!(ctx.media_type, MediaType::Screen);
        assert!(DynamicSelector::Media(MediaType::All).matches(&ctx));
        assert!(DynamicSelector::Media(MediaType::Screen).matches(&ctx));
        assert!(!DynamicSelector::Media(MediaType::Print).matches(&ctx));
    }
    #[test]
    fn selector_matches_aspect_ratio_never_divides_by_zero() {
        let base = DynamicSelectorContext::default();
        // height 0 is clamped to 1.0 by `.max(1.0)`, so the ratio stays finite.
        let flat = base.with_viewport(800.0, 0.0);
        assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
            .matches(&flat));
        // NaN height also clamps to 1.0 (f32::max ignores NaN).
        let nan_h = base.with_viewport(800.0, f32::NAN);
        assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
            .matches(&nan_h));
        // A NaN *width* yields a NaN ratio, which any real bound rejects.
        let nan_w = base.with_viewport(f32::NAN, 600.0);
        assert!(!DynamicSelector::AspectRatio(MinMaxRange::with_min(0.0)).matches(&nan_w));
    }
    #[test]
    fn selector_matches_container_name_requires_an_exact_name() {
        let ctx = DynamicSelectorContext::default().with_container(
            100.0,
            100.0,
            Some(AzString::from_const_str("sidebar")),
        );
        assert!(
            DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&ctx)
        );
        assert!(
            !DynamicSelector::ContainerName(AzString::from_const_str("Sidebar")).matches(&ctx)
        );
        assert!(!DynamicSelector::ContainerName(AzString::from_const_str("main")).matches(&ctx));
        // No container at all.
        let no_ctr = DynamicSelectorContext::default();
        assert!(
            !DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&no_ctr)
        );
    }
    #[test]
    fn selector_matches_bool_conditions_compare_both_polarities() {
        let ctx = DynamicSelectorContext::default();
        assert_eq!(ctx.prefers_reduced_motion, BoolCondition::False);
        // The impl compares equality, so `False` matches a "no preference" runtime.
        assert!(DynamicSelector::PrefersReducedMotion(BoolCondition::False).matches(&ctx));
        assert!(!DynamicSelector::PrefersReducedMotion(BoolCondition::True).matches(&ctx));
        assert!(DynamicSelector::PrefersHighContrast(BoolCondition::False).matches(&ctx));
        assert!(!DynamicSelector::PrefersHighContrast(BoolCondition::True).matches(&ctx));
    }
    #[test]
    fn bool_condition_roundtrips_through_bool() {
        for b in [false, true] {
            assert_eq!(bool::from(BoolCondition::from(b)), b);
        }
        assert_eq!(BoolCondition::default(), BoolCondition::False);
        assert!(!bool::from(BoolCondition::False));
        assert!(bool::from(BoolCondition::True));
    }
    // ---------------------------------------------------------------
    // 40-57. CssPropertyWithConditions
    // ---------------------------------------------------------------
    #[test]
    fn simple_property_is_unconditional_and_always_matches() {
        let p = CssPropertyWithConditions::simple(paint_prop());
        assert!(p.apply_if.as_slice().is_empty());
        assert!(!p.is_conditional());
        assert!(!p.is_pseudo_state_only());
        assert!(p.matches(&DynamicSelectorContext::default()));
    }
    #[test]
    fn with_condition_and_with_single_condition_agree() {
        let a = CssPropertyWithConditions::with_condition(
            paint_prop(),
            DynamicSelector::PseudoState(PseudoStateType::Hover),
        );
        let b = CssPropertyWithConditions::on_hover(paint_prop());
        assert_eq!(a.apply_if.as_slice(), b.apply_if.as_slice());
        assert_eq!(a.apply_if.as_slice().len(), 1);
    }
    #[test]
    fn with_conditions_preserves_order_and_length() {
        let conds = all_selector_variants();
        let p = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(conds.clone()),
        );
        assert_eq!(p.apply_if.as_slice().len(), conds.len());
        for (i, c) in conds.iter().enumerate() {
            assert_eq!(p.apply_if.as_slice()[i].variant_tag(), c.variant_tag());
        }
        assert!(p.is_conditional());
    }
    #[test]
    fn with_conditions_empty_vec_is_unconditional() {
        let p = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(vec![]),
        );
        assert!(!p.is_conditional());
        assert!(p.matches(&DynamicSelectorContext::default()));
    }
    #[test]
    fn pseudo_state_constructors_build_the_right_condition() {
        let cases = [
            (
                CssPropertyWithConditions::on_hover(paint_prop()),
                PseudoStateType::Hover,
            ),
            (
                CssPropertyWithConditions::on_active(paint_prop()),
                PseudoStateType::Active,
            ),
            (
                CssPropertyWithConditions::on_focus(paint_prop()),
                PseudoStateType::Focus,
            ),
            (
                CssPropertyWithConditions::when_disabled(paint_prop()),
                PseudoStateType::Disabled,
            ),
        ];
        for (prop, expected) in cases {
            assert_eq!(
                prop.apply_if.as_slice(),
                &[DynamicSelector::PseudoState(expected)]
            );
            assert!(prop.is_pseudo_state_only());
            assert!(prop.is_conditional());
        }
    }
    #[test]
    fn os_and_theme_constructors_build_the_right_condition() {
        assert_eq!(
            CssPropertyWithConditions::on_windows(paint_prop())
                .apply_if
                .as_slice(),
            &[DynamicSelector::Os(OsCondition::Windows)]
        );
        assert_eq!(
            CssPropertyWithConditions::on_macos(paint_prop())
                .apply_if
                .as_slice(),
            &[DynamicSelector::Os(OsCondition::MacOS)]
        );
        assert_eq!(
            CssPropertyWithConditions::on_linux(paint_prop())
                .apply_if
                .as_slice(),
            &[DynamicSelector::Os(OsCondition::Linux)]
        );
        assert_eq!(
            CssPropertyWithConditions::on_os(paint_prop(), OsCondition::Web)
                .apply_if
                .as_slice(),
            &[DynamicSelector::Os(OsCondition::Web)]
        );
        assert_eq!(
            CssPropertyWithConditions::dark_theme(paint_prop())
                .apply_if
                .as_slice(),
            &[DynamicSelector::Theme(ThemeCondition::Dark)]
        );
        assert_eq!(
            CssPropertyWithConditions::light_theme(paint_prop())
                .apply_if
                .as_slice(),
            &[DynamicSelector::Theme(ThemeCondition::Light)]
        );
        // OS / theme conditions are not pseudo-state conditions.
        assert!(!CssPropertyWithConditions::on_linux(paint_prop()).is_pseudo_state_only());
        assert!(!CssPropertyWithConditions::dark_theme(paint_prop()).is_pseudo_state_only());
    }
    #[test]
    fn matches_requires_all_conditions_to_hold() {
        let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
            hover: true,
            ..PseudoStateFlags::default()
        });
        // hover (true) AND focus (false) -> false.
        let both = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(vec![
                DynamicSelector::PseudoState(PseudoStateType::Hover),
                DynamicSelector::PseudoState(PseudoStateType::Focus),
            ]),
        );
        assert!(!both.matches(&ctx));
        // hover (true) AND normal (always true) -> true.
        let ok = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(vec![
                DynamicSelector::PseudoState(PseudoStateType::Hover),
                DynamicSelector::PseudoState(PseudoStateType::Normal),
            ]),
        );
        assert!(ok.matches(&ctx));
    }
    #[test]
    fn matches_with_a_large_condition_list_terminates() {
        let conds: Vec<DynamicSelector> = (0..50_000)
            .map(|_| DynamicSelector::PseudoState(PseudoStateType::Normal))
            .collect();
        let p = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(conds),
        );
        assert!(p.matches(&DynamicSelectorContext::default()));
    }
    #[test]
    fn is_pseudo_state_only_is_false_for_empty_and_for_mixed_lists() {
        // Empty -> false (there is no pseudo-state condition at all).
        assert!(!CssPropertyWithConditions::simple(paint_prop()).is_pseudo_state_only());
        // Mixed -> false.
        let mixed = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(vec![
                DynamicSelector::PseudoState(PseudoStateType::Hover),
                DynamicSelector::Os(OsCondition::Linux),
            ]),
        );
        assert!(!mixed.is_pseudo_state_only());
        // All pseudo -> true.
        let all_pseudo = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(vec![
                DynamicSelector::PseudoState(PseudoStateType::Hover),
                DynamicSelector::PseudoState(PseudoStateType::Focus),
            ]),
        );
        assert!(all_pseudo.is_pseudo_state_only());
    }
    #[test]
    fn is_layout_affecting_splits_layout_from_paint() {
        assert!(CssPropertyWithConditions::simple(layout_prop()).is_layout_affecting());
        assert!(!CssPropertyWithConditions::simple(paint_prop()).is_layout_affecting());
        // Conditions must not influence the answer — only the property does.
        assert!(CssPropertyWithConditions::on_hover(layout_prop()).is_layout_affecting());
        assert!(!CssPropertyWithConditions::on_hover(paint_prop()).is_layout_affecting());
    }
    #[test]
    fn css_property_with_conditions_hash_agrees_with_eq() {
        let a = CssPropertyWithConditions::on_hover(paint_prop());
        let b = CssPropertyWithConditions::on_hover(paint_prop());
        assert_eq!(a, b);
        assert_eq!(hash_of(&a), hash_of(&b));
        assert_eq!(a.cmp(&b), Ordering::Equal);
        // Same property, different condition sets must not collide on the hash and
        // must not compare Equal (the old impl keyed on condition *count* only).
        let c = CssPropertyWithConditions::on_focus(paint_prop());
        assert_ne!(a, c);
        assert_ne!(a.cmp(&c), Ordering::Equal);
        assert_ne!(hash_of(&a), hash_of(&c));
    }
    #[test]
    fn css_property_with_conditions_ord_is_lexicographic_over_conditions() {
        let short = CssPropertyWithConditions::on_hover(paint_prop());
        let long = CssPropertyWithConditions::with_conditions(
            paint_prop(),
            DynamicSelectorVec::from_vec(vec![
                DynamicSelector::PseudoState(PseudoStateType::Hover),
                DynamicSelector::PseudoState(PseudoStateType::Focus),
            ]),
        );
        // A prefix sorts before the longer list.
        assert_eq!(short.cmp(&long), Ordering::Less);
        assert_eq!(long.cmp(&short), Ordering::Greater);
    }
    // ---------------------------------------------------------------
    // 36-39. @os at-rule parsing (parser feature)
    // ---------------------------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_bare_and_parenthesized_forms_agree() {
        let bare = parse_os_at_rule_content("linux").expect("bare linux");
        let paren = parse_os_at_rule_content("(linux)").expect("(linux)");
        let quoted = parse_os_at_rule_content("(\"linux\")").expect("quoted");
        let spaced = parse_os_at_rule_content("   (  linux  )   ").expect("spaced");
        assert_eq!(bare, vec![DynamicSelector::Os(OsCondition::Linux)]);
        assert_eq!(bare, paren);
        assert_eq!(bare, quoted);
        assert_eq!(bare, spaced);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_emits_the_family_even_for_any() {
        // `(any)` still emits `Os(Any)` (documented: kept for introspection), and
        // `Os(Any)` matches unconditionally.
        for s in ["(any)", "(all)", "(*)"] {
            let conds = parse_os_at_rule_content(s).unwrap_or_else(|| panic!("{s} must parse"));
            assert_eq!(conds, vec![DynamicSelector::Os(OsCondition::Any)]);
            assert!(conds[0].matches(&DynamicSelectorContext::default()));
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_desktop_env_forms() {
        assert_eq!(
            parse_os_at_rule_content("(linux:gnome)"),
            Some(vec![
                DynamicSelector::Os(OsCondition::Linux),
                DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
                    LinuxDesktopEnv::Gnome
                )),
            ])
        );
        // Unknown DE names silently become `Other` (documented `parse_de_token` fallback).
        assert_eq!(
            parse_os_at_rule_content("(linux:notarealde)"),
            Some(vec![
                DynamicSelector::Os(OsCondition::Linux),
                DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
                    LinuxDesktopEnv::Other
                )),
            ])
        );
        // A trailing ':' with no DE is treated as "no DE".
        assert_eq!(
            parse_os_at_rule_content("(linux:)"),
            Some(vec![DynamicSelector::Os(OsCondition::Linux)])
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_version_operators() {
        assert_eq!(
            parse_os_at_rule_content("(windows >= win-11)"),
            Some(vec![
                DynamicSelector::Os(OsCondition::Windows),
                DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
            ])
        );
        // `>` is documented to behave as `>=` (version ids are discrete).
        assert_eq!(
            parse_os_at_rule_content("(windows > win-11)"),
            parse_os_at_rule_content("(windows >= win-11)")
        );
        assert_eq!(
            parse_os_at_rule_content("(macos <= sonoma)"),
            Some(vec![
                DynamicSelector::Os(OsCondition::MacOS),
                DynamicSelector::OsVersion(OsVersionCondition::Max(OsVersion::MACOS_SONOMA)),
            ])
        );
        assert_eq!(
            parse_os_at_rule_content("(ios = 17)"),
            Some(vec![
                DynamicSelector::Os(OsCondition::IOS),
                DynamicSelector::OsVersion(OsVersionCondition::Exact(OsVersion::IOS_17)),
            ])
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_desktop_env_version() {
        assert_eq!(
            parse_os_at_rule_content("(linux:gnome > 40)"),
            Some(vec![
                DynamicSelector::Os(OsCondition::Linux),
                DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvMin(
                    DesktopEnvVersion {
                        env: LinuxDesktopEnv::Gnome,
                        version_id: 40,
                    }
                )),
            ])
        );
        // DE version must be a plain u32: overflow and junk are rejected, not wrapped.
        assert_eq!(parse_os_at_rule_content("(linux:gnome > 4294967296)"), None);
        assert_eq!(parse_os_at_rule_content("(linux:gnome > -1)"), None);
        assert_eq!(parse_os_at_rule_content("(linux:gnome > abc)"), None);
        assert_eq!(parse_os_at_rule_content("(linux:gnome > )"), None);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_rejects_versions_on_versionless_families() {
        // Apple / Web / Any have no version line -> reject rather than guess.
        assert_eq!(parse_os_at_rule_content("(apple >= 14)"), None);
        assert_eq!(parse_os_at_rule_content("(web >= 1)"), None);
        assert_eq!(parse_os_at_rule_content("(any >= 1)"), None);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_empty_and_garbage_is_none() {
        for s in [
            "",
            "   ",
            "\t\n",
            "()",
            "(   )",
            "(\"\")",
            "('')",
            "(:)",
            "(:gnome)",
            "(notanos)",
            "(linux;drop)",
            "\u{1F600}",
            "(\u{1F600})",
            "(linux linux)",
        ] {
            assert_eq!(parse_os_at_rule_content(s), None, "{s:?} must not parse");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_at_rule_huge_and_deeply_parenthesized_input_terminates() {
        // Only one layer of parens is stripped; the rest is junk -> None, no hang.
        let nested = format!("{}linux{}", "(".repeat(10_000), ")".repeat(10_000));
        assert_eq!(parse_os_at_rule_content(&nested), None);
        assert_eq!(parse_os_at_rule_content(&"a".repeat(1_000_000)), None);
        assert_eq!(
            parse_os_at_rule_content(&format!("(linux >= {})", "9".repeat(100_000))),
            None
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn split_op_and_version_picks_the_earliest_then_longest_operator() {
        let (subject, op) = split_op_and_version("linux >= 6.0");
        assert_eq!(subject, "linux ");
        let (op, ver) = op.expect("operator found");
        assert!(matches!(op, VersionOp::Min));
        assert_eq!(ver, "6.0");
        // ">=" must beat "=" at the same position.
        let (_, op) = split_op_and_version("a>=1");
        assert!(matches!(op.expect("op").0, VersionOp::Min));
        let (_, op) = split_op_and_version("a<=1");
        assert!(matches!(op.expect("op").0, VersionOp::Max));
        let (_, op) = split_op_and_version("a=1");
        assert!(matches!(op.expect("op").0, VersionOp::Exact));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn split_op_and_version_with_no_operator_returns_the_whole_string() {
        let (subject, op) = split_op_and_version("linux");
        assert_eq!(subject, "linux");
        assert!(op.is_none());
        let (subject, op) = split_op_and_version("");
        assert_eq!(subject, "");
        assert!(op.is_none());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn split_op_and_version_operator_only_yields_empty_sides() {
        let (subject, op) = split_op_and_version(">=");
        assert_eq!(subject, "");
        assert_eq!(op.expect("op").1, "");
        let (subject, op) = split_op_and_version("=");
        assert_eq!(subject, "");
        assert_eq!(op.expect("op").1, "");
    }
    #[cfg(feature = "parser")]
    #[test]
    fn split_op_and_version_does_not_split_inside_a_multibyte_char() {
        // Operators are ASCII, so the byte offsets returned by `find` are always char
        // boundaries — but assert it, because slicing here would panic otherwise.
        let (subject, op) = split_op_and_version("日本語 >= 6.0");
        assert_eq!(subject, "日本語 ");
        assert_eq!(op.expect("op").1, "6.0");
        // No operator at all in a multibyte string.
        let (subject, op) = split_op_and_version("🦀🦀🦀");
        assert_eq!(subject, "🦀🦀🦀");
        assert!(op.is_none());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_family_token_accepts_aliases_case_insensitively() {
        assert_eq!(parse_os_family_token("LINUX"), Some(OsCondition::Linux));
        assert_eq!(parse_os_family_token("Win"), Some(OsCondition::Windows));
        assert_eq!(parse_os_family_token("windows"), Some(OsCondition::Windows));
        assert_eq!(parse_os_family_token("osx"), Some(OsCondition::MacOS));
        assert_eq!(parse_os_family_token("mac"), Some(OsCondition::MacOS));
        assert_eq!(parse_os_family_token("wasm"), Some(OsCondition::Web));
        assert_eq!(parse_os_family_token("*"), Some(OsCondition::Any));
        assert_eq!(parse_os_family_token("all"), Some(OsCondition::Any));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_os_family_token_rejects_empty_padded_and_junk() {
        // The token is *not* trimmed here — the caller trims.
        for s in [
            "",
            " ",
            " linux",
            "linux ",
            "lin",
            "linux2",
            "0",
            "-1",
            "NaN",
            "\u{1F600}",
        ] {
            assert_eq!(parse_os_family_token(s), None, "{s:?} must not parse");
        }
        assert_eq!(parse_os_family_token(&"linux".repeat(100_000)), None);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_de_token_is_total_and_falls_back_to_other() {
        assert_eq!(parse_de_token("GNOME"), LinuxDesktopEnv::Gnome);
        assert_eq!(parse_de_token("kde"), LinuxDesktopEnv::KDE);
        assert_eq!(parse_de_token("XFCE"), LinuxDesktopEnv::XFCE);
        assert_eq!(parse_de_token("unity"), LinuxDesktopEnv::Unity);
        assert_eq!(parse_de_token("Cinnamon"), LinuxDesktopEnv::Cinnamon);
        assert_eq!(parse_de_token("mate"), LinuxDesktopEnv::MATE);
        // `parse_de_token` returns a value, not an Option: everything else is `Other`.
        for s in ["", "  ", "gnome ", "\u{1F600}", "日本語", "\0"] {
            assert_eq!(parse_de_token(s), LinuxDesktopEnv::Other, "{s:?}");
        }
        assert_eq!(
            parse_de_token(&"gnome".repeat(200_000)),
            LinuxDesktopEnv::Other
        );
    }
    // ---------------------------------------------------------------
    // 58-65. CssPropertyWithConditionsVec parsing (parser feature)
    // ---------------------------------------------------------------
    #[cfg(feature = "parser")]
    fn parse_len(style: &str) -> usize {
        CssPropertyWithConditionsVec::parse(style)
            .into_library_owned_vec()
            .len()
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_valid_minimal_positive_control() {
        let props = CssPropertyWithConditionsVec::parse("color: red;").into_library_owned_vec();
        assert_eq!(props.len(), 1);
        assert!(!props[0].is_conditional());
        assert!(matches!(props[0].property, CssProperty::TextColor(_)));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_empty_and_whitespace_yields_no_properties() {
        for s in ["", " ", "\t\n\r ", ";", ";;;;", "   ;   ;   "] {
            assert_eq!(parse_len(s), 0, "{s:?} must yield no properties");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_garbage_never_panics_and_yields_nothing() {
        for s in [
            "not css at all",
            "color",
            "color:",
            ":",
            "::::",
            "{}",
            "}{",
            "}",
            "{",
            "{{{",
            "}}}",
            "}{color: red}",
            "color: ;",
            "\0: \0;",
            "%s%n%s",
            "\u{1F600}: \u{1F600};",
            "日本語: 赤;",
        ] {
            // Must terminate and not panic; the value itself is allowed to be empty.
            let _ = parse_len(s);
        }
        assert_eq!(parse_len("not css at all"), 0);
        assert_eq!(parse_len("color:"), 0);
        assert_eq!(parse_len("}{"), 0);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_tolerates_a_missing_trailing_semicolon() {
        assert_eq!(parse_len("color: red"), 1);
        assert_eq!(parse_len("color: red;color: blue"), 2);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_pseudo_selector_block_attaches_the_condition() {
        let props = CssPropertyWithConditionsVec::parse(":hover { color: red; }")
            .into_library_owned_vec();
        assert_eq!(props.len(), 1);
        assert!(props[0].is_pseudo_state_only());
        assert_eq!(
            props[0].apply_if.as_slice(),
            &[DynamicSelector::PseudoState(PseudoStateType::Hover)]
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_unknown_selector_block_is_dropped_wholesale() {
        // An unknown pseudo-class must drop the whole block, not leak its properties
        // as unconditional.
        assert_eq!(parse_len(":nosuchstate { color: red; }"), 0);
        assert_eq!(parse_len("@nosuchrule { color: red; }"), 0);
        assert_eq!(parse_len("div { color: red; }"), 0);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_nesting_accumulates_inherited_conditions() {
        let props =
            CssPropertyWithConditionsVec::parse("@os linux { font-size: 14px; :hover { color: red; }}")
                .into_library_owned_vec();
        assert_eq!(props.len(), 2);
        // Both properties carry the @os condition; the hover one carries both.
        let font = props
            .iter()
            .find(|p| matches!(p.property, CssProperty::FontSize(_)))
            .expect("font-size present");
        assert_eq!(
            font.apply_if.as_slice(),
            &[DynamicSelector::Os(OsCondition::Linux)]
        );
        let color = props
            .iter()
            .find(|p| matches!(p.property, CssProperty::TextColor(_)))
            .expect("color present");
        assert_eq!(
            color.apply_if.as_slice(),
            &[
                DynamicSelector::Os(OsCondition::Linux),
                DynamicSelector::PseudoState(PseudoStateType::Hover),
            ]
        );
        assert!(!color.is_pseudo_state_only());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_moderately_deep_nesting_terminates() {
        // NOTE: `parse_block_segment` recurses once per nesting level with no depth cap,
        // so a pathologically nested stylesheet (~10k levels) would abort the process on
        // a stack overflow. Kept at a depth that is safe to run in-process; the missing
        // depth limit is reported separately.
        const DEPTH: usize = 50;
        let style = format!(
            "{}color: red;{}",
            ":hover {".repeat(DEPTH),
            "}".repeat(DEPTH)
        );
        let props = CssPropertyWithConditionsVec::parse(&style).into_library_owned_vec();
        assert_eq!(props.len(), 1);
        assert_eq!(props[0].apply_if.as_slice().len(), DEPTH);
        assert!(props[0].is_pseudo_state_only());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_unbalanced_braces_do_not_panic() {
        // brace_depth goes negative / never returns to zero; both paths must be inert.
        for s in [
            "color: red; }",
            "{ color: red;",
            ":hover { color: red;",
            ":hover }",
            &"{".repeat(1_000),
            &"}".repeat(1_000),
        ] {
            let _ = parse_len(s);
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_very_long_input_terminates() {
        let long = "color: red;".repeat(5_000);
        assert_eq!(parse_len(&long), 5_000);
        // A single enormous junk token must not blow up either.
        assert_eq!(parse_len(&"a".repeat(500_000)), 0);
        assert_eq!(parse_len(&format!("color: {};", "z".repeat(500_000))), 0);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_selector_to_conditions_covers_every_pseudo_class() {
        let cases = [
            ("hover", PseudoStateType::Hover),
            ("active", PseudoStateType::Active),
            ("focus", PseudoStateType::Focus),
            ("focus-within", PseudoStateType::FocusWithin),
            ("disabled", PseudoStateType::Disabled),
            ("checked", PseudoStateType::CheckedTrue),
            ("visited", PseudoStateType::Visited),
            ("backdrop", PseudoStateType::Backdrop),
            ("dragging", PseudoStateType::Dragging),
            ("drag-over", PseudoStateType::DragOver),
        ];
        for (name, expected) in cases {
            assert_eq!(
                CssPropertyWithConditionsVec::parse_selector_to_conditions(&format!(":{name}")),
                Some(vec![DynamicSelector::PseudoState(expected)]),
                ":{name} must parse"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_selector_to_conditions_wildcards_are_unconditional() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_selector_to_conditions("*"),
            Some(vec![])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_selector_to_conditions(""),
            Some(vec![])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_selector_to_conditions("   "),
            Some(vec![])
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_selector_to_conditions_rejects_unknown_selectors() {
        for s in [
            ":", ":hoverr", ":HOVER", "div", "#id", ".class", "@", "\u{1F600}", ":\u{1F600}",
        ] {
            assert_eq!(
                CssPropertyWithConditionsVec::parse_selector_to_conditions(s),
                None,
                "{s:?} must not parse"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_at_rule_theme_lang_and_accessibility() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("theme dark"),
            Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("theme light"),
            Some(vec![DynamicSelector::Theme(ThemeCondition::Light)])
        );
        assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("theme neon"), None);
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("lang(\"de-DE\")"),
            Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
                AzString::from_const_str("de-DE")
            ))])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("lang de"),
            Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
                AzString::from_const_str("de")
            ))])
        );
        assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang()"), None);
        assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang(\"\")"), None);
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("prefers-reduced-motion"),
            Some(vec![DynamicSelector::PrefersReducedMotion(
                BoolCondition::True
            )])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("high-contrast"),
            Some(vec![DynamicSelector::PrefersHighContrast(
                BoolCondition::True
            )])
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_at_rule_container_named_and_sized() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule("container sidebar"),
            Some(vec![DynamicSelector::ContainerName(
                AzString::from_const_str("sidebar")
            )])
        );
        let conds = CssPropertyWithConditionsVec::parse_at_rule("container (min-width: 400px)")
            .expect("sized container must parse");
        assert_eq!(conds.len(), 1);
        // Destructured rather than compared with `==`: `MinMaxRange`'s derived PartialEq
        // is not reflexive while its `max` is the NaN sentinel (see
        // `nan_sentinel_range_selector_is_reflexive_under_partial_eq`).
        match conds[0] {
            DynamicSelector::ContainerWidth(r) => {
                assert_eq!(r.min(), Some(400.0));
                assert_eq!(r.max(), None);
            }
            ref other => panic!("expected ContainerWidth, got {other:?}"),
        }
        let named = CssPropertyWithConditionsVec::parse_at_rule(
            "container sidebar (max-height: 200px)",
        )
        .expect("named + sized container must parse");
        assert_eq!(named.len(), 2);
        assert!(matches!(named[0], DynamicSelector::ContainerName(_)));
        assert!(matches!(named[1], DynamicSelector::ContainerHeight(_)));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_at_rule_empty_and_garbage_is_none() {
        for s in [
            "",
            " ",
            "os",
            "os ",
            "os()",
            "os(notanos)",
            "media",
            "media ",
            "media (min-width: abc)",
            "theme",
            "container",
            "container ()",
            "nosuchrule",
            "\u{1F600}",
        ] {
            assert_eq!(
                CssPropertyWithConditionsVec::parse_at_rule(s),
                None,
                "{s:?} must not parse"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_at_rule_huge_input_terminates() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule(&format!("os {}", "(".repeat(50_000))),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule(&"z".repeat(500_000)),
            None
        );
        // A million-char language tag is accepted verbatim (no hang, no truncation).
        let long_lang = "e".repeat(100_000);
        assert_eq!(
            CssPropertyWithConditionsVec::parse_at_rule(&format!("lang {long_lang}")),
            Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
                AzString::from(long_lang)
            ))])
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_query_media_types_and_dimensions() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query("screen"),
            Some(vec![DynamicSelector::Media(MediaType::Screen)])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query("print"),
            Some(vec![DynamicSelector::Media(MediaType::Print)])
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query("all"),
            Some(vec![DynamicSelector::Media(MediaType::All)])
        );
        let w = CssPropertyWithConditionsVec::parse_media_query("(min-width: 800px)")
            .expect("min-width must parse");
        assert_eq!(w.len(), 1);
        match w[0] {
            DynamicSelector::ViewportWidth(r) => {
                assert_eq!(r.min(), Some(800.0));
                assert_eq!(r.max(), None);
            }
            ref other => panic!("expected ViewportWidth, got {other:?}"),
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_query_boundary_pixel_values() {
        // Zero and negative pixel values are accepted verbatim by `f32::parse`.
        let zero = CssPropertyWithConditionsVec::parse_media_query("(min-width: 0px)")
            .expect("0px must parse");
        match zero[0] {
            DynamicSelector::ViewportWidth(r) => assert_eq!(r.min(), Some(0.0)),
            ref other => panic!("expected ViewportWidth, got {other:?}"),
        }
        let neg = CssPropertyWithConditionsVec::parse_media_query("(max-height: -1px)")
            .expect("-1px must parse");
        match neg[0] {
            DynamicSelector::ViewportHeight(r) => assert_eq!(r.max(), Some(-1.0)),
            ref other => panic!("expected ViewportHeight, got {other:?}"),
        }
        // Missing / wrong unit is rejected (falls through to the media-type match).
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query("(min-width: 800)"),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query("(min-width: 800em)"),
            None
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_query_empty_and_garbage_is_none() {
        for s in [
            "",
            " ",
            "(",
            ")",
            "()",
            "(:)",
            "(min-width)",
            "(min-width: )",
            "(nosuchfeature: 1px)",
            "SCREEN",
            "\u{1F600}",
            "(🦀: 1px)",
        ] {
            assert_eq!(
                CssPropertyWithConditionsVec::parse_media_query(s),
                None,
                "{s:?} must not parse"
            );
        }
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query(&"(".repeat(100_000)),
            None
        );
    }
    // RED (genuine bug, low severity): `value.parse::<f32>()` accepts "NaN"/"nan", and
    // `MinMaxRange` uses NaN as the "no limit" sentinel. So `(min-width: NaNpx)` — an
    // invalid media feature — silently becomes an *unconditional* viewport-width match
    // instead of being rejected. Per CSS, an unparseable feature value makes the query
    // invalid (never matches); it must certainly not make it always match.
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_query_nan_pixel_value_is_rejected() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_query("(min-width: NaNpx)"),
            None,
            "a NaN px value collapses into the 'no limit' sentinel and matches everything"
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_query_infinite_pixel_value_never_matches() {
        // "inf" also parses as f32 — unlike NaN it degrades safely (matches nothing),
        // so assert that rather than a rejection.
        let q = CssPropertyWithConditionsVec::parse_media_query("(min-width: infpx)")
            .expect("infpx currently parses");
        let ctx = DynamicSelectorContext::default().with_viewport(1e30, 1000.0);
        assert!(!q[0].matches(&ctx), "an infinite min-width can never be met");
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_feature_inline_known_features() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "PORTRAIT"),
            Some(DynamicSelector::Orientation(OrientationType::Portrait))
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(
                "prefers-color-scheme",
                "Dark"
            ),
            Some(DynamicSelector::Theme(ThemeCondition::Dark))
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(
                "prefers-reduced-motion",
                "reduce"
            ),
            Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(
                "prefers-reduced-motion",
                "no-preference"
            ),
            Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline("prefers-contrast", "more"),
            Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(
                "prefers-high-contrast",
                "none"
            ),
            Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_media_feature_inline_rejects_unknown_keys_and_values() {
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", ""),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "sideways"),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline("", ""),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline("nosuchkey", "dark"),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(
                "prefers-color-scheme",
                "\u{1F600}"
            ),
            None
        );
        // The key is *not* trimmed by this helper.
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(" orientation", "portrait"),
            None
        );
        assert_eq!(
            CssPropertyWithConditionsVec::parse_media_feature_inline(
                &"k".repeat(200_000),
                &"v".repeat(200_000)
            ),
            None
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_property_segment_valid_and_invalid() {
        let key_map = crate::props::property::CssKeyMap::get();
        let ok = CssPropertyWithConditionsVec::parse_property_segment("color: red", &[], &key_map)
            .expect("color: red must parse");
        assert_eq!(ok.len(), 1);
        assert!(!ok[0].is_conditional());
        // A shorthand expands into several properties, all sharing the conditions.
        let inherited = vec![DynamicSelector::PseudoState(PseudoStateType::Hover)];
        let shorthand = CssPropertyWithConditionsVec::parse_property_segment(
            "padding: 10px",
            &inherited,
            &key_map,
        )
        .expect("padding shorthand must parse");
        assert!(shorthand.len() > 1, "padding must expand to >1 property");
        for p in &shorthand {
            assert_eq!(p.apply_if.as_slice(), inherited.as_slice());
        }
        for s in ["", "   ", "color", "color:", ": red", "nosuchprop: red", "\u{1F600}"] {
            assert!(
                CssPropertyWithConditionsVec::parse_property_segment(s, &[], &key_map).is_none(),
                "{s:?} must not parse"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_block_segment_requires_balanced_braces() {
        let key_map = crate::props::property::CssKeyMap::get();
        // No brace at all.
        assert!(CssPropertyWithConditionsVec::parse_block_segment(
            "color: red",
            &[],
            &key_map
        )
        .is_none());
        // Opening brace, no closing brace.
        assert!(CssPropertyWithConditionsVec::parse_block_segment(
            ":hover { color: red",
            &[],
            &key_map
        )
        .is_none());
        // `}` before `{` -> content_end <= content_start -> None.
        assert!(
            CssPropertyWithConditionsVec::parse_block_segment("}{", &[], &key_map).is_none()
        );
        // Empty body is an empty (but valid) block.
        let empty = CssPropertyWithConditionsVec::parse_block_segment(
            ":hover {}",
            &[],
            &key_map,
        );
        // "{}" has content_end == content_start -> rejected by the guard.
        assert!(empty.is_none());
        // Valid block.
        let ok = CssPropertyWithConditionsVec::parse_block_segment(
            ":hover { color: red; }",
            &[],
            &key_map,
        )
        .expect("valid block");
        assert_eq!(ok.len(), 1);
        assert!(ok[0].is_pseudo_state_only());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_with_conditions_prepends_inherited_conditions() {
        let inherited = vec![DynamicSelector::Os(OsCondition::Linux)];
        let props =
            CssPropertyWithConditionsVec::parse_with_conditions("color: red;", &inherited)
                .into_library_owned_vec();
        assert_eq!(props.len(), 1);
        assert_eq!(props[0].apply_if.as_slice(), inherited.as_slice());
        // Empty input with inherited conditions still yields nothing.
        let none = CssPropertyWithConditionsVec::parse_with_conditions("   ", &inherited)
            .into_library_owned_vec();
        assert!(none.is_empty());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parsed_media_selector_evaluates_against_a_context() {
        // End-to-end: parse -> match. Guards against a parse that silently produces an
        // always-true or never-true condition.
        let props =
            CssPropertyWithConditionsVec::parse("@media (min-width: 800px) { color: red; }")
                .into_library_owned_vec();
        assert_eq!(props.len(), 1);
        let base = DynamicSelectorContext::default();
        assert!(props[0].matches(&base.with_viewport(1024.0, 768.0)));
        assert!(props[0].matches(&base.with_viewport(800.0, 600.0)));
        assert!(!props[0].matches(&base.with_viewport(799.0, 600.0)));
    }
}