//! Windowed application runner
//!
//! Provides a unified API for running windowed Blinc applications across
//! desktop and Android platforms.
//!
//! # Example
//!
//! ```ignore
//! use blinc_app::prelude::*;
//! use blinc_app::windowed::WindowedApp;
//!
//! fn main() -> Result<()> {
//!     WindowedApp::run(WindowConfig::default(), |ctx| {
//!         // Build your UI using reactive signals
//!         let count = ctx.use_signal(0);
//!         let doubled = ctx.use_derived(move |cx| cx.get(count).unwrap_or(0) * 2);
//!
//!         div().w_full().h_full()
//!             .flex_center()
//!             .child(text(&format!("Count: {}", ctx.get(count).unwrap_or(0))).size(48.0))
//!     })
//! }
//! ```

use std::hash::Hash;
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc, Mutex,
};

use blinc_animation::{
    AnimatedTimeline, AnimatedValue, AnimationContext, AnimationScheduler, SchedulerHandle,
    SharedAnimatedTimeline, SharedAnimatedValue, SpringConfig,
};
use blinc_core::context_state::{BlincContextState, HookState, SharedHookState, StateKey};
use blinc_core::reactive::{Derived, ReactiveGraph, Signal, SignalId, State, StatefulDepsCallback};
use blinc_layout::overlay_state::{get_overlay_manager, OverlayContext};
use blinc_layout::prelude::*;
use blinc_layout::widgets::overlay::{overlay_manager, OverlayManager, OverlayManagerExt};
use blinc_platform::{
    ControlFlow, Event, EventLoop, InputEvent, Key, KeyState, LifecycleEvent, MouseEvent, Platform,
    TouchEvent, Window, WindowConfig, WindowEvent,
};

use crate::app::BlincApp;
use crate::error::{BlincError, Result};

/// Shared animation scheduler for the application (thread-safe)
pub type SharedAnimationScheduler = Arc<Mutex<AnimationScheduler>>;

// SharedAnimatedValue and SharedAnimatedTimeline are re-exported from blinc_animation

#[cfg(all(feature = "windowed", not(target_os = "android")))]
use blinc_platform_desktop::DesktopPlatform;

/// Shared dirty flag type for element refs
pub type RefDirtyFlag = Arc<AtomicBool>;

/// Shared reactive graph for the application (thread-safe)
pub type SharedReactiveGraph = Arc<Mutex<ReactiveGraph>>;

/// Shared element registry for query API (thread-safe)
pub type SharedElementRegistry = Arc<blinc_layout::selector::ElementRegistry>;

/// Callback type for on_ready handlers
pub type ReadyCallback = Box<dyn FnOnce() + Send + Sync>;

/// Shared storage for ready callbacks
pub type SharedReadyCallbacks = Arc<Mutex<Vec<ReadyCallback>>>;

/// UI builder function for a window. Called each frame to produce the UI tree.
/// Returns a `Div` (the root element type for all Blinc UIs).
pub type WindowBuilder = Box<dyn FnMut(&mut WindowedContext) -> Div + Send>;

/// Pending window request: config + optional UI builder
struct PendingWindowRequest {
    config: WindowConfig,
    builder: Option<WindowBuilder>,
}

/// Queue of pending window requests (builder closures waiting to be picked up
/// by the event loop after AppCommand::CreateWindow fires).
static PENDING_WINDOW_BUILDERS: std::sync::OnceLock<Mutex<Vec<PendingWindowRequest>>> =
    std::sync::OnceLock::new();

fn pending_builders() -> &'static Mutex<Vec<PendingWindowRequest>> {
    PENDING_WINDOW_BUILDERS.get_or_init(|| Mutex::new(Vec::new()))
}

/// Global callback for sending CreateWindow command to the event loop.
static OPEN_WINDOW_FN: std::sync::OnceLock<Arc<dyn Fn(WindowConfig) + Send + Sync>> =
    std::sync::OnceLock::new();

/// Global callback for initiating a window drag operation (custom title bars).
static DRAG_WINDOW_FN: std::sync::OnceLock<Arc<dyn Fn() + Send + Sync>> =
    std::sync::OnceLock::new();

/// Start a window drag operation (for custom title bars).
///
/// Call this from a mouse-down handler on a draggable element.
/// The OS takes over and the window follows the cursor until release.
pub fn drag_window() {
    if let Some(f) = DRAG_WINDOW_FN.get() {
        f();
    }
}

/// Open a new window with a UI builder from anywhere in the application.
///
/// The builder closure is called each frame to produce the window's UI.
///
/// # Example
/// ```ignore
/// use blinc_app::windowed::open_window_with;
///
/// open_window_with(
///     WindowConfig::new("Settings").size(400, 300),
///     |ctx| {
///         Box::new(div()
///             .w(ctx.width).h(ctx.height)
///             .bg(Color::rgb(0.1, 0.1, 0.15))
///             .child(text("Settings Window").size(24.0).color(Color::WHITE)))
///     },
/// );
/// ```
pub fn open_window_with<F>(config: WindowConfig, builder: F)
where
    F: FnMut(&mut WindowedContext) -> Div + Send + 'static,
{
    // Queue the builder so the event loop can pick it up
    pending_builders()
        .lock()
        .unwrap()
        .push(PendingWindowRequest {
            config: config.clone(),
            builder: Some(Box::new(builder)),
        });

    // Send the CreateWindow command to the event loop
    if let Some(f) = OPEN_WINDOW_FN.get() {
        f(config);
    } else {
        tracing::warn!("open_window_with() called before app initialization");
    }
}

/// Open a new window with a default blank UI.
///
/// For windows with custom UI, use `open_window_with()` instead.
pub fn open_window(config: WindowConfig) {
    // Queue without builder (uses default UI)
    pending_builders()
        .lock()
        .unwrap()
        .push(PendingWindowRequest {
            config: config.clone(),
            builder: None,
        });

    if let Some(f) = OPEN_WINDOW_FN.get() {
        f(config);
    } else {
        tracing::warn!("open_window() called before app initialization");
    }
}

/// Per-window state bundle.
///
/// Groups all state that is specific to a single window, extracted from the
/// monolithic event loop closure. This is the foundation for multi-window support.
#[cfg(all(feature = "windowed", not(target_os = "android")))]
pub(crate) struct WindowState {
    /// GPU app (renderer, device, queue)
    pub app: Option<BlincApp>,
    /// Window surface for rendering
    pub surface: Option<wgpu::Surface<'static>>,
    /// Surface configuration
    pub surface_config: Option<wgpu::SurfaceConfiguration>,
    /// UI context (dimensions, event router, shared handles)
    pub ctx: Option<WindowedContext>,
    /// Render tree (layout + render nodes)
    pub render_tree: Option<RenderTree>,
    /// Render state (cursor blink, animated values, motion)
    pub render_state: Option<blinc_layout::RenderState>,
    /// CSS animation/transition store
    pub css_anim_store: Arc<Mutex<blinc_layout::CssAnimationStore>>,
    /// Shared motion animation states
    pub shared_motion_states:
        Arc<std::sync::RwLock<std::collections::HashMap<String, blinc_core::MotionAnimationState>>>,
    /// Whether the UI tree needs rebuilding
    pub needs_rebuild: bool,
    /// Whether layout needs recomputing
    pub needs_relayout: bool,
    /// Last frame timestamp for CSS animation delta
    pub last_frame_time_ms: u64,
    /// Active touch point IDs
    pub active_touch_ids: std::collections::HashSet<u64>,
    /// UI builder for this window (None = default static UI)
    pub ui_builder: Option<WindowBuilder>,
}

#[cfg(all(feature = "windowed", not(target_os = "android")))]
impl WindowState {
    /// Create a new empty WindowState with shared resources
    pub fn new(
        css_anim_store: Arc<Mutex<blinc_layout::CssAnimationStore>>,
        shared_motion_states: Arc<
            std::sync::RwLock<std::collections::HashMap<String, blinc_core::MotionAnimationState>>,
        >,
    ) -> Self {
        Self {
            app: None,
            surface: None,
            surface_config: None,
            ctx: None,
            render_tree: None,
            render_state: None,
            css_anim_store,
            shared_motion_states,
            needs_rebuild: true,
            needs_relayout: false,
            last_frame_time_ms: 0,
            active_touch_ids: std::collections::HashSet::new(),
            ui_builder: None,
        }
    }
}

/// Context passed to the UI builder function
pub struct WindowedContext {
    /// Current window width in logical pixels (for UI layout)
    ///
    /// This is the width you should use when building UI. It automatically
    /// accounts for DPI scaling, so elements sized to `ctx.width` will
    /// fill the window regardless of display scale factor.
    pub width: f32,
    /// Current window height in logical pixels (for UI layout)
    pub height: f32,
    /// Current scale factor (physical / logical)
    pub scale_factor: f64,
    /// Safe area insets (top, right, bottom, left) in logical pixels.
    /// On mobile: notch, status bar, home indicator.
    /// On desktop: all zeros.
    pub safe_area: (f32, f32, f32, f32),
    /// Physical window width (for internal use)
    pub(crate) physical_width: f32,
    /// Physical window height (for internal use)
    pub(crate) physical_height: f32,
    /// Whether the window is focused
    pub focused: bool,
    /// Number of completed UI rebuilds (0 = first build in progress)
    ///
    /// Use `is_ready()` to check if the UI has been built at least once.
    /// This is useful for triggering animations after motion bindings are registered.
    pub rebuild_count: u32,
    /// Event router for input event handling
    pub event_router: EventRouter,
    /// Animation scheduler for spring/keyframe animations
    pub animations: SharedAnimationScheduler,
    /// Shared dirty flag for element refs - when set, triggers UI rebuild
    ref_dirty_flag: RefDirtyFlag,
    /// Reactive graph for signal-based state management
    reactive: SharedReactiveGraph,
    /// Hook state for call-order based signal persistence
    hooks: SharedHookState,
    /// Overlay manager for modals, dialogs, toasts, etc.
    overlay_manager: OverlayManager,
    /// Whether overlays were visible last frame (for triggering rebuilds)
    had_visible_overlays: bool,
    /// Element registry for query API (shared with RenderTree)
    element_registry: SharedElementRegistry,
    /// Callbacks to run after UI is ready (motion bindings registered)
    ready_callbacks: SharedReadyCallbacks,
    /// CSS stylesheet for automatic style application (hover, animations, base styles)
    /// Multiple stylesheets cascade — later rules override earlier ones.
    pub stylesheet: Option<Arc<blinc_layout::css_parser::Stylesheet>>,
    /// Raw CSS source strings, preserved for reparsing on theme changes.
    /// Each entry corresponds to one `add_css()` call, in order.
    css_sources: Vec<String>,
    /// Continuous pointer query state (per-element pointer tracking)
    pub pointer_query: blinc_layout::pointer_query::PointerQueryState,
    /// Callback to request opening a new window (set by desktop runner)
    open_window_fn: Option<Arc<dyn Fn(WindowConfig) + Send + Sync>>,
    /// Per-window close callback (sends CloseWindow command for THIS window)
    close_fn: Option<Arc<dyn Fn() + Send + Sync>>,
    /// Per-window drag callback (starts OS drag for THIS window)
    drag_fn: Option<Arc<dyn Fn() + Send + Sync>>,
    /// Per-window minimize callback
    minimize_fn: Option<Arc<dyn Fn() + Send + Sync>>,
    /// Per-window maximize callback
    maximize_fn: Option<Arc<dyn Fn() + Send + Sync>>,
}

impl WindowedContext {
    #[allow(clippy::too_many_arguments)]
    fn from_window<W: Window>(
        window: &W,
        event_router: EventRouter,
        animations: SharedAnimationScheduler,
        ref_dirty_flag: RefDirtyFlag,
        reactive: SharedReactiveGraph,
        hooks: SharedHookState,
        overlay_mgr: OverlayManager,
        element_registry: SharedElementRegistry,
        ready_callbacks: SharedReadyCallbacks,
    ) -> Self {
        // Get physical size (actual surface pixels) and scale factor
        let (physical_width, physical_height) = window.size();
        let scale_factor = window.scale_factor();

        // Compute logical size (what users work with in their UI code)
        // This ensures elements sized with ctx.width/height fill the window
        // regardless of DPI, and font sizes appear consistent across displays
        let logical_width = physical_width as f32 / scale_factor as f32;
        let logical_height = physical_height as f32 / scale_factor as f32;

        Self {
            width: logical_width,
            height: logical_height,
            scale_factor,
            safe_area: window.safe_area_insets(),
            physical_width: physical_width as f32,
            physical_height: physical_height as f32,
            focused: window.is_focused(),
            rebuild_count: 0,
            event_router,
            animations,
            ref_dirty_flag,
            reactive,
            hooks,
            overlay_manager: overlay_mgr,
            had_visible_overlays: false,
            element_registry,
            ready_callbacks,
            stylesheet: None,
            css_sources: Vec::new(),
            pointer_query: blinc_layout::pointer_query::PointerQueryState::new(),
            open_window_fn: None,
            close_fn: None,
            drag_fn: None,
            minimize_fn: None,
            maximize_fn: None,
        }
    }

    /// Create a WindowedContext for Android
    ///
    /// This is used by the Android runner since it doesn't have a Window trait implementation.
    #[cfg(all(feature = "android", target_os = "android"))]
    pub(crate) fn new_android(
        logical_width: f32,
        logical_height: f32,
        scale_factor: f64,
        physical_width: f32,
        physical_height: f32,
        focused: bool,
        animations: SharedAnimationScheduler,
        ref_dirty_flag: RefDirtyFlag,
        reactive: SharedReactiveGraph,
        hooks: SharedHookState,
        overlay_mgr: OverlayManager,
        element_registry: SharedElementRegistry,
        ready_callbacks: SharedReadyCallbacks,
    ) -> Self {
        Self {
            width: logical_width,
            height: logical_height,
            scale_factor,
            safe_area: (0.0, 0.0, 0.0, 0.0),
            physical_width,
            physical_height,
            focused,
            rebuild_count: 0,
            event_router: EventRouter::new(),
            animations,
            ref_dirty_flag,
            reactive,
            hooks,
            overlay_manager: overlay_mgr,
            had_visible_overlays: false,
            element_registry,
            ready_callbacks,
            stylesheet: None,
            css_sources: Vec::new(),
            pointer_query: blinc_layout::pointer_query::PointerQueryState::new(),
            open_window_fn: None,
            close_fn: None,
            drag_fn: None,
            minimize_fn: None,
            maximize_fn: None,
        }
    }

    /// Create a WindowedContext for iOS
    ///
    /// This is used by the iOS runner since it doesn't have a Window trait implementation.
    #[cfg(all(feature = "ios", target_os = "ios"))]
    pub(crate) fn new_ios(
        logical_width: f32,
        logical_height: f32,
        scale_factor: f64,
        physical_width: f32,
        physical_height: f32,
        focused: bool,
        animations: SharedAnimationScheduler,
        ref_dirty_flag: RefDirtyFlag,
        reactive: SharedReactiveGraph,
        hooks: SharedHookState,
        overlay_mgr: OverlayManager,
        element_registry: SharedElementRegistry,
        ready_callbacks: SharedReadyCallbacks,
    ) -> Self {
        Self {
            width: logical_width,
            height: logical_height,
            scale_factor,
            safe_area: (0.0, 0.0, 0.0, 0.0),
            physical_width,
            physical_height,
            focused,
            rebuild_count: 0,
            event_router: EventRouter::new(),
            animations,
            ref_dirty_flag,
            reactive,
            hooks,
            overlay_manager: overlay_mgr,
            had_visible_overlays: false,
            element_registry,
            ready_callbacks,
            stylesheet: None,
            css_sources: Vec::new(),
            pointer_query: blinc_layout::pointer_query::PointerQueryState::new(),
            open_window_fn: None,
            close_fn: None,
            drag_fn: None,
            minimize_fn: None,
            maximize_fn: None,
        }
    }

    /// Create a WindowedContext for Fuchsia
    ///
    /// This is used by the Fuchsia runner since it doesn't have a Window trait implementation.
    #[cfg(all(feature = "fuchsia", target_os = "fuchsia"))]
    pub(crate) fn new_fuchsia(
        logical_width: f32,
        logical_height: f32,
        scale_factor: f64,
        physical_width: f32,
        physical_height: f32,
        focused: bool,
        animations: SharedAnimationScheduler,
        ref_dirty_flag: RefDirtyFlag,
        reactive: SharedReactiveGraph,
        hooks: SharedHookState,
        overlay_mgr: OverlayManager,
        element_registry: SharedElementRegistry,
        ready_callbacks: SharedReadyCallbacks,
    ) -> Self {
        Self {
            width: logical_width,
            height: logical_height,
            scale_factor,
            safe_area: (0.0, 0.0, 0.0, 0.0),
            physical_width,
            physical_height,
            focused,
            rebuild_count: 0,
            event_router: EventRouter::new(),
            animations,
            ref_dirty_flag,
            reactive,
            hooks,
            overlay_manager: overlay_mgr,
            had_visible_overlays: false,
            element_registry,
            ready_callbacks,
            stylesheet: None,
            css_sources: Vec::new(),
            pointer_query: blinc_layout::pointer_query::PointerQueryState::new(),
            open_window_fn: None,
            close_fn: None,
            drag_fn: None,
            minimize_fn: None,
            maximize_fn: None,
        }
    }

    /// Update context from window (preserving event router, dirty flag, and reactive graph)
    fn update_from_window<W: Window>(&mut self, window: &W) {
        let (physical_width, physical_height) = window.size();
        let scale_factor = window.scale_factor();

        self.physical_width = physical_width as f32;
        self.physical_height = physical_height as f32;
        self.width = physical_width as f32 / scale_factor as f32;
        self.height = physical_height as f32 / scale_factor as f32;
        self.scale_factor = scale_factor;
        self.focused = window.is_focused();
    }

    // =========================================================================
    // DPI-Related Helpers
    // =========================================================================

    /// Get the physical window width (for advanced use cases)
    ///
    /// Most users should use `ctx.width` which is in logical pixels.
    /// Physical dimensions are only needed when directly interfacing
    /// with GPU surfaces or platform-specific code.
    pub fn physical_width(&self) -> f32 {
        self.physical_width
    }

    /// Get the physical window height (for advanced use cases)
    pub fn physical_height(&self) -> f32 {
        self.physical_height
    }

