diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -898,6 +898,9 @@ dependencies = [
  "fmv-macos-events",
  "image",
  "log",
+ "muda",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
  "rfd",
  "serde_json",
  "winit",
@@ -2399,6 +2402,16 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "keyboard-types"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
+dependencies = [
+ "bitflags 2.13.2",
+ "serde",
+]
+
 [[package]]
 name = "khronos-egl"
 version = "6.0.0"
@@ -2625,6 +2638,24 @@ dependencies = [
  "pxfm",
 ]
 
+[[package]]
+name = "muda"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "keyboard-types",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "png",
+ "thiserror 2.0.21",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "multiversion_no_op"
 version = "1.0.0"
diff --git a/apps/deckcraft/Cargo.toml b/apps/deckcraft/Cargo.toml
--- a/apps/deckcraft/Cargo.toml
+++ b/apps/deckcraft/Cargo.toml
@@ -36,6 +36,15 @@ cpal = "0.16"
 # GridCraft, PdfCraft and PhotoCraft) registers the handler (Objective-C interop needs unsafe, which
 # our workspace forbids) behind a safe API. Pinned exactly: review the diff before bumping.
 fmv-macos-events = "=0.1.0"
+muda = { version = "0.21", default-features = false }
+
+[target.'cfg(target_os = "macos")'.dev-dependencies]
+objc2 = "0.6"
+objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSMenu", "NSMenuItem", "NSResponder"] }
+
+[[test]]
+name = "mac_menu_appkit"
+harness = false
 
 [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
 winit = { version = "0.30", default-features = false, features = ["x11"] }
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
@@ -23,6 +23,8 @@ mod display;
 #[cfg(any(target_os = "windows", test))]
 mod graphics;
 mod logging;
+#[cfg(target_os = "macos")]
+mod native_menu;
 #[cfg(not(target_arch = "wasm32"))]
 mod pasteboard;
 
@@ -31,6 +33,7 @@ use deckcraft_ui_egui::{Services, SlideApp};
 
 struct App(
     SlideApp,
+    #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>,
     /// Files opened from Finder and quit requests (see `apple_events`).
     #[cfg(target_os = "macos")]
     fmv_macos_events::Inbox,
@@ -39,7 +42,15 @@ struct App(
 impl eframe::App for App {
     fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
         #[cfg(target_os = "macos")]
-        apple_events::poll(&self.1, &mut self.0, ctx);
+        {
+            if self.1.is_none() && std::env::var_os("DECKCRAFT_NO_NATIVE_MENU").is_none() {
+                self.1 = Some(native_menu::NativeMenu::install(ctx));
+            }
+            if let Some(m) = &self.1 {
+                m.poll(&mut self.0, ctx);
+            }
+            apple_events::poll(&self.2, &mut self.0, ctx);
+        }
         self.0.logic(ctx);
     }
     fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
@@ -284,6 +295,8 @@ fn main() -> eframe::Result {
             Ok(Box::new(App(
                 app,
                 #[cfg(target_os = "macos")]
+                None,
+                #[cfg(target_os = "macos")]
                 apple_events.connect(&cc.egui_ctx),
             )))
         }),
@@ -302,6 +315,8 @@ mod tests {
             let mut app = App(
                 SlideApp::new(Session::new(), Services::default()),
                 #[cfg(target_os = "macos")]
+                None,
+                #[cfg(target_os = "macos")]
                 fmv_macos_events::Inbox::default(),
             );
             println!("calling on_exit");
diff --git a/apps/deckcraft/src/native_menu.rs b/apps/deckcraft/src/native_menu.rs
new file mode 100644
--- /dev/null
+++ b/apps/deckcraft/src/native_menu.rs
@@ -0,0 +1,793 @@
+//! macOS: the native menu bar (DeckCraft, File, Edit, View, Insert, Format, Arrange, Tools,
+//! Slide Show, Window, Help) matching Microsoft PowerPoint's top-level menu hierarchy on the Mac.
+//! Items dispatch to SlideApp commands, dialogs, or system actions.
+
+use std::str::FromStr;
+use std::sync::mpsc::{Receiver, channel};
+
+use deckcraft_ui_egui::{SlideApp, ViewMode, dialogs::Dialog};
+use muda::accelerator::Accelerator;
+use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
+use serde_json::json;
+
+pub struct NativeMenu {
+    _menu: Menu,
+    events: Receiver<MenuEvent>,
+    view_items: Vec<(&'static str, CheckMenuItem)>,
+}
+
+/// Creates a standard menu item with an identifier, label, and optional accelerator string.
+fn item(id: &'static str, label: &'static str, acc: Option<&'static str>) -> MenuItem {
+    let accel = acc.and_then(|a| Accelerator::from_str(a).ok());
+    MenuItem::with_id(id, label, true, accel)
+}
+
+/// Builds the DeckCraft application menu (About, Preferences, Services, Hide, Quit).
+fn build_app_menu() -> Submenu {
+    let app_menu = Submenu::new("DeckCraft", true);
+    let _ = app_menu.append_items(&[
+        &item("app.about", "About DeckCraft", None),
+        &PredefinedMenuItem::separator(),
+        &item("app.preferences", "Preferences…", Some("CMD+,")),
+        &PredefinedMenuItem::separator(),
+        &PredefinedMenuItem::services(None),
+        &PredefinedMenuItem::separator(),
+        &PredefinedMenuItem::hide(Some("Hide DeckCraft")),
+        &PredefinedMenuItem::hide_others(Some("Hide Others")),
+        &PredefinedMenuItem::show_all(Some("Show All")),
+        &PredefinedMenuItem::separator(),
+        &item("app.quit", "Quit DeckCraft", Some("CMD+Q")),
+    ]);
+    app_menu
+}
+
+/// Builds the File menu containing presentation lifecycle and document export actions.
+fn build_file_menu() -> Submenu {
+    let file = Submenu::new("File", true);
+    let _ = file.append_items(&[
+        &item("file.new", "New Presentation", Some("CMD+N")),
+        &item("file.newTemplate", "New from Template…", None),
+        &item("app.openDialog", "Open…", Some("CMD+O")),
+    ]);
+
+    let recent = Submenu::new("Open Recent", true);
+    let _ = recent.append(&item("file.clearRecent", "Clear Recent Files", None));
+    let _ = file.append(&recent);
+
+    let _ = file.append(&PredefinedMenuItem::separator());
+    let _ = file.append_items(&[
+        &item("app.closePresentation", "Close", Some("CMD+W")),
+        &item("file.save", "Save", Some("CMD+S")),
+        &item("app.saveAsDialog", "Save As…", Some("CMD+SHIFT+S")),
+        &item("file.saveTemplate", "Save as Template…", None),
+        &item("app.exportDialog", "Export…", None),
+    ]);
+
+    let share = Submenu::new("Share", true);
+    let _ = share.append_items(&[
+        &item("share.invite", "Invite People…", None),
+        &item("share.copyLink", "Copy Link", None),
+        &item("share.sendCopy", "Send a Copy", None),
+    ]);
+    let _ = file.append(&share);
+
+    let _ = file.append(&PredefinedMenuItem::separator());
+    let _ = file.append(&item("file.readOnly", "Always Open Read-Only", None));
+
+    let perms = Submenu::new("Restrict Permissions", true);
+    let _ = perms.append_items(&[&item("perm.unrestricted", "Unrestricted Access", None), &item("perm.restricted", "Restricted Access…", None)]);
+    let _ = file.append(&perms);
+    let _ = file.append(&item("file.passwords", "Passwords…", None));
+
+    let _ = file.append(&PredefinedMenuItem::separator());
+    let _ = file.append_items(&[
+        &item("dialog:slideSize", "Page Setup…", None),
+        &item("app.print", "Print…", Some("CMD+P")),
+        &PredefinedMenuItem::separator(),
+        &item("file.properties", "Properties", None),
+    ]);
+
+    file
+}
+
+/// Builds the Edit menu containing undo/redo, clipboard, find/replace, and selection commands.
+fn build_edit_menu() -> Submenu {
+    let edit = Submenu::new("Edit", true);
+    let _ = edit.append_items(&[
+        &item("edit.undo", "Undo", None),
+        &item("edit.redo", "Repeat", None),
+        &PredefinedMenuItem::separator(),
+        &item("edit.cut", "Cut", None),
+        &item("edit.copy", "Copy", None),
+        &item("edit.paste", "Paste", None),
+        &item("edit.pasteSpecial", "Paste Special…", Some("CTRL+CMD+V")),
+        &item("edit.pasteText", "Paste and Match Formatting", Some("ALT+SHIFT+CMD+V")),
+        &PredefinedMenuItem::separator(),
+        &item("edit.clear", "Clear", None),
+        &item("edit.selectAll", "Select All", None),
+        &item("edit.duplicate", "Duplicate", Some("CMD+D")),
+        &item("slide.delete", "Delete Slide", None),
+        &PredefinedMenuItem::separator(),
+        &item("section.delete", "Remove Section", None),
+        &item("dialog:renameSection", "Rename Section", None),
+        &PredefinedMenuItem::separator(),
+    ]);
+
+    let find = Submenu::new("Find", true);
+    let _ = find.append_items(&[&item("app.palette", "Find…", Some("CMD+F")), &item("edit.replace", "Replace…", Some("ALT+CMD+F"))]);
+    let _ = edit.append(&find);
+
+    let _ = edit.append_items(&[
+        &item("dialog:chartData", "Select Data…", None),
+        &PredefinedMenuItem::separator(),
+        &item("draw.toggle", "Toggle Drawing", Some("CTRL+CMD+Z")),
+        &PredefinedMenuItem::separator(),
+    ]);
+
+    let autofill = Submenu::new("AutoFill", true);
+    let _ = autofill.append(&item("edit.autofill", "AutoFill Options…", None));
+    let _ = edit.append(&autofill);
+
+    let _ = edit.append_items(&[&item("edit.dictation", "Start Dictation", None), &item("edit.emoji", "Emoji & Symbols", None)]);
+
+    edit
+}
+
+/// Creates a checkable menu item for toggling view modes and options.
+fn check_item(id: &'static str, label: &'static str, checked: bool, acc: Option<&'static str>) -> CheckMenuItem {
+    let accel = acc.and_then(|a| Accelerator::from_str(a).ok());
+    CheckMenuItem::with_id(id, label, true, checked, accel)
+}
+
+/// Builds the View menu containing presentation view modes, zoom controls, and UI toggles.
+fn build_view_menu(view_items: &mut Vec<(&'static str, CheckMenuItem)>) -> Submenu {
+    let view = Submenu::new("View", true);
+
+    let normal = check_item("view.normal", "Normal", true, Some("CMD+1"));
+    let sorter = check_item("view.sorter", "Slide Sorter", false, Some("CMD+2"));
+    let notes = check_item("view.notesPage", "Notes Page", false, Some("CMD+3"));
+    let outline = check_item("view.outline", "Outline View", false, Some("CMD+4"));
+    let reading = item("view.reading", "Reading View", Some("CMD+5"));
+
+    view_items.push(("normal", normal.clone()));
+    view_items.push(("sorter", sorter.clone()));
+    view_items.push(("notesPage", notes.clone()));
+    view_items.push(("outline", outline.clone()));
+
+    let _ = view.append_items(&[
+        &normal,
+        &sorter,
+        &notes,
+        &outline,
+        &reading,
+        &item("show.presenter", "Presenter View", Some("ALT+ENTER")),
+        &item("show.start", "Slide Show", Some("CMD+SHIFT+ENTER")),
+        &PredefinedMenuItem::separator(),
+        &item("view.showOutline", "Show Outline", Some("CTRL+SHIFT+TAB")),
+    ]);
+
+    let master = Submenu::new("Master", true);
+    let _ = master.append_items(&[
+        &item("view.slideMaster", "Slide Master", None),
+        &item("view.handoutMaster", "Handout Master", None),
+        &item("view.notesMaster", "Notes Master", None),
+    ]);
+    let _ = view.append(&master);
+
+    let _ = view.append(&PredefinedMenuItem::separator());
+    let ribbon = check_item("view.collapseRibbon", "Ribbon", true, Some("ALT+CMD+R"));
+    view_items.push(("ribbon", ribbon.clone()));
+    let _ = view.append(&ribbon);
+    let _ = view.append(&item("view.messageBar", "Message Bar", None));
+
+    let _ = view.append(&PredefinedMenuItem::separator());
+    let _ = view.append_items(&[
+        &item("dialog:headerFooter", "Header and Footer…", None),
+        &item("view.markup", "Markup", None),
+        &item("view.advancedMarkup", "Advanced Markup", None),
+        &PredefinedMenuItem::separator(),
+        &item("view.ruler", "Ruler", None),
+    ]);
+
+    let grid_guides = Submenu::new("Grid and Guides", true);
+    let _ = grid_guides.append_items(&[&item("view.gridlines", "Gridlines", Some("SHIFT+F9")), &item("view.guides", "Guides", Some("ALT+F9"))]);
+    let _ = view.append(&grid_guides);
+
+    let zoom = Submenu::new("Zoom", true);
+    let _ = zoom.append_items(&[
+        &item("dialog:zoom", "Zoom…", None),
+        &item("view.zoomIn", "Zoom In", Some("CMD+=")),
+        &item("view.zoomOut", "Zoom Out", Some("CMD+-")),
+        &item("view.fit", "Fit to Window", Some("CMD+0")),
+    ]);
+    let _ = view.append(&zoom);
+
+    let _ = view.append(&item("view.fullScreen", "Enter Full Screen", Some("CTRL+CMD+F")));
+
+    view
+}
+
+/// Builds the Insert menu containing slide, media, shape, and text box insertion commands.
+fn build_insert_menu() -> Submenu {
+    let insert = Submenu::new("Insert", true);
+    let _ = insert
+        .append_items(&[&item("slide.new", "New Slide", Some("CMD+SHIFT+N")), &item("slide.duplicate", "Duplicate Slide", Some("CMD+SHIFT+D"))]);
+
+    let slides_from = Submenu::new("Slides From", true);
+    let _ = slides_from.append_items(&[&item("dialog:outline", "Outline…", None), &item("slide.reuse", "Reuse Slides…", None)]);
+    let _ = insert.append(&slides_from);
+
+    let _ = insert.append_items(&[
+        &PredefinedMenuItem::separator(),
+        &item("section.add", "Section", None),
+        &item("dialog:comment", "Comment", Some("CMD+SHIFT+M")),
+        &PredefinedMenuItem::separator(),
+        &item("insert.textBox", "Text Box", None),
+        &item("insert.wordArt", "WordArt", None),
+        &item("dialog:headerFooter", "Header and Footer…", None),
+        &item("insert.dateTime", "Date and Time…", None),
+        &item("insert.slideNumber", "Slide Number", None),
+        &PredefinedMenuItem::separator(),
+        &item("dialog:table", "Table…", None),
+    ]);
+
+    let chart = Submenu::new("Chart", true);
+    let _ = chart.append(&item("insert.chart", "Insert Chart…", None));
+    let _ = insert.append(&chart);
+
+    let smartart = Submenu::new("SmartArt", true);
+    let _ = smartart.append(&item("dialog:smartart", "SmartArt…", None));
+    let _ = insert.append(&smartart);
+
+    let _ = insert.append(&PredefinedMenuItem::separator());
+
+    let pic = Submenu::new("Picture", true);
+    let _ = pic.append(&item("app.insertPictureDialog", "Picture from File…", None));
+    let _ = insert.append(&pic);
+
+    let audio = Submenu::new("Audio", true);
+    let _ = audio.append(&item("app.insertAudioDialog", "Audio from File…", None));
+    let _ = insert.append(&audio);
+
+    let video = Submenu::new("Video", true);
+    let _ = video.append(&item("app.insertVideoDialog", "Video from File…", None));
+    let _ = insert.append(&video);
+
+    let _ = insert.append_items(&[
+        &item("insert.cameo", "Cameo", None),
+        &item("dialog:equation", "Equation", None),
+        &item("dialog:symbol", "Symbol…", None),
+    ]);
+
+    let shape = Submenu::new("Shape", true);
+    let _ = shape.append_items(&[
+        &item("shape.rect", "Rectangle", None),
+        &item("shape.roundRect", "Rounded Rectangle", None),
+        &item("shape.ellipse", "Oval", None),
+        &item("shape.line", "Line", None),
+        &item("shape.arrow", "Arrow", None),
+    ]);
+    let _ = insert.append(&shape);
+
+    let _ = insert.append(&PredefinedMenuItem::separator());
+    let _ = insert.append(&item("insert.icons", "Icons…", None));
+    let _ = insert.append(&PredefinedMenuItem::separator());
+
+    let models = Submenu::new("3D Models", true);
+    let _ = models.append(&item("insert.model3d", "From a File…", None));
+    let _ = insert.append(&models);
+
+    let _ = insert.append(&PredefinedMenuItem::separator());
+
+    let zoom = Submenu::new("Zoom", true);
+    let _ = zoom.append_items(&[
+        &item("insert.summaryZoom", "Summary Zoom", None),
+        &item("insert.sectionZoom", "Section Zoom", None),
+        &item("insert.slideZoom", "Slide Zoom", None),
+    ]);
+    let _ = insert.append(&zoom);
+
+    let _ = insert.append(&PredefinedMenuItem::separator());
+
+    let actions = Submenu::new("Action Buttons", true);
+    let _ = actions.append_items(&[
+        &item("action.back", "Back or Previous", None),
+        &item("action.forward", "Forward or Next", None),
+        &item("action.home", "Home", None),
+        &item("action.info", "Information", None),
+    ]);
+    let _ = insert.append(&actions);
+    let _ = insert.append(&item("action.settings", "Action Settings…", None));
+
+    let _ = insert.append(&PredefinedMenuItem::separator());
+    let _ = insert.append_items(&[&item("insert.object", "Object…", None), &item("dialog:hyperlink", "Hyperlink…", Some("CMD+K"))]);
+
+    insert
+}
+
+/// Builds the Format menu containing font formatting, text alignment, and object style options.
+fn build_format_menu() -> Submenu {
+    let format = Submenu::new("Format", true);
+    let _ = format.append_items(&[
+        &item("format.font", "Font…", Some("CMD+T")),
+        &item("dialog:paragraph", "Paragraph…", Some("ALT+CMD+M")),
+        &PredefinedMenuItem::separator(),
+        &item("format.bullets", "Bullets and Numbering…", None),
+        &item("format.columns", "Columns…", None),
+    ]);
+
+    let align = Submenu::new("Alignment", true);
+    let _ = align.append_items(&[
+        &item("format.alignLeft", "Align Left", Some("CMD+L")),
+        &item("format.alignCenter", "Center", Some("CMD+E")),
+        &item("format.alignRight", "Align Right", Some("CMD+R")),
+        &item("format.justify", "Justify", Some("CMD+J")),
+    ]);
+    let _ = format.append(&align);
+    let _ = format.append(&item("format.moreOptions", "More Options…", None));
+
+    let _ = format.append(&PredefinedMenuItem::separator());
+    let _ = format.append_items(&[
+        &item("format.pickUpStyle", "Pick Up Object Style", Some("CMD+SHIFT+C")),
+        &item("format.applyDefaults", "Apply To Defaults", Some("CMD+SHIFT+V")),
+        &item("format.animPainter", "Animation Painter", Some("ALT+CMD+SHIFT+C")),
+        &PredefinedMenuItem::separator(),
+        &item("format.replaceFonts", "Replace Fonts…", None),
+        &item("format.themeColors", "Theme Colors…", None),
+        &item("format.slideBackground", "Slide Background…", Some("CMD+SHIFT+2")),
+        &PredefinedMenuItem::separator(),
+        &item("format.crop", "Crop", None),
+        &item("format.object", "Format Object…", Some("CMD+SHIFT+1")),
+    ]);
+    format
+}
+
+/// Builds the Arrange menu containing z-order, grouping, alignment, and distribution commands.
+fn build_arrange_menu() -> Submenu {
+    let arrange = Submenu::new("Arrange", true);
+    let _ = arrange.append_items(&[
+        &item("arrange.reorder", "Reorder Objects", None),
+        &item("arrange.reorderOverlapping", "Reorder Overlapping Objects", None),
+        &PredefinedMenuItem::separator(),
+        &item("arrange.bringToFront", "Bring to Front", Some("CMD+SHIFT+F")),
+        &item("arrange.sendToBack", "Send to Back", Some("CMD+SHIFT+B")),
+        &item("arrange.bringForward", "Bring Forward", Some("ALT+CMD+SHIFT+F")),
+        &item("arrange.sendBackward", "Send Backward", Some("ALT+CMD+SHIFT+B")),
+        &PredefinedMenuItem::separator(),
+        &item("arrange.group", "Group", Some("ALT+CMD+G")),
+        &item("arrange.ungroup", "Ungroup", Some("ALT+CMD+SHIFT+G")),
+        &item("arrange.regroup", "Regroup", Some("ALT+CMD+J")),
+        &PredefinedMenuItem::separator(),
+    ]);
+
+    let rotate = Submenu::new("Rotate or Flip", true);
+    let _ = rotate.append_items(&[
+        &item("arrange.rotateRight", "Rotate Right 90°", None),
+        &item("arrange.rotateLeft", "Rotate Left 90°", None),
+        &item("arrange.flipHorizontal", "Flip Horizontal", None),
+        &item("arrange.flipVertical", "Flip Vertical", None),
+    ]);
+    let _ = arrange.append(&rotate);
+
+    let align = Submenu::new("Align or Distribute", true);
+    let _ = align.append_items(&[
+        &item("align.left", "Align Left", None),
+        &item("align.center", "Align Center", None),
+        &item("align.right", "Align Right", None),
+        &item("align.top", "Align Top", None),
+        &item("align.middle", "Align Middle", None),
+        &item("align.bottom", "Align Bottom", None),
+        &PredefinedMenuItem::separator(),
+        &item("distribute.horizontal", "Distribute Horizontally", None),
+        &item("distribute.vertical", "Distribute Vertically", None),
+    ]);
+    let _ = arrange.append(&align);
+
+    let _ = arrange.append(&PredefinedMenuItem::separator());
+    let _ = arrange.append(&item("arrange.selpane", "Selection Pane…", Some("ALT+CMD+U")));
+    arrange
+}
+
+/// Builds the Tools menu containing spelling, thesaurus, translation, and accessibility options.
+fn build_tools_menu() -> Submenu {
+    let tools = Submenu::new("Tools", true);
+    let _ = tools.append_items(&[
+        &item("dialog:spelling", "Spelling…", None),
+        &item("review.thesaurusPane", "Thesaurus…", Some("CTRL+ALT+CMD+R")),
+        &item("tools.translate", "Translate…", None),
+        &item("dialog:language", "Set Proofing Language…", None),
+        &item("dialog:preferences", "AutoCorrect Options…", None),
+        &PredefinedMenuItem::separator(),
+        &item("dialog:accessibility", "Check Accessibility", None),
+        &PredefinedMenuItem::separator(),
+    ]);
+
+    let macro_menu = Submenu::new("Macro", true);
+    let _ = macro_menu.append(&item("tools.macros", "Macros…", Some("ALT+F8")));
+    let _ = tools.append(&macro_menu);
+
+    let _ = tools.append(&item("tools.addins", "PowerPoint Add-ins…", None));
+    tools
+}
+
+/// Builds the Slide Show menu for controlling playback and presentation setup.
+fn build_slide_show_menu() -> Submenu {
+    let show = Submenu::new("Slide Show", true);
+    let _ = show.append_items(&[
+        &item("show.start", "Play from Start", Some("CMD+SHIFT+ENTER")),
+        &item("show.fromCurrent", "Play from Current Slide", Some("CMD+ENTER")),
+        &PredefinedMenuItem::separator(),
+    ]);
+
+    let custom = Submenu::new("Custom Slide Show", true);
+    let _ = custom.append(&item("dialog:customShow", "Custom Shows…", None));
+    let _ = show.append(&custom);
+
+    let _ = show.append_items(&[
+        &PredefinedMenuItem::separator(),
+        &item("show.coach", "Rehearse with Coach", None),
+        &item("show.presenter", "Presenter View", Some("ALT+ENTER")),
+        &item("show.timings", "Rehearse Timings", None),
+        &item("show.record", "Record Slide Show", None),
+        &PredefinedMenuItem::separator(),
+        &item("slide.hide", "Hide Slide", None),
+        &item("dialog:setupShow", "Set Up Show…", None),
+        &PredefinedMenuItem::separator(),
+        &item("show.subtitles", "Always Use Subtitles", None),
+    ]);
+
+    let sub_settings = Submenu::new("Subtitle Settings", true);
+    let _ = sub_settings.append_items(&[
+        &item("subtitles.top", "Top (Overlaid)", None),
+        &item("subtitles.bottom", "Bottom (Overlaid)", None),
+        &item("subtitles.below", "Below Slide", None),
+        &item("subtitles.above", "Above Slide", None),
+    ]);
+    let _ = show.append(&sub_settings);
+
+    show
+}
+
+/// Builds the Window menu integrated with macOS system window management.
+fn build_window_menu() -> Submenu {
+    let window = Submenu::new("Window", true);
+    let _ = window.append_items(&[
+        &item("window.minimize", "Minimize", Some("CMD+M")),
+        &item("window.zoom", "Zoom", None),
+        &PredefinedMenuItem::separator(),
+        &item("window.new", "New Window", None),
+        &item("window.arrangeAll", "Arrange All", None),
+        &item("window.nextPane", "Next Pane", None),
+        &PredefinedMenuItem::separator(),
+        &PredefinedMenuItem::bring_all_to_front(None),
+    ]);
+    window
+}
+
+/// Builds the Help menu offering command palette search and community support links.
+fn build_help_menu() -> Submenu {
+    let help = Submenu::new("Help", true);
+    let _ = help.append_items(&[
+        &item("app.palette", "DeckCraft Help / Search", Some("CMD+SHIFT+P")),
+        &item("url:https://discord.gg/artcraft", "Join Community Discord…", None),
+        &PredefinedMenuItem::separator(),
+        &item("app.about", "About DeckCraft", None),
+    ]);
+    help
+}
+
+impl NativeMenu {
+    /// Installs the native macOS menu bar onto NSApp and registers the event handler.
+    pub fn install(ctx: &egui::Context) -> NativeMenu {
+        let menu = Menu::new();
+        let mut view_items = Vec::new();
+
+        let _ = menu.append(&build_app_menu());
+        let _ = menu.append(&build_file_menu());
+        let _ = menu.append(&build_edit_menu());
+        let _ = menu.append(&build_view_menu(&mut view_items));
+        let _ = menu.append(&build_insert_menu());
+        let _ = menu.append(&build_format_menu());
+        let _ = menu.append(&build_arrange_menu());
+        let _ = menu.append(&build_tools_menu());
+        let _ = menu.append(&build_slide_show_menu());
+
+        let window = build_window_menu();
+        let _ = menu.append(&window);
+        let _ = menu.append(&build_help_menu());
+
+        menu.init_for_nsapp();
+        window.set_as_windows_menu_for_nsapp();
+
+        let (tx, rx) = channel();
+        let ctx_clone = ctx.clone();
+        MenuEvent::set_event_handler(Some(move |e: MenuEvent| {
+            let _ = tx.send(e);
+            ctx_clone.request_repaint();
+        }));
+
+        NativeMenu { _menu: menu, events: rx, view_items }
+    }
+
+    /// Polls pending native menu events, dispatches commands to SlideApp, and syncs view states.
+    pub fn poll(&self, app: &mut SlideApp, ctx: &egui::Context) {
+        while let Ok(event) = self.events.try_recv() {
+            let id = event.id().as_ref();
+            let text_edit_focused = app.dialog.is_some() || ctx.text_edit_focused();
+            if text_edit_focused {
+                let clipboard = match id {
+                    "edit.copy" => Some(egui::ViewportCommand::RequestCopy),
+                    "edit.cut" => Some(egui::ViewportCommand::RequestCut),
+                    "edit.paste" => Some(egui::ViewportCommand::RequestPaste),
+                    _ => None,
+                };
+                if let Some(cmd) = clipboard {
+                    ctx.send_viewport_cmd(cmd);
+                    continue;
+                }
+            }
+            let modal_safe = matches!(id, "app.about" | "app.quit");
+            if app.dialog.is_some() && !modal_safe {
+                continue;
+            }
+            if let Some(url) = id.strip_prefix("url:") {
+                app.open_url(url);
+            } else if let Some(d) = id.strip_prefix("dialog:") {
+                app.dialog = Some(Dialog::new(d));
+            } else {
+                match id {
+                    "app.quit" => {
+                        ctx.send_viewport_cmd(egui::ViewportCommand::Close);
+                    }
+                    "file.newTemplate" => app.open_backstage(deckcraft_ui_egui::backstage::Page::New),
+                    "file.save" => app.save(),
+                    "file.saveTemplate" => app.save_as_dialog("deckcraft"),
+                    "file.passwords" => app.set_status("Password protection is not supported in this version"),
+                    "file.properties" => app.open_backstage(deckcraft_ui_egui::backstage::Page::Info),
+                    "file.clearRecent" => {
+                        app.ui.recent.clear();
+                        app.set_status("Recent presentations cleared");
+                    }
+                    "file.readOnly" => {
+                        app.set_status("Read-only mode is not available yet");
+                    }
+                    "edit.pasteSpecial" => {
+                        let _ = app.run("edit.paste", json!({}));
+                    }
+                    "edit.replace" => {
+                        let _ = app.run("app.replace", json!({}));
+                    }
+                    "slide.reuse" => {
+                        let _ = app.run("app.reuseSlidesDialog", json!({}));
+                    }
+                    "app.closePresentation" => {
+                        if app.session.active().is_none() {
+                            ctx.send_viewport_cmd(egui::ViewportCommand::Close);
+                        } else {
+                            app.close_presentation();
+                        }
+                    }
+                    "view.normal" => app.ui.view = ViewMode::Normal,
+                    "view.sorter" => app.ui.view = ViewMode::Sorter,
+                    "view.notesPage" => app.ui.view = ViewMode::NotesPage,
+                    "view.outline" => app.ui.view = ViewMode::Outline,
+                    "view.reading" => {
+                        let i = app.session.active().map(|d| d.selection.slide).unwrap_or(0);
+                        app.start_show(i, true);
+                    }
+                    "view.collapseRibbon" => app.ui.ribbon_collapsed = !app.ui.ribbon_collapsed,
+                    "view.fullScreen" => {
+                        let is_fs = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
+                        ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(!is_fs));
+                    }
+                    "window.minimize" => {
+                        ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true));
+                    }
+                    "window.zoom" => {
+                        let is_max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
+                        ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(!is_max));
+                    }
+                    "window.new" => {
+                        let _ = app.run("view.newWindow", json!({}));
+                    }
+                    "window.arrangeAll" => {
+                        app.set_status("Window arrangement is not available yet");
+                    }
+                    "window.nextPane" => {
+                        app.set_status("Pane switching is not available yet");
+                    }
+                    "show.fromCurrent" => {
+                        let i = app.session.active().map(|d| d.selection.slide).unwrap_or(0);
+                        app.start_show(i, false);
+                    }
+                    "show.coach" => {
+                        app.set_status("Rehearsal coach is not available yet");
+                    }
+                    "show.timings" => {
+                        app.set_status("Slide timings recording is not available yet");
+                    }
+                    "show.record" => {
+                        app.set_status("Slide show recording is not available yet");
+                    }
+                    "format.font" => {
+                        app.palette = Some((String::new(), 0));
+                    }
+                    "format.alignLeft" => {
+                        let _ = app.run("format.align", json!({"align": "left"}));
+                    }
+                    "format.alignCenter" => {
+                        let _ = app.run("format.align", json!({"align": "center"}));
+                    }
+                    "format.alignRight" => {
+                        let _ = app.run("format.align", json!({"align": "right"}));
+                    }
+                    "format.justify" => {
+                        let _ = app.run("format.align", json!({"align": "justify"}));
+                    }
+                    "format.slideBackground" => {
+                        app.ui.pane = Some("background".into());
+                    }
+                    "format.object" => {
+                        app.ui.pane = Some("format".into());
+                    }
+                    "format.animPainter" => {
+                        app.ui.pane = Some("animation".into());
+                    }
+                    "format.crop" => {
+                        app.set_status("Crop tool is not available yet");
+                    }
+                    "format.pickUpStyle" => {
+                        app.set_status("Style copying is not available yet");
+                    }
+                    "format.applyDefaults" => {
+                        app.set_status("Default formatting is not available yet");
+                    }
+                    "arrange.selpane" => {
+                        app.ui.pane = Some("selection".into());
+                    }
+                    "align.left" => {
+                        let _ = app.run("arrange.align", json!({"edge": "left"}));
+                    }
+                    "align.center" => {
+                        let _ = app.run("arrange.align", json!({"edge": "center"}));
+                    }
+                    "align.right" => {
+                        let _ = app.run("arrange.align", json!({"edge": "right"}));
+                    }
+                    "align.top" => {
+                        let _ = app.run("arrange.align", json!({"edge": "top"}));
+                    }
+                    "align.middle" => {
+                        let _ = app.run("arrange.align", json!({"edge": "middle"}));
+                    }
+                    "align.bottom" => {
+                        let _ = app.run("arrange.align", json!({"edge": "bottom"}));
+                    }
+                    "distribute.horizontal" => {
+                        let _ = app.run("arrange.distribute", json!({"dir": "horizontal"}));
+                    }
+                    "distribute.vertical" => {
+                        let _ = app.run("arrange.distribute", json!({"dir": "vertical"}));
+                    }
+                    "shape.rect" => {
+                        let _ = app.run("shape.insert", json!({"preset": "rect"}));
+                    }
+                    "shape.roundRect" => {
+                        let _ = app.run("shape.insert", json!({"preset": "roundRect"}));
+                    }
+                    "shape.ellipse" => {
+                        let _ = app.run("shape.insert", json!({"preset": "ellipse"}));
+                    }
+                    "shape.line" => {
+                        let _ = app.run("shape.insert", json!({"preset": "line"}));
+                    }
+                    "shape.arrow" => {
+                        let _ = app.run("shape.insert", json!({"preset": "rightArrow"}));
+                    }
+                    cmd => {
+                        if let Err(e) = app.run(cmd, json!({})) {
+                            log::debug!("native menu command '{cmd}' failed: {e}");
+                        }
+                    }
+                }
+            }
+        }
+
+        // Sync view checkboxes
+        for (mode, item) in &self.view_items {
+            let checked = match *mode {
+                "normal" => app.ui.view == ViewMode::Normal,
+                "sorter" => app.ui.view == ViewMode::Sorter,
+                "notesPage" => app.ui.view == ViewMode::NotesPage,
+                "outline" => app.ui.view == ViewMode::Outline,
+                "ribbon" => !app.ui.ribbon_collapsed,
+                _ => false,
+            };
+            if item.is_checked() != checked {
+                item.set_checked(checked);
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_menu_accelerators_parse() {
+        for acc in [
+            "CMD+,",
+            "CMD+N",
+            "CMD+SHIFT+P",
+            "CMD+O",
+            "CMD+W",
+            "CMD+S",
+            "CMD+SHIFT+S",
+            "CMD+P",
+            "CMD+Z",
+            "CMD+Y",
+            "CMD+X",
+            "CMD+C",
+            "CMD+V",
+            "CTRL+CMD+V",
+            "ALT+SHIFT+CMD+V",
+            "CMD+A",
+            "CMD+D",
+            "CMD+F",
+            "ALT+CMD+F",
+            "CTRL+CMD+Z",
+            "CMD+1",
+            "CMD+2",
+            "CMD+3",
+            "CMD+4",
+            "CMD+5",
+            "ALT+ENTER",
+            "CMD+SHIFT+ENTER",
+            "CTRL+SHIFT+TAB",
+            "ALT+CMD+R",
+            "SHIFT+F9",
+            "ALT+F9",
+            "CMD+=",
+            "CMD+-",
+            "CMD+0",
+            "CTRL+CMD+F",
+            "CMD+SHIFT+N",
+            "CMD+SHIFT+D",
+            "CMD+SHIFT+M",
+            "CMD+K",
+            "CMD+B",
+            "CMD+I",
+            "CMD+U",
+            "CMD+T",
+            "ALT+CMD+M",
+            "CMD+L",
+            "CMD+E",
+            "CMD+R",
+            "CMD+J",
+            "CMD+SHIFT+C",
+            "CMD+SHIFT+V",
+            "ALT+CMD+SHIFT+C",
+            "CMD+SHIFT+2",
+            "SHIFT+C",
+            "CMD+SHIFT+1",
+            "CMD+SHIFT+F",
+            "CMD+SHIFT+B",
+            "ALT+CMD+SHIFT+F",
+            "ALT+CMD+SHIFT+B",
+            "ALT+CMD+G",
+            "ALT+CMD+SHIFT+G",
+            "ALT+CMD+J",
+            "ALT+CMD+U",
+            "CTRL+ALT+CMD+R",
+            "ALT+F8",
+            "CMD+ENTER",
+            "CMD+M",
+            "CMD+Q",
+        ] {
+            if let Err(e) = Accelerator::from_str(acc) {
+                panic!("accelerator {acc} failed: {e:?}");
+            }
+        }
+    }
+}
diff --git a/apps/deckcraft/tests/mac_menu_appkit.rs b/apps/deckcraft/tests/mac_menu_appkit.rs
new file mode 100644
--- /dev/null
+++ b/apps/deckcraft/tests/mac_menu_appkit.rs
@@ -0,0 +1,32 @@
+//! The macOS menu bar as AppKit sees it, on the main thread (`harness = false`): install it through
+//! the real muda backend (`src/native_menu.rs`) and read NSApp back. The Window menu must be NSApp's
+//! `windowsMenu`: that is where macOS adds Move & Resize, Fill and Center, the targets of the system
+//! window tiling shortcuts (Fn+Ctrl+arrows, Fn+F), and lists the open windows.
+
+#[cfg(target_os = "macos")]
+#[path = "../src/native_menu.rs"]
+#[allow(unused_imports, dead_code)]
+mod native_menu;
+
+#[cfg(target_os = "macos")]
+fn main() {
+    use objc2::MainThreadMarker;
+    use objc2_app_kit::NSApplication;
+
+    let mtm = MainThreadMarker::new().expect("harness = false runs on the main thread");
+    let nsapp = NSApplication::sharedApplication(mtm);
+    nsapp.finishLaunching();
+
+    let ctx = egui::Context::default();
+    let _menu = native_menu::NativeMenu::install(&ctx);
+
+    let bar = nsapp.mainMenu().expect("a main menu");
+    let tops: Vec<String> = (0..bar.numberOfItems()).filter_map(|i| bar.itemAtIndex(i)?.submenu()).map(|m| m.title().to_string()).collect();
+    assert!(tops.starts_with(&["DeckCraft".into(), "File".into(), "Edit".into(), "View".into(), "Insert".into()]));
+    assert_eq!(nsapp.windowsMenu().map(|m| m.title().to_string()).as_deref(), Some("Window"), "AppKit knows the Window menu");
+
+    println!("mac_menu_appkit: ok");
+}
+
+#[cfg(not(target_os = "macos"))]
+fn main() {}
