diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs
--- a/crates/ui/src/docking/surface.rs
+++ b/crates/ui/src/docking/surface.rs
@@ -346,10 +346,12 @@ struct Renderer<'a, P> {
 }
 
 impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
+    /// Retain renderer-owned controls so workspace replacement can stop their drags.
     fn register_interaction(&mut self, ctx: &Context, id: Id) {
-        // A validated tree has at most 2*MAX_PANELS nodes. Tabs, stack dividers,
-        // floating grips/corners and splits fit comfortably inside this fixed bound.
-        const CAP: usize = 8 * super::MAX_PANELS;
+        // At most 12 controls per panel cover tabs, splits, stack dividers, a move
+        // grip and eight floating resize handles. Allow both previous and current
+        // frame identities during rendering, including a complete workspace change.
+        const CAP: usize = 32 * super::MAX_PANELS;
         if self.owned_interactions.len() < CAP && !self.owned_interactions.contains(&id) {
             self.owned_interactions.push(id);
         }
@@ -544,12 +546,19 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
         }
     }
 
+    /// Render tabs and body inside the reserved border while moving the outer frame.
     fn group(&mut self, ui: &mut Ui, panels: &[P], active: usize, rect: Rect, floating: bool) {
         let Some(selected) = panels.get(active) else { return };
         let policy = (self.permissions)(selected);
+        // Reserve the complete corner depth, not just the thinner edge bands, so
+        // no resize region overlaps tabs, group controls or body scrollbars.
+        let inner = if floating { float_content_rect(rect) } else { rect };
+        if floating {
+            ui.painter().rect_filled(rect, 0.0, self.style.background);
+        }
         let bare = panels.len() == 1 && policy == Permissions::PROTECTED;
         if bare {
-            self.contents(ui, selected, rect);
+            self.contents(ui, selected, inner);
             self.targets.push(DropTarget {
                 anchor: selected.clone(),
                 rect,
@@ -560,7 +569,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
             });
             return;
         }
-        let mut strip = Rect::from_min_max(rect.min, pos2(rect.right(), (rect.top() + self.style.tab_height).min(rect.bottom())));
+        let mut strip = Rect::from_min_max(inner.min, pos2(inner.right(), (inner.top() + self.style.tab_height).min(inner.bottom())));
         ui.painter().rect_filled(strip, 0.0, self.style.tab_background);
         if floating {
             let grip = Rect::from_min_max(strip.min, pos2((strip.left() + 12.0).min(strip.right()), strip.bottom()));
@@ -655,7 +664,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
             allow_splits: !floating,
             accepts_tabs: policy.accepts_tabs,
         });
-        self.contents(ui, selected, Rect::from_min_max(pos2(rect.left(), strip.bottom()), rect.max));
+        self.contents(ui, selected, Rect::from_min_max(pos2(inner.left(), strip.bottom()), inner.max));
         ui.painter().rect_stroke(rect, 0.0, self.style.border, StrokeKind::Inside);
     }
 
@@ -735,6 +744,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
         });
     }
 