    /// Check if the UI is ready (has completed at least one rebuild)
    ///
    /// This is useful for triggering animations after the first UI build,
    /// when motion bindings have been registered with the renderer.
    ///
    /// # Example
    ///
    /// ```ignore
    /// fn my_component(ctx: &WindowedContext) -> impl ElementBuilder {
    ///     let progress = ctx.use_animated_value_for("progress", 0.0, SpringConfig::gentle());
    ///
    ///     // Only trigger animation after UI is ready
    ///     let triggered = ctx.use_state_keyed("triggered", || false);
    ///     if ctx.is_ready() && !triggered.get() {
    ///         triggered.set(true);
    ///         progress.lock().unwrap().set_target(100.0);
    ///     }
    ///
    ///     // ... build UI ...
    /// }
    /// ```
    pub fn is_ready(&self) -> bool {
        self.rebuild_count > 0
    }

    /// Safe area inset from the top (status bar, notch)
    pub fn safe_top(&self) -> f32 {
        self.safe_area.0
    }

    /// Safe area inset from the right
    pub fn safe_right(&self) -> f32 {
        self.safe_area.1
    }

    /// Safe area inset from the bottom (home indicator)
    pub fn safe_bottom(&self) -> f32 {
        self.safe_area.2
    }

    /// Safe area inset from the left
    pub fn safe_left(&self) -> f32 {
        self.safe_area.3
    }

    /// Content width excluding safe area insets
    pub fn safe_width(&self) -> f32 {
        self.width - self.safe_area.1 - self.safe_area.3
    }

    /// Content height excluding safe area insets
    pub fn safe_height(&self) -> f32 {
        self.height - self.safe_area.0 - self.safe_area.2
    }

    /// Open a new window with the given configuration.
    ///
    /// The window is created asynchronously on the next event loop tick.
    /// Only available on desktop platforms.
    ///
    /// # Example
    ///
    /// ```ignore
    /// ctx.open_window(WindowConfig::new("Settings").size(400, 300));
    /// ```
    pub fn open_window(&self, config: WindowConfig) {
        if let Some(ref open_fn) = self.open_window_fn {
            open_fn(config);
        } else {
            tracing::warn!(
                "open_window() called but no window creation callback is set (not on desktop?)"
            );
        }
    }

    /// Set the callback for opening new windows (called by the desktop runner)
    pub(crate) fn set_open_window_fn(&mut self, f: Arc<dyn Fn(WindowConfig) + Send + Sync>) {
        self.open_window_fn = Some(f);
    }

    /// Set per-window action callbacks (called by the desktop runner)
    pub(crate) fn set_window_actions(
        &mut self,
        close: Arc<dyn Fn() + Send + Sync>,
        drag: Arc<dyn Fn() + Send + Sync>,
        minimize: Arc<dyn Fn() + Send + Sync>,
        maximize: Arc<dyn Fn() + Send + Sync>,
    ) {
        self.close_fn = Some(close);
        self.drag_fn = Some(drag);
        self.minimize_fn = Some(minimize);
        self.maximize_fn = Some(maximize);
    }

    /// Close THIS window. Safe to call from any click handler.
    pub fn close(&self) {
        if let Some(ref f) = self.close_fn {
            f();
        }
    }

    /// Start dragging THIS window (for custom title bars).
    pub fn drag(&self) {
        if let Some(ref f) = self.drag_fn {
            f();
        }
    }

    /// Minimize THIS window.
    pub fn minimize(&self) {
        if let Some(ref f) = self.minimize_fn {
            f();
        }
    }

    /// Maximize/restore THIS window.
    pub fn maximize(&self) {
        if let Some(ref f) = self.maximize_fn {
            f();
        }
    }

    /// Get a cloneable close callback for THIS window.
    /// Use this to capture the close action in event handler closures.
    pub fn close_callback(&self) -> Arc<dyn Fn() + Send + Sync> {
        self.close_fn.clone().unwrap_or_else(|| Arc::new(|| {}))
    }

    /// Get a cloneable drag callback for THIS window.
    pub fn drag_callback(&self) -> Arc<dyn Fn() + Send + Sync> {
        self.drag_fn.clone().unwrap_or_else(|| Arc::new(|| {}))
    }

    /// Get a cloneable minimize callback for THIS window.
    pub fn minimize_callback(&self) -> Arc<dyn Fn() + Send + Sync> {
        self.minimize_fn.clone().unwrap_or_else(|| Arc::new(|| {}))
    }

    /// Get a cloneable maximize callback for THIS window.
    pub fn maximize_callback(&self) -> Arc<dyn Fn() + Send + Sync> {
        self.maximize_fn.clone().unwrap_or_else(|| Arc::new(|| {}))
    }

    /// Register a callback to run once after the UI is ready
    ///
    /// The callback will be executed after the first UI rebuild completes,
    /// when motion bindings have been registered with the renderer.
    /// This is the recommended way to trigger initial animations.
    ///
    /// Callbacks are executed once and then discarded. If `is_ready()` is
    /// already true, the callback will run on the next frame.
    ///
    /// # Example
    ///
    /// ```ignore
    /// fn my_component(ctx: &WindowedContext) -> impl ElementBuilder {
    ///     let progress = ctx.use_animated_value_for("progress", 0.0, SpringConfig::gentle());
    ///
    ///     // Register animation to trigger when UI is ready
    ///     let progress_clone = progress.clone();
    ///     ctx.on_ready(move || {
    ///         if let Ok(mut value) = progress_clone.lock() {
    ///             value.set_target(100.0);
    ///         }
    ///     });
    ///
    ///     // ... build UI ...
    /// }
    /// ```
    /// Register a callback to run once when the UI is ready (context-level).
    ///
    /// **Note:** For element-specific callbacks, prefer using the query API:
    /// ```ignore
    /// ctx.query_element("my-element").on_ready(|bounds| {
    ///     // Triggered once after element is laid out
    /// });
    /// ```
    /// The query-based approach uses stable string IDs that survive tree rebuilds.
    ///
    /// This context-level callback runs after the first rebuild completes.
    /// If called after the UI is already ready, executes immediately.
    pub fn on_ready<F>(&self, callback: F)
    where
        F: FnOnce() + Send + Sync + 'static,
    {
        // If already ready, execute immediately
        if self.rebuild_count > 0 {
            callback();
            return;
        }
        // Otherwise queue for execution after first rebuild
        if let Ok(mut callbacks) = self.ready_callbacks.lock() {
            callbacks.push(Box::new(callback));
        }
    }

    // =========================================================================
    // Reactive Signal API
    // =========================================================================

    /// Create a persistent state value that survives across UI rebuilds (keyed)
    ///
    /// This creates component-level state identified by a unique string key.
    /// Returns a `State<T>` with direct `.get()` and `.set()` methods.
    ///
    /// For stateful UI elements with `StateTransitions`, prefer `use_state(initial)`
    /// which auto-keys by source location.
    ///
    /// # Example
    ///
    /// ```ignore
    /// fn my_button(ctx: &WindowedContext, id: &str) -> impl ElementBuilder {
    ///     // Each button gets its own hover state, keyed by id
    ///     let hovered = ctx.use_state_keyed(id, || false);
    ///
    ///     div()
    ///         .bg(if hovered.get() { Color::RED } else { Color::BLUE })
    ///         .on_hover_enter({
    ///             let hovered = hovered.clone();
    ///             move |_| hovered.set(true)
    ///         })
    ///         .on_hover_leave({
    ///             let hovered = hovered.clone();
    ///             move |_| hovered.set(false)
    ///         })
    /// }
    /// ```
    pub fn use_state_keyed<T, F>(&self, key: &str, init: F) -> State<T>
    where
        T: Clone + Send + 'static,
        F: FnOnce() -> T,
    {
        use blinc_core::reactive::SignalId;

        let state_key = StateKey::from_string::<T>(key);
        let mut hooks = self.hooks.lock().unwrap();

        // Check if we have an existing signal with this key
        let signal = if let Some(raw_id) = hooks.get(&state_key) {
            // Reconstruct the signal from stored ID
            let signal_id = SignalId::from_raw(raw_id);
            Signal::from_id(signal_id)
        } else {
            // First time - create a new signal and store it
            let signal = self.reactive.lock().unwrap().create_signal(init());
            let raw_id = signal.id().to_raw();
            hooks.insert(state_key, raw_id);
            signal
        };

        // Create callback for stateful deps notification
        let callback: StatefulDepsCallback = Arc::new(|signal_ids| {
            blinc_layout::check_stateful_deps(signal_ids);
        });

        State::with_stateful_callback(
            signal,
            Arc::clone(&self.reactive),
            Arc::clone(&self.ref_dirty_flag),
            callback,
        )
    }

    /// Create a persistent signal that survives across UI rebuilds (keyed)
    ///
    /// Unlike `use_signal()` which creates a new signal each call, this method
    /// persists the signal using a unique string key. Use this for simple
    /// reactive values that need to survive rebuilds.
    ///
    /// For FSM-based state with `StateTransitions`, use `use_state_keyed()` instead.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let current_index = ctx.use_signal_keyed("current_index", || 0usize);
    ///
    /// // Read the value
    /// let index = ctx.get(current_index).unwrap_or(0);
    ///
    /// // Set the value (in an event handler)
    /// ctx.set(current_index, 1);
    /// ```
    pub fn use_signal_keyed<T, F>(&self, key: &str, init: F) -> Signal<T>
    where
        T: Clone + Send + 'static,
        F: FnOnce() -> T,
    {
        use blinc_core::reactive::SignalId;

        let state_key = StateKey::from_string::<T>(key);
        let mut hooks = self.hooks.lock().unwrap();

        // Check if we have an existing signal with this key
        if let Some(raw_id) = hooks.get(&state_key) {
            // Reconstruct the signal from stored ID
            let signal_id = SignalId::from_raw(raw_id);
            Signal::from_id(signal_id)
        } else {
            // First time - create a new signal and store it
            let signal = self.reactive.lock().unwrap().create_signal(init());
            let raw_id = signal.id().to_raw();
            hooks.insert(state_key, raw_id);
            signal
        }
    }

    /// Create a persistent ScrollRef for programmatic scroll control
    ///
    /// This creates a ScrollRef that survives across UI rebuilds. Use `.bind()`
    /// on a scroll widget to connect it, then call methods like `.scroll_to()`
    /// to programmatically control scrolling.
    ///
    /// # Example
    ///
    /// ```ignore
    /// fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    ///     let scroll_ref = ctx.use_scroll_ref("my_scroll");
    ///
    ///     div()
    ///         .child(
    ///             scroll()
    ///                 .bind(&scroll_ref)
    ///                 .child(items.iter().map(|i| div().id(format!("item-{}", i.id))))
    ///         )
    ///         .child(
    ///             button("Scroll to item 5").on_click({
    ///                 let scroll_ref = scroll_ref.clone();
    ///                 move |_| scroll_ref.scroll_to("item-5")
    ///             })
    ///         )
    /// }
    /// ```
    pub fn use_scroll_ref(&self, key: &str) -> blinc_layout::selector::ScrollRef {
        use blinc_core::reactive::SignalId;
        use blinc_layout::selector::{ScrollRef, SharedScrollRefInner, TriggerCallback};

        // Create a unique key for the scroll ref's inner state
        let state_key =
            StateKey::from_string::<SharedScrollRefInner>(&format!("scroll_ref:{}", key));
        let mut hooks = self.hooks.lock().unwrap();

        // Check if we have an existing signal with this key
        let (signal_id, inner) = if let Some(raw_id) = hooks.get(&state_key) {
            // Reconstruct the signal ID and get the inner state from the reactive graph
            let signal_id = SignalId::from_raw(raw_id);
            let inner = self
                .reactive
                .lock()
                .unwrap()
                .get_untracked(Signal::<SharedScrollRefInner>::from_id(signal_id))
                .unwrap_or_else(ScrollRef::new_inner);
            (signal_id, inner)
        } else {
            // First time - create a new inner state and store it in the reactive graph
            let new_inner = ScrollRef::new_inner();
            let signal = self
                .reactive
                .lock()
                .unwrap()
                .create_signal(Arc::clone(&new_inner));
            let raw_id = signal.id().to_raw();
            hooks.insert(state_key, raw_id);
            (signal.id(), new_inner)
        };

        drop(hooks);

        // ScrollRef doesn't need to trigger rebuilds - scroll operations are processed
        // every frame by process_pending_scroll_refs()
        let noop_trigger: TriggerCallback = Arc::new(|| {});

        ScrollRef::with_inner(inner, signal_id, noop_trigger)
    }

