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
#[repr(C)]
9
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
10
pub struct PseudoStateFlags {
11
    pub hover: bool,
12
    pub active: bool,
13
    pub focused: bool,
14
    pub disabled: bool,
15
    pub checked: bool,
16
    pub focus_within: bool,
17
    pub visited: bool,
18
    /// Window is not focused (equivalent to GTK :backdrop)
19
    pub backdrop: bool,
20
    /// Element is currently being dragged (:dragging)
21
    pub dragging: bool,
22
    /// A dragged element is over this drop target (:drag-over)
23
    pub drag_over: bool,
24
}
25

            
26
impl PseudoStateFlags {
27
    /// Check if a specific pseudo-state is active
28
    pub fn has_state(&self, state: PseudoStateType) -> bool {
29
        match state {
30
            PseudoStateType::Normal => true,
31
            PseudoStateType::Hover => self.hover,
32
            PseudoStateType::Active => self.active,
33
            PseudoStateType::Focus => self.focused,
34
            PseudoStateType::Disabled => self.disabled,
35
            PseudoStateType::CheckedTrue => self.checked,
36
            PseudoStateType::CheckedFalse => !self.checked,
37
            PseudoStateType::FocusWithin => self.focus_within,
38
            PseudoStateType::Visited => self.visited,
39
            PseudoStateType::Backdrop => self.backdrop,
40
            PseudoStateType::Dragging => self.dragging,
41
            PseudoStateType::DragOver => self.drag_over,
42
        }
43
    }
44
}
45

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

            
84
impl_option!(
85
    DynamicSelector,
86
    OptionDynamicSelector,
87
    copy = false,
88
    [Debug, Clone, PartialEq]
89
);
90

            
91
impl_vec!(DynamicSelector, DynamicSelectorVec, DynamicSelectorVecDestructor, DynamicSelectorVecDestructorType, DynamicSelectorVecSlice, OptionDynamicSelector);
92
impl_vec_clone!(
93
    DynamicSelector,
94
    DynamicSelectorVec,
95
    DynamicSelectorVecDestructor
96
);
97
impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
98
impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
99

            
100
/// Min/Max Range for numeric conditions (C-compatible)
101
#[repr(C)]
102
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
103
pub struct MinMaxRange {
104
    /// Minimum value (NaN = no minimum limit)
105
    pub min: f32,
106
    /// Maximum value (NaN = no maximum limit)
107
    pub max: f32,
108
}
109

            
110
impl MinMaxRange {
111
70
    pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
112
        Self {
113
70
            min: if let Some(m) = min { m } else { f32::NAN },
114
70
            max: if let Some(m) = max { m } else { f32::NAN },
115
        }
116
70
    }
117
    
118
    /// Create a range with only a minimum value (>= min)
119
    pub const fn with_min(min_val: f32) -> Self {
120
        Self {
121
            min: min_val,
122
            max: f32::NAN,
123
        }
124
    }
125
    
126
    /// Create a range with only a maximum value (<= max)
127
    pub const fn with_max(max_val: f32) -> Self {
128
        Self {
129
            min: f32::NAN,
130
            max: max_val,
131
        }
132
    }
133

            
134
5
    pub fn min(&self) -> Option<f32> {
135
5
        if self.min.is_nan() {
136
2
            None
137
        } else {
138
3
            Some(self.min)
139
        }
140
5
    }
141

            
142
4
    pub fn max(&self) -> Option<f32> {
143
4
        if self.max.is_nan() {
144
2
            None
145
        } else {
146
2
            Some(self.max)
147
        }
148
4
    }
149

            
150
98
    pub fn matches(&self, value: f32) -> bool {
151
98
        let min_ok = self.min.is_nan() || value >= self.min;
152
98
        let max_ok = self.max.is_nan() || value <= self.max;
153
98
        min_ok && max_ok
154
98
    }
155
}
156

            
157
/// Boolean condition (C-compatible)
158
#[repr(C)]
159
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
160
pub enum BoolCondition {
161
    #[default]
162
    False,
163
    True,
164
}
165

            
166
impl From<bool> for BoolCondition {
167
    fn from(b: bool) -> Self {
168
        if b {
169
            Self::True
170
        } else {
171
            Self::False
172
        }
173
    }
174
}
175

            
176
impl From<BoolCondition> for bool {
177
    fn from(b: BoolCondition) -> Self {
178
        matches!(b, BoolCondition::True)
179
    }
180
}
181

            
182
/// Operating system condition for `@os` CSS selectors
183
#[repr(C)]
184
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185
pub enum OsCondition {
186
    Any,
187
    Apple, // macOS + iOS
188
    MacOS,
189
    IOS,
190
    Linux,
191
    Windows,
192
    Android,
193
    Web, // WASM
194
}
195

            
196
impl_option!(
197
    OsCondition,
198
    OptionOsCondition,
199
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
200
);
201

            
202
impl OsCondition {
203
    /// Convert from css::system::Platform
204
    pub fn from_system_platform(platform: &crate::system::Platform) -> Self {
205
        use crate::system::Platform;
206
        match platform {
207
            Platform::Windows => OsCondition::Windows,
208
            Platform::MacOs => OsCondition::MacOS,
209
            Platform::Linux(_) => OsCondition::Linux,
210
            Platform::Android => OsCondition::Android,
211
            Platform::Ios => OsCondition::IOS,
212
            Platform::Unknown => OsCondition::Any,
213
        }
214
    }
215
}
216

            
217
#[repr(C, u8)]
218
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
219
pub enum OsVersionCondition {
220
    /// Minimum version: >= specified version
221
    /// Format: OsVersion { os, version_id }
222
    Min(OsVersion),
223
    /// Maximum version: <= specified version
224
    Max(OsVersion),
225
    /// Exact version match
226
    Exact(OsVersion),
227
    /// Desktop environment (Linux only)
228
    DesktopEnvironment(LinuxDesktopEnv),
229
    /// Desktop environment with min version (e.g. `@os(linux:gnome > 40)`)
230
    DesktopEnvMin(DesktopEnvVersion),
231
    /// Desktop environment with max version
232
    DesktopEnvMax(DesktopEnvVersion),
233
    /// Desktop environment with exact version
234
    DesktopEnvExact(DesktopEnvVersion),
235
}
236

            
237
/// A desktop environment together with a numeric version (e.g. GNOME 40).
238
/// Used by `OsVersionCondition::DesktopEnv{Min,Max,Exact}` for `@os(linux:gnome > 40)` style selectors.
239
#[repr(C)]
240
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
241
pub struct DesktopEnvVersion {
242
    pub env: LinuxDesktopEnv,
243
    pub version_id: u32,
244
}
245

            
246
/// OS version with ordering - only comparable within the same OS family
247
/// 
248
/// Each OS has its own version numbering system with named versions.
249
/// Comparisons between different OS families always return false.
250
#[repr(C)]
251
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252
pub struct OsVersion {
253
    /// Which OS family this version belongs to
254
    pub os: OsFamily,
255
    /// Numeric version ID for ordering (higher = newer)
256
    /// Each OS has its own numbering scheme starting from 0
257
    pub version_id: u32,
258
}
259

            
260
impl Default for OsVersion {
261
42
    fn default() -> Self {
262
42
        Self::unknown()
263
42
    }
264
}
265

            
266
impl OsVersion {
267
14
    pub const fn new(os: OsFamily, version_id: u32) -> Self {
268
14
        Self { os, version_id }
269
14
    }
270
    
271
    /// Compare two versions - only meaningful within the same OS family
272
    /// Returns None if OS families don't match (comparison not meaningful)
273
    pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
274
        if self.os != other.os {
275
            None // Cross-OS comparison not meaningful
276
        } else {
277
            Some(self.version_id.cmp(&other.version_id))
278
        }
279
    }