+    /// Render floating groups and register their move and resize interactions.
     fn floating(&mut self, ctx: &Context) {
         let screen = ctx.content_rect();
         if !super::targets::usable(screen) {
@@ -763,42 +773,51 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
                 let rect = Rect::from_min_size(ui.min_rect().min, rect.size());
                 ui.allocate_rect(rect, Sense::hover());
                 self.group(ui, &group.panels, group.active, rect, true);
-                let corner = Rect::from_min_max((rect.max - vec2(12.0, 12.0)).max(rect.min), rect.max);
-                let response = ui.interact(corner, id.with("resize"), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeNwSe);
-                self.register_interaction(ui.ctx(), response.id);
-                response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.resize_window_label));
-                if response.drag_started() {
-                    response.request_focus();
-                }
-                let mut delta = if response.dragged() { ui.input(|i| i.pointer.delta()) } else { Vec2::ZERO };
-                if response.has_focus() {
-                    ui.memory_mut(|memory| {
-                        memory.set_focus_lock_filter(
-                            response.id,
-                            egui::EventFilter { horizontal_arrows: true, vertical_arrows: true, ..Default::default() },
-                        )
-                    });
-                    ui.input_mut(|input| {
-                        if input.consume_key(egui::Modifiers::NONE, Key::ArrowLeft) {
-                            delta.x -= 8.0;
-                        }
-                        if input.consume_key(egui::Modifiers::NONE, Key::ArrowRight) {
-                            delta.x += 8.0;
-                        }
-                        if input.consume_key(egui::Modifiers::NONE, Key::ArrowUp) {
-                            delta.y -= 8.0;
-                        }
-                        if input.consume_key(egui::Modifiers::NONE, Key::ArrowDown) {
-                            delta.y += 8.0;
-                        }
-                    });
-                }
-                if response.enabled() && delta != Vec2::ZERO {
-                    let size = (rect.size() + delta).max(limits.min.max(vec2(40.0, self.style.tab_height + 20.0))).min(limits.max).min(screen.size());
-                    self.output.actions.push(Action::MoveFloating { panel: first.clone(), rect: [rect.left(), rect.top(), size.x, size.y] });
-                }
-                if response.hovered() || response.dragged() || response.has_focus() {
-                    ui.painter().line_segment([corner.left_bottom(), corner.right_top()], Stroke::new(2.0, self.style.accent));
+                for (hx, hy, hit, cursor) in float_resize_handles(rect) {
+                    // Keep the original lower-right grip ID for keyboard focus and automation.
+                    let handle_id = if (hx, hy) == (1, 1) { id.with("resize") } else { id.with(("resize", hx, hy)) };
+                    let response = ui.interact(hit, handle_id, Sense::drag()).on_hover_and_drag_cursor(cursor);
+                    self.register_interaction(ui.ctx(), response.id);
+                    response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.resize_window_label));
+                    if response.drag_started() {
+                        response.request_focus();
+                    }
+                    // A press can arrive with a hover move from a different handle in the same
+                    // frame. Only motion after the press belongs to this resize gesture.
+                    let mut delta =
+                        if response.dragged() && !ui.input(|i| i.pointer.primary_pressed()) { ui.input(|i| i.pointer.delta()) } else { Vec2::ZERO };
+                    if response.has_focus() {
+                        ui.memory_mut(|memory| {
+                            memory.set_focus_lock_filter(
+                                response.id,
+                                egui::EventFilter { horizontal_arrows: true, vertical_arrows: true, ..Default::default() },
+                            )
+                        });
+                        ui.input_mut(|input| {
+                            if input.consume_key(egui::Modifiers::NONE, Key::ArrowLeft) {
+                                delta.x -= 8.0;
+                            }
+                            if input.consume_key(egui::Modifiers::NONE, Key::ArrowRight) {
+                                delta.x += 8.0;
+                            }
+                            if input.consume_key(egui::Modifiers::NONE, Key::ArrowUp) {
+                                delta.y -= 8.0;
+                            }
+                            if input.consume_key(egui::Modifiers::NONE, Key::ArrowDown) {
+                                delta.y += 8.0;
+                            }
+                        });
+                    }
+                    if response.enabled() && delta != Vec2::ZERO {
+                        let resized = resize_float(rect, hx, hy, delta, limits, screen, self.style.tab_height);
+                        self.output.actions.push(Action::MoveFloating {
+                            panel: first.clone(),
+                            rect: [resized.left(), resized.top(), resized.width(), resized.height()],
+                        });
+                    }
+                    if (hx, hy) == (1, 1) && (response.hovered() || response.dragged() || response.has_focus()) {
+                        ui.painter().line_segment([hit.left_bottom(), hit.right_top()], Stroke::new(2.0, self.style.accent));
+                    }
                 }
             });
         }
@@ -915,3 +934,134 @@ fn split_bounds(available: f32, axis: SplitAxis, first: PanelLimits, second: Pan
     let constrained_hi = hi.min(a_max);
     if constrained_lo <= constrained_hi { (constrained_lo, constrained_hi) } else { (lo, hi) }
 }