    /// Create a new reactive signal with an initial value (low-level API)
    ///
    /// **Note**: Prefer `use_state` in most cases, as it automatically
    /// persists signals across rebuilds.
    ///
    /// This method always creates a new signal. Use this for advanced
    /// cases where you manage signal lifecycle manually.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let count = ctx.use_signal(0);
    ///
    /// // In an event handler:
    /// ctx.set(count, ctx.get(count).unwrap_or(0) + 1);
    /// ```
    pub fn use_signal<T: Send + 'static>(&self, initial: T) -> Signal<T> {
        self.reactive.lock().unwrap().create_signal(initial)
    }

    /// Get the current value of a signal
    ///
    /// This automatically tracks the signal as a dependency when called
    /// within a derived computation or effect.
    pub fn get<T: Clone + 'static>(&self, signal: Signal<T>) -> Option<T> {
        self.reactive.lock().unwrap().get(signal)
    }

    /// Set the value of a signal, triggering reactive updates
    ///
    /// This will automatically trigger a UI rebuild.
    pub fn set<T: Send + 'static>(&self, signal: Signal<T>, value: T) {
        self.reactive.lock().unwrap().set(signal, value);
        // Mark dirty to trigger rebuild
        self.ref_dirty_flag.store(true, Ordering::SeqCst);
    }

    /// Update a signal using a function
    ///
    /// This is useful for incrementing counters or modifying state based
    /// on the current value.
    ///
    /// # Example
    ///
    /// ```ignore
    /// ctx.update(count, |n| n + 1);
    /// ```
    pub fn update<T: Clone + Send + 'static, F: FnOnce(T) -> T>(&self, signal: Signal<T>, f: F) {
        self.reactive.lock().unwrap().update(signal, f);
        self.ref_dirty_flag.store(true, Ordering::SeqCst);
    }

    /// Create a derived (computed) value
    ///
    /// Derived values are lazily computed and cached. They automatically
    /// track their signal dependencies and recompute when those signals change.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let count = ctx.use_signal(5);
    /// let doubled = ctx.use_derived(move |cx| cx.get(count).unwrap_or(0) * 2);
    ///
    /// assert_eq!(ctx.get_derived(doubled), Some(10));
    /// ```
    pub fn use_derived<T, F>(&self, compute: F) -> Derived<T>
    where
        T: Clone + Send + 'static,
        F: Fn(&ReactiveGraph) -> T + Send + 'static,
    {
        self.reactive.lock().unwrap().create_derived(compute)
    }

    /// Get the value of a derived computation
    pub fn get_derived<T: Clone + 'static>(&self, derived: Derived<T>) -> Option<T> {
        self.reactive.lock().unwrap().get_derived(derived)
    }

    /// Create an effect that runs when its dependencies change
    ///
    /// Effects are useful for side effects like logging, network requests,
    /// or syncing state with external systems.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let count = ctx.use_signal(0);
    ///
    /// ctx.use_effect(move |cx| {
    ///     let value = cx.get(count).unwrap_or(0);
    ///     println!("Count changed to: {}", value);
    /// });
    /// ```
    pub fn use_effect<F>(&self, run: F) -> blinc_core::reactive::Effect
    where
        F: FnMut(&ReactiveGraph) + Send + 'static,
    {
        self.reactive.lock().unwrap().create_effect(run)
    }

    /// Batch multiple signal updates into a single reactive update
    ///
    /// This is useful when updating multiple signals at once to avoid
    /// redundant recomputations.
    ///
    /// # Example
    ///
    /// ```ignore
    /// ctx.batch(|g| {
    ///     g.set(x, 10);
    ///     g.set(y, 20);
    ///     g.set(z, 30);
    /// });
    /// // Only one UI rebuild triggered
    /// ```
    pub fn batch<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&mut ReactiveGraph) -> R,
    {
        let result = self.reactive.lock().unwrap().batch(f);
        self.ref_dirty_flag.store(true, Ordering::SeqCst);
        result
    }

    /// Get the shared reactive graph for advanced usage
    ///
    /// This is useful when you need to pass the graph to closures or
    /// store it for later use.
    pub fn reactive(&self) -> SharedReactiveGraph {
        Arc::clone(&self.reactive)
    }

    /// Create a new DivRef that will trigger rebuilds when modified
    ///
    /// Use this to create refs that can be mutated in event handlers.
    /// When you call `.borrow_mut()` or `.with_mut()` on the returned ref,
    /// the UI will automatically rebuild when the mutation completes.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let card_ref = ctx.create_ref::<Div>();
    ///
    /// div()
    ///     .child(
    ///         div()
    ///             .bind(&card_ref)
    ///             .on_hover_enter({
    ///                 let r = card_ref.clone();
    ///                 move |_| {
    ///                     // This automatically triggers a rebuild
    ///                     r.with_mut(|d| *d = d.swap().bg(Color::RED));
    ///                 }
    ///             })
    ///     )
    /// ```
    pub fn create_ref<T>(&self) -> ElementRef<T> {
        ElementRef::with_dirty_flag(Arc::clone(&self.ref_dirty_flag))
    }

    /// Create a new DivRef (convenience method)
    pub fn div_ref(&self) -> DivRef {
        self.create_ref::<Div>()
    }

    /// Get the shared dirty flag for manual state management
    ///
    /// Use this when you want to create your own state types that trigger
    /// UI rebuilds when modified. When you modify state, set this flag to true.
    ///
    /// # Example
    ///
    /// ```ignore
    /// struct MyState {
    ///     value: i32,
    ///     dirty_flag: RefDirtyFlag,
    /// }
    ///
    /// impl MyState {
    ///     fn set_value(&mut self, v: i32) {
    ///         self.value = v;
    ///         self.dirty_flag.store(true, Ordering::SeqCst);
    ///     }
    /// }
    /// ```
    pub fn dirty_flag(&self) -> RefDirtyFlag {
        Arc::clone(&self.ref_dirty_flag)
    }

    /// Get a handle to the animation scheduler for creating animated values
    ///
    /// Components use this handle to create `AnimatedValue`s that automatically
    /// register with the scheduler. The scheduler ticks all animations each frame
    /// and triggers UI rebuilds while animations are active.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_animation::{AnimatedValue, SpringConfig};
    ///
    /// let opacity = AnimatedValue::new(ctx.animations(), 1.0, SpringConfig::stiff());
    /// opacity.set_target(0.5); // Auto-registers and animates
    /// let current = opacity.get(); // Get interpolated value
    /// ```
    pub fn animation_handle(&self) -> SchedulerHandle {
        self.animations.lock().unwrap().handle()
    }

    /// Get the overlay manager for showing modals, dialogs, toasts, etc.
    ///
    /// The overlay manager provides a fluent API for creating overlays that
    /// render in a separate pass after the main UI tree, ensuring they always
    /// appear on top.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_layout::prelude::*;
    ///
    /// fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    ///     let overlay_mgr = ctx.overlay_manager();
    ///
    ///     div()
    ///         .child(
    ///             button("Show Modal").on_click({
    ///                 let mgr = overlay_mgr.clone();
    ///                 move |_| {
    ///                     mgr.modal()
    ///                         .content(|| {
    ///                             div().p(20.0).bg(Color::WHITE)
    ///                                 .child(text("Hello from modal!"))
    ///                         })
    ///                         .show();
    ///                 }
    ///             })
    ///         )
    /// }
    /// ```
    pub fn overlay_manager(&self) -> OverlayManager {
        Arc::clone(&self.overlay_manager)
    }

    // =========================================================================
    // Query API
    // =========================================================================

    /// Query an element by ID and get an ElementHandle for programmatic manipulation
    ///
    /// Returns an `ElementHandle` for interacting with the element. The handle
    /// provides methods like `scroll_into_view()`, `focus()`, `click()`, `on_ready()`,
    /// and tree traversal.
    ///
    /// The handle works even if the element doesn't exist yet - operations like
    /// `on_ready()` will queue until the element is laid out. Use `handle.exists()`
    /// to check if the element currently exists.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Register on_ready callback (works before element exists):
    /// ctx.query("progress-bar").on_ready(|bounds| {
    ///     progress_anim.lock().unwrap().set_target(bounds.width * 0.75);
    /// });
    ///
    /// // In UI builder:
    /// div().id("progress-bar").child(...)
    ///
    /// // Later, interact with existing element:
    /// let handle = ctx.query("my-element");
    /// if handle.exists() {
    ///     handle.scroll_into_view();
    ///     handle.focus();
    /// }
    /// ```
    pub fn query(&self, id: &str) -> blinc_layout::selector::ElementHandle<()> {
        blinc_layout::selector::ElementHandle::new(id, self.element_registry.clone())
    }

    /// Get the shared element registry
    ///
    /// This provides access to the element registry for advanced query operations.
    pub fn element_registry(&self) -> &SharedElementRegistry {
        &self.element_registry
    }

    /// Create a persistent state for stateful UI elements
    ///
    /// This creates a `SharedState<S>` that survives across UI rebuilds.
    /// State is keyed automatically by source location using `#[track_caller]`.
    ///
    /// Use with `stateful()` for the cleanest API:
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_layout::prelude::*;
    ///
    /// fn my_button(ctx: &WindowedContext) -> impl ElementBuilder {
    ///     let handle = ctx.use_state(ButtonState::Idle);
    ///
    ///     stateful(handle)
    ///         .on_state(|state, div| {
    ///             match state {
    ///                 ButtonState::Hovered => { *div = div.swap().bg(Color::RED); }
    ///                 _ => { *div = div.swap().bg(Color::BLUE); }
    ///             }
    ///         })
    /// }
    /// ```
    #[track_caller]
    pub fn use_state<S>(&self, initial: S) -> blinc_layout::SharedState<S>
    where
        S: blinc_layout::StateTransitions + Clone + Send + 'static,
    {
        // Use caller location as the key
        let location = std::panic::Location::caller();
        let key = format!(
            "{}:{}:{}",
            location.file(),
            location.line(),
            location.column()
        );
        self.use_state_for(&key, initial)
    }

    /// Create a persistent state with an explicit key
    ///
    /// Use this for reusable components that are called multiple times
    /// from the same location (e.g., in a loop or when the same component
    /// function is called multiple times with different props).
    ///
    /// The key can be any type that implements `Hash` (strings, numbers, etc).
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Reusable component - string key
    /// fn feature_card(ctx: &WindowedContext, id: &str) -> impl ElementBuilder {
    ///     let handle = ctx.use_state_for(id, ButtonState::Idle);
    ///     stateful(handle).on_state(|state, div| { ... })
    /// }
    ///
    /// // Or with numeric key in a loop
    /// for i in 0..3 {
    ///     let handle = ctx.use_state_for(i, ButtonState::Idle);
    ///     // ...
    /// }
    /// ```
    pub fn use_state_for<K, S>(&self, key: K, initial: S) -> blinc_layout::SharedState<S>
    where
        K: Hash,
        S: blinc_layout::StateTransitions + Clone + Send + 'static,
    {
        use blinc_core::reactive::SignalId;
        use blinc_layout::stateful::StatefulInner;

        // We store the SharedState<S> as a signal value
        let state_key = StateKey::new::<blinc_layout::SharedState<S>, _>(&key);
        let mut hooks = self.hooks.lock().unwrap();

        if let Some(raw_id) = hooks.get(&state_key) {
            // Existing state - get the SharedState from the signal
            let signal_id = SignalId::from_raw(raw_id);
            let signal: Signal<blinc_layout::SharedState<S>> = Signal::from_id(signal_id);
            self.reactive.lock().unwrap().get(signal).unwrap()
        } else {
            // New state - create SharedState and store in signal
            let shared_state: blinc_layout::SharedState<S> =
                Arc::new(Mutex::new(StatefulInner::new(initial)));
            let signal = self
                .reactive
                .lock()
                .unwrap()
                .create_signal(shared_state.clone());
            let raw_id = signal.id().to_raw();
            hooks.insert(state_key, raw_id);
            shared_state
        }
    }

    /// Create a persistent animated value using caller location as key
    ///
    /// The animated value survives UI rebuilds, preserving its current value
    /// and active spring animations. This is essential for continuous animations
    /// driven by state changes.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Animated value persists across rebuilds
    /// let offset_y = ctx.use_animated_value(0.0, SpringConfig::wobbly());
    ///
    /// // Can be used in motion bindings
    /// motion().translate_y(offset_y.clone()).child(content)
    /// ```
    #[track_caller]
    pub fn use_animated_value(&self, initial: f32, config: SpringConfig) -> SharedAnimatedValue {
        let location = std::panic::Location::caller();
        let key = format!(
            "{}:{}:{}",
            location.file(),
            location.line(),
            location.column()
        );
        self.use_animated_value_for(&key, initial, config)
    }

    /// Create a persistent animated value with an explicit key
    ///
    /// Use this for reusable components or when creating multiple animated
    /// values at the same source location (e.g., in a loop).
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Multiple animated values with unique keys
    /// for i in 0..3 {
    ///     let scale = ctx.use_animated_value_for(
    ///         format!("item_{}_scale", i),
    ///         1.0,
    ///         SpringConfig::snappy(),
    ///     );
    /// }
    /// ```
    pub fn use_animated_value_for<K: Hash>(
        &self,
        key: K,
        initial: f32,
        config: SpringConfig,
    ) -> SharedAnimatedValue {
        use blinc_core::reactive::SignalId;

        // Use a type marker for SharedAnimatedValue
        let state_key = StateKey::new::<SharedAnimatedValue, _>(&key);
        let mut hooks = self.hooks.lock().unwrap();

        if let Some(raw_id) = hooks.get(&state_key) {
            // Existing animated value - retrieve from signal
            let signal_id = SignalId::from_raw(raw_id);
            let signal: Signal<SharedAnimatedValue> = Signal::from_id(signal_id);
            self.reactive.lock().unwrap().get(signal).unwrap()
        } else {
            // New animated value - create and store in signal
            let animated_value: SharedAnimatedValue = Arc::new(Mutex::new(AnimatedValue::new(
                self.animation_handle(),
                initial,
                config,
            )));
            let signal = self
                .reactive
                .lock()
                .unwrap()
                .create_signal(animated_value.clone());
            let raw_id = signal.id().to_raw();
            hooks.insert(state_key, raw_id);
            animated_value
        }
    }

    /// Create or retrieve a persistent animated timeline
    ///
    /// AnimatedTimeline provides keyframe-based animations that persist across
    /// UI rebuilds. Use this for timeline animations that need to survive
    /// layout changes and window resizes.
    ///
    /// The returned timeline is empty on first call - add keyframes using
    /// `timeline.add()` then call `start()`. Use `has_entries()` to check
    /// if the timeline needs configuration.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let timeline = ctx.use_animated_timeline();
    /// let entry_id = {
    ///     let mut t = timeline.lock().unwrap();
    ///     if !t.has_entries() {
    ///         let id = t.add(0, 2000, 0.0, 1.0);
    ///         t.start();
    ///         id
    ///     } else {
    ///         t.entry_ids().first().copied().unwrap()
    ///     }
    /// };
    /// ```
    #[track_caller]
    pub fn use_animated_timeline(&self) -> SharedAnimatedTimeline {
        let location = std::panic::Location::caller();
        let key = format!(
            "{}:{}:{}",
            location.file(),
            location.line(),
            location.column()
        );
        self.use_animated_timeline_for(&key)
    }

    /// Create or retrieve a persistent animated timeline with an explicit key
    ///
    /// Use this for reusable components or when creating multiple timelines
    /// at the same source location (e.g., in a loop).
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Multiple timelines with unique keys
    /// for i in 0..3 {
    ///     let timeline = ctx.use_animated_timeline_for(format!("dot_{}", i));
    ///     // ...
    /// }
    /// ```
    pub fn use_animated_timeline_for<K: Hash>(&self, key: K) -> SharedAnimatedTimeline {
        use blinc_core::reactive::SignalId;

        // Use a type marker for SharedAnimatedTimeline
        let state_key = StateKey::new::<SharedAnimatedTimeline, _>(&key);
        let mut hooks = self.hooks.lock().unwrap();

        if let Some(raw_id) = hooks.get(&state_key) {
            // Existing timeline - retrieve from signal
            let signal_id = SignalId::from_raw(raw_id);
            let signal: Signal<SharedAnimatedTimeline> = Signal::from_id(signal_id);
            self.reactive.lock().unwrap().get(signal).unwrap()
        } else {
            // New timeline - create and store in signal
            let timeline: SharedAnimatedTimeline =
                Arc::new(Mutex::new(AnimatedTimeline::new(self.animation_handle())));
            let signal = self
                .reactive
                .lock()
                .unwrap()
                .create_signal(timeline.clone());
            let raw_id = signal.id().to_raw();
            hooks.insert(state_key, raw_id);
            timeline
        }
    }

    // =========================================================================
    // Tick Callback API (for per-frame updates like ECS systems)
    // =========================================================================

    /// Register a callback that runs each frame in the animation scheduler
    ///
    /// This creates a persistent tick callback keyed by source location.
    /// The callback receives delta time in seconds and runs on the animation
    /// scheduler's background thread at 120fps.
    ///
    /// Use this for ECS systems, physics, or any per-frame updates.
    /// The callback is registered once and persists across UI rebuilds.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Create ECS world (persisted via use_state)
    /// let world = ctx.use_state_keyed("world", || Arc::new(Mutex::new(World::new())));
    ///
    /// // Register tick callback to run ECS systems
    /// ctx.use_tick_callback({
    ///     let world = world.get();
    ///     move |dt| {
    ///         let mut w = world.lock().unwrap();
    ///         // Run ECS systems with delta time
    ///         w.run_systems(dt);
    ///     }
    /// });
    /// ```
    #[track_caller]
    pub fn use_tick_callback<F>(&self, callback: F) -> blinc_animation::TickCallbackId
    where
        F: FnMut(f32) + Send + Sync + 'static,
    {
        let location = std::panic::Location::caller();
        let key = format!(
            "tick_{}:{}:{}",
            location.file(),
            location.line(),
            location.column()
        );
        self.use_tick_callback_for(&key, callback)
    }

    /// Register a tick callback with an explicit key
    ///
    /// Use this when you need to create multiple tick callbacks at the same
    /// source location (e.g., in a loop) or in reusable components.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Multiple tick callbacks with unique keys
    /// for i in 0..3 {
    ///     ctx.use_tick_callback_for(format!("system_{}", i), move |dt| {
    ///         // Per-frame update
    ///     });
    /// }
    /// ```
    pub fn use_tick_callback_for<K: Hash, F>(
        &self,
        key: K,
        callback: F,
    ) -> blinc_animation::TickCallbackId
    where
        F: FnMut(f32) + Send + Sync + 'static,
    {
        // Marker type for TickCallbackId storage
        struct TickCallbackMarker;

        let state_key = StateKey::new::<TickCallbackMarker, _>(&key);
        let mut hooks = self.hooks.lock().unwrap();

        if let Some(raw_id) = hooks.get(&state_key) {
            // Already registered - return existing ID
            blinc_animation::TickCallbackId::from_raw(raw_id)
        } else {
            // First time - register the callback with the scheduler
            let id = self
                .animation_handle()
                .register_tick_callback(callback)
                .expect("Animation scheduler should be alive");
            hooks.insert(state_key, id.to_raw());
            id
        }
    }

    // =========================================================================
    // Theme API
    // =========================================================================

    /// Get the current color scheme (light or dark)
    ///
    /// # Example
    ///
    /// ```ignore
    /// let scheme = ctx.color_scheme();
    /// match scheme {
    ///     ColorScheme::Light => println!("Light mode"),
    ///     ColorScheme::Dark => println!("Dark mode"),
    /// }
    /// ```
    pub fn color_scheme(&self) -> blinc_theme::ColorScheme {
        blinc_theme::ThemeState::get().scheme()
    }

    /// Set the color scheme (triggers smooth theme transition)
    ///
    /// # Example
    ///
    /// ```ignore
    /// ctx.set_color_scheme(ColorScheme::Dark);
    /// ```
    pub fn set_color_scheme(&self, scheme: blinc_theme::ColorScheme) {
        blinc_theme::ThemeState::get().set_scheme(scheme);
    }

    /// Toggle between light and dark mode
    ///
    /// # Example
    ///
    /// ```ignore
    /// button("Toggle Theme").on_click(|ctx| {
    ///     ctx.toggle_color_scheme();
    /// })
    /// ```
    pub fn toggle_color_scheme(&self) {
        blinc_theme::ThemeState::get().toggle_scheme();
    }

    /// Get a color from the current theme
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_theme::ColorToken;
    ///
    /// let primary = ctx.theme_color(ColorToken::Primary);
    /// let bg = ctx.theme_color(ColorToken::Background);
    /// ```
    pub fn theme_color(&self, token: blinc_theme::ColorToken) -> blinc_core::Color {
        blinc_theme::ThemeState::get().color(token)
    }

    /// Get spacing from the current theme
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_theme::SpacingToken;
    ///
    /// let padding = ctx.theme_spacing(SpacingToken::Space4); // 16px
    /// ```
    pub fn theme_spacing(&self, token: blinc_theme::SpacingToken) -> f32 {
        blinc_theme::ThemeState::get().spacing_value(token)
    }

    /// Get border radius from the current theme
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_theme::RadiusToken;
    ///
    /// let radius = ctx.theme_radius(RadiusToken::Lg); // 8px
    /// ```
    pub fn theme_radius(&self, token: blinc_theme::RadiusToken) -> f32 {
        blinc_theme::ThemeState::get().radius(token)
    }

    // =========================================================================
    // CSS Stylesheet API
    // =========================================================================

    /// Add inline CSS to the application stylesheet.
    ///
    /// Multiple calls cascade — later rules override earlier ones.
    /// Stylesheets are visual-only: they update render props on existing nodes
    /// and trigger redraws. They never cause tree rebuilds.
    pub fn add_css(&mut self, css: &str) {
        // Store raw CSS for reparsing on theme changes
        self.css_sources.push(css.to_string());

        // Seed parser with theme variables + any previously defined CSS variables
        let mut external_vars = blinc_theme::ThemeState::try_get()
            .map(|t| t.to_css_variable_map())
            .unwrap_or_default();
        if let Some(existing) = &self.stylesheet {
            for (k, v) in existing.variables() {
                external_vars.insert(k.clone(), v.clone());
            }
        }
        match blinc_layout::css_parser::Stylesheet::parse_with_variables(css, &external_vars) {
            Ok(sheet) => self.add_stylesheet(sheet),
            Err(e) => {
                tracing::warn!("Failed to parse CSS: {}", e);
            }
        }
    }

    /// Load and add a `.css` file to the application stylesheet.
    ///
    /// Multiple calls cascade — later rules override earlier ones.
    pub fn load_css(&mut self, path: &str) {
        match std::fs::read_to_string(path) {
            Ok(css) => self.add_css(&css),
            Err(e) => {
                tracing::warn!("Failed to load CSS file '{}': {}", path, e);
            }
        }
    }

    /// Add a pre-parsed stylesheet to the application.
    ///
    /// Multiple calls cascade — later rules override earlier ones.
    pub fn add_stylesheet(&mut self, sheet: blinc_layout::css_parser::Stylesheet) {
        match self.stylesheet.as_mut() {
            Some(existing) => {
                // Cascade: merge into existing (Arc::make_mut for COW)
                Arc::make_mut(existing).merge(sheet);
            }
            None => {
                self.stylesheet = Some(Arc::new(sheet));
            }
        }
        // Publish to global so stateful widgets (buttons, etc.) can read CSS
        // overrides during tree construction, before set_stylesheet_arc() runs
        if let Some(ref stylesheet) = self.stylesheet {
            blinc_layout::css_parser::set_active_stylesheet(std::sync::Arc::clone(stylesheet));
        }
    }

    /// Reparse all stored CSS sources with fresh theme variables.
    ///
    /// Called automatically when the theme color scheme changes to ensure
    /// CSS `var()` and `theme()` references resolve to the new colors.
    pub fn reparse_css(&mut self) {
        if self.css_sources.is_empty() {
            return;
        }

        tracing::debug!(
            "Reparsing {} CSS sources with updated theme variables",
            self.css_sources.len()
        );

        // Clear existing stylesheet
        self.stylesheet = None;

        // Reparse each CSS source with fresh theme variables
        for css in self.css_sources.clone() {
            let mut external_vars = blinc_theme::ThemeState::try_get()
                .map(|t| t.to_css_variable_map())
                .unwrap_or_default();
            if let Some(existing) = &self.stylesheet {
                for (k, v) in existing.variables() {
                    external_vars.insert(k.clone(), v.clone());
                }
            }
            match blinc_layout::css_parser::Stylesheet::parse_with_variables(&css, &external_vars) {
                Ok(sheet) => self.add_stylesheet(sheet),
                Err(e) => {
                    tracing::warn!("Failed to reparse CSS on theme change: {}", e);
                }
            }
        }
    }

    /// Set a style for an element by ID.
    ///
    /// This is the Rust-native alternative to `add_css()`. Use with `css!` or `style!`
    /// macros to define styles in Rust syntax that are applied automatically to matching
    /// elements — just like CSS stylesheets.
    ///
    /// # Example
    ///
    /// ```ignore
    /// ctx.set_style("card", css! {
    ///     background: Color::BLUE;
    ///     border-radius: 12.0;
    ///     box-shadow: md;
    /// });
    ///
    /// // Then just give the element an ID:
    /// div().id("card").w(200.0).h(100.0)
    /// ```
    pub fn set_style(&mut self, id: &str, style: blinc_layout::element_style::ElementStyle) {
        match self.stylesheet.as_mut() {
            Some(existing) => {
                Arc::make_mut(existing).insert(id, style);
            }
            None => {
                let mut sheet = blinc_layout::css_parser::Stylesheet::new();
                sheet.insert(id, style);
                self.stylesheet = Some(Arc::new(sheet));
            }
        }
    }

    /// Set a state-specific style for an element by ID.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use blinc_layout::css_parser::ElementState;
    ///
    /// ctx.set_style("button", style! { bg: Color::BLUE, rounded: 8.0 });
    /// ctx.set_state_style("button", ElementState::Hover, style! {
    ///     bg: Color::from_hex(0x2563EB),
    ///     shadow_md,
    /// });
    /// ```
    pub fn set_state_style(
        &mut self,
        id: &str,
        state: blinc_layout::css_parser::ElementState,
        style: blinc_layout::element_style::ElementStyle,
    ) {
        match self.stylesheet.as_mut() {
            Some(existing) => {
                Arc::make_mut(existing).insert_with_state(id, state, style);
            }
            None => {
                let mut sheet = blinc_layout::css_parser::Stylesheet::new();
                sheet.insert_with_state(id, state, style);
                self.stylesheet = Some(Arc::new(sheet));
            }
        }
    }
}