280
    
281
    /// Check if self >= other (for Min conditions)
282
    pub fn is_at_least(&self, other: &Self) -> bool {
283
        self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Less)
284
    }
285
    
286
    /// Check if self <= other (for Max conditions)
287
    pub fn is_at_most(&self, other: &Self) -> bool {
288
        self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Greater)
289
    }
290
}
291

            
292
impl_option!(
293
    OsVersion,
294
    OptionOsVersion,
295
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
296
);
297

            
298
impl OsVersion {
299
    
300
    /// Check if self == other
301
    pub fn is_exactly(&self, other: &Self) -> bool {
302
        self.compare(other) == Some(core::cmp::Ordering::Equal)
303
    }
304
}
305

            
306
/// OS family for version comparisons
307
#[repr(C)]
308
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
309
pub enum OsFamily {
310
    Windows,
311
    MacOS,
312
    IOS,
313
    Linux,
314
    Android,
315
}
316

            
317
// ============================================================================
318
// Windows Version IDs (chronological order)
319
// ============================================================================
320

            
321
/// Windows version constants - use these in CSS like `@os(windows >= win-xp)`
322
impl OsVersion {
323
    // Windows versions (version_id = NT version * 100 + minor)
324
    pub const WIN_2000: Self = Self::new(OsFamily::Windows, 500);       // NT 5.0
325
    pub const WIN_XP: Self = Self::new(OsFamily::Windows, 501);         // NT 5.1
326
    pub const WIN_XP_64: Self = Self::new(OsFamily::Windows, 502);      // NT 5.2
327
    pub const WIN_VISTA: Self = Self::new(OsFamily::Windows, 600);      // NT 6.0
328
    pub const WIN_7: Self = Self::new(OsFamily::Windows, 601);          // NT 6.1
329
    pub const WIN_8: Self = Self::new(OsFamily::Windows, 602);          // NT 6.2
330
    pub const WIN_8_1: Self = Self::new(OsFamily::Windows, 603);        // NT 6.3
331
    pub const WIN_10: Self = Self::new(OsFamily::Windows, 1000);        // NT 10.0
332
    pub const WIN_10_1507: Self = Self::new(OsFamily::Windows, 1000);   // Initial release
333
    pub const WIN_10_1511: Self = Self::new(OsFamily::Windows, 1001);   // November Update
334
    pub const WIN_10_1607: Self = Self::new(OsFamily::Windows, 1002);   // Anniversary Update
335
    pub const WIN_10_1703: Self = Self::new(OsFamily::Windows, 1003);   // Creators Update
336
    pub const WIN_10_1709: Self = Self::new(OsFamily::Windows, 1004);   // Fall Creators Update
337
    pub const WIN_10_1803: Self = Self::new(OsFamily::Windows, 1005);   // April 2018 Update
338
    pub const WIN_10_1809: Self = Self::new(OsFamily::Windows, 1006);   // October 2018 Update
339
    pub const WIN_10_1903: Self = Self::new(OsFamily::Windows, 1007);   // May 2019 Update
340
    pub const WIN_10_1909: Self = Self::new(OsFamily::Windows, 1008);   // November 2019 Update
341
    pub const WIN_10_2004: Self = Self::new(OsFamily::Windows, 1009);   // May 2020 Update
342
    pub const WIN_10_20H2: Self = Self::new(OsFamily::Windows, 1010);   // October 2020 Update
343
    pub const WIN_10_21H1: Self = Self::new(OsFamily::Windows, 1011);   // May 2021 Update
344
    pub const WIN_10_21H2: Self = Self::new(OsFamily::Windows, 1012);   // November 2021 Update
345
    pub const WIN_10_22H2: Self = Self::new(OsFamily::Windows, 1013);   // 2022 Update
346
    pub const WIN_11: Self = Self::new(OsFamily::Windows, 1100);        // Windows 11 base
347
    pub const WIN_11_21H2: Self = Self::new(OsFamily::Windows, 1100);   // Initial release
348
    pub const WIN_11_22H2: Self = Self::new(OsFamily::Windows, 1101);   // 2022 Update
349
    pub const WIN_11_23H2: Self = Self::new(OsFamily::Windows, 1102);   // 2023 Update
350
    pub const WIN_11_24H2: Self = Self::new(OsFamily::Windows, 1103);   // 2024 Update
351
    
352
    // macOS versions (version_id = major * 100 + minor)
353
    pub const MACOS_CHEETAH: Self = Self::new(OsFamily::MacOS, 1000);       // 10.0
354
    pub const MACOS_PUMA: Self = Self::new(OsFamily::MacOS, 1001);          // 10.1
355
    pub const MACOS_JAGUAR: Self = Self::new(OsFamily::MacOS, 1002);        // 10.2
356
    pub const MACOS_PANTHER: Self = Self::new(OsFamily::MacOS, 1003);       // 10.3
357
    pub const MACOS_TIGER: Self = Self::new(OsFamily::MacOS, 1004);         // 10.4
358
    pub const MACOS_LEOPARD: Self = Self::new(OsFamily::MacOS, 1005);       // 10.5
359
    pub const MACOS_SNOW_LEOPARD: Self = Self::new(OsFamily::MacOS, 1006);  // 10.6
360
    pub const MACOS_LION: Self = Self::new(OsFamily::MacOS, 1007);          // 10.7
361
    pub const MACOS_MOUNTAIN_LION: Self = Self::new(OsFamily::MacOS, 1008); // 10.8
362
    pub const MACOS_MAVERICKS: Self = Self::new(OsFamily::MacOS, 1009);     // 10.9
363
    pub const MACOS_YOSEMITE: Self = Self::new(OsFamily::MacOS, 1010);      // 10.10
364
    pub const MACOS_EL_CAPITAN: Self = Self::new(OsFamily::MacOS, 1011);    // 10.11
365
    pub const MACOS_SIERRA: Self = Self::new(OsFamily::MacOS, 1012);        // 10.12
366
    pub const MACOS_HIGH_SIERRA: Self = Self::new(OsFamily::MacOS, 1013);   // 10.13
367
    pub const MACOS_MOJAVE: Self = Self::new(OsFamily::MacOS, 1014);        // 10.14
368
    pub const MACOS_CATALINA: Self = Self::new(OsFamily::MacOS, 1015);      // 10.15
369
    pub const MACOS_BIG_SUR: Self = Self::new(OsFamily::MacOS, 1100);       // 11.0
370
    pub const MACOS_MONTEREY: Self = Self::new(OsFamily::MacOS, 1200);      // 12.0
371
    pub const MACOS_VENTURA: Self = Self::new(OsFamily::MacOS, 1300);       // 13.0
372
    pub const MACOS_SONOMA: Self = Self::new(OsFamily::MacOS, 1400);        // 14.0
373
    pub const MACOS_SEQUOIA: Self = Self::new(OsFamily::MacOS, 1500);       // 15.0
374
    pub const MACOS_TAHOE: Self = Self::new(OsFamily::MacOS, 2600);         // 26.0
375
    
376
    // iOS versions (version_id = major * 100 + minor)
377
    pub const IOS_1: Self = Self::new(OsFamily::IOS, 100);
378
    pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
379
    pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
380
    pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
381
    pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
382
    pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
383
    pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
384
    pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
385
    pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
386
    pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
387
    pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
388
    pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
389
    pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
390
    pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
391
    pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
392
    pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
393
    pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
394
    pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
395
    
396
    // Android versions (API level as version_id)
397
    pub const ANDROID_CUPCAKE: Self = Self::new(OsFamily::Android, 3);      // 1.5
398
    pub const ANDROID_DONUT: Self = Self::new(OsFamily::Android, 4);        // 1.6
399
    pub const ANDROID_ECLAIR: Self = Self::new(OsFamily::Android, 7);       // 2.1
400
    pub const ANDROID_FROYO: Self = Self::new(OsFamily::Android, 8);        // 2.2
401
    pub const ANDROID_GINGERBREAD: Self = Self::new(OsFamily::Android, 10); // 2.3
402
    pub const ANDROID_HONEYCOMB: Self = Self::new(OsFamily::Android, 13);   // 3.2
403
    pub const ANDROID_ICE_CREAM_SANDWICH: Self = Self::new(OsFamily::Android, 15); // 4.0
404
    pub const ANDROID_JELLY_BEAN: Self = Self::new(OsFamily::Android, 18);  // 4.3
405
    pub const ANDROID_KITKAT: Self = Self::new(OsFamily::Android, 19);      // 4.4
406
    pub const ANDROID_LOLLIPOP: Self = Self::new(OsFamily::Android, 22);    // 5.1
407
    pub const ANDROID_MARSHMALLOW: Self = Self::new(OsFamily::Android, 23); // 6.0
408
    pub const ANDROID_NOUGAT: Self = Self::new(OsFamily::Android, 25);      // 7.1
409
    pub const ANDROID_OREO: Self = Self::new(OsFamily::Android, 27);        // 8.1
410
    pub const ANDROID_PIE: Self = Self::new(OsFamily::Android, 28);         // 9.0
411
    pub const ANDROID_10: Self = Self::new(OsFamily::Android, 29);          // 10
412
    pub const ANDROID_11: Self = Self::new(OsFamily::Android, 30);          // 11
413
    pub const ANDROID_12: Self = Self::new(OsFamily::Android, 31);          // 12
414
    pub const ANDROID_12L: Self = Self::new(OsFamily::Android, 32);         // 12L
415
    pub const ANDROID_13: Self = Self::new(OsFamily::Android, 33);          // 13
416
    pub const ANDROID_14: Self = Self::new(OsFamily::Android, 34);          // 14
417
    pub const ANDROID_15: Self = Self::new(OsFamily::Android, 35);          // 15
418
    
419
    // Linux kernel versions (major * 1000 + minor * 10 + patch)
420
    pub const LINUX_2_6: Self = Self::new(OsFamily::Linux, 2060);
421
    pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
422
    pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
423
    pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
424
    pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
425
    
426
    /// Unknown OS version (for when detection fails or OS is unknown)
427
32536
    pub const fn unknown() -> Self {
428
32536
        Self {
429
32536
            os: OsFamily::Linux, // Fallback, but version_id 0 means "unknown"
430
32536
            version_id: 0,
431
32536
        }
432
32536
    }
433
}
434

            
435
/// Parse a named or numeric OS version string
436
/// Returns None if the version string is not recognized
437
70
pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
438
70
    let version_str = version_str.trim().to_lowercase();
