/// JavaScript emitter — generates executable JS from DreamStack AST + signal graph.

use std::collections::HashSet;
use ds_parser::*;
use ds_analyzer::{SignalGraph, SignalKind, AnalyzedView, InitialValue};

pub struct JsEmitter {
    output: String,
    indent: usize,
    node_id_counter: usize,
    /// Scoped local variables (e.g., for-in loop vars). Stack of scopes for nesting.
    local_var_scopes: Vec<HashSet<String>>,
}

impl JsEmitter {
    pub fn new() -> Self {
        Self {
            output: String::new(),
            indent: 0,
            node_id_counter: 0,
            local_var_scopes: Vec::new(),
        }
    }

    /// Push a new variable scope (e.g., entering a for-in loop)
    fn push_scope(&mut self, vars: &[&str]) {
        let set: HashSet<String> = vars.iter().map(|s| s.to_string()).collect();
        self.local_var_scopes.push(set);
    }

    /// Pop the top variable scope (e.g., leaving a for-in loop)
    fn pop_scope(&mut self) {
        self.local_var_scopes.pop();
    }

    /// Check if a name is a local variable in any active scope
    fn is_local_var(&self, name: &str) -> bool {
        self.local_var_scopes.iter().any(|scope| scope.contains(name))
    }

    /// Generate a complete HTML page with embedded runtime and compiled app.
    pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView], minify: bool) -> String {
        let mut emitter = Self::new();
        let app_js = emitter.emit_program(program, graph, views);

        // Tree-shake unused runtime features
        let features = detect_features(program);
        let shaken_runtime = tree_shake_runtime(RUNTIME_JS, &features);

        let runtime = if minify {
            minify_js(&shaken_runtime)
        } else {
            shaken_runtime
        };

        let css = if minify {
            minify_css(CSS_RESET)
        } else {
            CSS_RESET.to_string()
        };

        let app = if minify {
            minify_js(&app_js)
        } else {
            app_js
        };

        format!(
            r#"<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DreamStack App</title>
  <style>
    {css}
  </style>
</head>
<body>
  <div id="ds-root"></div>
  <script>
{runtime}
  </script>
  <script>
{app}
  </script>
</body>
</html>"#
        )
    }

    /// Generate the application JS module.
    pub fn emit_program(&mut self, program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String {
        self.output.clear();

        self.emit_line("// DreamStack compiled output");
        self.emit_line("// Generated by dreamstack compiler v0.1.0");
        self.emit_line("");
        self.emit_line("(function() {");
        self.indent += 1;

        // Phase 1: Create all signals
        // Collect explicit output list from stream declaration (if any)
        let stream_outputs: Vec<String> = program.declarations.iter()
            .filter_map(|d| if let Declaration::Stream(s) = d { Some(s) } else { None })
            .flat_map(|s| s.output.iter().cloned())
            .collect();
        let has_explicit_output = !stream_outputs.is_empty();

        self.emit_line("// ── Signals ──");
        for node in &graph.nodes {
            match &node.kind {
                SignalKind::Source => {
                    let init = match &node.initial_value {
                        Some(InitialValue::Int(n)) => format!("{n}"),
                        Some(InitialValue::Float(n)) => format!("{n}"),
                        Some(InitialValue::Bool(b)) => format!("{b}"),
                        Some(InitialValue::String(s)) => format!("\"{s}\""),
                        None => {
                            // Fall back to the let declaration expression
                            // (handles List, Record, etc.)
                            if let Some(expr) = self.find_let_expr(program, &node.name) {
                                // Check if it's a spring() call — Spring is already signal-compatible
                                if matches!(expr, Expr::Call(name, _) if name == "spring") {
                                    let js_expr = self.emit_expr(expr);
                                    self.emit_line(&format!("const {} = {};", node.name, js_expr));
                                    continue;
                                }
                                // Check if it's a stream from — _connectStream returns a signal proxy
                                if matches!(expr, Expr::StreamFrom { .. }) {
                                    let js_expr = self.emit_expr(expr);
                                    self.emit_line(&format!("const {} = {};", node.name, js_expr));
                                    continue;
                                }
                                self.emit_expr(expr)
                            } else {
                                "null".to_string()
                            }
                        }
                    };
                    self.emit_line(&format!("const {} = DS.signal({});", node.name, init));
                    // Register for streaming only if in output list (or no explicit output = all)
                    if !has_explicit_output || stream_outputs.contains(&node.name) {
                        self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name));
                    }
                }
                SignalKind::Derived => {
                    // Find the let declaration to get the expression
                    let expr = self.find_let_expr(program, &node.name);
                    if let Some(expr) = expr {
                        let js_expr = self.emit_expr(expr);
                        self.emit_line(&format!(
                            "const {} = DS.derived(() => {});",
                            node.name, js_expr
                        ));
                        // Register for streaming only if in output list (or no explicit output = all)
                        if !has_explicit_output || stream_outputs.contains(&node.name) {
                            self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name));
                        }
                    }
                }
                SignalKind::Handler { .. } => {} // Handled later
            }
        }

        // Phase 1b: Emit enum declarations as frozen JS objects
        for decl in &program.declarations {
            if let Declaration::Enum(enum_decl) = decl {
                // Emit: const Status = Object.freeze({ Loading: "Loading", Ok: (data) => ({ _tag: "Ok", data }), ... })
                let mut variant_entries = Vec::new();
                for variant in &enum_decl.variants {
                    if variant.fields.is_empty() {
                        // Unit variant: Status.Loading === "Loading"
                        variant_entries.push(format!("{}: \"{}\"", variant.name, variant.name));
                    } else {
                        // Data variant: Status.Ok(data) => { _tag: "Ok", data }
                        let params: Vec<&str> = variant.fields.iter()
                            .map(|p| p.name.as_str())
                            .collect();
                        let fields: Vec<String> = params.iter()
                            .map(|p| format!("{p}"))
                            .collect();
                        variant_entries.push(format!(
                            "{}: ({}) => ({{ _tag: \"{}\", {} }})",
                            variant.name, params.join(", "), variant.name, fields.join(", ")
                        ));
                    }
                }
                self.emit_line(&format!(
                    "const {} = Object.freeze({{ {} }});",
                    enum_decl.name,
                    variant_entries.join(", ")
                ));
            }
        }

        // Phase 1c: Emit runtime refinement guards
        // Collect type aliases from program
        let mut type_aliases: std::collections::HashMap<String, &TypeExpr> = std::collections::HashMap::new();
        for decl in &program.declarations {
            if let Declaration::TypeAlias(alias) = decl {
                type_aliases.insert(alias.name.clone(), &alias.definition);
            }
        }

        let mut guards_emitted = false;
        for decl in &program.declarations {
            if let Declaration::Let(let_decl) = decl {
                // Skip literals — they're statically checked by the type checker
                if matches!(let_decl.value,
                    Expr::IntLit(_) | Expr::FloatLit(_) | Expr::StringLit(_) | Expr::BoolLit(_)
                ) {
                    continue;
                }

                if let Some(ref type_ann) = let_decl.type_annotation {
                    // Resolve type annotation to find refinement predicate
                    let resolved = match type_ann {
                        TypeExpr::Named(name) => type_aliases.get(name).copied(),
                        TypeExpr::Refined { .. } => Some(type_ann),
                        _ => None,
                    };

                    if let Some(TypeExpr::Refined { predicate, .. }) = resolved {
                        if !guards_emitted {
                            self.emit_line("");
                            self.emit_line("// ── Refinement Guards ──");
                            guards_emitted = true;
                        }
                        let type_name = match type_ann {
                            TypeExpr::Named(n) => n.clone(),
                            _ => "refined type".to_string(),
                        };
                        let js_pred = Self::predicate_to_js(predicate, &let_decl.name);
                        self.emit_line(&format!(
                            "if (!({js_pred})) throw new Error(\"Refinement violated: `{name}` must satisfy {type_name}\");",
                            js_pred = js_pred,
                            name = let_decl.name,
                            type_name = type_name,
                        ));
                    }
                }
            }
        }

        self.emit_line("");

        // Phase 2a: Component functions
        self.emit_line("// ── Components ──");
        for decl in &program.declarations {
            if let Declaration::Component(comp) = decl {
                let params = comp.props.iter()
                    .map(|p| p.name.clone())
                    .collect::<Vec<_>>()
                    .join(", ");
                self.emit_line(&format!("function component_{}({}) {{", comp.name, params));
                self.indent += 1;
                let child_var = self.emit_view_expr(&comp.body, graph);
                self.emit_line(&format!("return {};", child_var));
                self.indent -= 1;
                self.emit_line("}");
                self.emit_line("");
            }
        }

        // Phase 2b: Component functions
        let has_components = program.declarations.iter().any(|d| matches!(d, Declaration::Component(_)));
        if has_components {
            self.emit_line("// ── Components ──");
            for decl in &program.declarations {
                if let Declaration::Component(comp) = decl {
                    self.emit_component_decl(comp, graph);
                }
                // Also handle exported components
                if let Declaration::Export(_, inner) = decl {
                    if let Declaration::Component(comp) = inner.as_ref() {
                        self.emit_component_decl(comp, graph);
                    }
                }
            }
        }

        // Phase 2c: Build views
        self.emit_line("// ── Views ──");
        for decl in &program.declarations {
            if let Declaration::View(view) = decl {
                self.emit_view(view, graph);
            }
        }

        // Phase 3: Event handlers
        self.emit_line("");
        self.emit_line("// ── Handlers ──");
        for decl in &program.declarations {
            if let Declaration::OnHandler(handler) = decl {
                self.emit_handler(handler);
            }
        }

        // Phase 4: Route view functions
        let routes: Vec<_> = program.declarations.iter()
            .filter_map(|d| if let Declaration::Route(r) = d { Some(r) } else { None })
            .collect();

        if !routes.is_empty() {
            self.emit_line("");
            self.emit_line("// ── Routes ──");
            for (i, route) in routes.iter().enumerate() {
                self.emit_line(&format!("function route_view_{}() {{", i));
                self.indent += 1;
                let child = self.emit_view_expr(&route.body, graph);
                self.emit_line(&format!("return {};", child));
                self.indent -= 1;
                self.emit_line("}");
            }
        }

        // Phase 5: Mount to DOM
        self.emit_line("");
        self.emit_line("// ── Mount ──");

        if !routes.is_empty() {
            // Router-based mounting
            self.emit_line("const __root = document.getElementById('ds-root');");
            self.emit_line("let __currentView = null;");

            // Emit any non-route views as layout (e.g., nav bar)
            for view in views {
                self.emit_line(&format!("_a(__root, view_{}());", view.name));
            }

            self.emit_line("const __routeContainer = _c('div');");
            self.emit_line("_cn(__routeContainer, 'ds-route-container');");
            self.emit_line("_a(__root, __routeContainer);");
            self.emit_line("");
            self.emit_line("DS.effect(() => {");
            self.indent += 1;
            self.emit_line("const path = DS.route.value;");
            self.emit_line("__routeContainer.innerHTML = '';");

            for (i, route) in routes.iter().enumerate() {
                let branch = if i == 0 { "if" } else { "} else if" };
                self.emit_line(&format!(
                    "{} (DS.matchRoute(\"{}\", path)) {{",
                    branch, route.path
                ));
                self.indent += 1;
                self.emit_line(&format!("_a(__routeContainer, route_view_{}());", i));
                self.indent -= 1;
            }
            self.emit_line("}");

            self.indent -= 1;
            self.emit_line("});");
        } else if let Some(view) = views.first() {
            self.emit_line(&format!(
                "_a(document.getElementById('ds-root'), view_{}());",
                view.name
            ));
        }

        // Phase 6: Constraints
        let constraints: Vec<_> = program.declarations.iter()
            .filter_map(|d| if let Declaration::Constrain(c) = d { Some(c) } else { None })
            .collect();

        if !constraints.is_empty() {
            self.emit_line("");
            self.emit_line("// ── Constraints ──");
            for c in &constraints {
                let expr_js = self.emit_expr(&c.expr);
                // Use view element name or fall back to querySelector
                self.emit_line(&format!(
                    "DS.constrain(document.querySelector('[data-ds=\"{}\"]:not(.x)') || document.getElementById('ds-root'), '{}', () => {});",
                    c.element, c.prop, expr_js
                ));
            }
        }

        // Phase 6b: Layout blocks
        let layouts: Vec<_> = program.declarations.iter()
            .filter_map(|d| if let Declaration::Layout(l) = d { Some(l) } else { None })
            .collect();

        for layout in &layouts {
            self.emit_line("");
            self.emit_line(&format!("// ── Layout: {} ──", layout.name));
            self.emit_line(&format!("(function setupLayout_{}() {{", layout.name));
            self.indent += 1;
            self.emit_line("function solve() {");
            self.indent += 1;
            self.emit_line("const vp = { width: window.innerWidth, height: window.innerHeight };");

            // Collect all element names referenced
            let mut elements = std::collections::HashSet::new();
            for c in &layout.constraints {
                Self::collect_layout_elements(&c.left, &mut elements);
                Self::collect_layout_elements(&c.right, &mut elements);
            }

            // Create element variable objects: { x, y, width, height }
            for el in &elements {
                if el == "parent" {
                    self.emit_line(&format!(
                        "const {el} = {{ x: 0, y: 0, width: vp.width, height: vp.height }};"
                    ));
                } else {
                    self.emit_line(&format!(
                        "const {el} = {{ x: 0, y: 0, width: 0, height: 0 }};"
                    ));
                }
            }

            // Emit constraint assignments (simple direct assignment for == constraints)
            for c in &layout.constraints {
                let left_js = Self::layout_expr_to_js(&c.left);
                let right_js = Self::layout_expr_to_js(&c.right);
                match c.op {
                    ds_parser::ConstraintOp::Eq => {
                        self.emit_line(&format!("{left_js} = {right_js};"));
                    }
                    ds_parser::ConstraintOp::Gte => {
                        self.emit_line(&format!("{left_js} = Math.max({left_js}, {right_js});"));
                    }
                    ds_parser::ConstraintOp::Lte => {
                        self.emit_line(&format!("{left_js} = Math.min({left_js}, {right_js});"));
                    }
                }
            }

            // Apply solved values to DOM elements
            for el in &elements {
                if el == "parent" { continue; }
                self.emit_line(&format!(
                    "const {el}El = document.querySelector('[data-ds=\"{el}\"]');"
                ));
                self.emit_line(&format!(
                    "if ({el}El) {{ {el}El.style.position = 'absolute'; {el}El.style.left = {el}.x + 'px'; {el}El.style.top = {el}.y + 'px'; {el}El.style.width = {el}.width + 'px'; {el}El.style.height = {el}.height + 'px'; }}"
                ));
            }

            self.indent -= 1;
            self.emit_line("}");
            self.emit_line("solve();");
            self.emit_line("window.addEventListener('resize', solve);");
            self.indent -= 1;
            self.emit_line("})();");
        }

        // Phase 7: Stream initialization
        let streams: Vec<_> = program.declarations.iter()
            .filter_map(|d| if let Declaration::Stream(s) = d { Some(s) } else { None })
            .collect();

        if !streams.is_empty() {
            self.emit_line("");
            self.emit_line("// ── Bitstream Streaming ──");
            for stream in &streams {
                let mode = match stream.mode {
                    StreamMode::Pixel => "pixel",
                    StreamMode::Delta => "delta",
                    StreamMode::Signal => "signal",
                };
                let url = self.emit_expr(&stream.relay_url);

                match stream.transport {
                    StreamTransport::WebRTC => {
                        // Derive signaling URL: replace /source/ with /signal/
                        self.emit_line(&format!(
                            "DS._initWebRTC({url}.replace('/source/', '/signal/'), {url}, '{mode}');"
                        ));
                    }
                    StreamTransport::WebSocket => {
                        self.emit_line(&format!(
                            "DS._initStream({url}, '{mode}');"
                        ));
                    }
                }

                // Set output filter if stream has explicit output list
                if !stream.output.is_empty() {
                    let names_js = stream.output.iter()
                        .map(|n| format!("\"{}\"", n))
                        .collect::<Vec<_>>()
                        .join(",");
                    self.emit_line(&format!(
                        "DS._streamOutputFilter = new Set([{}]);",
                        names_js
                    ));
                }
            }
        }

        // Phase 8: Timer / interval declarations — MERGED by interval
        // Group all `every N` bodies with the same interval into one setInterval
        {
            let mut interval_groups: std::collections::BTreeMap<String, Vec<String>> =
                std::collections::BTreeMap::new();

            for decl in &program.declarations {
                if let Declaration::Every(every) = decl {
                    let interval_js = self.emit_expr(&every.interval_ms);
                    let body_js = self.emit_event_handler_expr(&every.body);
                    interval_groups
                        .entry(interval_js)
                        .or_insert_with(Vec::new)
                        .push(body_js);
                }
            }

            for (interval, bodies) in &interval_groups {
                self.emit_line("");
                self.emit_line(&format!("// ── Timer ({}ms, {} actions) ──", interval, bodies.len()));
                let merged = bodies.join("; ");
                self.emit_line(&format!(
                    "setInterval(() => {{ {}; DS.flush(); }}, {});",
                    merged, interval
                ));
            }
        }

        // Phase 9: Top-level expression statements (log, push, etc.)
        for decl in &program.declarations {
            if let Declaration::ExprStatement(expr) = decl {
                let js = self.emit_expr(expr);
                self.emit_line(&format!("{};", js));
            }
        }

        self.indent -= 1;
        self.emit_line("})();");

        self.output.clone()
    }

    fn emit_view(&mut self, view: &ViewDecl, graph: &SignalGraph) {
        self.emit_line(&format!("function view_{}() {{", view.name));
        self.indent += 1;
        let root_id = self.emit_view_expr(&view.body, graph);
        self.emit_line(&format!("return {};", root_id));
        self.indent -= 1;
        self.emit_line("}");
    }

    fn emit_component_decl(&mut self, comp: &ComponentDecl, graph: &SignalGraph) {
        self.emit_line(&format!("function DS_{}(props, __children) {{", comp.name));
        self.indent += 1;
        // Destructure props into local signal-compatible variables
        // Props may be raw values, signals, or callback functions
        for p in &comp.props {
            // Create a signal-like wrapper:
            // - If prop is a function (reactive getter), wrap as { get value() { return props.x(); } }
            // - If already a signal object, use it directly
            // - Otherwise wrap raw value as a getter
            self.emit_line(&format!(
                "const {} = (typeof props.{} === 'function') ? {{ get value() {{ return props.{}(); }} }} : (props.{} !== undefined && props.{} !== null) ? (typeof props.{} === 'object' && 'value' in props.{} ? props.{} : {{ get value() {{ return props.{}; }} }}) : {{ get value() {{ return \"\"; }} }};",
                p.name, p.name, p.name, p.name, p.name, p.name, p.name, p.name, p.name
            ));
        }
        let root_id = self.emit_view_expr(&comp.body, graph);
        self.emit_line(&format!("return {};", root_id));
        self.indent -= 1;
        self.emit_line("}");
    }

    /// Emit a view expression and return the variable name of the created DOM node.
    fn emit_view_expr(&mut self, expr: &Expr, graph: &SignalGraph) -> String {
        match expr {
            Expr::Container(container) => {
                // ─── PHYSICS SCENE ───
                if matches!(container.kind, ContainerKind::Scene) {
                    return self.emit_scene(container, graph);
                }

                let node_var = self.next_node_id();
                let tag = match &container.kind {
                    ContainerKind::Column => "div",
                    ContainerKind::Row => "div",
                    ContainerKind::Stack => "div",
                    ContainerKind::Panel => "div",
                    ContainerKind::Form => "form",
                    ContainerKind::List => "ul",
                    ContainerKind::Scene => "canvas", // handled above
                    ContainerKind::Custom(s) => s,
                };

                let class = match &container.kind {
                    ContainerKind::Column => "ds-column",
                    ContainerKind::Row => "ds-row",
                    ContainerKind::Stack => "ds-stack",
                    ContainerKind::Panel => "ds-panel",
                    _ => "",
                };

                self.emit_line(&format!("const {} = _c('{}');", node_var, tag));
                if !class.is_empty() {
                    self.emit_line(&format!("_cn({}, '{}');", node_var, class));
                }

                // Handle container props (variant, class, events, style, layout)
                let container_tag = match &container.kind {
                    ContainerKind::Column => "column",
                    ContainerKind::Row => "row",
                    ContainerKind::Stack => "stack",
                    ContainerKind::Panel => "panel",
                    _ => "column",
                };
                for (key, val) in &container.props {
                    match key.as_str() {
                        "variant" => {
                            match val {
                                Expr::StringLit(s) if s.segments.len() == 1 => {
                                    if let Some(ds_parser::StringSegment::Literal(v)) = s.segments.first() {
                                        let css_class = variant_to_css(container_tag, v);
                                        self.emit_line(&format!(
                                            "{}.className += ' {}';",
                                            node_var, css_class
                                        ));
                                    }
                                }
                                _ => {
                                    let js = self.emit_expr(val);
                                    let map_entries = variant_map_js(container_tag);
                                    self.emit_line(&format!(
                                        "DS.effect(() => {{ const v = {js}; const cls = ({map_entries})[typeof v === 'object' ? v.value : v] || ''; {node_var}.className = 'ds-{container_tag} ' + cls; }});"
                                    ));
                                }
                            }
                        }
                        "class" => {
                            let js = self.emit_expr(val);
                            if js.contains(".value") {
                                self.emit_line(&format!(
                                    "DS.effect(() => {{ {}.className = 'ds-{}' + ' ' + {}; }});",
                                    node_var, container_tag, js
                                ));
                            } else {
                                self.emit_line(&format!("{}.className += ' ' + {};", node_var, js));
                            }
                        }
                        "click" | "submit" => {
                            let handler_js = self.emit_event_handler_expr(val);
                            self.emit_line(&format!(
                                "{}.addEventListener('{}', (e) => {{ {}; DS.flush(); }});",
                                node_var, key, handler_js
                            ));
                        }
                        "style" => {
                            let js = self.emit_expr(val);
                            if js.contains(".value") {
                                self.emit_line(&format!(
                                    "DS.effect(() => {{ {}.style.cssText = {}; }});",
                                    node_var, js
                                ));
                            } else {
                                self.emit_line(&format!("{}.style.cssText = {};", node_var, js));
                            }
                        }
                        _ => {
                            // Layout props (x, y, width, height) or arbitrary style
                            let js_val = self.emit_expr(val);
                            // Check if this is a signal reference: emit_expr on signal `foo`
                            // returns `foo.value`, so also check with `.value` stripped.
                            // For dotted access like `game.value.p1y`, check if .value appears anywhere.
                            let raw_name = js_val.strip_suffix(".value").unwrap_or(&js_val);
                            if graph.name_to_id.contains_key(raw_name)
                                || graph.name_to_id.contains_key(&js_val)
                                || self.is_signal_ref(&js_val)
                                || js_val.contains(".value")
                            {
                                self.emit_line(&format!(
                                    "DS.effect(() => {{ {}.style.{} = {} + 'px'; }});",
                                    node_var, key, js_val
                                ));
                            } else {
                                self.emit_line(&format!("{}.style.{} = {};", node_var, key, js_val));
                            }
                        }
                    }
                }

                // Emit children
                for child in &container.children {
                    let child_var = self.emit_view_expr(child, graph);
                    self.emit_line(&format!("_a({}, {});", node_var, child_var));
                }

                node_var
            }

            Expr::Element(element) => {
                let node_var = self.next_node_id();
                let html_tag = match element.tag.as_str() {
                    "text" => "span",
                    "button" => "button",
                    "input" => "input",
                    "image" | "avatar" => "img",
                    "link" => "a",
                    "label" => "label",
                    "spinner" => "div",
                    "skeleton" => "div",
                    _ => "div",
                };

                self.emit_line(&format!("const {} = _c('{}');", node_var, html_tag));
                self.emit_line(&format!("_cn({}, 'ds-{}');", node_var, element.tag));

                // Handle text content / arguments
                for arg in &element.args {
                    match arg {
                        Expr::StringLit(s) => {
                            let has_interp = s.segments.iter().any(|seg| matches!(seg, StringSegment::Interpolation(_)));
                            if has_interp {
                                // Reactive interpolated string: wrap in effect
                                let template_js = self.emit_expr(arg);
                                self.emit_line(&format!(
                                    "DS.effect(() => {{ {}.textContent = {}; }});",
                                    node_var, template_js
                                ));
                            } else if let Some(StringSegment::Literal(text)) = s.segments.first() {
                                self.emit_line(&format!(
                                    "{}.textContent = \"{}\";",
                                    node_var,
                                    text.replace('"', "\\\"")
                                ));
                            }
                        }
                        Expr::Ident(name) => {
                            if self.is_local_var(name) {
                                // Non-reactive local variable (e.g., for-in loop var)
                                self.emit_line(&format!(
                                    "{}.textContent = {};",
                                    node_var, name
                                ));
                            } else {
                                // Reactive text binding!
                                self.emit_line(&format!(
                                    "DS.effect(() => {{ {}.textContent = {}.value; }});",
                                    node_var, name
                                ));
                            }
                        }
                        _ => {
                            let js = self.emit_expr(arg);
                            // Wrap in DS.effect if the expression references signals
                            if js.contains(".value") {
                                self.emit_line(&format!(
                                    "DS.effect(() => {{ {}.textContent = {}; }});",
                                    node_var, js
                                ));
                            } else {
                                self.emit_line(&format!("_t({}, {});", node_var, js));
                            }
                        }
                    }
                }

                // Handle props (event handlers, attributes)
                for (key, val) in &element.props {
                    match key.as_str() {
                        "click" | "input" | "change" | "submit" | "keydown" | "keyup" | "mousedown" | "mouseup" => {
                            let handler_js = self.emit_event_handler_expr(val);
                            let dom_event = match key.as_str() {
                                "click" => "click",
                                "input" => "input",
                                "change" => "change",
                                "submit" => "submit",
                                "keydown" => "keydown",
                                "keyup" => "keyup",
                                "mousedown" => "mousedown",
                                "mouseup" => "mouseup",
                                _ => key,
                            };
                            self.emit_line(&format!(
                                "{}.addEventListener('{}', (e) => {{ {}; DS.flush(); }});",
                                node_var, dom_event, handler_js
                            ));
                        }
                        "bind" => {
                            // Two-way binding: signal <-> input
                            // Use raw signal name (not emit_expr which adds .value)
                            let signal_name = match val {
                                Expr::Ident(name) => name.clone(),
                                _ => self.emit_expr(val),
                            };
                            // Signal -> input
                            self.emit_line(&format!(
                                "DS.effect(() => {{ {}.value = {}.value; }});",
                                node_var, signal_name
                            ));
                            // Input -> signal (with streaming diff)
                            self.emit_line(&format!(
                                "{}.addEventListener('input', (e) => {{ {}.value = e.target.value; DS._streamDiff(\"{}\", e.target.value); DS.flush(); }});",
                                node_var, signal_name, signal_name
                            ));
                        }
                        "class" => {
                            let js = self.emit_expr(val);
                            self.emit_line(&format!("{}.className += ' ' + {};", node_var, js));
                        }
                        "placeholder" => {
                            let js = self.emit_expr(val);
                            self.emit_line(&format!("{}.placeholder = {};", node_var, js));
                        }
                        "value" => {
                            let js = self.emit_expr(val);
                            self.emit_line(&format!(
                                "DS.effect(() => {{ {}.value = {}.value; }});",
                                node_var, js
                            ));
                        }
                        "style" => {
                            let js = self.emit_expr(val);
                            self.emit_line(&format!("{}.style.cssText = {};", node_var, js));
                        }
                        "disabled" => {
                            let js = self.emit_expr(val);
                            self.emit_line(&format!(
                                "DS.effect(() => {{ {}.disabled = {}.value; }});",
                                node_var, js
                            ));
                        }
                        "variant" => {
                            // Map variant prop to CSS class based on element tag
                            let tag = element.tag.as_str();
                            match val {
                                Expr::StringLit(s) if s.segments.len() == 1 => {
                                    // Static variant: emit class directly
                                    if let Some(ds_parser::StringSegment::Literal(v)) = s.segments.first() {
                                        let css_class = variant_to_css(tag, v);
                                        self.emit_line(&format!(
                                            "{}.className += ' {}';",
                                            node_var, css_class
                                        ));
                                    }
                                }
                                _ => {
                                    // Dynamic variant: reactive class via inline lookup
                                    let js = self.emit_expr(val);
                                    let tag = element.tag.as_str();
                                    let map_entries = variant_map_js(tag);
                                    self.emit_line(&format!(
                                        "DS.effect(() => {{ const v = {js}; const cls = ({map_entries})[typeof v === 'object' ? v.value : v] || ''; {node_var}.className = 'ds-{tag} ' + cls; }});"
                                    ));
                                }
                            }
                        }
                        _ => {
                            let js = self.emit_expr(val);
                            self.emit_line(&format!("{}.setAttribute('{}', {});", node_var, key, js));
                        }
                    }
                }

                node_var
            }

            Expr::When(cond, body, else_body) => {
                let else_expr = else_body.clone();
                let anchor_var = self.next_node_id();
                let container_var = self.next_node_id();
                let cond_js = self.emit_expr(cond);

                self.emit_line(&format!("const {} = document.createComment('when');", anchor_var));
                self.emit_line(&format!("let {} = null;", container_var));

                let has_else = else_expr.is_some();
                let else_container = if has_else {
                    let ec = self.next_node_id();
                    self.emit_line(&format!("let {} = null;", ec));
                    Some(ec)
                } else {
                    None
                };

                let effect_fn = format!("_when_{}", anchor_var);
                self.emit_line(&format!("DS.effect(function {}() {{", effect_fn));
                self.indent += 1;
                // Guard: if anchor not yet in DOM (e.g. inside slot DocumentFragment), defer
                self.emit_line(&format!(
                    "if (!{}.parentNode) {{ requestAnimationFrame(() => DS.effect({})); return; }}",
                    anchor_var, effect_fn
                ));
                self.emit_line(&format!("const show = {};", cond_js));

                // Show when: condition true and not already showing
                self.emit_line(&format!("if (show && !{}) {{", container_var));
                self.indent += 1;
                if let Some(ref ec) = else_container {
                    self.emit_line(&format!("if ({}) {{ {}.remove(); {} = null; }}", ec, ec, ec));
                }
                let child_var = self.emit_view_expr(body, graph);
                self.emit_line(&format!("{} = {};", container_var, child_var));
                self.emit_line(&format!(
                    "{}.parentNode.insertBefore({}, {}.nextSibling);",
                    anchor_var, container_var, anchor_var
                ));
                self.indent -= 1;

                // Hide when: condition false and currently showing
                self.emit_line(&format!("}} else if (!show && {}) {{", container_var));
                self.indent += 1;
                self.emit_line(&format!("{}.remove();", container_var));
                self.emit_line(&format!("{} = null;", container_var));
                if let Some(eb) = &else_expr {
                    if let Some(ec) = &else_container {
                        let else_child = self.emit_view_expr(eb, graph);
                        self.emit_line(&format!("{} = {};", ec, else_child));
                        self.emit_line(&format!(
                            "{}.parentNode.insertBefore({}, {}.nextSibling);",
                            anchor_var, ec, anchor_var
                        ));
                    }
                }
                self.indent -= 1;

                // Initial else: show=false and nothing rendered yet
                if let Some(eb) = &else_expr {
                    if let Some(ec) = &else_container {
                        self.emit_line(&format!("}} else if (!show && !{} && !{}) {{", container_var, ec));
                        self.indent += 1;
                        let else_child2 = self.emit_view_expr(eb, graph);
                        self.emit_line(&format!("{} = {};", ec, else_child2));
                        self.emit_line(&format!(
                            "{}.parentNode.insertBefore({}, {}.nextSibling);",
                            anchor_var, ec, anchor_var
                        ));
                        self.indent -= 1;
                    }
                }

                self.emit_line("}");
                self.indent -= 1;
                self.emit_line("});");

                anchor_var
            }

            // Each loop: `each item in list -> template`
            Expr::Each(item_name, list_expr, body) => {
                let container_var = self.next_node_id();
                let iter_js = self.emit_expr(list_expr);
                let iter_var = self.next_node_id();

                self.emit_line(&format!("const {} = _c('div');", container_var));
                self.emit_line(&format!("_cn({}, 'ds-each-list');", container_var));

                self.emit_line("DS.effect(() => {");
                self.indent += 1;
                self.emit_line(&format!("const {} = {};", iter_var, iter_js));
                self.emit_line(&format!("{}.innerHTML = '';", container_var));

                self.emit_line(&format!(
                    "const __list = ({0} && {0}.value !== undefined) ? {0}.value : (Array.isArray({0}) ? {0} : []);",
                    iter_var
                ));
                self.emit_line(&format!("__list.forEach(({item_name}, _idx) => {{"));
                self.indent += 1;
                self.push_scope(&[item_name.as_str(), "_idx"]);
                let child_var = self.emit_view_expr(body, graph);
                self.emit_line(&format!("_a({}, {});", container_var, child_var));
                self.pop_scope();
                self.indent -= 1;
                self.emit_line("});");
                self.indent -= 1;
                self.emit_line("});");

                container_var
            }

            // ForIn reactive list: `for item in items -> body`
            Expr::ForIn { item, index, iter, body } => {
                let container_var = self.next_node_id();
                let iter_js = self.emit_expr(iter);
                let iter_var = self.next_node_id(); // unique name to avoid shadowing

                self.emit_line(&format!("const {} = _c('div');", container_var));
                self.emit_line(&format!("_cn({}, 'ds-for-list');", container_var));

                // Reactive effect that re-renders the list when the iterable changes
                self.emit_line("DS.effect(() => {");
                self.indent += 1;
                self.emit_line(&format!("const {} = {};", iter_var, iter_js));
                self.emit_line(&format!("{}.innerHTML = '';", container_var));

                let idx_var = index.as_deref().unwrap_or("_idx");
                self.emit_line(&format!("const __list = ({0} && {0}.value !== undefined) ? {0}.value : (Array.isArray({0}) ? {0} : []);", iter_var));
                self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{"));
                self.indent += 1;

                // Push scope for loop variables
                let mut scope_vars: Vec<&str> = vec![item.as_str()];
                if let Some(idx) = index {
                    scope_vars.push(idx.as_str());
                }
                self.push_scope(&scope_vars);

                let child_var = self.emit_view_expr(body, graph);
                self.emit_line(&format!("_a({}, {});", container_var, child_var));

                // Pop scope
                self.pop_scope();

                self.indent -= 1;
                self.emit_line("});");
                self.indent -= 1;
                self.emit_line("});");

                container_var
            }

            // Component usage: `Button { label: "hello" }` or `Card { title: "x" } [ children ]`
            Expr::ComponentUse { name, props, children } => {
                let node_var = self.next_node_id();
                let props_js: Vec<String> = props.iter()
                    .map(|(k, v)| {
                        // Detect callback props (assignments, method calls, blocks)
                        let is_callback = matches!(v,
                            Expr::Assign(_, _, _) | Expr::MethodCall(_, _, _) | Expr::Block(_)
                        );
                        if is_callback {
                            let handler_js = self.emit_event_handler_expr(v);
                            format!("{}: () => {{ {}; DS.flush() }}", k, handler_js)
                        } else {
                            let js = self.emit_expr(v);
                            // If prop expression references signals, wrap as reactive getter
                            if js.contains(".value") {
                                format!("{}: () => {}", k, js)
                            } else {
                                format!("{}: {}", k, js)
                            }
                        }
                    })
                    .collect();
                if children.is_empty() {
                    self.emit_line(&format!("const {} = DS_{}({{ {} }});", node_var, name, props_js.join(", ")));
                } else {
                    // Build children factory function
                    let children_fn = self.next_node_id();
                    self.emit_line(&format!("function {}() {{", children_fn));
                    self.indent += 1;
                    let container = self.next_node_id();
                    self.emit_line(&format!("const {} = document.createDocumentFragment();", container));
                    for child in children {
                        let child_var = self.emit_view_expr(child, graph);
                        self.emit_line(&format!("_a({}, {});", container, child_var));
                    }
                    self.emit_line(&format!("return {};", container));
                    self.indent -= 1;
                    self.emit_line("}");
                    self.emit_line(&format!("const {} = DS_{}({{ {} }}, {});", node_var, name, props_js.join(", "), children_fn));
                }
                node_var
            }

            Expr::Match(scrutinee, arms) => {
                let container_var = self.next_node_id();
                self.emit_line(&format!("const {} = _c('div');", container_var));
                self.emit_line(&format!("_cn({}, 'ds-match');", container_var));

                let scrutinee_js = self.emit_expr(scrutinee);
                self.emit_line("DS.effect(() => {");
                self.indent += 1;
                self.emit_line(&format!("const val = {};", scrutinee_js));
                self.emit_line(&format!("{}.innerHTML = '';", container_var));

                for (i, arm) in arms.iter().enumerate() {
                    let prefix = if i == 0 { "if" } else { "} else if" };
                    let pattern_js = self.emit_pattern_check(&arm.pattern, "val");
                    self.emit_line(&format!("{} ({}) {{", prefix, pattern_js));
                    self.indent += 1;
                    let child = self.emit_view_expr(&arm.body, graph);
                    self.emit_line(&format!("_a({}, {});", container_var, child));
                    self.indent -= 1;
                }
                self.emit_line("}");
                self.indent -= 1;
                self.emit_line("});");

                container_var
            }


            // Slot: render children passed to this component
            Expr::Slot => {
                let node_var = self.next_node_id();
                self.emit_line(&format!(
                    "const {} = __children ? __children() : document.createComment('empty-slot');",
                    node_var
                ));
                node_var
            }

            // Fallback: just create a text node
            _ => {
                let node_var = self.next_node_id();
                let js = self.emit_expr(expr);
                self.emit_line(&format!(
                    "const {} = document.createTextNode({});",
                    node_var, js
                ));
                node_var
            }
        }
    }

    fn emit_handler(&mut self, handler: &OnHandler) {
        // Push handler param into scope so it's treated as a local variable
        // (prevents emit_expr from appending .value to the param name)
        if let Some(param) = &handler.param {
            self.push_scope(&[param.as_str()]);
        }

        let handler_js = self.emit_event_handler_expr(&handler.body);

        if let Some(param) = &handler.param {
            self.pop_scope();
        }

        // Standard DOM events: wire to document.addEventListener
        let dom_events = ["keydown", "keyup", "keypress", "mousemove",
                          "mousedown", "mouseup", "resize", "scroll",
                          "touchstart", "touchend", "touchmove"];

        if dom_events.contains(&handler.event.as_str()) {
            if let Some(param) = &handler.param {
                self.emit_line(&format!(
                    "document.addEventListener('{}', ({}) => {{ {}; DS.flush(); }});",
                    handler.event, param, handler_js
                ));
            } else {
                self.emit_line(&format!(
                    "document.addEventListener('{}', () => {{ {}; DS.flush(); }});",
                    handler.event, handler_js
                ));
            }
        } else {
            // Custom DreamStack events: use the event bus
            if let Some(param) = &handler.param {
                self.emit_line(&format!(
                    "DS.onEvent('{}', ({}) => {{ {}; DS.flush(); }});",
                    handler.event, param, handler_js
                ));
            } else {
                self.emit_line(&format!(
                    "DS.onEvent('{}', () => {{ {}; DS.flush(); }});",
                    handler.event, handler_js
                ));
            }
        }
    }

    // ── Expression emitters ─────────────────────────────

    pub fn emit_expr(&self, expr: &Expr) -> String {
        match expr {
            Expr::IntLit(n) => format!("{n}"),
            Expr::FloatLit(n) => format!("{n}"),
            Expr::BoolLit(b) => format!("{b}"),
            Expr::StringLit(s) => {
                if s.segments.len() == 1 {
                    if let StringSegment::Literal(text) = &s.segments[0] {
                        return format!("\"{}\"", text.replace('"', "\\\""));
                    }
                }
                // Template literal with interpolation
                let mut parts = Vec::new();
                for seg in &s.segments {
                    match seg {
                        StringSegment::Literal(text) => parts.push(text.clone()),
                        StringSegment::Interpolation(expr) => {
                            parts.push(format!("${{{}}}", self.emit_expr(expr)));
                        }
                    }
                }
                format!("`{}`", parts.join(""))
            }
            Expr::Ident(name) if self.is_local_var(name) => {
                format!("{name}")
            }
            Expr::Ident(name) => {
                if self.is_local_var(name) {
                    // Local variables (loop vars, function params) — no .value
                    format!("{name}")
                } else {
                    format!("{name}.value")
                }
            }
            Expr::DotAccess(base, field) => {
                let base_js = self.emit_expr(base);
                format!("{base_js}.{field}")
            }
            Expr::Index(base, index) => {
                let base_js = self.emit_expr(base);
                let idx_js = self.emit_expr(index);
                format!("{base_js}[{idx_js}]")
            }
            Expr::BinOp(left, op, right) => {
                let l = self.emit_expr(left);
                let r = self.emit_expr(right);
                match op {
                    BinOp::Div => format!("Math.trunc({l} / {r})"),
                    _ => {
                        let op_str = match op {
                            BinOp::Add => "+",
                            BinOp::Sub => "-",
                            BinOp::Mul => "*",
                            BinOp::Div => unreachable!(),
                            BinOp::Mod => "%",
                            BinOp::Eq => "===",
                            BinOp::Neq => "!==",
                            BinOp::Lt => "<",
                            BinOp::Gt => ">",
                            BinOp::Lte => "<=",
                            BinOp::Gte => ">=",
                            BinOp::And => "&&",
                            BinOp::Or => "||",
                        };
                        format!("({l} {op_str} {r})")
                    }
                }
            }
            Expr::UnaryOp(op, inner) => {
                let inner_js = self.emit_expr(inner);
                match op {
                    UnaryOp::Neg => format!("(-{inner_js})"),
                    UnaryOp::Not => format!("(!{inner_js})"),
                }
            }
            Expr::Call(name, args) => {
                let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();

                // ── Built-in function dispatch ──
                match name.as_str() {
                    // Navigation & springs
                    "navigate" => format!("DS.navigate({})", args_js.join(", ")),
                    "spring" => format!("DS.spring({})", args_js.join(", ")),

                    // ── Array operations ──
                    "len" if args.len() == 1 => format!("{}.length", args_js[0]),
                    "push" if args.len() == 2 => {
                        // push mutates array, need to get signal root name
                        let root = self.get_signal_root_name(&args[0]);
                        format!("(() => {{ {}.push({}); {root}.value = [...{root}.value]; DS._streamDiff(\"{root}\", {root}.value); return {root}.value; }})()",
                            args_js[0], args_js[1], )
                    }
                    "pop" if args.len() == 1 => {
                        let root = self.get_signal_root_name(&args[0]);
                        format!("(() => {{ const _v = {}.pop(); {root}.value = [...{root}.value]; DS._streamDiff(\"{root}\", {root}.value); return _v; }})()",
                            args_js[0])
                    }
                    "filter" if args.len() == 2 => format!("{}.filter({})", args_js[0], args_js[1]),
                    "map" if args.len() == 2 => format!("{}.map({})", args_js[0], args_js[1]),
                    "concat" if args.len() == 2 => format!("[...{}, ...{}]", args_js[0], args_js[1]),
                    "contains" if args.len() == 2 => format!("{}.includes({})", args_js[0], args_js[1]),
                    "reverse" if args.len() == 1 => {
                        let root = self.get_signal_root_name(&args[0]);
                        format!("(() => {{ {}.reverse(); {root}.value = [...{root}.value]; DS._streamDiff(\"{root}\", {root}.value); return {root}.value; }})()",
                            args_js[0])
                    }
                    "slice" if args.len() >= 2 => format!("{}.slice({})", args_js[0], args_js[1..].join(", ")),
                    "indexOf" if args.len() == 2 => format!("{}.indexOf({})", args_js[0], args_js[1]),
                    "find" if args.len() == 2 => format!("{}.find({})", args_js[0], args_js[1]),
                    "some" if args.len() == 2 => format!("{}.some({})", args_js[0], args_js[1]),
                    "every" if args.len() == 2 => format!("{}.every({})", args_js[0], args_js[1]),
                    "flat" if args.len() == 1 => format!("{}.flat()", args_js[0]),
                    "sort" if args.len() >= 1 => {
                        let root = self.get_signal_root_name(&args[0]);
                        if args.len() == 2 {
                            format!("(() => {{ {}.sort({}); {root}.value = [...{root}.value]; return {root}.value; }})()",
                                args_js[0], args_js[1])
                        } else {
                            format!("(() => {{ {}.sort(); {root}.value = [...{root}.value]; return {root}.value; }})()",
                                args_js[0])
                        }
                    }

                    // ── Math operations ──
                    "abs" if args.len() == 1 => format!("Math.abs({})", args_js[0]),
                    "min" => format!("Math.min({})", args_js.join(", ")),
                    "max" => format!("Math.max({})", args_js.join(", ")),
                    "floor" if args.len() == 1 => format!("Math.floor({})", args_js[0]),
                    "ceil" if args.len() == 1 => format!("Math.ceil({})", args_js[0]),
                    "round" if args.len() == 1 => format!("Math.round({})", args_js[0]),
                    "random" if args.is_empty() => "Math.random()".to_string(),
                    "sqrt" if args.len() == 1 => format!("Math.sqrt({})", args_js[0]),
                    "pow" if args.len() == 2 => format!("Math.pow({}, {})", args_js[0], args_js[1]),
                    "sin" if args.len() == 1 => format!("Math.sin({})", args_js[0]),
                    "cos" if args.len() == 1 => format!("Math.cos({})", args_js[0]),
                    "tan" if args.len() == 1 => format!("Math.tan({})", args_js[0]),
                    "atan2" if args.len() == 2 => format!("Math.atan2({}, {})", args_js[0], args_js[1]),
                    "clamp" if args.len() == 3 => format!("Math.min(Math.max({}, {}), {})", args_js[0], args_js[1], args_js[2]),
                    "lerp" if args.len() == 3 => format!("({} + ({} - {}) * {})", args_js[0], args_js[1], args_js[0], args_js[2]),

                    // ── String operations ──
                    "split" if args.len() == 2 => format!("{}.split({})", args_js[0], args_js[1]),
                    "join" if args.len() == 2 => format!("{}.join({})", args_js[0], args_js[1]),
                    "trim" if args.len() == 1 => format!("{}.trim()", args_js[0]),
                    "upper" if args.len() == 1 => format!("{}.toUpperCase()", args_js[0]),
                    "lower" if args.len() == 1 => format!("{}.toLowerCase()", args_js[0]),
                    "replace" if args.len() == 3 => format!("{}.replace({}, {})", args_js[0], args_js[1], args_js[2]),
                    "starts_with" if args.len() == 2 => format!("{}.startsWith({})", args_js[0], args_js[1]),
                    "ends_with" if args.len() == 2 => format!("{}.endsWith({})", args_js[0], args_js[1]),
                    "char_at" if args.len() == 2 => format!("{}.charAt({})", args_js[0], args_js[1]),
                    "substring" if args.len() == 3 => format!("{}.substring({}, {})", args_js[0], args_js[1], args_js[2]),

                    // ── Conversion ──
                    "int" if args.len() == 1 => format!("parseInt({})", args_js[0]),
                    "float" if args.len() == 1 => format!("parseFloat({})", args_js[0]),
                    "string" if args.len() == 1 => format!("String({})", args_js[0]),
                    "bool" if args.len() == 1 => format!("Boolean({})", args_js[0]),

                    // ── Console / debug ──
                    "log" => format!("console.log({})", args_js.join(", ")),
                    "debug" => format!("console.debug({})", args_js.join(", ")),
                    "warn" => format!("console.warn({})", args_js.join(", ")),

                    // ── Timer ──
                    "delay" if args.len() == 2 => format!("setTimeout(() => {{ {} }}, {})", args_js[0], args_js[1]),

                    // ── Audio ──
                    "play_tone" if args.len() == 2 => format!("DS._playTone({}, {})", args_js[0], args_js[1]),
                    "play_tone" if args.len() == 3 => format!("DS._playTone({}, {}, {})", args_js[0], args_js[1], args_js[2]),
                    "play_noise" if args.len() == 1 => format!("DS._playNoise({})", args_js[0]),
                    "play_noise" if args.len() == 2 => format!("DS._playNoise({}, {})", args_js[0], args_js[1]),

                    // ── Fallback: user-defined function ──
                    _ => format!("{}({})", name, args_js.join(", ")),
                }
            }
            Expr::If(cond, then_b, else_b) => {
                let c = self.emit_expr(cond);
                let t = self.emit_expr(then_b);
                let e = self.emit_expr(else_b);
                format!("({c} ? {t} : {e})")
            }
            Expr::Lambda(params, body) => {
                let body_js = self.emit_expr(body);
                format!("({}) => {}", params.join(", "), body_js)
            }
            Expr::Record(fields) => {
                let fields_js: Vec<String> = fields
                    .iter()
                    .map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
                    .collect();
                format!("{{ {} }}", fields_js.join(", "))
            }
            Expr::List(items) => {
                let items_js: Vec<String> = items.iter().map(|i| self.emit_expr(i)).collect();
                format!("[{}]", items_js.join(", "))
            }
            Expr::StreamFrom { source, select, .. } => {
                if select.is_empty() {
                    format!("DS._connectStream(\"{}\")", source)
                } else {
                    let select_js: Vec<String> = select.iter().map(|s| format!("\"{}\"", s)).collect();
                    format!("DS._connectStream(\"{}\", [{}])", source, select_js.join(","))
                }
            }
            Expr::Await(inner) => {
                let inner_js = self.emit_expr(inner);
                format!("(await {})", inner_js)
            }
            Expr::Merge(sources) => {
                let sources_js: Vec<String> = sources.iter().map(|s| self.emit_expr(s)).collect();
                format!("DS._mergeStreams([{}])", sources_js.join(", "))
            }
            Expr::Pipe(left, right) => {
                let left_js = self.emit_expr(left);
                match right.as_ref() {
                    // Named operator with args: `stream | debounce(100)`
                    Expr::Call(name, args) => {
                        let runtime_fn = match name.as_str() {
                            "map" => "DS._streamMap",
                            "filter" => "DS._streamFilter",
                            "debounce" => "DS._streamDebounce",
                            "distinct" => "DS._streamDistinct",
                            "throttle" => "DS._streamThrottle",
                            other => other,
                        };
                        if args.is_empty() {
                            format!("{}({})", runtime_fn, left_js)
                        } else {
                            let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
                            format!("{}({}, {})", runtime_fn, left_js, args_js.join(", "))
                        }
                    }
                    // Bare operator: `stream | distinct`
                    Expr::Ident(name) => {
                        let runtime_fn = match name.as_str() {
                            "map" => "DS._streamMap",
                            "filter" => "DS._streamFilter",
                            "debounce" => "DS._streamDebounce",
                            "distinct" => "DS._streamDistinct",
                            "throttle" => "DS._streamThrottle",
                            other => other,
                        };
                        format!("{}({})", runtime_fn, left_js)
                    }
                    // Lambda: `stream | (x -> x.count)` becomes map
                    Expr::Lambda(params, body) => {
                        let body_js = self.emit_expr(body);
                        let fn_js = format!("({}) => {}", params.join(", "), body_js);
                        format!("DS._streamMap({}, {})", left_js, fn_js)
                    }
                    // Generic: right(left) — function application
                    _ => {
                        let right_js = self.emit_expr(right);
                        format!("{}({})", right_js, left_js)
                    }
                }
            }
            Expr::Match(scrutinee, arms) => {
                let scrut_js = self.emit_expr(scrutinee);
                if arms.is_empty() {
                    return "null".to_string();
                }
                // Build chained ternary: (s === "a" ? exprA : s === "b" ? exprB : defaultExpr)
                let mut parts = Vec::new();
                let mut default_js = "null".to_string();
                for arm in arms {
                    let body_js = self.emit_expr(&arm.body);
                    match &arm.pattern {
                        Pattern::Wildcard | Pattern::Ident(_) => {
                            default_js = body_js;
                        }
                        Pattern::Literal(lit_expr) => {
                            let lit_js = self.emit_expr(lit_expr);
                            parts.push(format!("({scrut_js} === {lit_js} ? {body_js}"));
                        }
                        Pattern::Constructor(name, _) => {
                            parts.push(format!("({scrut_js}.tag === \"{name}\" ? {body_js}"));
                        }
                        Pattern::IntLiteral(n) => {
                            parts.push(format!("({scrut_js} === {n} ? {body_js}"));
                        }
                        Pattern::BoolLiteral(b) => {
                            parts.push(format!("({scrut_js} === {b} ? {body_js}"));
                        }
                        Pattern::Tuple(elements) => {
                            // Tuple match: check Array.isArray and element count
                            let checks: Vec<String> = elements.iter().enumerate()
                                .filter_map(|(i, p)| match p {
                                    Pattern::Wildcard => None,
                                    _ => Some(self.emit_pattern_check(p, &format!("{scrut_js}[{i}]"))),
                                })
                                .collect();
                            let cond = if checks.is_empty() {
                                format!("Array.isArray({scrut_js})")
                            } else {
                                format!("(Array.isArray({scrut_js}) && {})", checks.join(" && "))
                            };
                            parts.push(format!("({cond} ? {body_js}"));
                        }
                    }
                }
                if parts.is_empty() {
                    default_js
                } else {
                    let close_parens = ")".repeat(parts.len());
                    format!("{} : {}{}", parts.join(" : "), default_js, close_parens)
                }
            }
            Expr::ComponentUse { name, props, children: _ } => {
                let props_js: Vec<String> = props
                    .iter()
                    .map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
                    .collect();
                format!("DS_{}({{ {} }})", name, props_js.join(", "))
            }
            Expr::MethodCall(obj, method, args) => {
                let obj_js = self.emit_expr(obj);
                let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
                format!("{}.{}({})", obj_js, method, args_js.join(", "))
            }
            Expr::RawString(text) => {
                format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n"))
            }
            _ => "null".to_string(),
        }
    }

    /// Emit an event handler expression (assignment, etc.)
    fn emit_event_handler_expr(&self, expr: &Expr) -> String {
        match expr {
            Expr::Assign(target, op, value) => {
                let value_js = self.emit_expr(value);
                // Determine the assignment target and root signal for streaming
                let (assign, root_for_diff) = match target.as_ref() {
                    Expr::Ident(name) => {
                        let a = match op {
                            AssignOp::Set => format!("{name}.value = {value_js}"),
                            AssignOp::AddAssign => format!("{name}.value += {value_js}"),
                            AssignOp::SubAssign => format!("{name}.value -= {value_js}"),
                            AssignOp::MulAssign => format!("{name}.value *= {value_js}"),
                            AssignOp::DivAssign => format!("{name}.value /= {value_js}"),
                        };
                        (a, name.clone())
                    }
                    Expr::DotAccess(base, field) => {
                        let base_str = self.emit_expr(base);
                        let target_str = format!("{base_str}.{field}");
                        let a = match op {
                            AssignOp::Set => format!("{target_str} = {value_js}"),
                            AssignOp::AddAssign => format!("{target_str} += {value_js}"),
                            AssignOp::SubAssign => format!("{target_str} -= {value_js}"),
                            AssignOp::MulAssign => format!("{target_str} *= {value_js}"),
                            AssignOp::DivAssign => format!("{target_str} /= {value_js}"),
                        };
                        (a, base_str)
                    }
                    Expr::Index(base, index) => {
                        let base_str = self.emit_expr(base);
                        let idx_str = self.emit_expr(index);
                        let target_str = format!("{base_str}[{idx_str}]");
                        let root = match base.as_ref() {
                            Expr::Ident(name) => name.clone(),
                            _ => base_str.clone(),
                        };
                        let a = match op {
                            AssignOp::Set => format!("{target_str} = {value_js}"),
                            AssignOp::AddAssign => format!("{target_str} += {value_js}"),
                            AssignOp::SubAssign => format!("{target_str} -= {value_js}"),
                            AssignOp::MulAssign => format!("{target_str} *= {value_js}"),
                            AssignOp::DivAssign => format!("{target_str} /= {value_js}"),
                        };
                        (a, root)
                    }
                    _ => {
                        let s = self.emit_expr(target);
                        let a = match op {
                            AssignOp::Set => format!("{s} = {value_js}"),
                            AssignOp::AddAssign => format!("{s} += {value_js}"),
                            AssignOp::SubAssign => format!("{s} -= {value_js}"),
                            AssignOp::MulAssign => format!("{s} *= {value_js}"),
                            AssignOp::DivAssign => format!("{s} /= {value_js}"),
                        };
                        (a, s)
                    }
                };
                // For indexed mutations, re-trigger the signal to notify the reactive system
                match target.as_ref() {
                    Expr::Index(_, _) => {
                        // Mutate in-place then re-assign to trigger signal change detection
                        format!(
                            "{}; {}.value = [...{}.value]; DS._streamDiff(\"{}\", {}.value)",
                            assign, root_for_diff, root_for_diff, root_for_diff, root_for_diff
                        )
                    }
                    _ => {
                        // Stream diff: broadcast signal change if streaming is active
                        format!("{}; DS._streamDiff(\"{}\", {}.value)", assign, root_for_diff, root_for_diff)
                    }
                }
            }
            // Method calls on arrays: items.push(x), items.remove(idx), items.filter(fn)
            Expr::MethodCall(obj, method, args) => {
                let obj_js = self.emit_expr(obj);
                let signal_name = match obj.as_ref() {
                    Expr::Ident(name) => name.clone(),
                    _ => obj_js.clone(),
                };
                let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
                match method.as_str() {
                    "push" => {
                        // items.push(x) → items.value = [...items.value, x]
                        let val = args_js.first().map(|s| s.as_str()).unwrap_or("undefined");
                        format!(
                            "{sig}.value = [...{sig}.value, {val}]; DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name, val = val
                        )
                    }
                    "remove" => {
                        // items.remove(idx) → items.value = items.value.filter((_, i) => i !== idx)
                        let idx = args_js.first().map(|s| s.as_str()).unwrap_or("0");
                        format!(
                            "{sig}.value = {sig}.value.filter((_, _i) => _i !== {idx}); DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name, idx = idx
                        )
                    }
                    "pop" => {
                        // items.pop() → items.value = items.value.slice(0, -1)
                        format!(
                            "{sig}.value = {sig}.value.slice(0, -1); DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name
                        )
                    }
                    "clear" => {
                        // items.clear() → items.value = []
                        format!(
                            "{sig}.value = []; DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name
                        )
                    }
                    "insert" => {
                        // items.insert(idx, val) → splice
                        let idx = args_js.first().map(|s| s.as_str()).unwrap_or("0");
                        let val = args_js.get(1).map(|s| s.as_str()).unwrap_or("undefined");
                        format!(
                            "{{ const _a = [...{sig}.value]; _a.splice({idx}, 0, {val}); {sig}.value = _a; DS._streamDiff(\"{sig}\", {sig}.value) }}",
                            sig = signal_name, idx = idx, val = val
                        )
                    }
                    "sort" => {
                        // items.sort() → sort a copy
                        format!(
                            "{sig}.value = [...{sig}.value].sort(); DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name
                        )
                    }
                    "reverse" => {
                        // items.reverse() → reverse a copy
                        format!(
                            "{sig}.value = [...{sig}.value].reverse(); DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name
                        )
                    }
                    "filter" => {
                        // items.filter(fn) → filter in place
                        let fn_js = args_js.first().map(|s| s.as_str()).unwrap_or("() => true");
                        format!(
                            "{sig}.value = {sig}.value.filter({fn_js}); DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name, fn_js = fn_js
                        )
                    }
                    "map" => {
                        // items.map(fn) → map in place
                        let fn_js = args_js.first().map(|s| s.as_str()).unwrap_or("x => x");
                        format!(
                            "{sig}.value = {sig}.value.map({fn_js}); DS._streamDiff(\"{sig}\", {sig}.value)",
                            sig = signal_name, fn_js = fn_js
                        )
                    }
                    _ => {
                        // Generic method call: obj.method(args)
                        format!("{}.{}({})", obj_js, method, args_js.join(", "))
                    }
                }
            }
            Expr::Block(exprs) => {
                let stmts: Vec<String> = exprs.iter().map(|e| self.emit_event_handler_expr(e)).collect();
                stmts.join("; ")
            }
            _ => {
                // For plain identifiers that might be callback props, call them
                if let Expr::Ident(name) = expr {
                    format!("if (typeof {} === 'function') {}()", name, name)
                } else {
                    self.emit_expr(expr)
                }
            }
        }
    }

    /// Emit a pattern matching condition check.
    fn emit_pattern_check(&self, pattern: &Pattern, scrutinee: &str) -> String {
        match pattern {
            Pattern::Wildcard => "true".to_string(),
            Pattern::Ident(name) => {
                // Bind: always true, but assign
                format!("(({name} = {scrutinee}), true)")
            }
            Pattern::Constructor(name, fields) => {
                if fields.is_empty() {
                    format!("{scrutinee}.tag === '{name}'")
                } else {
                    // Constructor with bindings: check tag and bind payload
                    // e.g., Ok(v) → (s.tag === 'Ok' && ((v = s.value), true))
                    let mut conditions = vec![format!("{scrutinee}.tag === '{name}'")];
                    for (i, field) in fields.iter().enumerate() {
                        let accessor = if fields.len() == 1 {
                            format!("{scrutinee}.value")
                        } else {
                            format!("{scrutinee}.value[{i}]")
                        };
                        conditions.push(self.emit_pattern_check(field, &accessor));
                    }
                    format!("({})", conditions.join(" && "))
                }
            }
            Pattern::Literal(expr) => {
                let val = self.emit_expr(expr);
                format!("{scrutinee} === {val}")
            }
            Pattern::Tuple(elements) => {
                // Tuple destructuring: check Array.isArray and bind elements
                let mut conditions = vec![format!("Array.isArray({scrutinee})")];
                for (i, elem) in elements.iter().enumerate() {
                    conditions.push(self.emit_pattern_check(elem, &format!("{scrutinee}[{i}]")));
                }
                format!("({})", conditions.join(" && "))
            }
            Pattern::IntLiteral(n) => {
                format!("{scrutinee} === {n}")
            }
            Pattern::BoolLiteral(b) => {
                format!("{scrutinee} === {b}")
            }
        }
    }

    // ── Helpers ──────────────────────────────────────────

    fn next_node_id(&mut self) -> String {
        let id = self.node_id_counter;
        self.node_id_counter += 1;
        format!("n{id}")
    }

    /// Extract the root signal name from an expression.
    /// `Ident("todos")` → "todos", `DotAccess(Ident("a"), "b")` → "a", fallback → "_arr"
    fn get_signal_root_name(&self, expr: &Expr) -> String {
        match expr {
            Expr::Ident(name) => name.clone(),
            Expr::DotAccess(base, _) => self.get_signal_root_name(base),
            Expr::Index(base, _) => self.get_signal_root_name(base),
            _ => "_arr".to_string(),
        }
    }

    fn emit_line(&mut self, line: &str) {
        for _ in 0..self.indent {
            self.output.push_str("  ");
        }
        self.output.push_str(line);
        self.output.push('\n');
    }

    fn find_let_expr<'a>(&self, program: &'a Program, name: &str) -> Option<&'a Expr> {
        for decl in &program.declarations {
            if let Declaration::Let(let_decl) = decl {
                if let_decl.name == name {
                    return Some(&let_decl.value);
                }
            }
        }
        None
    }

    /// Convert a predicate expression from a `where` clause into a JavaScript boolean expression.
    /// The `value` identifier is replaced with `signal_name.value` to read the signal's current value.
    fn predicate_to_js(expr: &Expr, signal_name: &str) -> String {
        match expr {
            Expr::Ident(name) if name == "value" => format!("{}.value", signal_name),
            Expr::Ident(name) => name.clone(),
            Expr::IntLit(n) => format!("{}", n),
            Expr::FloatLit(f) => format!("{}", f),
            Expr::BoolLit(b) => format!("{}", b),
            Expr::StringLit(s) => {
                let text: String = s.segments.iter().map(|seg| match seg {
                    StringSegment::Literal(l) => l.clone(),
                    _ => String::new(),
                }).collect();
                format!("\"{}\"", text)
            }
            Expr::BinOp(left, op, right) => {
                let l = Self::predicate_to_js(left, signal_name);
                let r = Self::predicate_to_js(right, signal_name);
                match op {
                    BinOp::Div => format!("Math.trunc({} / {})", l, r),
                    _ => {
                        let op_str = match op {
                            BinOp::Gt => ">",
                            BinOp::Gte => ">=",
                            BinOp::Lt => "<",
                            BinOp::Lte => "<=",
                            BinOp::Eq => "===",
                            BinOp::Neq => "!==",
                            BinOp::And => "&&",
                            BinOp::Or => "||",
                            BinOp::Add => "+",
                            BinOp::Sub => "-",
                            BinOp::Mul => "*",
                            BinOp::Div => unreachable!(),
                            BinOp::Mod => "%",
                        };
                        format!("({} {} {})", l, op_str, r)
                    }
                }
            }
            Expr::UnaryOp(UnaryOp::Not, inner) => {
                format!("!({})", Self::predicate_to_js(inner, signal_name))
            }
            Expr::UnaryOp(UnaryOp::Neg, inner) => {
                format!("-({})", Self::predicate_to_js(inner, signal_name))
            }
            Expr::Call(name, args) => {
                let js_args: Vec<String> = args.iter()
                    .map(|a| Self::predicate_to_js(a, signal_name))
                    .collect();
                // Map common predicate functions to JS equivalents
                match name.as_str() {
                    "len" => format!("{}.length", js_args.first().unwrap_or(&"null".to_string())),
                    "contains" if js_args.len() == 2 => format!("{}.includes({})", js_args[0], js_args[1]),
                    _ => format!("{}({})", name, js_args.join(", ")),
                }
            }
            _ => format!("{}.value", signal_name), // fallback
        }
    }

    fn is_signal_ref(&self, expr: &str) -> bool {
        // Must start with a letter/underscore (not a digit) and contain only ident chars
        !expr.is_empty()
            && expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
            && expr.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
    }

    /// Collect all element names referenced in a layout expression.
    fn collect_layout_elements(expr: &LayoutExpr, out: &mut std::collections::HashSet<String>) {
        match expr {
            LayoutExpr::Prop(el, _) => { out.insert(el.clone()); }
            LayoutExpr::Add(a, b) | LayoutExpr::Sub(a, b) | LayoutExpr::Mul(a, b) => {
                Self::collect_layout_elements(a, out);
                Self::collect_layout_elements(b, out);
            }
            LayoutExpr::Const(_) => {}
        }
    }

    /// Convert a layout expression to a JavaScript expression string.
    fn layout_expr_to_js(expr: &LayoutExpr) -> String {
        match expr {
            LayoutExpr::Prop(el, prop) => format!("{el}.{prop}"),
            LayoutExpr::Const(v) => {
                if *v == (*v as i64) as f64 {
                    format!("{}", *v as i64)
                } else {
                    format!("{v}")
                }
            }
            LayoutExpr::Add(a, b) => format!("({} + {})", Self::layout_expr_to_js(a), Self::layout_expr_to_js(b)),
            LayoutExpr::Sub(a, b) => format!("({} - {})", Self::layout_expr_to_js(a), Self::layout_expr_to_js(b)),
            LayoutExpr::Mul(a, b) => format!("({} * {})", Self::layout_expr_to_js(a), Self::layout_expr_to_js(b)),
        }
    }

    /// Emit a physics scene — canvas + WASM physics engine + animation loop
    fn emit_scene(&mut self, container: &Container, graph: &SignalGraph) -> String {
        let wrapper_var = self.next_node_id();
        let canvas_var = self.next_node_id();

        // Extract scene props
        let mut scene_width = "800".to_string();
        let mut scene_height = "500".to_string();
        let mut gravity_x_expr = None;
        let mut gravity_y_expr = None;
        let mut gx_is_signal = false;
        let mut gy_is_signal = false;

        for (key, val) in &container.props {
            match key.as_str() {
                "width" => scene_width = self.emit_expr(val),
                "height" => scene_height = self.emit_expr(val),
                "gravity_x" => {
                    gx_is_signal = matches!(val, Expr::Ident(_));
                    gravity_x_expr = Some(if let Expr::Ident(name) = val {
                        name.clone()
                    } else {
                        self.emit_expr(val)
                    });
                }
                "gravity_y" => {
                    gy_is_signal = matches!(val, Expr::Ident(_));
                    gravity_y_expr = Some(if let Expr::Ident(name) = val {
                        name.clone()
                    } else {
                        self.emit_expr(val)
                    });
                }
                _ => {}
            }
        }

        // Create wrapper div
        self.emit_line(&format!("const {} = _c('div');", wrapper_var));
        self.emit_line(&format!("_cn({}, 'ds-scene-wrapper');", wrapper_var));

        // Create canvas
        let w_val = if graph.name_to_id.contains_key(&scene_width) || self.is_signal_ref(&scene_width) {
            format!("{}.value", scene_width)
        } else {
            scene_width.clone()
        };
        let h_val = if graph.name_to_id.contains_key(&scene_height) || self.is_signal_ref(&scene_height) {
            format!("{}.value", scene_height)
        } else {
            scene_height.clone()
        };

        self.emit_line(&format!("const {} = _c('canvas');", canvas_var));
        self.emit_line(&format!("{}.width = {};", canvas_var, w_val));
        self.emit_line(&format!("{}.height = {};", canvas_var, h_val));
        self.emit_line(&format!("{}.style.width = {} + 'px';", canvas_var, w_val));
        self.emit_line(&format!("{}.style.height = {} + 'px';", canvas_var, h_val));
        self.emit_line(&format!("{}.style.borderRadius = '16px';", canvas_var));
        self.emit_line(&format!("{}.style.background = 'rgba(255,255,255,0.02)';", canvas_var));
        self.emit_line(&format!("{}.style.border = '1px solid rgba(255,255,255,0.06)';", canvas_var));
        self.emit_line(&format!("{}.style.cursor = 'pointer';", canvas_var));
        self.emit_line(&format!("_a({}, {});", wrapper_var, canvas_var));

        // Async IIFE to load WASM and set up physics
        self.emit_line("(async () => {");
        self.indent += 1;

        self.emit_line("const { default: init, PhysicsWorld } = await import('./pkg/ds_physics.js');");
        self.emit_line("await init();");
        self.emit_line(&format!("const _sceneW = {};", w_val));
        self.emit_line(&format!("const _sceneH = {};", h_val));
        self.emit_line("const _world = new PhysicsWorld(_sceneW, _sceneH);");
        self.emit_line(&format!("const _ctx = {}.getContext('2d');", canvas_var));
        self.emit_line("DS._streamCanvas = _ctx;");

        // Create bodies from child elements
        for child in &container.children {
            if let Expr::Element(element) = child {
                match element.tag.as_str() {
                    "circle" => self.emit_scene_circle(element, graph),
                    "rect" => self.emit_scene_rect(element, graph),
                    _ => {} // skip unknown elements
                }
            }
        }

        // Wire reactive gravity
        if let Some(ref gx) = gravity_x_expr {
            if gx_is_signal {
                if let Some(ref gy) = gravity_y_expr {
                    if gy_is_signal {
                        self.emit_line(&format!(
                            "DS.effect(() => {{ _world.set_gravity({}.value, {}.value); }});",
                            gx, gy
                        ));
                    } else {
                        self.emit_line(&format!(
                            "DS.effect(() => {{ _world.set_gravity({}.value, {}); }});",
                            gx, gy
                        ));
                    }
                } else {
                    self.emit_line(&format!(
                        "DS.effect(() => {{ _world.set_gravity({}.value, 980); }});",
                        gx
                    ));
                }
            }
        } else if let Some(ref gy) = gravity_y_expr {
            if gy_is_signal {
                self.emit_line(&format!(
                    "DS.effect(() => {{ _world.set_gravity(0, {}.value); }});",
                    gy
                ));
            } else {
                self.emit_line(&format!("_world.set_gravity(0, {});", gy));
            }
        }

        // Drag interaction
        self.emit_line("let _dragBody = -1, _dragOffX = 0, _dragOffY = 0;");
        self.emit_line("let _lastMX = 0, _lastMY = 0, _velX = 0, _velY = 0;");
        self.emit_line(&format!(
            "{}.addEventListener('mousedown', (e) => {{", canvas_var
        ));
        self.indent += 1;
        self.emit_line(&format!("const rect = {}.getBoundingClientRect();", canvas_var));
        self.emit_line("const mx = e.clientX - rect.left, my = e.clientY - rect.top;");
        self.emit_line("_lastMX = mx; _lastMY = my;");
        self.emit_line("for (let b = 0; b < _world.body_count(); b++) {");
        self.indent += 1;
        self.emit_line("const c = _world.get_body_center(b);");
        self.emit_line("const dx = mx - c[0], dy = my - c[1];");
        self.emit_line("if (Math.sqrt(dx*dx + dy*dy) < 80) { _dragBody = b; _dragOffX = dx; _dragOffY = dy; e.preventDefault(); return; }");
        self.indent -= 1;
        self.emit_line("}");
        self.indent -= 1;
        self.emit_line("});");

        self.emit_line("window.addEventListener('mousemove', (e) => {");
        self.indent += 1;
        self.emit_line(&format!("const rect = {}.getBoundingClientRect();", canvas_var));
        self.emit_line("const mx = e.clientX - rect.left, my = e.clientY - rect.top;");
        self.emit_line("_velX = mx - _lastMX; _velY = my - _lastMY; _lastMX = mx; _lastMY = my;");
        self.emit_line("if (_dragBody >= 0) { const c = _world.get_body_center(_dragBody); _world.apply_impulse(_dragBody, (mx - _dragOffX - c[0]) * 5000, (my - _dragOffY - c[1]) * 5000); }");
        self.indent -= 1;
        self.emit_line("});");

        self.emit_line("window.addEventListener('mouseup', () => { if (_dragBody >= 0) { _world.apply_impulse(_dragBody, _velX * 3000, _velY * 3000); _dragBody = -1; } });");

        // Event listener for physics impulse
        self.emit_line("DS.onEvent('physics_impulse', (data) => { if (data && data.body !== undefined) _world.apply_impulse(data.body, data.fx || 0, data.fy || 0); });");

        // Animation loop with draw
        self.emit_line("function _sceneDraw() {");
        self.indent += 1;
        self.emit_line("_ctx.clearRect(0, 0, _sceneW, _sceneH);");
        // Grid
        self.emit_line("_ctx.strokeStyle = 'rgba(255,255,255,0.02)'; _ctx.lineWidth = 1;");
        self.emit_line("for (let x = 0; x < _sceneW; x += 40) { _ctx.beginPath(); _ctx.moveTo(x,0); _ctx.lineTo(x,_sceneH); _ctx.stroke(); }");
        self.emit_line("for (let y = 0; y < _sceneH; y += 40) { _ctx.beginPath(); _ctx.moveTo(0,y); _ctx.lineTo(_sceneW,y); _ctx.stroke(); }");
        // Bodies
        self.emit_line("for (let b = 0; b < _world.body_count(); b++) {");
        self.indent += 1;
        self.emit_line("const pos = _world.get_body_positions(b), col = _world.get_body_color(b), bt = _world.get_body_type(b);");
        self.emit_line("const r = col[0]*255|0, g = col[1]*255|0, bl = col[2]*255|0, a = col[3];");
        self.emit_line("const fill = `rgba(${r},${g},${bl},${a*0.5})`, stroke = `rgba(${r},${g},${bl},${a*0.9})`, glow = `rgba(${r},${g},${bl},0.4)`;");

        // Rect
        self.emit_line("if (bt === 1 && pos.length >= 8) {");
        self.indent += 1;
        self.emit_line("_ctx.beginPath(); _ctx.moveTo(pos[0],pos[1]); _ctx.lineTo(pos[2],pos[3]); _ctx.lineTo(pos[4],pos[5]); _ctx.lineTo(pos[6],pos[7]); _ctx.closePath();");
        self.emit_line("_ctx.shadowColor = glow; _ctx.shadowBlur = 18; _ctx.fillStyle = fill; _ctx.fill(); _ctx.shadowBlur = 0; _ctx.strokeStyle = stroke; _ctx.lineWidth = 2; _ctx.stroke();");
        self.indent -= 1;
        self.emit_line("} else {");
        self.indent += 1;
        // Circle
        self.emit_line("const pc = _world.get_body_perimeter_count(b);");
        self.emit_line("if (pc >= 3) { _ctx.beginPath(); _ctx.moveTo(pos[0],pos[1]); for (let i=1;i<pc;i++) _ctx.lineTo(pos[i*2],pos[i*2+1]); _ctx.closePath();");
        self.emit_line("_ctx.shadowColor = glow; _ctx.shadowBlur = 20; _ctx.fillStyle = fill; _ctx.fill(); _ctx.shadowBlur = 0; _ctx.strokeStyle = stroke; _ctx.lineWidth = 1.5; _ctx.stroke();");
        self.emit_line("if (pos.length >= (pc+1)*2) { _ctx.beginPath(); _ctx.arc(pos[pc*2],pos[pc*2+1],2.5,0,Math.PI*2); _ctx.fillStyle = stroke; _ctx.fill(); } }");
        self.indent -= 1;
        self.emit_line("}");

        self.indent -= 1;
        self.emit_line("}");
        // Boundary
        self.emit_line("_ctx.strokeStyle = 'rgba(99,102,241,0.06)'; _ctx.lineWidth = 2; _ctx.strokeRect(1,1,_sceneW-2,_sceneH-2);");
        self.indent -= 1;
        self.emit_line("}");

        // Loop
        self.emit_line(&format!(
            "function _sceneLoop() {{ _world.step(1/60); _sceneDraw(); if (DS._streamWs) DS._streamSceneState(_world, {}, {}); requestAnimationFrame(_sceneLoop); }}",
            scene_width, scene_height
        ));
        self.emit_line("requestAnimationFrame(_sceneLoop);");

        self.indent -= 1;
        self.emit_line("})();");

        wrapper_var
    }

    /// Emit a circle body creation inside a scene
    fn emit_scene_circle(&mut self, element: &Element, _graph: &SignalGraph) {
        let mut x = "200".to_string();
        let mut y = "100".to_string();
        let mut radius = "30".to_string();
        let mut color = None;
        let mut segments = "16".to_string();
        let mut pinned = false;

        for (key, val) in &element.props {
            let js = self.emit_expr(val);
            match key.as_str() {
                "x" => x = js,
                "y" => y = js,
                "radius" => radius = js,
                "color" => color = Some(js),
                "segments" => segments = js,
                "pinned" => pinned = js == "true",
                _ => {}
            }
        }

        let body_var = self.next_node_id();
        self.emit_line(&format!(
            "const {} = _world.create_soft_circle({}, {}, {}, {}, 50);",
            body_var, x, y, radius, segments
        ));

        if let Some(c) = color {
            self.emit_scene_set_color(&body_var, c);
        }

        if pinned {
            self.emit_line(&format!("_world.pin_particle({}, true);", body_var));
        }
    }

    /// Emit a rect body creation inside a scene
    fn emit_scene_rect(&mut self, element: &Element, _graph: &SignalGraph) {
        let mut x = "200".to_string();
        let mut y = "100".to_string();
        let mut width = "60".to_string();
        let mut height = "40".to_string();
        let mut color = None;
        let mut pinned = false;

        for (key, val) in &element.props {
            let js = self.emit_expr(val);
            match key.as_str() {
                "x" => x = js,
                "y" => y = js,
                "width" => width = js,
                "height" => height = js,
                "color" => color = Some(js),
                "pinned" => pinned = js == "true",
                _ => {}
            }
        }

        let body_var = self.next_node_id();
        self.emit_line(&format!(
            "const {} = _world.create_soft_rect({}, {}, {}, {}, 4, 3, 30);",
            body_var, x, y, width, height
        ));

        if let Some(c) = color {
            self.emit_scene_set_color(&body_var, c);
        }

        if pinned {
            self.emit_line(&format!("_world.pin_particle({}, true);", body_var));
        }
    }

    /// Emit set_body_color from a hex color string
    fn emit_scene_set_color(&mut self, body_var: &str, color_str: String) {
        // Parse hex color at compile time or emit runtime parser
        let clean = color_str.replace('"', "").replace('\\', "");
        if clean.starts_with('#') && clean.len() == 7 {
            let r = u8::from_str_radix(&clean[1..3], 16).unwrap_or(128);
            let g = u8::from_str_radix(&clean[3..5], 16).unwrap_or(128);
            let b = u8::from_str_radix(&clean[5..7], 16).unwrap_or(128);
            self.emit_line(&format!(
                "_world.set_body_color({}, {:.3}, {:.3}, {:.3}, 1.0);",
                body_var,
                r as f64 / 255.0,
                g as f64 / 255.0,
                b as f64 / 255.0
            ));
        }
    }
}