+
+/// Space reserved for floating resize corners; shared with content layout.
+fn float_border(rect: Rect) -> f32 {
+    10.0_f32.min(rect.width() / 2.0).min(rect.height() / 2.0)
+}
+
+/// Keep application chrome and body widgets strictly inside the resize border.
+fn float_content_rect(rect: Rect) -> Rect {
+    rect.shrink(float_border(rect))
+}
+
+/// Non-overlapping hit regions occupy only the reserved border, inside the window.
+fn float_resize_handles(rect: Rect) -> [(i8, i8, Rect, egui::CursorIcon); 8] {
+    use egui::CursorIcon::{ResizeHorizontal, ResizeNeSw, ResizeNwSe, ResizeVertical};
+    let corner = float_border(rect);
+    let edge = 5.0_f32.min(corner);
+    let (l, r, t, b) = (rect.left(), rect.right(), rect.top(), rect.bottom());
+    [
+        (-1, -1, Rect::from_min_max(pos2(l, t), pos2(l + corner, t + corner)), ResizeNwSe),
+        (1, -1, Rect::from_min_max(pos2(r - corner, t), pos2(r, t + corner)), ResizeNeSw),
+        (-1, 1, Rect::from_min_max(pos2(l, b - corner), pos2(l + corner, b)), ResizeNeSw),
+        (1, 1, Rect::from_min_max(pos2(r - corner, b - corner), pos2(r, b)), ResizeNwSe),
+        (-1, 0, Rect::from_min_max(pos2(l, t + corner), pos2(l + edge, b - corner)), ResizeHorizontal),
+        (1, 0, Rect::from_min_max(pos2(r - edge, t + corner), pos2(r, b - corner)), ResizeHorizontal),
+        (0, -1, Rect::from_min_max(pos2(l + corner, t), pos2(r - corner, t + edge)), ResizeVertical),
+        (0, 1, Rect::from_min_max(pos2(l + corner, b - edge), pos2(r - corner, b)), ResizeVertical),
+    ]
+}
+
+/// Move selected edges within panel and screen limits while preserving opposite edges.
+fn resize_float(rect: Rect, hx: i8, hy: i8, delta: Vec2, limits: PanelLimits, screen: Rect, tab_height: f32) -> Rect {
+    let min = limits.min.max(vec2(40.0, tab_height + 20.0)).min(limits.max).min(screen.size());
+    let mut result = rect;
+    // Clamp each moving edge while anchoring the opposite edge, including at the screen bounds.
+    if hx != 0 {
+        let available = if hx < 0 { rect.right() - screen.left() } else { screen.right() - rect.left() };
+        let max = limits.max.x.min(available);
+        let width = (rect.width() + f32::from(hx) * delta.x).clamp(min.x.min(max), max);
+        if hx < 0 {
+            result.min.x = rect.right() - width;
+        } else {
+            result.max.x = rect.left() + width;
+        }
+    }
+    if hy != 0 {
+        let available = if hy < 0 { rect.bottom() - screen.top() } else { screen.bottom() - rect.top() };
+        let max = limits.max.y.min(available);
+        let height = (rect.height() + f32::from(hy) * delta.y).clamp(min.y.min(max), max);
+        if hy < 0 {
+            result.min.y = rect.bottom() - height;
+        } else {
+            result.max.y = rect.top() + height;
+        }
+    }
+    result
+}
+
+#[cfg(test)]
+mod float_resize_tests {
+    use super::*;
+
+    /// Body scrollbars and all tab/header controls share the reserved inner rectangle.
+    #[test]
+    fn resize_regions_do_not_overlap_content_or_chrome() {
+        for size in [vec2(300.0, 220.0), vec2(40.0, 46.0), vec2(800.0, 600.0)] {
+            let frame = Rect::from_min_size(pos2(80.0, 70.0), size);
+            let content = float_content_rect(frame);
+            assert!(content.is_positive());
+            for (_, _, hit, _) in float_resize_handles(frame) {
+                let overlap = content.intersect(hit);
+                assert!(overlap.width() <= 0.0 || overlap.height() <= 0.0);
+            }
+        }
+    }
+
+    /// A late handle beyond the old capacity must remain cancellable in both ID lists.
+    #[test]
+    fn late_floating_handle_is_registered_and_cancelled() {
+        let ctx = Context::default();
+        ctx.run_ui(egui::RawInput::default(), |ui| {
+            let area = Id::new("capacity-test");
+            let handle = area.with("last-floating-handle");
+            let previous: Vec<_> = (0..8 * super::super::MAX_PANELS).map(|n| area.with(n)).collect();
+            ctx.data_mut(|d| d.insert_temp(area.with("owned-interactions"), previous.clone()));
+            let style = DockStyle::from_ui(ui);
+            let layout = Layout::<u32>::default();
+            let mut content = BodyContent(|_: &mut Ui, _: &u32| {});
+            let mut renderer = Renderer {
+                id: area,
+                style: &style,
+                label: &|_: &u32| String::new(),
+                permissions: &|_: &u32| Permissions::default(),
+                limits: &|_: &u32| PanelLimits::default(),
+                content: &mut content,
+                activation: None,
+                blockers: Vec::new(),
+                layout: &layout,
+                targets: Vec::new(),
+                owned_interactions: previous,
+                output: DockOutput::default(),
+            };
+            renderer.register_interaction(&ctx, handle);
+            assert!(renderer.owned_interactions.contains(&handle));
+            ctx.set_dragged_id(handle);
+            cancel_drag::<u32>(&ctx, area);
+            assert_eq!(ctx.dragged_id(), None);
+        })
+        .textures_delta
+        .clear();
+    }
+
+    /// Exercise growth and shrink limits without moving the anchored edges.
+    #[test]
+    fn moving_edges_respect_limits_and_screen_without_shifting_the_anchor() {
+        let rect = Rect::from_min_size(pos2(100.0, 100.0), vec2(300.0, 200.0));
+        let screen = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
+        let limits = PanelLimits { min: vec2(100.0, 80.0), max: vec2(450.0, 350.0) };
+        for (hx, hy) in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)] {
+            let grown = resize_float(rect, hx, hy, vec2(f32::from(hx) * 1000.0, f32::from(hy) * 1000.0), limits, screen, 26.0);
+            assert!(screen.contains_rect(grown));
+            assert!(grown.width() <= limits.max.x && grown.height() <= limits.max.y);
+            let shrunk = resize_float(rect, hx, hy, vec2(f32::from(hx) * -1000.0, f32::from(hy) * -1000.0), limits, screen, 26.0);
+            assert_eq!(shrunk.width(), if hx == 0 { rect.width() } else { limits.min.x });
+            assert_eq!(shrunk.height(), if hy == 0 { rect.height() } else { limits.min.y });
+            for result in [grown, shrunk] {
+                assert_eq!(if hx < 0 { result.right() } else { result.left() }, if hx < 0 { rect.right() } else { rect.left() });
+                assert_eq!(if hy < 0 { result.bottom() } else { result.top() }, if hy < 0 { rect.bottom() } else { rect.top() });
+            }
+        }
+    }
+}
diff --git a/crates/ui/src/docking/tests.rs b/crates/ui/src/docking/tests.rs
--- a/crates/ui/src/docking/tests.rs
+++ b/crates/ui/src/docking/tests.rs
@@ -300,10 +300,16 @@ fn old_hidden_locations_accept_panel_ids_without_a_default_identity() {
 }
 
 fn frame(ctx: &Context, layout: &Layout<u32>, events: Vec<Event>, focused: bool) -> (DockOutput<u32>, Vec<Id>) {
+    let (output, ids, _) = frame_with_cursor(ctx, layout, events, focused);
+    (output, ids)
+}
+
+/// Render one input frame and expose the cursor output alongside docking actions.
+fn frame_with_cursor(ctx: &Context, layout: &Layout<u32>, events: Vec<Event>, focused: bool) -> (DockOutput<u32>, Vec<Id>, egui::CursorIcon) {
     let mut output = DockOutput::default();
     let mut ids = Vec::new();
     let input = RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), events, focused, ..Default::default() };