439
70
    let version_str = version_str.as_str();
440
    
441
70
    match os {
442
42
        OsFamily::Windows => parse_windows_version(version_str),
443
14
        OsFamily::MacOS => parse_macos_version(version_str),
444
        OsFamily::IOS => parse_ios_version(version_str),
445
        OsFamily::Android => parse_android_version(version_str),
446
14
        OsFamily::Linux => parse_linux_version(version_str),
447
    }
448
70
}
449

            
450
42
fn parse_windows_version(s: &str) -> Option<OsVersion> {
451
    // Strip optional "win"/"windows" prefix (allowing -, _ separators).
452
    // This collapses "11", "win11", "win-11", "windows11", "windows-11", "windows_11" to "11".
453
42
    let core = strip_os_prefix(s, &["windows", "win"]);
454
42
    match core {
455
42
        "2000" => Some(OsVersion::WIN_2000),
456
42
        "xp" => Some(OsVersion::WIN_XP),
457
42
        "vista" => Some(OsVersion::WIN_VISTA),
458
42
        "7" => Some(OsVersion::WIN_7),
459
42
        "8" => Some(OsVersion::WIN_8),
460
42
        "8.1" | "8-1" => Some(OsVersion::WIN_8_1),
461
42
        "10" => Some(OsVersion::WIN_10),
462
42
        "11" => Some(OsVersion::WIN_11),
463
        // Numeric NT versions
464
        "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
465
        "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
466
        "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
467
        "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
468
        "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
469
        "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
470
        "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
471
        _ => None,
472
    }
473
42
}
474

            
475
/// If `s` starts with any of the given prefixes, strip the prefix plus an optional
476
/// trailing `-` or `_` separator. Otherwise return `s` unchanged. Matching is
477
/// case-insensitive (callers already lowercase, this just makes the helper safe).
478
56
fn strip_os_prefix<'a>(s: &'a str, prefixes: &[&str]) -> &'a str {
479
119
    for p in prefixes {
480
91
        if let Some(rest) = s.strip_prefix(p) {
481
28
            return rest.strip_prefix(['-', '_']).unwrap_or(rest);
482
63
        }
483
    }
484
28
    s