impl Default for JsEmitter {
    fn default() -> Self {
        Self::new()
    }
}

/// Minimal CSS reset and layout classes.
const CSS_RESET: &str = r#"
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  background: #0a0a0f;
  color: #e2e8f0;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}
#ds-root {
  width: 100%;
  max-width: 800px;
  padding: 2rem;
}
.ds-column {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.ds-row {
  display: flex;
  flex-direction: row;
  gap: 1rem;
  align-items: center;
}
.ds-stack {
  position: relative;
}
.ds-panel {
  position: absolute;
}
.ds-text, .ds-text span {
  font-size: 1.25rem;
  color: #e2e8f0;
}
.ds-button, button.ds-button {
  background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 12px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
}
.ds-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
}
.ds-button:active {
  transform: translateY(0);
}
/* ── Button Variants ── */
button.ds-btn-primary, .ds-button.ds-btn-primary {
  background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
}
button.ds-btn-primary:hover { box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); }
button.ds-btn-secondary, .ds-button.ds-btn-secondary {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #e2e8f0;
  box-shadow: none;
}
button.ds-btn-secondary:hover { background: rgba(255, 255, 255, 0.12); }
button.ds-btn-ghost, .ds-button.ds-btn-ghost {
  background: transparent;
  box-shadow: none;
  color: #a5b4fc;
}
button.ds-btn-ghost:hover { background: rgba(99, 102, 241, 0.1); }
button.ds-btn-destructive, .ds-button.ds-btn-destructive {
  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
}
button.ds-btn-destructive:hover { box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4); }
/* ── Card ── */
.ds-card {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 16px;
  padding: 1.5rem;
  backdrop-filter: blur(12px);
  transition: border-color 0.2s, box-shadow 0.2s;
}
.ds-card:hover {
  border-color: rgba(99, 102, 241, 0.3);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.ds-card-title {
  font-size: 1.25rem;
  font-weight: 700;
  color: #f1f5f9;
  margin-bottom: 0.25rem;
}
.ds-card-subtitle {
  font-size: 0.875rem;
  color: #94a3b8;
}
/* ── Badge ── */
.ds-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.75rem;
  border-radius: 9999px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.025em;
  text-transform: uppercase;
}
.ds-badge-success { background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.2); }
.ds-badge-warning { background: rgba(234, 179, 8, 0.15); color: #facc15; border: 1px solid rgba(234, 179, 8, 0.2); }
.ds-badge-error { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.2); }
.ds-badge-info { background: rgba(56, 189, 248, 0.15); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.2); }
.ds-badge-default { background: rgba(148, 163, 184, 0.15); color: #94a3b8; border: 1px solid rgba(148, 163, 184, 0.2); }
/* ── Dialog ── */
.ds-dialog-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  animation: ds-fade-in 0.2s ease-out;
}
.ds-dialog-content {
  background: #1a1a2e;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  padding: 2rem;
  min-width: 400px;
  max-width: 90vw;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  animation: ds-fade-in 0.3s ease-out;
}
.ds-dialog-title {
  font-size: 1.375rem;
  font-weight: 700;
  color: #f1f5f9;
  margin-bottom: 1rem;
}
/* ── Toast ── */
.ds-toast {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  background: #1e1e3a;
  border: 1px solid rgba(99, 102, 241, 0.3);
  border-radius: 12px;
  padding: 1rem 1.5rem;
  color: #e2e8f0;
  font-size: 0.875rem;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
  animation: ds-slide-in 0.3s ease-out;
  z-index: 200;
}
@keyframes ds-slide-in {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}
/* ── Input Label ── */
.ds-input, input.ds-input {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: #e2e8f0;
  padding: 0.75rem 1rem;
  border-radius: 12px;
  font-size: 1rem;
  outline: none;
  transition: border-color 0.2s;
  width: 100%;
  box-sizing: border-box;
}
.ds-input-label {
  font-size: 0.875rem;
  font-weight: 500;
  color: #94a3b8;
  margin-bottom: 0.375rem;
}
.ds-input-error {
  font-size: 0.75rem;
  color: #f87171;
  margin-top: 0.25rem;
}
.ds-input.ds-input-has-error {
  border-color: rgba(239, 68, 68, 0.5);
}
.ds-input:focus {
  border-color: #6366f1;
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
.ds-spinner {
  width: 2rem;
  height: 2rem;
  border: 3px solid rgba(255,255,255,0.1);
  border-top-color: #6366f1;
  border-radius: 50%;
  animation: ds-spin 0.8s linear infinite;
}
@keyframes ds-spin {
  to { transform: rotate(360deg); }
}
@keyframes ds-fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.ds-fade-in {
  animation: ds-fade-in 0.3s ease-out;
}
/* ── Progress ── */
.ds-progress-track {
  width: 100%;
  height: 8px;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 9999px;
  overflow: hidden;
}
.ds-progress-fill {
  height: 100%;
  border-radius: 9999px;
  background: linear-gradient(90deg, #6366f1 0%, #8b5cf6 100%);
  transition: width 0.4s ease;
}
/* ── Avatar ── */
.ds-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 700;
  font-size: 1rem;
  flex-shrink: 0;
}
.ds-avatar-lg { width: 56px; height: 56px; font-size: 1.25rem; }
.ds-avatar-sm { width: 28px; height: 28px; font-size: 0.75rem; }
/* ── Separator ── */
.ds-separator {
  width: 100%;
  height: 1px;
  background: rgba(255, 255, 255, 0.08);
  border: none;
  margin: 0.5rem 0;
}
/* ── Alert ── */
.ds-alert {
  padding: 1rem 1.25rem;
  border-radius: 12px;
  font-size: 0.875rem;
  line-height: 1.5;
}
.ds-alert-info {
  background: rgba(56, 189, 248, 0.08);
  border: 1px solid rgba(56, 189, 248, 0.2);
  color: #7dd3fc;
}
.ds-alert-warning {
  background: rgba(234, 179, 8, 0.08);
  border: 1px solid rgba(234, 179, 8, 0.2);
  color: #fde047;
}
.ds-alert-error {
  background: rgba(239, 68, 68, 0.08);
  border: 1px solid rgba(239, 68, 68, 0.2);
  color: #fca5a5;
}
.ds-alert-success {
  background: rgba(34, 197, 94, 0.08);
  border: 1px solid rgba(34, 197, 94, 0.2);
  color: #86efac;
}
/* ── Toggle ── */
.ds-toggle {
  width: 44px;
  height: 24px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.1);
  position: relative;
  cursor: pointer;
  transition: background 0.2s;
  border: none;
  padding: 0;
}
.ds-toggle-on {
  background: #6366f1;
}
.ds-toggle::after {
  content: '';
  position: absolute;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: white;
  top: 3px;
  left: 3px;
  transition: transform 0.2s;
}
.ds-toggle-on::after {
  transform: translateX(20px);
}
/* ── Stat ── */
.ds-stat-value {
  font-size: 2rem;
  font-weight: 800;
  color: #f1f5f9;
  line-height: 1;
}
.ds-stat-delta-up {
  font-size: 0.75rem;
  font-weight: 600;
  color: #4ade80;
}
.ds-stat-delta-down {
  font-size: 0.75rem;
  font-weight: 600;
  color: #f87171;
}
/* ── Outline Button ── */
button.ds-btn-outline, .ds-button.ds-btn-outline {
  background: transparent;
  border: 1px solid rgba(99, 102, 241, 0.4);
  color: #a5b4fc;
  box-shadow: none;
}
button.ds-btn-outline:hover { background: rgba(99, 102, 241, 0.1); border-color: rgba(99, 102, 241, 0.6); }
/* ── Tabs ── */
.ds-tabs {
  display: flex;
  gap: 0.25rem;
  background: rgba(255, 255, 255, 0.04);
  padding: 0.25rem;
  border-radius: 12px;
}
.ds-tab {
  background: transparent;
  border: none;
  color: #94a3b8;
  padding: 0.5rem 1rem;
  border-radius: 8px;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
  box-shadow: none;
}
.ds-tab:hover { color: #e2e8f0; background: rgba(255, 255, 255, 0.06); }
.ds-tab-active {
  background: rgba(99, 102, 241, 0.2) !important;
  color: #a5b4fc !important;
  font-weight: 600;
}
/* ── Select Group ── */
.ds-select-group {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
/* ── Separator Row ── */
.ds-separator-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
}
.ds-separator-v {
  width: 1px;
  height: 100%;
  min-height: 1.5rem;
}
/* ── Toast Variants ── */
.ds-toast-success { border-color: rgba(34, 197, 94, 0.3); }
.ds-toast-error { border-color: rgba(239, 68, 68, 0.3); }
.ds-toast-warning { border-color: rgba(234, 179, 8, 0.3); }
.ds-toast-info { border-color: rgba(56, 189, 248, 0.3); }
/* ── Avatar Status ── */
.ds-avatar-status {
  font-size: 0.625rem;
  line-height: 1;
  text-align: center;
}
"#;

/// Map a DreamStack variant prop to CSS class(es) based on element tag.
fn variant_to_css(tag: &str, variant: &str) -> String {
    match tag {
        "button" => match variant {
            "primary" => "ds-btn-primary".to_string(),
            "secondary" => "ds-btn-secondary".to_string(),
            "ghost" => "ds-btn-ghost".to_string(),
            "destructive" => "ds-btn-destructive".to_string(),
            "outline" => "ds-btn-outline".to_string(),
            "toggle" => "ds-toggle".to_string(),
            "toggle-on" => "ds-toggle ds-toggle-on".to_string(),
            "toggle-off" => "ds-toggle".to_string(),
            "tab-active" => "ds-tab ds-tab-active".to_string(),
            "tab-inactive" => "ds-tab".to_string(),
            "select-active" => "ds-btn-primary".to_string(),
            "select-option" => "ds-btn-secondary".to_string(),
            _ => format!("ds-btn-{}", variant),
        },
        "text" => match variant {
            "success" | "warning" | "error" | "info" | "default" =>
                format!("ds-badge ds-badge-{}", variant),
            "title" => "ds-card-title".to_string(),
            "subtitle" | "muted" => "ds-card-subtitle".to_string(),
            "label" => "ds-input-label".to_string(),
            "card-title" => "ds-card-title".to_string(),
            "card-subtitle" => "ds-card-subtitle".to_string(),
            "card-footer" => "ds-card-subtitle".to_string(),
            "input-label" => "ds-input-label".to_string(),
            "input-error-text" => "ds-input-error".to_string(),
            "input-helper" => "ds-card-subtitle".to_string(),
            "dialog-title" => "ds-dialog-title".to_string(),
            "stat-label" => "ds-card-subtitle".to_string(),
            "stat-value" => "ds-stat-value".to_string(),
            "stat-trend-up" => "ds-stat-delta-up".to_string(),
            "stat-trend-down" => "ds-stat-delta-down".to_string(),
            "stat-trend-neutral" => "ds-card-subtitle".to_string(),
            "separator-label" => "ds-card-subtitle".to_string(),
            "avatar" => "ds-avatar".to_string(),
            "avatar-sm" => "ds-avatar ds-avatar-sm".to_string(),
            "avatar-md" => "ds-avatar".to_string(),
            "avatar-lg" => "ds-avatar ds-avatar-lg".to_string(),
            "avatar-status" => "ds-avatar-status".to_string(),
            _ => format!("ds-text-{}", variant),
        },
        "column" => match variant {
            "card" => "ds-card".to_string(),
            "stat-card" => "ds-card".to_string(),
            "dialog" | "dialog-panel" => "ds-dialog-content".to_string(),
            "dialog-overlay" | "overlay" => "ds-dialog-overlay".to_string(),
            "avatar" => "ds-avatar".to_string(),
            "separator" => "ds-separator".to_string(),
            "separator-line" => "ds-separator".to_string(),
            "separator-horizontal" => "ds-separator".to_string(),
            "separator-vertical" => "ds-separator ds-separator-v".to_string(),
            "progress-track" => "ds-progress-track".to_string(),
            "progress-fill" | "progress-fill-success" | "progress-fill-warning" | "progress-fill-error" => {
                let cls = format!("ds-progress-fill ds-progress-fill-{}", variant.strip_prefix("progress-fill-").unwrap_or("default"));
                if variant == "progress-fill" { "ds-progress-fill".to_string() } else { cls }
            }
            "toast-success" | "toast-error" | "toast-warning" | "toast-info" => format!("ds-toast ds-toast-{}", variant.strip_prefix("toast-").unwrap_or("info")),
            "alert-info" | "alert-warning" | "alert-error" | "alert-success" => format!("ds-alert ds-alert-{}", variant.strip_prefix("alert-").unwrap_or("info")),
            _ => format!("ds-column-{}", variant),
        },
        "row" => match variant {
            "card" => "ds-card".to_string(),
            "tabs" => "ds-tabs".to_string(),
            "separator" => "ds-separator-row".to_string(),
            "select-group" => "ds-select-group".to_string(),
            _ => format!("ds-row-{}", variant),
        },
        "input" => match variant {
            "error" | "input-error" => "ds-input-has-error".to_string(),
            "input-default" => "".to_string(),
            _ => format!("ds-input-{}", variant),
        },
        _ => format!("ds-{}-{}", tag, variant),
    }
}

/// Generate a JS object literal mapping variant names to CSS classes for dynamic variant.
fn variant_map_js(tag: &str) -> String {
    match tag {
        "button" => "{'primary':'ds-btn-primary','secondary':'ds-btn-secondary','ghost':'ds-btn-ghost','destructive':'ds-btn-destructive','outline':'ds-btn-outline','toggle':'ds-toggle','toggle-on':'ds-toggle ds-toggle-on','toggle-off':'ds-toggle','tab-active':'ds-tab ds-tab-active','tab-inactive':'ds-tab','select-active':'ds-btn-primary','select-option':'ds-btn-secondary'}".to_string(),
        "text" => "{'success':'ds-badge ds-badge-success','warning':'ds-badge ds-badge-warning','error':'ds-badge ds-badge-error','info':'ds-badge ds-badge-info','default':'ds-badge ds-badge-default','title':'ds-card-title','subtitle':'ds-card-subtitle','label':'ds-input-label','card-title':'ds-card-title','card-subtitle':'ds-card-subtitle','card-footer':'ds-card-subtitle','input-label':'ds-input-label','input-error-text':'ds-input-error','input-helper':'ds-card-subtitle','dialog-title':'ds-dialog-title','stat-label':'ds-card-subtitle','stat-value':'ds-stat-value','stat-trend-up':'ds-stat-delta-up','stat-trend-down':'ds-stat-delta-down','stat-trend-neutral':'ds-card-subtitle','separator-label':'ds-card-subtitle','avatar':'ds-avatar','avatar-sm':'ds-avatar ds-avatar-sm','avatar-md':'ds-avatar','avatar-lg':'ds-avatar ds-avatar-lg'}".to_string(),
        "column" => "{'card':'ds-card','stat-card':'ds-card','dialog':'ds-dialog-content','dialog-panel':'ds-dialog-content','dialog-overlay':'ds-dialog-overlay','overlay':'ds-dialog-overlay','avatar':'ds-avatar','separator':'ds-separator','separator-line':'ds-separator','progress-track':'ds-progress-track','progress-fill':'ds-progress-fill'}".to_string(),
        "row" => "{'card':'ds-card','tabs':'ds-tabs','separator':'ds-separator-row','select-group':'ds-select-group'}".to_string(),
        "input" => "{'error':'ds-input-has-error','input-error':'ds-input-has-error','input-default':''}".to_string(),
        _ => "{}".to_string(),
    }
}

/// The DreamStack client-side reactive runtime (~3KB).
const RUNTIME_JS: &str = r#"
const DS = (() => {
  // ── Signal System ──
  let currentEffect = null;
  let batchDepth = 0;
  let pendingEffects = new Set();

  class Signal {
    constructor(initialValue) {
      this._value = initialValue;
      this._subscribers = new Set();
    }
    get value() {
      if (currentEffect) {
        this._subscribers.add(currentEffect);
      }
      return this._value;
    }
    set value(newValue) {
      if (this._value === newValue) return;
      this._value = newValue;
      if (batchDepth > 0) {
        for (const sub of this._subscribers) {
          pendingEffects.add(sub);
        }
      } else {
        for (const sub of [...this._subscribers]) {
          sub._run();
        }
      }
    }
  }

  class Derived {
    constructor(fn) {
      this._fn = fn;
      this._value = undefined;
      this._dirty = true;
      this._subscribers = new Set();
      this._effect = new Effect(() => {
        this._value = this._fn();
        this._dirty = false;
        // Notify our subscribers
        if (batchDepth > 0) {
          for (const sub of this._subscribers) {
            pendingEffects.add(sub);
          }
        } else {
          for (const sub of [...this._subscribers]) {
            sub._run();
          }
        }
      });
      this._effect._run();
    }
    get value() {
      if (currentEffect) {
        this._subscribers.add(currentEffect);
      }
      return this._value;
    }
  }

  class Effect {
    constructor(fn) {
      this._fn = fn;
      this._disposed = false;
    }
    _run() {
      if (this._disposed) return;
      const prev = currentEffect;
      currentEffect = this;
      try {
        this._fn();
      } finally {
        currentEffect = prev;
      }
    }
    dispose() {
      this._disposed = true;
    }
  }

  // ── Public API ──
  function signal(value) {
    return new Signal(value);
  }

  function derived(fn) {
    return new Derived(fn);
  }

  function effect(fn) {
    const eff = new Effect(fn);
    eff._run();
    return eff;
  }

  function batch(fn) {
    batchDepth++;
    try {
      fn();
    } finally {
      batchDepth--;
      if (batchDepth === 0) {
        flush();
      }
    }
  }

  function flush() {
    const effects = [...pendingEffects];
    pendingEffects.clear();
    for (const eff of effects) {
      eff._run();
    }
    // After effects recompute derived signals, flush batched diffs to stream
    // (individual _streamDiff calls already batched changes — no full sync needed)
  }

  // ── Event System ──
  const eventHandlers = {};

  function onEvent(name, handler) {
    if (!eventHandlers[name]) eventHandlers[name] = [];
    eventHandlers[name].push(handler);
  }

  function emit(name, data) {
    const handlers = eventHandlers[name];
    if (handlers) {
      batch(() => {
        for (const h of handlers) {
          h(data);
        }
      });
    }
  }

  // ── Keyed List Reconciliation ──
  function keyedList(container, getItems, keyFn, renderFn) {
    let nodeMap = new Map();
    const eff = effect(() => {
      const raw = getItems();
      const items = (raw && raw.value !== undefined) ? raw.value : (Array.isArray(raw) ? raw : []);
      const newMap = new Map();
      const frag = document.createDocumentFragment();
      items.forEach((item, idx) => {
        const key = keyFn ? keyFn(item, idx) : idx;
        let node = nodeMap.get(key);
        if (!node) {
          node = renderFn(item, idx);
        }
        newMap.set(key, node);
        frag.appendChild(node);
      });
      container.innerHTML = '';
      container.appendChild(frag);
      nodeMap = newMap;
    });
    return eff;
  }

  // @TREESHAKE:BEGIN:router
  // ── Router ──
  const _route = new Signal(window.location.hash.slice(1) || '/');
  window.addEventListener('hashchange', () => {
    _route.value = window.location.hash.slice(1) || '/';
  });

  function navigate(path) {
    window.location.hash = path;
  }

  function matchRoute(pattern, path) {
    if (pattern === path) return {};
    const patParts = pattern.split('/');
    const pathParts = path.split('/');
    if (patParts.length !== pathParts.length) return null;
    const params = {};
    for (let i = 0; i < patParts.length; i++) {
      if (patParts[i].startsWith(':')) {
        params[patParts[i].slice(1)] = pathParts[i];
      } else if (patParts[i] !== pathParts[i]) {
        return null;
      }
    }
    return params;
  }
  // @TREESHAKE:END:router

  // ── Async Resources ──
  function resource(fetcher) {
    const state = new Signal({ status: 'loading', data: null, error: null });
    const eff = effect(() => {
      state.value = { status: 'loading', data: state._value.data, error: null };
      Promise.resolve(fetcher())
        .then(data => { state.value = { status: 'ok', data, error: null }; })
        .catch(err => { state.value = { status: 'error', data: null, error: err.message || String(err) }; });
    });
    return state;
  }

  function fetchJSON(url) {
    return resource(() => fetch(url).then(r => r.json()));
  }

  // @TREESHAKE:BEGIN:spring
  // ── Spring Physics Engine ──
  const _activeSprings = new Set();
  let _rafId = null;
  let _lastTime = 0;

  class Spring {
    constructor({ value = 0, target, stiffness = 170, damping = 26, mass = 1 } = {}) {
      this._signal = new Signal(value);
      this._velocity = 0;
      this._target = target !== undefined ? target : value;
      this.stiffness = stiffness;
      this.damping = damping;
      this.mass = mass;
      this._settled = true;
    }
    get value() { return this._signal.value; }
    set value(v) {
      // Assignment animates to new target (not instant set)
      this.target = v;
    }
    get target() { return this._target; }
    set target(t) {
      this._target = t;
      this._settled = false;
      _activeSprings.add(this);
      _startSpringLoop();
    }
    set(v) {
      this._signal.value = v;
      this._target = v;
      this._velocity = 0;
      this._settled = true;
      _activeSprings.delete(this);
    }
    _step(dt) {
      const pos = this._signal._value;
      const vel = this._velocity;
      const k = this.stiffness, d = this.damping, m = this.mass;
      const accel = (p, v) => (-k * (p - this._target) - d * v) / m;
      const k1v = accel(pos, vel), k1p = vel;
      const k2v = accel(pos + k1p*dt/2, vel + k1v*dt/2), k2p = vel + k1v*dt/2;
      const k3v = accel(pos + k2p*dt/2, vel + k2v*dt/2), k3p = vel + k2v*dt/2;
      const k4v = accel(pos + k3p*dt, vel + k3v*dt), k4p = vel + k3v*dt;
      this._velocity = vel + (dt/6)*(k1v + 2*k2v + 2*k3v + k4v);
      this._signal.value = pos + (dt/6)*(k1p + 2*k2p + 2*k3p + k4p);
      if (Math.abs(this._velocity) < 0.01 && Math.abs(this._signal._value - this._target) < 0.01) {
        this._signal.value = this._target;
        this._velocity = 0;
        this._settled = true;
        _activeSprings.delete(this);
      }
    }
  }

  function _startSpringLoop() {
    if (_rafId !== null) return;
    _lastTime = performance.now();
    _rafId = requestAnimationFrame(_springLoop);
  }

  function _springLoop(now) {
    const dt = Math.min((now - _lastTime) / 1000, 0.064);
    _lastTime = now;
    batch(() => {
      for (const s of _activeSprings) {
        const steps = Math.ceil(dt / (1/120));
        const subDt = dt / steps;
        for (let i = 0; i < steps; i++) s._step(subDt);
      }
    });
    if (_activeSprings.size > 0) _rafId = requestAnimationFrame(_springLoop);
    else _rafId = null;
  }

  function spring(opts) { return new Spring(typeof opts === 'object' ? opts : { value: opts, target: opts }); }

  // @TREESHAKE:END:spring

  // @TREESHAKE:BEGIN:constraint
  // ── Constraint Solver ──
  function constrain(element, prop, fn) {
    return effect(() => {
      const val = fn();
      if (prop === 'width' || prop === 'height' || prop === 'left' || prop === 'top' ||
          prop === 'right' || prop === 'bottom' || prop === 'maxWidth' || prop === 'minWidth' ||
          prop === 'maxHeight' || prop === 'minHeight' || prop === 'fontSize' ||
          prop === 'padding' || prop === 'margin' || prop === 'gap') {
        element.style[prop] = typeof val === 'number' ? val + 'px' : val;
      } else if (prop === 'opacity' || prop === 'scale') {
        element.style[prop] = val;
      } else {
        element.style[prop] = val;
      }
    });
  }

  // Viewport signal for responsive constraints
  const _viewport = {
    width: new Signal(window.innerWidth),
    height: new Signal(window.innerHeight)
  };
  window.addEventListener('resize', () => {
    _viewport.width.value = window.innerWidth;
    _viewport.height.value = window.innerHeight;
  });

  // @TREESHAKE:END:constraint

  // @TREESHAKE:BEGIN:scene
  // ── 2D Scene Rendering Engine ──
  function scene(width, height) {
    const canvas = document.createElement('canvas');
    canvas.width = width || 600;
    canvas.height = height || 400;
    canvas.style.borderRadius = '16px';
    canvas.style.background = 'rgba(255,255,255,0.03)';
    canvas.style.border = '1px solid rgba(255,255,255,0.08)';
    canvas.style.display = 'block';
    canvas.style.margin = '8px 0';
    const ctx = canvas.getContext('2d');
    const shapes = [];
    let _dirty = false;

    function _render() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      for (const s of shapes) s._draw(ctx);
    }

    function _scheduleRender() {
      if (!_dirty) {
        _dirty = true;
        queueMicrotask(() => { _dirty = false; _render(); });
      }
    }

    return { canvas, ctx, shapes, _render, _scheduleRender };
  }

  function circle(scn, opts) {
    const shape = {
      type: 'circle',
      _draw(ctx) {
        const x = typeof opts.x === 'object' && 'value' in opts.x ? opts.x.value : (typeof opts.x === 'function' ? opts.x() : opts.x);
        const y = typeof opts.y === 'object' && 'value' in opts.y ? opts.y.value : (typeof opts.y === 'function' ? opts.y() : opts.y);
        const r = typeof opts.r === 'object' && 'value' in opts.r ? opts.r.value : (typeof opts.r === 'function' ? opts.r() : (opts.r || 20));
        const fill = opts.fill || '#8b5cf6';

        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);

        // Gradient fill for nice look
        const grad = ctx.createRadialGradient(x - r*0.3, y - r*0.3, r*0.1, x, y, r);
        grad.addColorStop(0, '#a78bfa');
        grad.addColorStop(1, fill);
        ctx.fillStyle = grad;
        ctx.fill();

        // Glow
        ctx.shadowColor = fill;
        ctx.shadowBlur = 20;
        ctx.fill();
        ctx.shadowBlur = 0;
      }
    };
    scn.shapes.push(shape);
    // Reactive: re-render when signal deps change
    effect(() => {
      shape._draw; // trigger reads
      if (opts.x && typeof opts.x === 'object' && 'value' in opts.x) opts.x.value;
      if (opts.y && typeof opts.y === 'object' && 'value' in opts.y) opts.y.value;
      if (opts.r && typeof opts.r === 'object' && 'value' in opts.r) opts.r.value;
      scn._scheduleRender();
    });
    return shape;
  }

  function rect(scn, opts) {
    const shape = {
      type: 'rect',
      _draw(ctx) {
        const x = typeof opts.x === 'object' && 'value' in opts.x ? opts.x.value : (typeof opts.x === 'function' ? opts.x() : opts.x);
        const y = typeof opts.y === 'object' && 'value' in opts.y ? opts.y.value : (typeof opts.y === 'function' ? opts.y() : opts.y);
        const w = typeof opts.w === 'object' && 'value' in opts.w ? opts.w.value : (typeof opts.w === 'function' ? opts.w() : (opts.w || 40));
        const h = typeof opts.h === 'object' && 'value' in opts.h ? opts.h.value : (typeof opts.h === 'function' ? opts.h() : (opts.h || 40));
        const fill = opts.fill || '#6366f1';
        const r = opts.radius || 8;

        ctx.beginPath();
        ctx.roundRect(x, y, w, h, r);
        ctx.fillStyle = fill;
        ctx.fill();
      }
    };
    scn.shapes.push(shape);
    effect(() => {
      if (opts.x && typeof opts.x === 'object' && 'value' in opts.x) opts.x.value;
      if (opts.y && typeof opts.y === 'object' && 'value' in opts.y) opts.y.value;
      if (opts.w && typeof opts.w === 'object' && 'value' in opts.w) opts.w.value;
      if (opts.h && typeof opts.h === 'object' && 'value' in opts.h) opts.h.value;
      scn._scheduleRender();
    });
    return shape;
  }

  function line(scn, opts) {
    const shape = {
      type: 'line',
      _draw(ctx) {
        const x1 = typeof opts.x1 === 'object' && 'value' in opts.x1 ? opts.x1.value : opts.x1;
        const y1 = typeof opts.y1 === 'object' && 'value' in opts.y1 ? opts.y1.value : opts.y1;
        const x2 = typeof opts.x2 === 'object' && 'value' in opts.x2 ? opts.x2.value : opts.x2;
        const y2 = typeof opts.y2 === 'object' && 'value' in opts.y2 ? opts.y2.value : opts.y2;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.strokeStyle = opts.stroke || 'rgba(139,92,246,0.3)';
        ctx.lineWidth = opts.width || 2;
        ctx.stroke();
      }
    };
    scn.shapes.push(shape);
    effect(() => {
      if (opts.x1 && typeof opts.x1 === 'object' && 'value' in opts.x1) opts.x1.value;
      if (opts.y1 && typeof opts.y1 === 'object' && 'value' in opts.y1) opts.y1.value;
      if (opts.x2 && typeof opts.x2 === 'object' && 'value' in opts.x2) opts.x2.value;
      if (opts.y2 && typeof opts.y2 === 'object' && 'value' in opts.y2) opts.y2.value;
      scn._scheduleRender();
    });
    return shape;
  }

  // @TREESHAKE:END:scene

  // @TREESHAKE:BEGIN:streaming
  // ── Bitstream Streaming ──
  const HEADER_SIZE = 16;
  let _streamWs = null;
  let _streamSeq = 0;
  let _streamMode = 'signal';
  let _streamStart = 0;
  let _prevSignals = null;

  // ── Diff Batching — coalesce multiple signal changes into one WS frame ──
  var _pendingDiffs = {};     // accumulated changes: { name: value }
  var _pendingVersions = {};  // version for each pending change
  var _diffFlushTimer = null; // microtask coalescing

  // ── Connection Status — reactive signals for stream health ──
  var _streamConnected = false;
  var _streamLatency = 0;     // RTT in milliseconds
  var _streamReconnects = 0;
  var _streamFrameCount = 0;
  var _streamByteCount = 0;
  var _lastPingTime = 0;      // for RTT calculation

  function _encodeHeader(type, flags, seq, ts, w, h, len) {
    const b = new ArrayBuffer(HEADER_SIZE);
    const v = new DataView(b);
    v.setUint8(0, type); v.setUint8(1, flags);
    v.setUint16(2, seq, true); v.setUint32(4, ts, true);
    v.setUint16(8, w, true); v.setUint16(10, h, true);
    v.setUint32(12, len, true);
    return new Uint8Array(b);
  }

  function _decodeHeader(buf) {
    const v = new DataView(buf.buffer || buf, buf.byteOffset || 0);
    return {
      type: v.getUint8(0), flags: v.getUint8(1),
      seq: v.getUint16(2, true), timestamp: v.getUint32(4, true),
      width: v.getUint16(8, true), height: v.getUint16(10, true),
      length: v.getUint32(12, true)
    };
  }

  // ── Pixel streaming state ──
  var _streamCanvas = null;    // canvas 2D context registered by physics scene
  var _streamPrevFrame = null; // previous RGBA frame for delta computation
  var _domCaptureCanvas = null; // offscreen canvas for DOM rasterization
  var _domCaptureCtx = null;
  var _domCaptureActive = false;

  // ── DOM Pixel Capture (for DOM-based apps without a physics canvas) ──
  function _streamDomCapture(rootEl, w, h) {
    if (!rootEl) return;
    if (!_domCaptureCanvas) {
      _domCaptureCanvas = document.createElement('canvas');
      _domCaptureCanvas.width = w || 800;
      _domCaptureCanvas.height = h || 600;
      _domCaptureCtx = _domCaptureCanvas.getContext('2d');
    }
    // Use SVG foreignObject to rasterize DOM to canvas
    var svgNs = 'http://www.w3.org/2000/svg';
    var html = rootEl.outerHTML;
    var svgStr = '<svg xmlns="' + svgNs + '" width="' + w + '" height="' + h + '">' +
      '<foreignObject width="100%" height="100%">' +
      '<div xmlns="http://www.w3.org/1999/xhtml">' + html + '</div>' +
      '</foreignObject></svg>';
    var blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
    var url = URL.createObjectURL(blob);
    var img = new Image();
    img.onload = function() {
      _domCaptureCtx.clearRect(0, 0, w, h);
      _domCaptureCtx.drawImage(img, 0, 0);
      URL.revokeObjectURL(url);
      // Send frame data
      var imageData = _domCaptureCtx.getImageData(0, 0, w, h);
      var pixels = new Uint8Array(imageData.data.buffer);
      if (_streamMode === 'delta' && _streamPrevFrame) {
        var delta = new Uint8Array(pixels.length);
        var zeroCount = 0;
        for (var i = 0; i < pixels.length; i++) {
          delta[i] = pixels[i] ^ _streamPrevFrame[i];
          if (delta[i] === 0) zeroCount++;
        }
        if (zeroCount > pixels.length * 0.3) {
          _streamSendPixel(0x03, 0x04, w, h, _rleEncode(delta));
        } else {
          _streamSendPixel(0x01, 0x02, w, h, pixels);
        }
      } else {
        _streamSendPixel(0x01, 0x02, w, h, pixels);
      }
      _streamPrevFrame = pixels.slice();
    };
    img.onerror = function() { URL.revokeObjectURL(url); };
    img.src = url;
  }

  function _startDomCapture() {
    if (_domCaptureActive) return;
    _domCaptureActive = true;
    var root = document.getElementById('ds-root');
    if (!root) return;
    var w = root.offsetWidth || 800;
    var h = root.offsetHeight || 600;
    function captureLoop() {
      if (!_domCaptureActive || !_streamWs || _streamWs.readyState !== 1) return;
      if (_streamMode === 'pixel' || _streamMode === 'delta') {
        _streamDomCapture(root, w, h);
      }
      setTimeout(captureLoop, 100); // ~10fps for DOM capture
    }
    captureLoop();
  }

  // ── Opus Audio Compression ──
  var _audioRecorder = null;
  var _audioRecorderSeq = 0;

  function _streamAudio(mediaStream) {
    if (!mediaStream || _audioRecorder) return;
    try {
      _audioRecorder = new MediaRecorder(mediaStream, {
        mimeType: 'audio/webm;codecs=opus',
        audioBitsPerSecond: 32000
      });
    } catch(e) {
      console.warn('[ds-stream] MediaRecorder not available for Opus:', e);
      return;
    }
    _audioRecorder.ondataavailable = function(e) {
      if (e.data.size > 0 && _streamWs && _streamWs.readyState === 1) {
        e.data.arrayBuffer().then(function(buf) {
          var payload = new Uint8Array(buf);
          var ts = (performance.now() - _streamStart) | 0;
          var msg = new Uint8Array(HEADER_SIZE + payload.length);
          var v = new DataView(msg.buffer);
          v.setUint8(0, 0x11); // AudioCompressed
          v.setUint8(1, 0x04); // FLAG_COMPRESSED
          v.setUint16(2, (_audioRecorderSeq++) & 0xFFFF, true);
          v.setUint32(4, ts, true);
          v.setUint16(8, 1, true);   // channels
          v.setUint16(10, 480, true); // 48000/100
          v.setUint32(12, payload.length, true);
          msg.set(payload, HEADER_SIZE);
          _streamWs.send(msg.buffer);
        });
      }
    };
    _audioRecorder.start(20); // 20ms chunks
    console.log('[ds-stream] Audio streaming started (Opus)');
  }

  function _stopAudio() {
    if (_audioRecorder) {
      _audioRecorder.stop();
      _audioRecorder = null;
    }
  }

  // ── Adaptive Quality ──
  var _adaptiveEnabled = false;
  var _adaptiveByteWindow = []; // timestamps + sizes in 5s window
  var _adaptiveStableCount = 0;

  function _startAdaptiveQuality() {
    if (_adaptiveEnabled) return;
    _adaptiveEnabled = true;
    setInterval(function() {
      if (!_streamWs || _streamWs.readyState !== 1) return;
      var now = performance.now();
      // Prune old entries (older than 5s)
      _adaptiveByteWindow = _adaptiveByteWindow.filter(function(e) { return now - e.t < 5000; });
      var totalBytes = 0;
      for (var i = 0; i < _adaptiveByteWindow.length; i++) totalBytes += _adaptiveByteWindow[i].b;
      var byteRateKBps = totalBytes / 5 / 1024; // KB/s
      var lat = _streamLatency;

      var currentMode = _streamMode;
      // Downgrade logic
      if (currentMode === 'pixel' && (lat > 200 || byteRateKBps > 500)) {
        _streamMode = 'delta';
        _adaptiveStableCount = 0;
        console.log('[ds-stream] Adaptive: pixel -> delta (lat=' + lat + 'ms, rate=' + byteRateKBps.toFixed(0) + 'KB/s)');
      } else if ((currentMode === 'pixel' || currentMode === 'delta') && (lat > 500 || byteRateKBps > 1000)) {
        _streamMode = 'signal';
        _adaptiveStableCount = 0;
        console.log('[ds-stream] Adaptive: ' + currentMode + ' -> signal (lat=' + lat + 'ms, rate=' + byteRateKBps.toFixed(0) + 'KB/s)');
      }
      // Upgrade logic (needs stability for 5 checks = 10s)
      else if (currentMode === 'signal' && lat < 100 && byteRateKBps < 200) {
        _adaptiveStableCount++;
        if (_adaptiveStableCount >= 5) {
          _streamMode = 'delta';
          _adaptiveStableCount = 0;
          console.log('[ds-stream] Adaptive: signal -> delta (conditions good)');
        }
      } else if (currentMode === 'delta' && lat < 50 && byteRateKBps < 100) {
        _adaptiveStableCount++;
        if (_adaptiveStableCount >= 5) {
          _streamMode = 'pixel';
          _adaptiveStableCount = 0;
          console.log('[ds-stream] Adaptive: delta -> pixel (conditions excellent)');
        }
      } else {
        _adaptiveStableCount = 0;
      }
    }, 2000);
  }

  function _streamSendPixel(type, flags, w, h, payload) {
    if (!_streamWs || _streamWs.readyState !== 1) return;
    var ts = (performance.now() - _streamStart) | 0;
    var msg = new Uint8Array(HEADER_SIZE + payload.length);
    var v = new DataView(msg.buffer);
    v.setUint8(0, type);
    v.setUint8(1, flags);
    v.setUint16(2, (_streamSeq++) & 0xFFFF, true);
    v.setUint32(4, ts, true);
    v.setUint16(8, w, true);
    v.setUint16(10, h, true);
    v.setUint32(12, payload.length, true);
    msg.set(payload, HEADER_SIZE);
    _streamWs.send(msg.buffer);
    if (_adaptiveEnabled) { _adaptiveByteWindow.push({ t: performance.now(), b: msg.length }); }
  }

  function _rleEncode(data) {
    var out = [];
    var i = 0;
    while (i < data.length) {
      if (data[i] === 0) {
        var count = 0;
        while (i < data.length && data[i] === 0 && count < 65535) { count++; i++; }
        out.push(0, count & 0xFF, (count >> 8) & 0xFF);
      } else {
        out.push(data[i]);
        i++;
      }
    }
    return new Uint8Array(out);
  }

  // ── Signal registry for bidirectional sync ──
  var _signalRegistry = {};
  var _signalVersions = {}; // per-signal version counters for conflict resolution
  var _applyingRemoteDiff = false;
  var _peerId = Math.random().toString(36).substr(2, 8); // unique per client

  function _registerSignal(name, sig) {
    _signalRegistry[name] = sig;
    _signalVersions[name] = 0;
  }

  function _applyRemoteDiff(json) {
    try {
      var data = JSON.parse(json);
      // Ignore our own diffs echoed back by the relay
      if (data._pid === _peerId) return;
      var versions = data._v || {};
      _applyingRemoteDiff = true;
      for (var name in data) {
        if (name === '_pid' || name === '_v') continue;
        var sig = _signalRegistry[name];
        if (!sig) continue;
        var remoteV = versions[name] || 0;
        var localV = _signalVersions[name] || 0;
        // Only apply if remote version >= local version (last-write-wins)
        if (remoteV >= localV) {
          sig.value = data[name];
          _signalVersions[name] = remoteV;
        }
      }
      flush();
    } catch(e) { console.warn('[ds-stream] bad diff:', e); }
    _applyingRemoteDiff = false;
  }

  function _initStream(url, mode) {
    _streamMode = mode || 'signal';
    _streamStart = performance.now();

    // Use peer path for bidirectional sync: ws://host:port → ws://host:port/peer/default
    var peerUrl = url;
    if (!url.match(/\/(source|stream|peer)\//)) {
      peerUrl = url.replace(/\/?$/, '/peer/default');
    }
    _streamWs = new WebSocket(peerUrl);
    _streamWs.binaryType = 'arraybuffer';
    _streamWs.onmessage = function(e) {
      if (!(e.data instanceof ArrayBuffer) || e.data.byteLength < HEADER_SIZE) return;
      var bytes = new Uint8Array(e.data);
      var view = new DataView(bytes.buffer);
      var type = view.getUint8(0);
      _streamFrameCount++;
      _streamByteCount += e.data.byteLength;
      // Signal diff from another peer — apply to local signals
      if (type === 0x31) {
        var payload = bytes.subarray(HEADER_SIZE);
        var json = new TextDecoder().decode(payload);
        _applyRemoteDiff(json);
      }
      // Pong response — calculate RTT
      if (type === 0xFE) {
        if (_lastPingTime > 0) {
          _streamLatency = Math.round(performance.now() - _lastPingTime);
        }
      }
    };
    _streamWs.onclose = function() {
      _streamConnected = false;
      _streamReconnects++;
      var delay = Math.min(_reconnectDelay, 30000);
      console.log('[ds-stream] Disconnected, reconnecting in ' + (delay/1000) + 's (attempt ' + _streamReconnects + ')');
      setTimeout(function() { _initStream(url, mode); }, delay);
      _reconnectDelay = Math.min(_reconnectDelay * 2, 30000); // exponential backoff, max 30s
    };
    _streamWs.onerror = function() {
      _streamConnected = false;
    };
    _streamWs.onopen = function() {
      _streamConnected = true;
      _reconnectDelay = 2000; // reset backoff on success
      console.log('[ds-stream] Peer connected:', peerUrl);
      // Send schema announcement (0x32) with output signal list
      var outputNames = Object.keys(_signalRegistry);
      if (outputNames.length > 0) {
        _streamSend(0x32, 0, new TextEncoder().encode(
          JSON.stringify({ signals: outputNames, mode: _streamMode })
        ));
      }
      // Broadcast full state snapshot with versions so other peers can sync
      var fullState = { _pid: _peerId, _v: {} };
      for (var name in _signalRegistry) {
        var sig = _signalRegistry[name];
        fullState[name] = (typeof sig === 'object' && sig !== null && '_value' in sig) ? sig._value : sig;
        fullState._v[name] = _signalVersions[name] || 0;
      }
      _streamSend(0x31, 0, new TextEncoder().encode(JSON.stringify(fullState)));

      // Start periodic ping for RTT measurement (every 5s)
      if (!_streamWs._pingInterval) {
        _streamWs._pingInterval = setInterval(function() {
          if (_streamWs && _streamWs.readyState === 1) {
            _lastPingTime = performance.now();
            _streamSend(0xFE, 0, new Uint8Array(0));
          }
        }, 5000);
      }
      // Auto-start DOM capture for pixel/delta mode without a physics canvas
      if ((_streamMode === 'pixel' || _streamMode === 'delta') && !_streamCanvas) {
        setTimeout(_startDomCapture, 100);
      }
    };
  }

  function _streamSend(type, flags, payload) {
    if (!_streamWs || _streamWs.readyState !== 1) return;
    var ts = (performance.now() - _streamStart) | 0;
    var msg = new Uint8Array(HEADER_SIZE + payload.length);
    var v = new DataView(msg.buffer);
    v.setUint8(0, type);
    v.setUint8(1, flags);
    v.setUint16(2, (_streamSeq++) & 0xFFFF, true);
    v.setUint32(4, ts, true);
    v.setUint32(12, payload.length, true);
    msg.set(payload, HEADER_SIZE);
    _streamWs.send(msg.buffer);
  }

  // ── Batched diff: coalesce multiple signal changes into one WS frame ──
  var _lastSentValues = {};  // deduplication: track last-sent value per signal
  var _diffBatchCount = 0;   // count batches for periodic auto-sync
  var _streamOutputFilter = null; // Set of signal names to stream (null = all)
  var _reconnectDelay = 2000;  // exponential backoff: starts at 2s, max 30s

  function _streamDiff(name, value) {
    if (!_streamWs || _streamWs.readyState !== 1 || _streamMode !== 'signal') return;
    if (_applyingRemoteDiff) return; // prevent echo loops
    if (_streamOutputFilter && !_streamOutputFilter.has(name)) return; // skip non-output signals
    var val = (typeof value === 'object' && value !== null && 'value' in value) ? value.value : value;
    // Deduplication: skip if value hasn't changed
    var lastStr = JSON.stringify(_lastSentValues[name]);
    var newStr = JSON.stringify(val);
    if (lastStr === newStr) return;
    _lastSentValues[name] = val;
    // Increment version for conflict resolution
    _signalVersions[name] = (_signalVersions[name] || 0) + 1;
    // Accumulate into pending batch
    _pendingDiffs[name] = val;
    _pendingVersions[name] = _signalVersions[name];
    // Schedule flush on next microtask (coalesces all changes in current event loop tick)
    if (!_diffFlushTimer) {
      _diffFlushTimer = Promise.resolve().then(_flushDiffBatch);
    }
  }

  function _flushDiffBatch() {
    _diffFlushTimer = null;
    var keys = Object.keys(_pendingDiffs);
    if (keys.length === 0) return;
    _diffBatchCount++;
    // Every 50 batches, send a full SignalSync instead of a diff
    // This lets the relay compact its cache (merge diffs into one sync)
    if (_diffBatchCount % 50 === 0) {
      _streamSync(_signalRegistry);
      _pendingDiffs = {};
      _pendingVersions = {};
      return;
    }
    // Build single frame with all changes
    var obj = { _pid: _peerId, _v: {} };
    for (var i = 0; i < keys.length; i++) {
      var k = keys[i];
      obj[k] = _pendingDiffs[k];
      obj._v[k] = _pendingVersions[k];
    }
    _streamSend(0x31, 0, new TextEncoder().encode(JSON.stringify(obj)));
    _pendingDiffs = {};
    _pendingVersions = {};
  }

  function _streamSync(signals) {
    var state = {};
    for (var name in signals) {
      var sig = signals[name];
      state[name] = (typeof sig === 'object' && sig !== null && '_value' in sig) ? sig._value : sig;
    }
    _streamSend(0x30, 0x02, new TextEncoder().encode(JSON.stringify(state)));
  }

  function _streamSceneState(world, w, h) {
    if (_streamMode === 'signal') {
      var bodies = [];
      for (var b = 0; b < world.body_count(); b++) {
        var p = world.get_body_center(b);
        bodies.push({ x: p[0] | 0, y: p[1] | 0 });
      }
      _streamSend(0x31, 0, new TextEncoder().encode(JSON.stringify({ _bodies: bodies })));
    } else if (_streamMode === 'pixel' || _streamMode === 'delta') {
      if (!_streamCanvas) return;
      var imageData = _streamCanvas.getImageData(0, 0, w, h);
      var pixels = new Uint8Array(imageData.data.buffer);
      if (_streamMode === 'delta' && _streamPrevFrame) {
        var delta = new Uint8Array(pixels.length);
        var zeroCount = 0;
        for (var i = 0; i < pixels.length; i++) {
          delta[i] = pixels[i] ^ _streamPrevFrame[i];
          if (delta[i] === 0) zeroCount++;
        }
        if (zeroCount > pixels.length * 0.3) {
          var rle = _rleEncode(delta);
          _streamSendPixel(0x03, 0x04, w, h, rle);
        } else {
          _streamSendPixel(0x01, 0x02, w, h, pixels);
        }
      } else {
        _streamSendPixel(0x01, 0x02, w, h, pixels);
      }
      _streamPrevFrame = pixels.slice();
    }
  }

  function _handleRemoteInput(type, payload) {
    if (payload.length < 1) return;
    var view = new DataView(payload.buffer, payload.byteOffset);
    switch (type) {
      case 0x01:
      case 0x02:
        emit('remote_pointer', {
          x: view.getUint16(0, true), y: view.getUint16(2, true),
          buttons: payload.length > 4 ? view.getUint8(4) : 0,
          type: type === 0x02 ? 'down' : 'move'
        });
        break;
      case 0x03:
        emit('remote_pointer', { x: 0, y: 0, buttons: 0, type: 'up' });
        break;
      case 0x10:
        emit('remote_key', { keyCode: view.getUint16(0, true), type: 'down' });
        break;
      case 0x11:
        emit('remote_key', { keyCode: view.getUint16(0, true), type: 'up' });
        break;
      case 0x20:
      case 0x21:
        emit('remote_touch', {
          id: payload[0],
          x: view.getUint16(1, true), y: view.getUint16(3, true),
          phase: payload.length > 5 ? payload[5] : (type === 0x21 ? 1 : 0)
        });
        break;
      case 0x30:
        emit('remote_gamepad', {
          axis: payload[0],
          value: view.getInt16(1, true) / 32767
        });
        break;
      case 0x31:
        emit('remote_gamepad_button', {
          button: payload[0],
          pressed: payload[1] === 1,
          analog: payload.length > 2 ? payload[2] / 255 : (payload[1] ? 1 : 0)
        });
        break;
      case 0x50:
        emit('remote_scroll', { dx: view.getInt16(0, true), dy: view.getInt16(2, true) });
        break;
      case 0x60:
        emit('remote_resize', {
          width: view.getUint16(0, true), height: view.getUint16(2, true)
        });
        break;
      case 0x40: // MIDI
        emit('remote_midi', {
          status: payload[0], data1: payload[1], data2: payload.length > 2 ? payload[2] : 0
        });
        break;
      case 0x70: // VoiceInput
        emit('remote_voice', {
          sampleRate: view.getUint16(0, true), channels: payload[2],
          format: payload[3], data: payload.subarray(4)
        });
        break;
      case 0x71: // CameraInput
        emit('remote_camera', {
          width: view.getUint16(0, true), height: view.getUint16(2, true),
          format: payload[4], data: payload.subarray(5)
        });
        break;
      case 0x80: // SensorInput
        emit('remote_sensor', {
          sensorType: payload[0],
          x: view.getInt16(1, true) / 1000,
          y: view.getInt16(3, true) / 1000,
          z: view.getInt16(5, true) / 1000
        });
        break;
      case 0x90: // BciInput
        emit('remote_bci', {
          channelCount: payload[0],
          sampleRate: view.getUint16(1, true),
          data: payload.subarray(3)
        });
        break;
    }
  }

  function _connectStream(url, selectFields) {
    var _csSelect = selectFields || [];
    // Auto-detect bare relay URL and append default receiver path
    var _csUrl = url;
    try {
      var u = new URL(url);
      if (u.pathname === '/' || u.pathname === '') {
        _csUrl = url.replace(/\/$/, '') + '/stream/default';
      }
    } catch(e) {}
    var state = signal({});
    var _csWs = null;
    var _csReconnectDelay = 1000;
    var _csStats = { frames: 0, bytes: 0, reconnects: 0 };
    var _csPixelBuffer = null;
    var _csSchema = null; // cached schema from 0x32
    var _csConnected = false;
    var _csLastPingTime = 0;
    var _csLatency = 0;

    // RLE decoder — 2-byte LE count (matches relay's codec::rle_encode)
    function _csRleDecode(data) {
      var out = [];
      var i = 0;
      while (i < data.length) {
        if (data[i] === 0 && i + 2 < data.length) {
          // 0x00 followed by 2-byte little-endian count
          var count = data[i + 1] | (data[i + 2] << 8);
          for (var j = 0; j < count; j++) out.push(0);
          i += 3;
        } else if (data[i] === 0) {
          // Trailing 0x00 without enough bytes for count — output as literal
          out.push(0);
          i++;
        } else {
          out.push(data[i]);
          i++;
        }
      }
      return new Uint8Array(out);
    }

    // Apply select filter to state object
    function _csFilter(obj) {
      if (_csSelect.length === 0) return obj;
      var filtered = {};
      for (var i = 0; i < _csSelect.length; i++) {
        var k = _csSelect[i];
        if (k in obj) filtered[k] = obj[k];
      }
      return filtered;
    }

    function _csConnect() {
      _csWs = new WebSocket(_csUrl);
      _csWs.binaryType = 'arraybuffer';
      _csWs.onopen = function() {
        console.log('[ds-stream] Receiver connected:', _csUrl);
        _csReconnectDelay = 1000;
        _csConnected = true;
        // Update connection status on signal proxy
        var cur = state._value || {};
        state.value = Object.assign({}, cur, {
          _connected: true, _latency: _csLatency,
          _frames: _csStats.frames, _reconnects: _csStats.reconnects
        });
        // Send subscribe filter (0x33) if select is set
        if (_csSelect.length > 0) {
          var filterPayload = new TextEncoder().encode(JSON.stringify({ select: _csSelect }));
          var msg = new Uint8Array(HEADER_SIZE + filterPayload.length);
          var v = new DataView(msg.buffer);
          v.setUint8(0, 0x33); // SubscribeFilter
          v.setUint8(1, 0);
          v.setUint32(12, filterPayload.length, true);
          msg.set(filterPayload, HEADER_SIZE);
          _csWs.send(msg.buffer);
        }
        // Start periodic ping for RTT measurement
        if (!_csWs._pingInterval) {
          _csWs._pingInterval = setInterval(function() {
            if (_csWs && _csWs.readyState === 1) {
              _csLastPingTime = performance.now();
              var pingPayload = new Uint8Array(0);
              var msg = new Uint8Array(HEADER_SIZE);
              var v = new DataView(msg.buffer);
              v.setUint8(0, 0xFE); // Ping
              _csWs.send(msg.buffer);
            }
          }, 5000);
        }
      };
      _csWs.onmessage = function(e) {
        if (!(e.data instanceof ArrayBuffer) || e.data.byteLength < HEADER_SIZE) return;
        var bytes = new Uint8Array(e.data);
        var view = new DataView(bytes.buffer);
        var type = view.getUint8(0);
        var flags = view.getUint8(1);
        var payloadLen = view.getUint32(12, true);
        var pl = bytes.subarray(HEADER_SIZE, HEADER_SIZE + payloadLen);

        _csStats.frames++;
        _csStats.bytes += e.data.byteLength;

        // Handle input events forwarded from source
        if (flags & 0x01) {
          _handleRemoteInput(type, pl);
          return;
        }

        switch (type) {
          case 0x30: // SignalSync — full state
          case 0x31: // SignalDiff — partial state update
            try {
              var newState = JSON.parse(new TextDecoder().decode(pl));
              // Strip internal sync metadata
              delete newState._pid;
              delete newState._v;
              // Re-synthesize streamed audio on receiver
              if ('_tone' in newState) {
                var t = newState._tone;
                if (t && t.freq > 0) _playTone(t.freq, t.dur || 80, t.type || 'sine');
                delete newState._tone;
              }
              // Apply select filter
              newState = _csFilter(newState);
              // Inject connection metadata
              newState._connected = _csConnected;
              newState._latency = _csLatency;
              newState._frames = _csStats.frames;
              if (type === 0x30) {
                state.value = newState;
              } else {
                state.value = Object.assign({}, state._value || {}, newState);
              }
            } catch(ex) {}
            break;
          case 0x32: // SchemaAnnounce — cache signal names
            try {
              _csSchema = JSON.parse(new TextDecoder().decode(pl));
              console.log('[ds-stream] Schema:', _csSchema.signals || []);
            } catch(ex) {}
            break;
          case 0x01: // Pixels (keyframe)
          case 0x04: // Keyframe
            var w = view.getUint16(8, true);
            var h = view.getUint16(10, true);
            _csPixelBuffer = pl.slice();
            emit('stream_frame', { type: 'keyframe', width: w, height: h, pixels: _csPixelBuffer });
            break;
          case 0x03: // DeltaPixels (XOR + RLE)
            if (_csPixelBuffer) {
              var decoded = _csRleDecode(pl);
              for (var i = 0; i < _csPixelBuffer.length && i < decoded.length; i++) {
                _csPixelBuffer[i] ^= decoded[i];
              }
              emit('stream_frame', { type: 'delta', pixels: _csPixelBuffer });
            }
            break;
          case 0x11: // AudioCompressed (Opus)
            try {
              var audioBlob = new Blob([pl], { type: 'audio/webm;codecs=opus' });
              audioBlob.arrayBuffer().then(function(abuf) {
                var actx = _ensureAudio();
                actx.decodeAudioData(abuf, function(decoded) {
                  var src = actx.createBufferSource();
                  src.buffer = decoded;
                  src.connect(actx.destination);
                  src.start();
                }, function() {});
              });
            } catch(ex) {}
            break;
          case 0x40: // NeuralFrame
            var nw = view.getUint16(8, true), nh = view.getUint16(10, true);
            emit('neural_frame', { type: 'neural', width: nw, height: nh, data: pl });
            break;
          case 0x41: // NeuralAudio
            emit('neural_audio', { data: pl });
            break;
          case 0x42: // NeuralActuator
            emit('neural_actuator', { data: pl });
            break;
          case 0x43: // NeuralLatent
            emit('neural_latent', { data: pl });
            break;
          case 0xFE: // Pong — calculate RTT
            if (_csLastPingTime > 0) {
              _csLatency = Math.round(performance.now() - _csLastPingTime);
              var cur = state._value || {};
              state.value = Object.assign({}, cur, { _latency: _csLatency });
            }
            break;
          case 0xFF: // StreamEnd
            console.log('[ds-stream] Stream ended by source');
            emit('stream_end', {});
            break;
        }
      };
      _csWs.onclose = function() {
        _csConnected = false;
        _csStats.reconnects++;
        var delay = Math.min(_csReconnectDelay, 10000);
        console.log('[ds-stream] Receiver disconnected from', _csUrl, '— reconnecting in', delay, 'ms (attempt', _csStats.reconnects, ')');
        // Update connection status on signal proxy
        var cur = state._value || {};
        state.value = Object.assign({}, cur, {
          _connected: false, _reconnects: _csStats.reconnects
        });
        setTimeout(_csConnect, delay);
        _csReconnectDelay = Math.min(_csReconnectDelay * 1.5, 10000);
      };
      _csWs.onerror = function() {
        _csConnected = false;
      };
    }
    _csConnect();

    // ── Receiver Gamepad Polling ──
    var _csGamepadActive = false;
    var _csLastAxes = {};
    var _csLastButtons = {};
    function _csStartGamepadPolling() {
      if (_csGamepadActive) return;
      _csGamepadActive = true;
      function gpPoll() {
        if (!_csGamepadActive) return;
        var gps = navigator.getGamepads ? navigator.getGamepads() : [];
        for (var gi = 0; gi < gps.length; gi++) {
          var gp = gps[gi];
          if (!gp) continue;
          var gpKey = 'gp' + gi;
          // Axes
          for (var ai = 0; ai < gp.axes.length; ai++) {
            var axisVal = gp.axes[ai];
            var axisKey = gpKey + 'a' + ai;
            var prev = _csLastAxes[axisKey] || 0;
            if (Math.abs(axisVal - prev) > 0.05) { // dead zone
              _csLastAxes[axisKey] = axisVal;
              if (_csWs && _csWs.readyState === 1) {
                var aPayload = new Uint8Array(3);
                aPayload[0] = ai;
                var scaled = Math.round(axisVal * 32767);
                aPayload[1] = scaled & 0xFF;
                aPayload[2] = (scaled >> 8) & 0xFF;
                var aMsg = new Uint8Array(HEADER_SIZE + 3);
                var aV = new DataView(aMsg.buffer);
                aV.setUint8(0, 0x30); // GamepadAxis
                aV.setUint8(1, 0x01); // FLAG_INPUT
                aV.setUint32(12, 3, true);
                aMsg.set(aPayload, HEADER_SIZE);
                _csWs.send(aMsg.buffer);
              }
            }
          }
          // Buttons
          for (var bi = 0; bi < gp.buttons.length; bi++) {
            var btnPressed = gp.buttons[bi].pressed ? 1 : 0;
            var btnAnalog = Math.round(gp.buttons[bi].value * 255);
            var btnKey = gpKey + 'b' + bi;
            var prevBtn = _csLastButtons[btnKey] || 0;
            if (btnPressed !== prevBtn) {
              _csLastButtons[btnKey] = btnPressed;
              if (_csWs && _csWs.readyState === 1) {
                var bPayload = new Uint8Array(3);
                bPayload[0] = bi;
                bPayload[1] = btnPressed;
                bPayload[2] = btnAnalog;
                var bMsg = new Uint8Array(HEADER_SIZE + 3);
                var bV = new DataView(bMsg.buffer);
                bV.setUint8(0, 0x31); // GamepadButton
                bV.setUint8(1, 0x01); // FLAG_INPUT
                bV.setUint32(12, 3, true);
                bMsg.set(bPayload, HEADER_SIZE);
                _csWs.send(bMsg.buffer);
              }
            }
          }
        }
        requestAnimationFrame(gpPoll);
      }
      gpPoll();
    }
    // Auto-start gamepad polling when gamepad is connected
    window.addEventListener('gamepadconnected', function() {
      console.log('[ds-stream] Gamepad connected on receiver, starting polling');
      _csStartGamepadPolling();
    });

    return state;
  }

  // ── Stream Merge ──
  // Merge multiple stream proxies into a single reactive signal.
  function _mergeStreams(sources) {
    var merged = signal({});
    var _mergeState = {};
    for (var i = 0; i < sources.length; i++) {
      (function(src) {
        effect(function() {
          var v = src.value;
          if (v && typeof v === 'object') {
            Object.assign(_mergeState, v);
            merged.value = Object.assign({}, _mergeState);
          }
        });
      })(sources[i]);
    }
    return merged;
  }

  // ── Stream Transform Operators ──
  // Create derived signal proxies via pipe: `stream from '...' | _streamMap(fn)`
  function _streamMap(source, fn) {
    var out = signal(null);
    effect(function() {
      var v = source.value;
      if (v !== undefined && v !== null) out.value = fn(v);
    });
    return out;
  }

  function _streamFilter(source, predicate) {
    var out = signal(source.value);
    effect(function() {
      var v = source.value;
      if (v !== undefined && predicate(v)) out.value = v;
    });
    return out;
  }

  function _streamDebounce(source, ms) {
    var out = signal(source.value);
    var _dbTimer = null;
    effect(function() {
      var v = source.value;
      if (_dbTimer) clearTimeout(_dbTimer);
      _dbTimer = setTimeout(function() { out.value = v; }, ms || 100);
    });
    return out;
  }

  function _streamDistinct(source) {
    var out = signal(source.value);
    var _lastJson = '';
    effect(function() {
      var v = source.value;
      var json = JSON.stringify(v);
      if (json !== _lastJson) { _lastJson = json; out.value = v; }
    });
    return out;
  }

  function _streamThrottle(source, ms) {
    var out = signal(source.value);
    var _tLast = 0;
    effect(function() {
      var v = source.value;
      var now = performance.now();
      if (now - _tLast >= (ms || 100)) { _tLast = now; out.value = v; }
    });
    return out;
  }

  // ── WebRTC Data Channel Transport ──
  var _rtcPc = null;
  var _rtcDc = null;

  function _initWebRTC(signalingUrl, streamUrl, mode) {
    var sigWs = new WebSocket(signalingUrl);
    var pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });
    _rtcPc = pc;

    // Create data channel for frames (source side)
    var dc = pc.createDataChannel('ds-frames', { ordered: false, maxRetransmits: 0 });
    dc.binaryType = 'arraybuffer';
    dc.onopen = function() {
      console.log('[ds-webrtc] Data channel open');
      _rtcDc = dc;
      // Override stream send to use data channel
      var origSend = _streamSend;
      _streamSend = function(type, flags, payload) {
        if (_rtcDc && _rtcDc.readyState === 'open') {
          var ts = (performance.now() - _streamStart) | 0;
          var msg = new Uint8Array(16 + payload.length);
          var v = new DataView(msg.buffer);
          v.setUint8(0, type);
          v.setUint8(1, flags);
          v.setUint16(2, (_streamSeq++) & 0xFFFF, true);
          v.setUint32(4, ts, true);
          v.setUint32(12, payload.length, true);
          msg.set(payload, 16);
          _rtcDc.send(msg.buffer);
        } else {
          origSend(type, flags, payload);
        }
      };
    };
    dc.onclose = function() {
      console.log('[ds-webrtc] Data channel closed, falling back to WebSocket');
      _rtcDc = null;
    };

    // Handle incoming data channels (receiver side)
    pc.ondatachannel = function(event) {
      var incoming = event.channel;
      incoming.binaryType = 'arraybuffer';
      incoming.onmessage = function(e) {
        if (_streamWs && _streamWs.onmessage) {
          _streamWs.onmessage(e);
        }
      };
      incoming.onopen = function() {
        console.log('[ds-webrtc] Incoming data channel open');
        _rtcDc = incoming;
      };
    };

    // ICE candidate exchange
    pc.onicecandidate = function(e) {
      if (e.candidate && sigWs.readyState === 1) {
        sigWs.send(JSON.stringify({ type: 'ice', candidate: e.candidate }));
      }
    };

    // Signaling messages
    sigWs.onmessage = function(e) {
      try {
        var msg = JSON.parse(e.data);
        if (msg.type === 'offer') {
          pc.setRemoteDescription(new RTCSessionDescription(msg.sdp))
            .then(function() { return pc.createAnswer(); })
            .then(function(answer) {
              pc.setLocalDescription(answer);
              sigWs.send(JSON.stringify({ type: 'answer', sdp: answer }));
            });
        } else if (msg.type === 'answer') {
          pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
        } else if (msg.type === 'ice' && msg.candidate) {
          pc.addIceCandidate(new RTCIceCandidate(msg.candidate));
        }
      } catch(ex) {}
    };

    sigWs.onopen = function() {
      console.log('[ds-webrtc] Signaling connected:', signalingUrl);
      // Source creates offer
      pc.createOffer()
        .then(function(offer) {
          pc.setLocalDescription(offer);
          sigWs.send(JSON.stringify({ type: 'offer', sdp: offer }));
        });
    };

    // Fallback: if WebRTC doesn't connect in 5s, use WebSocket
    setTimeout(function() {
      if (!_rtcDc || _rtcDc.readyState !== 'open') {
        console.log('[ds-webrtc] Timeout, falling back to WebSocket');
        _initStream(streamUrl, mode);
      }
    }, 5000);

    // Also init WebSocket as immediate fallback
    _initStream(streamUrl, mode);
  }

  // ── Web Audio Synthesis ──
  var _audioCtx = null;
  function _ensureAudio() {
    if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (_audioCtx.state === 'suspended') _audioCtx.resume();
    return _audioCtx;
  }

  function _playTone(freq, durationMs, type) {
    if (!freq || freq <= 0 || !durationMs || durationMs <= 0) return;
    var ctx = _ensureAudio();
    var osc = ctx.createOscillator();
    var gain = ctx.createGain();
    osc.type = type || 'sine';
    osc.frequency.value = freq;
    gain.gain.setValueAtTime(0.3, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + durationMs / 1000);
    // Stream tone params so receivers can re-synthesize
    if (_streamWs && _streamWs.readyState === 1) {
      _streamDiff('_tone', { freq: freq, dur: durationMs, type: type || 'sine' });
    }
  }

  function _playNoise(durationMs, vol) {
    if (!durationMs || durationMs <= 0) return;
    var ctx = _ensureAudio();
    var bufSize = ctx.sampleRate * (durationMs / 1000);
    var buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
    var data = buf.getChannelData(0);
    for (var i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
    var src = ctx.createBufferSource();
    src.buffer = buf;
    var gain = ctx.createGain();
    gain.gain.setValueAtTime(vol || 0.3, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
    var filt = ctx.createBiquadFilter();
    filt.type = 'highpass';
    filt.frequency.value = 5000;
    src.connect(filt);
    filt.connect(gain);
    gain.connect(ctx.destination);
    src.start();
  }
  // @TREESHAKE:END:streaming

  // ── Build DS object dynamically (only include available features) ──
  var _ds = { signal: signal, derived: derived, effect: effect, batch: batch, flush: flush,
              onEvent: onEvent, emit: emit, keyedList: keyedList,
              resource: resource, fetchJSON: fetchJSON,
              Signal: Signal, Derived: Derived, Effect: Effect };

  // Router (tree-shakeable)
  if (typeof _route !== 'undefined') { _ds.route = _route; _ds.navigate = navigate; _ds.matchRoute = matchRoute; }
  else { _ds.route = { get value() { return '/'; } }; _ds.navigate = function(){}; _ds.matchRoute = function(){ return false; }; }

  // Spring (tree-shakeable)
  if (typeof Spring !== 'undefined') { _ds.spring = spring; _ds.Spring = Spring; }
  else { _ds.spring = function(v) { return typeof v === 'object' ? signal(v.value||0) : signal(v||0); }; }

  // Constraint (tree-shakeable)
  if (typeof constrain !== 'undefined') { _ds.constrain = constrain; _ds.viewport = _viewport; }
  else { _ds.constrain = function(){}; _ds.viewport = { width: signal(0), height: signal(0) }; }

  // Scene (tree-shakeable)
  if (typeof scene !== 'undefined') { _ds.scene = scene; _ds.circle = circle; _ds.rect = rect; _ds.line = line; }
  else { var _noop = function(){ return {}; }; _ds.scene = _noop; _ds.circle = _noop; _ds.rect = _noop; _ds.line = _noop; }

  // Streaming (tree-shakeable)
  if (typeof _initStream !== 'undefined') {
    _ds._initStream = _initStream; _ds._streamDiff = _streamDiff; _ds._streamSync = _streamSync;
    _ds._streamSceneState = _streamSceneState; _ds._connectStream = _connectStream;
    _ds._initWebRTC = _initWebRTC; _ds._registerSignal = _registerSignal;
    _ds._streamAudio = _streamAudio; _ds._stopAudio = _stopAudio;
    _ds._startDomCapture = _startDomCapture; _ds._startAdaptiveQuality = _startAdaptiveQuality;
    _ds._mergeStreams = _mergeStreams;
    _ds._streamMap = _streamMap; _ds._streamFilter = _streamFilter;
    _ds._streamDebounce = _streamDebounce; _ds._streamDistinct = _streamDistinct;
    _ds._streamThrottle = _streamThrottle;
    _ds._playTone = _playTone; _ds._playNoise = _playNoise;
    Object.defineProperty(_ds, '_streamWs', { get: function() { return _streamWs; } });
    Object.defineProperty(_ds, '_rtcDc', { get: function() { return _rtcDc; } });
    Object.defineProperty(_ds, '_streamCanvas', {
      get: function() { return _streamCanvas; },
      set: function(v) { _streamCanvas = v; }
    });
  } else {
    _ds._registerSignal = function(){}; _ds._streamDiff = function(){};
    _ds._initStream = function(){}; _ds._streamSync = function(){};
    _ds._streamSceneState = function(){}; _ds._connectStream = function(){ return signal(null); };
    _ds._initWebRTC = function(){}; _ds._streamAudio = function(){}; _ds._stopAudio = function(){};
    _ds._startDomCapture = function(){}; _ds._startAdaptiveQuality = function(){};
    _ds._mergeStreams = function(){}; _ds._streamMap = function(){}; _ds._streamFilter = function(){};
    _ds._streamDebounce = function(){}; _ds._streamDistinct = function(){};
    _ds._streamThrottle = function(){}; _ds._playTone = function(){}; _ds._playNoise = function(){};
  }
  return _ds;
})();
// ── DOM Helpers (shorthand for generated code) ──
const _c = (t) => document.createElement(t);
const _a = (p, c) => p.appendChild(c);
const _t = (e, t) => { e.textContent = t; };
const _cn = (e, c) => { e.className = c; };
"#;

