diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -142,6 +142,7 @@ app's JSON control channel (`deckcraft --control PORT`).
 | 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) |
 | 2026-10-23 | Chart text colour: `chart.options {textColor}` and Font Color on a selected chart colour its title, axes, legend and data labels; the colour is written to the chart part (`c:txPr` on the axes, legend, labels and chart, the title's run defaults) so an imported chart keeps its `txPr` colours through an edit and a save; text without a colour of its own takes PowerPoint's default, the theme's text colour at 65 % plus 35 % (595959 on a light theme, light grey on a dark one) |
 | 2026-10-24 | A picture's own fill shows behind its transparent pixels, and an embedded object (an old equation, `p:oleObj`) takes the fill and outline of its fallback picture, as PowerPoint for Mac draws them (synthetic probe: a half-transparent PNG on orange and on yellow with an outline); on a private 166-deck corpus 51 slides of equations on coloured boxes move closer to PowerPoint's PDF, none further |
+| 2026-10-22 | Slide Show with Presenter View, as in PowerPoint: with two screens the slides go full screen on one and Presenter View on the presenter's; with one screen only the slides show. Both windows on one screen covered one, and a covered window stalled the show at about a frame a second. In a Wayland session DeckCraft now starts on Wayland (`DECKCRAFT_X11` asks for X11), where windows go to the screen asked for |
 | 2026-10-23 | Format Background ▸ Apply to All also sets the background of the slide masters the slides use (and clears their layouts' overrides), so new slides keep it (#891) |
 | 2026-10-22 | Table text takes the master's other-text style (`otherStyle`), shapes and text boxes the presentation's default text style, as PowerPoint for Mac draws them (synthetic probe: 14 pt cells under a 14 pt `otherStyle` and a 26 pt `defaultTextStyle`); a private deck's table had been drawn 20 pt instead of 18 |
 | 2026-10-18 | Find (Ctrl+F): "Find what:" has the keyboard as soon as the dialog opens, and keeps it after Enter finds a match, so the search can be typed and changed without clicking the field (#951) |
diff --git a/apps/deckcraft/Cargo.toml b/apps/deckcraft/Cargo.toml
--- a/apps/deckcraft/Cargo.toml
+++ b/apps/deckcraft/Cargo.toml
@@ -29,6 +29,8 @@ rfd = { workspace = true }
 arboard = { workspace = true, features = ["image-data"] }
 image = { workspace = true }
 cpal = "0.16"
+# The screens a slide show and Presenter View go to (the same winit eframe runs on).
+winit = { version = "0.30", default-features = false }
 
 [target.'cfg(target_os = "macos")'.dependencies]
 # Finder double-click, Open With, Dock drops and `open -a DeckCraft deck.pptx` arrive as a
diff --git a/apps/deckcraft/src/display.rs b/apps/deckcraft/src/display.rs
--- a/apps/deckcraft/src/display.rs
+++ b/apps/deckcraft/src/display.rs
@@ -1,54 +1,46 @@
 //! Which windowing system to start on, on Linux and the BSDs.
 //!
-//! DeckCraft prefers X11 when `DISPLAY` is set (see `main`). A stale `DISPLAY` (left over from
-//! an ended `ssh -X` session or a desktop with Xwayland turned off) used to make start-up fail
-//! with winit's "failed to open display" even though a Wayland compositor was running. When a
-//! local `DISPLAY` names an X server whose socket isn't there and Wayland is available, start on
-//! Wayland instead. With no display at all, say so plainly.
-
-use std::path::Path;
+//! In a Wayland session DeckCraft starts on Wayland, even when Xwayland also sets `DISPLAY`.
+//! Under Xwayland the compositor can ignore where DeckCraft puts its windows: a slide show's
+//! slides stayed on the presenter's screen, over Presenter View, and the covered window stalled
+//! the show. A stale `DISPLAY` (an ended `ssh -X` session, Xwayland turned off) no longer matters
+//! either. `DECKCRAFT_X11` asks for X11 anyway; without Wayland, X11 is used. With no display at
+//! all, say so plainly.
 
 /// The windowing system to ask winit for.
 #[derive(Debug, PartialEq, Eq)]
 pub enum Choice {
     X11,
-    /// Let winit decide (Wayland when `WAYLAND_DISPLAY` is set).
+    /// Let winit decide (Wayland when `WAYLAND_DISPLAY` or `WAYLAND_SOCKET` is set).
     Default,
-    /// Neither `DISPLAY` nor `WAYLAND_DISPLAY`: there is nothing to open a window on.
+    /// Neither `DISPLAY` nor Wayland: there is nothing to open a window on.
     None,
 }
 
-/// The X server number of a local `DISPLAY` (`:1`, `:1.0`, `unix:1`); `None` for a remote one
-/// (`host:10.0`, as `ssh -X` sets) or one we can't read, which are left to X11 itself.
-fn local_display_number(display: &str) -> Option<u32> {
-    let rest = display.strip_prefix(':').or_else(|| display.strip_prefix("unix:"))?;
-    rest.split('.').next()?.parse().ok()
-}
-
-/// Choose from the environment. `socket_exists` checks an X socket path (the filesystem in
-/// [`choose_from_env`], a stub in tests).
-pub fn choose(display: Option<&str>, wayland: Option<&str>, force_wayland: bool, socket_exists: impl Fn(&Path) -> bool) -> Choice {
-    let display = display.filter(|d| !d.is_empty());
+/// Choose from the environment: `DISPLAY`, the Wayland connection ([`wayland`]) and whether X11
+/// is asked for.
+pub fn choose(display: Option<&str>, wayland: Option<&str>, force_x11: bool) -> Choice {
+    let display = display.is_some_and(|d| !d.is_empty());
     let wayland = wayland.is_some_and(|w| !w.is_empty());
-    match display {
-        None if wayland => Choice::Default,
-        None => Choice::None,
-        Some(_) if force_wayland && wayland => Choice::Default,
-        Some(d) => match local_display_number(d) {
-            Some(n) if wayland && !socket_exists(&Path::new("/tmp/.X11-unix").join(format!("X{n}"))) => {
-                log::warn!("DISPLAY={d} has no X server; starting on Wayland");
-                Choice::Default
-            }
-            _ => Choice::X11,
-        },
+    match (display, wayland) {
+        (true, true) if force_x11 => Choice::X11,
+        (_, true) => Choice::Default,
+        (true, false) => Choice::X11,
+        (false, false) => Choice::None,
     }
 }
 
+/// The Wayland connection winit would use: `WAYLAND_DISPLAY`, or else `WAYLAND_SOCKET` (a
+/// connection handed over by whoever started DeckCraft).
+fn wayland(display: Option<String>, socket: Option<String>) -> Option<String> {
+    display.filter(|d| !d.is_empty()).or(socket)
+}
+
 /// [`choose`] for this process.
 pub fn choose_from_env() -> Choice {
     let display = std::env::var("DISPLAY").ok();
-    let wayland = std::env::var("WAYLAND_DISPLAY").ok();
-    choose(display.as_deref(), wayland.as_deref(), std::env::var_os("DECKCRAFT_WAYLAND").is_some(), |p| p.exists())
+    let wayland = wayland(std::env::var("WAYLAND_DISPLAY").ok(), std::env::var("WAYLAND_SOCKET").ok());
+    choose(display.as_deref(), wayland.as_deref(), std::env::var_os("DECKCRAFT_X11").is_some())
 }
 
 #[cfg(test)]
@@ -56,30 +48,31 @@ mod tests {
     use super::*;
 
     #[test]
-    fn a_stale_local_display_falls_back_to_wayland() {
-        let none = |_: &Path| false;
-        let all = |_: &Path| true;
-        assert_eq!(choose(Some(":1"), Some("wayland-0"), false, none), Choice::Default);
-        assert_eq!(choose(Some(":1.0"), Some("wayland-0"), false, none), Choice::Default);
-        assert_eq!(choose(Some("unix:0"), Some("wayland-0"), false, none), Choice::Default);
-        // A live X server, no Wayland, or a remote DISPLAY: X11 as before.
-        assert_eq!(choose(Some(":1"), Some("wayland-0"), false, all), Choice::X11);
-        assert_eq!(choose(Some(":1"), None, false, none), Choice::X11);
-        assert_eq!(choose(Some("host:10.0"), Some("wayland-0"), false, none), Choice::X11);
-        // The socket checked is the display's own.
-        assert_eq!(choose(Some(":3"), Some("w"), false, |p: &Path| p.ends_with("X3")), Choice::X11);
-        assert_eq!(choose(Some(":3"), Some("w"), false, |p: &Path| p.ends_with("X0")), Choice::Default);
+    fn a_wayland_session_starts_on_wayland() {
+        // Xwayland's DISPLAY, live or stale, doesn't take it over.
+        assert_eq!(choose(Some(":0"), Some("wayland-0"), false), Choice::Default);
+        assert_eq!(choose(Some(":1.0"), Some("wayland-1"), false), Choice::Default);
+        assert_eq!(choose(None, Some("wayland-0"), false), Choice::Default);
+        assert_eq!(choose(Some(""), Some("wayland-0"), false), Choice::Default);
+    }
+
+    #[test]
+    fn a_wayland_socket_is_a_wayland_session() {
+        let socket = wayland(None, Some("5".into()));
+        assert_eq!(choose(Some(":0"), socket.as_deref(), false), Choice::Default);
+        assert_eq!(wayland(Some(String::new()), Some("5".into())).as_deref(), Some("5"));
+        assert_eq!(wayland(Some("wayland-0".into()), Some("5".into())).as_deref(), Some("wayland-0"));
+        assert_eq!(wayland(None, None), None);
     }
 
     #[test]
-    fn wayland_only_forced_and_no_display_at_all() {
-        let all = |_: &Path| true;
-        assert_eq!(choose(None, Some("wayland-0"), false, all), Choice::Default);
-        assert_eq!(choose(Some(""), Some("wayland-0"), false, all), Choice::Default);
-        assert_eq!(choose(Some(":0"), Some("wayland-0"), true, all), Choice::Default);
-        // Forcing Wayland without a compositor keeps X11 rather than failing.
-        assert_eq!(choose(Some(":0"), None, true, all), Choice::X11);
-        assert_eq!(choose(None, None, false, all), Choice::None);
-        assert_eq!(choose(Some(""), Some(""), false, all), Choice::None);
+    fn x11_when_asked_or_alone_and_nothing_without_a_display() {
+        assert_eq!(choose(Some(":0"), Some("wayland-0"), true), Choice::X11);
+        assert_eq!(choose(Some(":0"), None, false), Choice::X11);
+        assert_eq!(choose(Some("host:10.0"), Some(""), false), Choice::X11);
+        // Asking for X11 without an X server keeps Wayland rather than failing.
+        assert_eq!(choose(None, Some("wayland-0"), true), Choice::Default);
+        assert_eq!(choose(None, None, false), Choice::None);
+        assert_eq!(choose(Some(""), Some(""), true), Choice::None);
     }
 }
diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs
--- a/apps/deckcraft/src/main.rs
+++ b/apps/deckcraft/src/main.rs
@@ -135,6 +135,21 @@ fn services() -> Services {
     }
 }
 
+/// The screens `window` can show on, in winit's order (the one `SetMonitor` takes). A screen
+/// winit lists twice (Wayland does) counts once, by its first number.
+fn screens(window: &winit::window::Window) -> Option<deckcraft_ui_egui::Screens> {
+    let all: Vec<_> = window.available_monitors().collect();
+    let id = |m: &winit::monitor::MonitorHandle| (m.name(), m.position(), m.size());
+    // Each listed screen's number, mapped to the first listing of the same screen.
+    let first = |i: usize| all.get(i).and_then(|m| all.iter().position(|x| id(x) == id(m)));
+    let list: Vec<usize> = (0..all.len()).filter(|i| first(*i) == Some(*i)).collect();
+    if list.is_empty() {
+        return None;
+    }
+    let find = |m: Option<winit::monitor::MonitorHandle>| m.and_then(|m| all.iter().position(|x| *x == m)).and_then(first);
+    Some(deckcraft_ui_egui::Screens { list, current: find(window.current_monitor()), primary: find(window.primary_monitor()) })
+}
+
 const APP_ID: &str = "ai.storyteller.deckcraft";
 
 const USAGE: &str = "\
@@ -259,7 +274,11 @@ fn main() -> eframe::Result {
         options,
         Box::new(move |cc| {
             deckcraft_ui_egui::install_ime_pass_hooks(&cc.egui_ctx);
-            let mut app = SlideApp::new(Session::new(), services());
+            let mut services = services();
+            if let Some(window) = cc.winit_window().cloned() {
+                services.screens = Some(Box::new(move || screens(&window)));
+            }
+            let mut app = SlideApp::new(Session::new(), services);
             load_prefs(&mut app);
             // AutoRecover: reopen what a previous run left unsaved, then keep it current.
             if std::env::var_os("DECKCRAFT_NO_RECOVERY").is_none() {
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
@@ -261,6 +261,36 @@ pub struct Services {
     pub clipboard_set_html: Option<Box<dyn FnMut(&str, &str) -> bool>>,
     /// Audio output for media playback (cpal on desktop); without one media plays silently.
     pub audio_out: Option<Box<dyn deckcraft_media::AudioOut>>,
+    /// The screens the computer shows to, read when a slide show starts. Without it (the web,
+    /// tests) the show stays where the window is.
+    pub screens: Option<Box<dyn FnMut() -> Option<Screens>>>,
+}
+
+/// The screens connected, numbered as the windowing system lists them (the numbers a window is
+/// sent to with `ViewportCommand::SetMonitor`). Wayland can list a screen more than once: `list`
+/// holds one number per screen.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct Screens {
+    pub list: Vec<usize>,
+    /// The screen DeckCraft's window is on, when known.
+    pub current: Option<usize>,
+    /// The system's main screen, when known (Wayland doesn't say).
+    pub primary: Option<usize>,
+}
+
+impl Screens {
+    /// Where a slide show with Presenter View goes, as in PowerPoint: the slides full screen on
+    /// another screen and Presenter View on the presenter's (the one DeckCraft is on). `None`
+    /// with one screen: there, PowerPoint shows only the slides. Returns (slides, presenter).
+    pub fn for_presenter_view(&self) -> Option<(usize, usize)> {
+        if self.list.len() < 2 {
+            return None;
+        }
+        let listed = |i: &usize| self.list.contains(i);
+        let presenter = self.current.filter(listed).or(self.primary.filter(listed)).or_else(|| self.list.first().copied())?;
+        let slides = self.list.iter().copied().find(|i| *i != presenter)?;
+        Some((slides, presenter))
+    }
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -1445,9 +1475,20 @@ impl SlideApp {
     pub fn start_show(&mut self, from: usize, reading: bool) {
         self.anim_preview = None;
         self.media.stop_owned(media::Owner::Editor);
+        let wanted = self.ui.presenter_view && !reading;
+        // Presenter View needs a screen of its own: on one screen, PowerPoint shows only the
+        // slides (two windows on one screen also hid one, and a hidden window stalled the show).
+        let (presenter, screens) = match wanted.then(|| self.services.screens.as_mut().and_then(|f| f())).flatten() {
+            Some(s) => match s.for_presenter_view().inspect(|p| log::info!("slide show on screen {} and Presenter View on {} of {s:?}", p.0, p.1)) {
+                Some(placed) => (true, Some(placed)),
+                None => (false, None),
+            },
+            None => (wanted, None),
+        };
         if let Some(d) = self.session.active() {
             self.show_instance = self.show_instance.wrapping_add(1).max(1);
-            let mut show = show::Show::new(&d.doc, from, reading, self.ui.presenter_view && !reading);
+            let mut show = show::Show::new(&d.doc, from, reading, presenter);
+            show.screens = screens;
             show.instance = self.show_instance;
             self.show = Some(show);
         }
diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs
--- a/crates/ui-egui/src/show.rs
+++ b/crates/ui-egui/src/show.rs
@@ -36,6 +36,12 @@ pub struct Show {
     /// The next slide's first frame, rendered in the background while this one is shown so
     /// advancing only uploads it (#240).
     prefetch: Option<Prefetch>,
+    /// The screens the slides and Presenter View fill, when DeckCraft placed them (two screens
+    /// or more); otherwise the slides fill the window's screen.
+    pub screens: Option<(usize, usize)>,
+    /// Where the window was (outside and inside, in points) before the slides were sent to
+    /// another screen, to put it back after the show: X11 brings it back on the slides' screen.
+    window_before: Option<(Rect, Rect)>,
     /// The current slide's backdrop, under its animation frames.
     backdrop: Option<Backdrop>,
     blank: Option<Color32>,
@@ -334,6 +340,8 @@ impl Show {
             overlay: None,
             next_tex: None,
             prefetch: None,
+            screens: None,
+            window_before: None,
             backdrop: None,
             blank: None,
             grid: false,
@@ -870,7 +878,13 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
         show.timer = PresenterTimer::start(now);
         show.last_move = now;
         if show_fills_screen(show.reading, &doc.show.show_type) {
-            ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
+            if show.screens.is_some() {
+                show.window_before = ctx.input(|i| i.viewport().outer_rect.zip(i.viewport().inner_rect));
+            }
+            ctx.send_viewport_cmd(match show.screens {
+                Some((slides, _)) => egui::ViewportCommand::SetMonitor(slides),
+                None => egui::ViewportCommand::Fullscreen(true),
+            });
         }
         show.after_enter(&doc, now);
     }
@@ -1416,6 +1430,10 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
         app.media.stop_owned(crate::media::Owner::Show);
         if !show.reading {
             ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
+            if let Some((outer, inner)) = show.window_before {
+                ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(outer.min));
+                ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(inner.size()));
+            }
         }
         if show.rehearse {
             if let Some((_, tl)) = &show.timeline {
@@ -2188,140 +2206,140 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
     let button = |ui: &mut Ui, s: &str| {
         ui.add(egui::Button::new(egui::RichText::new(s).size(15.0).color(Color32::WHITE)).fill(Color32::from_gray(60)).min_size(vec2(0.0, 30.0)))
     };
-    ctx.show_viewport_immediate(
-        egui::ViewportId::from_hash_of("deckcraft_presenter"),
-        egui::ViewportBuilder::default().with_title("Presenter View — DeckCraft").with_inner_size([1100.0, 700.0]),
-        |ui, _| {
-            if !keyboard(ui.ctx(), show, doc, now, true) {
-                *keep = false;
-            }
-            ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
-            let elapsed = show.timer.elapsed(now);
-            let paused = show.timer.paused();
-            egui::CentralPanel::default().frame(egui::Frame::NONE.fill(Color32::from_gray(24)).inner_margin(egui::Margin::same(16))).show(ui, |ui| {
-                ui.horizontal(|ui| {
-                    ui.label(egui::RichText::new(format_elapsed(elapsed)).size(22.0).color(if paused {
-                        Color32::from_gray(160)
-                    } else {
-                        Color32::WHITE
-                    }));
-                    ui.add_space(12.0);
-                    ui.label(egui::RichText::new(clock.clone()).size(16.0).color(Color32::from_gray(200)));
-                    ui.add_space(20.0);
-                    ui.label(egui::RichText::new(format!("Slide {} of {}", cur + 1, doc.slides.len())).size(16.0).color(Color32::from_gray(200)));
-                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
-                        if button(ui, "End Show").clicked() {
-                            *keep = false;
-                        }
-                        if button(ui, "Next ▶").clicked() {
-                            actions.push(show.state.next(doc));
-                        }
-                        if button(ui, "◀ Previous").clicked() {
-                            actions.push(show.state.prev(doc));
-                        }
-                    });
-                });
-                ui.horizontal(|ui| {
-                    if button(ui, if paused { "Resume" } else { "Pause" }).clicked() {
-                        show.timer.toggle(now);
-                    }
-                    if button(ui, "Reset").clicked() {
-                        show.timer.reset(now);
-                    }
-                    if button(ui, if show.blank == Some(Color32::BLACK) { "Black on" } else { "Black" }).clicked() {
-                        toggle_blank(show, Color32::BLACK);
-                    }
-                    if button(ui, if show.blank == Some(Color32::WHITE) { "White on" } else { "White" }).clicked() {
-                        toggle_blank(show, Color32::WHITE);
+    let mut window = egui::ViewportBuilder::default().with_title("Presenter View — DeckCraft").with_inner_size([1100.0, 700.0]);
+    if let Some((_, screen)) = show.screens {
+        window = window.with_monitor(screen);
+    }
+    ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("deckcraft_presenter"), window, |ui, _| {
+        if !keyboard(ui.ctx(), show, doc, now, true) {
+            *keep = false;
+        }
+        ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
+        let elapsed = show.timer.elapsed(now);
+        let paused = show.timer.paused();
+        egui::CentralPanel::default().frame(egui::Frame::NONE.fill(Color32::from_gray(24)).inner_margin(egui::Margin::same(16))).show(ui, |ui| {
+            ui.horizontal(|ui| {
+                ui.label(egui::RichText::new(format_elapsed(elapsed)).size(22.0).color(if paused {
+                    Color32::from_gray(160)
+                } else {
+                    Color32::WHITE
+                }));
+                ui.add_space(12.0);
+                ui.label(egui::RichText::new(clock.clone()).size(16.0).color(Color32::from_gray(200)));
+                ui.add_space(20.0);
+                ui.label(egui::RichText::new(format!("Slide {} of {}", cur + 1, doc.slides.len())).size(16.0).color(Color32::from_gray(200)));
+                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                    if button(ui, "End Show").clicked() {
+                        *keep = false;
                     }
-                    if button(ui, if show.presenter_grid { "All slides on" } else { "All slides" }).clicked() {
-                        show.presenter_grid = !show.presenter_grid;
+                    if button(ui, "Next ▶").clicked() {
+                        actions.push(show.state.next(doc));
                     }
-                    if button(ui, show.zoom_label()).clicked() {
-                        show.zoom_key();
+                    if button(ui, "◀ Previous").clicked() {
+                        actions.push(show.state.prev(doc));
                     }
                 });
-                ui.add_space(8.0);
-                let avail = ui.available_rect_before_wrap();
-                if show.presenter_grid {
-                    // The grid fills this window; the audience keeps seeing the current slide.
-                    paint_grid(ui.painter(), ui.ctx(), show, doc, avail);
-                    let hit = ui.interact(avail, ui.scope_id().with("presenter_grid"), Sense::click());
-                    if hit.clicked()
-                        && let Some(p) = hit.interact_pointer_pos()
-                        && let Some(n) = see_all_hit(avail, grid_indices(doc.slides.len()).len(), slide_aspect(doc), p)
-                        && let Some(&i) = grid_indices(doc.slides.len()).get(n)
-                    {
-                        show.presenter_grid = false;
-                        show.blank = None;
-                        show.zoom = None;
-                        show.zoom_arm = false;
-                        actions.push(show.state.goto(doc, i));
-                    }
-                    return;
+            });
+            ui.horizontal(|ui| {
+                if button(ui, if paused { "Resume" } else { "Pause" }).clicked() {
+                    show.timer.toggle(now);
                 }
-                let left = Rect::from_min_max(avail.min, pos2(avail.min.x + avail.width() * 0.62, avail.max.y));
-                let right = Rect::from_min_max(pos2(left.max.x + 16.0, avail.min.y), avail.max);
-                let cr = fit(Rect::from_min_max(left.min, pos2(left.max.x, left.min.y + left.height() * 0.8)), doc);
-                let cr = Rect::from_min_size(left.min, cr.size());
-                if let Some(t) = &cur_tex {
-                    ui.painter().image(t.id(), cr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
+                if button(ui, "Reset").clicked() {
+                    show.timer.reset(now);
                 }
-                if let Some(z) = show.zoom {
-                    let mark = zoom_frame(cr, z.uv());
-                    ui.painter().rect_stroke(mark, CornerRadius::ZERO, Stroke::new(2.0, Color32::WHITE), egui::StrokeKind::Inside);
+                if button(ui, if show.blank == Some(Color32::BLACK) { "Black on" } else { "Black" }).clicked() {
+                    toggle_blank(show, Color32::BLACK);
                 }
-                if show.zoom_arm {
-                    let hit = ui.interact(cr, ui.scope_id().with("presenter_zoom"), Sense::click());
-                    hit.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, "Zoom"));
-                    if hit.clicked()
-                        && let Some(p) = hit.interact_pointer_pos()
-                        && cr.width() > 1.0
-                        && cr.height() > 1.0
-                    {
-                        show.zoom = Some(Zoom::at((p.x - cr.min.x) / cr.width(), (p.y - cr.min.y) / cr.height()));
-                        show.zoom_arm = false;
-                    }
+                if button(ui, if show.blank == Some(Color32::WHITE) { "White on" } else { "White" }).clicked() {
+                    toggle_blank(show, Color32::WHITE);
                 }
-                if let Some(c) = show.blank {
-                    ui.painter().rect_filled(cr, CornerRadius::ZERO, c);
+                if button(ui, if show.presenter_grid { "All slides on" } else { "All slides" }).clicked() {
+                    show.presenter_grid = !show.presenter_grid;
                 }
-                let nr = next_rect(right, doc);
-                ui.painter().text(right.min, Align2::LEFT_TOP, "Next", theme::font(13.0), Color32::from_gray(170));
-                match &next_tex {
-                    Some(t) => {
-                        ui.painter().image(t.id(), nr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
-                    }
-                    None => {
-                        ui.painter().rect_filled(nr, CornerRadius::ZERO, Color32::from_gray(40));
-                        ui.painter().text(nr.center(), Align2::CENTER_CENTER, "End of slide show", theme::font(13.0), Color32::from_gray(170));
-                    }
+                if button(ui, show.zoom_label()).clicked() {
+                    show.zoom_key();
                 }
-                let notes_head = Rect::from_min_size(pos2(right.min.x, nr.max.y + 12.0), vec2(right.width(), 28.0));
-                let mut head = ui.new_child(egui::UiBuilder::new().max_rect(notes_head));
-                head.horizontal(|ui| {
-                    ui.label(egui::RichText::new("Notes").size(13.0).color(Color32::from_gray(170)));
-                    if ui.button("A-").clicked() {
-                        show.nudge_notes(-NOTES_STEP);
-                    }
-                    ui.label(egui::RichText::new(format!("{}", show.notes_px as i32)).size(13.0).color(Color32::from_gray(200)));
-                    if ui.button("A+").clicked() {
-                        show.nudge_notes(NOTES_STEP);
-                    }
-                });
-                let notes_rect = Rect::from_min_max(pos2(right.min.x, notes_head.max.y + 4.0), right.max);
-                let mut child = ui.new_child(egui::UiBuilder::new().max_rect(notes_rect));
-                egui::ScrollArea::vertical().show(&mut child, |ui| {
-                    presenter_notes(ui, notes, show.notes_px);
-                });
             });
-            if ui.input(|i| i.viewport().close_requested()) {
-                show.presenter = false;
-                // A grid left open in the closed window would hold auto-advance for the rest of the show.
-                show.presenter_grid = false;
+            ui.add_space(8.0);
+            let avail = ui.available_rect_before_wrap();
+            if show.presenter_grid {
+                // The grid fills this window; the audience keeps seeing the current slide.
+                paint_grid(ui.painter(), ui.ctx(), show, doc, avail);
+                let hit = ui.interact(avail, ui.scope_id().with("presenter_grid"), Sense::click());
+                if hit.clicked()
+                    && let Some(p) = hit.interact_pointer_pos()
+                    && let Some(n) = see_all_hit(avail, grid_indices(doc.slides.len()).len(), slide_aspect(doc), p)
+                    && let Some(&i) = grid_indices(doc.slides.len()).get(n)
+                {
+                    show.presenter_grid = false;
+                    show.blank = None;
+                    show.zoom = None;
+                    show.zoom_arm = false;
+                    actions.push(show.state.goto(doc, i));
+                }
+                return;
             }
-        },
-    );
+            let left = Rect::from_min_max(avail.min, pos2(avail.min.x + avail.width() * 0.62, avail.max.y));
+            let right = Rect::from_min_max(pos2(left.max.x + 16.0, avail.min.y), avail.max);
+            let cr = fit(Rect::from_min_max(left.min, pos2(left.max.x, left.min.y + left.height() * 0.8)), doc);
+            let cr = Rect::from_min_size(left.min, cr.size());
+            if let Some(t) = &cur_tex {
+                ui.painter().image(t.id(), cr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
+            }
+            if let Some(z) = show.zoom {
+                let mark = zoom_frame(cr, z.uv());
+                ui.painter().rect_stroke(mark, CornerRadius::ZERO, Stroke::new(2.0, Color32::WHITE), egui::StrokeKind::Inside);
+            }
+            if show.zoom_arm {
+                let hit = ui.interact(cr, ui.scope_id().with("presenter_zoom"), Sense::click());
+                hit.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, "Zoom"));
+                if hit.clicked()
+                    && let Some(p) = hit.interact_pointer_pos()
+                    && cr.width() > 1.0
+                    && cr.height() > 1.0
+                {
+                    show.zoom = Some(Zoom::at((p.x - cr.min.x) / cr.width(), (p.y - cr.min.y) / cr.height()));
+                    show.zoom_arm = false;
+                }
+            }
+            if let Some(c) = show.blank {
+                ui.painter().rect_filled(cr, CornerRadius::ZERO, c);
+            }
+            let nr = next_rect(right, doc);
+            ui.painter().text(right.min, Align2::LEFT_TOP, "Next", theme::font(13.0), Color32::from_gray(170));
+            match &next_tex {
+                Some(t) => {
+                    ui.painter().image(t.id(), nr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
+                }
+                None => {
+                    ui.painter().rect_filled(nr, CornerRadius::ZERO, Color32::from_gray(40));
+                    ui.painter().text(nr.center(), Align2::CENTER_CENTER, "End of slide show", theme::font(13.0), Color32::from_gray(170));
+                }
+            }
+            let notes_head = Rect::from_min_size(pos2(right.min.x, nr.max.y + 12.0), vec2(right.width(), 28.0));
+            let mut head = ui.new_child(egui::UiBuilder::new().max_rect(notes_head));
+            head.horizontal(|ui| {
+                ui.label(egui::RichText::new("Notes").size(13.0).color(Color32::from_gray(170)));
+                if ui.button("A-").clicked() {
+                    show.nudge_notes(-NOTES_STEP);
+                }
+                ui.label(egui::RichText::new(format!("{}", show.notes_px as i32)).size(13.0).color(Color32::from_gray(200)));
+                if ui.button("A+").clicked() {
+                    show.nudge_notes(NOTES_STEP);
+                }
+            });
+            let notes_rect = Rect::from_min_max(pos2(right.min.x, notes_head.max.y + 4.0), right.max);
+            let mut child = ui.new_child(egui::UiBuilder::new().max_rect(notes_rect));
+            egui::ScrollArea::vertical().show(&mut child, |ui| {
+                presenter_notes(ui, notes, show.notes_px);
+            });
+        });
+        if ui.input(|i| i.viewport().close_requested()) {
+            show.presenter = false;
+            // A grid left open in the closed window would hold auto-advance for the rest of the show.
+            show.presenter_grid = false;
+        }
+    });
     for a in actions {
         if !show.apply(a, doc, now) {
             *keep = false;
@@ -2728,6 +2746,28 @@ mod tests {
         assert_eq!(got.pixels, want.pixels);
     }
 
+    /// As in PowerPoint: with two screens the slides go to the other screen and Presenter View
+    /// stays with the presenter; with one screen there is no Presenter View.
+    #[test]
+    fn presenter_view_gets_a_screen_of_its_own() {
+        use crate::Screens;
+        let one = Screens { list: vec![0], current: Some(0), primary: Some(0) };
+        assert_eq!(one.for_presenter_view(), None);
+        // A screen Wayland lists twice reaches here once (the host merges them): still one.
+        let listed_twice = Screens { list: vec![0], current: Some(0), primary: None };
+        assert_eq!(listed_twice.for_presenter_view(), None);
+        let two = |current, primary| Screens { list: vec![0, 1], current, primary };
+        assert_eq!(two(Some(1), None).for_presenter_view(), Some((0, 1)), "Presenter View where DeckCraft is");
+        assert_eq!(two(Some(0), Some(1)).for_presenter_view(), Some((1, 0)));
+        assert_eq!(two(None, Some(1)).for_presenter_view(), Some((0, 1)), "else the main screen");
+        assert_eq!(two(None, None).for_presenter_view(), Some((1, 0)));
+        // A screen number that isn't listed is ignored.
+        assert_eq!(two(Some(7), None).for_presenter_view(), Some((1, 0)));
+        assert_eq!(two(Some(7), Some(1)).for_presenter_view(), Some((0, 1)), "an unlisted current falls back to the main screen");
+        let three = Screens { list: vec![0, 2, 3], current: Some(2), primary: None };
+        assert_eq!(three.for_presenter_view(), Some((0, 2)));
+    }
+
     /// A glowing full-slide picture in a layout made every animation frame re-render (and re-blur)
     /// the backdrop: 2–3 frames a second. Frames now draw the slide's shapes over the kept
     /// backdrop, and must look the same as a whole-slide render.