485
56
}
486

            
487
14
fn parse_macos_version(s: &str) -> Option<OsVersion> {
488
14
    match s {
489
14
        "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
490
14
        "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
491
14
        "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
492
14
        "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
493
14
        "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
494
14
        "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
495
14
        "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
496
14
        "lion" | "10.7" => Some(OsVersion::MACOS_LION),
497
14
        "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
498
14
        "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
499
14
        "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
500
14
        "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
501
14
        "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
502
14
        "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
503
14
        "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
504
14
        "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
505
14
        "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
506
14
        "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
507
14
        "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
508
14
        "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
509
        "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
510
        "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
511
        _ => None,
512
    }
513
14
}
514

            
515
fn parse_ios_version(s: &str) -> Option<OsVersion> {
516
    match s {
517
        "1" | "1.0" => Some(OsVersion::IOS_1),
518
        "2" | "2.0" => Some(OsVersion::IOS_2),
519
        "3" | "3.0" => Some(OsVersion::IOS_3),
520
        "4" | "4.0" => Some(OsVersion::IOS_4),
521
        "5" | "5.0" => Some(OsVersion::IOS_5),
522
        "6" | "6.0" => Some(OsVersion::IOS_6),
523
        "7" | "7.0" => Some(OsVersion::IOS_7),
524
        "8" | "8.0" => Some(OsVersion::IOS_8),
525
        "9" | "9.0" => Some(OsVersion::IOS_9),
526
        "10" | "10.0" => Some(OsVersion::IOS_10),
527
        "11" | "11.0" => Some(OsVersion::IOS_11),
528
        "12" | "12.0" => Some(OsVersion::IOS_12),
529
        "13" | "13.0" => Some(OsVersion::IOS_13),
530
        "14" | "14.0" => Some(OsVersion::IOS_14),
531
        "15" | "15.0" => Some(OsVersion::IOS_15),
532
        "16" | "16.0" => Some(OsVersion::IOS_16),
533
        "17" | "17.0" => Some(OsVersion::IOS_17),
534
        "18" | "18.0" => Some(OsVersion::IOS_18),
535
        _ => None,
536
    }
537
}
538

            
539
fn parse_android_version(s: &str) -> Option<OsVersion> {
540
    match s {
541
        "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
542
        "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
543
        "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
544
        "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
545
        "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
546
        "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
547
        "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
548
        "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
549
        "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
550
        "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
551
        "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
552
        "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
553
        "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
554
        "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
555
        "10" | "q" => Some(OsVersion::ANDROID_10),
556
        "11" | "r" => Some(OsVersion::ANDROID_11),
557
        "12" | "s" => Some(OsVersion::ANDROID_12),
558
        "12l" | "12L" => Some(OsVersion::ANDROID_12L),
559
        "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
560
        "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
561
        "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
562
        _ => {
563
            // Try parsing as API level
564
            if let Some(api) = s.strip_prefix("api") {
565
                if let Ok(level) = api.trim().parse::<u32>() {
566
                    return Some(OsVersion::new(OsFamily::Android, level));
567
                }
568
            }
569
            None
570
        }
571
    }
572
}
573

            
574
14
fn parse_linux_version(s: &str) -> Option<OsVersion> {
575
    // Strip optional "linux" prefix so "linux6.1" / "linux-6.1" also work.
576
14
    let s = strip_os_prefix(s, &["linux"]);
577
    // Parse kernel version like "5.4", "6.0", or bare major like "5" (== "5.0").
578
14
    let mut parts = s.split('.');
579
14
    let major = parts.next()?.parse::<u32>().ok()?;
580
14
    let minor = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
581
14
    let patch = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
582
14
    Some(OsVersion::new(OsFamily::Linux, major * 1000 + minor * 10 + patch))
583
14
}
584

            
585
/// Linux desktop environment for `@os(linux:<de>)` CSS selectors.
586
///
587
/// Note: `from_system_desktop_env` currently only maps Gnome, KDE, and Other.
588
/// XFCE, Unity, Cinnamon, and MATE can be matched via CSS parsing (`@os(linux:xfce)`)
589
/// but will not be auto-detected from the system — they map to `Other` at runtime.
590
#[repr(C)]
591
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
592
pub enum LinuxDesktopEnv {
593
    Gnome,
594
    KDE,
595
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
596
    XFCE,
597
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
598
    Unity,
599
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
600
    Cinnamon,
601
    /// CSS-parse-only: not auto-detected from system (maps to `Other` at runtime)
602
    MATE,
603
    Other,
604
}
605

            
606
impl LinuxDesktopEnv {
607
    /// Convert from css::system::DesktopEnvironment
608
    pub fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
609
        use crate::system::DesktopEnvironment;
610
        match de {
611
            DesktopEnvironment::Gnome => LinuxDesktopEnv::Gnome,
612
            DesktopEnvironment::Kde => LinuxDesktopEnv::KDE,
613
            DesktopEnvironment::Other(_) => LinuxDesktopEnv::Other,
614
        }
615
    }
616
}
617

            
618
/// Media type for `@media` CSS selectors (screen, print, all)
619
#[repr(C)]
620
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
621
pub enum MediaType {
622
    Screen,
623
    Print,
624
    All,
625
}
626

            
627
#[repr(C, u8)]
628
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
629
pub enum ThemeCondition {
630
    Light,
631
    Dark,
632
    Custom(AzString),
633
    /// System preference
634
    SystemPreferred,
635
}
636

            
637
impl_option!(
638
    ThemeCondition,
639
    OptionThemeCondition,
640
    copy = false,
641
    [Debug, Clone, PartialEq, Eq, Hash]
642
);
643

            
644
impl ThemeCondition {
645
    /// Convert from css::system::Theme
646
    pub fn from_system_theme(theme: crate::system::Theme) -> Self {
647
        use crate::system::Theme;
648
        match theme {
649
            Theme::Light => ThemeCondition::Light,
650
            Theme::Dark => ThemeCondition::Dark,
651
        }
652
    }
653
}
654

            
655
/// Orientation type for `@media (orientation: ...)` CSS selectors
656
#[repr(C)]
657
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
658
pub enum OrientationType {
659
    Portrait,
660
    Landscape,
661
}
662

            
663
/// Language/Locale condition for @lang() CSS selector
664
/// Matches BCP 47 language tags with prefix matching
665
#[repr(C, u8)]
666
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
667
pub enum LanguageCondition {
668
    /// Exact match (e.g., "de-DE" matches only "de-DE")
669
    Exact(AzString),
670
    /// Prefix match (e.g., "de" matches "de", "de-DE", "de-AT", etc.)
671
    Prefix(AzString),
672
}
673

            
674
impl LanguageCondition {
675
    /// Check if this condition matches the given language tag
676
126
    pub fn matches(&self, language: &str) -> bool {
677
126
        match self {
678
35
            LanguageCondition::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
679
91
            LanguageCondition::Prefix(prefix) => {
680
91
                let prefix_str = prefix.as_str();
681
91
                if language.len() < prefix_str.len() {
682
14
                    return false;
683
77
                }
684
                // Check if language starts with prefix (case-insensitive)
685
77
                let lang_prefix = &language[..prefix_str.len()];
686
77
                if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
687
21
                    return false;
688
56
                }
689
                // Must be exact match or followed by '-'
690
56
                language.len() == prefix_str.len()
691
42
                    || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
692
            }
693
        }
694
126
    }