// =============================================================================
// BlincContext Implementation
// =============================================================================

impl blinc_core::BlincContext for WindowedContext {
    fn use_state_keyed<T, F>(&self, key: &str, init: F) -> State<T>
    where
        T: Clone + Send + 'static,
        F: FnOnce() -> T,
    {
        // Delegate to the existing method
        WindowedContext::use_state_keyed(self, key, init)
    }

    fn use_signal_keyed<T, F>(&self, key: &str, init: F) -> Signal<T>
    where
        T: Clone + Send + 'static,
        F: FnOnce() -> T,
    {
        WindowedContext::use_signal_keyed(self, key, init)
    }

    fn use_signal<T: Send + 'static>(&self, initial: T) -> Signal<T> {
        WindowedContext::use_signal(self, initial)
    }

    fn get<T: Clone + 'static>(&self, signal: Signal<T>) -> Option<T> {
        WindowedContext::get(self, signal)
    }

    fn set<T: Send + 'static>(&self, signal: Signal<T>, value: T) {
        WindowedContext::set(self, signal, value)
    }

    fn update<T: Clone + Send + 'static, F: FnOnce(T) -> T>(&self, signal: Signal<T>, f: F) {
        WindowedContext::update(self, signal, f)
    }

    fn use_derived<T, F>(&self, compute: F) -> Derived<T>
    where
        T: Clone + Send + 'static,
        F: Fn(&ReactiveGraph) -> T + Send + 'static,
    {
        WindowedContext::use_derived(self, compute)
    }

    fn get_derived<T: Clone + 'static>(&self, derived: Derived<T>) -> Option<T> {
        WindowedContext::get_derived(self, derived)
    }

    fn batch<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&mut ReactiveGraph) -> R,
    {
        WindowedContext::batch(self, f)
    }

    fn dirty_flag(&self) -> blinc_core::DirtyFlag {
        WindowedContext::dirty_flag(self)
    }

    fn request_rebuild(&self) {
        self.ref_dirty_flag.store(true, Ordering::SeqCst);
    }

    fn width(&self) -> f32 {
        self.width
    }

    fn height(&self) -> f32 {
        self.height
    }

    fn scale_factor(&self) -> f64 {
        self.scale_factor
    }
}

// =============================================================================
// AnimationContext Implementation
// =============================================================================

impl AnimationContext for WindowedContext {
    fn animation_handle(&self) -> SchedulerHandle {
        WindowedContext::animation_handle(self)
    }

    fn use_animated_value_for<K: Hash>(
        &self,
        key: K,
        initial: f32,
        config: SpringConfig,
    ) -> SharedAnimatedValue {
        WindowedContext::use_animated_value_for(self, key, initial, config)
    }

    fn use_animated_timeline_for<K: Hash>(&self, key: K) -> SharedAnimatedTimeline {
        WindowedContext::use_animated_timeline_for(self, key)
    }
}

/// Windowed application runner
///
/// Provides a simple way to run a Blinc application in a window
/// with automatic event handling and rendering.
pub struct WindowedApp;

impl WindowedApp {
    /// Initialize the platform asset loader
    ///
    /// On desktop, this sets up a filesystem-based loader.
    /// On Android, this would use the NDK AssetManager.
    #[cfg(all(feature = "windowed", not(target_os = "android")))]
    fn init_asset_loader() {
        use blinc_platform::assets::{set_global_asset_loader, FilesystemAssetLoader};

        // Create a filesystem loader (uses current directory as base)
        let loader = FilesystemAssetLoader::new();

        // Try to set the global loader (ignore error if already set)
        let _ = set_global_asset_loader(Box::new(loader));
    }

    /// Initialize the theme system with platform detection
    ///
    /// This sets up the global ThemeState with:
    /// - Platform-appropriate theme bundle (macOS, Windows, Linux, etc.)
    /// - System color scheme detection (light/dark mode)
    /// - Redraw callback to trigger UI updates on theme changes
    #[cfg(all(feature = "windowed", not(target_os = "android")))]
    fn init_theme() {
        use blinc_theme::{
            detect_system_color_scheme, platform_theme_bundle, set_redraw_callback, ThemeState,
        };

        // Only initialize if not already initialized
        if ThemeState::try_get().is_none() {
            let bundle = platform_theme_bundle();
            let scheme = detect_system_color_scheme();
            ThemeState::init(bundle, scheme);
        }

        // Set up the redraw callback to trigger full UI rebuilds when theme changes
        // We use request_full_rebuild() to trigger all three phases:
        // 1. Tree rebuild - reconstruct UI with new theme values
        // 2. Layout recompute - recalculate flexbox layout
        // 3. Visual redraw - render the frame
        set_redraw_callback(|| {
            tracing::debug!("Theme changed - requesting full rebuild + CSS reparse");
            blinc_layout::widgets::request_css_reparse();
            blinc_layout::widgets::request_full_rebuild();
        });
    }

    /// Run a windowed Blinc application on desktop platforms
    ///
    /// This is the main entry point for desktop applications. It creates
    /// a window, sets up GPU rendering, and runs the event loop.
    ///
    /// # Arguments
    ///
    /// * `config` - Window configuration (title, size, etc.)
    /// * `ui_builder` - Function that builds the UI tree given the window context
    ///
    /// # Example
    ///
    /// ```ignore
    /// WindowedApp::run(WindowConfig::default(), |ctx| {
    ///     div()
    ///         .w(ctx.width).h(ctx.height)
    ///         .bg([0.1, 0.1, 0.15, 1.0])
    ///         .flex_center()
    ///         .child(
    ///             div().glass().rounded(16.0).p(24.0)
    ///                 .child(text("Hello Blinc!").size(32.0))
    ///         )
    /// })
    /// ```
    #[cfg(all(feature = "windowed", not(target_os = "android")))]
    pub fn run<F, E>(config: WindowConfig, ui_builder: F) -> Result<()>
    where
        F: FnMut(&mut WindowedContext) -> E + 'static,
        E: ElementBuilder + 'static,
    {
        Self::run_desktop(config, ui_builder)
    }

    /// Create per-window action closures (close, drag, minimize, maximize).
    /// Returns (close, drag, minimize, maximize) Arcs.
    #[cfg(all(feature = "windowed", not(target_os = "android")))]
    #[allow(clippy::type_complexity)]
    fn make_window_actions(
        win: std::sync::Arc<winit::window::Window>,
        wake: blinc_platform_desktop::WakeProxy,
    ) -> (
        Arc<dyn Fn() + Send + Sync>,
        Arc<dyn Fn() + Send + Sync>,
        Arc<dyn Fn() + Send + Sync>,
        Arc<dyn Fn() + Send + Sync>,
    ) {
        let d = Arc::downgrade(&win);
        let mi = Arc::downgrade(&win);
        let ma = Arc::downgrade(&win);
        let cl = Arc::downgrade(&win);
        let wake_for_close = wake;
        (
            Arc::new(move || {
                if let Some(w) = cl.upgrade() {
                    use std::hash::{Hash, Hasher};
                    let mut hasher = std::collections::hash_map::DefaultHasher::new();
                    w.id().hash(&mut hasher);
                    wake_for_close.close_window(blinc_platform::WindowId(hasher.finish()));
                }
            }),
            Arc::new(move || {
                if let Some(w) = d.upgrade() {
                    let _ = w.drag_window();
                }
            }),
            Arc::new(move || {
                if let Some(w) = mi.upgrade() {
                    w.set_minimized(true);
                }
            }),
            Arc::new(move || {
                if let Some(w) = ma.upgrade() {
                    w.set_maximized(!w.is_maximized());
                }
            }),
        )
    }

    /// Register global window action callbacks (for drag_region() on Div).
    /// Called for both primary and secondary windows, and on focus changes.
    #[cfg(all(feature = "windowed", not(target_os = "android")))]
    fn register_window_actions_static(
        win: std::sync::Arc<winit::window::Window>,
        wake: blinc_platform_desktop::WakeProxy,
    ) {
        let d = win.clone();
        let mi = win.clone();
        let ma = win.clone();
        let cl = win;
        blinc_layout::window_actions::set_active_window_actions(
            move || {
                let _ = d.drag_window();
            },
            move || mi.set_minimized(true),
            move || ma.set_maximized(!ma.is_maximized()),
            move || {
                use std::hash::{Hash, Hasher};
                let mut hasher = std::collections::hash_map::DefaultHasher::new();
                cl.id().hash(&mut hasher);
                wake.close_window(blinc_platform::WindowId(hasher.finish()));
            },
        );
    }