/// Simple JS minifier — strips blank lines and leading/trailing whitespace.
/// Does NOT strip // comments (to avoid regex literal corruption).
/// Conservative: no variable renaming, no dead code elimination.
fn minify_js(input: &str) -> String {
    let mut result = String::with_capacity(input.len());

    for line in input.lines() {
        let trimmed = line.trim();
        // Skip blank lines
        if trimmed.is_empty() {
            continue;
        }
        result.push_str(trimmed);
        result.push('\n');
    }

    result
}

/// Simple CSS minifier — strips comments, collapses whitespace.
fn minify_css(input: &str) -> String {
    let mut result = String::with_capacity(input.len());
    let chars: Vec<char> = input.chars().collect();
    let len = chars.len();
    let mut i = 0;

    while i < len {
        // Skip CSS comments
        if chars[i] == '/' && i + 1 < len && chars[i + 1] == '*' {
            i += 2;
            while i + 1 < len && !(chars[i] == '*' && chars[i + 1] == '/') {
                i += 1;
            }
            i += 2;
            continue;
        }

        // Collapse whitespace
        if chars[i].is_whitespace() {
            // Only emit a single space if not next to punctuation
            if !result.is_empty() {
                let last = result.chars().last().unwrap_or(' ');
                if !matches!(last, '{' | '}' | ';' | ':' | ',' | '>' | '+' | '~') {
                    result.push(' ');
                }
            }
            while i < len && chars[i].is_whitespace() {
                i += 1;
            }
            // Skip space before punctuation
            if i < len && matches!(chars[i], '{' | '}' | ';' | ':' | ',') {
                // Remove trailing space we just added
                if result.ends_with(' ') {
                    result.pop();
                }
            }
            continue;
        }

        result.push(chars[i]);
        i += 1;
    }

    result
}