695
}
696

            
697
#[repr(C)]
698
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
699
pub enum PseudoStateType {
700
    /// No special state (corresponds to "Normal" in NodeDataInlineCssProperty)
701
    Normal,
702
    /// Element is being hovered (:hover)
703
    Hover,
704
    /// Element is active/being clicked (:active)
705
    Active,
706
    /// Element has focus (:focus)
707
    Focus,
708
    /// Element is disabled (:disabled)
709
    Disabled,
710
    /// Element is checked/selected (:checked)
711
    CheckedTrue,
712
    /// Element is unchecked (:not(:checked))
713
    CheckedFalse,
714
    /// Element or child has focus (:focus-within)
715
    FocusWithin,
716
    /// Link has been visited (:visited)
717
    Visited,
718
    /// Window is not focused (:backdrop) - GTK compatibility
719
    Backdrop,
720
    /// Element is currently being dragged (:dragging)
721
    Dragging,
722
    /// A dragged element is over this drop target (:drag-over)
723
    DragOver,
724
}
725

            
726
impl_option!(
727
    LinuxDesktopEnv,
728
    OptionLinuxDesktopEnv,
729
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
730
);
731

            
732
/// Default viewport width used when actual window size is not yet known.
733
pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
734
/// Default viewport height used when actual window size is not yet known.
735
pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
736

            
737
/// Context for evaluating dynamic selectors
738
#[repr(C)]
739
#[derive(Debug, Clone)]
740
pub struct DynamicSelectorContext {
741
    /// Operating system info
742
    pub os: OsCondition,
743
    pub os_version: OsVersion,
744
    pub desktop_env: OptionLinuxDesktopEnv,
745
    /// Numeric version of the active desktop environment (0 = unknown).
746
    /// Used by `@os(linux:gnome > 40)` style selectors. A value of 0 never
747
    /// satisfies any DE-version constraint, so detection can be wired up
748
    /// later without breaking parsed rules.
749
    pub de_version: u32,
750

            
751
    /// Theme info
752
    pub theme: ThemeCondition,
753

            
754
    /// Media info (from WindowState)
755
    pub media_type: MediaType,
756
    pub viewport_width: f32,
757
    pub viewport_height: f32,
758

            
759
    /// Container info (from parent node)
760
    /// NaN = no container
761
    pub container_width: f32,
762
    pub container_height: f32,
763
    pub container_name: OptionString,
764

            
765
    /// Accessibility preferences
766
    pub prefers_reduced_motion: BoolCondition,
767
    pub prefers_high_contrast: BoolCondition,
768

            
769
    /// Orientation
770
    pub orientation: OrientationType,
771

            
772
    /// Node state (hover, active, focus, disabled, checked, focus_within, visited)
773
    pub pseudo_state: PseudoStateFlags,
774

            
775
    /// Language/Locale (BCP 47 tag, e.g., "en-US", "de-DE")
776
    pub language: AzString,
777

            
778
    /// Whether the window currently has focus (for :backdrop pseudo-class)
779
    /// When false, :backdrop styles should be applied
780
    pub window_focused: bool,
781
}
782

            
783
impl Default for DynamicSelectorContext {
784
27030
    fn default() -> Self {
785
27030
        Self {
786
27030
            os: OsCondition::Any,
787
27030
            os_version: OsVersion::unknown(),
788
27030
            desktop_env: OptionLinuxDesktopEnv::None,
789
27030
            de_version: 0,
790
27030
            theme: ThemeCondition::Light,
791
27030
            media_type: MediaType::Screen,
792
27030
            viewport_width: DEFAULT_VIEWPORT_WIDTH,
793
27030
            viewport_height: DEFAULT_VIEWPORT_HEIGHT,
794
27030
            container_width: f32::NAN,
795
27030
            container_height: f32::NAN,
796
27030
            container_name: OptionString::None,
797
27030
            prefers_reduced_motion: BoolCondition::False,
798
27030
            prefers_high_contrast: BoolCondition::False,
799
27030
            orientation: OrientationType::Landscape,
800
27030
            pseudo_state: PseudoStateFlags::default(),
801
27030
            language: AzString::from_const_str("en-US"),
802
27030
            window_focused: true,
803
27030
        }
804
27030
    }
805
}
806

            
807
impl DynamicSelectorContext {
808
    /// Create a context from SystemStyle
809
    pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
810
        let os = OsCondition::from_system_platform(&system_style.platform);
811
        let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
812
            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
813
        } else {
814
            OptionLinuxDesktopEnv::None
815
        };
816
        let theme = ThemeCondition::from_system_theme(system_style.theme);
817

            
818
        Self {
819
            os,
820
            os_version: system_style.os_version, // Use version from SystemStyle
821
            desktop_env,
822
            de_version: 0, // TODO: wire up DE version detection in system::detect_*
823
            theme,
824
            media_type: MediaType::Screen,
825
            viewport_width: DEFAULT_VIEWPORT_WIDTH, // Will be updated with window size
826
            viewport_height: DEFAULT_VIEWPORT_HEIGHT,
827
            container_width: f32::NAN,
828
            container_height: f32::NAN,
829
            container_name: OptionString::None,
830
            prefers_reduced_motion: system_style.prefers_reduced_motion,
831
            prefers_high_contrast: system_style.prefers_high_contrast,
832
            orientation: OrientationType::Landscape,
833
            pseudo_state: PseudoStateFlags::default(),
834
            language: system_style.language.clone(),
835
            window_focused: true,
836
        }
837
    }
838

            
839
    /// Update viewport dimensions (e.g., on window resize)
840
    pub fn with_viewport(&self, width: f32, height: f32) -> Self {
841
        let mut ctx = self.clone();
842
        ctx.viewport_width = width;
843
        ctx.viewport_height = height;
844
        ctx.orientation = if width > height {
845
            OrientationType::Landscape
846
        } else {
847
            OrientationType::Portrait
848
        };
849
        ctx
850
    }
851

            
852
    /// Update container dimensions (for @container queries)
853
    pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
854
        let mut ctx = self.clone();
855
        ctx.container_width = width;
856
        ctx.container_height = height;
857
        ctx.container_name = name.into();
858
        ctx
859
    }
860

            
861
    /// Update pseudo-state (hover, active, focus, etc.)
862
    pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
863
        let mut ctx = self.clone();
864
        ctx.pseudo_state = state;
865
        ctx
866
    }
867

            
868
    /// Check if viewport changed significantly (for breakpoint detection)
869
    pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
870
        for bp in breakpoints {
871
            let self_above = self.viewport_width >= *bp;
872
            let other_above = other.viewport_width >= *bp;
873
            if self_above != other_above {
874
                return true;
875
            }
876
        }
877
        false
878
    }
879
}
880

            
881
impl DynamicSelector {
882
    /// Check if this selector matches in the given context
883
681037
    pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
884
681037
        match self {
885
648564
            Self::Os(os) => Self::match_os(*os, ctx.os),
886
28
            Self::OsVersion(ver) => Self::match_os_version(ver, &ctx.os_version, &ctx.desktop_env, ctx.de_version),
887
7
            Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
888
            Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
889
            Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
890
            Self::ContainerWidth(range) => {
891
                !ctx.container_width.is_nan() && range.matches(ctx.container_width)
892
            }
893
            Self::ContainerHeight(range) => {
894
                !ctx.container_height.is_nan() && range.matches(ctx.container_height)
895
            }
896
            Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
897
32438
            Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
898
            Self::AspectRatio(range) => {
899
                let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
900
                range.matches(ratio)
901
            }
902
            Self::Orientation(orient) => *orient == ctx.orientation,
903
            Self::PrefersReducedMotion(pref) => {
904
                bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
905
            }
906
            Self::PrefersHighContrast(pref) => {
907
                bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
908
            }
909
            Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
910
            Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
911
        }
912
681037
    }
913

            
914
648564
    fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
915
648564
        match condition {
916
21
            OsCondition::Any => true,
917
21
            OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
918
648522
            _ => condition == actual,
919
        }
920
648564
    }
921

            
922
28
    fn match_os_version(
923
28
        condition: &OsVersionCondition,
924
28
        actual: &OsVersion,
925
28
        desktop_env: &OptionLinuxDesktopEnv,
926
28
        de_version: u32,
927
28
    ) -> bool {
928
        // de_version == 0 means the runtime hasn't reported a version,
929
        // so any DE-version constraint fails until detection is wired up.
930
28
        let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
931
28
        match condition {
932
            OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
933
            OsVersionCondition::Min(ver) => actual.is_at_least(ver),
934
            OsVersionCondition::Max(ver) => actual.is_at_most(ver),
935
            OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
936
28
            OsVersionCondition::DesktopEnvMin(d) =>
937
28
                de_matches(&d.env) && de_version != 0 && de_version >= d.version_id,
938
            OsVersionCondition::DesktopEnvMax(d) =>
939
                de_matches(&d.env) && de_version != 0 && de_version <= d.version_id,
940
            OsVersionCondition::DesktopEnvExact(d) =>
941
                de_matches(&d.env) && de_version == d.version_id,
942
        }
943
28
    }
944

            
945
32438
    fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
946
32438
        match (condition, actual) {
947
            (ThemeCondition::SystemPreferred, _) => true,
948
32438
            _ => condition == actual,
949
        }
950
32438
    }
951

            
952
    fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
953
        let node_state = &ctx.pseudo_state;
