diff --git a/crates/engine/src/cmd/arrange.rs b/crates/engine/src/cmd/arrange.rs
--- a/crates/engine/src/cmd/arrange.rs
+++ b/crates/engine/src/cmd/arrange.rs
@@ -226,6 +226,94 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
     })
 }
 
+/// After an edit: refit every group whose children's geometry changed to the union of its
+/// children (PowerPoint keeps a group's box on its children, so moving one child grows or
+/// shrinks the group). The children keep their place on the slide: the group's box and its child
+/// space change together. `old` is the presentation before the edit; only slides the edit
+/// touched are looked at, and a group whose own box changed (it was moved or resized) is left to
+/// that edit.
+pub(crate) fn refit_groups(old: &deckcraft_model::Presentation, new: &mut deckcraft_model::Presentation) {
+    for (i, slide) in new.slides.iter_mut().enumerate() {
+        let Some(before) = old.slides.get(i) else { continue };
+        if std::sync::Arc::ptr_eq(before, slide) {
+            continue;
+        }
+        let shapes = &mut std::sync::Arc::make_mut(slide).shapes;
+        refit_list(Some(&before.shapes), shapes);
+    }
+    // A child edited in Slide Master, layout, notes or handout view refits its group there too.
+    for (i, master) in new.masters.iter_mut().enumerate() {
+        if let Some(before) = old.masters.get(i)
+            && !std::sync::Arc::ptr_eq(before, master)
+        {
+            refit_master(before, std::sync::Arc::make_mut(master));
+        }
+    }
+    for (before, after) in [(old.notes_master.as_ref(), new.notes_master.as_mut()), (old.handout_master.as_ref(), new.handout_master.as_mut())] {
+        if let (Some(before), Some(after)) = (before, after)
+            && !std::sync::Arc::ptr_eq(before, after)
+        {
+            refit_master(before, std::sync::Arc::make_mut(after));
+        }
+    }
+}
+
+fn refit_master(old: &deckcraft_model::Master, new: &mut deckcraft_model::Master) {
+    refit_list(Some(&old.shapes), &mut new.shapes);
+    for (i, layout) in new.layouts.iter_mut().enumerate() {
+        refit_list(old.layouts.get(i).map(|l| &l.shapes), &mut layout.shapes);
+    }
+}
+
+fn refit_list(old: Option<&Vec<Shape>>, list: &mut [Shape]) {
+    // Most edits touch a slide without groups: nothing to refit, and no per-shape lookup.
+    if !list.iter().any(|sh| matches!(sh.kind, ShapeKind::Group { .. })) {
+        return;
+    }
+    let by_id: std::collections::HashMap<ShapeId, &Shape> = old.map(|o| o.iter().map(|x| (x.id, x)).collect()).unwrap_or_default();
+    for sh in list.iter_mut() {
+        let before = by_id.get(&sh.id).copied();
+        let old_children = before.and_then(|b| match &b.kind {
+            ShapeKind::Group { children, .. } => Some(children),
+            _ => None,
+        });
+        let own_before = before.map(|b| {
+            (
+                b.xfrm,
+                match &b.kind {
+                    ShapeKind::Group { child, .. } => Some(*child),
+                    _ => None,
+                },
+            )
+        });
+        let smart = sh.diagram.is_some();
+        let ShapeKind::Group { children, child } = &mut sh.kind else { continue };
+        refit_list(old_children, children);
+        if smart {
+            // A SmartArt graphic's box is its own; its shapes are regenerated inside it.
+            continue;
+        }
+        let (Some(old_children), Some((old_x, old_child))) = (old_children, own_before) else { continue };
+        if old_x != sh.xfrm || old_child != Some(*child) || children.is_empty() {
+            continue;
+        }
+        let same = old_children.len() == children.len() && old_children.iter().zip(children.iter()).all(|(a, b)| a.id == b.id && a.xfrm == b.xfrm);
+        if same {
+            continue;
+        }
+        let Some(group) = sh.xfrm else { continue };
+        let xs: Vec<Xfrm> = children.iter().filter_map(|c| c.xfrm).collect();
+        let Some(u) = union_bounds(&xs) else { continue };
+        if ![u.x0, u.y0, u.x1, u.y1].iter().all(|v| v.is_finite()) {
+            continue;
+        }
+        let union = Xfrm::new(u.x0, u.y0, u.width(), u.height());
+        let placed = child_to_parent(&group, child, &union);
+        sh.xfrm = Some(Xfrm { x: placed.x, y: placed.y, w: placed.w, h: placed.h, rot: group.rot, flip_h: group.flip_h, flip_v: group.flip_v });
+        *child = union;
+    }
+}
+
 /// A child's xfrm in slide space, given its group.
 pub fn child_to_parent(group: &Xfrm, ch: &Xfrm, c: &Xfrm) -> Xfrm {
     let sx = if ch.w.abs() > 1e-9 { group.w / ch.w } else { 1.0 };
diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs
--- a/crates/engine/src/cmd/insert.rs
+++ b/crates/engine/src/cmd/insert.rs
@@ -80,7 +80,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "Date & Time",
             ["Insert", "Text"],
             None,
-            "{format?: datetime1..datetime13, update?: bool, language?: en-US|en-GB|fr-FR|ja-JP|sv-SE} — no keys opens the dialog",
+            "{format?: datetime1..datetime13, update?: bool, language?: en-US|en-GB|fr-FR|ja-JP|sv-SE} — no keys opens the dialog; in the Handout or Notes Master view it fills the selected date placeholder (else the master's own) and answers where: \"master\"",
             has_slide,
             date_time
         ),
