diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs
--- a/crates/ui/src/docking/native_tests.rs
+++ b/crates/ui/src/docking/native_tests.rs
@@ -1999,6 +1999,125 @@ fn blocking_input_hands_focus_from_a_native_window_to_the_main_window() {
     assert_eq!(focus_main(&d), 0);
 }
 
+#[test]
+fn the_main_window_loses_the_pointer_while_a_native_window_has_it() {
+    // macOS tells a window the pointer left only when it leaves the window's frame: over a
+    // native window in front of it, the main window kept its last pointer, and an app's canvas
+    // cursor stayed drawn at the native window's edge.
+    let main_pointer = |d: &DockDriver<u32, Content>| d.ctx.input_for(egui::ViewportId::ROOT, |i| i.pointer.hover_pos());
+    let mut d = driver(layout(true));
+    let id = window(&d, 5).id;
+    let main = pos2(50.0, 50.0);
+    d.move_to(main);
+    assert_eq!(main_pointer(&d), Some(main));
+    d.window_pointer(id, ORIGIN + vec2(400.0, 200.0));
+    d.step(vec![]);
+    assert_eq!(main_pointer(&d), None, "the pointer is the native window's");
+    // Back over the main window, it has the pointer again.
+    d.move_to(main);
+    assert_eq!(main_pointer(&d), Some(main));
+    // The native window's input can tell of the move into it and of the pointer leaving it at
+    // once: the pointer was there, so the main window is still told it left.
+    let local = d.window_local(id, ORIGIN + vec2(400.0, 200.0)).unwrap();
+    d.window_step(id, vec![Event::PointerMoved(local), Event::PointerGone]);
+    d.step(vec![]);
+    assert_eq!(main_pointer(&d), None, "the pointer moved over the native window");
+}
+
+/// AppKit's resize area reaches a few points past a palette's frame, over the main window:
+/// there AppKit shows its resize cursor and takes a press, so the main window is told the
+/// pointer left, as over the palette itself, except while a drag in the main window holds it.
+#[cfg(target_os = "macos")]
+#[test]
+fn the_main_window_loses_the_pointer_over_a_palettes_outer_resize_area() {
+    let main_pointer = |d: &DockDriver<u32, Content>| d.ctx.input_for(egui::ViewportId::ROOT, |i| i.pointer.hover_pos());
+    let mut d = driver(layout(true));
+    let viewport = d.viewport(window(&d, 5).id);
+    d.step(vec![]);
+    let rect = d.host().windows[&viewport].rect;
+    // 2 pt left of the palette's left edge, in the main window's coordinates.
+    let band = pos2(rect.left() - 2.0, rect.center().y) - ORIGIN.to_vec2();
+    let main = pos2(50.0, 50.0);
+    d.move_to(main);
+    assert_eq!(main_pointer(&d), Some(main));
+    d.move_to(band);
+    d.step(vec![]);
+    assert_eq!(main_pointer(&d), None, "AppKit's resize area is the palette's");
+    d.move_to(main);
+    assert_eq!(main_pointer(&d), Some(main));
+    // Further out, the pointer stays the main window's.
+    let beyond = band - vec2(6.0, 0.0);
+    d.move_to(beyond);
+    assert_eq!(main_pointer(&d), Some(beyond));
+    // A drag in the main window keeps its moves over the resize area, also once another button
+    // was pressed and released during it.
+    d.press(main);
+    let secondary = |pressed| Event::PointerButton { pos: main, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() };
+    d.step(vec![secondary(true)]);
+    d.step(vec![secondary(false)]);
+    d.move_to(band);
+    d.step(vec![]);
+    assert_eq!(main_pointer(&d), Some(band));
+    d.release(band);
+}
+
+#[test]
+fn a_palette_hands_the_keyboard_back_when_the_pointer_comes_back_to_the_main_window() {
+    // After a click in a palette, its OS window has the keyboard: the main window's tool cursor
+    // and shortcuts waited for a click there. Once the pointer comes back from the palette to
+    // the main window, the main window takes the keyboard, after the palette's text field is
+    // done with it if one has it.
+    let focus_main = |d: &DockDriver<u32, Content>| {
+        d.host().commands.iter().filter(|(v, c)| *v == egui::ViewportId::ROOT && matches!(c, ViewportCommand::Focus)).count()
+    };
+    let palette = || {
+        let mut d = driver(layout(true));
+        let id = window(&d, 5).id;
+        let viewport = d.viewport(id);
+        d.host().focus = Some(viewport);
+        d.window_pointer(id, ORIGIN + vec2(400.0, 200.0));
+        d.step(vec![]);
+        (d, id)
+    };
+    let (mut d, _) = palette();
+    assert_eq!(focus_main(&d), 0, "the pointer is over the palette");
+    d.move_to(pos2(50.0, 50.0));
+    d.step(vec![]);
+    assert_eq!(focus_main(&d), 1, "the pointer came back to the main window, once");
+    // A text field of the palette keeps the keyboard until it is done.
+    let (mut d, _) = palette();
+    d.content.text = Some(5);
+    d.content.focus_text = true;
+    d.step(vec![]);
+    d.move_to(pos2(50.0, 50.0));
+    d.step(vec![]);
+    assert_eq!(focus_main(&d), 0, "a field of the palette has the keyboard");
+    d.content.blur_text = true;
+    d.step(vec![]);
+    d.step(vec![]);
+    assert_eq!(focus_main(&d), 1, "the field is done");
+    // A viewer is an ordinary window: it keeps the keyboard.
+    let (mut d, _) = palette();
+    d.content.normal = vec![5];
+    d.step(vec![]);
+    d.move_to(pos2(50.0, 50.0));
+    d.step(vec![]);
+    assert_eq!(focus_main(&d), 0, "a viewer has the keyboard");
+    // A press in AppKit's resize area, which reaches past the palette's frame, focuses the
+    // palette with the pointer still over the main window: the palette keeps the keyboard, or it
+    // would not redraw while it resizes.
+    let mut d = driver(layout(true));
+    let viewport = d.viewport(window(&d, 5).id);
+    d.move_to(pos2(296.0, 200.0));
+    d.host().focus = Some(viewport);
+    for _ in 0..3 {
+        d.step(vec![]);
+    }
+    d.move_to(pos2(290.0, 200.0));
+    d.step(vec![]);
+    assert_eq!(focus_main(&d), 0, "the pointer never left the main window");
+}
+
 #[test]
 fn blocking_input_cancels_a_drag_in_progress() {
     // A tab drag a native window owns.
diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs
--- a/crates/ui/src/docking/render/native.rs
+++ b/crates/ui/src/docking/render/native.rs
@@ -60,6 +60,10 @@ pub(in crate::docking) const APPKIT_CORNER: f32 = 15.0;
 /// How far AppKit's edge resize area reaches into the content between the corners: a few
 /// points; most of it lies outside the window.
 const APPKIT_EDGE: f32 = 4.0;
+/// How far AppKit's edge resize area reaches past the window's frame, over what lies behind
+/// it: 3 pt resizes on macOS 26, 4 pt doesn't. Past a rounded corner it reaches about 1 pt,
+/// which [`in_appkit_outer_band`] leaves out.
+const APPKIT_OUTSIDE: f32 = 3.5;
 
 /// Where the × of a native window's bar sits and where it takes clicks, in points from the
 /// window's top and trailing edges, mirrored in a right-to-left layout.
@@ -662,6 +666,26 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
             if own && !blocked && ui.ctx().text_edit_focused() {
                 let _ = body_input(ui.ctx()).lock().typing_next.insert(viewport);
             }
+            // A palette keeps the keyboard only while one of its text fields has it, as macOS
+            // palettes do: once the pointer comes back from it to the main window, the main
+            // window takes the keyboard, so its shortcuts and tool cursor work without a click
+            // there. Only on that return: a press in AppKit's resize area, which reaches past the
+            // palette's frame over the main window, focuses the palette with the pointer still the
+            // main window's, and handing the keyboard back then kept the palette from redrawing
+            // while it resized. While a text field has the keyboard, a button is held or a menu of
+            // the palette is open, the hand-back waits. A viewer is an ordinary window: it keeps
+            // the keyboard.
+            if own
+                && !normal
+                && !blocked
+                && ui.input(|i| i.viewport().focused == Some(true) && !i.pointer.any_down())
+                && !ui.ctx().text_edit_focused()
+                && !egui::Popup::is_any_open(ui.ctx())
+                && !ui.ctx().input_for(ViewportId::ROOT, |i| i.pointer.any_down())
+                && std::mem::take(&mut body_input(ui.ctx()).lock().pointer_returned)
+            {
+                ui.ctx().send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus);
+            }
             self.cross.held |= ui.ctx().dragged_id().is_some_and(|id| self.owned_interactions.contains(&id));
             if debug_close() {
                 debug_outlines(ui, full, &buttons);
@@ -672,6 +696,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
             if window.panels().all(|p| self.permissions(p).movable) {
                 if OS_RESIZES {
                     leave_cursor_to_os(ui, full);
+                    // Where AppKit resizes the window from outside it, over the main window.
+                    if !blocked && let Some(rect) = ui.input(|i| i.viewport().inner_rect) {
+                        let body = body_input(ui.ctx());
+                        let mut body = body.lock();
+                        let pass = body.pass;
+                        body.resize_bands.insert(viewport, (pass, rect));
+                    }
                 } else {
                     resize_edges(ui, self.id.with(("native-edges", window.id)), full, &buttons);
                 }
@@ -1067,6 +1098,19 @@ pub(super) struct NativeBodyInput {
     typing_at: u64,
     /// Key and clipboard events for each window's next input, and the pass they were queued in.
     keys: std::collections::HashMap<ViewportId, (u64, Vec<Event>)>,
+    /// The native window the pointer moved over last, by its own input, since it last moved or
+    /// pressed in the main window.
+    pointer_window: Option<ViewportId>,
+    /// The main window was told the pointer left it for `pointer_window`.
+    root_pointer_gone: bool,
+    /// The pointer came back to the main window from a native window while the main window had
+    /// no keyboard: a palette with the keyboard hands it back.
+    pointer_returned: bool,
+    /// The desktop content rect of each native window AppKit resizes, and the main window's pass
+    /// that drew it: its resize area reaches past it, over the main window.
+    resize_bands: std::collections::HashMap<ViewportId, (u64, Rect)>,
+    /// The buttons held in the main window, a bit each.
+    root_held: u8,
 }
 
 /// The app drag routing and routed events shared by every native input hook of `ctx`.
@@ -1107,6 +1151,64 @@ impl egui::Plugin for NativeInput {
         {
             let mut body = body.lock();
             let body = &mut *body;
+            // macOS tells a window the pointer left it only when the pointer leaves its frame:
+            // over a native window in front of the main window, the main window got no exit and
+            // kept its last pointer (an app's canvas cursor stayed drawn at the window's edge).
+            // The pointer is the native window's once it moves there, and the main window is told
+            // it left, until it moves or presses in the main window again.
+            if viewport == ViewportId::ROOT {
+                if input.focused {
+                    body.pointer_returned = false;
+                }
+                // AppKit's resize area of a palette reaches a few points past its frame: there
+                // the main window gets the moves, but AppKit shows its resize cursor and takes a
+                // press, so the pointer is the palette's. Not while a button is held in the main
+                // window: a drag there keeps its moves.
+                let pass = body.pass;
+                body.resize_bands.retain(|_, (at, _)| *at + 1 >= pass);
+                for event in &input.events {
+                    if let Event::PointerButton { button, pressed, .. } = event {
+                        let bit = 1 << (*button as u8);
+                        if *pressed {
+                            body.root_held |= bit;
+                        } else {
+                            body.root_held &= !bit;
+                        }
+                    }
+                }
+                let origin = input.viewports.get(&ViewportId::ROOT).and_then(|info| info.inner_rect).map(|r| r.min.to_vec2());
+                let last_move = input.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None });
+                let pressing = input.events.iter().any(|e| matches!(e, Event::PointerButton { .. }));
+                if let (Some(origin), Some(pos), false, false) = (origin, last_move, pressing, body.root_held != 0)
+                    && let Some(window) = body.resize_bands.iter().find(|(_, (_, rect))| in_appkit_outer_band(*rect, pos + origin)).map(|(v, _)| *v)
+                {
+                    input.events.retain(|e| !matches!(e, Event::PointerMoved(_)));
+                    if body.pointer_window != Some(window) {
+                        body.pointer_window = Some(window);
+                        body.root_pointer_gone = false;
+                        body.pointer_returned = false;
+                    }
+                }
+                if input.events.iter().any(|e| matches!(e, Event::PointerMoved(_) | Event::PointerButton { .. })) {
+                    body.pointer_returned |= body.pointer_window.take().is_some() && !input.focused;
+                    body.root_pointer_gone = false;
+                } else if body.pointer_window.is_some() && !body.root_pointer_gone {
+                    input.events.push(Event::PointerGone);
+                    body.root_pointer_gone = true;
+                }
+            } else {
+                for event in &input.events {
+                    match event {
+                        Event::PointerMoved(_) if body.pointer_window != Some(viewport) => {
+                            body.pointer_window = Some(viewport);
+                            body.root_pointer_gone = false;
+                            body.pointer_returned = false;
+                            ctx.request_repaint_of(ViewportId::ROOT);
+                        }
+                        _ => {}
+                    }
+                }
+            }
             if viewport == ViewportId::ROOT {
                 body.pass += 1;
                 // Every window draws in the main window's pass: events for one that has not
@@ -1389,6 +1491,12 @@ fn leave_cursor_to_os(ui: &Ui, full: Rect) {
     }
 }
 
+/// Whether desktop point `p` is in AppKit's resize area outside the window whose content is
+/// `rect`: within [`APPKIT_OUTSIDE`] of an edge, beside it rather than past a corner.
+fn in_appkit_outer_band(rect: Rect, p: Pos2) -> bool {
+    !rect.contains(p) && rect.expand(APPKIT_OUTSIDE).contains(p) && (rect.x_range().contains(p.x) || rect.y_range().contains(p.y))
+}
+
 /// AppKit's resize areas inside a window's content `rect`: a square at each corner and a strip
 /// along each edge.
 pub(in crate::docking) fn appkit_resize_areas(rect: Rect) -> [Rect; 8] {
diff --git a/docs/ui-docking.md b/docs/ui-docking.md
--- a/docs/ui-docking.md
+++ b/docs/ui-docking.md
@@ -575,6 +575,21 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
   `Paste` without the key). The app feeds them to the shortcut handling it runs on the main
   window. A window where any widget has keyboard focus forwards nothing, and an Escape that
   cancels a drag is not forwarded.
+- **Keyboard back to the main window.** A click in a palette gives its OS window the keyboard.
+  Once the pointer comes back from the palette to the main window, the palette hands the
+  keyboard to it (`ViewportCommand::Focus`), as macOS palettes do, so the app's shortcuts and
+  tool cursor work there without a click. While one of its text fields has the keyboard, a
+  button is held or one of its popups is open, the hand-back waits. A press in AppKit's resize
+  area, which reaches a few points past the palette's frame over the main window, focuses the
+  palette without the pointer coming from it, and the palette keeps the keyboard. A normal
+  window (a viewer) keeps the keyboard as any window does.
+- **Pointer.** macOS tells a window that the pointer left only when it leaves the window's
+  frame, so over a native window in front of the main window, the main window kept its last
+  pointer position and the hover there. Once the pointer moves over a native window, the main
+  window gets `Event::PointerGone` at the start of its next pass, until the pointer moves or
+  presses in the main window again. On macOS the same holds within 3.5 pt outside an edge of a
+  palette AppKit resizes: its resize area reaches past the frame there, and the main window,
+  which gets those moves, drops them, unless a button is held in it.
 - **Input methods.** egui's IME descriptor (`PlatformOutput::ime`) goes out from the pass of
   the window whose editor has the keyboard. A `craft_ui::ime::Composer` in a panel body of a
   native window records its descriptor while the window draws, and the dock area publishes it