/// Detect which optional runtime features the program uses.
fn detect_features(program: &Program) -> HashSet<String> {
    let mut features = HashSet::new();

    for decl in &program.declarations {
        match decl {
            Declaration::Route(_) => { features.insert("router".to_string()); }
            Declaration::Stream(_) => { features.insert("streaming".to_string()); }
            Declaration::Constrain(_) => { features.insert("constraint".to_string()); }
            Declaration::Let(l) => {
                // Check for spring() calls
                if matches!(&l.value, Expr::Call(name, _) if name == "spring") {
                    features.insert("spring".to_string());
                }
                // Check for stream from
                if matches!(&l.value, Expr::StreamFrom { .. }) {
                    features.insert("streaming".to_string());
                }
            }
            Declaration::View(v) => {
                // Check view body for scenes
                if contains_scene(&v.body) {
                    features.insert("scene".to_string());
                }
                // Check for spring references in the view
                if contains_spring_ref(&v.body) {
                    features.insert("spring".to_string());
                }
            }
            _ => {}
        }
    }

    features
}

/// Check if an expression tree contains a Scene container.
fn contains_scene(expr: &Expr) -> bool {
    match expr {
        Expr::Container(c) => {
            if matches!(c.kind, ContainerKind::Scene) { return true; }
            c.children.iter().any(contains_scene)
        }
        _ => false,
    }
}

