diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -3,7 +3,7 @@
 **Stage: pre-alpha** · next: alpha, ~5–7 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%)
 - FID1: PDF export normalizes mixed emitted gradient color spaces to RGB with an explicit warning; homogeneous device-space gradients retain their components. Linear/radial, midpoint, archival and print policies have synthetic regression coverage.
 
-> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (File ▸ Open Recent; .indd and .indt open through indd-utils; text threads by hand; the alpha gate waits on Primary Text Frame page fixes; Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); window and menu quit confirmation; failed writes preserve live save and collection state; spring-loaded Zoom navigation; orderly native exit recovery; Pages panel parents; cancelable macOS Dock Quit; text stroke behind fill; Based on Parent; object styles carry effects; table anchors keep their own paragraphs; grouped HTML/EPUB reading order; Links source paths; XLSX cached cell values retain numeric precision and text) · **Target:** Adobe InDesign 2026
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (independent parent page resizing, #1620; File ▸ Open Recent; .indd and .indt open through indd-utils; text threads by hand; the alpha gate waits on Primary Text Frame page fixes; Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); window and menu quit confirmation; failed writes preserve live save and collection state; spring-loaded Zoom navigation; orderly native exit recovery; Pages panel parents; cancelable macOS Dock Quit; text stroke behind fill; Based on Parent; object styles carry effects; table anchors keep their own paragraphs; grouped HTML/EPUB reading order; Links source paths; XLSX cached cell values retain numeric precision and text) · **Target:** Adobe InDesign 2026
 - AR1.1: Kashida justification tests verify space fallback without Tatweel fonts using an isolated font database, while retaining elongation assertions when fonts support it.
 
 - AR1: Arabic IDML controls now retain character direction, kashida switches, diacritic offsets/forms, paragraph policy values and independent story/table directions. Shaping preserves joining context and script/language selection; paragraph-level bidi, contextual digit conversion, mark-safe elongation, RTL columns/tables and physical column insertion have regression coverage. Vendor-specific justification and diacritic presets remain explicit preflight warnings; see docs/arabic-typography.md.
@@ -348,6 +348,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
 
 Newest first.
 
+- **2026-10-23** · Parent page size edits target the selected parent page from Properties, the Page Tool Control panel and page handles (#1620), preserving independent document page sizes, Undo/Redo and the existing preview/Alt-drag behavior. No readiness percentage change.
+
 - **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change.
 - **2026-10-22** · A ruler guide dragged with the Selection tools, out of a ruler, on its own or with other selected guides, snaps along its axis as objects do (#1221): to other ruler guides, margins, columns, bleed, page and object edges and centers (with the smart-guide mark) and the document grid, within the snap zone and following Snap to Guides and Smart Guides. Shift snaps to the ruler's ticks and Cmd suspends snapping. The preview line, the position readout and the drop use the same snapped position. No readiness percentage change.
 - **2026-10-22** · macOS: the app menu has a Language submenu after Settings… with each interface language in its own name and the current one checked, as in the suite's other apps; Preferences › General › Interface Language stays. No readiness percentage change.
diff --git a/crates/doc/src/selection.rs b/crates/doc/src/selection.rs
--- a/crates/doc/src/selection.rs
+++ b/crates/doc/src/selection.rs
@@ -3,7 +3,7 @@
 use serde::{Deserialize, Serialize};
 
 use crate::edit::SpreadRef;
-use crate::ids::{ItemId, StoryId};
+use crate::ids::{ItemId, SpreadId, StoryId};
 use crate::table::CellRange;
 
 /// A selected ruler guide: index `index` in the guides of page `page` of spread `spread`.
@@ -93,6 +93,9 @@ pub struct Selection {
     /// The page selected with the Page tool (absolute index).
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub page: Option<usize>,
+    /// Parent selected with the Page tool: stable spread id and page index within it.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub parent_page: Option<(SpreadId, usize)>,
     /// Selected ruler guides. Never together with items, text or cells: selecting one clears
     /// the other.
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs
--- a/crates/engine/src/cmd/layout.rs
+++ b/crates/engine/src/cmd/layout.rs
@@ -622,6 +622,30 @@ pub fn specs() -> Vec<CommandSpec> {
             has_doc,
             page_size
         ),
+        cmd!(
+            "layout.parentPageSize",
+            "Parent Page Size",
+            [],
+            None,
+            "{parent: spread id, page: 0-based parent page index, width?, height?, anchor?: 0–8} — resize only this parent page, keeping document page sizes independent",
+            has_doc,
+            parent_page_size
+        ),
+        cmd!(
+            noundo "layout.selectParentPage", "Select Parent Page", [], None,
+            "{parent: spread id|null, page?: 0-based parent page index} — select a parent page with the Page tool; null clears the selection",
+            has_doc, |s, p| {
+                let target = match p.get("parent") {
+                    Some(Value::Null) => None,
+                    _ => Some(parent_page_target(&s.doc()?.doc, p, "layout.selectParentPage")?),
+                };
+                let st = s.doc_mut()?;
+                st.selection = Default::default();
+                st.selection.parent_page = target.map(|(si, pi)| (st.doc.parents[si].id, pi));
+                st.bump_revision();
+                ok()
+            }
+        ),
         cmd!(
             "layout.pageOptions",
             "Page Options",
@@ -656,6 +680,7 @@ pub fn specs() -> Vec<CommandSpec> {
                 st.selection = designcraft_doc::Selection::items(ids);
             }
             st.selection.page = page;
+            st.selection.parent_page = None;
             st.bump_revision();
             Ok(json!({"page": page.map(|p| p + 1)}))
         }
@@ -1021,6 +1046,43 @@ fn page_size(s: &mut crate::Session, p: &Value) -> Result<Value> {
     })
 }
 
+fn parent_page_target(d: &designcraft_doc::Document, p: &Value, cmd: &str) -> Result<(usize, usize)> {
+    let parent = p.get("parent").and_then(Value::as_u64).map(SpreadId).ok_or_else(|| bad(cmd, "give a parent spread id"))?;
+    let si = d.parent_index(parent).ok_or_else(|| bad(cmd, "no such parent"))?;
+    let pi = p
+        .get("page")
+        .and_then(Value::as_u64)
+        .and_then(|n| usize::try_from(n).ok())
+        .filter(|i| *i < d.parents[si].pages.len())
+        .ok_or_else(|| bad(cmd, "no such parent page"))?;
+    Ok((si, pi))
+}
+
+fn parent_page_size(s: &mut Session, p: &Value) -> Result<Value> {
+    const ID: &str = "layout.parentPageSize";
+    let (si, pi) = parent_page_target(&s.doc()?.doc, p, ID)?;
+    let size = |key| {
+        p.get(key)
+            .map(|v| {
+                v.as_f64().filter(|v| v.is_finite() && *v > 0.0 && *v <= 15552.0).ok_or_else(|| bad(ID, "page size out of range (0 < size ≤ 216 in)"))
+            })
+            .transpose()
+    };
+    let (w, h) = (size("width")?, size("height")?);
+    if w.is_none() && h.is_none() {
+        return Err(bad(ID, "give width or height"));
+    }
+    let anchor = match p.get("anchor") {
+        None => 0,
+        Some(v) => v.as_u64().filter(|n| *n <= 8).ok_or_else(|| bad(ID, "anchor: 0–8"))? as usize,
+    };
+    s.edit(|d, _| {
+        resize_spread_page(d, designcraft_doc::SpreadRef::Parent(si), pi, w, h, anchor);
+        d.sync_parent_margins();
+        Ok(json!({"parent": d.parents[si].id.0, "page": pi}))
+    })
+}
+
 fn page_options(s: &mut crate::Session, p: &Value) -> Result<Value> {
     let pages: Vec<usize> =
         p.get("pages").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|n| n as usize).collect()).unwrap_or_default();
@@ -1050,84 +1112,86 @@ pub(crate) fn resize_pages(d: &mut designcraft_doc::Document, pages: &[usize], w
 /// [`resize_pages`] keeping reference point `anchor` (0–8, row-major from the top-left) in place:
 /// the page grows away from it, so objects keep their place relative to it.
 pub(crate) fn resize_pages_from(d: &mut designcraft_doc::Document, pages: &[usize], w: Option<f64>, h: Option<f64>, anchor: usize) {
+    for page in pages {
+        let Some((si, pi)) = page.checked_sub(1).and_then(|i| d.page_loc(i)) else { continue };
+        resize_spread_page(d, designcraft_doc::SpreadRef::Doc(si), pi, w.map(|v| v.max(1.0)), h.map(|v| v.max(1.0)), anchor);
+    }
+}
+
+fn resize_spread_page(d: &mut designcraft_doc::Document, sr: designcraft_doc::SpreadRef, pi: usize, w: Option<f64>, h: Option<f64>, anchor: usize) {
     let (fx, fy) = ((anchor % 3) as f64 / 2.0, (anchor / 3).min(2) as f64 / 2.0);
-    {
-        for page in pages {
-            let Some((si, pi)) = d.page_loc(page - 1) else { continue };
-            let sp = std::sync::Arc::make_mut(&mut d.spreads[si]);
-            let old: Vec<f64> = sp.pages.iter().map(|x| x.x).collect();
-            let old_bounds = sp.pages[pi].bounds();
-            // The page's own objects (outer pages take the pasteboard beside them).
-            let last = sp.pages.len() - 1;
-            let (lo, hi) = (if pi == 0 { f64::NEG_INFINITY } else { old_bounds.x0 }, if pi == last { f64::INFINITY } else { old_bounds.x1 });
-            let own: Vec<usize> = sp
-                .items
-                .iter()
-                .enumerate()
-                .filter(|(_, it)| {
-                    let c = it.bounds().center().x;
-                    c >= lo && c < hi
-                })
-                .map(|(i, _)| i)
-                .collect();
-            let pg = &mut sp.pages[pi];
-            let dw = w.map_or(0.0, |w| w.max(1.0) - pg.width);
-            let dh = h.map_or(0.0, |h| h.max(1.0) - pg.height);
-            if let Some(w) = w {
-                set_page_width(pg, w.max(1.0));
-            }
-            if let Some(h) = h {
-                pg.height = h.max(1.0);
-            }
-            let left = pg.side == designcraft_doc::PageSide::Left;
-            sp.relayout();
-            // Keep the spine: shift the whole spread back when a left page grows.
-            if left && dw != 0.0 {
-                for q in &mut sp.pages {
-                    q.x -= dw;
-                }
-            }
-            let new: Vec<f64> = sp.pages.iter().map(|x| x.x).collect();
-            // Objects follow their page.
-            let shifts: Vec<(f64, f64, f64)> =
-                sp.pages.iter().enumerate().map(|(k, q)| (old[k], old[k] + q.width - if k == pi { dw } else { 0.0 }, new[k] - old[k])).collect();
-            for it in &mut sp.items {
-                let cx = it.bounds().center().x;
-                if let Some((_, _, dx)) = shifts.iter().find(|(a, b, _)| cx >= *a && cx < *b)
-                    && *dx != 0.0
-                {
-                    let it = std::sync::Arc::make_mut(it);
-                    it.xf = designcraft_geom::Affine::translate((*dx, 0.0)) * it.xf;
-                }
-            }
-            // Page guides move with their page.
-            for (k, q) in sp.pages.iter_mut().enumerate() {
-                let dx = new[k] - old[k];
-                if dx != 0.0 {
-                    for g in q.guides.iter_mut().filter(|g| g.orientation == designcraft_doc::Orientation::Vertical) {
-                        g.position += dx;
-                    }
-                }
+    let Some(sp) = d.spread_mut(sr) else { return };
+    let origin = sp.bounds().x0;
+    let old: Vec<f64> = sp.pages.iter().map(|x| x.x).collect();
+    let old_bounds = sp.pages[pi].bounds();
+    // The page's own objects (outer pages take the pasteboard beside them).
+    let Some((leftmost, rightmost)) = sp.outer_page_indices() else { return };
+    let (lo, hi) = (if pi == leftmost { f64::NEG_INFINITY } else { old_bounds.x0 }, if pi == rightmost { f64::INFINITY } else { old_bounds.x1 });
+    let own: Vec<usize> = sp
+        .items
+        .iter()
+        .enumerate()
+        .filter(|(_, it)| {
+            let c = it.bounds().center().x;
+            c >= lo && c < hi
+        })
+        .map(|(i, _)| i)
+        .collect();
+    let pg = &mut sp.pages[pi];
+    let dw = w.map_or(0.0, |w| w - pg.width);
+    let dh = h.map_or(0.0, |h| h - pg.height);
+    if let Some(w) = w {
+        set_page_width(pg, w);
+    }
+    if let Some(h) = h {
+        pg.height = h;
+    }
+    let left = pg.side == designcraft_doc::PageSide::Left;
+    sp.relayout();
+    // Relayout resets the origin; retain prior edits and grow left pages away from the spine.
+    let offset = origin - if left { dw } else { 0.0 };
+    for q in &mut sp.pages {
+        q.x += offset;
+    }
+    let new: Vec<f64> = sp.pages.iter().map(|x| x.x).collect();
+    // Objects follow their page.
+    let shifts: Vec<(f64, f64, f64)> =
+        sp.pages.iter().enumerate().map(|(k, q)| (old[k], old[k] + q.width - if k == pi { dw } else { 0.0 }, new[k] - old[k])).collect();
+    for it in &mut sp.items {
+        let cx = it.bounds().center().x;
+        if let Some((_, _, dx)) = shifts.iter().find(|(a, b, _)| cx >= *a && cx < *b)
+            && *dx != 0.0
+        {
+            let it = std::sync::Arc::make_mut(it);
+            it.xf = designcraft_geom::Affine::translate((*dx, 0.0)) * it.xf;
+        }
+    }
+    // Page guides move with their page.
+    for (k, q) in sp.pages.iter_mut().enumerate() {
+        let dx = new[k] - old[k];
+        if dx != 0.0 {
+            for g in q.guides.iter_mut().filter(|g| g.orientation == designcraft_doc::Orientation::Vertical) {
+                g.position += dx;
             }
-            // Growing away from the anchor: the page's objects and guides keep their place
-            // relative to it.
-            let shift = designcraft_geom::Vec2::new(fx * dw, fy * dh);
-            if shift != designcraft_geom::Vec2::ZERO {
-                for i in &own {
-                    if let Some(it) = sp.items.get_mut(*i) {
-                        let it = std::sync::Arc::make_mut(it);
-                        it.xf = designcraft_geom::Affine::translate(shift) * it.xf;
-                    }
-                }
-                for g in &mut sp.pages[pi].guides {
-                    g.position += if g.orientation == designcraft_doc::Orientation::Vertical { shift.x } else { shift.y };
-                }
+        }
+    }
+    // Growing away from the anchor: the page's objects and guides keep their place
+    // relative to it.
+    let shift = designcraft_geom::Vec2::new(fx * dw, fy * dh);
+    if shift != designcraft_geom::Vec2::ZERO {
+        for i in &own {
+            if let Some(it) = sp.items.get_mut(*i) {
+                let it = std::sync::Arc::make_mut(it);
+                it.xf = designcraft_geom::Affine::translate(shift) * it.xf;
             }
-            // Liquid rules: objects were carried along with the page's left edge (and the anchor).
-            let moved = old_bounds + designcraft_geom::Vec2::new(new[pi] - old[pi], 0.0) + shift;
-            super::liquid::apply(d, designcraft_doc::SpreadRef::Doc(si), pi, moved);
+        }
+        for g in &mut sp.pages[pi].guides {
+            g.position += if g.orientation == designcraft_doc::Orientation::Vertical { shift.x } else { shift.y };
         }
     }
+    // Liquid rules: objects were carried along with the page's left edge (and the anchor).
+    let moved = old_bounds + designcraft_geom::Vec2::new(new[pi] - old[pi], 0.0) + shift;
+    super::liquid::apply(d, sr, pi, moved);
 }
 
 /// File › Document Setup values.
@@ -2497,3 +2561,7 @@ mod parent_base_tests {
 #[cfg(test)]
 #[path = "parent_delete_tests.rs"]
 mod parent_delete_tests;
+
+#[cfg(test)]
+#[path = "parent_resize_tests.rs"]
+mod parent_resize_tests;
diff --git a/crates/engine/src/cmd/liquid.rs b/crates/engine/src/cmd/liquid.rs
--- a/crates/engine/src/cmd/liquid.rs
+++ b/crates/engine/src/cmd/liquid.rs
@@ -70,8 +70,8 @@ pub(crate) fn apply(d: &mut Document, sr: SpreadRef, pi: usize, old: Rect) {
     let liquid_guides = |o: Orientation| pg.guides.iter().filter(|g| g.liquid && g.orientation == o).map(|g| g.position).collect::<Vec<_>>();
     let (vg, hg) = (liquid_guides(Orientation::Vertical), liquid_guides(Orientation::Horizontal));
     // The page's objects: centres within its old horizontal extent (outer pages take the pasteboard).
-    let last = sp.pages.len() - 1;
-    let (lo, hi) = (if pi == 0 { f64::NEG_INFINITY } else { old.x0 }, if pi == last { f64::INFINITY } else { old.x1 });
+    let Some((leftmost, rightmost)) = sp.outer_page_indices() else { return };
+    let (lo, hi) = (if pi == leftmost { f64::NEG_INFINITY } else { old.x0 }, if pi == rightmost { f64::INFINITY } else { old.x1 });
     let strokes = false;
     let Some(sp) = d.spread_mut(sr) else { return };
     for it in &mut sp.items {
diff --git a/crates/engine/src/cmd/parent_resize_tests.rs b/crates/engine/src/cmd/parent_resize_tests.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/src/cmd/parent_resize_tests.rs
@@ -0,0 +1,299 @@
+use std::sync::Arc;
+
+use designcraft_doc::{Columns, LiquidRule, PageSide, SpreadId};
+use designcraft_geom::Rect;
+use serde_json::json;
+
+use crate::Session;
+
+fn parent_id(s: &Session, index: usize) -> u64 {
+    s.doc().unwrap().doc.parents[index].id.0
+}
+
+fn parent_page(s: &Session, index: usize, page: usize) -> designcraft_doc::Page {
+    s.doc().unwrap().doc.parents[index].pages[page].clone()
+}
+
+fn parent_frame(s: &mut Session, x: f64) -> u64 {
+    s.execute(
+        "frame.create",
+        &json!({
+            "spread": {"kind": "parent", "index": 0},
+            "rect": [x, 80.0, x + 100.0, 180.0]
+        }),
+    )
+    .unwrap()["id"]
+        .as_u64()
+        .unwrap()
+}
+
+fn bounds(s: &Session, id: u64) -> Rect {
+    s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().bounds()
+}
+
+fn guide_positions(s: &Session, page: usize) -> Vec<f64> {
+    s.doc().unwrap().doc.parents[0].pages[page].guides.iter().map(|g| g.position).collect()
+}
+
+fn prepare_parent_pages(s: &mut Session, binding: &str, rule: LiquidRule) -> (u64, u64, u64, usize, usize) {
+    s.execute("file.new", &json!({"pages": 2, "facingPages": true, "width": 400.0, "height": 400.0})).unwrap();
+    s.execute("layout.documentSetup", &json!({"binding": binding})).unwrap();
+    assert_eq!(s.doc().unwrap().doc.settings.right_to_left_binding, binding == "rightToLeft");
+    assert_eq!(s.doc().unwrap().doc.parents[0].pages[0].side, if binding == "rightToLeft" { PageSide::Right } else { PageSide::Left });
+    let target = s.doc().unwrap().doc.parents[0].pages.iter().position(|p| p.side == PageSide::Right).unwrap();
+    let other = 1 - target;
+    let target_page = parent_page(s, 0, target);
+    let other_page = parent_page(s, 0, other);
+    let target_item = parent_frame(s, target_page.x + 80.0);
+    let other_item = parent_frame(s, other_page.x + 80.0);
+    s.execute(
+        "guide.add",
+        &json!({"spread": {"kind": "parent", "index": 0}, "page": target, "orientation": "vertical", "position": target_page.x + 120.0}),
+    )
+    .unwrap();
+    s.execute(
+        "guide.add",
+        &json!({"spread": {"kind": "parent", "index": 0}, "page": target, "orientation": "horizontal", "position": 120.0, "at": target_page.x + 10.0}),
+    )
+    .unwrap();
+    s.execute(
+        "guide.add",
+        &json!({"spread": {"kind": "parent", "index": 0}, "page": other, "orientation": "vertical", "position": other_page.x + 120.0}),
+    )
+    .unwrap();
+    s.execute(
+        "guide.add",
+        &json!({"spread": {"kind": "parent", "index": 0}, "page": other, "orientation": "horizontal", "position": 120.0, "at": other_page.x + 10.0}),
+    )
+    .unwrap();
+    s.edit(|d, _| {
+        let sp = Arc::make_mut(&mut d.parents[0]);
+        let positions = |page: &designcraft_doc::Page| {
+            let w = page.margin_rect().width();
+            vec![0.0, 100.0, 120.0, w]
+        };
+        for page in &mut sp.pages {
+            page.columns = Columns { count: 2, gutter: 20.0, positions: Some(positions(page)) };
+        }
+        sp.pages[target].liquid = rule;
+        Ok(())
+    })
+    .unwrap();
+    (parent_id(s, 0), target_item, other_item, target, other)
+}
+
+#[test]
+fn parent_resize_changes_only_the_target_page_in_ltr_and_rtl() {
+    for binding in ["leftToRight", "rightToLeft"] {
+        for rule in [
+            LiquidRule::Off,
+            LiquidRule::Scale,
+            LiquidRule::ReCenter,
+            LiquidRule::ObjectBased,
+            LiquidRule::GuideBased,
+            LiquidRule::ControlledByParent,
+        ] {
+            let mut s = Session::new();
+            let (parent, target_item, other_item, target, other) = prepare_parent_pages(&mut s, binding, rule);
+            let before_target = parent_page(&s, 0, target);
+            let before_other = parent_page(&s, 0, other);
+            let before_target_item = bounds(&s, target_item);
+            let before_other_item = bounds(&s, other_item);
+            let before_target_guides = guide_positions(&s, target);
+            let before_other_guides = guide_positions(&s, other);
+            let before_target_columns = before_target.columns.positions.clone();
+            let before_other_columns = before_other.columns.positions.clone();
+
+            s.execute("layout.selectParentPage", &json!({"parent": parent, "page": target})).unwrap();
+            assert_eq!(s.doc().unwrap().selection.parent_page, Some((SpreadId(parent), target)));
+            assert!(s.doc().unwrap().selection.items.is_empty());
+            s.execute("layout.parentPageSize", &json!({"parent": parent, "page": target, "width": 500.0, "height": 500.0, "anchor": 8})).unwrap();
+
+            let after_target = parent_page(&s, 0, target);
+            let after_other = parent_page(&s, 0, other);
+            assert_eq!((after_target.width, after_target.height), (500.0, 500.0), "{binding} {rule:?}");
+            assert_eq!((after_other.width, after_other.height), (before_other.width, before_other.height), "{binding} {rule:?}");
+            assert_ne!(bounds(&s, target_item), before_target_item, "target object did not follow the resize: {binding} {rule:?}");
+            assert_eq!(bounds(&s, other_item), before_other_item, "other page object moved: {binding} {rule:?}");
+            assert_ne!(guide_positions(&s, target), before_target_guides, "target guides did not follow the resize: {binding} {rule:?}");
+            assert_eq!(guide_positions(&s, other), before_other_guides, "other page guides moved: {binding} {rule:?}");
+            assert_ne!(after_target.columns.positions, before_target_columns, "target columns did not scale: {binding} {rule:?}");
+            assert_eq!(after_other.columns.positions, before_other_columns, "other page columns changed: {binding} {rule:?}");
+            assert_eq!(s.doc().unwrap().doc.settings.page_width, 400.0);
+            assert_eq!(s.doc().unwrap().doc.settings.page_height, 400.0);
+            s.doc().unwrap().doc.check().unwrap();
+        }
+    }
+}
+
+#[test]
+fn parent_resize_is_independent_of_applied_and_based_on_parents_and_undoable() {
+    let mut s = Session::new();
+    s.execute("file.new", &json!({"pages": 1, "facingPages": false, "width": 400.0, "height": 400.0})).unwrap();
+    let a = parent_id(&s, 0);
+    let b = s.execute("layout.parents.new", &json!({"prefix": "B", "name": "Child"})).unwrap()["id"].as_u64().unwrap();
+    s.execute("layout.parents.options", &json!({"parent": b, "basedOn": a})).unwrap();
+    s.execute("layout.pages.applyParent", &json!({"pages": [0], "parent": b})).unwrap();
+    let before = s.doc().unwrap().doc.as_ref().clone();
+    let undo_before = s.doc().unwrap().history.undo.len();
+
+    s.execute("layout.parentPageSize", &json!({"parent": a, "page": 0, "width": 0.5, "anchor": 0})).unwrap();
+    let d = &s.doc().unwrap().doc;
+    assert_eq!(d.parents[0].pages[0].width, 0.5);
+    assert_eq!(d.parents[1].pages[0].width, before.parents[1].pages[0].width);
+    assert_eq!(d.spreads[0].pages[0].width, before.spreads[0].pages[0].width);
+    assert_eq!(d.parents[1].parent.as_ref().unwrap().based_on, Some(SpreadId(a)));
+    assert_eq!(d.settings.page_width, before.settings.page_width);
+    d.check().unwrap();
+    assert_eq!(s.doc().unwrap().history.undo.len(), undo_before + 1);
+
+    s.execute("edit.undo", &json!({})).unwrap();
+    assert_eq!(s.doc().unwrap().doc.parents[0].pages[0].width, before.parents[0].pages[0].width);
+    s.execute("edit.redo", &json!({})).unwrap();
+    assert_eq!(s.doc().unwrap().doc.parents[0].pages[0].width, 0.5);
+    s.doc().unwrap().doc.check().unwrap();
+}
+
+#[test]
+fn parent_resize_survives_native_and_idml_round_trips() {
+    let mut s = Session::new();
+    s.execute("file.new", &json!({"pages": 1, "facingPages": false, "width": 400.0, "height": 400.0})).unwrap();
+    let a = parent_id(&s, 0);
+    s.execute("layout.parentPageSize", &json!({"parent": a, "page": 0, "width": 0.5, "height": 0.75})).unwrap();
+    let d = s.doc().unwrap().doc.as_ref().clone();
+    d.check().unwrap();
+
+    let native = crate::cmd::from_bytes(&crate::cmd::to_bytes(&d).unwrap()).unwrap();
+    assert_eq!((native.parents[0].pages[0].width, native.parents[0].pages[0].height), (0.5, 0.75));
+    native.check().unwrap();
+
+    let idml = designcraft_idml::export_idml(&d);
+    let reopened = designcraft_idml::import_idml(&idml).unwrap();
+    assert_eq!((reopened.parents[0].pages[0].width, reopened.parents[0].pages[0].height), (0.5, 0.75));
+    reopened.check().unwrap();
+}
+
+#[test]
+fn parent_resize_rejects_invalid_inputs_atomically() {
+    let mut s = Session::new();
+    s.execute("file.new", &json!({"pages": 1, "facingPages": false})).unwrap();
+    let parent = parent_id(&s, 0);
+    let before = s.doc().unwrap().doc.as_ref().clone();
+    let undo_before = s.doc().unwrap().history.undo.len();
+    let invalid = [
+        json!({"parent": parent, "page": 0, "width": null}),
+        json!({"parent": parent, "page": 0, "width": "400"}),
+        json!({"parent": parent, "page": 0, "width": "NaN"}),
+        json!({"parent": parent, "page": 0, "width": "Infinity"}),
+        json!({"parent": parent, "page": 0, "width": 0}),
+        json!({"parent": parent, "page": 0, "width": -1}),
+        json!({"parent": parent, "page": 0, "width": 15552.1}),
+        json!({"parent": parent, "page": 0, "width": 400, "anchor": 9}),
+        json!({"parent": parent, "page": 1, "width": 400}),
+        json!({"parent": 999_999, "page": 0, "width": 400}),
+    ];
+    for params in invalid {
+        assert!(s.execute("layout.parentPageSize", &params).is_err(), "accepted {params}");
+        assert_eq!(s.doc().unwrap().doc.as_ref(), &before, "mutated after {params}");
+        assert_eq!(s.doc().unwrap().history.undo.len(), undo_before, "added undo after {params}");
+    }
+    assert!(s.execute("layout.parentPageSize", &json!({"parent": parent, "page": 0})).is_err());
+    assert_eq!(s.doc().unwrap().doc.as_ref(), &before);
+    assert_eq!(s.doc().unwrap().history.undo.len(), undo_before);
+    s.doc().unwrap().doc.check().unwrap();
+}
+
+#[test]
+fn parent_and_document_page_selection_validate_and_clear_each_other() {
+    let mut s = Session::new();
+    s.execute("file.new", &json!({"pages": 1, "facingPages": false})).unwrap();
+    let parent = parent_id(&s, 0);
+    let item = s.execute("frame.create", &json!({"rect": [40, 40, 100, 100]})).unwrap()["id"].as_u64().unwrap();
+    assert!(!s.doc().unwrap().selection.items.is_empty());
+    s.execute("layout.selectParentPage", &json!({"parent": parent, "page": 0})).unwrap();
+    assert_eq!(s.doc().unwrap().selection.parent_page, Some((SpreadId(parent), 0)));
+    assert!(s.doc().unwrap().selection.items.is_empty());
+    assert!(s.execute("layout.selectParentPage", &json!({"parent": parent, "page": 1})).is_err());
+    assert_eq!(s.doc().unwrap().selection.parent_page, Some((SpreadId(parent), 0)));
+    s.execute("layout.selectPage", &json!({"page": 1, "ids": [item]})).unwrap();
+    assert_eq!(s.doc().unwrap().selection.page, Some(0));
+    assert_eq!(s.doc().unwrap().selection.parent_page, None);
+    s.execute("layout.selectParentPage", &json!({"parent": null})).unwrap();
+    assert_eq!(s.doc().unwrap().selection, Default::default());
+}
+
+#[test]
+fn parent_resize_repeated_edits_keep_the_spine_and_other_page_objects() {
+    for binding in ["leftToRight", "rightToLeft"] {
+        for pi in 0..2 {
+            let mut s = Session::new();
+            s.execute("file.new", &json!({"width":400,"height":600,"pages":3})).unwrap();
+            s.execute("layout.documentSetup", &json!({"binding":binding})).unwrap();
+            let id = parent_id(&s, 0);
+            let before = s.doc().unwrap().doc.clone();
+            let spine = before.parents[0].spine_x();
+            let other = 1 - pi;
+            let ox = before.parents[0].pages[other].x + 80.0;
+            let object = parent_frame(&mut s, ox);
+            let old_object = bounds(&s, object);
+            for params in
+                [json!({"parent":id,"page":pi,"width":500}), json!({"parent":id,"page":pi,"height":700}), json!({"parent":id,"page":pi,"width":550})]
+            {
+                s.execute("layout.parentPageSize", &params).unwrap();
+                assert_eq!(s.doc().unwrap().doc.parents[0].spine_x(), spine, "spine moved: {binding}, page {pi}, {params}");
+                assert_eq!(bounds(&s, object), old_object, "other-page object moved: {binding}, page {pi}");
+            }
+        }
+    }
+}
+
+#[test]
+fn parent_resize_keeps_margin_and_column_inheritance_without_resizing_applied_pages() {
+    let mut s = Session::new();
+    s.execute("file.new", &json!({"width":400,"height":600,"pages":3})).unwrap();
+    let id = parent_id(&s, 0);
+    s.edit(|d, _| {
+        let pg = &mut Arc::make_mut(&mut d.parents[0]).pages[1];
+        pg.columns = Columns { count: 2, gutter: 20.0, positions: Some(vec![0.0, 100.0, 120.0, 328.0]) };
+        d.sync_parent_margins();
+        Arc::make_mut(&mut d.spreads[1]).pages[1].local_margins = true;
+        Ok(())
+    })
+    .unwrap();
+    let before = s.doc().unwrap().doc.clone();
+    s.execute("layout.parentPageSize", &json!({"parent":id,"page":1,"width":500})).unwrap();
+    let d = &s.doc().unwrap().doc;
+    assert_eq!(d.page(0).unwrap().columns, d.parents[0].pages[1].columns);
+    assert_eq!(d.page(0).unwrap().margins, d.parents[0].pages[1].margins);
+    assert_eq!((d.page(0).unwrap().width, d.page(0).unwrap().height), (400.0, 600.0));
+    assert_eq!(d.page(2).unwrap(), before.page(2).unwrap());
+    d.check().unwrap();
+    s.execute("layout.pages.applyParent", &json!({"pages":[0],"parent":null})).unwrap();
+    s.execute("layout.pages.applyParent", &json!({"pages":[0],"parent":id})).unwrap();
+    assert_eq!((s.doc().unwrap().doc.page(0).unwrap().width, s.doc().unwrap().doc.page(0).unwrap().height), (400.0, 600.0));
+}
+
+#[test]
+fn parent_resize_facing_sizes_survive_idml_in_both_bindings() {
+    for binding in ["leftToRight", "rightToLeft"] {
+        let mut s = Session::new();
+        s.execute("file.new", &json!({"width":400,"height":600,"pages":3,"facingPages":true})).unwrap();
+        s.execute("layout.documentSetup", &json!({"binding":binding})).unwrap();
+        let id = parent_id(&s, 0);
+        for (pi, w, h) in [(0, 500, 700), (1, 300, 550)] {
+            s.execute("layout.parentPageSize", &json!({"parent":id,"page":pi,"width":w,"height":h})).unwrap();
+        }
+        let d = &s.doc().unwrap().doc;
+        let reopened = designcraft_idml::import_idml(&designcraft_idml::export_idml(d)).unwrap();
+        assert_eq!(reopened.settings.right_to_left_binding, d.settings.right_to_left_binding);
+        for page in &d.parents[0].pages {
+            let restored = reopened.parents[0].pages.iter().find(|p| p.side == page.side).unwrap();
+            assert_eq!((restored.width, restored.height), (page.width, page.height), "{binding}, {:?}", page.side);
+        }
+        for abs in 0..d.page_count() {
+            let (old, new) = (d.page(abs).unwrap(), reopened.page(abs).unwrap());
+            assert_eq!((new.width, new.height), (old.width, old.height));
+        }
+        reopened.check().unwrap();
+    }
+}
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -1651,3 +1651,59 @@ fn a_line_fits_when_its_baseline_does() {
     assert!(!overset(&mut s, 78.5), "the line is set; its descenders hang below the frame");
     assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line");
 }
+
+#[test]
+fn parent_resize_tool_previews_cancels_commits_and_undoes_on_the_selected_spread() {
+    use designcraft_tools::{Mods, Overlay, PointerEvent, PointerKind};
+    let mut s = Session::new();
+    s.execute("file.new", &json!({"width":400,"height":600,"pages":3,"facingPages":true})).unwrap();
+    s.execute("layout.parents.new", &json!({"prefix":"B"})).unwrap();
+    s.execute("layout.parents.edit", &json!({"on":true})).unwrap();
+    s.set_tool("page");
+    let v = ViewInfo::at_zoom(1.0);
+    let before = s.doc().unwrap().doc.clone();
+    for si in 0..2 {
+        for pi in 0..2 {
+            let sr = designcraft_doc::SpreadRef::Parent(si);
+            let rect = |s: &Session| s.layout().spread_page_rect(&s.doc().unwrap().doc, sr, pi).unwrap();
+            let c = rect(&s).center();
+            s.pointer(&PointerEvent::new(PointerKind::Down, c.x, c.y), v).unwrap();
+            s.pointer(&PointerEvent::new(PointerKind::Up, c.x, c.y), v).unwrap();
+            assert_eq!(s.doc().unwrap().selection.parent_page, Some((before.parents[si].id, pi)));
+            assert_eq!(s.doc().unwrap().selection.page, None);
+            assert!(s.overlays(v).iter().any(|o| matches!(o,Overlay::PageHandles(r) if *r==rect(&s))));
+            for alt in [false, true] {
+                let original = s.doc().unwrap().doc.clone();
+                let undo = s.doc().unwrap().history.undo.len();
+                let r = rect(&s);
+                let p = designcraft_geom::Point::new(r.x1, r.center().y);
+                let ev = |kind, x| PointerEvent::new(kind, x, p.y).with_mods(Mods { alt, ..Default::default() });
+                s.pointer(&ev(PointerKind::Down, p.x), v).unwrap();
+                s.pointer(&ev(PointerKind::Drag, p.x + 80.0), v).unwrap();
+                assert_eq!(s.doc().unwrap().doc.parents[si].pages[pi].width, 480.0);
+                assert_eq!(s.doc().unwrap().doc.spreads, before.spreads);
+                assert_eq!(s.doc().unwrap().doc.parents[1 - si], before.parents[1 - si]);
+                s.pointer(&ev(PointerKind::Up, p.x + 80.0), v).unwrap();
+                if alt {
+                    assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
+                    assert_eq!(s.doc().unwrap().doc.parents[si].pages[pi].width, 480.0);
+                    s.execute("edit.undo", &json!({})).unwrap();
+                    assert_eq!(s.doc().unwrap().doc.parents, original.parents);
+                    s.execute("edit.redo", &json!({})).unwrap();
+                    assert_eq!(s.doc().unwrap().doc.parents[si].pages[pi].width, 480.0);
+                    s.execute("edit.undo", &json!({})).unwrap();
+                } else {
+                    assert!(std::sync::Arc::ptr_eq(&s.doc().unwrap().doc, &original));
+                    assert_eq!(s.doc().unwrap().history.undo.len(), undo);
+                }
+            }
+            let c = rect(&s).center();
+            s.pointer(&PointerEvent::new(PointerKind::DoubleClick, c.x, c.y), v).unwrap();
+            let req = std::mem::take(&mut s.ui_requests);
+            assert!(
+                matches!(req.last(),Some(crate::UiRequest::Dialog{id,params}) if id=="cmd:layout.parentPageSize" && params["parent"]==before.parents[si].id.0 && params["page"]==pi)
+            );
+            s.doc().unwrap().doc.check().unwrap();
+        }
+    }
+}
diff --git a/crates/tools/src/layout.rs b/crates/tools/src/layout.rs
--- a/crates/tools/src/layout.rs
+++ b/crates/tools/src/layout.rs
@@ -1,9 +1,10 @@
 //! Where spreads sit on the canvas (the pasteboard): stacked vertically, spines aligned at x = 0,
 //! parents shown on their own canvas when a parent spread is being edited.
 
-use designcraft_doc::{Document, SpreadRef};
+use designcraft_doc::{Document, Selection, SpreadRef};
 use designcraft_geom::{Affine, Point, Rect, Vec2};
 use serde::Serialize;
+use serde_json::{Value, json};
 
 #[derive(Clone, Copy, Debug, PartialEq, Serialize)]
 pub struct SpreadSlot {
@@ -100,6 +101,31 @@ impl CanvasLayout {
         (m * Point::new(d.x, d.y)) - (m * Point::ORIGIN)
     }
 
+    /// The Page tool selection on the canvas currently shown.
+    pub fn selected_page(&self, doc: &Document, selection: &Selection) -> Option<(SpreadRef, usize)> {
+        let (sr, pi) = if self.slots.first().is_some_and(|s| matches!(s.spread, SpreadRef::Parent(_))) {
+            let (id, pi) = selection.parent_page?;
+            (SpreadRef::Parent(doc.parent_index(id)?), pi)
+        } else {
+            let (si, pi) = doc.page_loc(selection.page?)?;
+            (SpreadRef::Doc(si), pi)
+        };
+        self.slot(sr)?;
+        doc.spread(sr)?.pages.get(pi)?;
+        Some((sr, pi))
+    }
+
+    /// A page on the visible canvas, including parent pages.
+    pub fn page_at(&self, doc: &Document, p: Point) -> Option<(SpreadRef, usize)> {
+        self.slots
+            .iter()
+            .find_map(|slot| doc.spread(slot.spread)?.pages.iter().position(|pg| pg.bounds().contains(slot.to_spread(p))).map(|pi| (slot.spread, pi)))
+    }
+
+    pub fn spread_page_rect(&self, doc: &Document, sr: SpreadRef, pi: usize) -> Option<Rect> {
+        Some(self.slot(sr)?.xf.transform_rect_bbox(doc.spread(sr)?.pages.get(pi)?.bounds()))
+    }
+
     /// Canvas rect of a document page.
     pub fn page_rect(&self, doc: &Document, abs: usize) -> Option<Rect> {
         let (si, pi) = doc.page_loc(abs)?;
@@ -107,6 +133,24 @@ impl CanvasLayout {
     }
 }
 
+/// The same resize contract for panels, handles and the exact-size dialog.
+pub fn page_size_command(doc: &Document, sr: SpreadRef, page: usize, mut params: Value) -> Option<(&'static str, Value)> {
+    let sp = doc.spread(sr)?;
+    sp.pages.get(page)?;
+    let command = match sr {
+        SpreadRef::Doc(si) => {
+            params["pages"] = json!([doc.first_page_of_spread(si) + page + 1]);
+            "layout.pageSize"
+        }
+        SpreadRef::Parent(_) => {
+            params["parent"] = json!(sp.id.0);
+            params["page"] = json!(page);
+            "layout.parentPageSize"
+        }
+    };
+    Some((command, params))
+}
+
 fn dist_y(r: Rect, y: f64) -> f64 {
     if y < r.y0 - SPREAD_GAP / 2.0 {
         r.y0 - y
diff --git a/crates/tools/src/nav.rs b/crates/tools/src/nav.rs
--- a/crates/tools/src/nav.rs
+++ b/crates/tools/src/nav.rs
@@ -249,7 +249,9 @@ fn liquid_control_points(cx: &ToolContext, r: designcraft_geom::Rect) -> [(Liqui
 /// A page handle being dragged.
 #[derive(Clone, Copy, Debug)]
 struct PageDrag {
-    /// Absolute page index.
+    spread: designcraft_doc::SpreadRef,
+    parent: Option<designcraft_doc::SpreadId>,
+    /// Page index within the spread.
     page: usize,
     /// Reference-point index of the handle (0–8, row-major; 4 is the centre and has none).
     handle: usize,
@@ -297,6 +299,9 @@ impl PageTool {
     fn liquid_target(
         cx: &ToolContext,
     ) -> Option<(designcraft_doc::ItemId, designcraft_doc::ObjectLiquid, designcraft_geom::Rect, designcraft_geom::Rect)> {
+        if cx.layout.slots.first().is_some_and(|s| matches!(s.spread, designcraft_doc::SpreadRef::Parent(_))) {
+            return None;
+        }
         let page = cx.selection.page?;
         let [id] = cx.selection.items.as_slice() else { return None };
         let (si, _) = cx.doc.page_loc(page)?;
@@ -316,17 +321,16 @@ impl PageTool {
     }
 
     /// The selected page's handle under `p`.
-    fn handle_at(&self, cx: &ToolContext, p: Point) -> Option<(usize, usize)> {
-        let page = cx.selection.page?;
-        let r = cx.layout.page_rect(cx.doc, page)?;
-        page_handles(r).find(|(_, h)| (h.x - p.x).abs() <= cx.tol(5.0) && (h.y - p.y).abs() <= cx.tol(5.0)).map(|(i, _)| (page, i))
+    fn handle_at(&self, cx: &ToolContext, p: Point) -> Option<(designcraft_doc::SpreadRef, usize, usize)> {
+        let (sr, page) = cx.layout.selected_page(cx.doc, cx.selection)?;
+        let r = cx.layout.spread_page_rect(cx.doc, sr, page)?;
+        page_handles(r).find(|(_, h)| (h.x - p.x).abs() <= cx.tol(5.0) && (h.y - p.y).abs() <= cx.tol(5.0)).map(|(i, _)| (sr, page, i))
     }
 
     /// The new page size for the drag at `p`: the handle's edges follow the pointer (Shift: in
     /// proportion).
     fn size_at(cx: &ToolContext, d: &PageDrag, p: Point, shift: bool) -> (f64, f64) {
-        let si = cx.doc.page_loc(d.page).map_or(0, |l| l.0);
-        let v = cx.layout.delta_to_spread(designcraft_doc::SpreadRef::Doc(si), p - d.start);
+        let v = cx.layout.delta_to_spread(d.spread, p - d.start);
         let (col, row) = (d.handle % 3, d.handle / 3);
         let dx = match col {
             0 => -v.x,
@@ -361,10 +365,21 @@ impl Tool for PageTool {
     fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
         match ev.kind {
             PointerKind::Down => {
-                if let Some((page, handle)) = self.handle_at(cx, ev.pos) {
-                    let Some((si, pi)) = cx.doc.page_loc(page) else { return vec![] };
-                    let pg = &cx.doc.spreads[si].pages[pi];
-                    self.drag = Some(PageDrag { page, handle, start: ev.pos, cur: ev.pos, size: (pg.width, pg.height), alt: ev.mods.alt });
+                if let Some((spread, page, handle)) = self.handle_at(cx, ev.pos) {
+                    let Some(pg) = cx.doc.spread(spread).and_then(|sp| sp.pages.get(page)) else { return vec![] };
+                    self.drag = Some(PageDrag {
+                        spread,
+                        parent: match spread {
+                            designcraft_doc::SpreadRef::Parent(si) => cx.doc.parents.get(si).map(|sp| sp.id),
+                            _ => None,
+                        },
+                        page,
+                        handle,
+                        start: ev.pos,
+                        cur: ev.pos,
+                        size: (pg.width, pg.height),
+                        alt: ev.mods.alt,
+                    });
                     return vec![Action::Begin("Resize Page".into())];
                 }
                 // An object-based page's object: a click on a pin or lock toggles it.
@@ -373,7 +388,18 @@ impl Tool for PageTool {
                 }
                 // A click selects the page under the pointer (none on the pasteboard), and on an
                 // object-based page the object clicked too.
-                let page = (0..cx.doc.page_count()).find(|p| cx.layout.page_rect(cx.doc, *p).is_some_and(|r| r.contains(ev.pos)));
+                let target = cx.layout.page_at(cx.doc, ev.pos);
+                if let Some((designcraft_doc::SpreadRef::Parent(si), page)) = target {
+                    let Some(sp) = cx.doc.parents.get(si) else { return vec![] };
+                    return vec![Action::Exec("layout.selectParentPage".into(), json!({"parent": sp.id.0, "page": page}))];
+                }
+                if cx.layout.slots.first().is_some_and(|s| matches!(s.spread, designcraft_doc::SpreadRef::Parent(_))) {
+                    return vec![Action::Exec("layout.selectParentPage".into(), json!({"parent": null}))];
+                }
+                let page = target.and_then(|(sr, pi)| match sr {
+                    designcraft_doc::SpreadRef::Doc(si) => Some(cx.doc.first_page_of_spread(si) + pi),
+                    _ => None,
+                });
                 let object = page
                     .filter(|p| object_based(cx.doc, *p))
                     .and_then(|p| cx.hit(ev.pos).map(|(_, id)| id).filter(|id| cx.doc.page_of_item(*id) == Some(p)));
@@ -387,9 +413,21 @@ impl Tool for PageTool {
                 let Some(d) = self.drag.as_mut() else { return vec![] };
                 d.cur = ev.pos;
                 d.alt |= ev.mods.alt;
-                let d = *d;
+                let mut d = *d;
+                if let Some(id) = d.parent {
+                    let Some(si) = cx.doc.parent_index(id) else {
+                        self.drag = None;
+                        return vec![Action::Cancel];
+                    };
+                    d.spread = designcraft_doc::SpreadRef::Parent(si);
+                }
                 let (w, h) = Self::size_at(cx, &d, ev.pos, ev.mods.shift);
-                vec![Action::Preview("layout.pageSize".into(), json!({"pages": [d.page + 1], "width": w, "height": h, "anchor": 8 - d.handle}))]
+                let Some((cmd, params)) =
+                    crate::layout::page_size_command(cx.doc, d.spread, d.page, json!({"width": w, "height": h, "anchor": 8 - d.handle}))
+                else {
+                    return vec![];
+                };
+                vec![Action::Preview(cmd.into(), params)]
             }
             PointerKind::Up => match self.drag.take() {
                 // Alt-drag resizes; a plain drag previews the liquid rule and springs back.
@@ -399,12 +437,12 @@ impl Tool for PageTool {
             },
             PointerKind::DoubleClick => {
                 // Exact sizes: the Page Size dialog for the page.
-                let Some(page) = (0..cx.doc.page_count()).find(|p| cx.layout.page_rect(cx.doc, *p).is_some_and(|r| r.contains(ev.pos))) else {
+                let Some((sr, pi)) = cx.layout.page_at(cx.doc, ev.pos) else { return vec![] };
+                let Some(pg) = cx.doc.spread(sr).and_then(|sp| sp.pages.get(pi)) else { return vec![] };
+                let Some((cmd, params)) = crate::layout::page_size_command(cx.doc, sr, pi, json!({"width": pg.width, "height": pg.height})) else {
                     return vec![];
                 };
-                let Some((si, pi)) = cx.doc.page_loc(page) else { return vec![] };
-                let pg = &cx.doc.spreads[si].pages[pi];
-                vec![Action::Dialog("cmd:layout.pageSize".into(), json!({"pages": [page + 1], "width": pg.width, "height": pg.height}))]
+                vec![Action::Dialog(format!("cmd:{cmd}"), params)]
             }
             PointerKind::Move => {
                 self.hover = Self::control_at(cx, ev.pos).map(|(_, _, c)| (c, ev.pos));
@@ -413,10 +451,9 @@ impl Tool for PageTool {
         }
     }
     fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
-        let Some(page) = cx.selection.page else { return vec![] };
-        let Some(r) = cx.layout.page_rect(cx.doc, page) else { return vec![] };
-        let Some((si, pi)) = cx.doc.page_loc(page) else { return vec![] };
-        let pg = &cx.doc.spreads[si].pages[pi];
+        let Some((sr, pi)) = cx.layout.selected_page(cx.doc, cx.selection) else { return vec![] };
+        let Some(r) = cx.layout.spread_page_rect(cx.doc, sr, pi) else { return vec![] };
+        let Some(pg) = cx.doc.spread(sr).and_then(|sp| sp.pages.get(pi)) else { return vec![] };
         let mut v = vec![Overlay::PageHandles(r), Overlay::Tag { p: Point::new(r.x0, r.y0 - cx.tol(6.0)), text: rule_label(pg.liquid).into() }];
         if let Some((_, l, object, page)) = Self::liquid_target(cx) {
             v.push(Overlay::LiquidPins {
@@ -445,7 +482,7 @@ impl Tool for PageTool {
         if self.drag.is_none() && Self::control_at(cx, p).is_some() {
             return Cursor::Hand;
         }
-        let handle = self.drag.map(|d| d.handle).or_else(|| self.handle_at(cx, p).map(|(_, h)| h));
+        let handle = self.drag.map(|d| d.handle).or_else(|| self.handle_at(cx, p).map(|(_, _, h)| h));
         match handle {
             Some(1 | 7) => Cursor::ResizeV,
             Some(3 | 5) => Cursor::ResizeH,
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
@@ -1936,3 +1936,38 @@ 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);
 }
+
+#[test]
+fn parent_resize_page_tool_targets_the_visible_parent() {
+    let d = Document::new(&NewDocument { width: 400.0, height: 600.0, pages: 3, facing_pages: true, ..Default::default() });
+    let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, true));
+    let cx = ctx(&d, &s, &c, &l);
+    let p = l.to_canvas(SpreadRef::Parent(0), d.parents[0].pages[0].bounds().center());
+    let mut t = create("page");
+    let a = t.pointer(&cx, &PointerEvent::new(PointerKind::DoubleClick, p.x, p.y));
+    assert_eq!(
+        a,
+        vec![Action::Dialog(
+            "cmd:layout.parentPageSize".into(),
+            serde_json::json!({"parent": d.parents[0].id.0, "page": 0, "width": 400.0, "height": 600.0})
+        )],
+        "double click must target the visible parent instead of document page zero"
+    );
+}
+
+#[test]
+fn parent_resize_selection_tracks_ids_and_never_crosses_canvas_modes() {
+    let mut d = Document::new(&NewDocument::default());
+    let second = d.add_parent("B", "Parent", 1, 12.0, designcraft_doc::Margins::uniform(36.0));
+    let selection = Selection { page: Some(0), parent_page: Some((second, 1)), ..Default::default() };
+    let l = CanvasLayout::new(&d, true);
+    assert_eq!(l.selected_page(&d, &selection), Some((SpreadRef::Parent(1), 1)));
+    d.parents.swap(0, 1);
+    let l = CanvasLayout::new(&d, true);
+    assert_eq!(l.selected_page(&d, &selection), Some((SpreadRef::Parent(0), 1)));
+    assert_eq!(CanvasLayout::new(&d, false).selected_page(&d, &selection), Some((SpreadRef::Doc(0), 0)));
+    d.parents.remove(0);
+    assert_eq!(CanvasLayout::new(&d, true).selected_page(&d, &selection), None);
+    let selection = Selection { page: Some(0), ..Default::default() };
+    assert_eq!(CanvasLayout::new(&d, true).selected_page(&d, &selection), None);
+}
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
@@ -198,7 +198,7 @@ pub fn current_page(app: &DesignApp) -> Option<usize> {
     let layout = CanvasLayout::new(&st.doc, st.editing_parents);
     let i = current_slot(app, &layout)?;
     let slot = layout.slots.get(i)?;
-    let SpreadRef::Doc(si) = slot.spread else { return Some(0) };
+    let SpreadRef::Doc(si) = slot.spread else { return None };
     let v = app.view()?;
     let r = app.canvas_rect?;
     let c = slot.to_spread(Point::new(v.origin.x + r.width() as f64 / 2.0 / v.zoom, slot.bounds.center().y));
@@ -206,6 +206,16 @@ pub fn current_page(app: &DesignApp) -> Option<usize> {
     Some(st.doc.first_page_of_spread(si) + pi)
 }
 
+/// The Page tool selection, or the page in view, without crossing canvas modes.
+pub fn page_target(app: &DesignApp) -> Option<(SpreadRef, usize)> {
+    let st = app.session.active()?;
+    let selected = CanvasLayout::new(&st.doc, st.editing_parents).selected_page(&st.doc, &st.selection);
+    if st.editing_parents && st.selection.parent_page.is_some() {
+        return selected;
+    }
+    selected.or_else(|| current_spread_page(app))
+}
+
 /// The spread (document or parent) under the view centre and the page index within it: where
 /// commands that act "on the page I am working on" should go.
 pub fn current_spread_page(app: &DesignApp) -> Option<(SpreadRef, usize)> {
@@ -220,22 +230,10 @@ pub fn current_spread_page(app: &DesignApp) -> Option<(SpreadRef, usize)> {
     Some((slot.spread, pg))
 }
 
-/// While parent spreads are being edited: (parent spread index, page index within it) under the
-/// view centre. `None` when document pages are shown.
+/// The selected parent page, or the parent page under the view centre.
 pub fn current_parent_page(app: &DesignApp) -> Option<(usize, usize)> {
-    let st = app.session.active()?;
-    if !st.editing_parents {
-        return None;
-    }
-    let layout = CanvasLayout::new(&st.doc, true);
-    let i = current_slot(app, &layout)?;
-    let slot = layout.slots.get(i)?;
-    let SpreadRef::Parent(pi) = slot.spread else { return None };
-    let v = app.view()?;
-    let r = app.canvas_rect?;
-    let c = slot.to_spread(Point::new(v.origin.x + r.width() as f64 / 2.0 / v.zoom, slot.bounds.center().y));
-    let qi = st.doc.parents.get(pi)?.page_at_x(c.x).unwrap_or(0);
-    Some((pi, qi))
+    let (SpreadRef::Parent(si), pi) = page_target(app)? else { return None };
+    Some((si, pi))
 }
 
 /// Scroll so absolute page `abs` is centred (keeps zoom).
diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs
--- a/crates/ui-egui/src/chrome.rs
+++ b/crates/ui-egui/src/chrome.rs
@@ -484,12 +484,20 @@ fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
     let lang = app.ui.language.clone();
     let Some(st) = app.session.active() else { return };
     let units = st.doc.settings.horizontal_units;
-    let Some(page) = st.selection.page.or_else(|| crate::canvas::current_page(app)) else { return };
+    let Some((spread, page)) = crate::canvas::page_target(app) else { return };
     let Some(st) = app.session.active() else { return };
     let d = st.doc.clone();
-    let Some((si, pi)) = d.page_loc(page) else { return };
-    let pg = d.spreads[si].pages[pi].clone();
-    let n = page + 1;
+    let Some(sp) = d.spread(spread) else { return };
+    let Some(pg) = sp.pages.get(page).cloned() else { return };
+    let n = match spread {
+        designcraft_doc::SpreadRef::Doc(si) => Some(d.first_page_of_spread(si) + page + 1),
+        _ => None,
+    };
+    let resize = |app: &mut DesignApp, params| {
+        if let Some((cmd, params)) = designcraft_tools::layout::page_size_command(&d, spread, page, params) {
+            let _ = app.run(cmd, params);
+        }
+    };
     // Reference point: W/H edits keep it in place.
     let (r, _) = ui.allocate_exact_size(vec2(30.0, 44.0), Sense::hover());
     if let Some(p) = panels::properties::ref_point_proxy(ui, egui::Rect::from_center_size(r.center(), vec2(27.0, 27.0)), app.ui.ref_point) {
@@ -498,21 +506,21 @@ fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
     let anchor = app.ui.ref_point.min(8);
     let (fx, fy) = ((anchor % 3) as f64 / 2.0, (anchor / 3) as f64 / 2.0);
     // The reference point's position: from the spine across, from the top down.
-    let (x, y) = (pg.x - d.spreads[si].spine_x() + fx * pg.width, fy * pg.height);
+    let (x, y) = (pg.x - sp.spine_x() + fx * pg.width, fy * pg.height);
     egui::Grid::new("ctl_page_xywh").num_columns(4).spacing(vec2(4.0, 4.0)).show(ui, |ui| {
         caption(ui, crate::i18n::tr(&lang, "X:"));
         // Pages don't move within their spread yet.
         ui.add_enabled_ui(false, |ui| measure(ui, "cpx", Some(x), units, 64.0));
         caption(ui, crate::i18n::tr(&lang, "W:"));
         if let Some(v) = measure(ui, "cpw", Some(pg.width), units, 64.0) {
-            let _ = app.run("layout.pageSize", json!({"pages": [n], "width": v, "anchor": anchor}));
+            resize(app, json!({"width": v, "anchor": anchor}));
         }
         ui.end_row();
         caption(ui, crate::i18n::tr(&lang, "Y:"));
         ui.add_enabled_ui(false, |ui| measure(ui, "cpy", Some(y), units, 64.0));
         caption(ui, crate::i18n::tr(&lang, "H:"));
         if let Some(v) = measure(ui, "cph", Some(pg.height), units, 64.0) {
-            let _ = app.run("layout.pageSize", json!({"pages": [n], "height": v, "anchor": anchor}));
+            resize(app, json!({"height": v, "anchor": anchor}));
         }
         ui.end_row();
     });
@@ -527,7 +535,7 @@ fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
                 for (name, w, h) in panels::properties::PAGE_PRESETS {
                     if ui.selectable_label(cur == *name, crate::rtl::widget(ui, crate::i18n::tr(&lang, name))).clicked() {
                         let (w, h) = if landscape { (h, w) } else { (w, h) };
-                        let _ = app.run("layout.pageSize", json!({"pages": [n], "width": w, "height": h, "anchor": anchor}));
+                        resize(app, json!({"width": w, "height": h, "anchor": anchor}));
                     }
                 }
             },
@@ -535,20 +543,23 @@ fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
         ui.horizontal(|ui| {
             for (icon, tip, want) in [("orient-portrait", "Portrait", false), ("orient-landscape", "Landscape", true)] {
                 if crate::widgets::icon_toggle(ui, icon, landscape == want, crate::i18n::tr(&lang, tip)).clicked() && landscape != want {
-                    let _ = app.run("layout.pageSize", json!({"pages": [n], "width": pg.height, "height": pg.width, "anchor": anchor}));
+                    resize(app, json!({"width": pg.height, "height": pg.width, "anchor": anchor}));
                 }
             }
         });
     });
     vsep(ui);
-    ui.vertical(|ui| {
+    ui.add_enabled_ui(n.is_some(), |ui| {
         caption(ui, crate::i18n::tr(&lang, "Liquid Page Rule:"));
         let label = panels::interactive::liquid_rule_label(pg.liquid);
         egui::ComboBox::from_id_salt("ctl_page_rule").selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).width(150.0).show_ui(
             ui,
             |ui| {
                 for (r, l) in panels::interactive::LIQUID_RULES {
-                    if ui.selectable_label(pg.liquid == r, crate::rtl::widget(ui, crate::i18n::tr(&lang, l))).clicked() && pg.liquid != r {
+                    if ui.selectable_label(pg.liquid == r, crate::rtl::widget(ui, crate::i18n::tr(&lang, l))).clicked()
+                        && pg.liquid != r
+                        && let Some(n) = n
+                    {
                         let _ = app.run("liquid.pageRule", json!({"rule": r, "pages": [n]}));
                     }
                 }
@@ -556,9 +567,11 @@ fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
         );
     });
     vsep(ui);
-    ui.vertical(|ui| {
+    ui.add_enabled_ui(n.is_some(), |ui| {
         let mut overlay = pg.show_parent_overlay;
-        if ui.checkbox(&mut overlay, crate::rtl::widget(ui, crate::i18n::tr(&lang, "Show Parent Page Overlay"))).changed() {
+        if ui.checkbox(&mut overlay, crate::rtl::widget(ui, crate::i18n::tr(&lang, "Show Parent Page Overlay"))).changed()
+            && let Some(n) = n
+        {
             let _ = app.run("layout.pageOptions", json!({"pages": [n], "showParentOverlay": overlay}));
         }
     });
@@ -1304,6 +1317,107 @@ mod tests {
         assert!(short.min.x - first.min.x <= 2.0 * super::DOC_TAB_MAX_W, "the last tab {short:?} starts too far right");
     }
 
+    #[test]
+    fn parent_resize_properties_changes_the_parent_not_document_page_zero() {
+        parent_resize_panel(true);
+    }
+
+    #[test]
+    fn parent_resize_control_changes_the_parent_not_document_page_zero() {
+        parent_resize_panel(false);
+    }
+
+    fn parent_resize_panel(properties: bool) {
+        use egui_kittest::Harness;
+        {
+            let mut app = app();
+            app.run("file.new", serde_json::json!({"width": 400, "height": 600, "pages": 3, "facingPages": true})).unwrap();
+            app.run("layout.parents.new", serde_json::json!({"prefix": "B"})).unwrap();
+            app.run("layout.pages.applyParent", serde_json::json!({"pages": [0], "parent": "A"})).unwrap();
+            app.run("layout.parents.edit", serde_json::json!({"on": true, "parent": "A"})).unwrap();
+            app.session.set_tool("page");
+            app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(800.0, 600.0)));
+            *app.view_mut().unwrap() = crate::View { zoom: 1.0, origin: designcraft_geom::Point::new(-600.0, 0.0), fitted: true, rotation: 0 };
+            let before = app.session.doc().unwrap().doc.clone();
+            let mut h = Harness::builder().with_size(vec2(1600.0, 900.0)).build_ui_state(
+                move |ui, app: &mut DesignApp| {
+                    let id = egui::Id::new("parent_resize_fonts");
+                    if ui.data(|d| d.get_temp::<bool>(id)).is_none() {
+                        crate::theme::install_fonts(ui.ctx(), "");
+                        ui.data_mut(|d| d.insert_temp(id, true));
+                        return;
+                    }
+                    if properties {
+                        panels::properties::show(app, ui);
+                    } else {
+                        control_page(app, ui);
+                    }
+                },
+                app,
+            );
+            h.run_steps(3);
+            edit_parent_resize_field(&mut h, "33p4", "500 pt");
+            let d = &h.state().session.doc().unwrap().doc;
+            assert_eq!(d.parents[0].pages[0].width, 500.0, "the parent page in view must change");
+            assert_eq!(d.spreads, before.spreads);
+            assert_eq!(d.parents[1], before.parents[1]);
+            let b = before.parents[1].id.0;
+            h.state_mut().run("layout.selectParentPage", json!({"parent": b, "page": 1})).unwrap();
+            h.run_steps(3);
+            assert_eq!(crate::canvas::current_parent_page(h.state()), Some((1, 1)));
+            if properties {
+                h.state_mut().session.set_tool("selection");
+            }
+            h.run_steps(3);
+            edit_parent_resize_field(&mut h, "50p0", "700 pt");
+            edit_parent_resize_field(&mut h, "33p4", "560 pt");
+            let d = &h.state().session.doc().unwrap().doc;
+            assert_eq!((d.parents[1].pages[1].width, d.parents[1].pages[1].height), (560.0, 700.0));
+            assert_eq!(d.parents[1].pages[0].width, 400.0);
+            assert_eq!(d.parents[0].pages[0].width, 500.0);
+            assert_eq!(d.spreads, before.spreads);
+            let parents = d.parents.clone();
+            if properties {
+                let saved = d.clone();
+                h.get_by_label("Back").click();
+                h.run_steps(3);
+                assert!(!h.state().session.doc().unwrap().editing_parents, "Back returns to document properties");
+                assert_eq!(h.state().session.tool_id(), "selection");
+                assert!(h.query_by_label("Back").is_none(), "the next frame must leave the page properties screen");
+                assert!(std::sync::Arc::ptr_eq(&saved, &h.state().session.doc().unwrap().doc));
+            }
+            h.state_mut().run("layout.parents.edit", json!({"on": false})).unwrap();
+            h.state_mut().session.set_tool("page");
+            h.state_mut().run("layout.selectPage", json!({"page": 1})).unwrap();
+            h.run_steps(3);
+            edit_parent_resize_field(&mut h, "33p4", "530 pt");
+            let d = &h.state().session.doc().unwrap().doc;
+            assert_eq!(d.spreads[0].pages[0].width, 530.0);
+            assert_eq!(d.parents, parents);
+            if properties {
+                let saved = d.clone();
+                h.get_by_label("Back").click();
+                h.run_steps(3);
+                assert_eq!(h.state().session.tool_id(), "selection");
+                assert!(h.query_by_label("Back").is_none());
+                assert_eq!(h.state().session.doc().unwrap().selection.page, None);
+                assert!(std::sync::Arc::ptr_eq(&saved, &h.state().session.doc().unwrap().doc));
+            }
+        }
+    }
+
+    fn edit_parent_resize_field(h: &mut egui_kittest::Harness<'_, DesignApp>, value: &str, input: &str) {
+        h.query_all_by(|n| n.role() == egui::accesskit::Role::TextInput && n.value().as_deref() == Some(value))
+            .next()
+            .expect("page size field")
+            .click();
+        h.run_steps(2);
+        h.event(egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND });
+        h.event(egui::Event::Text(input.into()));
+        h.key_press(egui::Key::Enter);
+        h.run_steps(3);
+    }
+
     #[test]
     fn the_page_tool_has_its_own_control_panel() {
         // Chris Cicchino's Page Tool and Liquid Layout video (1:02): with the Page tool, the
diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs
--- a/crates/ui-egui/src/control.rs
+++ b/crates/ui-egui/src/control.rs
@@ -413,7 +413,12 @@ fn handle_request(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest
         }
         "ui.render" => {
             let Some(st) = app.session.active() else { return err("no document") };
-            let page = match page_index(p, crate::canvas::current_page(app).unwrap_or(0), st.doc.page_count()) {
+            let current = match crate::canvas::current_page(app) {
+                Some(page) => page,
+                None if p.get("page").is_some() => 0,
+                None => return err("select a document page or give an explicit page"),
+            };
+            let page = match page_index(p, current, st.doc.page_count()) {
                 Ok(page) => page,
                 Err(e) => return err(e),
             };
@@ -683,6 +688,20 @@ mod automation_contract_tests {
         assert_eq!(app.ui.palette.as_deref(), Some("field"));
         assert_eq!(app.session.execute("story.get", &json!({"story":f["story"]})).unwrap()["text"], "Hello");
     }
+    #[test]
+    fn parent_resize_mode_does_not_default_document_actions_to_page_zero() {
+        let mut app = app();
+        let ctx = egui::Context::default();
+        app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0)));
+        app.run("layout.parents.edit", json!({"on":true,"parent":"A"})).unwrap();
+        let before = app.session.doc().unwrap().doc.clone();
+        assert!(app.run("app.deletePage", json!({})).is_err());
+        assert!(app.run("app.duplicateSpread", json!({})).is_err());
+        assert_eq!(call(&mut app, &ctx, "ui.render", json!({"scale":0.1}))["ok"], false);
+        assert_eq!(call(&mut app, &ctx, "ui.render", json!({"page":0,"scale":0.1}))["ok"], true);
+        assert_eq!(app.session.doc().unwrap().doc, before);
+    }
+
     #[test]
     fn control_pages_validate_before_writer_and_preserve_omission() {
         let mut app = app();
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -7695,6 +7695,36 @@ mod tests {
         }
     }
 
+    #[test]
+    fn parent_resize_exact_dialog_keeps_its_target_and_rejects_invalid_input() {
+        use designcraft_tools::{PointerEvent, PointerKind};
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+        app.run("file.new", json!({"width":400,"height":600,"pages":3,"facingPages":true})).unwrap();
+        app.run("layout.parents.new", json!({"prefix":"B"})).unwrap();
+        app.run("layout.parents.edit", json!({"on":true})).unwrap();
+        app.session.set_tool("page");
+        let before = app.session.doc().unwrap().doc.clone();
+        let sr = designcraft_doc::SpreadRef::Parent(1);
+        let p = app.session.layout().spread_page_rect(&before, sr, 1).unwrap().center();
+        app.session.pointer(&PointerEvent::new(PointerKind::DoubleClick, p.x, p.y), designcraft_engine::ViewInfo::at_zoom(1.0)).unwrap();
+        let request = app.session.ui_requests.pop().unwrap();
+        let designcraft_engine::UiRequest::Dialog { id, params } = request else { panic!("expected exact-size dialog") };
+        app.ui.dialog = Some(Dialog::new(&id, params));
+        let d = app.ui.dialog.as_mut().unwrap();
+        d.fields.insert("width".into(), json!("530"));
+        d.fields.insert("height".into(), json!("710"));
+        confirm(&mut app).unwrap();
+        let doc = &app.session.doc().unwrap().doc;
+        assert_eq!((doc.parents[1].pages[1].width, doc.parents[1].pages[1].height), (530.0, 710.0));
+        assert_eq!(doc.parents[0], before.parents[0]);
+        assert_eq!(doc.spreads, before.spreads);
+        let saved = doc.clone();
+        app.ui.dialog = Some(Dialog::new("cmd:layout.parentPageSize", json!({"parent":saved.parents[1].id.0,"page":1,"width":"NaN"})));
+        assert!(confirm(&mut app).is_err());
+        assert!(app.ui.dialog.is_some());
+        assert!(std::sync::Arc::ptr_eq(&saved, &app.session.doc().unwrap().doc));
+    }
+
     fn command_app(id: &str, fields: Value) -> DesignApp {
         let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
         app.ui.dialog = Some(Dialog::new(&format!("cmd:{id}"), fields));
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -1335,7 +1335,7 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
         }
         "app.deletePage" | "app.duplicateSpread" => {
             let Some(st) = app.session.active() else { return Some(Err("no document open".into())) };
-            let page = crate::canvas::current_page(app).unwrap_or(0);
+            let Some(page) = crate::canvas::current_page(app) else { return Some(Err("select a document page".into())) };
             let r = if id == "app.deletePage" {
                 crate::dialogs::delete_pages(app, vec![page])
             } else {
diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs
--- a/crates/ui-egui/src/panels/properties.rs
+++ b/crates/ui-egui/src/panels/properties.rs
@@ -115,12 +115,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
         return;
     }
     // The Page tool with no objects selected: the page's own settings, as InDesign's.
-    if info.is_none() && app.session.tool_id() == "page" {
-        let page = app.session.active().and_then(|st| st.selection.page).or_else(|| crate::canvas::current_page(app));
-        if let Some(page) = page {
-            page_sections(app, ui, page);
-            return;
+    if info.is_none() && (app.session.tool_id() == "page" || app.session.active().is_some_and(|st| st.editing_parents)) {
+        if let Some((spread, page)) = crate::canvas::page_target(app) {
+            page_sections(app, ui, spread, page);
         }
+        return;
     }
     let Some(i) = info else {
         document_sections(app, ui);
@@ -213,15 +212,14 @@ pub(crate) fn preset_name(w: f64, h: f64) -> &'static str {
 
 /// Properties with the Page tool (Properties › Edit Page), as InDesign's: a "Page" band with Back,
 /// Page Size for this page alone (which page, preset, orientation, W/H), Margins and Columns and
-/// Quick Actions. Back returns to the Selection tool and the document's properties.
-fn page_sections(app: &mut DesignApp, ui: &mut Ui, page: usize) {
+/// Quick Actions. Back leaves parent editing and returns to the Selection tool and document properties.
+fn page_sections(app: &mut DesignApp, ui: &mut Ui, spread: designcraft_doc::SpreadRef, page: usize) {
     let u = units(app);
     let lang = app.ui.language.clone();
     let Some(st) = app.session.active() else { return };
     let d = st.doc.clone();
-    let Some((si, pi)) = d.page_loc(page) else { return };
-    let Some(pg) = d.spreads.get(si).and_then(|s| s.pages.get(pi)).cloned() else { return };
-    let n = page + 1;
+    let Some(sp) = d.spread(spread) else { return };
+    let Some(pg) = sp.pages.get(page).cloned() else { return };
     let t = Tokens::get(ui.ctx());
     // The band: "Page", and Back at its end.
     let band = block(ui, 33.0);
@@ -238,6 +236,9 @@ fn page_sections(app: &mut DesignApp, ui: &mut Ui, page: usize) {
     });
     rule(ui);
     if back {
+        if matches!(spread, designcraft_doc::SpreadRef::Parent(_)) {
+            let _ = app.run("layout.parents.edit", json!({"on": false}));
+        }
         let _ = app.run("layout.selectPage", json!({"page": null}));
         app.select_tool("selection");
         return;
@@ -245,13 +246,25 @@ fn page_sections(app: &mut DesignApp, ui: &mut Ui, page: usize) {
     section(ui, crate::i18n::tr(&lang, "Page Size"), true);
     // Which page.
     let b = block(ui, FIELD_H);
-    let names: Vec<String> = (0..d.page_count()).map(|i| app.session.page_label(i)).collect();
+    let (names, selected): (Vec<String>, usize) = match spread {
+        designcraft_doc::SpreadRef::Doc(si) => ((0..d.page_count()).map(|i| app.session.page_label(i)).collect(), d.first_page_of_spread(si) + page),
+        designcraft_doc::SpreadRef::Parent(_) => {
+            ((0..sp.pages.len()).map(|i| format!("{} · {}", sp.parent.as_ref().map(|p| p.label()).unwrap_or_default(), i + 1)).collect(), page)
+        }
+    };
     let picked = place(ui, sub(b, 0.0, 0.0, 93.0, FIELD_H), |ui| {
-        widgets::dropdown_list(ui, names.get(page).map_or("", String::as_str), 93.0, &names, Some(page))
+        widgets::dropdown_list(ui, names.get(selected).map_or("", String::as_str), 93.0, &names, Some(selected))
     });
     if let Some(p) = picked {
-        let _ = app.run("layout.selectPage", json!({"page": p + 1}));
-        crate::canvas::go_to_page(app, p);
+        match spread {
+            designcraft_doc::SpreadRef::Doc(_) => {
+                let _ = app.run("layout.selectPage", json!({"page": p + 1}));
+                crate::canvas::go_to_page(app, p);
+            }
+            designcraft_doc::SpreadRef::Parent(_) => {
+                let _ = app.run("layout.selectParentPage", json!({"parent": sp.id.0, "page": p}));
+            }
+        }
         return;
     }
     ui.add_space(3.0);
@@ -293,13 +306,19 @@ fn page_sections(app: &mut DesignApp, ui: &mut Ui, page: usize) {
             size = Some(if key == "width" { (nv, h) } else { (w, nv) });
         }
     }
-    if let Some((nw, nh)) = size {
-        let _ = app.run("layout.pageSize", json!({"pages": [n], "width": nw, "height": nh, "anchor": app.ui.ref_point.min(8)}));
+    if let Some((nw, nh)) = size
+        && let Some((cmd, params)) =
+            designcraft_tools::layout::page_size_command(&d, spread, page, json!({"width": nw, "height": nh, "anchor": app.ui.ref_point.min(8)}))
+    {
+        let _ = app.run(cmd, params);
     }
     ui.add_space(6.0);
     let fw = full_width(ui);
     if outline_button(ui, crate::i18n::tr(&lang, "Margins and Columns"), fw).clicked() {
-        crate::canvas::go_to_page(app, page);
+        match spread {
+            designcraft_doc::SpreadRef::Doc(si) => crate::canvas::go_to_page(app, d.first_page_of_spread(si) + page),
+            designcraft_doc::SpreadRef::Parent(_) => crate::canvas::show_spread(app, spread),
+        }
         crate::menus::activate(app, "layout.marginsAndColumns", &Value::Null);
     }
     divider(ui);
diff --git a/docs/roadmap.md b/docs/roadmap.md
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
 # DesignCraft roadmap (detail)
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (.indd and .indt open through indd-utils; text threads by hand; the gate waits on the Primary Text Frame page fixes) · **Target:** Adobe InDesign 2026 (21.6)
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (independent parent page resizing, #1620; .indd and .indt open through indd-utils; text threads by hand; the gate waits on the Primary Text Frame page fixes) · **Target:** Adobe InDesign 2026 (21.6)
 
 Forward-looking plan. The one-page summary is [ROADMAP.md](../ROADMAP.md); numbers are in
 [target-app-parity.md](target-app-parity.md); the work list is [gaps.md](gaps.md). Hours are
@@ -17,7 +17,7 @@ but are next in line.
 
 | Core workflow | Works end to end? | Evidence | Hours to pass |
 |---|---|---|---|
-| Set up a document (size, margins, columns, parent pages, page numbers, sections); save and reopen | yes | New Document, Document Setup, parent pages and overrides, Numbering & Section Options; `.designcraft` round-trip tests; recovery | 0 |
+| Set up a document (size, margins, columns, parent pages, page numbers, sections); save and reopen | yes | New Document, Document Setup, independently sized parent pages (Properties and Page Tool, #1620), parent overrides, Numbering & Section Options; `.designcraft` round-trip tests; recovery | 0 |
 | Flow a long story across frames and pages: thread, unthread, autoflow | **no** | threading by hand works (ports, loaded text cursor, `text.thread`/`text.unthread`/`text.removeFromThread`); Primary Text Frame autoflow adds pages users can't delete (#165) | 5–7 |
 | Format text with paragraph and character styles | partial (not blocking) | Paragraph/Character Style Options, Quick Apply, based-on, nested/GREP styles work; style edit/delete from the panel reported (#169); formatting set at a caret is discarded (#289) | 6–12 |
 | Place, crop and fit images; wrap text around them | yes | place gun, frame fitting and Fitting Options, content grabber, text wrap, Links panel | 0 |