954
        match state {
955
            PseudoStateType::Normal => true, // Normal is always active (base state)
956
            PseudoStateType::Hover => node_state.hover,
957
            PseudoStateType::Active => node_state.active,
958
            PseudoStateType::Focus => node_state.focused,
959
            PseudoStateType::Disabled => node_state.disabled,
960
            PseudoStateType::CheckedTrue => node_state.checked,
961
            PseudoStateType::CheckedFalse => !node_state.checked,
962
            PseudoStateType::FocusWithin => node_state.focus_within,
963
            PseudoStateType::Visited => node_state.visited,
964
            PseudoStateType::Backdrop => node_state.backdrop,
965
            PseudoStateType::Dragging => node_state.dragging,
966
            PseudoStateType::DragOver => node_state.drag_over,
967
        }
968
    }
969
}
970

            
971
/// Parse the content of an `@os(...)` at-rule into a list of dynamic-selector conditions.
972
///
973
/// Accepts both bare-identifier and parenthesized forms:
974
///
975
/// - `linux`                       → `[Os(Linux)]`
976
/// - `(linux)`                     → `[Os(Linux)]`
977
/// - `(linux:gnome)`               → `[Os(Linux), OsVersion(DesktopEnvironment(Gnome))]`
978
/// - `(windows >= win-11)`         → `[Os(Windows), OsVersion(Min(WIN_11))]`
979
/// - `(linux:gnome > 40)`          → `[Os(Linux), OsVersion(DesktopEnvMin{ env: Gnome, version_id: 40 })]`
980
/// - `(any)` / `(*)` / `(all)`     → `[]` (always-match, no conditions emitted)
981
///
982
/// Returns `None` only when the content is a parse error.
983
/// `Some(vec![])` means "always match" (the rule applies unconditionally).
984
#[cfg(feature = "parser")]
985
203
pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
986
203
    let trimmed = content.trim();
987
203
    let inner = trimmed
988
203
        .strip_prefix('(').and_then(|s| s.strip_suffix(')'))
989
203
        .unwrap_or(trimmed)
990
203
        .trim();
991
203
    let inner = inner
992
203
        .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
993
203
        .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
994
203
        .unwrap_or(inner)
995
203
        .trim();
996
203
    if inner.is_empty() {
997
        return None;
998
203
    }
999

            
    // Split off the operator + version, if any.
203
    let (subject, op_and_version) = split_op_and_version(inner);
203
    let subject = subject.trim();
    // subject is "family" or "family:de"
203
    let (family_str, de_str) = match subject.split_once(':') {
14
        Some((f, d)) => (f.trim(), Some(d.trim())),
189
        None => (subject, None),
    };
203
    let family = parse_os_family_token(family_str)?;
203
    let de = match de_str {
14
        Some(s) if !s.is_empty() => Some(parse_de_token(s)),
189
        _ => None,
    };
203
    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.
203
    out.push(DynamicSelector::Os(family));
203
    match (de, op_and_version) {
        // Bare DE with no version: just "is the DE this one"
7
        (Some(env), None) => {
7
            out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
7
        }
        // DE + version: emit a DesktopEnv* condition
7
        (Some(env), Some((op, ver_str))) => {
7
            let v: u32 = ver_str.parse().ok()?;
7
            let dev = DesktopEnvVersion { env, version_id: v };
7
            let cond = match op {
7
                VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
                VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
                VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
            };
7
            out.push(DynamicSelector::OsVersion(cond));
        }
        // OS family + version
70
        (None, Some((op, ver_str))) => {
70
            let os_family = match family {
14
                OsCondition::Linux => OsFamily::Linux,
42
                OsCondition::Windows => OsFamily::Windows,
14
                OsCondition::MacOS => OsFamily::MacOS,
                OsCondition::IOS => OsFamily::IOS,
                OsCondition::Android => OsFamily::Android,
                // Apple, Web, Any have no version line — reject.
                _ => return None,
            };
70
            let version = parse_os_version(os_family, ver_str)?;
70
            let cond = match op {
70
                VersionOp::Min => OsVersionCondition::Min(version),
                VersionOp::Max => OsVersionCondition::Max(version),
                VersionOp::Exact => OsVersionCondition::Exact(version),
            };
70
            out.push(DynamicSelector::OsVersion(cond));
        }
        // Family only — already pushed above (or empty for `any`).
119
        (None, None) => {}
    }
203
    Some(out)
203
}
#[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")]
203
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).
203
    let candidates: &[(&str, VersionOp)] = &[
203
        (">=", VersionOp::Min),
203
        ("<=", VersionOp::Max),
203
        ("=",  VersionOp::Exact),
203
        (">",  VersionOp::Min),
203
        ("<",  VersionOp::Max),
203
    ];
203
    let mut best: Option<(usize, usize, VersionOp)> = None;
1218
    for (op_str, op) in candidates {
1015
        if let Some(pos) = s.find(op_str) {
217
            let len = op_str.len();
140
            best = Some(match best {
77
                None => (pos, len, *op),
140
                Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
140
                Some(b) => b,
            });
798
        }
    }
203
    match best {
77
        Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
126
        None => (s, None),
    }
203
}
#[cfg(feature = "parser")]
203
fn parse_os_family_token(s: &str) -> Option<OsCondition> {
203
    match s.to_lowercase().as_str() {
203
        "linux" => Some(OsCondition::Linux),
126
        "windows" | "win" => Some(OsCondition::Windows),
63
        "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
28
        "ios" => Some(OsCondition::IOS),
28
        "android" => Some(OsCondition::Android),
28
        "apple" => Some(OsCondition::Apple),
21
        "web" | "wasm" => Some(OsCondition::Web),
7
        "any" | "all" | "*" => Some(OsCondition::Any),
        _ => None,
    }
203
}
#[cfg(feature = "parser")]
14
fn parse_de_token(s: &str) -> LinuxDesktopEnv {
14
    match s.to_lowercase().as_str() {
14
        "gnome" => LinuxDesktopEnv::Gnome,
        "kde" => LinuxDesktopEnv::KDE,
        "xfce" => LinuxDesktopEnv::XFCE,
        "unity" => LinuxDesktopEnv::Unity,
        "cinnamon" => LinuxDesktopEnv::Cinnamon,
        "mate" => LinuxDesktopEnv::MATE,
        _ => LinuxDesktopEnv::Other,
    }
14
}
// ============================================================================
// 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, PartialOrd]
);
impl Eq for CssPropertyWithConditions {}
impl PartialOrd for CssPropertyWithConditions {
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for CssPropertyWithConditions {
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        // Compare by condition count only (simple stable ordering)
        self.apply_if
            .as_slice()
            .len()
            .cmp(&other.apply_if.as_slice().len())
    }
}
impl CssPropertyWithConditions {
    /// Create an unconditional property (always applies) - const version
1404
    pub const fn simple(property: CssProperty) -> Self {
1404
        Self {
1404
            property,
1404
            apply_if: DynamicSelectorVec::from_const_slice(&[]),
1404
        }
1404
    }
    /// Create a property with a single condition (const version using slice reference)
119
    pub const fn with_single_condition(
119
        property: CssProperty,
119
        conditions: &'static [DynamicSelector],
119
    ) -> Self {
119
        Self {
119
            property,
119
            apply_if: DynamicSelectorVec::from_const_slice(conditions),
119
        }
119
    }
    /// Create a property with a single condition (non-const, allocates)
    pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
        Self {
            property,
            apply_if: DynamicSelectorVec::from_vec(vec![condition]),
        }
    }
    /// Create a property with multiple conditions (all must match)
    pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
        Self {
            property,
            apply_if: conditions,
        }
    }
    /// Create a property that applies only on hover (const version)