/// Check if an expression tree references spring-related calls.
fn contains_spring_ref(expr: &Expr) -> bool {
    match expr {
        Expr::Call(name, _) if name == "spring" => true,
        Expr::Container(c) => c.children.iter().any(contains_spring_ref),
        Expr::Element(e) => e.args.iter().any(contains_spring_ref),
        _ => false,
    }
}

/// Strip unused runtime sections based on @TREESHAKE markers.
fn tree_shake_runtime(runtime: &str, used_features: &HashSet<String>) -> String {
    let mut result = String::with_capacity(runtime.len());
    let mut skipping = false;

    for line in runtime.lines() {
        let trimmed = line.trim();

        if let Some(feature) = trimmed.strip_prefix("// @TREESHAKE:BEGIN:") {
            if !used_features.contains(feature.trim()) {
                skipping = true;
                continue;
            }
        }

        if let Some(_feature) = trimmed.strip_prefix("// @TREESHAKE:END:") {
            if skipping {
                skipping = false;
                continue;
            }
        }

        if !skipping {
            result.push_str(line);
            result.push('\n');
        }
    }

    result
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Helper: parse source → emit HTML (not minified).
    fn emit(source: &str) -> String {
        let mut lexer = ds_parser::Lexer::new(source);
        let tokens = lexer.tokenize();
        let mut parser = ds_parser::Parser::new(tokens);
        let program = parser.parse_program().unwrap();
        let graph = ds_analyzer::SignalGraph::from_program(&program);
        let views = ds_analyzer::SignalGraph::analyze_views(&program);
        JsEmitter::emit_html(&program, &graph, &views, false)
    }

    #[test]
    fn test_counter_signals() {
        let html = emit("let count = 0\nview main = text \"hi\"");
        assert!(html.contains("DS.signal(0)"), "should emit DS.signal(0)");
    }

    #[test]
    fn test_derived_signal() {
        let html = emit("let count = 0\nlet doubled = count * 2\nview main = text \"x\"");
        assert!(html.contains("DS.derived("), "should emit DS.derived()");
    }

    #[test]
    fn test_view_container() {
        let html = emit("view main = column [\n  text \"hello\"\n  text \"world\"\n]");
        assert!(html.contains("createElement"), "should create DOM elements");
    }

    #[test]
    fn test_event_handler() {
        let html = emit("let count = 0\nview main = button \"+\" { click: count += 1 }");
        assert!(html.contains("addEventListener"), "should add event listener for click");
        assert!(html.contains("click"), "should reference click event");
    }

    #[test]
    fn test_for_in_loop() {
        let html = emit("let items = [\"a\", \"b\"]\nview main = column [\n  for item in items -> text item\n]");
        assert!(html.contains("keyedList") || html.contains("forEach") || html.contains("for"),
            "should emit list rendering code");
    }

    #[test]
    fn test_when_conditional() {
        let html = emit("let show = true\nview main = column [\n  when show -> text \"visible\"\n]");
        assert!(html.contains("DS.effect("), "should emit effect for conditional");
    }

    #[test]
    fn test_match_expression() {
        let html = emit("let state = \"loading\"\nview main = match state\n  \"loading\" -> text \"Loading...\"\n  \"done\" -> text \"Done!\"");
        // Match should generate some kind of conditional/switch
        assert!(html.contains("DS.effect(") || html.contains("match") || html.contains("==="),
            "should emit match logic");
    }

    #[test]
    fn test_enum_declaration() {
        let html = emit("enum Color { Red, Green, Blue }\nview main = text \"hi\"");
        assert!(html.contains("Object.freeze") || html.contains("Red"),
            "should emit enum object");
    }

    #[test]
    fn test_component_emission() {
        let html = emit("component Card(title) = text title\nview main = Card { title: \"hello\" }");
        // Component should be emitted as a function
        assert!(html.contains("function") || html.contains("Card"),
            "should emit component as function");
    }

    #[test]
    fn test_string_interpolation() {
        let html = emit("let name = \"World\"\nview main = text \"Hello, {name}!\"");
        assert!(html.contains("DS.effect("), "interpolation should use reactive effect");
    }

    #[test]
    fn test_spring_animation() {
        let html = emit("let pos = spring(100, 300, 20)\nview main = text \"x\"");
        assert!(html.contains("DS.spring(") || html.contains("spring"),
            "should emit spring creation");
    }


    #[test]
    fn test_tree_shaking_no_spring() {
        let html = emit("let count = 0\nview main = text \"hi\"");
        // If no spring is used, spring code should be excluded
        assert!(!html.contains("class Spring") || !html.contains("_activeSprings"),
            "should tree-shake unused spring runtime");
    }

    // ── v0.8 Codegen Output Verification ────────────────────

    /// Helper: parse source → emit HTML (minified).
    fn emit_minified(source: &str) -> String {
        let mut lexer = ds_parser::Lexer::new(source);
        let tokens = lexer.tokenize();
        let mut parser = ds_parser::Parser::new(tokens);
        let program = parser.parse_program().unwrap();
        let graph = ds_analyzer::SignalGraph::from_program(&program);
        let views = ds_analyzer::SignalGraph::analyze_views(&program);
        JsEmitter::emit_html(&program, &graph, &views, true)
    }

    #[test]
    fn test_route_emission() {
        let html = emit("view home = text \"Home\"\nroute \"/\" -> home\nroute \"/about\" -> home");
        // Routes should emit navigation/routing logic
        assert!(html.contains("/") || html.contains("route") || html.contains("path"),
            "should emit route-related code");
    }

    #[test]
    fn test_layout_constraints() {
        let html = emit("layout dashboard {\n  sidebar.width == 250\n}\nview main = text \"x\"");
        // Layout constraints should emit solver or dimension code
        assert!(!html.is_empty(), "layout program should produce output");
    }

    #[test]
    fn test_every_timer() {
        let html = emit("let tick = 0\nevery 16 -> tick = tick + 1\nview main = text tick");
        assert!(html.contains("setInterval") || html.contains("every") || html.contains("16"),
            "timer should emit setInterval: {}", &html[..html.len().min(500)]);
    }

    #[test]
    fn test_stream_declaration() {
        let html = emit("view main = text \"x\"\nstream main on \"ws://localhost:9100\" { mode: signal }");
        assert!(html.contains("WebSocket") || html.contains("ws://") || html.contains("9100"),
            "stream should emit WebSocket connection code");
    }

    #[test]
    fn test_component_with_slots() {
        let html = emit("component Card(title) =\n  column [\n    text title\n    slot\n  ]\nview main = Card { title: \"hi\" } [\n  text \"child content\"\n]");
        assert!(html.contains("child content") || html.contains("slot"),
            "component with children should render slot content");
    }

    #[test]
    fn test_nested_when_else() {
        let html = emit("let a = true\nlet b = true\nview main = column [\n  when a ->\n    when b -> text \"both true\"\n  else -> text \"a is false\"\n]");
        assert!(html.contains("DS.effect("), "nested when should use effects");
        assert!(html.contains("both true"), "should contain inner text");
    }

    #[test]
    fn test_style_bindings() {
        let html = emit("view main = text \"styled\" { color: \"red\", fontSize: \"24px\" }");
        assert!(html.contains("red") || html.contains("style"),
            "style bindings should emit style attributes");
    }

    #[test]
    fn test_export_wrapper() {
        let html = emit("export let theme = \"dark\"\nview main = text theme");
        // Export wraps inner let — should produce valid output without panic
        assert!(!html.is_empty(), "exported signal program should produce output");
        assert!(html.contains("createElement") || html.contains("textContent") || html.contains("DS."),
            "should emit DOM or signal code");
    }

    #[test]
    fn test_import_code() {
        let html = emit("import { Card } from \"./components\"\nview main = text \"x\"");
        // Import should not crash and should produce valid output
        assert!(!html.is_empty(), "program with import should still produce output");
    }

    #[test]
    fn test_reactive_each() {
        let html = emit("let items = [\"a\", \"b\", \"c\"]\nview main = column [\n  each item in items -> text item\n]");
        assert!(html.contains("forEach") || html.contains("each") || html.contains("keyedList"),
            "each loop should emit list rendering");
    }

    #[test]
    fn test_doc_comment_passthrough() {
        let html = emit("/// Counter signal\nlet count = 0\nview main = text count");
        // Doc comments should not break compilation
        assert!(html.contains("DS.signal(0)"), "signal should still emit with doc comment");
    }

    #[test]
    fn test_minify_produces_compact_output() {
        let normal = emit("let count = 0\nview main = text \"hi\"");
        let minified = emit_minified("let count = 0\nview main = text \"hi\"");
        assert!(minified.len() <= normal.len(),
            "minified ({}) should be <= normal ({})", minified.len(), normal.len());
        assert!(minified.contains("DS.signal(0)"));
    }

    // ── v0.9 Rust-Like Match Codegen Tests ──────────────────

    #[test]
    fn test_match_constructor_binding() {
        let html = emit("let r = 0\nview main = match r\n  Ok(v) -> text \"good\"\n  Error(e) -> text \"bad\"");
        // Constructor match should use .tag check
        assert!(html.contains(".tag") || html.contains("==="),
            "should emit tag-based constructor check");
    }

    #[test]
    fn test_match_wildcard_fallback() {
        let html = emit("let x = 1\nview main = match x\n  0 -> text \"zero\"\n  _ -> text \"other\"");
        assert!(html.contains("other"), "wildcard arm body should be emitted");
        assert!(html.contains("DS.effect("), "match should use reactive effect");
    }

    #[test]
    fn test_match_let_expression() {
        let html = emit("let status = \"ok\"\nlet msg = match status\n  \"loading\" -> \"wait\"\n  \"ok\" -> \"done\"\n  _ -> \"??\"\nview main = text msg");
        assert!(html.contains("==="), "expression match should use ternary with ===");
    }

    #[test]
    fn test_match_int_literal_codegen() {
        let html = emit("let n = 42\nview main = match n\n  0 -> text \"zero\"\n  1 -> text \"one\"\n  _ -> text \"other\"");
        assert!(html.contains("=== 0") || html.contains("=== 1"),
            "int literal patterns should emit === checks");
    }

    #[test]
    fn test_match_bool_literal_codegen() {
        let html = emit("let flag = true\nview main = match flag\n  true -> text \"yes\"\n  false -> text \"no\"");
        assert!(html.contains("true") && html.contains("false"),
            "bool patterns should be present in output");
    }
}



