@@ -1571,6 +1571,18 @@ fn strip_string_quotes(val: &str) -> &str {
15711571 val
15721572}
15731573
1574+// True if any direct child is `@in-front` or `@behind`. Such children render
1575+// as absolutely positioned overlays, so the parent automatically becomes a
1576+// positioning context (position:relative + isolation:isolate).
1577+fn has_overlay_children(elem: &Element) -> bool {
1578+ elem.children.iter().any(|child| {
1579+ matches!(
1580+ child,
1581+ Node::Element(e) if matches!(e.kind, ElementKind::InFront | ElementKind::Behind)
1582+ )
1583+ })
1584+}
1585+
15741586fn generate_element(
15751587 elem: &Element,
15761588 parent_kind: Option<&ElementKind>,
@@ -1641,7 +1653,7 @@ fn generate_element(
16411653 }
16421654 // @breadcrumb generates semantic <nav aria-label="breadcrumb"><ol>...</ol></nav>
16431655 if elem.kind == ElementKind::Breadcrumb {
1644− let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles);
1656+ let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles, false);
16451657 let (id, user_class) = extract_id_class(&elem.attrs);
16461658 out.push_str(&ctx.indent());
16471659 out.push_str("<nav aria-label=\"breadcrumb\"");
@@ -1696,7 +1708,9 @@ fn generate_element(
16961708 | ElementKind::Column
16971709 | ElementKind::El
16981710 | ElementKind::Grid
1699− | ElementKind::Stack => "div",
1711+ | ElementKind::Stack
1712+ | ElementKind::InFront
1713+ | ElementKind::Behind => "div",
17001714 ElementKind::Text => "span",
17011715 ElementKind::Paragraph => "p",
17021716 ElementKind::Link => "a",
@@ -1853,6 +1867,8 @@ fn generate_element(
18531867 ElementKind::Noscript => "noscript",
18541868 ElementKind::Address => "address",
18551869 ElementKind::Search => "search",
1870+ ElementKind::InFront => "in-front",
1871+ ElementKind::Behind => "behind",
18561872 ElementKind::H1 => "h1",
18571873 ElementKind::H2 => "h2",
18581874 ElementKind::H3 => "h3",
@@ -1875,7 +1891,14 @@ fn generate_element(
18751891 }
18761892
18771893 // Compute CSS for each state and get a class name
1878− let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles);
1894+ let overlay_children = has_overlay_children(elem);
1895+ let gen_class = compute_class(
1896+ &elem.attrs,
1897+ &elem.kind,
1898+ parent_kind,
1899+ styles,
1900+ overlay_children,
1901+ );
18791902 let (id, user_class) = extract_id_class(&elem.attrs);
18801903
18811904 if ctx.dev && elem.line_num > 0 {
@@ -1953,7 +1976,7 @@ fn generate_element(
19531976 // Critical CSS: inline styles directly instead of using a class
19541977 let is_critical = elem.attrs.iter().any(|a| a.key == "critical");
19551978 if is_critical {
1956− let inline_css = attrs_to_css(&elem.attrs, "", &elem.kind, parent_kind);
1979+ let inline_css = attrs_to_css(&elem.attrs, "", &elem.kind, parent_kind, overlay_children);
19571980 if !inline_css.is_empty() {
19581981 out.push_str(" style=\"");
19591982 out.push_str(&html_escape(&inline_css));
@@ -2059,7 +2082,7 @@ fn generate_self_closing(
20592082 styles: &mut StyleCollector,
20602083 ctx: &mut GenContext,
20612084) {
2062− let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles);
2085+ let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles, false);
20632086 let (id, user_class) = extract_id_class(&elem.attrs);
20642087
20652088 let (tag, kind_label) = match elem.kind {
@@ -2255,13 +2278,14 @@ fn compute_class(
22552278 kind: &ElementKind,
22562279 parent_kind: Option<&ElementKind>,
22572280 styles: &mut StyleCollector,
2281+ has_overlay_children: bool,
22582282) -> Option<String> {
2259− let base = attrs_to_css(attrs, "", kind, parent_kind);
2283+ let base = attrs_to_css(attrs, "", kind, parent_kind, has_overlay_children);
22602284
22612285 // Collect pseudo-state overrides
22622286 let mut pseudo = Vec::new();
22632287 for &(prefix, selector) in PSEUDO_PREFIXES {
2264− let css = attrs_to_css(attrs, prefix, kind, parent_kind);
2288+ let css = attrs_to_css(attrs, prefix, kind, parent_kind, has_overlay_children);
22652289 if !css.is_empty() {
22662290 pseudo.push((selector.to_string(), css));
22672291 }
@@ -2291,7 +2315,7 @@ fn compute_class(
22912315 for prefix in &nth_prefixes {
22922316 let expr = &prefix[4..prefix.len() - 1];
22932317 let selector = format!(":nth-child({})", expr);
2294− let css = attrs_to_css(attrs, prefix, kind, parent_kind);
2318+ let css = attrs_to_css(attrs, prefix, kind, parent_kind, has_overlay_children);
22952319 if !css.is_empty() {
22962320 pseudo.push((selector, css));
22972321 }
@@ -2312,7 +2336,7 @@ fn compute_class(
23122336 for prefix in &has_prefixes {
23132337 let inner = &prefix[4..prefix.len() - 2]; // extract selector from has(selector):
23142338 let selector = format!(":has({})", inner);
2315− let css = attrs_to_css(attrs, prefix, kind, parent_kind);
2339+ let css = attrs_to_css(attrs, prefix, kind, parent_kind, has_overlay_children);
23162340 if !css.is_empty() {
23172341 pseudo.push((selector, css));
23182342 }
@@ -2322,7 +2346,7 @@ fn compute_class(
23222346 let mut responsive = Vec::new();
23232347 for &(bp_name, _) in BREAKPOINTS {
23242348 let prefix = format!("{}:", bp_name);
2325− let css = attrs_to_css(attrs, &prefix, kind, parent_kind);
2349+ let css = attrs_to_css(attrs, &prefix, kind, parent_kind, has_overlay_children);
23262350 if !css.is_empty() {
23272351 responsive.push((bp_name.to_string(), css));
23282352 }
@@ -2332,18 +2356,19 @@ fn compute_class(
23322356 let mut container = Vec::new();
23332357 for &(bp_name, _) in BREAKPOINTS {
23342358 let prefix = format!("cq-{}:", bp_name);
2335− let css = attrs_to_css(attrs, &prefix, kind, parent_kind);
2359+ let css = attrs_to_css(attrs, &prefix, kind, parent_kind, has_overlay_children);
23362360 if !css.is_empty() {
23372361 container.push((bp_name.to_string(), css));
23382362 }
23392363 }
23402364
2341− let dark = attrs_to_css(attrs, "dark:", kind, parent_kind);
2342− let print = attrs_to_css(attrs, "print:", kind, parent_kind);
2343− let motion_safe = attrs_to_css(attrs, "motion-safe:", kind, parent_kind);
2344− let motion_reduce = attrs_to_css(attrs, "motion-reduce:", kind, parent_kind);
2345− let landscape = attrs_to_css(attrs, "landscape:", kind, parent_kind);
2346− let portrait = attrs_to_css(attrs, "portrait:", kind, parent_kind);
2365+ let dark = attrs_to_css(attrs, "dark:", kind, parent_kind, has_overlay_children);
2366+ let print = attrs_to_css(attrs, "print:", kind, parent_kind, has_overlay_children);
2367+ let motion_safe = attrs_to_css(attrs, "motion-safe:", kind, parent_kind, has_overlay_children);
2368+ let motion_reduce =
2369+ attrs_to_css(attrs, "motion-reduce:", kind, parent_kind, has_overlay_children);
2370+ let landscape = attrs_to_css(attrs, "landscape:", kind, parent_kind, has_overlay_children);
2371+ let portrait = attrs_to_css(attrs, "portrait:", kind, parent_kind, has_overlay_children);
23472372
23482373 // Dedupe: if a property is declared twice within a single rule, keep only
23492374 // the last occurrence (element-kind defaults are written before
@@ -2545,11 +2570,18 @@ fn attrs_to_css(
25452570 state_prefix: &str,
25462571 kind: &ElementKind,
25472572 parent_kind: Option<&ElementKind>,
2573+ has_overlay_children: bool,
25482574) -> String {
25492575 let mut css = String::new();
25502576
25512577 // Base element styles only for the default (non-state) pass
25522578 if state_prefix.is_empty() {
2579+ // Elements with @in-front / @behind children become positioning
2580+ // contexts. Pushed before user attrs so an explicit `position` wins
2581+ // via dedupe (only the last declaration of a property is kept).
2582+ if has_overlay_children {
2583+ css.push_str("position:relative;isolation:isolate;");
2584+ }
25532585 match kind {
25542586 ElementKind::Row => css.push_str("display:flex;flex-direction:row;"),
25552587 ElementKind::Column
@@ -2613,6 +2645,16 @@ fn attrs_to_css(
26132645 ElementKind::Avatar => css.push_str("display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;overflow:hidden;flex-shrink:0;"),
26142646 ElementKind::Carousel => css.push_str("display:flex;flex-direction:row;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;"),
26152647 ElementKind::Chip => css.push_str("display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:9999px;font-size:0.875rem;border:1px solid currentColor;"),
2648+ // Overlay layers fill the parent. Parent gets position:relative
2649+ // applied automatically (see has_overlay_children check below).
2650+ ElementKind::InFront => {
2651+ css.push_str(FLEX_COLUMN);
2652+ css.push_str("position:absolute;inset:0;");
2653+ }
2654+ ElementKind::Behind => {
2655+ css.push_str(FLEX_COLUMN);
2656+ css.push_str("position:absolute;inset:0;z-index:-1;");
2657+ }
26162658 _ => {}
26172659 }
26182660 // Children of @carousel get scroll-snap-align and flex-shrink