-    ctx.run_ui(input, |ui| {
+    let mut full_output = ctx.run_ui(input, |ui| {
         let style = DockStyle::from_ui(ui);
         output = DockArea::new(Id::new("test-area")).show(
             ui,
@@ -316,10 +322,9 @@ fn frame(ctx: &Context, layout: &Layout<u32>, events: Vec<Event>, focused: bool)
                 ui.label("Actual panel content");
             },
         );
-    })
-    .textures_delta
-    .clear();
-    (output, ids)
+    });
+    full_output.textures_delta.clear();
+    (output, ids, full_output.platform_output.cursor_icon)
 }
 
 #[test]
@@ -614,6 +619,7 @@ fn floating_only_surface_does_not_allocate_the_callers_document_region() {
     .clear();
 }
 
+/// Moving the group preserves membership; dragging a tab detaches only that tab.
 #[test]
 fn floating_group_grip_moves_every_member_while_tab_drag_detaches_one() {
     let ctx = Context::default();
@@ -622,7 +628,7 @@ fn floating_group_grip_moves_every_member_while_tab_drag_detaches_one() {
     for _ in 0..2 {
         step(&ctx, &mut layout, vec![]);
     }
-    let start = pos2(86.0, 82.0);
+    let start = ctx.read_response(Id::new("test-area").with(("move-group", Some(&5u32)))).unwrap().rect.center();
     step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
     let actions = step(&ctx, &mut layout, vec![Event::PointerMoved(start + vec2(50.0, 40.0))]);
     assert!(actions.iter().any(|a| matches!(a, Action::MoveFloating { .. })));
@@ -650,6 +656,7 @@ struct CustomContent {
     bodies: usize,
 }
 
+/// Workspace replacement cancels the old group gesture before further pointer motion.
 #[test]
 fn host_cancel_discards_group_drag_before_workspace_replacement() {
     let ctx = Context::default();
@@ -657,7 +664,7 @@ fn host_cancel_discards_group_drag_before_workspace_replacement() {
     for _ in 0..2 {
         step(&ctx, &mut layout, vec![]);
     }
-    let start = pos2(86.0, 82.0);
+    let start = ctx.read_response(Id::new("test-area").with(("move-group", Some(&5u32)))).unwrap().rect.center();
     step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
     let moved = start + vec2(50.0, 40.0);
     let actions = step(&ctx, &mut layout, vec![Event::PointerMoved(moved)]);
@@ -771,6 +778,7 @@ fn overlay_appearing_mid_drag_blocks_release_and_float_fallback() {
     assert!(out.actions.is_empty());
 }
 
+/// Escape restores the outer frame after moving a floating group.
 #[test]
 fn group_move_escape_restores_original_geometry() {
     let ctx = Context::default();
@@ -779,7 +787,7 @@ fn group_move_escape_restores_original_geometry() {
     for _ in 0..2 {
         step(&ctx, &mut layout, vec![]);
     }
-    let start = pos2(86.0, 82.0);
+    let start = ctx.read_response(Id::new("test-area").with(("move-group", Some(&5u32)))).unwrap().rect.center();
     step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
     step(&ctx, &mut layout, vec![Event::PointerMoved(start + vec2(50.0, 40.0))]);
     assert_ne!(layout.floating[0].rect, original);
@@ -819,11 +827,15 @@ fn custom_headers_and_labels_render_at_small_and_large_scales() {
     }
 }
 
+/// A protected member prevents moving the entire floating group.
 #[test]
 fn protected_group_member_blocks_whole_group_gesture_and_host_drop() {
     let ctx = Context::default();
     let layout = sample();
-    let start = pos2(86.0, 82.0);
+    for _ in 0..2 {
+        frame(&ctx, &layout, vec![], true);
+    }
+    let start = ctx.read_response(Id::new("test-area").with(("move-group", Some(&5u32)))).unwrap().rect.center();
     for events in [
         vec![],
         vec![],
@@ -1090,3 +1102,119 @@ fn popup_action_labels_use_custom_visual_text_and_logical_accessibility() {
         );
     }
 }
+
+/// All eight handles retain the opposite edge and emit matching hover and drag cursors.
+#[test]
+fn floating_windows_resize_from_every_edge_and_corner_without_moving_the_opposite_edge() {
+    use egui::CursorIcon::{ResizeHorizontal, ResizeNeSw, ResizeNwSe, ResizeVertical};
+    for (hx, hy, cursor) in [
+        (-1_i8, 0_i8, ResizeHorizontal),
+        (1, 0, ResizeHorizontal),
+        (0, -1, ResizeVertical),
+        (0, 1, ResizeVertical),
+        (-1, -1, ResizeNwSe),
+        (1, -1, ResizeNeSw),
+        (-1, 1, ResizeNeSw),
+        (1, 1, ResizeNwSe),
+    ] {
+        let ctx = Context::default();
+        let mut layout = sample();
+        for _ in 0..3 {
+            step(&ctx, &mut layout, Vec::new());
+        }
+        let [x, y, w, h] = layout.floating[0].rect;
+        let start = pos2(
+            x + if hx < 0 {
+                2.0
+            } else if hx > 0 {
+                w - 2.0
+            } else {
+                w / 2.0
+            },
+            y + if hy < 0 {
+                2.0
+            } else if hy > 0 {
+                h - 2.0
+            } else {
+                h / 2.0
+            },
+        );
+        let (_, _, actual_cursor) = frame_with_cursor(&ctx, &layout, vec![Event::PointerMoved(start)], true);
+        assert_eq!(actual_cursor, cursor, "hover at ({hx}, {hy})");
+        step(&ctx, &mut layout, vec![pointer_button(start, true)]);
+        let delta = vec2(f32::from(hx) * 24.0, f32::from(hy) * 24.0);
+        let end = start + delta;
+        let (out, _, actual_cursor) = frame_with_cursor(&ctx, &layout, vec![Event::PointerMoved(end)], true);
+        let actions = out.actions;
+        for action in &actions {
+            layout.apply(action.clone()).unwrap();
+        }
+        assert!(actions.iter().any(|a| matches!(a, Action::MoveFloating { panel: 5, .. })), "({hx}, {hy}): {actions:?}");
+        assert_eq!(actual_cursor, cursor, "drag at ({hx}, {hy})");
+        step(&ctx, &mut layout, vec![pointer_button(end, false)]);
+        let expected = [
+            x + if hx < 0 { -24.0 } else { 0.0 },
+            y + if hy < 0 { -24.0 } else { 0.0 },
+            w + if hx != 0 { 24.0 } else { 0.0 },
+            h + if hy != 0 { 24.0 } else { 0.0 },
+        ];
+        assert_eq!(layout.floating[0].rect, expected, "({hx}, {hy})");
+        assert_eq!(layout.panels().len(), 6, "resizing must keep group membership");
+    }
+}
+
+/// Scrollbar drags near the right border must scroll the body, not resize the window.
+#[test]
+fn floating_body_scrollbar_keeps_its_drag_region() {
+    let ctx = Context::default();
+    let mut layout = sample();
+    let original = layout.floating[0].rect;
+    let mut body = Rect::NOTHING;
+    let mut offset = 0.0;
+    let mut render = |events: Vec<Event>| {
+        let mut output = DockOutput::default();
+        ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), events, ..Default::default() }, |ui| {
+            let style = DockStyle::from_ui(ui);
+            output = DockArea::new(Id::new("test-area")).show(
+                ui,
+                &layout,
+                &style,
+                |p| format!("Panel {p}"),
+                |_| Permissions::default(),
+                |ui, panel| {
+                    ui.style_mut().spacing.scroll = egui::style::ScrollStyle::solid();
+                    let rect = ui.max_rect();
+                    let scroll = egui::ScrollArea::vertical()
+                        .id_salt(panel)
+                        .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
+                        .show(ui, |ui| {
+                            for _ in 0..100 {
+                                ui.label("Scrollable panel content");
+                            }
+                        });
+                    if *panel == 6 {
+                        body = ui.ctx().read_response(scroll.id.with(1_usize)).unwrap().rect.intersect(rect);
+                        offset = scroll.state.offset.y;
+                    }
+                },
+            );
+        })
+        .textures_delta
+        .clear();
+        assert!(!output.actions.iter().any(|action| matches!(action, Action::MoveFloating { .. })));
+        for action in output.actions {
+            layout.apply(action).unwrap();
+        }
+        (body, offset)
+    };
+    for _ in 0..3 {
+        render(vec![]);
+    }
+    let (body, _) = render(vec![]);
+    let start = pos2(body.center().x, body.top() + 6.0);
+    render(vec![Event::PointerMoved(start), pointer_button(start, true)]);
+    let (_, scrolled) = render(vec![Event::PointerMoved(start + vec2(0.0, 40.0))]);
+    assert!(scrolled > 0.0, "the scrollbar must actually scroll");
+    render(vec![pointer_button(start + vec2(0.0, 40.0), false)]);
+    assert_eq!(layout.floating[0].rect, original);
+}
diff --git a/crates/ui/tests/docking_workflows.rs b/crates/ui/tests/docking_workflows.rs
--- a/crates/ui/tests/docking_workflows.rs
+++ b/crates/ui/tests/docking_workflows.rs
@@ -250,3 +250,58 @@ fn floating_brushes_merge_detach_and_redock_through_real_pointer_input() {
         }
     }
 }