@@ -1478,6 +1478,11 @@ fn apply_date(s: &mut Session, o: &DateOpts) -> Result<Value> {
         }
         return Ok(json!({"where": "caret", "format": o.format, "update": o.update, "text": o.sample}));
     }
+    // Handout or Notes Master view: the master's date placeholder (the selected one, else the
+    // master's own), which every handout or notes page then shows.
+    if s.doc()?.selection.target.is_page_master() {
+        return write_master_date(s, o);
+    }
     // Not editing: the slide's date placeholder (this slide only). A layout with no date
     // placeholder gets a text box instead, so the chosen format is still inserted.
     s.execute("design.headerFooter", &json!({"date": true, "all": false}))?;
@@ -1487,6 +1492,49 @@ fn apply_date(s: &mut Session, o: &DateOpts) -> Result<Value> {
     Ok(json!({"where": "footer", "format": o.format, "update": o.update, "text": o.sample}))
 }
 
+/// Put the date in the date placeholder of the handout or notes master being edited.
+fn write_master_date(s: &mut Session, o: &DateOpts) -> Result<Value> {
+    let (format, sample, lang, update) = (o.format.clone(), o.sample.clone(), o.lang.clone(), o.update);
+    let has_date = {
+        let st = s.doc()?;
+        crate::page_master(&st.doc, st.selection.target).is_some_and(|m| m.shapes.iter().any(|x| x.ph_type() == Some(deckcraft_model::PhType::Date)))
+    };
+    if !has_date {
+        return Err(bad(
+            "insert.dateTime",
+            "this master has no date placeholder; show it with handout.placeholders or notes.placeholders {date: true}",
+        ));
+    }
+    // The text the master holds afterwards, which the answer reports (it can differ from the
+    // caller's sample when the placeholder's own language formats a fixed date).
+    let mut written = o.sample.clone();
+    s.edit(|doc, sel| {
+        let Some(m) = crate::page_master_mut(doc, sel.target) else { return Ok(false) };
+        let m = std::sync::Arc::make_mut(m);
+        let date = |x: &deckcraft_model::Shape| x.ph_type() == Some(deckcraft_model::PhType::Date);
+        let chosen = sel.shapes.iter().find_map(|id| m.shapes.iter().position(|x| x.id == *id && date(x)));
+        let Some(i) = chosen.or_else(|| m.shapes.iter().position(date)) else { return Ok(false) };
+        let Some(sh) = m.shapes.get_mut(i) else { return Ok(false) };
+        let mut props = sh.text.as_ref().and_then(|t| t.paragraphs.first()).and_then(|p| p.runs.first()).map(|r| r.props.clone()).unwrap_or_default();
+        // A fixed date reads in the placeholder's own language when the caller named none.
+        let text = match (&lang, &props.lang) {
+            (None, Some(l)) if !update => date_sample(&format, Some(l)).unwrap_or_else(|_| sample.clone()),
+            _ => sample.clone(),
+        };
+        if let Some(lang) = lang {
+            props.lang = Some(lang);
+        }
+        let kind = if update { RunKind::Field { field: format } } else { RunKind::Text };
+        let body = sh.text.clone().unwrap_or_default();
+        let mut para = body.paragraphs.first().cloned().unwrap_or_default();
+        written.clone_from(&text);
+        para.runs = vec![deckcraft_model::Run { text, props, kind }];
+        sh.text = Some(TextBody { paragraphs: vec![para], ..body });
+        Ok(true)
+    })?;
+    Ok(json!({"where": "master", "format": o.format, "update": o.update, "text": written}))
+}
+
 /// Point the current slide's date placeholder at this format (a field, or the sample as plain text).
 fn write_date_placeholder(s: &mut Session, o: &DateOpts) -> Result<bool> {
     let format = o.format.clone();
@@ -1943,6 +1991,17 @@ mod tests {
         assert_eq!(undo(&s), before + 1);
     }
 
+    #[test]
+    fn a_long_british_date_puts_the_day_first() {
+        let mut s = session();
+        edit_title(&mut s);
+        let v = s.execute("insert.dateTime", &json!({"format": "datetime4", "update": false, "language": "en-GB"})).unwrap();
+        let text = v["text"].as_str().unwrap().to_string();
+        assert!(text.chars().next().is_some_and(|c| c.is_ascii_digit()) && !text.contains(','), "{text}");
+        let v = s.execute("insert.dateTime", &json!({"format": "datetime4", "update": false, "language": "en-US"})).unwrap();
+        assert!(v["text"].as_str().is_some_and(|t| t.contains(',')), "{v}");
+    }
+
     #[test]
     fn a_named_format_in_french_and_a_bad_value_change_nothing() {
         let mut s = session();
@@ -1967,6 +2026,68 @@ mod tests {
         }
     }
 
+    /// ZZ1: in the handout and notes master views the date goes into the master's date placeholder
+    /// (not the current slide), so the exported handout and notes pages show it.
+    #[test]
+    fn on_a_page_master_the_date_fills_its_date_placeholder() {
+        for view in ["view.handoutMaster", "view.notesMaster"] {
+            let mut s = session();
+            s.execute(view, &json!({})).unwrap();
+            let (id, master) = {
+                let st = s.doc().unwrap();
+                let m = crate::page_master(&st.doc, st.selection.target).unwrap().clone();
+                (m.shapes.iter().find(|x| x.ph_type() == Some(deckcraft_model::PhType::Date)).unwrap().id.0, m)
+            };
+            assert!(master.shapes.iter().find(|x| x.id.0 == id).and_then(|x| x.text.as_ref()).is_none_or(|t| t.text().is_empty()));
+            s.execute("edit.select", &json!({"ids": [id]})).unwrap();
+            let v = s.execute("insert.dateTime", &json!({"format": "datetime10", "update": true})).unwrap();
+            assert_eq!(v["where"], "master", "{view}: {v}");
+            assert!(date_run(&s).is_none(), "the slide keeps no date");
+            let st = s.doc().unwrap();
+            let m = crate::page_master(&st.doc, st.selection.target).unwrap();
+            let run = m
+                .shapes
+                .iter()
+                .find(|x| x.id.0 == id)
+                .and_then(|x| x.text.as_ref())
+                .and_then(|t| t.paragraphs.first())
+                .and_then(|p| p.runs.first())
+                .cloned()
+                .unwrap();
+            assert!(matches!(run.kind, RunKind::Field { ref field } if field == "datetime10"), "{view}: {run:?}");
+            let placed = deckcraft_render::place_master_texts(&st.doc, m, 1);
+            assert!(placed.iter().any(|p| p.body.text() == run.text && run.text.contains(':')), "{view}: the page shows the date");
+            s.execute("edit.undo", &json!({})).unwrap();
+            let st = s.doc().unwrap();
+            let m = crate::page_master(&st.doc, st.selection.target).unwrap();
+            assert!(m.shapes.iter().find(|x| x.id.0 == id).and_then(|x| x.text.as_ref()).is_none_or(|t| t.text().is_empty()), "one undo");
+            // A fixed date in a placeholder whose run is en-GB reads day first, and the answer
+            // reports the text that was written, not the en-US sample.
+            s.execute("insert.dateTime", &json!({"format": "datetime4", "update": false, "language": "en-GB"})).unwrap();
+            let v = s.execute("insert.dateTime", &json!({"format": "datetime4", "update": false})).unwrap();
+            let st = s.doc().unwrap();
+            let m = crate::page_master(&st.doc, st.selection.target).unwrap();
+            let run = m
+                .shapes
+                .iter()
+                .find(|x| x.id.0 == id)
+                .and_then(|x| x.text.as_ref())
+                .and_then(|t| t.paragraphs.first())
+                .and_then(|p| p.runs.first())
+                .cloned()
+                .unwrap();
+            assert_eq!(run.props.lang.as_deref(), Some("en-GB"), "{view}: {run:?}");
+            assert_eq!(v["text"].as_str(), Some(run.text.as_str()), "{view}: the answer reports the written text: {v}");
+            assert!(!run.text.contains(','), "{view}: en-GB puts the day first, no comma: {}", run.text);
+            // No date placeholder: refused, nothing changed.
+            let key = if view == "view.handoutMaster" { "handout.placeholders" } else { "notes.placeholders" };
+            s.execute(key, &json!({"date": false})).unwrap();
+            let rev = s.doc().unwrap().revision;
+            assert!(s.execute("insert.dateTime", &json!({"format": "datetime1"})).is_err());
+            assert_eq!(s.doc().unwrap().revision, rev);
+        }
+    }
+
     #[test]
     fn without_a_caret_the_date_lands_on_this_slide_only() {
         let mut s = session();
diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs
--- a/crates/engine/src/cmd/review.rs
+++ b/crates/engine/src/cmd/review.rs
@@ -56,9 +56,17 @@ pub fn specs() -> Vec<CommandSpec> {
             has_text,
             insert_synonym
         ),
-        cmd!(noundo "review.compare", "Compare", ["Review", "Compare"], None, "{path} | {name, data: base64} — compare with a revised copy → {name, changes}", has_doc, compare),
-        cmd!(query "review.revisions", "Revisions", [], None, "{} → {name, changes: [{kind: inserted|deleted|changed, slide, revised, details}]}", has_doc, revisions),
-        cmd!("review.accept", "Accept Revision", ["Review", "Compare"], None, "{index} | {all: true}", comparing, accept),
+        cmd!(noundo "review.compare", "Compare", ["Review", "Compare"], None, "{path} | {name, data: base64} — compare the open deck with another file → {name, changes: [{kind, slide, number, onlyIn?, revised, details}]}; each change is described from the open deck: `onlyIn: \"deck\"` is in the deck and not in the file, `onlyIn: \"file\"` is in the file and not in the deck, `changed` is in both and differs; `slide` is 0-based, `number` the editor's slide number; review.accept takes the FILE's side, so it deletes what is only in the deck (Accept All keeps the deck's last remaining slide rather than deleting it)", has_doc, compare),
+        cmd!(query "review.revisions", "Revisions", [], None, "{} → {name, changes: [{kind: inserted|deleted|changed, slide (0-based), number (editor's), onlyIn?: deck|file, revised, details}]}; `onlyIn: deck` = only in the open deck, `file` = only in the compared file; accepting takes the file's side", has_doc, revisions),
+        cmd!(
+            "review.accept",
+            "Accept Revision",
+            ["Review", "Compare"],
+            None,
+            "{index} | {all: true} — accept the compared file's side of a change (or all): what is only in the file is added, what is only in the open deck is deleted (all: true leaves the deck's last remaining slide in place), a changed slide becomes the file's",
+            comparing,
+            accept
+        ),
         cmd!(
             noundo "review.reject",
             "Reject Revision",
diff --git a/crates/engine/src/cmd/shape.rs b/crates/engine/src/cmd/shape.rs
--- a/crates/engine/src/cmd/shape.rs
+++ b/crates/engine/src/cmd/shape.rs
@@ -149,7 +149,15 @@ pub fn specs() -> Vec<CommandSpec> {
             has_selection,
             picture_adjust
         ),
-        cmd!("picture.reset", "Reset Picture", ["Picture Format", "Adjust"], None, "{ids?}", has_selection, picture_reset),
+        cmd!(
+            "picture.reset",
+            "Reset Picture",
+            ["Picture Format", "Adjust"],
+            None,
+            "{ids?} (discards the picture's formatting: adjustments, artistic effect, crop, transparency, picture-style effects and outline; the frame shape from Change Shape or Crop to Shape, the size and the position stay)",
+            has_selection,
+            picture_reset
+        ),
         cmd!(
             "picture.change",
             "Change Picture",
diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs
--- a/crates/engine/src/compare.rs
+++ b/crates/engine/src/compare.rs
@@ -92,6 +92,12 @@ pub struct Change {
     pub kind: ChangeKind,
     /// Index in the open deck (for inserted: where it would go).
     pub slide: usize,
+    /// The editor's number of that slide (`slide` + 1).
+    pub number: usize,
+    /// Which side has it alone: `deck` (the open presentation; accepting deletes it) or `file`
+    /// (the compared file; accepting adds it). Absent when both have it and differ.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub only_in: Option<&'static str>,
     /// Index in the revised deck (none for deleted).
     pub revised: Option<usize>,
     /// What differs, readable: `Title 1: text`, `Picture 3 inserted`, `notes`…
@@ -118,19 +124,33 @@ pub fn diff(cur: &Presentation, rev: &Presentation, by_position: bool) -> Vec<Ch
         let (Some(a), Some(b)) = (cur.slides.get(*i), rev.slides.get(*j)) else { continue };
         let details = slide_details(cur, a, rev, b);
         if !details.is_empty() {
-            out.push(Change { kind: ChangeKind::Changed, slide: *i, revised: Some(*j), details });
+            out.push(Change { kind: ChangeKind::Changed, slide: *i, number: i.saturating_add(1), only_in: None, revised: Some(*j), details });
         }
     }
     for (i, s) in cur.slides.iter().enumerate() {
         if !cur_matched.contains(&i) {
-            out.push(Change { kind: ChangeKind::Deleted, slide: i, revised: None, details: vec![slide_title(s)] });
+            out.push(Change {
+                kind: ChangeKind::Deleted,
+                slide: i,
+                number: i.saturating_add(1),
+                only_in: Some("deck"),
+                revised: None,
+                details: vec![slide_title(s)],
+            });
         }
     }
     for (j, s) in rev.slides.iter().enumerate() {
         if !rev_matched.contains(&j) {
             // After the open slide that matches the revised slide before it.
             let at = (0..j).rev().find_map(|k| pairs.iter().find(|p| p.1 == k).map(|p| p.0 + 1)).unwrap_or(0);
-            out.push(Change { kind: ChangeKind::Inserted, slide: at, revised: Some(j), details: vec![slide_title(s)] });
+            out.push(Change {
+                kind: ChangeKind::Inserted,
+                slide: at,
+                number: at.saturating_add(1),
+                only_in: Some("file"),
+                revised: Some(j),
+                details: vec![slide_title(s)],
+            });
         }
     }
     out.sort_by_key(|c| (c.slide, c.revised.unwrap_or(0)));
@@ -184,9 +204,15 @@ fn shape_details(cp: &Presentation, a: &Shape, rp: &Presentation, b: &Shape, out
     match (&a.xfrm, &b.xfrm) {
         (Some(x), Some(y)) if (x.x, x.y) != (y.x, y.y) && (x.w, x.h) == (y.w, y.h) => what.push("moved"),
         (Some(x), Some(y)) if (x.w, x.h) != (y.w, y.h) => what.push("resized"),
-        (x, y) if x.as_ref().map(|x| x.rot) != y.as_ref().map(|y| y.rot) => what.push("rotated"),
+        // No stored box means the layout's: unrotated and unflipped, the same as a stored 0.
+        (x, y) if x.as_ref().map_or(0.0, |x| x.rot) != y.as_ref().map_or(0.0, |y| y.rot) => what.push("rotated"),
         _ => {}
     }
+    // A flip is reported alongside a move, resize or rotation, not hidden behind it.
+    let flips = |x: &Option<deckcraft_model::Xfrm>| x.as_ref().map_or((false, false), |x| (x.flip_h, x.flip_v));
+    if flips(&a.xfrm) != flips(&b.xfrm) {
+        what.push("flipped");
+    }
     if a.fill != b.fill || a.line != b.line || a.style != b.style || a.effects != b.effects {
         what.push("formatting");
     }
@@ -230,12 +256,12 @@ fn slide_details(cp: &Presentation, a: &Slide, rp: &Presentation, b: &Slide) ->
                     shape_details(cp, sa, rp, sb, &mut out);
                 }
             }
-            _ => out.push(format!("{} deleted", shape_label(sa))),
+            _ => out.push(format!("{} deleted (only in the deck)", shape_label(sa))),
         }
     }
     for (k, sb) in b.shapes.iter().enumerate() {
         if !used.contains(&k) {
-            out.push(format!("{} inserted", shape_label(sb)));
+            out.push(format!("{} inserted (only in the file)", shape_label(sb)));
         }
     }
     let order = |s: &Slide| s.shapes.iter().map(|x| x.name.clone()).collect::<Vec<_>>();
@@ -460,6 +486,41 @@ mod tests {
         assert_eq!(s.doc().unwrap().doc.slides.len(), n, "comparing alone changes nothing");
     }
 
+    /// NN6: a slide reset drops the placeholders' stored boxes; comparing it with the copy from
+    /// before the reset must not call that a rotation (no stored box is rotation 0).
+    #[test]
+    fn a_reset_slide_is_not_rotated_against_its_earlier_copy() {
+        let mut s = Session::with_new();
+        s.execute("slide.new", &json!({"layout": "titleAndContent", "title": "T", "body": "b"})).unwrap();
+        let ids: Vec<u32> = s.doc().unwrap().doc.slides[1].shapes.iter().map(|x| x.id.0).collect();
+        for id in &ids {
+            s.execute("shape.move", &json!({"ids": [id], "dx": 10, "dy": 0})).unwrap();
+            s.execute("shape.move", &json!({"ids": [id], "dx": -10, "dy": 0})).unwrap();
+        }
+        let before = s.execute("file.saveBytes", &json!({})).unwrap()["data"].as_str().unwrap().to_string();
+        s.execute("slide.reset", &json!({"index": 1})).unwrap();
+        let v = s.execute("review.compare", &json!({"name": "before.deckcraft", "data": before})).unwrap();
+        assert!(!v.to_string().contains("rotated"), "{v}");
+    }
+
+    #[test]
+    fn each_change_says_which_side_it_is_on() {
+        let (mut s, rev, _) = decks();
+        let v = s.execute("review.compare", &json!({"name": "rev.deckcraft", "data": rev})).unwrap();
+        for c in v["changes"].as_array().unwrap() {
+            assert_eq!(c["number"].as_u64().unwrap(), c["slide"].as_u64().unwrap() + 1, "{c}");
+            match c["kind"].as_str().unwrap() {
+                "deleted" => assert_eq!(c["onlyIn"], "deck", "{c}"),
+                "inserted" => assert_eq!(c["onlyIn"], "file", "{c}"),
+                _ => assert!(c.get("onlyIn").is_none(), "{c}"),
+            }
+        }
+        // A shape only in the open deck reads as only in the deck.
+        s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [10.0, 10.0, 40.0, 40.0]})).unwrap();
+        let v = s.execute("review.revisions", &json!({})).unwrap();
+        assert!(v.to_string().contains("deleted (only in the deck)"), "{v}");
+    }
+
     #[test]
     fn accept_one_then_all_makes_the_decks_equal_and_undoes() {
         let (mut s, rev, n) = decks();
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -638,7 +638,28 @@ impl Session {
                 self.journal.drain(..1000);
             }
         }