    #[cfg(all(feature = "windowed", not(target_os = "android")))]
    fn run_desktop<F, E>(config: WindowConfig, mut ui_builder: F) -> Result<()>
    where
        F: FnMut(&mut WindowedContext) -> E + 'static,
        E: ElementBuilder + 'static,
    {
        // Initialize the platform asset loader for cross-platform asset loading
        Self::init_asset_loader();

        // Initialize the text measurer for accurate text layout
        crate::text_measurer::init_text_measurer();

        // Initialize the theme system with platform detection
        Self::init_theme();

        let platform = DesktopPlatform::new().map_err(|e| BlincError::Platform(e.to_string()))?;
        let event_loop = platform
            .create_event_loop_with_config(config)
            .map_err(|e| BlincError::Platform(e.to_string()))?;

        // Get a wake proxy to allow the animation thread to wake up the event loop
        let wake_proxy = event_loop.wake_proxy();
        // Clone for the open_window callback
        let wake_proxy_for_windows = event_loop.wake_proxy();

        // Shared dirty flag for element refs
        let ref_dirty_flag: RefDirtyFlag = Arc::new(AtomicBool::new(false));
        // Shared reactive graph for signal-based state management
        let reactive: SharedReactiveGraph = Arc::new(Mutex::new(ReactiveGraph::new()));
        // Shared hook state for use_state persistence
        let hooks: SharedHookState = Arc::new(Mutex::new(HookState::new()));

        // Initialize global context state singleton (if not already initialized)
        // This allows components to create internal state without context parameters
        if !BlincContextState::is_initialized() {
            #[allow(clippy::type_complexity)]
            let stateful_callback: std::sync::Arc<dyn Fn(&[SignalId]) + Send + Sync> =
                Arc::new(|signal_ids| {
                    blinc_layout::check_stateful_deps(signal_ids);
                });
            BlincContextState::init_with_callback(
                Arc::clone(&reactive),
                Arc::clone(&hooks),
                Arc::clone(&ref_dirty_flag),
                stateful_callback,
            );
        }

        // Shared animation scheduler for spring/keyframe animations
        // Runs on background thread so animations continue even when window loses focus
        let mut scheduler = AnimationScheduler::new();
        // Set up wake callback so animation thread can wake the event loop
        scheduler.set_wake_callback(move || wake_proxy.wake());
        scheduler.start_background();
        let animations: SharedAnimationScheduler = Arc::new(Mutex::new(scheduler));

        // Set global scheduler handle for StateContext and component access
        {
            let scheduler_handle = animations.lock().unwrap().handle();
            blinc_animation::set_global_scheduler(scheduler_handle);
        }

        // Shared CSS animation/transition store
        // The scheduler's background thread keeps the redraw loop alive at 120fps
        // via the tick callback below (acts as keep-alive signal). Actual ticking
        // happens synchronously on the main thread to avoid phase jitter between
        // the bg thread's tick timing and the frame's render timing.
        let css_anim_store = Arc::new(Mutex::new(blinc_layout::CssAnimationStore::new()));
        {
            animations
                .lock()
                .unwrap()
                .add_tick_callback(move |_dt_secs| {
                    // Keep-alive no-op: the callback's existence ensures the scheduler
                    // considers tick_callbacks as "active", triggering wake_callback()
                    // at 120fps so the main thread gets continuous frame requests
                    // while CSS animations are running.
                    // Actual ticking happens on the main thread to avoid phase jitter.
                });
        }

        // Shared element registry for query API
        let element_registry: SharedElementRegistry =
            Arc::new(blinc_layout::selector::ElementRegistry::new());

        // Set up query callback in BlincContextState so components can query elements globally
        {
            let registry_for_query = Arc::clone(&element_registry);
            let query_callback: blinc_core::QueryCallback = Arc::new(move |id: &str| {
                registry_for_query.get(id).map(|node_id| node_id.to_raw())
            });
            BlincContextState::get().set_query_callback(query_callback);
        }

        // Set up bounds callback for ElementHandle.bounds()
        {
            let registry_for_bounds = Arc::clone(&element_registry);
            let bounds_callback: blinc_core::BoundsCallback =
                Arc::new(move |id: &str| registry_for_bounds.get_bounds(id));
            BlincContextState::get().set_bounds_callback(bounds_callback);
        }

        // Store element registry in BlincContextState for global query() function
        // Cast to Arc<dyn Any + Send + Sync> for type-erased storage
        BlincContextState::get()
            .set_element_registry(Arc::clone(&element_registry) as blinc_core::AnyElementRegistry);

        // Shared storage for on_ready callbacks
        let ready_callbacks: SharedReadyCallbacks = Arc::new(Mutex::new(Vec::new()));

        // Set up continuous redraw callback for text widget cursor animation
        // This bridges text widgets (which track focus) with the animation scheduler (which drives redraws)
        {
            let animations_for_callback = Arc::clone(&animations);
            blinc_layout::widgets::set_continuous_redraw_callback(move |enabled| {
                if let Ok(scheduler) = animations_for_callback.lock() {
                    scheduler.set_continuous_redraw(enabled);
                }
            });
        }

        // Connect theme animation to the animation scheduler
        // This enables smooth color transitions when switching between light/dark mode
        blinc_theme::ThemeState::get().set_scheduler(&animations);

        // Render state: dynamic properties that update every frame without tree rebuild
        // This includes cursor blink, animated colors, hover states, etc.
        let mut render_state: Option<blinc_layout::RenderState> = None;

        // Shared motion states for query API access
        // This allows components to query motion animation state via query_motion()
        let shared_motion_states = blinc_layout::create_shared_motion_states();

        // Set up motion state callback in BlincContextState
        {
            let motion_states_for_callback = Arc::clone(&shared_motion_states);
            let motion_callback: blinc_core::MotionStateCallback = Arc::new(move |key: &str| {
                motion_states_for_callback
                    .read()
                    .ok()
                    .and_then(|states| states.get(key).copied())
                    .unwrap_or(blinc_core::MotionAnimationState::NotFound)
            });
            BlincContextState::get().set_motion_state_callback(motion_callback);
        }

        // Overlay manager for modals, dialogs, toasts, etc.
        let overlays: OverlayManager = overlay_manager();

        // Initialize overlay context singleton for component access
        if !OverlayContext::is_initialized() {
            OverlayContext::init(Arc::clone(&overlays));
        }

        // Primary window state
        let mut ws = WindowState::new(
            Arc::clone(&css_anim_store),
            Arc::clone(&shared_motion_states),
        );
        // Track primary window ID once known
        let mut primary_wid: Option<blinc_platform::WindowId> = None;
        // Secondary windows (opened via ctx.open_window())
        let mut secondary_windows: std::collections::HashMap<
            blinc_platform::WindowId,
            WindowState,
        > = std::collections::HashMap::new();
        // UI builders for secondary windows (queued via open_window)
        // For now secondary windows get a blank UI — full UI builder support is future work

        event_loop
            .run(move |event, window| {
                // Check if this event is for a secondary window
                let event_wid = match &event {
                    Event::Window(wid, _) | Event::Input(wid, _) | Event::Frame(wid) => Some(*wid),
                    _ => None,
                };
                let is_secondary = event_wid
                    .map(|wid| primary_wid.is_some_and(|p| wid != p))
                    .unwrap_or(false);

                // Handle secondary window events
                if is_secondary {
                    let wid = event_wid.unwrap();
                    match event {
                        Event::Window(_, WindowEvent::Resized { width, height }) => {
                            if let Some(sws) = secondary_windows.get_mut(&wid) {
                                if let (Some(ref surf), Some(ref mut config)) =
                                    (&sws.surface, &mut sws.surface_config)
                                {
                                    if width > 0 && height > 0 {
                                        config.width = width;
                                        config.height = height;
                                        if let Some(ref blinc_app) = ws.app {
                                            surf.configure(blinc_app.device(), config);
                                        }
                                        sws.needs_rebuild = true;
                                        if let Some(ref mut ctx) = sws.ctx {
                                            let sf = window.scale_factor();
                                            ctx.width = width as f32 / sf as f32;
                                            ctx.height = height as f32 / sf as f32;
                                            ctx.physical_width = width as f32;
                                            ctx.physical_height = height as f32;
                                            ctx.scale_factor = sf;
                                        }
                                    }
                                }
                            }
                        }
                        Event::Window(_, WindowEvent::CloseRequested) => {
                            secondary_windows.remove(&wid);
                            tracing::info!("Secondary window closed (wid={:?})", wid);
                        }
                        Event::Window(_, WindowEvent::Focused(_focused)) => {}
                        Event::Input(_, ref input_event) => {

                            if let Some(sws) = secondary_windows.get_mut(&wid) {
                                if let (Some(ref mut ctx), Some(ref mut tree)) =
                                    (&mut sws.ctx, &mut sws.render_tree)
                                {
                                    let sf = ctx.scale_factor as f32;

                                    // Collect events from the router
                                    let mut pending: Vec<(blinc_layout::tree::LayoutNodeId, u32)> =
                                        Vec::new();
                                    ctx.event_router.set_event_callback({
                                        let events =
                                            &mut pending as *mut Vec<(blinc_layout::tree::LayoutNodeId, u32)>;
                                        move |node, event_type| unsafe {
                                            (*events).push((node, event_type));
                                        }
                                    });

                                    let convert_button =
                                        |b: &blinc_platform::MouseButton| match b {
                                            blinc_platform::MouseButton::Left => {
                                                blinc_layout::prelude::MouseButton::Left
                                            }
                                            blinc_platform::MouseButton::Right => {
                                                blinc_layout::prelude::MouseButton::Right
                                            }
                                            blinc_platform::MouseButton::Middle => {
                                                blinc_layout::prelude::MouseButton::Middle
                                            }
                                            _ => blinc_layout::prelude::MouseButton::Left,
                                        };

                                    match input_event {
                                        InputEvent::Mouse(MouseEvent::Moved { x, y }) => {
                                            ctx.event_router
                                                .on_mouse_move(tree, *x / sf, *y / sf);
                                        }
                                        InputEvent::Mouse(MouseEvent::ButtonPressed {
                                            button,
                                            x,
                                            y,
                                        }) => {
                                            ctx.event_router.on_mouse_down(
                                                tree,
                                                *x / sf,
                                                *y / sf,
                                                convert_button(button),
                                            );
                                        }
                                        InputEvent::Mouse(MouseEvent::ButtonReleased {
                                            button,
                                            x,
                                            y,
                                        }) => {
                                            ctx.event_router.on_mouse_up(
                                                tree,
                                                *x / sf,
                                                *y / sf,
                                                convert_button(button),
                                            );
                                        }
                                        _ => {}
                                    }

                                    ctx.event_router.clear_event_callback();

                                    // Dispatch collected events through render tree handlers
                                    for (node_id, event_type) in &pending {
                                        tree.dispatch_event(
                                            *node_id,
                                            *event_type,
                                            ctx.event_router.mouse_position().0,
                                            ctx.event_router.mouse_position().1,
                                        );
                                    }
                                }
                            }
                        }
                        Event::Frame(_) => {
                            if let Some(sws) = secondary_windows.get_mut(&wid) {

                                if let (Some(ref mut blinc_app), Some(ref surf), Some(ref config)) =
                                    (&mut ws.app, &sws.surface, &sws.surface_config)
                                {
                                    // Build render tree on first frame or after resize
                                    if sws.render_tree.is_none() || sws.needs_rebuild {
                                        let (w, h) = sws.ctx.as_ref()
                                            .map(|c| (c.width, c.height))
                                            .unwrap_or((400.0, 300.0));

                                        let ui: Div =
                                            if let Some(ref mut builder) = sws.ui_builder {
                                                if let Some(ref mut sctx) = sws.ctx {
                                                    builder(sctx)
                                                } else {
                                                    div().w(w).h(h)
                                                }
                                            } else {
                                                let title = window.winit_window().title();
                                                div()
                                                    .w(w)
                                                    .h(h)
                                                    .bg(blinc_core::Color::rgba(
                                                        0.06, 0.06, 0.09, 1.0,
                                                    ))
                                                    .flex_col()
                                                    .justify_center()
                                                    .items_center()
                                                    .gap_px(12.0)
                                                    .child(
                                                        text(&title)
                                                            .size(24.0)
                                                            .color(blinc_core::Color::WHITE)
                                                            .bold(),
                                                    )
                                                    .child(
                                                        text(format!("{:.0} x {:.0}", w, h))
                                                            .size(14.0)
                                                            .color(blinc_core::Color::rgba(
                                                                0.5, 0.5, 0.6, 1.0,
                                                            )),
                                                    )
                                            };

                                        let sf = sws
                                            .ctx
                                            .as_ref()
                                            .map(|c| c.scale_factor as f32)
                                            .unwrap_or(1.0);
                                        let mut tree = RenderTree::from_element(&ui);
                                        tree.set_scale_factor(sf);
                                        tree.compute_layout(w, h);
                                        sws.render_tree = Some(tree);
                                        sws.needs_rebuild = false;
                                    }

                                    // Render the tree (skip if minimized / zero size)
                                    if config.width > 0 && config.height > 0 {
                                        if let (Some(ref tree), Some(ref rs)) =
                                            (&sws.render_tree, &sws.render_state)
                                        {
                                            match surf.get_current_texture() {
                                                Ok(frame) => {
                                                    let view = frame.texture.create_view(
                                                        &wgpu::TextureViewDescriptor::default(),
                                                    );
                                                    let _ = blinc_app.render_tree_with_motion(
                                                        tree,
                                                        rs,
                                                        &view,
                                                        config.width,
                                                        config.height,
                                                    );
                                                    frame.present();
                                                }
                                                Err(
                                                    wgpu::SurfaceError::Lost
                                                    | wgpu::SurfaceError::Outdated,
                                                ) => {
                                                    surf.configure(blinc_app.device(), config);
                                                }
                                                Err(_) => {}
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        _ => {}
                    }
                    return ControlFlow::Continue;
                }

                match event {
                    Event::Lifecycle(LifecycleEvent::Resumed) => {
                        let wid = window.id();
                        // Initialize GPU if not already done (primary window)
                        if ws.app.is_none() {
                            primary_wid = Some(wid);
                            let winit_window = window.winit_window_arc();

                            match BlincApp::with_window(winit_window, None) {
                                Ok((blinc_app, surf)) => {
                                    let (width, height) = window.size();
                                    // Use the same texture format that the renderer's pipelines use
                                    let format = blinc_app.texture_format();
                                    let config = wgpu::SurfaceConfiguration {
                                        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                                            | wgpu::TextureUsages::COPY_SRC,
                                        format,
                                        width,
                                        height,
                                        present_mode: wgpu::PresentMode::AutoVsync,
                                        alpha_mode: wgpu::CompositeAlphaMode::Opaque,
                                        view_formats: vec![],
                                        desired_maximum_frame_latency: 2,
                                    };
                                    surf.configure(blinc_app.device(), &config);

                                    // Update text measurer with shared font registry for accurate measurement
                                    crate::text_measurer::init_text_measurer_with_registry(
                                        blinc_app.font_registry(),
                                    );

                                    ws.surface = Some(surf);
                                    ws.surface_config = Some(config);
                                    ws.app = Some(blinc_app);

                                    // Initialize context with event router, animations, dirty flag, reactive graph, hooks, overlay manager, registry, and ready callbacks
                                    ws.ctx = Some(WindowedContext::from_window(
                                        window,
                                        EventRouter::new(),
                                        Arc::clone(&animations),
                                        Arc::clone(&ref_dirty_flag),
                                        Arc::clone(&reactive),
                                        Arc::clone(&hooks),
                                        Arc::clone(&overlays),
                                        Arc::clone(&element_registry),
                                        Arc::clone(&ready_callbacks),
                                    ));

                                    // Wire open_window callback using the event loop's wake proxy
                                    let wp_for_ctx = wake_proxy_for_windows.clone();
                                    let open_fn: Arc<dyn Fn(WindowConfig) + Send + Sync> =
                                        Arc::new(move |config| {
                                            wp_for_ctx.create_window(config);
                                        });
                                    if let Some(ref mut windowed_ctx) = ws.ctx {
                                        windowed_ctx.set_open_window_fn(Arc::clone(&open_fn));
                                        // Per-window action callbacks
                                        let win_actions = Self::make_window_actions(
                                            window.winit_window_arc(),
                                            wake_proxy_for_windows.clone(),
                                        );
                                        windowed_ctx.set_window_actions(
                                            win_actions.0,
                                            win_actions.1,
                                            win_actions.2,
                                            win_actions.3,
                                        );
                                    }
                                    // Register globally so open_window() works from anywhere
                                    let _ = OPEN_WINDOW_FN.set(open_fn);

                                    // Register global window action callbacks (for drag_region() on Div)
                                    Self::register_window_actions_static(window.winit_window_arc(), wake_proxy_for_windows.clone());

                                    // Set initial viewport size in BlincContextState
                                    if let Some(ref windowed_ctx) = ws.ctx {
                                        BlincContextState::get().set_viewport_size(windowed_ctx.width, windowed_ctx.height);
                                    }

                                    // Initialize render state with the shared animation scheduler
                                    // RenderState handles dynamic properties (cursor blink, animations)
                                    // independently from tree structure changes
                                    let mut rs = blinc_layout::RenderState::new(Arc::clone(&animations));
                                    rs.set_shared_motion_states(Arc::clone(&shared_motion_states));
                                    ws.render_state = Some(rs);

                                    tracing::debug!("Blinc windowed ws.app initialized");
                                }
                                Err(e) => {
                                    tracing::error!("Failed to initialize Blinc: {}", e);
                                    return ControlFlow::Exit;
                                }
                            }
                        } else {
                            // Resumed for a secondary window
                            let wid = window.id();
                            #[allow(clippy::map_entry)]
                            if !secondary_windows.contains_key(&wid) {
                                if let Some(ref blinc_app) = ws.app {
                                    let winit_window = window.winit_window_arc();
                                    match blinc_app.create_surface_for_window(winit_window) {
                                        Ok(surf) => {
                                            let (w, h) = window.size();
                                            let format = blinc_app.texture_format();
                                            let config = wgpu::SurfaceConfiguration {
                                                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                                                    | wgpu::TextureUsages::COPY_SRC,
                                                format,
                                                width: w,
                                                height: h,
                                                present_mode: wgpu::PresentMode::AutoVsync,
                                                alpha_mode: wgpu::CompositeAlphaMode::Opaque,
                                                view_formats: vec![],
                                                desired_maximum_frame_latency: 2,
                                            };
                                            surf.configure(blinc_app.device(), &config);

                                            let mut sws = WindowState::new(
                                                Arc::clone(&css_anim_store),
                                                Arc::clone(&shared_motion_states),
                                            );
                                            sws.surface = Some(surf);
                                            sws.surface_config = Some(config);

                                            sws.ctx = Some(WindowedContext::from_window(
                                                window,
                                                EventRouter::new(),
                                                Arc::clone(&animations),
                                                Arc::clone(&ref_dirty_flag),
                                                Arc::clone(&reactive),
                                                Arc::clone(&hooks),
                                                Arc::clone(&overlays),
                                                Arc::clone(&element_registry),
                                                Arc::clone(&ready_callbacks),
                                            ));

                                            if let Some(ref mut ctx) = sws.ctx {
                                                let wp = wake_proxy_for_windows.clone();
                                                ctx.set_open_window_fn(Arc::new(move |c| {
                                                    wp.create_window(c);
                                                }));
                                                // Per-window actions
                                                let win_actions = Self::make_window_actions(
                                                    window.winit_window_arc(),
                                                    wake_proxy_for_windows.clone(),
                                                );
                                                ctx.set_window_actions(
                                                    win_actions.0,
                                                    win_actions.1,
                                                    win_actions.2,
                                                    win_actions.3,
                                                );
                                            }

                                            let mut rs = blinc_layout::RenderState::new(
                                                Arc::clone(&animations),
                                            );
                                            rs.set_shared_motion_states(
                                                Arc::clone(&shared_motion_states),
                                            );
                                            sws.render_state = Some(rs);

                                            // Pop the UI builder from the pending queue
                                            if let Ok(mut pending) =
                                                pending_builders().lock()
                                            {
                                                // Find matching request by config title
                                                let title =
                                                    window.winit_window().title();
                                                if let Some(idx) = pending.iter().position(|r| {
                                                    r.config.title == title
                                                }) {
                                                    let req = pending.remove(idx);
                                                    sws.ui_builder = req.builder;
                                                }
                                            }

                                            // Per-window callbacks are set via set_window_actions above.
                                            // Global window_actions is NOT set — secondary windows
                                            // use ctx.close_callback() etc. instead.

                                            secondary_windows.insert(wid, sws);
                                            tracing::info!(
                                                "Secondary window initialized (wid={:?})",
                                                wid
                                            );
                                        }
                                        Err(e) => {
                                            tracing::error!(
                                                "Failed to create surface for window: {}",
                                                e
                                            );
                                        }
                                    }
                                }
                            }
                        }
                    }

                    Event::Window(_, WindowEvent::Resized { width, height }) => {
                        if let (Some(ref blinc_app), Some(ref surf), Some(ref mut config)) =
                            (&ws.app, &ws.surface, &mut ws.surface_config)
                        {
                            if width > 0 && height > 0 {
                                config.width = width;
                                config.height = height;
                                surf.configure(blinc_app.device(), config);
                                ws.needs_rebuild = true;
                                ws.needs_relayout = true;

                                // Dispatch RESIZE event to elements (use logical dimensions)
                                if let (Some(ref mut windowed_ctx), Some(ref tree)) =
                                    (&mut ws.ctx, &ws.render_tree)
                                {
                                    let logical_width = width as f32 / windowed_ctx.scale_factor as f32;
                                    let logical_height = height as f32 / windowed_ctx.scale_factor as f32;

                                    // Update windowed context dimensions - CRITICAL for layout computation
                                    // Without this, compute_layout uses stale dimensions
                                    windowed_ctx.width = logical_width;
                                    windowed_ctx.height = logical_height;
                                    windowed_ctx.physical_width = width as f32;
                                    windowed_ctx.physical_height = height as f32;

                                    // Update viewport size in BlincContextState for ElementHandle.is_visible()
                                    BlincContextState::get().set_viewport_size(logical_width, logical_height);

                                    windowed_ctx
                                        .event_router
                                        .on_window_resize(tree, logical_width, logical_height);

                                    // Clear layout bounds storages to force fresh calculations
                                    // This prevents stale cached bounds from influencing the new layout
                                    tree.clear_layout_bounds_storages();
                                }

                                // Request redraw to trigger relayout with new dimensions
                                window.request_redraw();
                            }
                        }
                    }

                    Event::Window(_, WindowEvent::Focused(focused)) => {
                        // Update context focus state
                        if let Some(ref mut windowed_ctx) = ws.ctx {
                            windowed_ctx.focused = focused;
                            windowed_ctx.event_router.on_window_focus(focused);

                            if !focused {
                                blinc_layout::widgets::blur_all_text_inputs();
                            }
                        }
                    }

                    Event::Window(_, WindowEvent::CloseRequested) => {
                        return ControlFlow::Exit;
                    }

                    // File drop events — dispatch to drop handler and render tree
                    Event::Window(_, WindowEvent::DroppedFile { paths }) => {
                        crate::dnd::dispatch_drop_event(crate::dnd::DropEvent::Dropped(paths));
                    }
                    Event::Window(_, WindowEvent::DroppedFileHovered { paths }) => {
                        crate::dnd::dispatch_drop_event(crate::dnd::DropEvent::Hovered(paths));
                    }
                    Event::Window(_, WindowEvent::DroppedFileCancelled) => {
                        crate::dnd::dispatch_drop_event(crate::dnd::DropEvent::Cancelled);
                    }

                    // Handle input events
                    Event::Input(_, input_event) => {
                        // Pending event structure for deferred dispatch
                        #[derive(Clone)]
                        struct PendingEvent {
                            node_id: LayoutNodeId,
                            event_type: u32,
                            mouse_x: f32,
                            mouse_y: f32,
                            /// Local coordinates relative to element bounds
                            local_x: f32,
                            local_y: f32,
                            /// Absolute position of element bounds (top-left corner)
                            bounds_x: f32,
                            bounds_y: f32,
                            /// Computed bounds dimensions of the element
                            bounds_width: f32,
                            bounds_height: f32,
                            scroll_delta_x: f32,
                            scroll_delta_y: f32,
                            /// Drag delta for DRAG/DRAG_END events
                            drag_delta_x: f32,
                            drag_delta_y: f32,
                            key_char: Option<char>,
                            key_code: u32,
                            shift: bool,
                            ctrl: bool,
                            alt: bool,
                            meta: bool,
                            /// Pinch scale or rotation delta
                            pinch_scale: f32,
                        }

                        impl Default for PendingEvent {
                            fn default() -> Self {
                                Self {
                                    node_id: LayoutNodeId::default(),
                                    event_type: 0,
                                    mouse_x: 0.0,
                                    mouse_y: 0.0,
                                    local_x: 0.0,
                                    local_y: 0.0,
                                    bounds_x: 0.0,
                                    bounds_y: 0.0,
                                    bounds_width: 0.0,
                                    bounds_height: 0.0,
                                    scroll_delta_x: 0.0,
                                    scroll_delta_y: 0.0,
                                    drag_delta_x: 0.0,
                                    drag_delta_y: 0.0,
                                    key_char: None,
                                    key_code: 0,
                                    shift: false,
                                    ctrl: false,
                                    alt: false,
                                    meta: false,
                                    pinch_scale: 1.0,
                                }
                            }
                        }

                        // First phase: collect events using immutable borrow
                        let (pending_events, keyboard_events, scroll_ended, gesture_ended, scroll_info) = if let (Some(ref mut windowed_ctx), Some(ref tree)) =
                            (&mut ws.ctx, &ws.render_tree)
                        {
                            let router = &mut windowed_ctx.event_router;

                            // Collect events from router
                            let mut pending_events: Vec<PendingEvent> = Vec::new();
                            // Separate collection for keyboard events (TEXT_INPUT)
                            let mut keyboard_events: Vec<PendingEvent> = Vec::new();
                            // Track if scroll ended (momentum finished)
                            let mut scroll_ended = false;
                            // Track if gesture ended (finger lifted - may still have momentum)
                            let mut gesture_ended = false;
                            // Track scroll info for nested scroll dispatch (mouse_x, mouse_y, delta_x, delta_y)
                            let mut scroll_info: Option<(f32, f32, f32, f32)> = None;

                            // Set up callback to collect events
                            router.set_event_callback({
                                let events = &mut pending_events as *mut Vec<PendingEvent>;
                                move |node, event_type| {
                                    // SAFETY: This callback is only used within this scope
                                    unsafe {
                                        (*events).push(PendingEvent {
                                            node_id: node,
                                            event_type,
                                            ..Default::default()
                                        });
                                    }
                                }
                            });

                            // Note: Overlays are now part of the main tree, so all events
                            // are routed through the single main event router.

                            // Convert physical coordinates to logical for hit testing
                            let scale = windowed_ctx.scale_factor as f32;

                            match input_event {
                                InputEvent::Mouse(mouse_event) => match mouse_event {
                                    MouseEvent::Moved { x, y } => {
                                        // Convert physical to logical coordinates
                                        let lx = x / scale;
                                        let ly = y / scale;

                                        // Get overlay bounds and layer ID for occlusion-aware hit testing
                                        // This prevents background elements from receiving hover events
                                        // when they are visually occluded by overlay content
                                        let overlay_bounds = windowed_ctx.overlay_manager.get_visible_overlay_bounds();
                                        let overlay_layer_id = tree.query_by_id(
                                            blinc_layout::widgets::overlay::OVERLAY_LAYER_ID
                                        );

                                        // Route mouse move through main tree with overlay occlusion awareness
                                        router.on_mouse_move_with_occlusion(
                                            tree,
                                            lx,
                                            ly,
                                            &overlay_bounds,
                                            overlay_layer_id,
                                        );

                                        // Get drag delta from router (for DRAG events)
                                        let (drag_dx, drag_dy) = router.drag_delta();

                                        // Populate bounds for each event from the router's hit test results
                                        // This is needed for POINTER_ENTER/POINTER_LEAVE/POINTER_MOVE events
                                        for event in pending_events.iter_mut() {
                                            event.mouse_x = lx;
                                            event.mouse_y = ly;
                                            // Populate drag delta for DRAG events
                                            if event.event_type == blinc_core::events::event_types::DRAG
                                                || event.event_type == blinc_core::events::event_types::DRAG_END
                                            {
                                                event.drag_delta_x = drag_dx;
                                                event.drag_delta_y = drag_dy;
                                            }
                                            // Populate bounds from hit test results (stored in router)
                                            if let Some((bx, by, bw, bh)) = router.get_node_bounds(event.node_id) {
                                                event.bounds_x = bx;
                                                event.bounds_y = by;
                                                event.bounds_width = bw;
                                                event.bounds_height = bh;
                                                event.local_x = lx - bx;
                                                event.local_y = ly - by;
                                            }
                                        }

                                        // Update cursor based on hovered element
                                        let cursor = tree
                                            .get_cursor_at(router, lx, ly)
                                            .unwrap_or(CursorStyle::Default);
                                        window.set_cursor(convert_cursor_style(cursor));
                                    }
                                    MouseEvent::ButtonPressed { button, x, y } => {
                                        let lx = x / scale;
                                        let ly = y / scale;
                                        let btn = convert_mouse_button(button);
                                        windowed_ctx.pointer_query.set_pressure(1.0);

                                        // Check for backdrop clicks (dismisses overlays)
                                        // This still needs special handling because backdrop clicks should
                                        // not propagate to elements behind the overlay
                                        let overlay_dismissed = if windowed_ctx.overlay_manager.has_blocking_overlay()
                                            || windowed_ctx.overlay_manager.has_dismissable_overlay()
                                        {
                                            windowed_ctx.overlay_manager.handle_click_at(lx, ly)
                                        } else {
                                            false
                                        };

                                        // If overlay was dismissed by backdrop click, don't process further
                                        if !overlay_dismissed {
                                            // Blur any focused text inputs BEFORE processing mouse down
                                            // This mimics HTML behavior where clicking anywhere blurs inputs,
                                            // and clicking on an input then re-focuses it via its own handler
                                            blinc_layout::widgets::blur_all_text_inputs();

                                            // Route through main tree (includes overlay content)
                                            let _events = router.on_mouse_down(tree, lx, ly, btn);

                                            let (local_x, local_y) = router.last_hit_local();
                                            let (bounds_x, bounds_y) = router.last_hit_bounds_pos();
                                            let (bounds_width, bounds_height) = router.last_hit_bounds();
                                            for event in pending_events.iter_mut() {
                                                event.mouse_x = lx;
                                                event.mouse_y = ly;
                                                event.local_x = local_x;
                                                event.local_y = local_y;
                                                event.bounds_x = bounds_x;
                                                event.bounds_y = bounds_y;
                                                event.bounds_width = bounds_width;
                                                event.bounds_height = bounds_height;
                                            }
                                        }
                                    }
                                    MouseEvent::ButtonReleased { button, x, y } => {
                                        let lx = x / scale;
                                        let ly = y / scale;
                                        let btn = convert_mouse_button(button);
                                        windowed_ctx.pointer_query.set_pressure(0.0);

                                        // Route through main tree (includes overlay content)
                                        router.on_mouse_up(tree, lx, ly, btn);
                                        // Use the local coordinates from when the press started
                                        // (stored by on_mouse_down via last_hit_local)
                                        let (local_x, local_y) = router.last_hit_local();
                                        let (bounds_x, bounds_y) = router.last_hit_bounds_pos();
                                        let (bounds_width, bounds_height) = router.last_hit_bounds();
                                        for event in pending_events.iter_mut() {
                                            event.mouse_x = lx;
                                            event.mouse_y = ly;
                                            event.local_x = local_x;
                                            event.local_y = local_y;
                                            event.bounds_x = bounds_x;
                                            event.bounds_y = bounds_y;
                                            event.bounds_width = bounds_width;
                                            event.bounds_height = bounds_height;
                                        }
                                    }
                                    MouseEvent::Left => {
                                        // on_mouse_leave now emits POINTER_UP if there was a pressed target
                                        // This handles the case where mouse leaves window while dragging
                                        router.on_mouse_leave();
                                        // Reset cursor to default when mouse leaves window
                                        window.set_cursor(blinc_platform::Cursor::Default);
                                        // Events are collected via the callback set above
                                    }
                                    MouseEvent::Entered => {
                                        let (mx, my) = router.mouse_position();

                                        // Use occlusion-aware hit testing when mouse enters window
                                        let overlay_bounds = windowed_ctx.overlay_manager.get_visible_overlay_bounds();
                                        let overlay_layer_id = tree.query_by_id(
                                            blinc_layout::widgets::overlay::OVERLAY_LAYER_ID
                                        );
                                        router.on_mouse_move_with_occlusion(
                                            tree,
                                            mx,
                                            my,
                                            &overlay_bounds,
                                            overlay_layer_id,
                                        );

                                        for event in pending_events.iter_mut() {
                                            event.mouse_x = mx;
                                            event.mouse_y = my;
                                        }

                                        // Update cursor based on hovered element
                                        let cursor = tree
                                            .get_cursor_at(router, mx, my)
                                            .unwrap_or(CursorStyle::Default);
                                        window.set_cursor(convert_cursor_style(cursor));
                                    }
                                },
                                InputEvent::Keyboard(kb_event) => {
                                    let mods = &kb_event.modifiers;

                                    // Extract character from key if applicable
                                    let key_char = match &kb_event.key {
                                        Key::Char(c) => Some(*c),
                                        Key::Space => Some(' '),
                                        Key::A => Some(if mods.shift { 'A' } else { 'a' }),
                                        Key::B => Some(if mods.shift { 'B' } else { 'b' }),
                                        Key::C => Some(if mods.shift { 'C' } else { 'c' }),
                                        Key::D => Some(if mods.shift { 'D' } else { 'd' }),
                                        Key::E => Some(if mods.shift { 'E' } else { 'e' }),
                                        Key::F => Some(if mods.shift { 'F' } else { 'f' }),
                                        Key::G => Some(if mods.shift { 'G' } else { 'g' }),
                                        Key::H => Some(if mods.shift { 'H' } else { 'h' }),
                                        Key::I => Some(if mods.shift { 'I' } else { 'i' }),
                                        Key::J => Some(if mods.shift { 'J' } else { 'j' }),
                                        Key::K => Some(if mods.shift { 'K' } else { 'k' }),
                                        Key::L => Some(if mods.shift { 'L' } else { 'l' }),
                                        Key::M => Some(if mods.shift { 'M' } else { 'm' }),
                                        Key::N => Some(if mods.shift { 'N' } else { 'n' }),
                                        Key::O => Some(if mods.shift { 'O' } else { 'o' }),
                                        Key::P => Some(if mods.shift { 'P' } else { 'p' }),
                                        Key::Q => Some(if mods.shift { 'Q' } else { 'q' }),
                                        Key::R => Some(if mods.shift { 'R' } else { 'r' }),
                                        Key::S => Some(if mods.shift { 'S' } else { 's' }),
                                        Key::T => Some(if mods.shift { 'T' } else { 't' }),
                                        Key::U => Some(if mods.shift { 'U' } else { 'u' }),
                                        Key::V => Some(if mods.shift { 'V' } else { 'v' }),
                                        Key::W => Some(if mods.shift { 'W' } else { 'w' }),
                                        Key::X => Some(if mods.shift { 'X' } else { 'x' }),
                                        Key::Y => Some(if mods.shift { 'Y' } else { 'y' }),
                                        Key::Z => Some(if mods.shift { 'Z' } else { 'z' }),
                                        Key::Num0 => Some(if mods.shift { ')' } else { '0' }),
                                        Key::Num1 => Some(if mods.shift { '!' } else { '1' }),
                                        Key::Num2 => Some(if mods.shift { '@' } else { '2' }),
                                        Key::Num3 => Some(if mods.shift { '#' } else { '3' }),
                                        Key::Num4 => Some(if mods.shift { '$' } else { '4' }),
                                        Key::Num5 => Some(if mods.shift { '%' } else { '5' }),
                                        Key::Num6 => Some(if mods.shift { '^' } else { '6' }),
                                        Key::Num7 => Some(if mods.shift { '&' } else { '7' }),
                                        Key::Num8 => Some(if mods.shift { '*' } else { '8' }),
                                        Key::Num9 => Some(if mods.shift { '(' } else { '9' }),
                                        Key::Minus => Some(if mods.shift { '_' } else { '-' }),
                                        Key::Equals => Some(if mods.shift { '+' } else { '=' }),
                                        Key::LeftBracket => Some(if mods.shift { '{' } else { '[' }),
                                        Key::RightBracket => Some(if mods.shift { '}' } else { ']' }),
                                        Key::Backslash => Some(if mods.shift { '|' } else { '\\' }),
                                        Key::Semicolon => Some(if mods.shift { ':' } else { ';' }),
                                        Key::Quote => Some(if mods.shift { '"' } else { '\'' }),
                                        Key::Comma => Some(if mods.shift { '<' } else { ',' }),
                                        Key::Period => Some(if mods.shift { '>' } else { '.' }),
                                        Key::Slash => Some(if mods.shift { '?' } else { '/' }),
                                        Key::Grave => Some(if mods.shift { '~' } else { '`' }),
                                        _ => None,
                                    };

                                    // Key code for special key handling (backspace, arrows, etc)
                                    // Letter keys use ASCII uppercase (65=A, 90=Z) for Cmd+key shortcuts
                                    let key_code = match &kb_event.key {
                                        Key::Backspace => 8,
                                        Key::Delete => 127,
                                        Key::Enter => 13,
                                        Key::Tab => 9,
                                        Key::Escape => 27,
                                        Key::Left => 37,
                                        Key::Right => 39,
                                        Key::Up => 38,
                                        Key::Down => 40,
                                        Key::Home => 36,
                                        Key::End => 35,
                                        // Map letter keys to ASCII uppercase for Cmd+key shortcuts
                                        Key::A => 65, Key::B => 66, Key::C => 67,
                                        Key::D => 68, Key::E => 69, Key::F => 70,
                                        Key::G => 71, Key::H => 72, Key::I => 73,
                                        Key::J => 74, Key::K => 75, Key::L => 76,
                                        Key::M => 77, Key::N => 78, Key::O => 79,
                                        Key::P => 80, Key::Q => 81, Key::R => 82,
                                        Key::S => 83, Key::T => 84, Key::U => 85,
                                        Key::V => 86, Key::W => 87, Key::X => 88,
                                        Key::Y => 89, Key::Z => 90,
                                        Key::Back => {
                                            // System back button — dispatch through back handler
                                            if blinc_layout::back_handler::dispatch_back() {
                                                return ControlFlow::Continue;
                                            }
                                            // Not consumed — let default handling proceed
                                            0
                                        }
                                        _ => 0,
                                    };

                                    match kb_event.state {
                                        KeyState::Pressed => {
                                            // Handle Escape key for overlays first
                                            // If an overlay handles it, don't propagate further
                                            if kb_event.key == Key::Escape
                                                && windowed_ctx.overlay_manager.handle_escape()
                                            {
                                                // Escape was consumed by overlay, skip further processing
                                                // (but continue collecting events for non-overlay targets)
                                            }

                                            // Dispatch KEY_DOWN for all keys
                                            router.on_key_down(key_code);

                                            // For character-producing keys, dispatch TEXT_INPUT
                                            // We use broadcast dispatch so any focused text input can receive it
                                            if let Some(c) = key_char {
                                                // Don't send text input if ctrl/cmd is held (shortcuts)
                                                if !mods.ctrl && !mods.meta {
                                                    keyboard_events.push(PendingEvent {
                                                        event_type: blinc_core::events::event_types::TEXT_INPUT,
                                                        key_char: Some(c),
                                                        key_code,
                                                        shift: mods.shift,
                                                        ctrl: mods.ctrl,
                                                        alt: mods.alt,
                                                        meta: mods.meta,
                                                        ..Default::default()
                                                    });
                                                }
                                            }

                                            // For KEY_DOWN events with special keys (backspace, arrows)
                                            if key_code != 0 {
                                                keyboard_events.push(PendingEvent {
                                                    event_type: blinc_core::events::event_types::KEY_DOWN,
                                                    key_char: None,
                                                    key_code,
                                                    shift: mods.shift,
                                                    ctrl: mods.ctrl,
                                                    alt: mods.alt,
                                                    meta: mods.meta,
                                                    ..Default::default()
                                                });
                                            }
                                        }
                                        KeyState::Released => {
                                            router.on_key_up(key_code);
                                        }
                                    }
                                },
                                InputEvent::Touch(touch_event) => {
                                    // Track active touch IDs for touch count
                                    match &touch_event {
                                        TouchEvent::Started { .. } => {
                                            ws.active_touch_ids.insert(touch_event.id());
                                            windowed_ctx.pointer_query.set_touch_count(ws.active_touch_ids.len() as u32);
                                        }
                                        TouchEvent::Ended { .. } => {
                                            ws.active_touch_ids.remove(&touch_event.id());
                                            windowed_ctx.pointer_query.set_touch_count(ws.active_touch_ids.len() as u32);
                                        }
                                        TouchEvent::Cancelled { .. } => {
                                            ws.active_touch_ids.remove(&touch_event.id());
                                            windowed_ctx.pointer_query.set_touch_count(ws.active_touch_ids.len() as u32);
                                        }
                                        _ => {}
                                    }
                                    match touch_event {
                                        TouchEvent::Started { x, y, pressure, .. } => {
                                            let lx = x / scale;
                                            let ly = y / scale;
                                            windowed_ctx.pointer_query.set_pressure(pressure);
                                            router.on_mouse_down(tree, lx, ly, MouseButton::Left);
                                            let (local_x, local_y) = router.last_hit_local();
                                            let (bounds_x, bounds_y) = router.last_hit_bounds_pos();
                                            let (bounds_width, bounds_height) = router.last_hit_bounds();
                                            for event in pending_events.iter_mut() {
                                                event.mouse_x = lx;
                                                event.mouse_y = ly;
                                                event.local_x = local_x;
                                                event.local_y = local_y;
                                                event.bounds_x = bounds_x;
                                                event.bounds_y = bounds_y;
                                                event.bounds_width = bounds_width;
                                                event.bounds_height = bounds_height;
                                            }
                                        }
                                        TouchEvent::Moved { x, y, pressure, .. } => {
                                            let lx = x / scale;
                                            let ly = y / scale;
                                            windowed_ctx.pointer_query.set_pressure(pressure);

                                            // Use occlusion-aware hit testing for touch move as well
                                            let overlay_bounds = windowed_ctx.overlay_manager.get_visible_overlay_bounds();
                                            let overlay_layer_id = tree.query_by_id(
                                                blinc_layout::widgets::overlay::OVERLAY_LAYER_ID
                                            );
                                            router.on_mouse_move_with_occlusion(
                                                tree,
                                                lx,
                                                ly,
                                                &overlay_bounds,
                                                overlay_layer_id,
                                            );

                                            for event in pending_events.iter_mut() {
                                                event.mouse_x = lx;
                                                event.mouse_y = ly;
                                            }
                                        }
                                        TouchEvent::Ended { x, y, .. } => {
                                            let lx = x / scale;
                                            let ly = y / scale;
                                            windowed_ctx.pointer_query.set_pressure(0.0);
                                            router.on_mouse_up(tree, lx, ly, MouseButton::Left);
                                            for event in pending_events.iter_mut() {
                                                event.mouse_x = lx;
                                                event.mouse_y = ly;
                                            }
                                        }
                                        TouchEvent::Cancelled { .. } => {
                                            // Touch cancelled - treat like mouse leave
                                            // This will emit POINTER_UP if there was a pressed target
                                            windowed_ctx.pointer_query.set_pressure(0.0);
                                            windowed_ctx.pointer_query.set_touch_count(0);
                                            router.on_mouse_leave();
                                        }
                                    }
                                }
                                InputEvent::Scroll { delta_x, delta_y, phase } => {
                                    let (mx, my) = router.mouse_position();
                                    // Scroll deltas are also in physical pixels, convert to logical
                                    let ldx = delta_x;
                                    let ldy = delta_y;

                                    tracing::trace!(
                                        "InputEvent::Scroll received: pos=({:.1}, {:.1}) delta=({:.1}, {:.1}) phase={:?}",
                                        mx, my, ldx, ldy, phase
                                    );

                                    // Check if gesture ended (finger lifted from trackpad)
                                    // This happens before momentum ends
                                    if phase == blinc_platform::ScrollPhase::Ended {
                                        gesture_ended = true;
                                    }

                                    // Use nested scroll support - get hit result for smart dispatch
                                    // Store mouse position and delta for dispatch phase
                                    // We'll re-do hit test in dispatch phase since we need mutable borrow
                                    scroll_info = Some((mx, my, ldx, ldy));
                                }
                                InputEvent::ScrollEnd => {
                                    // Scroll momentum ended - full stop
                                    scroll_ended = true;
                                }
                                InputEvent::Pinch { scale, .. } => {
                                    let (mx, my) = router.mouse_position();
                                    pending_events.push(PendingEvent {
                                        event_type: blinc_core::events::event_types::PINCH,
                                        mouse_x: mx,
                                        mouse_y: my,
                                        pinch_scale: scale,
                                        ..Default::default()
                                    });
                                }
                                InputEvent::Rotation { angle, .. } => {
                                    let (mx, my) = router.mouse_position();
                                    pending_events.push(PendingEvent {
                                        event_type: blinc_core::events::event_types::ROTATE,
                                        mouse_x: mx,
                                        mouse_y: my,
                                        pinch_scale: angle,
                                        ..Default::default()
                                    });
                                }
                            }

                            router.clear_event_callback();
                            (pending_events, keyboard_events, scroll_ended, gesture_ended, scroll_info)
                        } else {
                            (Vec::new(), Vec::new(), false, false, None)
                        };

                        // Second phase: dispatch events with mutable borrow
                        // This automatically marks the tree dirty when handlers fire
                        if let Some(ref mut tree) = ws.render_tree {
                            // IMPORTANT: Process gesture_ended BEFORE scroll delta dispatch
                            // When gesture ends while overscrolling, we start bounce which
                            // sets state to Bouncing. Then apply_scroll_delta will early-return
                            // and ignore the momentum delta that came with this same event.
                            if gesture_ended {
                                tree.on_gesture_end();
                                // Request redraw to animate bounce-back
                                window.request_redraw();
                            }

                            // Handle scroll with nested scroll support
                            // Skip scroll delta entirely if gesture just ended - the delta
                            // from the same event as gesture_ended is the last finger movement,
                            // not momentum, but we still want to ignore it for instant snap-back
                            //
                            // Also skip scroll when an overlay with an actual backdrop is open to prevent
                            // background content from scrolling while dropdown/modal is visible.
                            // Note: We only check has_blocking_overlay(), not has_dismissable_overlay(),
                            // because overlays with dismiss_on_click_outside (like popovers) should allow
                            // scroll events to pass through to content behind them.
                            let has_overlay_backdrop = ws.ctx
                                .as_ref()
                                .map(|c| c.overlay_manager.has_blocking_overlay())
                                .unwrap_or(false);

                            if let Some((mouse_x, mouse_y, delta_x, delta_y)) = scroll_info {
                                // Skip if gesture ended in this same event - go straight to bounce
                                if gesture_ended {
                                    tracing::trace!("Skipping scroll delta - gesture ended, bouncing");
                                } else if has_overlay_backdrop {
                                    // Skip scroll when overlay is visible to prevent background scrolling
                                    tracing::trace!("Skipping scroll delta - overlay with backdrop is visible");
                                } else {
                                    tracing::trace!(
                                        "Scroll dispatch: pos=({:.1}, {:.1}) delta=({:.1}, {:.1})",
                                        mouse_x, mouse_y, delta_x, delta_y
                                    );

                                    // Update overlay positions for overlays with follows_scroll enabled
                                    // Use the singleton overlay manager since components use get_overlay_manager()
                                    if OverlayContext::is_initialized() {
                                        let mgr = get_overlay_manager();
                                        if mgr.handle_scroll(delta_y) {
                                            // Apply scroll offsets to render tree for visual movement
                                            for (element_id, offset_y) in mgr.get_scroll_offsets() {
                                                if let Some(node_id) = tree.query_by_id(&element_id) {
                                                    tree.set_scroll_offset(node_id, 0.0, offset_y);
                                                }
                                            }
                                            window.request_redraw();
                                        }
                                    }

                                    // Re-do hit test with mutable borrow to get ancestor chain
                                    // Then use dispatch_scroll_chain for proper nested scroll handling
                                    if let Some(ref mut windowed_ctx) = ws.ctx {
                                        let router = &mut windowed_ctx.event_router;
                                        if let Some(hit) = router.hit_test(tree, mouse_x, mouse_y) {
                                            tree.dispatch_scroll_chain(
                                                hit.node,
                                                &hit.ancestors,
                                                mouse_x,
                                                mouse_y,
                                                delta_x,
                                                delta_y,
                                            );
                                        }
                                    }
                                }
                            }

                            // Dispatch mouse/touch events (scroll is handled above with nested support)
                            if let Some(ref mut windowed_ctx) = ws.ctx {
                                let router = &windowed_ctx.event_router;
                                for mut event in pending_events {
                                    // Skip scroll events - already handled with nested scroll support
                                    if event.event_type == blinc_core::events::event_types::SCROLL {
                                        continue;
                                    }
                                    // Gesture events (PINCH/ROTATE) need hit testing since
                                    // they were collected without a node target
                                    if (event.event_type == blinc_core::events::event_types::PINCH
                                        || event.event_type
                                            == blinc_core::events::event_types::ROTATE)
                                        && event.node_id == LayoutNodeId::default()
                                    {
                                        if let Some(hit) =
                                            router.hit_test(tree, event.mouse_x, event.mouse_y)
                                        {
                                            event.node_id = hit.node;
                                            event.local_x = hit.local_x;
                                            event.local_y = hit.local_y;
                                            event.bounds_x = hit.bounds_x;
                                            event.bounds_y = hit.bounds_y;
                                            event.bounds_width = hit.bounds_width;
                                            event.bounds_height = hit.bounds_height;
                                        } else {
                                            continue; // No element under cursor
                                        }
                                    }
                                    // Look up the correct bounds for this specific node.
                                    // When events bubble from a child to a parent handler,
                                    // we need the parent's bounds, not the original hit target's bounds.
                                    let (bounds_x, bounds_y, bounds_width, bounds_height) =
                                        router.get_node_bounds(event.node_id).unwrap_or((
                                            event.bounds_x,
                                            event.bounds_y,
                                            event.bounds_width,
                                            event.bounds_height,
                                        ));
                                    let local_x = event.mouse_x - bounds_x;
                                    let local_y = event.mouse_y - bounds_y;
                                    tree.dispatch_event_full(
                                        event.node_id,
                                        event.event_type,
                                        event.mouse_x,
                                        event.mouse_y,
                                        local_x,
                                        local_y,
                                        bounds_x,
                                        bounds_y,
                                        bounds_width,
                                        bounds_height,
                                        event.drag_delta_x,
                                        event.drag_delta_y,
                                        event.pinch_scale,
                                    );
                                }
                            }

                            // Note: Overlay events are now dispatched through the main tree
                            // since overlays are composed into the main tree via build_overlay_layer()

                            // Dispatch keyboard events
                            // Use broadcast instead of bubbling to handle focus correctly after tree rebuilds.
                            // Text inputs track their own focus state internally via `s.visual.is_focused()`,
                            // so broadcasting to all handlers is safe - only the focused one will process.
                            for event in keyboard_events {
                                if event.event_type == blinc_core::events::event_types::TEXT_INPUT {
                                    if let Some(c) = event.key_char {
                                        // Broadcast to all text input handlers
                                        // Each handler checks its own focus state internally
                                        tree.broadcast_text_input_event(
                                            c,
                                            event.shift,
                                            event.ctrl,
                                            event.alt,
                                            event.meta,
                                        );
                                    }
                                } else {
                                    // Broadcast KEY_DOWN to all key handlers
                                    tree.broadcast_key_event(
                                        event.event_type,
                                        event.key_code,
                                        event.shift,
                                        event.ctrl,
                                        event.alt,
                                        event.meta,
                                    );
                                }
                            }

                            // If scroll momentum ended, notify scroll physics
                            if scroll_ended {
                                tree.on_scroll_end();
                                // Request redraw to animate bounce-back
                                window.request_redraw();
                            }
                        }
                    }

                    Event::Frame(_) => {
                        if let (
                            Some(ref mut blinc_app),
                            Some(ref surf),
                            Some(ref config),
                            Some(ref mut windowed_ctx),
                            Some(ref mut rs),
                        ) = (&mut ws.app, &ws.surface, &ws.surface_config, &mut ws.ctx, &mut ws.render_state)
                        {
                            // Get current frame
                            let frame = match surf.get_current_texture() {
                                Ok(f) => f,
                                Err(wgpu::SurfaceError::Lost) => {
                                    surf.configure(blinc_app.device(), config);
                                    return ControlFlow::Continue;
                                }
                                Err(wgpu::SurfaceError::OutOfMemory) => {
                                    tracing::error!("Out of GPU memory");
                                    return ControlFlow::Exit;
                                }
                                Err(e) => {
                                    tracing::warn!("Surface error: {:?}", e);
                                    return ControlFlow::Continue;
                                }
                            };

                            let view = frame
                                .texture
                                .create_view(&wgpu::TextureViewDescriptor::default());

                            // Update context from window
                            windowed_ctx.update_from_window(window);

                            // Update viewport for lazy loading visibility checks
                            // Uses logical pixels (width/height) as that's what layout uses
                            rs.set_viewport_size(windowed_ctx.width, windowed_ctx.height);

                            // Get current time for animation updates (used in multiple phases)
                            let current_time = elapsed_ms();

                            // Clear overlays from previous frame (cursor, selection, focus ring)
                            // These are re-added during rendering if still active
                            rs.clear_overlays();

                            // Tick scroll physics and sync ScrollRef state BEFORE any rebuilds
                            // This ensures ScrollRef has up-to-date values when stateful components
                            // query scroll position during rebuild
                            let scroll_animating = if let Some(ref mut tree) = ws.render_tree {
                                let animating = tree.tick_scroll_physics(current_time);
                                tree.process_pending_scroll_refs();
                                animating
                            } else {
                                false
                            };

                            // =========================================================
                            // PHASE 1: Check if tree structure needs rebuild
                            // Only structural changes require tree rebuild
                            // =========================================================

                            // Check if event handlers marked anything dirty (auto-rebuild)
                            if let Some(ref tree) = ws.render_tree {
                                if tree.needs_rebuild() {
                                    tracing::debug!("Rebuild triggered by: dirty_tracker");
                                    ws.needs_rebuild = true;
                                }
                            }

                            // Check if element refs were modified (triggers rebuild)
                            if ref_dirty_flag.swap(false, Ordering::SeqCst) {
                                tracing::debug!("Rebuild triggered by: ref_dirty_flag (State::set)");
                                ws.needs_rebuild = true;
                            }

                            // Check if text widgets requested a rebuild (focus/text changes)
                            if blinc_layout::widgets::take_needs_rebuild() {
                                tracing::debug!("Rebuild triggered by: text widget state change");
                                ws.needs_rebuild = true;
                            }

                            // Check if a full relayout was requested (e.g., theme changes)
                            if blinc_layout::widgets::take_needs_relayout() {
                                tracing::debug!("Relayout triggered by: theme or global state change");
                                ws.needs_relayout = true;
                            }

                            // Check if CSS stylesheets need reparsing (e.g., theme color scheme changed)
                            // This must happen before tree rebuild so the new stylesheet is available
                            if blinc_layout::widgets::take_needs_css_reparse() {
                                tracing::debug!("Reparsing CSS stylesheets due to theme change");
                                windowed_ctx.reparse_css();
                            }

                            // Process pending motion exit starts BEFORE overlay update
                            // This is critical: when an overlay closes, it queues a motion exit via
                            // query_motion(key).exit(). The overlay's update() method then checks
                            // if the motion is done animating. If we don't process the exit queue
                            // first, the motion won't be in Exiting state yet, and update() will
                            // incorrectly think the exit animation is complete.
                            rs.process_global_motion_exit_starts();
                            rs.process_global_motion_exit_cancels();
                            // Process suspended motion starts queued via query_motion(key).start()
                            rs.process_global_motion_starts();

                            // Sync motion states to shared store so overlay can query them
                            // This must happen after processing exits but before overlay update
                            rs.sync_shared_motion_states();

                            // Update overlay manager viewport and state for subtree rebuilds
                            // This must happen BEFORE checking is_dirty() so build_overlay_layer() works correctly
                            windowed_ctx.overlay_manager.set_viewport_with_scale(
                                windowed_ctx.width,
                                windowed_ctx.height,
                                windowed_ctx.scale_factor as f32,
                            );
                            windowed_ctx.overlay_manager.update(current_time);

                            // Check if overlay content changed (new overlay opened/closed)
                            // NOTE: We only rebuild on actual content changes, NOT during animations.
                            // Animation visual updates (backdrop opacity, motion transforms) are handled
                            // by the motion system and render-time interpolation, not content rebuilds.
                            // Rebuilding during animation breaks event handlers because node IDs change.
                            let overlay_content_dirty = windowed_ctx.overlay_manager.is_dirty();

                            if overlay_content_dirty {
                                tracing::debug!(
                                    "Overlay rebuild: dirty={}, has_visible={}",
                                    overlay_content_dirty,
                                    windowed_ctx.overlay_manager.has_visible_overlays()
                                );
                                // Look up the overlay layer node by its element ID
                                if let Some(overlay_node_id) = element_registry.get(
                                    blinc_layout::widgets::overlay::OVERLAY_LAYER_ID
                                ) {
                                    tracing::debug!("Overlay changed - queueing subtree rebuild for node {:?}", overlay_node_id);
                                    // Build the new overlay content and queue the subtree rebuild
                                    let overlay_content = windowed_ctx.overlay_manager.build_overlay_layer();
                                    blinc_layout::queue_subtree_rebuild(overlay_node_id, overlay_content);
                                } else {
                                    tracing::warn!("Overlay changed but node '{}' not found in registry - will rebuild on next frame",
                                        blinc_layout::widgets::overlay::OVERLAY_LAYER_ID);
                                }
                                // Consume the dirty flag
                                windowed_ctx.overlay_manager.take_dirty();
                            }

                            // Check if stateful elements requested a redraw (hover/press changes)
                            // Apply incremental prop updates without full rebuild
                            let has_stateful_updates = blinc_layout::take_needs_redraw();
                            let has_pending_rebuilds = blinc_layout::has_pending_subtree_rebuilds();

                            if has_stateful_updates || has_pending_rebuilds {
                                if has_stateful_updates {
                                    tracing::debug!("Redraw requested by: stateful state change");
                                }

                                // Get all pending prop updates
                                let prop_updates = blinc_layout::take_pending_prop_updates();
                                let had_prop_updates = !prop_updates.is_empty();

                                // Apply prop updates to the main tree
                                // (Overlays are now part of the main tree, so all nodes are here)
                                if let Some(ref mut tree) = ws.render_tree {
                                    for (node_id, props) in &prop_updates {
                                        tree.update_render_props(*node_id, |p| *p = props.clone());
                                    }
                                }

                                // Process subtree rebuilds (from stateful changes OR overlay changes)
                                let mut needs_layout = false;
                                if let Some(ref mut tree) = ws.render_tree {
                                    needs_layout = tree.process_pending_subtree_rebuilds();
                                }

                                if needs_layout {
                                    if let Some(ref mut tree) = ws.render_tree {
                                        tracing::debug!("Subtree rebuilds processed, recomputing layout");
                                        tree.apply_stylesheet_layout_overrides();
                                        tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                        // FLIP: detect position changes and start CSS transitions
                                        tree.apply_flip_transitions();
                                        // Update FLIP bounds cache for next rebuild
                                        tree.update_flip_bounds();
                                        // Begin/end motion frame to track which motions are still in tree
                                        rs.begin_stable_motion_frame();
                                        tree.initialize_motion_animations(rs);
                                        rs.end_stable_motion_frame();
                                        rs.process_global_motion_replays();
                                        // Start CSS animations for elements with animation properties
                                        tree.start_all_css_animations();
                                    }
                                }
                                if had_prop_updates && !needs_layout {
                                    tracing::trace!("Visual-only prop updates, skipping layout");
                                }

                                // Request window redraw without rebuild
                                window.request_redraw();
                            }

                            // =========================================================
                            // PHASE 2: Build/rebuild tree only for structural changes
                            // This must happen BEFORE tick() so motion animations are available
                            // =========================================================

                            // Begin stable motion frame tracking
                            // This clears the "used" set so we can detect which motions are no longer in the tree
                            rs.begin_stable_motion_frame();

                            if ws.needs_rebuild || ws.render_tree.is_none() {
                                // Reset call counters for stable key generation
                                reset_call_counters();
                                // Clear stale Stateful base_render_props updaters
                                blinc_layout::clear_stateful_base_updaters();
                                blinc_layout::click_outside::clear_click_outside_handlers();

                                // Reset stable motions so they replay on full rebuild
                                // This ensures motion animations play when UI is reconstructed
                                rs.reset_stable_motions_for_rebuild();

                                // Note: Viewport and overlay state are already updated in PHASE 1
                                // so build_overlay_layer() has correct dimensions

                                // Build UI element tree
                                let user_ui = ui_builder(windowed_ctx);

                                // Compose user UI with overlay layer using a regular Div container
                                // We use position:relative with the overlay absolutely positioned on top.
                                let overlay_layer = windowed_ctx.overlay_manager.build_overlay_layer();
                                let ui = div()
                                    .w(windowed_ctx.width)
                                    .h(windowed_ctx.height)
                                    .relative() // positioning context for overlay
                                    .child(user_ui)
                                    .child(overlay_layer);

                                // Use incremental update if we have an existing tree
                                // BUT: Skip incremental update during resize - do full rebuild instead
                                // This ensures parent constraints properly propagate to all children
                                if let Some(ref mut existing_tree) = ws.render_tree {
                                    if ws.needs_relayout {
                                        // Window resize: bypass incremental update, do full rebuild
                                        // This ensures proper constraint propagation from parents to children
                                        tracing::debug!("Window resize: full tree rebuild (bypassing incremental update)");

                                        // Clear layout bounds storages before rebuild
                                        existing_tree.clear_layout_bounds_storages();

                                        // Full rebuild: create new tree from element with shared registry
                                        // Pass registry to from_element_with_registry so IDs are registered during build
                                        let mut tree = RenderTree::from_element_with_registry(
                                            &ui,
                                            Arc::clone(&element_registry),
                                        );

                                        // Set animation scheduler for scroll bounce springs
                                        tree.set_animations(&windowed_ctx.animations);

                                        // Share the CSS animation store (ticked by scheduler thread)
                                        tree.set_css_anim_store(Arc::clone(&css_anim_store));

                                        // Set DPI scale factor for HiDPI rendering
                                        tree.set_scale_factor(windowed_ctx.scale_factor as f32);

                                        // Set CSS stylesheet for automatic style application
                                        if let Some(ref stylesheet) = windowed_ctx.stylesheet {
                                            tree.set_stylesheet_arc(stylesheet.clone());
                                        }
                                        // Apply CSS visual + layout styles in a single optimized pass
                                        // (builds class index once, iterates rules once)
                                        tree.apply_all_stylesheet_styles();

                                        // Register pointer-space elements from stylesheet
                                        if let Some(ref stylesheet) = windowed_ctx.stylesheet {
                                            windowed_ctx.pointer_query.register_from_stylesheet(stylesheet);
                                        }

                                        // Compute layout with new viewport dimensions
                                        tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                        tree.update_flip_bounds();

                                        // Initialize motion animations for any nodes wrapped in motion() containers
                                        tree.initialize_motion_animations(rs);
                                        // End motion frame to detect unmounted motions and trigger exit animations
                                        rs.end_stable_motion_frame();
                                        // Process any motion replay requests queued during tree building
                                        rs.process_global_motion_replays();
                                        // Start CSS animations for elements with animation properties
                                        tree.start_all_css_animations();

                                        // Replace existing tree with fresh one
                                        *existing_tree = tree;

                                        // Clear relayout flag after full rebuild
                                        ws.needs_relayout = false;
                                    } else {
                                        // Normal incremental update (no resize)
                                        use blinc_layout::UpdateResult;

                                        // Update stylesheet in case it changed between frames
                                        if let Some(ref stylesheet) = windowed_ctx.stylesheet {
                                            existing_tree.set_stylesheet_arc(stylesheet.clone());
                                        }

                                        let update_result = existing_tree.incremental_update(&ui);

                                        match update_result {
                                            UpdateResult::NoChanges => {
                                                tracing::debug!("Incremental update: NoChanges - skipping rebuild");
                                            }
                                            UpdateResult::VisualOnly => {
                                                tracing::debug!("Incremental update: VisualOnly - skipping layout");
                                                // Props already updated in-place by incremental_update
                                            }
                                            UpdateResult::LayoutChanged => {
                                                // Layout changed - recompute layout
                                                tracing::debug!("Incremental update: LayoutChanged - recomputing layout");
                                                existing_tree.apply_stylesheet_layout_overrides();
                                                existing_tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                                existing_tree.update_flip_bounds();
                                            }
                                            UpdateResult::ChildrenChanged => {
                                                // Children changed - subtrees were rebuilt in place
                                                tracing::debug!("Incremental update: ChildrenChanged - subtrees rebuilt");

                                                // Apply CSS styles to new nodes from rebuilt subtrees
                                                // (collect_render_props only applies ID-based CSS;
                                                // class selectors need apply_stylesheet_base_styles)
                                                existing_tree.apply_stylesheet_base_styles();
                                                // Recompute layout since structure changed
                                                existing_tree.apply_stylesheet_layout_overrides();
                                                existing_tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                                // FLIP: detect position changes and start CSS transitions
                                                existing_tree.apply_flip_transitions();
                                                existing_tree.update_flip_bounds();

                                                // Re-register pointer-space elements (new elements may have pointer-space)
                                                if let Some(ref stylesheet) = windowed_ctx.stylesheet {
                                                    windowed_ctx.pointer_query.register_from_stylesheet(stylesheet);
                                                }

                                                // Initialize motion animations for any new nodes wrapped in motion() containers
                                                existing_tree.initialize_motion_animations(rs);
                                                // End motion frame to detect unmounted motions and trigger exit animations
                                                rs.end_stable_motion_frame();

                                                // Process any global motion replays that were queued during tree building
                                                rs.process_global_motion_replays();
                                                // Start CSS animations for elements with animation properties
                                                existing_tree.start_all_css_animations();
                                            }
                                        }
                                    }
                                } else {
                                    // No existing tree - create new with shared registry
                                    let mut tree = RenderTree::from_element_with_registry(
                                        &ui,
                                        Arc::clone(&element_registry),
                                    );

                                    // Set animation scheduler for scroll bounce springs
                                    tree.set_animations(&windowed_ctx.animations);

                                    // Share the CSS animation store (ticked by scheduler thread)
                                    tree.set_css_anim_store(Arc::clone(&css_anim_store));

                                    // Set DPI scale factor for HiDPI rendering
                                    tree.set_scale_factor(windowed_ctx.scale_factor as f32);

                                    // Set CSS stylesheet for automatic style application
                                    if let Some(ref stylesheet) = windowed_ctx.stylesheet {
                                        tree.set_stylesheet_arc(stylesheet.clone());
                                    }
                                    // Apply CSS visual + layout styles in a single optimized pass
                                    tree.apply_all_stylesheet_styles();

                                    // Register pointer-space elements from stylesheet
                                    if let Some(ref stylesheet) = windowed_ctx.stylesheet {
                                        windowed_ctx.pointer_query.register_from_stylesheet(stylesheet);
                                    }

                                    // Compute layout in logical pixels
                                    tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                    tree.update_flip_bounds();

                                    // Initialize motion animations for any nodes wrapped in motion() containers
                                    tree.initialize_motion_animations(rs);
                                    // End motion frame to detect unmounted motions and trigger exit animations
                                    rs.end_stable_motion_frame();

                                    // Process any global motion replays that were queued during tree building
                                    rs.process_global_motion_replays();
                                    // Start CSS animations for elements with animation properties
                                    tree.start_all_css_animations();

                                    ws.render_tree = Some(tree);
                                }

                                ws.needs_rebuild = false;
                                let was_first_rebuild = windowed_ctx.rebuild_count == 0;
                                windowed_ctx.rebuild_count = windowed_ctx.rebuild_count.saturating_add(1);

                                // Execute on_ready callbacks after first rebuild
                                if was_first_rebuild {
                                    if let Ok(mut callbacks) = ready_callbacks.lock() {
                                        for callback in callbacks.drain(..) {
                                            callback();
                                        }
                                    }
                                }
                            } else {
                                // No rebuild needed - still need to end the motion frame
                                // If an existing tree exists, initialize motions to mark them as used
                                if let Some(ref tree) = ws.render_tree {
                                    tree.initialize_motion_animations(rs);
                                }
                                rs.end_stable_motion_frame();
                            }

                            // Note: on_ready callbacks are only executed after the FIRST rebuild
                            // (in the was_first_rebuild block above). Callbacks registered
                            // after the first rebuild are executed immediately since the UI
                            // is already ready at that point.

                            // =========================================================
                            // PHASE 3: Tick animations and dynamic render state
                            // This must happen AFTER tree rebuild so motions are initialized
                            // =========================================================

                            // Process any pending motion exit cancellations
                            // This must happen before tick() so cancelled motions don't continue exiting
                            rs.process_global_motion_exit_cancels();

                            // Process any pending motion exit starts (explicit exit triggers)
                            rs.process_global_motion_exit_starts();

                            // Process suspended motion starts queued via query_motion(key).start()
                            rs.process_global_motion_starts();

                            // Tick render state (handles cursor blink, color animations, etc.)
                            // This updates dynamic properties without touching tree structure
                            let _animations_active = rs.tick(current_time);

                            // Tick CSS animations/transitions synchronously on the main thread.
                            // The scheduler's bg thread drives 120fps redraws via wake_callback,
                            // but actual ticking is done here to stay in phase with rendering.
                            let dt_ms = if ws.last_frame_time_ms > 0 {
                                (current_time - ws.last_frame_time_ms) as f32
                            } else {
                                16.0
                            };
                            let css_active = if let Some(ref mut tree) = ws.render_tree {
                                let store = tree.css_anim_store();
                                let mut s = store.lock().unwrap();
                                let (anim, trans) = s.tick(dt_ms);
                                drop(s);
                                let flip = tree.tick_flip_animations(dt_ms);
                                anim || trans || flip || tree.css_has_active()
                            } else {
                                false
                            };
                            ws.last_frame_time_ms = current_time;

                            // Sync motion states to shared store for query_motion API
                            rs.sync_shared_motion_states();

                            // Tick theme animation (handles color interpolation during theme transitions)
                            let theme_animating = blinc_theme::ThemeState::get().tick();

                            // Note: scroll physics tick moved to before PHASE 1 (before any rebuilds)
                            // so that ScrollRef has up-to-date values when stateful components rebuild

                            // =========================================================
                            // PHASE 4: Render
                            // Combines stable tree structure with dynamic render state
                            // =========================================================

                            // Sync text input/textarea focus to EventRouter so CSS :focus matching works
                            {
                                let text_focus = blinc_layout::widgets::text_input::focused_text_input_node_id()
                                    .or_else(blinc_layout::widgets::text_input::focused_text_area_node_id);
                                let current_focus = windowed_ctx.event_router.focused();
                                if text_focus != current_focus {
                                    windowed_ctx.event_router.set_focus(text_focus);
                                }
                            }

                            // Apply CSS state styles (:hover, :active, :focus) from stylesheet
                            // This also detects property changes and starts new transitions
                            if let Some(ref mut tree) = ws.render_tree {
                                if tree.stylesheet().is_some() {
                                    let state_changed = tree.apply_stylesheet_state_styles(&windowed_ctx.event_router);
                                    // Recompute layout if state styles affected layout properties
                                    // (e.g. visibility: hidden → display: none, or height changes on hover)
                                    if state_changed {
                                        tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                        tree.update_flip_bounds();
                                    }
                                }
                            }

                            // Apply CSS animation/transition values AFTER state styles
                            // (state styles reset to base, animations must override)
                            if css_active || !ws.render_tree.as_ref().map_or(true, |t| t.css_transitions_empty()) {
                                if let Some(ref mut tree) = ws.render_tree {
                                    tree.apply_all_css_animation_props();
                                    tree.apply_all_css_transition_props();
                                    tree.apply_flip_animation_props();
                                    if tree.apply_animated_layout_props() {
                                        tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
                                        tree.update_flip_bounds();
                                    }
                                }
                            }

                            // Update continuous pointer query state
                            if !windowed_ctx.pointer_query.is_empty() {
                                let (mx, my) = windowed_ctx.event_router.mouse_position();
                                let is_pressed = windowed_ctx.event_router.pressed_target().is_some();
                                let dt_sec = dt_ms / 1000.0;
                                let time_sec = current_time as f64 / 1000.0;
                                // Use event router's hit test results for hover detection.
                                // The router already handles scroll offsets, transforms, and occlusion
                                // correctly, so bounds from get_node_bounds match the rendering pipeline.
                                windowed_ctx.pointer_query.update(
                                    mx, my, is_pressed, dt_sec, time_sec,
                                    |id| {
                                        let node = element_registry.get(id)?;
                                        if windowed_ctx.event_router.is_hovered(node) {
                                            windowed_ctx.event_router.get_node_bounds(node)
                                        } else {
                                            None
                                        }
                                    },
                                );
                                // Evaluate dynamic calc(env(...)) properties with current pointer state
                                if let Some(ref mut tree) = ws.render_tree {
                                    tree.apply_pointer_styles(
                                        &windowed_ctx.pointer_query,
                                        &windowed_ctx.event_router,
                                    );
                                }
                            }

                            if let Some(ref tree) = ws.render_tree {
                                // Set blend target for mix-blend-mode support
                                blinc_app.set_blend_target(&frame.texture);

                                // Pass cursor position for @flow pointer input
                                let (mx, my) = windowed_ctx.event_router.mouse_position();
                                let sf = windowed_ctx.scale_factor as f32;
                                blinc_app.set_cursor_position(mx * sf, my * sf);

                                // Render with motion animations
                                // Use physical pixel dimensions for the render surface
                                let result = blinc_app.render_tree_with_motion(
                                    tree,
                                    rs,
                                    &view,
                                    windowed_ctx.physical_width as u32,
                                    windowed_ctx.physical_height as u32,
                                );
                                if let Err(e) = result {
                                    tracing::error!("Render error: {}", e);
                                }

                                blinc_app.clear_blend_target();
                            }

                            // =========================================================
                            // PHASE 4b: Overlay state management (overlays now in main tree)
                            // Overlays are composed into the main tree via build_overlay_layer()
                            // so they share the same event routing and incremental update path.
                            // =========================================================

                            // Clear dirty flags for overlays (they've been processed in tree build)
                            let _content_dirty = windowed_ctx.overlay_manager.take_dirty();
                            let _animation_dirty = windowed_ctx.overlay_manager.take_animation_dirty();

                            // Track overlay visibility for triggering rebuilds
                            let has_visible_overlays = windowed_ctx.overlay_manager.has_visible_overlays();
                            windowed_ctx.had_visible_overlays = has_visible_overlays;

                            frame.present();

                            // =========================================================
                            // PHASE 5: Request next frame if animations are active
                            // This ensures smooth animation without waiting for events
                            // =========================================================

                            // Check if background animation thread signaled that redraw is needed
                            // The background thread runs at 120fps and sets this flag when
                            // there are active animations (springs, keyframes, timelines)
                            let scheduler = windowed_ctx.animations.lock().unwrap();
                            let needs_animation_redraw = scheduler.take_needs_redraw();
                            drop(scheduler); // Release lock before request_redraw

                            // Check if stateful elements have active spring animations
                            // If so, re-run their callbacks to get updated animation values
                            if needs_animation_redraw && blinc_layout::has_animating_statefuls() {
                                blinc_layout::check_stateful_animations();
                            }

                            // Check if text widgets need continuous redraws (cursor blink)
                            let needs_cursor_redraw = blinc_layout::widgets::take_needs_continuous_redraw();

                            // Check if motion animations are active (enter/exit animations)
                            let needs_motion_redraw = if let Some(ref rs) = ws.render_state {
                                rs.has_active_motions()
                            } else {
                                false
                            };

                            // Check if overlays changed (modal opened/closed, toast appeared, etc.)
                            let needs_overlay_redraw = {
                                let mgr = windowed_ctx.overlay_manager.lock().unwrap();
                                mgr.take_dirty() || mgr.has_visible_overlays()
                            };

                            // Check if CSS animations/transitions/FLIP need continued redraws
                            // (includes transitions created during apply_complex_selector_styles)
                            let css_needs_redraw = css_active
                                || !ws.render_tree
                                    .as_ref()
                                    .map_or(true, |t| t.css_transitions_empty())
                                || ws.render_tree
                                    .as_ref()
                                    .is_some_and(|t| t.has_active_flip_animations());

                            // Check if pointer query elements need continuous redraws
                            let pointer_query_active = !windowed_ctx.pointer_query.is_empty();

                            // @flow shaders using time/animation builtins need continuous redraws
                            let flow_needs_redraw = blinc_app.has_active_flows();

                            if needs_animation_redraw || needs_cursor_redraw || needs_motion_redraw || scroll_animating || needs_overlay_redraw || theme_animating || css_needs_redraw || pointer_query_active || flow_needs_redraw {
                                // Request another frame to render updated animation values
                                // For cursor blink, also re-request continuous redraw for next frame
                                if needs_cursor_redraw {
                                    // Keep requesting redraws as long as a text input is focused
                                    if blinc_layout::widgets::has_focused_text_input() {
                                        blinc_layout::widgets::text_input::request_continuous_redraw_pub();
                                    }
                                }
                                window.request_redraw();
                            }
                        }
                    }

                    _ => {}
                }

                ControlFlow::Continue
            })
            .map_err(|e| BlincError::Platform(e.to_string()))?;

        Ok(())
    }

    /// Placeholder for non-windowed builds
    #[cfg(not(feature = "windowed"))]
    pub fn run<F, E>(_config: WindowConfig, _ui_builder: F) -> Result<()>
    where
        F: FnMut(&mut WindowedContext) -> E + 'static,
        E: ElementBuilder + 'static,
    {
        Err(BlincError::Platform(
            "Windowed feature not enabled. Add 'windowed' feature to blinc_app".to_string(),
        ))
    }
}

/// Convert platform mouse button to layout mouse button
#[cfg(all(feature = "windowed", not(target_os = "android")))]
fn convert_mouse_button(button: blinc_platform::MouseButton) -> MouseButton {
    match button {
        blinc_platform::MouseButton::Left => MouseButton::Left,
        blinc_platform::MouseButton::Right => MouseButton::Right,
        blinc_platform::MouseButton::Middle => MouseButton::Middle,
        blinc_platform::MouseButton::Back => MouseButton::Back,
        blinc_platform::MouseButton::Forward => MouseButton::Forward,
        blinc_platform::MouseButton::Other(n) => MouseButton::Other(n),
    }
}

/// Convert layout cursor style to platform cursor
#[cfg(all(feature = "windowed", not(target_os = "android")))]
fn convert_cursor_style(cursor: CursorStyle) -> blinc_platform::Cursor {
    match cursor {
        CursorStyle::Default => blinc_platform::Cursor::Default,
        CursorStyle::Pointer => blinc_platform::Cursor::Pointer,
        CursorStyle::Text => blinc_platform::Cursor::Text,
        CursorStyle::Crosshair => blinc_platform::Cursor::Crosshair,
        CursorStyle::Move => blinc_platform::Cursor::Move,
        CursorStyle::NotAllowed => blinc_platform::Cursor::NotAllowed,
        CursorStyle::ResizeNS => blinc_platform::Cursor::ResizeNS,
        CursorStyle::ResizeEW => blinc_platform::Cursor::ResizeEW,
        CursorStyle::ResizeNESW => blinc_platform::Cursor::ResizeNESW,
        CursorStyle::ResizeNWSE => blinc_platform::Cursor::ResizeNWSE,
        CursorStyle::Grab => blinc_platform::Cursor::Grab,
        CursorStyle::Grabbing => blinc_platform::Cursor::Grabbing,
        CursorStyle::Wait => blinc_platform::Cursor::Wait,
        CursorStyle::Progress => blinc_platform::Cursor::Progress,
        CursorStyle::None => blinc_platform::Cursor::None,
    }
}

/// Convenience function to run a windowed ws.app with default configuration
#[cfg(all(feature = "windowed", not(target_os = "android")))]
pub fn run_windowed<F, E>(ui_builder: F) -> Result<()>
where
    F: FnMut(&mut WindowedContext) -> E + 'static,
    E: ElementBuilder + 'static,
{
    WindowedApp::run(WindowConfig::default(), ui_builder)
}

/// Convenience function to run a windowed ws.app with a title
#[cfg(all(feature = "windowed", not(target_os = "android")))]
pub fn run_windowed_with_title<F, E>(title: &str, ui_builder: F) -> Result<()>
where
    F: FnMut(&mut WindowedContext) -> E + 'static,
    E: ElementBuilder + 'static,
{
    let config = WindowConfig {
        title: title.to_string(),
        ..Default::default()
    };
    WindowedApp::run(config, ui_builder)
}