+
+/// Exercise every border while preserving content across themes, sizes and pixel scales.
+#[test]
+fn floating_borders_resize_content_at_multiple_themes_sizes_and_scales() {
+    let output = std::env::var_os("CRAFT_UI_DOCK_VISUAL_DIR").map(PathBuf::from);
+    for dark in [true, false] {
+        for width in [640.0, 1000.0] {
+            for scale in [1.0, 2.0] {
+                let name = format!("{}-{}-{}x", if dark { "dark" } else { "light" }, width as u32, scale as u32);
+                let directory = output.as_ref().map(|p| p.join(&name));
+                let mut h = Harness::new(dark, width, scale);
+                let body_id = h.body_ids[&BRUSHES];
+                let original_values = h.values.clone();
+                for (hx, hy) in [(-1_i8, 0_i8), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)] {
+                    h.layout.floating[0].rect = [100.0, 100.0, 210.0, 250.0];
+                    h.settle();
+                    let from = pos2(
+                        100.0
+                            + if hx < 0 {
+                                2.0
+                            } else if hx > 0 {
+                                208.0
+                            } else {
+                                105.0
+                            },
+                        100.0
+                            + if hy < 0 {
+                                2.0
+                            } else if hy > 0 {
+                                248.0
+                            } else {
+                                125.0
+                            },
+                    );
+                    h.drag(from, from + vec2(f32::from(hx) * 24.0, f32::from(hy) * 24.0));
+                    assert_eq!(
+                        h.layout.floating[0].rect,
+                        [
+                            100.0 - if hx < 0 { 24.0 } else { 0.0 },
+                            100.0 - if hy < 0 { 24.0 } else { 0.0 },
+                            210.0 + if hx != 0 { 24.0 } else { 0.0 },
+                            250.0 + if hy != 0 { 24.0 } else { 0.0 },
+                        ],
+                        "{name}: ({hx}, {hy})"
+                    );
+                    assert_eq!(h.body_ids[&BRUSHES], body_id);
+                    assert_eq!(h.values, original_values);
+                    h.capture(&directory, &format!("resize-{hx}-{hy}"));
+                    h.drag(from + vec2(f32::from(hx) * 24.0, f32::from(hy) * 24.0), from);
+                    assert_eq!(h.layout.floating[0].rect, [100.0, 100.0, 210.0, 250.0], "{name}: shrink ({hx}, {hy})");
+                }
+            }
+        }
+    }
+}
