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
@@ -74,7 +74,7 @@ pub fn specs() -> Vec<CommandSpec> {
         CommandSpec::new("pagesetup", "Page Setup Manager...", run_pagesetup)
             .menu(&["File", "Page Setup Manager..."])
             .params(
-                "{layout?: current, device?: plotter name (PLOTTERMANAGER) | \"None\", paper?: the device's paper | \"A4\"|\"A3\"|\"Letter\"|\"ANSI B\"|…, width?, height? (mm), landscape?, margins?: [l,b,r,t] mm, lineweights?, plotArea?, scale?, scaleToFit?, center?, plotStyleTable?: \"monochrome.ctb\"|\"grayscale.ctb\"|\"default.ctb\"|\"None\"|…, displayPlotStyles?: bool, plotTransparency?: bool}",
+                "{layout?: current | layouts?: [names] | \"all\" (the same changes to each, in one step), device?: plotter name (PLOTTERMANAGER) | \"None\", paper?: the device's paper | \"A4\"|\"A3\"|\"Letter\"|\"ANSI B\"|…, width?, height? (mm), landscape?, margins?: [l,b,r,t] mm, lineweights?, plotArea?, scale?, scaleToFit?, center?, plotStyleTable?: \"monochrome.ctb\"|\"grayscale.ctb\"|\"default.ctb\"|\"None\"|…, displayPlotStyles?: bool, plotTransparency?: bool}",
             ),
         CommandSpec::new("plot", "Print...", |s, p| run_plot(s, &super::plotters::plot_options(s, p, "plot")?, "plot"))
             .menu(&["File", "Print..."])
@@ -624,7 +624,57 @@ fn run_viewport_set(s: &mut Session, p: &Value) -> Result<Value> {
 
 // ---------- page setup ----------
 
+/// PAGESETUP. With `layouts` (a list of layout names, or "all") the same changes are applied to
+/// each of those layouts, as one step: if any of them refuses a change, none is changed.
 fn run_pagesetup(s: &mut Session, p: &Value) -> Result<Value> {
+    let cmd = "pagesetup";
+    let Some(list) = p.get("layouts").filter(|v| !v.is_null()) else { return pagesetup_one(s, p) };
+    if p.get("layout").is_some() {
+        return Err(bad(cmd, "pass `layout` or `layouts`, not both"));
+    }
+    let names: Vec<String> = match list {
+        Value::String(a) if a.eq_ignore_ascii_case("all") => {
+            let mut ls: Vec<&Layout> = s.doc()?.layouts.iter().collect();
+            ls.sort_by_key(|l| l.tab_order);
+            ls.iter().map(|l| l.name.clone()).collect()
+        }
+        Value::Array(a) => {
+            let mut names = Vec::new();
+            for n in a.iter().take(MAX_PLOT_SHEETS + 1) {
+                let n = n.as_str().ok_or_else(|| bad(cmd, "`layouts` must be a list of layout names or \"all\""))?;
+                let n = layout_name(cmd, s.doc()?, n)?;
+                if !names.contains(&n) {
+                    names.push(n);
+                }
+            }
+            names
+        }
+        _ => return Err(bad(cmd, "`layouts` must be a list of layout names or \"all\"")),
+    };
+    if names.is_empty() || names.len() > MAX_PLOT_SHEETS {
+        return Err(bad(cmd, format!("`layouts` needs between 1 and {MAX_PLOT_SHEETS} layouts")));
+    }
+    let before = s.state()?.doc.clone();
+    let mut done = Vec::new();
+    for n in &names {
+        let mut one = p.clone();
+        if let Some(o) = one.as_object_mut() {
+            o.remove("layouts");
+            o.insert("layout".into(), json!(n));
+        }
+        match pagesetup_one(s, &one) {
+            Ok(r) => done.push(r),
+            Err(e) => {
+                s.state_mut()?.doc = before;
+                return Err(e);
+            }
+        }
+    }
+    Ok(json!({ "layouts": done }))
+}
+
+/// PAGESETUP for one layout: the changes the options ask for, reported with the resulting page setup.
+fn pagesetup_one(s: &mut Session, p: &Value) -> Result<Value> {
     let cmd = "pagesetup";
     let keys = [
         "device",
@@ -1054,6 +1104,52 @@ mod tests {
         }
     }
 
+    /// The paper name of layout `name`.
+    fn paper_of(s: &Session, name: &str) -> String {
+        s.doc().unwrap().layout(name).unwrap().page.paper.clone()
+    }
+
+    /// One change given to several layouts is applied to each, as one undo step.
+    #[test]
+    fn pagesetup_applies_one_change_to_several_layouts_in_one_step() {
+        let mut s = Session::new();
+        s.execute("layout.new", &json!({"name": "Sheet 3"})).unwrap();
+        let a1 = paper_of(&s, "Layout1");
+        assert_ne!(a1, "ISO A3 (420.00 x 297.00 MM)", "the test needs another starting paper");
+        let r = s.execute("pagesetup", &json!({"layouts": ["Layout1", "layout2"], "paper": "A3", "plotStyleTable": "monochrome.ctb"})).unwrap();
+        assert_eq!(r["layouts"].as_array().map(Vec::len), Some(2));
+        for n in ["Layout1", "Layout2"] {
+            let page = &s.doc().unwrap().layout(n).unwrap().page;
+            assert_eq!((page.width_mm.max(page.height_mm), page.plot_style_table.as_str()), (420.0, "monochrome.ctb"), "{n}");
+        }
+        assert_ne!(paper_of(&s, "Sheet 3"), paper_of(&s, "Layout1"), "a layout not named keeps its settings");
+        // "all" takes every layout; one undo step reverts all of it.
+        s.execute("pagesetup", &json!({"layouts": "all", "landscape": false})).unwrap();
+        assert!(s.doc().unwrap().layouts.iter().all(|l| !l.page.landscape));
+        s.undo().unwrap();
+        assert!(s.doc().unwrap().layouts.iter().any(|l| l.page.landscape), "the whole change is one undo step");
+    }
+
+    /// A change a layout refuses leaves every layout as it was.
+    #[test]
+    fn pagesetup_for_several_layouts_is_all_or_nothing() {
+        let mut s = Session::new();
+        let before: Vec<String> = ["Layout1", "Layout2"].iter().map(|n| paper_of(&s, n)).collect();
+        for p in [
+            json!({"layouts": ["Layout1", "Nope"], "paper": "A3"}),
+            json!({"layouts": ["Layout1", "Layout2"], "paper": "Z12"}),
+            json!({"layouts": ["Layout1"], "layout": "Layout2", "paper": "A3"}),
+            json!({"layouts": ["Model"], "paper": "A3"}),
+            json!({"layouts": [], "paper": "A3"}),
+            json!({"layouts": "Layout1", "paper": "A3"}),
+            json!({"layouts": [7], "paper": "A3"}),
+        ] {
+            assert!(s.execute("pagesetup", &p).is_err(), "{p}");
+        }
+        let after: Vec<String> = ["Layout1", "Layout2"].iter().map(|n| paper_of(&s, n)).collect();
+        assert_eq!(before, after, "a refused change leaves every layout as it was");
+    }
+
     #[test]
     fn pagesetup_paper_table() {
         let mut s = Session::new();
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -81,6 +81,8 @@ menu	Modify	Edita
 	Array	Serie
 	Attach	Associa
 	Attribute	Attributo
+	Select all	Seleziona tutti
+	{n} layouts selected: a change applies to all of them	{n} layout selezionati: una modifica vale per tutti
 	Attribute Definition	Definizione di attributo
 	Attribute Display	Visualizzazione attributi
 	Audit	Controlla
diff --git a/crates/ui-egui/src/managers.rs b/crates/ui-egui/src/managers.rs
--- a/crates/ui-egui/src/managers.rs
+++ b/crates/ui-egui/src/managers.rs
@@ -480,6 +480,13 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
         .or(shown)
         .or_else(|| layouts.first().map(|l| l.1.clone()))
         .unwrap_or_default();
+    // The layouts a change applies to: `sel` (the one whose settings are shown) and the others
+    // picked with Ctrl / Shift.
+    let mut picked = temp::<Vec<String>>(ctx, "pagesetup_picked").unwrap_or_default();
+    picked.retain(|n| layouts.iter().any(|l| l.1 == *n));
+    if !picked.contains(&sel) {
+        picked = vec![sel.clone()];
+    }
     let mut action = None;
     let choices = PageChoices::new(ctx, d);
     egui::Window::new("Page Setup Manager").open(open).default_size(vec2(560.0, 300.0)).resizable(true).show(ctx, |ui| {
@@ -492,18 +499,28 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
             left.label(RichText::new("Layouts").strong());
             egui::ScrollArea::vertical().id_salt("pagesetup_list").max_height(200.0).auto_shrink([false, true]).show(left, |ui| {
                 for (_, name, _) in &layouts {
-                    if ui.selectable_label(*name == sel, name).clicked() {
-                        sel = name.clone();
+                    let resp = ui.selectable_label(picked.contains(name), name);
+                    if resp.clicked() {
+                        let order: Vec<&str> = layouts.iter().map(|l| l.1.as_str()).collect();
+                        click_layout(&mut picked, &mut sel, &order, name, ui.input(|i| i.modifiers));
                     }
                 }
             });
+            left.horizontal(|ui| {
+                if ui.small_button("Select all").on_hover_text("Ctrl+click or Shift+click pick several layouts").clicked() {
+                    picked = layouts.iter().map(|l| l.1.clone()).collect();
+                }
+            });
             let Some((_, name, page)) = layouts.iter().find(|l| l.1 == sel) else { return };
             right.label(RichText::new(format!("Page setup of {name}")).strong());
+            if picked.len() > 1 {
+                right.label(RichText::new(crate::tf!("{n} layouts selected: a change applies to all of them", n = picked.len())).color(t.text_dim));
+            }
             // Layout plotting always prints the whole sheet, so only that area is offered.
             let rows = PageRows { areas: &["layout"], scale: true, center: true };
             egui::Grid::new("pagesetup_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(right, |ui| {
-                if let Some((k, v)) = page_rows(ui, egui::Id::new(("pagesetup", name)), page, &rows, &choices, None) {
-                    action = Some(("pagesetup", json!({ "layout": name, k: v })));
+                if let Some((k, v)) = page_rows(ui, egui::Id::new(("pagesetup", name)), page, &rows, &choices, None, picked.len() > 1) {
+                    action = Some(("pagesetup", pagesetup_change(&picked, name, k, v)));
                 }
             });
             let (w, h) = if page.landscape { (page.height_mm, page.width_mm) } else { (page.width_mm, page.height_mm) };
@@ -511,9 +528,45 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
         });
     });
     set_temp(ctx, "pagesetup_sel", sel);
+    set_temp(ctx, "pagesetup_picked", picked);
     action
 }
 
+/// A click on layout `name` in the Page Setup Manager's list: Ctrl (or Cmd) adds or removes it,
+/// Shift picks the range from the layout shown to it, a plain click picks just that one. `sel` is
+/// the layout whose settings are shown, `order` the layouts in tab order.
+fn click_layout(picked: &mut Vec<String>, sel: &mut String, order: &[&str], name: &str, modifiers: egui::Modifiers) {
+    if modifiers.shift {
+        let (a, b) = (order.iter().position(|n| *n == sel.as_str()), order.iter().position(|n| *n == name));
+        if let (Some(a), Some(b)) = (a, b) {
+            let (lo, hi) = (a.min(b), a.max(b));
+            *picked = order.get(lo..=hi).unwrap_or_default().iter().map(|n| n.to_string()).collect();
+        }
+    } else if modifiers.command || modifiers.ctrl {
+        if let Some(i) = picked.iter().position(|n| n == name) {
+            // The last one stays: something must be shown.
+            if picked.len() > 1 {
+                picked.remove(i);
+                if sel == name {
+                    *sel = picked.last().cloned().unwrap_or_default();
+                }
+            }
+        } else {
+            picked.push(name.to_string());
+            *sel = name.to_string();
+        }
+    } else {
+        *picked = vec![name.to_string()];
+        *sel = name.to_string();
+    }
+}
+
+/// The `pagesetup` parameters for one changed setting: for the layout shown, or, with several
+/// picked, for all of them in one step.
+fn pagesetup_change(picked: &[String], shown: &str, key: &str, value: Value) -> Value {
+    if picked.len() > 1 { json!({ "layouts": picked, key: value }) } else { json!({ "layout": shown, key: value }) }
+}
+
 /// Which page-setup rows a dialog shows.
 struct PageRows {
     /// Plot area choices (`plotArea` values).
@@ -583,7 +636,8 @@ fn parse_ratio(t: &str) -> Option<f64> {
 /// transparency, plot style table) shared by the Page Setup Manager and the Plot dialog, inside a
 /// two-column grid. Returns the setting the user changed as a `pagesetup` parameter (`device`,
 /// `paper`, `landscape`, `plotArea`, `scaleToFit`, `scale`, `center`, `lineweights`,
-/// `plotTransparency` or `plotStyleTable`).
+/// `plotTransparency` or `plotStyleTable`). With `force`, picking the value already shown is a
+/// change too: with several layouts picked, that is how the others are made to match the one shown.
 fn page_rows(
     ui: &mut egui::Ui,
     salt: egui::Id,
@@ -591,19 +645,20 @@ fn page_rows(
     rows: &PageRows,
     choices: &PageChoices,
     placement: Option<(&mut bool, &mut [f64; 2], bool)>,
+    force: bool,
 ) -> Option<(&'static str, Value)> {
     let mut change = None;
     ui.label("Printer/plotter");
     egui::ComboBox::from_id_salt(salt.with("device")).selected_text(device_label(page)).width(190.0).show_ui(ui, |ui| {
         if ui.selectable_label(device_label(page) == "None", "None").on_hover_text("PDF on the paper chosen below").clicked()
-            && device_label(page) != "None"
+            && (force || device_label(page) != "None")
         {
             change = Some(("device", json!("None")));
         }
         for dev in choices.devices.iter() {
             let on = dev.name.eq_ignore_ascii_case(&page.device);
             if ui.selectable_label(on, &dev.name).on_hover_text(format!("{} ({})", dev.description, dev.output.as_str().to_uppercase())).clicked()
-                && !on
+                && (force || !on)
             {
                 change = Some(("device", json!(dev.name)));
             }
@@ -616,14 +671,14 @@ fn page_rows(
     egui::ComboBox::from_id_salt(salt.with("paper")).selected_text(page.paper.as_str()).width(190.0).show_ui(ui, |ui| match device {
         Some(dev) => {
             for p in &dev.papers {
-                if ui.selectable_label(p.name == page.paper, &p.name).clicked() && p.name != page.paper {
+                if ui.selectable_label(p.name == page.paper, &p.name).clicked() && (force || p.name != page.paper) {
                     change = Some(("paper", json!(p.name)));
                 }
             }
         }
         None => {
             for p in cadcraft_render::PAPER_SIZES {
-                if ui.selectable_label(p.name == page.paper, p.name).clicked() && p.name != page.paper {
+                if ui.selectable_label(p.name == page.paper, p.name).clicked() && (force || p.name != page.paper) {
                     change = Some(("paper", json!(p.name)));
                 }
             }
@@ -632,10 +687,10 @@ fn page_rows(
     ui.end_row();
     ui.label("Orientation");
     ui.horizontal(|ui| {
-        if ui.radio(!page.landscape, "Portrait").clicked() && page.landscape {
+        if ui.radio(!page.landscape, "Portrait").clicked() && (force || page.landscape) {
             change = Some(("landscape", json!(false)));
         }
-        if ui.radio(page.landscape, "Landscape").clicked() && !page.landscape {
+        if ui.radio(page.landscape, "Landscape").clicked() && (force || !page.landscape) {
             change = Some(("landscape", json!(true)));
         }
     });
@@ -644,7 +699,7 @@ fn page_rows(
     ui.add_enabled_ui(rows.areas.len() > 1, |ui| {
         egui::ComboBox::from_id_salt(salt.with("area")).selected_text(area_label(&page.plot_area)).show_ui(ui, |ui| {
             for a in rows.areas {
-                if ui.selectable_label(page.plot_area == *a, area_label(a)).clicked() && page.plot_area != *a {
+                if ui.selectable_label(page.plot_area == *a, area_label(a)).clicked() && (force || page.plot_area != *a) {
                     change = Some(("plotArea", json!(a)));
                 }
             }
@@ -719,12 +774,12 @@ fn page_rows(
     ui.end_row();
     ui.label("Plot style table");
     egui::ComboBox::from_id_salt(salt.with("styles")).selected_text(style_label(page)).width(190.0).show_ui(ui, |ui| {
-        if ui.selectable_label(style_label(page) == "None", "None").clicked() && style_label(page) != "None" {
+        if ui.selectable_label(style_label(page) == "None", "None").clicked() && (force || style_label(page) != "None") {
             change = Some(("plotStyleTable", json!("None")));
         }
         for name in &choices.styles {
             let on = name.eq_ignore_ascii_case(&page.plot_style_table);
-            if ui.selectable_label(on, name).clicked() && !on {
+            if ui.selectable_label(on, name).clicked() && (force || !on) {
                 change = Some(("plotStyleTable", json!(name)));
             }
         }
@@ -955,7 +1010,7 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
                     PageRows { areas: &["layout"], scale: false, center: false }
                 };
                 let placement = model.then_some((&mut st.center, &mut st.offset_mm, offset_supported));
-                if let Some((k, v)) = page_rows(ui, egui::Id::new(("plot", st.target.as_str())), &st.page, &rows, &choices, placement) {
+                if let Some((k, v)) = page_rows(ui, egui::Id::new(("plot", st.target.as_str())), &st.page, &rows, &choices, placement, false) {
                     apply_page_change(&mut st.page, k, &v);
                 }
                 if model && st.page.plot_area == "window" {
@@ -1256,3 +1311,72 @@ mod plot_dialog_tests {
         assert_eq!(temp::<PlotState>(&ctx, "plot_state").unwrap().window, [2.0, 3.0, 12.0, 9.0]);
     }
 }
+
+#[cfg(test)]
+mod page_setup_selection_tests {
+    use super::*;
+
+    const ORDER: [&str; 5] = ["Layout1", "Layout2", "Sheet 3", "Sheet 4", "Sheet 5"];
+
+    /// Modifier keys for a click.
+    fn mods(shift: bool, command: bool) -> egui::Modifiers {
+        egui::Modifiers { shift, command, ..Default::default() }
+    }
+
+    /// Ctrl alone counts like Cmd, as some key events set only `ctrl`.
+    #[test]
+    fn ctrl_alone_counts_like_command() {
+        // Synthetic and some platforms' key events set `ctrl` without `command`.
+        let ctrl = egui::Modifiers { ctrl: true, ..Default::default() };
+        let (mut picked, mut sel) = (vec!["Layout1".to_string()], "Layout1".to_string());
+        click_layout(&mut picked, &mut sel, &ORDER, "Sheet 3", ctrl);
+        assert_eq!(picked, ["Layout1", "Sheet 3"]);
+    }
+
+    /// A click on layout `name` with modifiers `m`.
+    fn click(picked: &mut Vec<String>, sel: &mut String, name: &str, m: egui::Modifiers) {
+        click_layout(picked, sel, &ORDER, name, m);
+    }
+
+    /// Plain, Ctrl and Shift clicks pick one, toggle, and take a range.
+    #[test]
+    fn click_picks_one_ctrl_toggles_shift_takes_a_range() {
+        let (mut picked, mut sel) = (vec!["Layout1".to_string()], "Layout1".to_string());
+        click(&mut picked, &mut sel, "Sheet 3", mods(false, false));
+        assert_eq!((picked.as_slice(), sel.as_str()), (&["Sheet 3".to_string()][..], "Sheet 3"));
+        // Ctrl adds and shows the one just added.
+        click(&mut picked, &mut sel, "Layout1", mods(false, true));
+        click(&mut picked, &mut sel, "Sheet 5", mods(false, true));
+        assert_eq!(picked, ["Sheet 3", "Layout1", "Sheet 5"]);
+        assert_eq!(sel, "Sheet 5");
+        // Ctrl on a picked layout removes it; the shown one moves to another picked.
+        click(&mut picked, &mut sel, "Sheet 5", mods(false, true));
+        assert_eq!((picked.as_slice(), sel.as_str()), (&["Sheet 3".to_string(), "Layout1".to_string()][..], "Layout1"));
+        // The last picked layout can't be removed.
+        click(&mut picked, &mut sel, "Layout1", mods(false, true));
+        click(&mut picked, &mut sel, "Sheet 3", mods(false, true));
+        assert_eq!(picked, ["Sheet 3"], "{picked:?}");
+        // Shift takes the range from the shown layout, in tab order, whichever way it goes.
+        click(&mut picked, &mut sel, "Sheet 5", mods(true, false));
+        assert_eq!(picked, ["Sheet 3", "Sheet 4", "Sheet 5"]);
+        click(&mut picked, &mut sel, "Layout2", mods(true, false));
+        assert_eq!(picked, ["Layout2", "Sheet 3"], "from the shown layout (Sheet 3) back to Layout2");
+    }
+
+    /// A change for several picked layouts is one `pagesetup` with `layouts`.
+    #[test]
+    fn a_change_goes_to_all_picked_layouts_in_one_command() {
+        let one = pagesetup_change(&["Layout1".to_string()], "Layout1", "paper", json!("A3"));
+        assert_eq!(one, json!({ "layout": "Layout1", "paper": "A3" }));
+        let many = pagesetup_change(&["Layout1".to_string(), "Sheet 3".to_string()], "Sheet 3", "plotStyleTable", json!("monochrome.ctb"));
+        assert_eq!(many, json!({ "layouts": ["Layout1", "Sheet 3"], "plotStyleTable": "monochrome.ctb" }));
+        // And the engine takes exactly that.
+        let mut app = crate::CadApp::new(cadcraft_engine::Session::default(), crate::Services::default());
+        app.run("layout.new", json!({ "name": "Sheet 3" })).unwrap();
+        app.run("pagesetup", many).unwrap();
+        let d = app.session.doc().unwrap();
+        assert_eq!(d.layout("Layout1").map(|l| l.page.plot_style_table.as_str()), Some("monochrome.ctb"));
+        assert_eq!(d.layout("Sheet 3").map(|l| l.page.plot_style_table.as_str()), Some("monochrome.ctb"));
+        assert_ne!(d.layout("Layout2").map(|l| l.page.plot_style_table.as_str()), Some("monochrome.ctb"), "an unpicked layout is untouched");
+    }
+}
diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs
--- a/crates/ui-egui/tests/it/main.rs
+++ b/crates/ui-egui/tests/it/main.rs
@@ -17,6 +17,7 @@ mod measure_distance_snapping;
 mod menubar_hover;
 mod mspace_viewport_view;
 mod options_dialog;
+mod pagesetup_multi;
 mod parameters_rename;
 mod pickdrag;
 mod properties_units;
diff --git a/crates/ui-egui/tests/it/pagesetup_multi.rs b/crates/ui-egui/tests/it/pagesetup_multi.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/pagesetup_multi.rs
@@ -0,0 +1,54 @@
+//! The Page Setup Manager with several layouts picked: choosing the value the layout shown
+//! already has still applies it to the others, which is how mixed layouts are made to match.
+
+use cadcraft_engine::Session;
+use cadcraft_ui_egui::{CadApp, Services, menus};
+use egui::vec2;
+use egui_kittest::Harness;
+use egui_kittest::kittest::Queryable;
+use serde_json::json;
+
+/// An app in a window, as the other integration tests build it.
+fn harness(app: CadApp) -> Harness<'static, CadApp> {
+    let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state(
+        |ui, app: &mut CadApp| {
+            app.logic(&ui.ctx().clone());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(5);
+    h
+}
+
+/// Whether layout `layout` is landscape.
+fn landscape(app: &CadApp, layout: &str) -> bool {
+    app.session.doc().unwrap().layout(layout).unwrap().page.landscape
+}
+
+/// Choosing the value the shown layout has makes the other picked layouts match.
+#[test]
+fn picking_the_value_shown_makes_the_other_picked_layouts_match() {
+    let mut app = CadApp::new(Session::default(), Services::default());
+    app.ui.start_tab = false;
+    app.ui.in_window_menu = false;
+    // Layout1 is shown and landscape; Layout2 is portrait.
+    app.run("pagesetup", json!({ "layout": "Layout1", "landscape": true })).unwrap();
+    app.run("pagesetup", json!({ "layout": "Layout2", "landscape": false })).unwrap();
+    app.run("layout.set", json!({ "name": "Layout1" })).unwrap();
+    let mut h = harness(app);
+    menus::activate(h.state_mut(), "pagesetup");
+    h.run_steps(5);
+
+    // With one layout picked, choosing what it already has changes nothing.
+    h.get_by_label("Landscape").click();
+    h.run_steps(3);
+    assert!(!landscape(h.state(), "Layout2"), "one layout picked: the other is left alone");
+
+    // With both picked, the same click is a change for both.
+    h.get_by_label("Select all").click();
+    h.run_steps(3);
+    h.get_by_label("Landscape").click();
+    h.run_steps(3);
+    assert!(landscape(h.state(), "Layout1") && landscape(h.state(), "Layout2"), "both layouts are landscape now");
+}
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where CADCraft falls short of AutoCAD
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Command Aliases dialog for ALIASEDIT: U12 and E5 narrowed) · **Target:** Autodesk AutoCAD 2027
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (The Page Setup Manager changes several layouts at once) · **Target:** Autodesk AutoCAD 2027
 
 Every known shortfall, one row each, ranked within each section by user impact. This is the work
 list: agents pick from here (top of a section first), and remove or update a row in the same PR
@@ -150,6 +150,7 @@ Detail: [localization-parity.md](localization-parity.md).
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-24 | minor | The Page Setup Manager picks several layouts (Ctrl, Shift, Select all) and a change applies to all of them; PAGESETUP takes `layouts` (a list of names or "all") for the same: one undo step, all or nothing |
 | 2026-10-22 | minor | APPLOAD typed or from Tools ▸ Load Application opens a `.lsp` file browser starting in the last-used folder (kept in the preferences); a picked file loads interactively like a typed name (routines that ask for input wait), the web says to drop the file (no file system), and the JSON form `{path}` reads the path as given and returns the commands the file defined, never opening a dialog (#1820): E1 narrowed |
 | 2026-10-16 | minor | ALIASEDIT typed or from Tools ▸ Customize opens the Command Aliases dialog (user aliases next to the built-in ones; Add, Edit, Remove; OK, Cancel, Apply), #1381: U12 and E5 narrowed |
 | 2026-10-16 | minor | MOCORO (Tools ▸ Express Tools ▸ Move/Copy/Rotate): select objects and a base point once, then Move, Copy, Rotate, Scale, Base and Undo until eXit, one undo step; JSON form `{base, ops}`: E3 narrowed |