39
    pub const fn on_hover(property: CssProperty) -> Self {
39
        Self::with_single_condition(
39
            property,
39
            &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
        )
39
    }
    /// Create a property that applies only when active (const version)
39
    pub const fn on_active(property: CssProperty) -> Self {
39
        Self::with_single_condition(
39
            property,
39
            &[DynamicSelector::PseudoState(PseudoStateType::Active)],
        )
39
    }
    /// Create a property that applies only when focused (const version)
41
    pub const fn on_focus(property: CssProperty) -> Self {
41
        Self::with_single_condition(
41
            property,
41
            &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
        )
41
    }
    /// Create a property that applies only when disabled (const version)
    pub const fn when_disabled(property: CssProperty) -> Self {
        Self::with_single_condition(
            property,
            &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
        )
    }
    /// Create a property that applies only on a specific OS (non-const, needs runtime value)
    pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
        Self::with_condition(property, DynamicSelector::Os(os))
    }
    /// Create a property that applies only in dark theme (const version)
    pub const fn dark_theme(property: CssProperty) -> Self {
        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
    }
    /// Create a property that applies only in light theme (const version)
    pub const fn light_theme(property: CssProperty) -> Self {
        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
    }
    /// Create a property for Windows only (const version)
    pub const fn on_windows(property: CssProperty) -> Self {
        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
    }
    /// Create a property for macOS only (const version)
    pub const fn on_macos(property: CssProperty) -> Self {
        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
    }
    /// Create a property for Linux only (const version)
    pub const fn on_linux(property: CssProperty) -> Self {
        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
    }
    /// Check if this property matches in the given context
843024
    pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
        // Empty conditions = always matches
843024
        if self.apply_if.as_slice().is_empty() {
162120
            return true;
680904
        }
        // All conditions must match
680904
        self.apply_if
680904
            .as_slice()
680904
            .iter()
680904
            .all(|selector| selector.matches(ctx))