-        Ok(r)
+        Ok(if outer { self.with_editing(id, r) } else { r })
+    }
+
+    /// In Slide Master / layout (or handout / notes master) view the insert commands edit the
+    /// master or layout, not a slide; their answer says so with `editing: {kind, name}`, as
+    /// `slide.inspect` does. Nothing is added on slides, so those answers stay as they were.
+    fn with_editing(&self, id: &str, mut r: Value) -> Value {
+        let inserts = id.starts_with("insert.") || matches!(id, "shape.insert" | "shape.freeform" | "shape.connect" | "slide.new");
+        let (true, Some(st), Some(o)) = (inserts, self.active(), r.as_object_mut()) else { return r };
+        let doc = &st.doc;
+        let edited = match st.selection.target {
+            Target::Master { master } => doc.masters.get(master).map(|m| ("master", m.name.clone())),
+            Target::Layout { master, layout } => doc.masters.get(master).and_then(|m| m.layouts.get(layout)).map(|l| ("layout", l.name.clone())),
+            t @ (Target::Handout | Target::NotesMaster) => {
+                page_master(doc, t).map(|m| (if matches!(t, Target::Handout) { "handout" } else { "notesMaster" }, m.name.clone()))
+            }
+            Target::Slides => None,
+        };
+        if let Some((kind, name)) = edited {
+            o.entry("editing").or_insert_with(|| serde_json::json!({"kind": kind, "name": name}));
+        }
+        r
     }
 
     pub fn commands(&self) -> Vec<CommandInfo> {
@@ -651,6 +672,7 @@ impl Session {
         let mut doc = (*st.doc).clone();
         let mut sel = st.selection.clone();
         let r = f(&mut doc, &mut sel)?;
+        cmd::arrange::refit_groups(&st.doc, &mut doc);
         connect::reroute(&mut doc, &sel);
         st.doc = Arc::new(doc);
         st.selection = sel;
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
@@ -4724,3 +4724,116 @@ fn slide_hide_and_layout_reject_invalid_index() {
     }
     assert!(s.doc().unwrap().doc.slides.iter().all(|x| !x.hidden));
 }
+
+/// HH9: Reset Picture discards the picture's formatting (adjustments, artistic effect, picture
+/// style effects and outline) but not the frame's shape (Change Shape / Crop to Shape).
+#[test]
+fn picture_reset_keeps_the_frame_shape() {
+    let mut img = image::RgbaImage::new(40, 30);
+    img.pixels_mut().for_each(|p| *p = image::Rgba([200, 30, 30, 255]));
+    let mut png = Vec::new();
+    image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
+    let mut s = Session::with_new();
+    s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
+    s.execute("insert.picture", &json!({"name": "a.png", "data": cmd::base64_encode(&png)})).unwrap();
+    s.execute("shape.change", &json!({"preset": "roundRect"})).unwrap();
+    s.execute("picture.adjust", &json!({"brightness": 0.3})).unwrap();
+    let geom = s.doc().unwrap().selected_shapes()[0].geom.clone();
+    assert_ne!(geom, deckcraft_model::Geom::preset("rect"));
+    s.execute("picture.reset", &json!({})).unwrap();
+    let sh = s.doc().unwrap().selected_shapes()[0].clone();
+    assert_eq!(sh.geom, geom, "the frame shape stays");
+    match &sh.kind {
+        deckcraft_model::ShapeKind::Picture { fill } => assert_eq!(fill.adjust, Default::default()),
+        _ => panic!("not a picture"),
+    }
+    let doc = command_specs().iter().find(|c| c.id == "picture.reset").unwrap().params;
+    assert!(doc.contains("frame shape"), "{doc}");
+}
+
+/// HH12: exporting the same slide twice to one PNG path after an edit overwrites the file with
+/// the new picture (not a bug: the second file differs).
+#[test]
+fn exporting_a_slide_again_overwrites_the_png_with_the_edit() {
+    /// Removes the folder when the test ends, a failed assertion included.
+    struct Cleanup(std::path::PathBuf);
+    impl Drop for Cleanup {
+        fn drop(&mut self) {
+            let _ = std::fs::remove_dir_all(&self.0);
+        }
+    }
+    let dir = std::env::temp_dir().join(format!("deckcraft-hh12-{}", std::process::id()));
+    let _cleanup = Cleanup(dir.clone());
+    let png = dir.join("slide.png");
+    std::fs::create_dir_all(&dir).unwrap();
+    let mut s = session();
+    let path = png.to_string_lossy().to_string();
+    s.execute("file.export", &json!({"path": path, "format": "png", "slide": 0, "scale": 0.5})).unwrap();
+    let first = std::fs::read(&png).unwrap();
+    s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 400, 300]})).unwrap();
+    s.execute("shape.fill", &json!({"color": "#FF0000"})).unwrap();
+    s.execute("file.export", &json!({"path": path, "format": "png", "slide": 0, "scale": 0.5})).unwrap();
+    let second = std::fs::read(&png).unwrap();
+    assert_ne!(first, second, "the second export replaced the file with the edited slide");
+}
+
+/// MM4: moving a group's child through a command refits the group's box to its children, as
+/// PowerPoint does, and the children stay where they were drawn.
+#[test]
+fn moving_a_group_child_refits_the_group() {
+    let mut s = session();
+    s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
+    let ins = |s: &mut Session, p: serde_json::Value| s.execute("shape.insert", &p).unwrap()["id"].as_u64().unwrap();
+    let a = ins(&mut s, json!({"preset": "rect", "rect": [100, 100, 100, 50]}));
+    let b = ins(&mut s, json!({"preset": "rect", "rect": [300, 100, 100, 50]}));
+    let g = s.execute("arrange.group", &json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap();
+    let gb = |s: &Session| s.doc().unwrap().shape(ShapeId(g as u32)).unwrap().xfrm.unwrap();
+    assert_eq!((gb(&s).x, gb(&s).w), (100.0, 300.0));
+    s.execute("shape.move", &json!({"id": b, "dx": 100, "dy": 60})).unwrap();
+    let x = gb(&s);
+    assert_eq!((x.x, x.y, x.w, x.h), (100.0, 100.0, 400.0, 110.0), "group follows its children: {x:?}");
+    // The child is where it was moved to on the slide.
+    let r = s.execute("slide.inspect", &json!({})).unwrap();
+    assert!(r.to_string().contains("400"), "{r}");
+    s.execute("shape.setBounds", &json!({"ids": [a], "x": 20, "y": 20, "w": 100, "h": 50})).unwrap();
+    let x = gb(&s);
+    assert_eq!((x.x, x.y, x.w), (20.0, 20.0, 480.0), "{x:?}");
+}
+
+#[test]
+fn action_button_fill_follows_the_theme() {
+    let mut s = session();
+    let id = s.execute("insert.actionButton", &json!({"kind": "next"})).unwrap()["id"].clone();
+    let fill = |s: &mut Session| {
+        let r = s.execute("slide.inspect", &json!({})).unwrap();
+        r["shapes"].as_array().unwrap().iter().find(|x| x["id"] == id).unwrap()["fill"].clone()
+    };
+    let before = fill(&mut s);
+    assert!(before.as_str().is_some_and(|f| f.starts_with('#')), "{before}");
+    s.execute("design.theme", &json!({"name": "Ember"})).unwrap();
+    let after = fill(&mut s);
+    assert_ne!(before, after, "the button takes the new theme's accent1");
+    let st = s.doc().unwrap();
+    let sh = st.doc.slides[0].shapes.iter().find(|x| x.fill.is_none() && x.style.is_some()).unwrap();
+    assert!(sh.fill.is_none(), "no explicit fill: the style binds it to accent1");
+}
+
+/// In Slide Master / layout view the insert commands edit the master or layout; their answers say
+/// so with `editing`, and on slides carry no such key.
+#[test]
+fn insert_answers_carry_editing_in_master_and_layout_view() {
+    let mut s = session();
+    let ins = json!({"preset": "rect", "rect": [10.0, 10.0, 100.0, 100.0]});
+    s.execute("view.slideMaster", &json!({"layout": 0})).unwrap();
+    let r = s.execute("shape.insert", &ins).unwrap();
+    assert_eq!(r["editing"]["kind"], "layout", "{r}");
+    assert!(r["editing"]["name"].is_string(), "{r}");
+    let r = s.execute("insert.textBox", &json!({"rect": [10.0, 10.0, 100.0, 50.0], "text": "x"})).unwrap();
+    assert_eq!(r["editing"]["kind"], "layout", "{r}");
+    s.execute("view.slideMaster", &json!({"master": 0})).unwrap();
+    let r = s.execute("shape.insert", &ins).unwrap();
+    assert_eq!(r["editing"]["kind"], "master", "{r}");
+    s.execute("view.closeMaster", &json!({})).unwrap();
+    let r = s.execute("shape.insert", &ins).unwrap();
+    assert!(r.get("editing").is_none(), "{r}");
+}
diff --git a/crates/text/src/datetime.rs b/crates/text/src/datetime.rs
--- a/crates/text/src/datetime.rs
+++ b/crates/text/src/datetime.rs
@@ -104,11 +104,15 @@ pub fn field_text(kind: &str, lang: Option<&str>, now: DateTime) -> Option<Strin
     let ampm = if now.hour < 12 { "AM" } else { "PM" };
     let (mi, s) = (now.minute, now.second);
     let short = short_date(&lang, now);
+    // British-style English writes the day before the month ("9 October 2026").
+    let day_first = matches!(lang.as_str(), "en-gb" | "en-au" | "en-nz" | "en-ie" | "en-in");
     let named = |text: String| english.then_some(text);
     match kind {
         "datetime" | "datetimeFigureOut" | "datetime1" => Some(short),
+        "datetime2" if day_first => named(format!("{day}, {d} {month} {y}")),
         "datetime2" => named(format!("{day}, {month} {d}, {y}")),
         "datetime3" => named(format!("{d} {month} {y}")),
+        "datetime4" if day_first => named(format!("{d} {month} {y}")),
         "datetime4" => named(format!("{month} {d}, {y}")),
         "datetime5" => named(format!("{d}-{mon}-{yy:02}")),
         "datetime6" => named(format!("{month} {yy:02}")),
@@ -183,7 +187,10 @@ mod tests {
         // Month names in another language: not computed (the field keeps its saved text).
         assert_eq!(field_text("datetime2", Some("de-DE"), FRI), None);
         assert_eq!(field_text("datetime12", Some("fr-FR"), FRI), None);
-        assert_eq!(field_text("datetime4", Some("en-GB"), FRI).as_deref(), Some("October 9, 2026"));
+        assert_eq!(field_text("datetime4", Some("en-GB"), FRI).as_deref(), Some("9 October 2026"));
+        assert_eq!(field_text("datetime2", Some("en-GB"), FRI).as_deref(), Some("Friday, 9 October 2026"));
+        assert_eq!(field_text("datetime4", Some("en-AU"), FRI).as_deref(), Some("9 October 2026"));
+        assert_eq!(field_text("datetime4", Some("en-US"), FRI).as_deref(), Some("October 9, 2026"));
     }
 
     #[test]
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -385,7 +385,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
     ("view.grayscale", "Grayscale", None, "{on?: bool}"),
     ("view.newWindow", "New Window", None, "{on?: bool, slide?: index, delta?: int}"),
     ("show.start", "Slide Show", Some("F5"), "{from?: index, custom?: name}"),
-    ("show.presenter", "Presenter View", None, "{on?: bool}"),
+    (
+        "show.presenter",
+        "Presenter View",
+        None,
+        "{on?: bool} → {on, window} (presenter view, with notes, a clock and the next slide, appears in its own separate window beside the show window when the show starts; window is always true)",
+    ),
     ("show.end", "End Show", None, "{}"),
     ("animation.preview", "Preview Animations", None, "{on?: bool, from?: effect index}"),
     ("app.home", "Home", None, "{on?}"),
@@ -971,7 +976,10 @@ impl SlideApp {
                     self.start_show(from, false);
                 }
             }
-            "show.presenter" => self.ui.presenter_view = self.flag(p, self.ui.presenter_view),
+            "show.presenter" => {
+                self.ui.presenter_view = self.flag(p, self.ui.presenter_view);
+                return Ok(json!({"on": self.ui.presenter_view, "window": true}));
+            }
             "show.end" => {
                 self.show = None;
                 self.media.stop_owned(media::Owner::Show);
@@ -2126,6 +2134,8 @@ mod ime_tests;
 #[cfg(test)]
 mod last_color_tests;
 #[cfg(test)]
+mod presenter_tests;
+#[cfg(test)]
 mod replacefonts_tests;
 #[cfg(test)]
 mod slide_size_tests;
diff --git a/crates/ui-egui/src/presenter_tests.rs b/crates/ui-egui/src/presenter_tests.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/presenter_tests.rs
@@ -0,0 +1,18 @@
+//! JJ3: `show.presenter` answers the setting and where presenter view appears.
+#![allow(clippy::unwrap_used, clippy::expect_used)]
+
+use serde_json::json;
+
+use crate::{Services, SlideApp};
+use deckcraft_engine::Session;
+
+#[test]
+fn show_presenter_answers_on_and_window() {
+    let mut app = SlideApp::new(Session::with_new(), Services::default());
+    let v = app.run("show.presenter", json!({"on": true})).unwrap();
+    assert_eq!(v, json!({"on": true, "window": true}));
+    let v = app.run("show.presenter", json!({})).unwrap();
+    assert_eq!(v["on"], false, "no `on` toggles: {v}");
+    let doc = crate::UI_COMMANDS.iter().find(|c| c.0 == "show.presenter").unwrap().3;
+    assert!(doc.contains("separate window"), "{doc}");
+}
