diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -12,6 +12,10 @@
 
 DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web.
 
+- **2026-10-26** · A turned object's selection turns with it: one box on the object's own
+  outline with its handles on the turned corners, instead of a second, upright box around it; a
+  handle drag resizes it along its own axes (`transform.resize` with `own`), and the rotate zone
+  sits outside the turned corners. No readiness percentage change.
 - **2026-10-25** · The application bar uses craft-window's shared title-bar code. A drag that
   starts on a floating panel over the bar moves the panel, not the window, and in full screen the
   bar no longer zooms or moves the window. No readiness percentage change.
diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs
--- a/crates/engine/src/cmd/object.rs
+++ b/crates/engine/src/cmd/object.rs
@@ -52,7 +52,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "Resize",
             [],
             None,
-            "{from: rect, to: rect, content?: bool (scale content), distribute?: bool (Live Distribute: objects keep their size, their centres spread with the bounds), ids?}",
+            "{from: rect, to: rect, content?: bool (scale content), distribute?: bool (Live Distribute: objects keep their size, their centres spread with the bounds), own?: bool (from and to are in each object's own space, before its rotation: a turned object resizes along its own axes), ids?}",
             has_selection,
             transform_resize
         ),
@@ -1229,7 +1229,8 @@ fn transform_resize(s: &mut Session, p: &Value) -> Result<Value> {
     let sy = ratio(to.height(), from.height());
     let sx = if sx.abs() < 1e-3 { 1e-3f64.copysign(sx) } else { sx };
     let sy = if sy.abs() < 1e-3 { 1e-3f64.copysign(sy) } else { sy };
-    let m = Affine::translate((to.x0, to.y0)) * Affine::scale_non_uniform(sx, sy) * Affine::translate((-from.x0, -from.y0));
+    let own = bool_or(p, "own", false);
+    let rect_map = Affine::translate((to.x0, to.y0)) * Affine::scale_non_uniform(sx, sy) * Affine::translate((-from.x0, -from.y0));
     let strokes = s.prefs.scale_strokes;
     s.edit(|d, _| {
         for id in &ids {
@@ -1237,6 +1238,8 @@ fn transform_resize(s: &mut Session, p: &Value) -> Result<Value> {
             if it.locked {
                 continue;
             }
+            // In its own space the map applies before the object's transform.
+            let m = if own && it.xf.determinant().abs() > 1e-12 { it.xf * rect_map * it.xf.inverse() } else { rect_map };
             if distribute {
                 let c = it.bounds().center();
                 it.xf = Affine::translate(m * c - c) * it.xf;
@@ -4075,6 +4078,27 @@ mod reference_point_tests {
 mod again_tests {
     use super::*;
 
+    /// `own`: a turned frame resizes along its own axes: its path grows, its rotation stays.
+    #[test]
+    fn a_turned_frame_resizes_along_its_own_axes() {
+        let mut s = Session::new();
+        s.execute("file.new", &json!({})).unwrap();
+        let id = ItemId(s.execute("frame.create", &json!({"rect": [100, 100, 200, 150]})).unwrap()["id"].as_u64().unwrap());
+        s.execute("transform.rotate", &json!({"angle": 30})).unwrap();
+        let before = s.doc().unwrap().doc.item(id).unwrap().clone();
+        let own = before.inner_bounds();
+        let wider = designcraft_geom::Rect::new(own.x0, own.y0, own.x0 + own.width() * 2.0, own.y1);
+        let r = |r: designcraft_geom::Rect| json!([r.x0, r.y0, r.x1, r.y1]);
+        s.execute("transform.resize", &json!({"from": r(own), "to": r(wider), "own": true})).unwrap();
+        let it = s.doc().unwrap().doc.item(id).unwrap().clone();
+        assert_eq!(it.xf, before.xf, "the rotation (and position) stay");
+        let b = it.inner_bounds();
+        assert!((b.width() - own.width() * 2.0).abs() < 1e-6 && (b.height() - own.height()).abs() < 1e-6, "{b:?}");
+        // The fixed side stays where it was on the spread.
+        let p = designcraft_geom::Point::new;
+        assert!(((it.xf * p(b.x0, b.y0)) - (before.xf * p(own.x0, own.y0))).hypot() < 1e-6);
+    }
+
     #[test]
     fn transform_again_sequence_and_clear() {
         let mut s = Session::new();
diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs
--- a/crates/tools/src/select.rs
+++ b/crates/tools/src/select.rs
@@ -48,6 +48,15 @@ enum Drag {
         from: Rect,
         spread: SpreadRef,
     },
+    /// Resizing one turned object along its own axes (see [`turned_box`]): `from` is its box in
+    /// its own space, `to_inner` maps spread points there.
+    ResizeTurned {
+        id: ItemId,
+        handle: usize,
+        from: Rect,
+        to_inner: Affine,
+        spread: SpreadRef,
+    },
     /// Resizing a selected placed graphic by its own bounding-box handles (`from`: its bounds on
     /// the spread at pointer down).
     ContentResize {
@@ -426,21 +435,57 @@ fn content_handle_at(cx: &ToolContext, p: Point) -> Option<(ItemId, usize)> {
     handles(b).iter().position(|h| (h.x - p.x).abs() <= tol && (h.y - p.y).abs() <= tol).map(|h| (id, h))
 }
 
+/// The box of `it` in its own space when `m` (its own space to the canvas) turns it off the
+/// canvas axes (rotated other than by quarter turns, or skewed), so its bounding box on the canvas
+/// is not its own box. Groups keep the canvas box.
+pub fn turned_box(it: &Item, m: Affine) -> Option<Rect> {
+    let [a, b, c, d, _, _] = m.as_coeffs();
+    let scale = (a.abs() + b.abs() + c.abs() + d.abs()).max(1e-12);
+    // Both the diagonal and the off-diagonal take part: neither upright nor a quarter turn.
+    let off_axes = (a.abs() + d.abs()).min(b.abs() + c.abs()) / scale > 1e-9;
+    if it.is_group() || !off_axes || m.determinant().abs() < 1e-12 {
+        return None;
+    }
+    let r = it.inner_bounds();
+    (r.width() > 0.0 && r.height() > 0.0 && [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite())).then_some(r)
+}
+
+/// The one selected object when it is turned: its id, its box in its own space and the map from
+/// there to the canvas.
+fn selected_turned(cx: &ToolContext) -> Option<(ItemId, Rect, Affine)> {
+    let [id] = cx.selection.items.as_slice() else { return None };
+    let it = cx.doc.item(*id)?;
+    let m = cx.item_canvas_xf(*id)? * it.xf;
+    Some((*id, turned_box(it, m)?, m))
+}
+
+/// The selection's handles on the canvas: on a turned object's own box, else on the bounding box.
+fn selection_handles(cx: &ToolContext) -> Option<[Point; 8]> {
+    if let Some((_, r, m)) = selected_turned(cx) {
+        return Some(handles(r).map(|h| m * h));
+    }
+    Some(handles(cx.selection_bounds()?))
+}
+
 fn handle_at(cx: &ToolContext, p: Point) -> Option<usize> {
     // With a placed graphic selected, the handles are the graphic's, not the frame's.
     if selected_graphic(cx).is_some() {
         return None;
     }
-    let b = cx.selection_bounds()?;
     let tol = cx.tol(5.0);
-    handles(b).iter().position(|h| (h.x - p.x).abs() <= tol && (h.y - p.y).abs() <= tol)
+    selection_handles(cx)?.iter().position(|h| (h.x - p.x).abs() <= tol && (h.y - p.y).abs() <= tol)
 }
 
-/// Side handles are drawn on the canvas bounding box; fitting uses the frame's local axes.
+/// Side handles are drawn on the canvas bounding box (a turned object's on its own box); fitting
+/// uses the frame's local axes.
 fn fitting_axis(cx: &ToolContext, handle: usize) -> &'static str {
     if handle.is_multiple_of(2) {
         return "both";
     }
+    // A turned object's handles sit on its own box: top and bottom set its height.
+    if selected_turned(cx).is_some() {
+        return if matches!(handle, 1 | 5) { "height" } else { "width" };
+    }
     let canvas_height = matches!(handle, 1 | 5);
     let local_width_is_vertical = cx
         .selection
@@ -458,9 +503,19 @@ fn fitting_axis(cx: &ToolContext, handle: usize) -> &'static str {
 
 /// Just outside a corner handle of the selection → rotate.
 fn rotate_zone(cx: &ToolContext, p: Point) -> Option<Point> {
-    let b = cx.selection_bounds()?;
     let inner = cx.tol(5.0);
     let outer = cx.tol(18.0);
+    // A turned object: just outside a corner of its own box.
+    if let Some((_, r, m)) = selected_turned(cx) {
+        let local = m.inverse() * p;
+        let outside = local.x < r.x0 || local.x > r.x1 || local.y < r.y0 || local.y > r.y1;
+        let near = [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)].iter().any(|c| {
+            let d = (m * *c - p).hypot();
+            d > inner && d <= outer
+        });
+        return (outside && near).then(|| m * r.center());
+    }
+    let b = cx.selection_bounds()?;
     for c in [Point::new(b.x0, b.y0), Point::new(b.x1, b.y0), Point::new(b.x1, b.y1), Point::new(b.x0, b.y1)] {
         let d = (c - p).hypot();
         let outside = p.x < b.x0 || p.x > b.x1 || p.y < b.y0 || p.y > b.y1;
@@ -785,6 +840,15 @@ impl Tool for SelectionTool {
                     self.drag = Drag::Rotate { center, start_angle: (p - center).atan2(), start_rotation: crate::snap::reference_rotation(cx) };
                     return vec![Action::Begin("Rotate".into())];
                 }
+                // A turned object resizes along its own axes.
+                if let Some(h) = handle_at(cx, p).filter(|_| !self.direct)
+                    && let Some((id, from, _)) = selected_turned(cx)
+                    && let (Some(loc), Some(it)) = (cx.doc.find(id), cx.doc.item(id))
+                {
+                    let to_inner = (cx.doc.parent_xf(&loc) * it.xf).inverse();
+                    self.drag = Drag::ResizeTurned { id, handle: h, from, to_inner, spread: loc.spread };
+                    return vec![Action::Begin("Resize".into())];
+                }
                 if let Some(h) = handle_at(cx, p).filter(|_| !self.direct)
                     && let (Some(b), Some(first)) = (cx.selection_bounds(), cx.selection.items.first())
                     && let Some(loc) = cx.doc.find(*first)
@@ -1029,6 +1093,14 @@ impl Tool for SelectionTool {
                         json!({"from": rect_json(from), "to": rect_json(to), "content": ev.mods.cmd, "distribute": distribute}),
                     )]
                 }
+                Drag::ResizeTurned { id, handle, from, to_inner, spread } => {
+                    let to = resize_rect(from, handle, to_inner * cx.layout.to_spread(spread, p), ev.mods);
+                    self.guides.clear();
+                    vec![Action::Preview(
+                        "transform.resize".into(),
+                        json!({"ids": [id.0], "from": rect_json(from), "to": rect_json(to), "own": true, "content": ev.mods.cmd}),
+                    )]
+                }
                 Drag::ContentResize { id, handle, from, spread } => {
                     let to = resize_rect(from, handle, cx.layout.to_spread(spread, p), ev.mods);
                     vec![Action::Preview("transform.set".into(), json!({"ids": [id.0], "content": true, "bounds": rect_json(to)}))]
@@ -1138,6 +1210,7 @@ impl Tool for SelectionTool {
                 match d {
                     Drag::Move { .. }
                     | Drag::Resize { .. }
+                    | Drag::ResizeTurned { .. }
                     | Drag::ContentResize { .. }
                     | Drag::Anchor { .. }
                     | Drag::Rotate { .. }
@@ -1229,7 +1302,7 @@ impl Tool for SelectionTool {
                 v.extend(self.guides.iter().cloned());
                 v
             }
-            Drag::Move { .. } | Drag::Resize { .. } | Drag::Rotate { .. } | Drag::Anchor { .. } => self.guides.clone(),
+            Drag::Move { .. } | Drag::Resize { .. } | Drag::ResizeTurned { .. } | Drag::Rotate { .. } | Drag::Anchor { .. } => self.guides.clone(),
             // The whole graphic shows while it's resized, outside the frame too.
             Drag::ContentResize { id, .. } => vec![Overlay::ContentGhost(*id)],
             Drag::None | Drag::Bracket { .. } => brackets(cx),
@@ -1273,7 +1346,7 @@ impl Tool for SelectionTool {
         }
         match self.drag {
             Drag::Move { .. } => return Cursor::Move,
-            Drag::Resize { handle, .. } => return handle_cursor(handle),
+            Drag::Resize { handle, .. } | Drag::ResizeTurned { handle, .. } => return handle_cursor(handle),
             Drag::Rotate { .. } => return Cursor::Rotate,
             Drag::Corner { .. } => return Cursor::Arrow,
             _ => {}
diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs
--- a/crates/tools/src/tests.rs
+++ b/crates/tools/src/tests.rs
@@ -1966,3 +1966,41 @@ fn loaded_text_cursor_threads_into_a_frame_inside_a_group() {
     assert_eq!(cx2.hit_text_frame(q).map(|(_, i)| i), None, "no text frame in that group");
     assert_eq!(t2.cursor(&cx2, q, Mods::default()), Cursor::NotAllowed);
 }
+
+/// A turned object's handles sit on its own (turned) box, not on the bounding box around it; a
+/// drag on one resizes it along its own axes (`transform.resize` with `own`).
+#[test]
+fn a_turned_objects_handles_turn_with_it() {
+    let mut d = Document::new(&NewDocument::default());
+    let id = add_rect(&mut d, Rect::new(100.0, 100.0, 200.0, 150.0));
+    let turn = designcraft_geom::Affine::translate((150.0, 125.0))
+        * designcraft_geom::Affine::rotate(30f64.to_radians())
+        * designcraft_geom::Affine::translate((-150.0, -125.0));
+    d.item_mut(id).unwrap().xf = turn;
+    let s = Selection { items: vec![id], ..Default::default() };
+    let (c, l) = (Cache::new(), CanvasLayout::new(&d, false));
+    let cx = ctx(&d, &s, &c, &l);
+    let m = l.xf(SpreadRef::Doc(0)) * turn;
+    // The corner of the bounding box around the turned rect is no handle.
+    let bbox = m.transform_rect_bbox(Rect::new(100.0, 100.0, 200.0, 150.0));
+    let mut t = create("selection");
+    let down = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, bbox.x1, bbox.y1));
+    assert_ne!(down, vec![Action::Begin("Resize".into())]);
+    // The turned bottom right corner is.
+    let mut t = create("selection");
+    let br = m * Point::new(200.0, 150.0);
+    assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, br.x, br.y)), vec![Action::Begin("Resize".into())]);
+    // Dragged 50 pt further along its own x axis: 50 pt wider, the same height.
+    let to = m * Point::new(250.0, 150.0);
+    match &t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, to.x, to.y))[0] {
+        Action::Preview(cmd, p) => {
+            assert_eq!(cmd, "transform.resize");
+            assert_eq!(p["own"], true);
+            assert_eq!(p["ids"], serde_json::json!([id.0]));
+            let r: Vec<f64> = p["to"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
+            let want = [100.0, 100.0, 250.0, 150.0];
+            assert!(r.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-6), "{r:?}");
+        }
+        other => panic!("{other:?}"),
+    }
+}
diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -1407,13 +1407,22 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
     }
     let mut union: Option<Rect> = None;
     let mut color = Color32::from_rgb(79, 153, 255);
+    // One turned object: its box and handles turn with it (screen corners of its own box).
+    let mut turned: Option<[Pos2; 8]> = None;
     for id in &sel.items {
         let (Some(it), Some((a, _))) = (doc.item(*id), item_canvas_xf(doc, layout, *id)) else { continue };
         color = layer_color(doc, it);
         for poly in path_screen(it, xf, a) {
             painter.add(egui::Shape::line(poly, Stroke::new(1.0, color)));
         }
-        if sel.items.len() == 1 && sel.text.is_none() {
+        let own = (sel.items.len() == 1 && !sel.content).then(|| designcraft_tools::select::turned_box(it, a * it.xf)).flatten();
+        if let Some(r) = own {
+            let hs = designcraft_tools::select::handles(r).map(|h| xf.to_screen(a * it.xf * h));
+            if sel.text.is_none() {
+                painter.add(egui::Shape::closed_line(vec![hs[0], hs[2], hs[4], hs[6]], Stroke::new(1.0, color)));
+            }
+            turned = Some(hs);
+        } else if sel.items.len() == 1 && sel.text.is_none() {
             // InDesign draws a 1 pt bounding box around the selection.
             painter.rect_stroke(xf.rect(a.transform_rect_bbox(it.bounds())), 0.0, Stroke::new(1.0, color), StrokeKind::Middle);
         } else if sel.key == Some(*id) {
@@ -1477,15 +1486,17 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
         }
         let hs = 6.5;
         // Centre point. The Selection tool draws the live-corner widget as an overlay.
-        painter.rect_filled(Rect::from_center_size(u.center(), vec2(3.5, 3.5)), 0.0, color);
+        let centre = turned.map_or(u.center(), |h| pos2((h[0].x + h[4].x) / 2.0, (h[0].y + h[4].y) / 2.0));
+        painter.rect_filled(Rect::from_center_size(centre, vec2(3.5, 3.5)), 0.0, color);
         // A selected placed graphic has its own handles instead of the frame's.
         let graphic_selected =
             sel.content && sel.items.len() == 1 && doc.item(sel.items[0]).is_some_and(|i| matches!(i.content, Content::Graphic(_)));
-        for h in designcraft_tools::select::handles(DRect::new(u.min.x as f64, u.min.y as f64, u.max.x as f64, u.max.y as f64))
-            .into_iter()
-            .filter(|_| !graphic_selected)
-        {
-            let r = Rect::from_center_size(pos2(h.x as f32, h.y as f32), vec2(hs, hs));
+        let box_handles = turned.unwrap_or_else(|| {
+            designcraft_tools::select::handles(DRect::new(u.min.x as f64, u.min.y as f64, u.max.x as f64, u.max.y as f64))
+                .map(|h| pos2(h.x as f32, h.y as f32))
+        });
+        for h in box_handles.into_iter().filter(|_| !graphic_selected) {
+            let r = Rect::from_center_size(h, vec2(hs, hs));
             painter.rect_filled(r, 0.0, Color32::WHITE);
             painter.rect_stroke(r, 0.0, Stroke::new(hair(painter), color), StrokeKind::Inside);
         }