843024
    }
    /// Check if this property has any conditions
    pub fn is_conditional(&self) -> bool {
        !self.apply_if.as_slice().is_empty()
    }
    /// Check if this property is a pseudo-state conditional only
    /// (hover, active, focus, etc.)
    pub fn is_pseudo_state_only(&self) -> bool {
        let conditions = self.apply_if.as_slice();
        !conditions.is_empty()
            && conditions
                .iter()
                .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
    }
    /// 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.
    pub fn is_layout_affecting(&self) -> bool {
        self.property.get_type().can_trigger_relayout()
    }
}
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 {
        self.as_slice().len().cmp(&other.as_slice().len())
    }
}
impl core::hash::Hash for CssPropertyWithConditions {
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        self.property.hash(state);
        // DynamicSelectorVec doesn't implement Hash, so we hash the length
        self.apply_if.as_slice().len().hash(state);
    }
}
impl core::hash::Hash for CssPropertyWithConditionsVec {
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        for item in self.as_slice() {
            item.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")]
4
    pub fn parse(style: &str) -> Self {
4
        Self::parse_with_conditions(style, Vec::new())
4
    }
    /// Internal recursive parser with inherited conditions
    #[cfg(feature = "parser")]
4
    fn parse_with_conditions(style: &str, inherited_conditions: Vec<DynamicSelector>) -> Self {
        use crate::props::property::{
            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
            CssPropertyType,
        };
4
        let mut props = Vec::new();
4
        let key_map = CssKeyMap::get();
4
        let style = style.trim();
4
        if style.is_empty() {
            return CssPropertyWithConditionsVec::from_vec(props);
4
        }
        // Tokenize into segments: properties, pseudo-selectors, and @-rules
4
        let mut chars = style.chars().peekable();
4
        let mut current_segment = String::new();
4
        let mut brace_depth = 0;
209
        for c in chars {
11
            match c {
                '{' => {
                    brace_depth += 1;
                    current_segment.push(c);
                }
                '}' => {
                    brace_depth -= 1;
                    current_segment.push(c);
                    if brace_depth == 0 {
                        // End of a block - process it
                        let segment = current_segment.trim().to_string();
                        current_segment.clear();
                        if let Some(parsed) = Self::parse_block_segment(&segment, &inherited_conditions, &key_map) {
                            props.extend(parsed);
                        }
                    }
                }
11
                ';' if brace_depth == 0 => {
                    // End of a simple property
11
                    let segment = current_segment.trim().to_string();
11
                    current_segment.clear();
11
                    if !segment.is_empty() {
11
                        if let Some(parsed) = Self::parse_property_segment(&segment, &inherited_conditions, &key_map) {
11
                            props.extend(parsed);
11
                        }
                    }
                }
194
                _ => {
194
                    current_segment.push(c);
194
                }
            }
        }
        // Handle any remaining segment (property without trailing semicolon)
4
        let remaining = current_segment.trim();
4
        if !remaining.is_empty() && !remaining.contains('{') {
            if let Some(parsed) = Self::parse_property_segment(remaining, &inherited_conditions, &key_map) {
                props.extend(parsed);
            }
4
        }
4
        CssPropertyWithConditionsVec::from_vec(props)
4
    }
    /// Parse a block segment like `:hover { ... }` or `@os linux { ... }`
    #[cfg(feature = "parser")]
    fn parse_block_segment(
        segment: &str,
        inherited_conditions: &[DynamicSelector],
        key_map: &crate::props::property::CssKeyMap,
    ) -> Option<Vec<CssPropertyWithConditions>> {
        // Find the opening brace
        let brace_pos = segment.find('{')?;
        let selector = segment[..brace_pos].trim();
        // Extract content between braces (excluding the braces themselves)
        let content_start = brace_pos + 1;
        let content_end = segment.rfind('}')?;
        if content_end <= content_start {
            return None;
        }
        let content = &segment[content_start..content_end];
        // Parse selector to get conditions
        let mut conditions = inherited_conditions.to_vec();
        if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
            conditions.extend(new_conditions);
        } else {
            // Unknown selector, skip this block
            return None;
        }
        // Recursively parse the content with the new conditions
        let parsed = Self::parse_with_conditions(content, conditions);
        Some(parsed.into_library_owned_vec())
    }
    /// Parse a selector string into DynamicSelector conditions
    #[cfg(feature = "parser")]
    fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
        let selector = selector.trim();
        // Handle pseudo-selectors
        if let Some(pseudo) = selector.strip_prefix(':') {
            match pseudo {
                "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
                "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
                "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
                "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
                "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
                "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::CheckedTrue)]),
                "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
                "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
                "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
                "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
                _ => return None,
            }
        }
        // Handle @-rules
        if let Some(rule_content) = selector.strip_prefix('@') {
            return Self::parse_at_rule(rule_content);
        }
        // Handle universal selector * (treat as unconditional)
        if selector == "*" {
            return Some(vec![]);
        }
        // Empty selector means unconditional
        if selector.is_empty() {
            return Some(vec![]);
        }
        None
    }
    /// 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")]
    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
        if let Some(rest) = rule_content
            .strip_prefix("os ")
            .or_else(|| if rule_content.starts_with("os(") { Some(&rule_content[2..]) } else { None })
        {
            if let Some(conds) = parse_os_at_rule_content(rest) {
                return Some(conds);
            }
        }
        // @media (min-width: 800px), etc.
        if rule_content.starts_with("media ") {
            let media_query = rule_content[6..].trim();
            if let Some(media_conds) = Self::parse_media_query(media_query) {
                return Some(media_conds);
            }
        }
        // @theme dark, @theme light
        if rule_content.starts_with("theme ") {
            let theme = rule_content[6..].trim();
            match theme {
                "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
                "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
                _ => return None,
            }
        }
        // @lang("de-DE") or @lang de-DE
        if rule_content.starts_with("lang ") || rule_content.starts_with("lang(") {
            let lang_str = if rule_content.starts_with("lang(") {
                rule_content[5..].trim_end_matches(')').trim()
            } else {
                rule_content[5..].trim()
            };
            let lang_str = lang_str
                .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
                .or_else(|| lang_str.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
                .unwrap_or(lang_str);
            if !lang_str.is_empty() {
                return Some(vec![DynamicSelector::Language(
                    LanguageCondition::Prefix(AzString::from(lang_str.to_string()))
                )]);
            }
        }
        // @container (min-width: 400px) or @container sidebar (min-width: 400px)
        if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
            let container_str = if rule_content.starts_with("container(") {
                &rule_content[9..] // keep the '(' for parsing
            } else {
                rule_content[10..].trim()
            };
            let mut conds = Vec::new();
            // Check for named container: "sidebar (min-width: 400px)"
            let (name_part, query_part) = if container_str.starts_with('(') {
                (None, container_str)
            } else if let Some(paren_idx) = container_str.find('(') {
                let name = container_str[..paren_idx].trim();
                if !name.is_empty() {
                    (Some(name), &container_str[paren_idx..])
                } else {
                    (None, container_str)
                }
            } else {
                if !container_str.is_empty() {
                    return Some(vec![DynamicSelector::ContainerName(
                        AzString::from(container_str.to_string())
                    )]);
                }
                return None;
            };
            if let Some(name) = name_part {
                conds.push(DynamicSelector::ContainerName(
                    AzString::from(name.to_string())
                ));
            }
            // Parse (min-width: 400px) style conditions
            if let Some(inner) = query_part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
                if let Some((key, value)) = inner.split_once(':') {
                    let key = key.trim();
                    let value = value.trim();
                    let px_value = value.strip_suffix("px")
                        .and_then(|v| v.trim().parse::<f32>().ok());
                    match key {
                        "min-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(px))); } }
                        "max-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(px))); } }
                        "min-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_min(px))); } }
                        "max-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_max(px))); } }
                        _ => {}
                    }
                }
            }
            if !conds.is_empty() {
                return Some(conds);
            }
        }
        // @prefers-reduced-motion or @reduced-motion
        if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
            return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
        }
        // @prefers-high-contrast or @high-contrast
        if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
            return Some(vec![DynamicSelector::PrefersHighContrast(BoolCondition::True)]);
        }
        None
    }
    /// Parse simple media query
    #[cfg(feature = "parser")]
    fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
        let query = query.trim();
        // Handle (min-width: XXXpx)
        if query.starts_with('(') && query.ends_with(')') {
            let inner = &query[1..query.len()-1];
            if let Some((key, value)) = inner.split_once(':') {
                let key = key.trim();
                let value = value.trim();
                // Parse pixel value
                let px_value = value.strip_suffix("px")
                    .and_then(|v| v.trim().parse::<f32>().ok());
                match key {
                    "min-width" => {
                        if let Some(px) = px_value {
                            return Some(vec![DynamicSelector::ViewportWidth(
                                MinMaxRange::with_min(px)
                            )]);
                        }
                    }
                    "max-width" => {
                        if let Some(px) = px_value {
                            return Some(vec![DynamicSelector::ViewportWidth(
                                MinMaxRange::with_max(px)
                            )]);
                        }
                    }
                    "min-height" => {
                        if let Some(px) = px_value {
                            return Some(vec![DynamicSelector::ViewportHeight(
                                MinMaxRange::with_min(px)
                            )]);
                        }
                    }
                    "max-height" => {
                        if let Some(px) = px_value {
                            return Some(vec![DynamicSelector::ViewportHeight(
                                MinMaxRange::with_max(px)
                            )]);
                        }
                    }
                    other => {
                        // Try orientation, prefers-color-scheme, prefers-reduced-motion, etc.
                        if let Some(sel) = Self::parse_media_feature_inline(other, value) {
                            return Some(vec![sel]);
                        }
                    }
                }
            }
        }
        // Handle screen, print, all
        match query {
            "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
            "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
            "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
            _ => None,
        }
    }
    /// Parse a media query feature value into a DynamicSelector
    /// Handles features like orientation, prefers-color-scheme, prefers-reduced-motion, etc.
    #[cfg(feature = "parser")]
    fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
        match key {
            "orientation" => {
                if value.eq_ignore_ascii_case("portrait") {
                    Some(DynamicSelector::Orientation(OrientationType::Portrait))
                } else if value.eq_ignore_ascii_case("landscape") {
                    Some(DynamicSelector::Orientation(OrientationType::Landscape))
                } else {
                    None
                }
            }
            "prefers-color-scheme" => {
                if value.eq_ignore_ascii_case("dark") {
                    Some(DynamicSelector::Theme(ThemeCondition::Dark))
                } else if value.eq_ignore_ascii_case("light") {
                    Some(DynamicSelector::Theme(ThemeCondition::Light))
                } else {
                    None
                }
            }
            "prefers-reduced-motion" => {
                if value.eq_ignore_ascii_case("reduce") {
                    Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
                } else if value.eq_ignore_ascii_case("no-preference") {
                    Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
                } else {
                    None
                }
            }
            "prefers-contrast" | "prefers-high-contrast" => {
                if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
                    Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
                } else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
                    Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
                } else {
                    None
                }
            }
            _ => None,
        }
    }
    /// Parse a simple property like "color: red"
    #[cfg(feature = "parser")]
11
    fn parse_property_segment(
11
        segment: &str,
11
        inherited_conditions: &[DynamicSelector],
11
        key_map: &crate::props::property::CssKeyMap,
11
    ) -> Option<Vec<CssPropertyWithConditions>> {
        use crate::props::property::{
            parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
            CssPropertyType,
        };
11
        let segment = segment.trim();
11
        if segment.is_empty() {
            return None;
11
        }
11
        let (key, value) = segment.split_once(':')?;
11
        let key = key.trim();
11
        let value = value.trim();
11
        let mut props = Vec::new();
11
        let conditions = if inherited_conditions.is_empty() {
11
            DynamicSelectorVec::from_const_slice(&[])
        } else {
            DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
        };
        // First, try to parse as a regular (non-shorthand) property
11
        if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
6
            if let Ok(prop) = parse_css_property(prop_type, value) {
6
                props.push(CssPropertyWithConditions {
6
                    property: prop,
6
                    apply_if: conditions.clone(),
6
                });
6
                return Some(props);
            }
5
        }
        // If not found, try as a shorthand (combined) property
5
        if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
5
            if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
18
                for prop in expanded_props {
13
                    props.push(CssPropertyWithConditions {
13
                        property: prop,
13
                        apply_if: conditions.clone(),
13
                    });
13
                }
5
                return Some(props);
            }
        }
        None
11
    }
}
#[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.len() > 0, "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.len() > 0, "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 {} should be Fixed(160px), got {:?}", i, track);
            }
        } else {
            panic!("Expected CssProperty::GridTemplateColumns");
        }
1
    }
}