diff --git a/apps/soundcraft/src/native_menu.rs b/apps/soundcraft/src/native_menu.rs
--- a/apps/soundcraft/src/native_menu.rs
+++ b/apps/soundcraft/src/native_menu.rs
@@ -33,6 +33,7 @@ impl NativeMenu {
         let mut native = Self { menu, entries: Vec::new(), events, last_sync: None };
         let app_menu = Submenu::new("SoundCraft", true);
         native.add_item(app, &app_menu, "About SoundCraft", "", Some("window.about".into()))?;
+        native.add_item(app, &app_menu, "Preferences...", "", Some("setup.preferences".into()))?;
         native.add_item(app, &app_menu, "Session Info", "", Some("window.session_info".into()))?;
         native.add_item(app, &app_menu, "Session Audio Health", "", Some("window.audio_health".into()))?;
         app_menu.append(&PredefinedMenuItem::separator())?;
@@ -94,8 +95,8 @@ impl NativeMenu {
                 && let Some(command) = &entry.command
                 && menus::item_state(app, &entry.path, Some(command)).0
             {
-                if entry.path.is_empty() && command.starts_with("window.") {
-                    // The application menu's About / Session Info open their window (never close it).
+                if entry.path.is_empty() && (command.starts_with("window.") || command == "setup.preferences") {
+                    // Application-menu window entries open their window rather than toggling it.
                     let _ = app.run(command, serde_json::json!({"value": true}));
                 } else {
                     menus::invoke_menu(app, command, &entry.path);
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
@@ -322,6 +322,18 @@ impl Engine {
         self.undo.iter().map(|h| h.label.clone()).collect()
     }
 
+    /// Change the retained document-history depth, dropping the oldest excess entries now.
+    pub fn set_history_limit(&mut self, limit: usize) {
+        let limit = limit.max(1);
+        self.history_limit = limit;
+        if self.undo.len() > limit {
+            self.undo.drain(..self.undo.len() - limit);
+        }
+        if self.redo.len() > limit {
+            self.redo.drain(..self.redo.len() - limit);
+        }
+    }
+
     pub fn undo(&mut self) -> bool {
         let Some(h) = self.undo.pop() else { return false };
         let cur = std::mem::replace(&mut self.doc, h.doc);
@@ -589,6 +601,20 @@ mod merge_tests {
 mod dirty_tests {
     use super::*;
 
+    #[test]
+    fn reducing_the_history_limit_drops_the_oldest_steps_immediately() {
+        let mut e = Engine::default();
+        for _ in 0..4 {
+            e.execute("track.new", &json!({})).unwrap();
+        }
+        assert_eq!(e.undo_history().len(), 4);
+        e.set_history_limit(2);
+        assert_eq!(e.undo_history().len(), 2);
+        assert!(e.undo());
+        assert!(e.undo());
+        assert!(!e.undo());
+    }
+
     #[test]
     fn selecting_locating_and_zooming_leave_the_session_clean() {
         let mut e = crate::demo::demo_engine();
diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs
--- a/crates/ui-egui/src/app.rs
+++ b/crates/ui-egui/src/app.rs
@@ -53,6 +53,7 @@ impl SoundApp {
             ops: ops_windows::OpsState::default(),
             extra: extra_windows::ExtraState::default(),
             palette: palette::PaletteState::default(),
+            preferences: preferences::PreferencesState::default(),
             focus_mix: false,
             edit_scroll_into_view: None,
             mix_scroll_into_view: None,
@@ -125,6 +126,8 @@ impl SoundApp {
     /// Each host iteration starts with `raw_input_hook`; its editor-poll admission prevents an
     /// empty queue on a discarded pass from ending a still-active plugin gesture.
     pub fn logic(&mut self, ctx: &egui::Context) {
+        self.ui.undo_levels = self.ui.undo_levels.clamp(1, 64);
+        self.engine.set_history_limit(self.ui.undo_levels);
         // Fonts set now take effect next frame, so draw only from the frame after.
         if !self.fonts_ready {
             if self.fonts_installed {
@@ -322,6 +325,7 @@ impl SoundApp {
         }
         panels::floating(self, &ctx);
         windows::show(self, &ctx);
+        preferences::show(self, &ctx);
         video_window::show(self, &ctx);
         ops_windows::show(self, &ctx);
         extra_windows::show(self, &ctx);
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
@@ -18,6 +18,7 @@ pub mod menus;
 pub mod mix_window;
 pub mod palette;
 pub mod panels;
+pub mod preferences;
 pub mod shortcuts;
 pub mod theme;
 pub mod toolbar;
@@ -105,6 +106,13 @@ pub struct UiState {
     pub show_workspace: bool,
     pub show_configurations: bool,
     pub show_playback_engine: bool,
+    /// Setup › Preferences. The selected page is kept per machine; the search and staged values
+    /// live in [`preferences::PreferencesState`] and are discarded when the window closes.
+    pub show_preferences: bool,
+    pub preferences_page: preferences::PreferencesPage,
+    pub preferences_window_pos: Option<[f32; 2]>,
+    pub reduce_visual_effects: preferences::ReduceVisualEffects,
+    pub undo_levels: usize,
     /// The audio device choice (Setup › Playback Engine), kept with the preferences: per machine.
     pub audio: soundcraft_playback::AudioConfig,
     /// The Mix window lives in its own native window (Window › Mix in Separate Window); the main
@@ -173,6 +181,11 @@ impl Default for UiState {
             show_workspace: false,
             show_configurations: false,
             show_playback_engine: false,
+            show_preferences: false,
+            preferences_page: preferences::PreferencesPage::default(),
+            preferences_window_pos: None,
+            reduce_visual_effects: preferences::ReduceVisualEffects::default(),
+            undo_levels: 64,
             audio: soundcraft_playback::AudioConfig::default(),
             mix_separate: false,
             show_io_setup: false,
@@ -292,6 +305,7 @@ pub struct SoundApp {
     pub ops: ops_windows::OpsState,
     pub extra: extra_windows::ExtraState,
     pub palette: palette::PaletteState,
+    pub preferences: preferences::PreferencesState,
     /// Bring the separate Mix window to the front on the next frame.
     pub(crate) focus_mix: bool,
     /// Scroll Into View: the track each window brings to its top (Edit) or left (Mix) when it
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -75,6 +75,8 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[
     ("ui.bounce_dialog", "Bounce Mix…", "", None),
     ("ui.set", "Set UI State", "", None),
     ("ui.theme", "Appearance", "", None),
+    ("ui.reduce_visual_effects", "Reduce Visual Effects", "", None),
+    ("ui.undo_levels", "Levels of Undo", "", None),
 ];
 
 /// Extra catalog mappings handled by the UI layer.
@@ -225,56 +227,65 @@ pub fn tree() -> Vec<MenuNode> {
 
 pub fn menu_bar(app: &mut SoundApp, ui: &mut egui::Ui) {
     let t = crate::theme::Tokens::current();
+    let menu_entries_enabled = !app.ui.show_preferences;
+    let preferences_enabled = preferences_menu_enabled(app);
     egui::Panel::top("menu_bar").exact_size(24.0).frame(egui::Frame::NONE.fill(t.menu_bar).inner_margin(egui::Margin::symmetric(8, 2))).show(
         ui,
         |ui| {
             egui::MenuBar::new().ui(ui, |ui| {
                 ui.menu_button("SoundCraft", |ui| {
-                    if ui.button("About SoundCraft").clicked() {
-                        app.ui.show_about = true;
-                    }
-                    if ui.button("Check for Updates…").clicked() {
-                        let _ = app.run("app.checkForUpdates", json!({}));
-                    }
-                    if ui.button("Session Info").clicked() {
-                        app.ui.show_session_info = true;
-                    }
-                    if ui.button("Session Audio Health").clicked() {
-                        let _ = app.run("window.audio_health", json!({"value": true}));
-                    }
-                    ui.separator();
-                    if ui.button("Quit").clicked() {
-                        let _ = crate::dialogs::run_or_ask(app, "app.quit", json!({}));
-                    }
+                    ui.add_enabled_ui(menu_entries_enabled, |ui| {
+                        if ui.button("About SoundCraft").clicked() {
+                            app.ui.show_about = true;
+                        }
+                        if ui.add_enabled(preferences_enabled, egui::Button::new("Preferences...")).clicked() {
+                            let _ = app.run("setup.preferences", json!({"value": true}));
+                        }
+                        if ui.button("Check for Updates…").clicked() {
+                            let _ = app.run("app.checkForUpdates", json!({}));
+                        }
+                        if ui.button("Session Info").clicked() {
+                            app.ui.show_session_info = true;
+                        }
+                        if ui.button("Session Audio Health").clicked() {
+                            let _ = app.run("window.audio_health", json!({"value": true}));
+                        }
+                        ui.separator();
+                        if ui.button("Quit").clicked() {
+                            let _ = crate::dialogs::run_or_ask(app, "app.quit", json!({}));
+                        }
+                    });
                 });
                 let extra = ui_aliases();
                 for root in tree() {
                     ui.menu_button(&root.label, |ui| {
-                        for c in &root.children {
-                            menu_node(app, ui, c, &extra);
-                            if c.path == "File > Open" {
-                                crate::recent::menu(app, ui);
+                        ui.add_enabled_ui(menu_entries_enabled, |ui| {
+                            for c in &root.children {
+                                menu_node(app, ui, c, &extra);
+                                if c.path == "File > Open" {
+                                    crate::recent::menu(app, ui);
+                                }
+                                if c.path == "Track > New..." {
+                                    instrument_menu(app, ui);
+                                }
                             }
-                            if c.path == "Track > New..." {
-                                instrument_menu(app, ui);
+                            if root.label == "AudioSuite" {
+                                ui.separator();
+                                ui.label(egui::RichText::new("Processes selected clips offline").small().color(t.text_dim));
                             }
-                        }
-                        if root.label == "AudioSuite" {
-                            ui.separator();
-                            ui.label(egui::RichText::new("Processes selected clips offline").small().color(t.text_dim));
-                        }
-                        if root.label == "Window" {
-                            ui.separator();
-                            let mut separate = app.ui.mix_separate;
-                            if ui
-                                .checkbox(&mut separate, "Mix in Separate Window")
-                                .on_hover_text("Show Edit and Mix at once, e.g. one per monitor")
-                                .changed()
-                            {
-                                let _ = run_ui_command(app, "window.mix_separate", &json!({"value": separate}));
-                                ui.close();
+                            if root.label == "Window" {
+                                ui.separator();
+                                let mut separate = app.ui.mix_separate;
+                                if ui
+                                    .checkbox(&mut separate, "Mix in Separate Window")
+                                    .on_hover_text("Show Edit and Mix at once, e.g. one per monitor")
+                                    .changed()
+                                {
+                                    let _ = run_ui_command(app, "window.mix_separate", &json!({"value": separate}));
+                                    ui.close();
+                                }
                             }
-                        }
+                        });
                     });
                 }
                 let title = format!(
@@ -338,18 +349,31 @@ pub fn command_for_path(path: &str, extra: &[(&str, &str)]) -> Option<String> {
 
 /// Enabled and checked state shared by all menu presentations.
 pub fn item_state(app: &SoundApp, path: &str, id: Option<&str>) -> (bool, bool) {
-    let enabled = match id {
-        Some(i)
-            if i.starts_with("window.") || i.starts_with("view.mix_section") || matches!(i, "audiosuite.process" | "ui.dock" | "ui.dock.reset") =>
-        {
-            true
+    let enabled = if app.ui.show_preferences {
+        false
+    } else {
+        match id {
+            Some("setup.preferences") => preferences_menu_enabled(app),
+            Some(i)
+                if i.starts_with("window.")
+                    || i.starts_with("view.mix_section")
+                    || matches!(i, "audiosuite.process" | "ui.dock" | "ui.dock.reset") =>
+            {
+                true
+            }
+            Some(i) => soundcraft_engine::find_command(i).is_some_and(|c| (c.enabled)(&app.engine).is_ok() && !app.engine.blocked_while_recording(i)),
+            None => false,
         }
-        Some(i) => soundcraft_engine::find_command(i).is_some_and(|c| (c.enabled)(&app.engine).is_ok() && !app.engine.blocked_while_recording(i)),
-        None => false,
     };
     (enabled, checked_state(app, path, id))
 }
 
+/// Preferences cannot replace audio resources while transport is active or another Preferences
+/// instance is already modal.
+fn preferences_menu_enabled(app: &SoundApp) -> bool {
+    !app.ui.show_preferences && !app.is_playing() && !app.engine.transport.recording
+}
+
 fn checked_state(app: &SoundApp, path: &str, id: Option<&str>) -> bool {
     let e = &app.engine.session().edit;
     match id {
@@ -469,6 +493,10 @@ pub fn live_label(app: &SoundApp, id: Option<&str>) -> Option<String> {
 
 /// Menu-style invocation: may open a dialog.
 pub fn invoke_menu(app: &mut SoundApp, id: &str, path: &str) {
+    if id == "setup.preferences" {
+        let _ = app.run(id, json!({"value": true}));
+        return;
+    }
     if let Some((_, w)) = MENU_WINDOWS.iter().find(|(p, _)| *p == path)
         && w.starts_with("window.")
     {
@@ -582,6 +610,15 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
         "window.midi_ops" => toggle(&mut app.ui.show_midi_ops),
         "window.rtp" => toggle(&mut app.ui.show_rtp),
         "window.playback_engine" => toggle(&mut app.ui.show_playback_engine),
+        // The engine catalog already exposes this command. Handling it here keeps the menu,
+        // control channel and scripted callers on the same public command id.
+        "setup.preferences" => {
+            let result = toggle(&mut app.ui.show_preferences);
+            if !app.ui.show_preferences {
+                app.preferences.discard_draft();
+            }
+            result
+        }
         "window.io_setup" => toggle(&mut app.ui.show_io_setup),
         "window.shortcuts" => toggle(&mut app.ui.show_shortcuts),
         "window.color_palette" => toggle(&mut app.ui.show_color_palette),
@@ -635,6 +672,8 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
             app.ui.show_undo_history = false;
             app.ui.show_session_info = false;
             app.ui.show_audio_health = false;
+            app.ui.show_preferences = false;
+            app.preferences.discard_draft();
             app.ui.show_automation = false;
             app.ui.plugin_windows.clear();
             app.ui.audiosuite = None;
@@ -689,6 +728,23 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
             app.ui.theme = mode;
             json!({"mode": mode.id()})
         }
+        "ui.reduce_visual_effects" => {
+            let Some(mode) = p.get("mode").and_then(Value::as_str) else {
+                return Some(Ok(json!({"mode": app.ui.reduce_visual_effects.id()})));
+            };
+            let Some(mode) = crate::preferences::ReduceVisualEffects::parse(mode) else {
+                return Some(Err(format!("unknown visual-effects mode `{mode}` (never, playback or always)")));
+            };
+            app.ui.reduce_visual_effects = mode;
+            json!({"mode": mode.id()})
+        }
+        "ui.undo_levels" => {
+            if let Some(levels) = p.get("levels").and_then(Value::as_u64) {
+                app.ui.undo_levels = usize::try_from(levels).unwrap_or(64).clamp(1, 64);
+                app.engine.set_history_limit(app.ui.undo_levels);
+            }
+            json!({"levels": app.ui.undo_levels})
+        }
         "ui.set" => match serde_json::to_value(&app.ui) {
             Ok(mut cur) => {
                 if let (Some(obj), Some(patch)) = (cur.as_object_mut(), p.as_object()) {
@@ -701,8 +757,14 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
                         if let Err(error) = crate::docking::validate(&ns.docking).and_then(|()| crate::docking::validate_mix(&ns.mix_docking)) {
                             return Some(Err(error));
                         }
+                        let closed_preferences = app.ui.show_preferences && !ns.show_preferences;
                         crate::docking::cancel_replaced_state(app, &ns);
                         app.ui = ns;
+                        app.ui.undo_levels = app.ui.undo_levels.clamp(1, 64);
+                        app.engine.set_history_limit(app.ui.undo_levels);
+                        if closed_preferences {
+                            app.preferences.discard_draft();
+                        }
                         json!({"ui": app.ui})
                     }
                     Err(e) => return Some(Err(format!("bad ui state: {e}"))),
@@ -726,6 +788,49 @@ mod tests {
     use super::*;
     use crate::{Services, SoundApp};
 
+    /// Preferences menu entries follow transport safety and the modal window locks every menu.
+    #[test]
+    fn preferences_menu_state_tracks_transport_and_modality() {
+        let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
+        assert!(item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
+
+        app.engine.transport.playing = true;
+        assert!(!item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
+        app.engine.transport.playing = false;
+        app.engine.transport.recording = true;
+        assert!(!item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
+
+        app.engine.transport.recording = false;
+        app.ui.show_preferences = true;
+        assert!(!item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
+        assert!(!item_state(&app, "Window > About", Some("window.about")).0);
+        assert!(!item_state(&app, "File > Save Session", Some("session.save")).0);
+    }
+
+    /// Re-selecting Preferences through either menu keeps its modal window open.
+    #[test]
+    fn preferences_menu_always_opens_instead_of_toggling() {
+        let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
+        invoke_menu(&mut app, "setup.preferences", "Setup > Preferences...");
+        assert!(app.ui.show_preferences);
+        invoke_menu(&mut app, "setup.preferences", "Setup > Preferences...");
+        assert!(app.ui.show_preferences);
+    }
+
+    /// Generic UI-state updates clamp and immediately synchronize the engine history limit.
+    #[test]
+    fn ui_set_synchronizes_undo_levels_immediately() {
+        let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
+
+        let result = app.run("ui.set", json!({"undo_levels": 0})).unwrap();
+        assert_eq!(result["ui"]["undo_levels"], 1);
+        assert_eq!((app.ui.undo_levels, app.engine.history_limit), (1, 1));
+
+        let result = app.run("ui.set", json!({"undo_levels": 100})).unwrap();
+        assert_eq!(result["ui"]["undo_levels"], 64);
+        assert_eq!((app.ui.undo_levels, app.engine.history_limit), (64, 64));
+    }
+
     #[test]
     fn mix_view_aliases_are_allocated_once() {
         let first = ui_aliases();
diff --git a/crates/ui-egui/src/panels/extra_windows.rs b/crates/ui-egui/src/panels/extra_windows.rs
--- a/crates/ui-egui/src/panels/extra_windows.rs
+++ b/crates/ui-egui/src/panels/extra_windows.rs
@@ -16,7 +16,6 @@ pub struct ExtraState {
     pub show_ui_customization: bool,
     pub auto_update_config: bool,
     pub ui_scale: f32,
-    pub reduce_motion: bool,
     pub tooltips: bool,
     last_auto_update: f64,
     applied: Option<(f32, bool)>,
@@ -29,7 +28,6 @@ impl Default for ExtraState {
             show_ui_customization: false,
             auto_update_config: false,
             ui_scale: 1.0,
-            reduce_motion: false,
             tooltips: true,
             last_auto_update: 0.0,
             applied: None,
@@ -59,7 +57,12 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
 }
 
 fn apply_customization(app: &mut SoundApp, ctx: &egui::Context) {
-    let want = (app.extra.ui_scale.clamp(0.75, 1.5), app.extra.reduce_motion);
+    let reduce = match app.ui.reduce_visual_effects {
+        crate::preferences::ReduceVisualEffects::Never => false,
+        crate::preferences::ReduceVisualEffects::Playback => app.is_playing(),
+        crate::preferences::ReduceVisualEffects::Always => true,
+    };
+    let want = (app.extra.ui_scale.clamp(0.75, 1.5), reduce);
     // Rescaling mid-drag would move the Interface scale slider out from under the pointer.
     if app.extra.applied == Some(want) || ctx.input(|i| i.pointer.any_down()) {
         return;
@@ -163,7 +166,17 @@ fn ui_customization(app: &mut SoundApp, ctx: &egui::Context) {
                     .custom_formatter(|v, _| format!("{:.0} %", v * 100.0))
                     .text("Interface scale"),
             );
-            crate::widgets::checkbox(ui, &mut app.extra.reduce_motion, "Reduce motion");
+            let mut chosen_effects = None;
+            craft_ui::ComboBox::from_label("Reduce visual effects").selected_text(app.ui.reduce_visual_effects.label()).show_ui(ui, |ui| {
+                for mode in crate::preferences::ReduceVisualEffects::ALL {
+                    if ui.selectable_label(app.ui.reduce_visual_effects == mode, mode.label()).clicked() {
+                        chosen_effects = Some(mode);
+                    }
+                }
+            });
+            if let Some(mode) = chosen_effects {
+                let _ = app.run("ui.reduce_visual_effects", json!({"mode": mode.id()}));
+            }
             let mut chosen = None;
             craft_ui::ComboBox::from_label("Theme").selected_text(app.ui.theme.label()).show_ui(ui, |ui| {
                 for mode in crate::theme::ThemeMode::ALL {
@@ -179,7 +192,7 @@ fn ui_customization(app: &mut SoundApp, ctx: &egui::Context) {
             ui.add_space(4.0);
             if ui.button("Reset").clicked() {
                 app.extra.ui_scale = 1.0;
-                app.extra.reduce_motion = false;
+                let _ = app.run("ui.reduce_visual_effects", json!({"mode": "never"}));
                 let _ = app.run("ui.theme", json!({"mode": crate::theme::ThemeMode::default().id()}));
             }
         },
diff --git a/crates/ui-egui/src/preferences.rs b/crates/ui-egui/src/preferences.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/preferences.rs
@@ -0,0 +1,1308 @@
+//! SoundCraft / Setup › Preferences: the complete settings surface, including placeholders for features whose
+//! underlying behaviour does not exist yet. A placeholder is never interactive: enabling a row
+//! requires a real command and persistence policy first.
+
+use crate::SoundApp;
+use crate::theme::{ThemeMode, Tokens};
+use egui::{Key, Modifiers, RichText, Stroke, vec2};
+use serde_json::json;
+
+const PREFERENCES_WINDOW_WIDTH: f32 = 900.0;
+const PREFERENCES_WINDOW_HEIGHT: f32 = 945.0;
+const PREFERENCES_BODY_HEIGHT: f32 = 800.0;
+const PREFERENCES_SCREEN_MARGIN: f32 = 24.0;
+const PREFERENCES_MIN_HEIGHT: f32 = 320.0;
+
+/// One of the pages exposed by the Preferences window, in shortcut order.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PreferencesPage {
+    #[default]
+    Display,
+    Operation,
+    Editing,
+    Mixing,
+    Metering,
+    Processing,
+    Midi,
+    Collaboration,
+    Synchronization,
+}
+
+/// Controls when optional interface animation and decoration are reduced.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ReduceVisualEffects {
+    #[default]
+    Never,
+    Playback,
+    Always,
+}
+
+impl ReduceVisualEffects {
+    pub const ALL: [Self; 3] = [Self::Never, Self::Playback, Self::Always];
+
+    /// Returns the text shown for this mode in preference selectors.
+    pub const fn label(self) -> &'static str {
+        match self {
+            Self::Never => "Never",
+            Self::Playback => "Only During Playback",
+            Self::Always => "Always",
+        }
+    }
+
+    /// Returns the stable command and persistence identifier for this mode.
+    pub const fn id(self) -> &'static str {
+        match self {
+            Self::Never => "never",
+            Self::Playback => "playback",
+            Self::Always => "always",
+        }
+    }
+
+    /// Parses a command or persistence identifier into a visual-effects mode.
+    pub fn parse(id: &str) -> Option<Self> {
+        match id {
+            "never" => Some(Self::Never),
+            "playback" => Some(Self::Playback),
+            "always" => Some(Self::Always),
+            _ => None,
+        }
+    }
+}
+
+impl PreferencesPage {
+    pub const ALL: [Self; 9] = [
+        Self::Display,
+        Self::Operation,
+        Self::Editing,
+        Self::Mixing,
+        Self::Metering,
+        Self::Processing,
+        Self::Midi,
+        Self::Collaboration,
+        Self::Synchronization,
+    ];
+
+    /// Returns the page title shown in the tab strip.
+    pub const fn label(self) -> &'static str {
+        match self {
+            Self::Display => "Display",
+            Self::Operation => "Operation",
+            Self::Editing => "Editing",
+            Self::Mixing => "Mixing",
+            Self::Metering => "Metering",
+            Self::Processing => "Processing",
+            Self::Midi => "MIDI",
+            Self::Collaboration => "Collaboration",
+            Self::Synchronization => "Synchronization",
+        }
+    }
+
+    /// Returns the static section catalog belonging to this page.
+    const fn sections(self) -> &'static [Section] {
+        match self {
+            Self::Display => DISPLAY,
+            Self::Operation => OPERATION,
+            Self::Editing => EDITING,
+            Self::Mixing => MIXING,
+            Self::Metering => METERING,
+            Self::Processing => PROCESSING,
+            Self::Midi => MIDI,
+            Self::Collaboration => COLLABORATION,
+            Self::Synchronization => SYNCHRONIZATION,
+        }
+    }
+}
+
+/// Values edited in the window before the user applies or cancels them.
+#[derive(Debug, Clone, Copy)]
+struct PreferencesDraft {
+    theme: ThemeMode,
+    reduce_visual_effects: ReduceVisualEffects,
+    undo_levels: usize,
+    theme_edited: bool,
+    reduce_visual_effects_edited: bool,
+    undo_levels_edited: bool,
+}
+
+impl PreferencesDraft {
+    /// Starts a clean draft from the current live, per-machine settings.
+    fn from_live(app: &SoundApp) -> Self {
+        Self {
+            theme: app.ui.theme,
+            reduce_visual_effects: app.ui.reduce_visual_effects,
+            undo_levels: app.ui.undo_levels,
+            theme_edited: false,
+            reduce_visual_effects_edited: false,
+            undo_levels_edited: false,
+        }
+    }
+
+    /// Pulls in scripted setting changes while retaining fields edited in this window.
+    fn sync_unedited_from_live(&mut self, app: &SoundApp) {
+        if !self.theme_edited {
+            self.theme = app.ui.theme;
+        }
+        if !self.reduce_visual_effects_edited {
+            self.reduce_visual_effects = app.ui.reduce_visual_effects;
+        }
+        if !self.undo_levels_edited {
+            self.undo_levels = app.ui.undo_levels;
+        }
+    }
+
+    /// Remembers which values the user changed during the current UI frame.
+    fn remember_user_edits(&mut self, before: Self) {
+        self.theme_edited |= self.theme != before.theme;
+        self.reduce_visual_effects_edited |= self.reduce_visual_effects != before.reduce_visual_effects;
+        self.undo_levels_edited |= self.undo_levels != before.undo_levels;
+    }
+}
+
+/// Transient Preferences-window state that is never stored in a session.
+#[derive(Debug, Default)]
+pub struct PreferencesState {
+    search: String,
+    draft: Option<PreferencesDraft>,
+    popup_open: bool,
+}
+
+impl PreferencesState {
+    /// Drops unapplied values and search text when the window is closed by any route.
+    pub(crate) fn discard_draft(&mut self) {
+        self.draft = None;
+        self.search.clear();
+        self.popup_open = false;
+    }
+}
+
+/// A declarative preference control, including disabled placeholders without a backend yet.
+#[derive(Clone, Copy)]
+enum Control {
+    Check(&'static str),
+    Choice(&'static str, &'static str),
+    ChoiceOptions(&'static str, &'static str, &'static [&'static str]),
+    Value(&'static str, &'static str),
+    Button(&'static str),
+    Radios(&'static str, &'static [&'static str]),
+    Theme,
+    ReduceVisualEffects,
+    UndoLevels,
+}
+
+impl Control {
+    /// Returns the user-facing name used by the control and search index.
+    const fn label(self) -> &'static str {
+        match self {
+            Self::Check(label)
+            | Self::Button(label)
+            | Self::Choice(label, _)
+            | Self::ChoiceOptions(label, _, _)
+            | Self::Value(label, _)
+            | Self::Radios(label, _) => label,
+            Self::Theme => "UI Theme",
+            Self::ReduceVisualEffects => "Reduce Visual Effects",
+            Self::UndoLevels => "Levels of Undo",
+        }
+    }
+
+    /// Builds the searchable text from a control's label, value and available choices.
+    fn search_text(self) -> String {
+        match self {
+            Self::Choice(label, value) | Self::Value(label, value) => format!("{label} {value}"),
+            Self::ChoiceOptions(label, value, values) => format!("{label} {value} {}", values.join(" ")),
+            Self::Radios(label, values) => format!("{label} {}", values.join(" ")),
+            Self::ReduceVisualEffects => format!("{} Never Only During Playback Always", self.label()),
+            _ => self.label().to_string(),
+        }
+    }
+}
+
+/// A titled group of controls within a preference page.
+#[derive(Clone, Copy)]
+struct Section {
+    title: &'static str,
+    controls: &'static [Control],
+}
+
+const DISPLAY: &[Section] = &[
+    Section {
+        title: "Basics",
+        controls: &[
+            Control::Check("Track Position Numbers Stay with Hidden Tracks"),
+            Control::Check("Function Tool Tips"),
+            Control::Check("Detailed Tool Tips"),
+            Control::Value("Edit Window Default Length", "00:00:00:00"),
+            Control::Choice("Organize Plugin Menus By", "Category & Manufacturer"),
+            Control::Choice("Organize Track I/O Menus By", "Type"),
+            Control::Check("Auto-Switch Input Language"),
+            Control::Choice("Language", "English"),
+            Control::Check("Default Automatic Naming to English"),
+            Control::Theme,
+            Control::ReduceVisualEffects,
+        ],
+    },
+    Section {
+        title: "General Color",
+        controls: &[
+            Control::Check("MIDI Note Color Shows Velocity"),
+            Control::Choice("Default Track Color Coding", "Track Type"),
+            Control::Choice("Default Clip Color Coding", "Track Color"),
+        ],
+    },
+    Section {
+        title: "Warnings & Dialogs",
+        controls: &[
+            Control::Button("Reset “Don’t Show This Again” Settings"),
+            Control::Check("Show Dashboard Window at Startup"),
+            Control::Check("Show Dashboard After Closing a Session"),
+        ],
+    },
+    Section {
+        title: "Marker Colors",
+        controls: &[
+            Control::Check("Randomize Marker Color Upon Creation"),
+            Control::Check("Show Marker Lane Colors in Timeline Rulers"),
+            Control::Check("Show Marker Lane Colors in Tracks"),
+        ],
+    },
+    Section {
+        title: "Pinned Tracks",
+        controls: &[
+            Control::Check("Auto-Adjust Pinned Area for Tracks"),
+            Control::Check("Adjusting Pinned Area Resizes Tracks"),
+            Control::Choice("Original Track Height While Pinned", "Small"),
+        ],
+    },
+];
+
+const OPERATION: &[Section] = &[
+    Section {
+        title: "Transport",
+        controls: &[
+            Control::Check("Timeline Insertion and Play Start Follow Playback"),
+            Control::Check("Edit Insertion Follows Scrub and Shuttle"),
+            Control::Check("Audio During Fast Forward and Rewind"),
+            Control::Check("Latch Fast Forward and Rewind"),
+            Control::Check("Play Start Follows Timeline Selection"),
+            Control::Check("Dynamic Transport Follows Selection During Playback"),
+            Control::Check("Reserve Voices for Preview in Context"),
+            Control::Value("Custom Shuttle Lock Speed", "800 %"),
+            Control::Value("Custom Fast Forward/Rewind Speed", "300 %"),
+            Control::Value("Back/Forward Amount", "2|0|000"),
+            Control::Choice("Back/Forward Time Scale", "Bars|Beats"),
+            Control::Radios("Numeric Keypad", &["Classic", "Transport", "Shuttle"]),
+            Control::Check("Use Separate Play and Stop Keys"),
+        ],
+    },
+    Section {
+        title: "Record",
+        controls: &[
+            Control::Choice("Record Enable Buttons", "Latch Audio Tracks"),
+            Control::Check("Enable Automation in Record"),
+            Control::Check("Link Record and Play Faders"),
+            Control::Check("Audio Track Record Lock"),
+            Control::Check("Transport Record Lock"),
+            Control::Check("Disable Input Monitoring when Disarming a Stopped Track"),
+            Control::Check("Mute Record-Armed Tracks While Stopped"),
+            Control::Check("Enable DSP Mode for Record-Armed or Input-Monitored Tracks"),
+            Control::Check("Disable DSP Mode when Disabling Record and Input"),
+            Control::Check("Console-Style Input Monitoring Indicators"),
+            Control::Check("Create New Playlists when Loop Recording"),
+            Control::Radios("Online Recording Starts", &["At Timecode Lock", "At Insertion or Selection"]),
+            Control::Value("Destructive Punch File Length", "25 minutes"),
+        ],
+    },
+    Section {
+        title: "Auto Backup",
+        controls: &[
+            Control::Check("Enable Session File Auto Backup"),
+            Control::Value("Keep Most Recent Session Backups", "10"),
+            Control::Value("Backup Every", "5 minutes"),
+        ],
+    },
+    Section {
+        title: "Miscellaneous",
+        controls: &[
+            Control::Check("Mouse Wheel Scrolling Snaps to Track"),
+            Control::Value("Clip Auto Fade In/Out Length", "0 ms"),
+            Control::Value("Local Waveform Cache Versions", "0"),
+        ],
+    },
+    Section {
+        title: "User Media and Settings Location",
+        controls: &[
+            Control::Button("Change Documentation and Settings Location"),
+            Control::Button("Change Project Media Cache Location"),
+            Control::Button("Change Sound Libraries Location"),
+            Control::Check("Show Sound Libraries in Workspace Locations"),
+        ],
+    },
+    Section {
+        title: "Video",
+        controls: &[
+            Control::Check("Enable GPU Processing"),
+            Control::Check("Prevent Tearing"),
+            Control::Choice("Play Start Latency", "1 — Shortest"),
+            Control::Choice("Slewing", "Disabled While Looping"),
+        ],
+    },
+    Section {
+        title: "Data Collection and Personalization",
+        controls: &[Control::Check("Anonymous Usage Data"), Control::Check("Personalize the Experience")],
+    },
+    Section {
+        title: "Logging",
+        controls: &[Control::Button("Save Logs"), Control::Button("Reset Logging"), Control::Check("Advanced Video Logging")],
+    },
+];
+
+const EDITING: &[Section] = &[
+    Section {
+        title: "Clips",
+        controls: &[
+            Control::Check("Clip List Selection Follows Edit Selection"),
+            Control::Check("Edit Selection Follows Clip List Selection"),
+            Control::Check("Auto-Name Separated Clips"),
+            Control::Check("Separate Clip Operates on All Related Takes"),
+            Control::Check("Send Fully Overlapped Clips to a Playlist While Editing"),
+            Control::Check("Send Fully Overlapped Clips to a Playlist While Recording"),
+            Control::Check("Tab Respects Breakpoints in Automation Lanes"),
+            Control::Check("Double-Click Selects Automation Breakpoints"),
+            Control::Value("Clip Gain Nudge Value", "0.5 dB"),
+            Control::Choice("Number Keys 1–5 Control", "Zoom Presets"),
+            Control::Choice("Double-Clicking an Audio Clip Opens", "Name Dialog"),
+        ],
+    },
+    Section {
+        title: "Fades",
+        controls: &[
+            Control::Value("Fades Dialog Preview Pre-Roll", "3000 ms"),
+            Control::Value("Fades Dialog Preview Post-Roll", "3000 ms"),
+            Control::Value("Punch Recording Crossfade Length", "0 ms"),
+            Control::Check("Preserve Fades when Editing"),
+            Control::Check("Auto Accept Adjust Bounds"),
+            Control::Choice("Smart Tool Fade Adjustment", "Always On"),
+            Control::Button("Default Fade In"),
+            Control::Button("Default Fade Out"),
+            Control::Button("Default Crossfade"),
+            Control::Button("Default Sliced-Loop Fade"),
+        ],
+    },
+    Section {
+        title: "Tracks",
+        controls: &[
+            Control::Check("New Tracks Default to Tick Timebase"),
+            Control::Check("Suppress Name Dialog when Creating New Playlists"),
+            Control::Check("Show Target Playlists after Sending a Clip Selection"),
+            Control::Choice("Add New Playlists", "At Bottom"),
+        ],
+    },
+    Section {
+        title: "Zoom Toggle",
+        controls: &[
+            Control::Choice("Vertical MIDI Zoom", "Selection"),
+            Control::Choice("Horizontal Zoom", "Selection"),
+            Control::Check("Remove Range Selection after Zooming In"),
+            Control::Choice("Track Height", "Fit to Window"),
+            Control::Choice("Track View", "No Change"),
+            Control::Check("Separate Grid Settings when Zoomed In"),
+            Control::Check("Zoom Toggle Follows Edit Selection"),
+        ],
+    },
+    Section {
+        title: "Memory Locations",
+        controls: &[
+            Control::Check("Auto-Name Memory Locations when Playing"),
+            Control::Check("Recall Memory Location at Original Track"),
+            Control::Check("Marker Target Follows First Selected Track"),
+        ],
+    },
+    Section { title: "Undo", controls: &[Control::UndoLevels] },
+];
+
+const MIXING: &[Section] = &[
+    Section {
+        title: "Setup",
+        controls: &[
+            Control::Value("Sends Default to", "−∞ dB"),
+            Control::Check("Send Pans Default to Follow Main Pan"),
+            Control::Check("Use Absolute Pan Linking"),
+            Control::Check("Allow Sends to Persist During Low-Latency Monitoring"),
+            Control::Choice("Default EQ", "None"),
+            Control::Choice("Default EQ Insert Slot", "None"),
+            Control::Choice("Default Dynamics", "None"),
+            Control::Choice("Default Dynamics Insert Slot", "None"),
+            Control::Check("Auto Insert Default Plugins from Control Surfaces"),
+        ],
+    },
+    Section {
+        title: "Controllers",
+        controls: &[
+            Control::Check("Edit Window Follows Banking"),
+            Control::Check("Edit Window Follows Selection"),
+            Control::Check("Mix Window Follows Banking"),
+            Control::Check("Mix Window Follows Selection"),
+            Control::Check("Scroll to Track Banks Controllers"),
+            Control::Check("Always Fill Channel Strips when Banking"),
+            Control::Check("Always Show Assignable Knob Highlight"),
+            Control::Value("Touch Timeout", "1000 ms"),
+            Control::Choice("EQ and Dynamics Cycling Starts at Insert", "None"),
+        ],
+    },
+    Section {
+        title: "Automation",
+        controls: &[
+            Control::Check("Smooth and Thin Data after Pass"),
+            Control::Choice("Degree of Thinning", "Some"),
+            Control::Check("Plugin Controls Default to Auto-Enabled"),
+            Control::Check("Suppress Automation Write-To Warnings"),
+            Control::Check("Latch Switch Controls in Touch Mode"),
+            Control::Check("Allow Latch Prime in Stop"),
+            Control::Check("Coalesce when Removing Members from a VCA Group"),
+            Control::Check("Standard VCA Logic for Group Attributes"),
+            Control::Check("Include Sends in Trim Mode"),
+            Control::Check("Include Control Changes in Undo Queue"),
+            Control::Check("Link Object and Audio Recording"),
+            Control::Radios("After Write Pass, Switch To", &["Touch", "Latch", "No Change"]),
+            Control::Radios("Coalesce Trim Automation", &["After Every Pass", "On Exiting Trim Mode", "Manually"]),
+            Control::Value("Steep Breakpoint Smoothing Time", "0 ms"),
+            Control::Value("Automation Match Time", "250 ms"),
+            Control::Value("Automation Glide Time", "1000 ms"),
+        ],
+    },
+    Section {
+        title: "Delay Compensation",
+        controls: &[Control::Choice("Time Mode", "Samples"), Control::Check("Compensate Side Chains"), Control::Check("Compensate Hardware Inputs")],
+    },
+    Section {
+        title: "Routing Coefficients",
+        controls: &[
+            Control::Choice("New Session Fold Down", "Modern"),
+            Control::Choice("New Session 5.x to 7.x/9.x", "Direct to Rear Surrounds"),
+            Control::Choice("New Session 7.x/9.x to 5.x", "Downmix to Surrounds"),
+            Control::Value("New Session Surround to Stereo", "−3.0 dB"),
+            Control::Check("New Session Includes LFE in Fold Down"),
+            Control::Choice("Current Session Fold Down", "Modern"),
+            Control::Choice("Current Session 5.x to 7.x/9.x", "Direct to Rear Surrounds"),
+            Control::Choice("Current Session 7.x/9.x to 5.x", "Downmix to Surrounds"),
+            Control::Value("Current Session Surround to Stereo", "−3.0 dB"),
+            Control::Check("Current Session Includes LFE in Fold Down"),
+        ],
+    },
+];
+
+const METER_TYPES: &[&str] = &[
+    "Sample Peak",
+    "Classic",
+    "Linear",
+    "Linear Extended",
+    "RMS",
+    "VU",
+    "Digital VU",
+    "PPM Digital",
+    "PPM BBC",
+    "PPM Nordic",
+    "PPM EBU",
+    "PPM DIN",
+    "K-12",
+    "K-14",
+    "K-20",
+    "Live Peak",
+    "Live RMS",
+];
+
+const METERING: &[Section] = &[
+    Section {
+        title: "Track and Master Meter Types",
+        controls: &[
+            Control::Check("Track and Master Meter Types Linked"),
+            Control::ChoiceOptions("Track Meters", "Digital VU", METER_TYPES),
+            Control::ChoiceOptions("Master Meters", "Linear", METER_TYPES),
+        ],
+    },
+    Section {
+        title: "Advanced Meter Type Settings",
+        controls: &[
+            Control::Choice("Meter Type", "Digital VU"),
+            Control::Value("Decay", "100 dB in 1.00 s"),
+            Control::Value("0 dB Reference", "0 dBFS"),
+            Control::Value("Integration Time", "— ms"),
+            Control::Value("Color Break High", "−6 dB"),
+            Control::Value("Color Break Low", "−20 dB"),
+            Control::Button("Reset Advanced Meter Settings"),
+        ],
+    },
+    Section {
+        title: "Peak / Clip",
+        controls: &[
+            Control::Radios("Peak Hold", &["3 Seconds", "Infinite", "None"]),
+            Control::Radios("Clip Indication", &["3 Seconds", "Infinite", "None"]),
+            Control::Value("Calibration Reference Level", "−18 dB"),
+        ],
+    },
+    Section {
+        title: "Display",
+        controls: &[
+            Control::Check("Show Send Assignment Level Meter"),
+            Control::Check("Show Insert Assignment Gain Reduction Meter"),
+            Control::Check("Show Track Gain Reduction Meter"),
+            Control::Radios(
+                "Gain Reduction Meter Type",
+                &["Compressor/Limiter Only", "Expander/Gate Only", "Priority Compressor/Limiter", "Priority Expander/Gate", "All — Summed"],
+            ),
+        ],
+    },
+];
+
+const PROCESSING: &[Section] = &[
+    Section {
+        title: "Offline Processing",
+        controls: &[Control::Radios("Default Handle Length", &["Whole File", "Length"]), Control::Value("Handle Length", "2.00 seconds")],
+    },
+    Section {
+        title: "Time Compression / Expansion",
+        controls: &[Control::Choice("Processor", "Time Stretch"), Control::Choice("Default Settings", "Factory Default")],
+    },
+    Section {
+        title: "Import",
+        controls: &[
+            Control::Check("Convert Imported WAV Files to Broadcast Wave"),
+            Control::Check("Automatically Copy Files on Import"),
+            Control::Check("Convert Copied Files to Session Format"),
+            Control::Check("Do Not Convert Sample Rate on Import"),
+            Control::Check("Import Sliced Loops as Clip Groups"),
+            Control::Check("Automatically Create Fades"),
+            Control::Radios("Desktop Drag-and-Drop Conforms to Session Tempo", &["No Files", "Tagged Loop Files Only", "All Files"]),
+            Control::Choice("Sample Rate Conversion Quality", "Highest"),
+        ],
+    },
+    Section {
+        title: "Elastic Audio",
+        controls: &[
+            Control::Choice("Default Plugin", "Polyphonic"),
+            Control::Value("Default Input Gain", "0.0 dB"),
+            Control::Check("Enable Elastic Audio on New Tracks"),
+        ],
+    },
+    Section { title: "Commit", controls: &[Control::Radios("Rendered File Bit Depth", &["Always Use 32-bit", "Follow Session Settings"])] },
+    Section {
+        title: "DSP Management",
+        controls: &[Control::Check("Open Unresourced DSP Plugins as Native"), Control::Check("Enable Analog Console Processing in New Sessions")],
+    },
+    Section { title: "ARA", controls: &[Control::Check("Automatic Analysis on Tick-Based Tracks")] },
+    Section {
+        title: "Miscellaneous",
+        controls: &[
+            Control::Choice("Parallel Task Optimization", "High"),
+            Control::Check("Display Waveforms in Low Resolution While Loading"),
+            Control::Check("Noise Burst Peak Protection"),
+        ],
+    },
+];
+
+const MIDI: &[Section] = &[
+    Section {
+        title: "Basics",
+        controls: &[
+            Control::Check("Remove Range Selection after Opening a MIDI or Score Editor"),
+            Control::Check("Use MIDI to Tap Tempo"),
+            Control::Check("Display Events as Modified by Real-Time Properties"),
+            Control::Check("Use F11 for Wait for Note"),
+            Control::Check("Automatically Create Click Track in New Sessions"),
+            Control::Check("MIDI Input Monitoring Follows Selected Track"),
+            Control::Value("Pencil Resolution for Controller Data", "30 ms"),
+            Control::Value("Global MIDI Playback Offset", "0 samples"),
+            Control::Choice("Double-Clicking a MIDI Clip Opens", "Docked MIDI Editor"),
+            Control::Choice("MIDI Merge Release Mode", "Latch Until Next Event"),
+        ],
+    },
+    Section { title: "Note Display", controls: &[Control::Radios("Middle C", &["C3", "C4", "MIDI Note 60"])] },
+    Section {
+        title: "Delay Compensation for External Devices",
+        controls: &[Control::Check("MIDI Beat Clock"), Control::Check("MIDI Timecode"), Control::Check("MIDI Notes and Controllers")],
+    },
+    Section { title: "MIDI / Score Editor Display", controls: &[Control::Value("Additional Empty Bars in the Score Editor", "8")] },
+];
+
+const COLLABORATION: &[Section] = &[
+    Section {
+        title: "Track Collaboration",
+        controls: &[
+            Control::Check("New Tracks Are Shared"),
+            Control::Check("First Invite Shares All Tracks"),
+            Control::Check("Joining a Project Downloads All Shared Tracks"),
+            Control::Check("Enable Track Notes Dialog"),
+            Control::Check("Automatically Download New Shared Tracks"),
+        ],
+    },
+    Section {
+        title: "Cloud Storage Services",
+        controls: &[
+            Control::Check("Prevent Others from Overwriting an Open Session"),
+            Control::Choice("Lock File Extension", ".lock"),
+            Control::Check("Keep Unused Folders on Session Close"),
+        ],
+    },
+];
+
+const SYNCHRONIZATION: &[Section] = &[
+    Section {
+        title: "Machine Control",
+        controls: &[
+            Control::Check("Machine Chases Memory Location"),
+            Control::Check("Machine Follows Edit Insertion and Scrub"),
+            Control::Choice("Controlled Device Type", "Non-Linear Device"),
+            Control::Check("Machine Cues Intelligently"),
+            Control::Check("Stop at Shuttle Speed Zero"),
+            Control::Check("Non-Linear Transport Error Suppression"),
+        ],
+    },
+    Section {
+        title: "Remote Mode",
+        controls: &[
+            Control::Check("Ignore Track Arming"),
+            Control::Check("Set Servo Lock at Play"),
+            Control::Check("Allow External Track-Arm Commands in Local Mode"),
+            Control::Check("Allow External Transport Commands in Local Mode"),
+            Control::Value("Punch In Frame Offset", "0 frames"),
+            Control::Value("Punch Out Frame Offset", "0 frames"),
+            Control::Value("Delay after Play Command", "0 frames"),
+        ],
+    },
+    Section {
+        title: "Synchronization",
+        controls: &[
+            Control::Value("Minimum Sync Delay", "30 frames"),
+            Control::Value("Delay before Locking to Longitudinal Timecode", "0 frames"),
+            Control::Value("Delay before Locking to Serial Timecode", "0 frames"),
+        ],
+    },
+    Section {
+        title: "Linked Systems",
+        controls: &[
+            Control::Check("Transmit Solos"),
+            Control::Check("Receive Solos"),
+            Control::Check("Solo Independent of Linked State"),
+            Control::Check("Audio Engine Errors Stop All Linked Systems"),
+            Control::Check("Transmit Play Selections"),
+            Control::Check("Receive Play Selections"),
+        ],
+    },
+];
+
+enum Action {
+    Apply,
+    Cancel,
+}
+
+/// Draws the modal Preferences window and applies or discards its staged values.
+pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
+    let mut open = app.ui.show_preferences;
+    if !open {
+        app.preferences.discard_draft();
+        return;
+    }
+
+    let mut draft = app.preferences.draft.unwrap_or_else(|| PreferencesDraft::from_live(app));
+    draft.sync_unedited_from_live(app);
+    let draft_before_ui = draft;
+    let mut search = std::mem::take(&mut app.preferences.search);
+    let mut page = app.ui.preferences_page;
+    let mut action = None;
+    select_page_from_keyboard(ctx, &mut page);
+    // A single-line text edit releases focus on Enter in the frame that receives the key. Capture
+    // ownership before drawing so Enter in Search cannot also apply and close the window.
+    let typing = ctx.egui_wants_keyboard_input();
+    // egui closes a ComboBox before this window observes Escape, so preserve the previous frame's
+    // popup state to keep the same key from also canceling the complete Preferences window.
+    let popup_was_open = app.preferences.popup_open;
+
+    // A transparent middle-layer area makes this a modal window without borrowing the app while
+    // the window body is drawn. The Preferences window itself is on the foreground layer.
+    let screen = ctx.content_rect();
+    let window_height = preferences_window_height(screen.height());
+    let body_height = (PREFERENCES_BODY_HEIGHT - (PREFERENCES_WINDOW_HEIGHT - window_height)).max(160.0);
+    egui::Area::new(egui::Id::unique("preferences_modal_blocker")).order(egui::Order::Middle).fixed_pos(screen.min).show(ctx, |ui| {
+        ui.allocate_exact_size(screen.size(), egui::Sense::click_and_drag());
+    });
+
+    let mut window = egui::Window::new("SoundCraft Preferences")
+        .id(egui::Id::unique("preferences_window"))
+        .open(&mut open)
+        .order(egui::Order::Foreground)
+        .collapsible(false)
+        .resizable(false)
+        .fixed_size(vec2(PREFERENCES_WINDOW_WIDTH, window_height));
+    if let Some([x, y]) = app.ui.preferences_window_pos {
+        window = window.default_pos(egui::pos2(x, y));
+    }
+
+    let shown = window.show(ctx, |ui| {
+        ui.horizontal(|ui| {
+            let (icon_rect, _) = ui.allocate_exact_size(vec2(18.0, 18.0), egui::Sense::hover());
+            crate::icons::draw(ui.painter(), icon_rect.shrink(1.0), "zoom", Tokens::current().text_dim);
+            let search_label = ui.label("Search");
+            let edit = egui::TextEdit::singleline(&mut search)
+                .id(egui::Id::unique("preferences_search"))
+                .hint_text("Type to search preferences")
+                .desired_width(f32::INFINITY);
+            ui.add(edit).labelled_by(search_label.id);
+            if !search.is_empty() && ui.small_button("Clear").clicked() {
+                search.clear();
+            }
+        });
+        ui.add_space(4.0);
+        tabs(ui, &mut page, &search);
+        ui.separator();
+
+        // Reserve the same body height on every page. Only screens shorter than the normal window
+        // get an emergency scroll area, keeping the footer reachable without changing tall-screen
+        // layout or making the window resizable.
+        let body_top = ui.cursor().top();
+        if window_height < PREFERENCES_WINDOW_HEIGHT {
+            egui::ScrollArea::vertical()
+                .id_salt("preferences_body")
+                .max_height(body_height)
+                .min_scrolled_height(body_height)
+                .auto_shrink([false, false])
+                .show(ui, |ui| page_body(ui, page, &search, &mut draft));
+        } else {
+            page_body(ui, page, &search, &mut draft);
+        }
+        let used_height = ui.cursor().top() - body_top;
+        ui.add_space((body_height - used_height).max(0.0));
+
+        ui.separator();
+        ui.horizontal(|ui| {
+            ui.add_space(ui.available_width() - 176.0);
+            if ui.add_sized([80.0, 24.0], egui::Button::new("Cancel")).clicked() {
+                action = Some(Action::Cancel);
+            }
+            if ui.add_sized([80.0, 24.0], egui::Button::new("OK").fill(Tokens::current().accent_dark)).clicked() {
+                action = Some(Action::Apply);
+            }
+        });
+    });
+    if let Some(shown) = shown {
+        let rect = shown.response.rect;
+        if rect.min.x.is_finite() && rect.min.y.is_finite() {
+            app.ui.preferences_window_pos = Some([rect.min.x, rect.min.y]);
+        }
+    }
+    draft.remember_user_edits(draft_before_ui);
+    app.preferences.popup_open = ctx.any_popup_open();
+
+    if ctx.input(|i| i.key_pressed(Key::Escape)) && !popup_was_open {
+        action = Some(Action::Cancel);
+    } else if action.is_none() && ctx.input(|i| i.key_pressed(Key::Enter)) && !typing {
+        action = Some(Action::Apply);
+    }
+
+    if !open && action.is_none() {
+        action = Some(Action::Cancel);
+    }
+    match action {
+        Some(Action::Apply) => {
+            let _ = app.run("ui.theme", json!({"mode": draft.theme.id()}));
+            let _ = app.run("ui.reduce_visual_effects", json!({"mode": draft.reduce_visual_effects.id()}));
+            let _ = app.run("ui.undo_levels", json!({"levels": draft.undo_levels}));
+            open = false;
+            app.preferences.discard_draft();
+        }
+        Some(Action::Cancel) => {
+            open = false;
+            app.preferences.discard_draft();
+        }
+        None => {
+            app.preferences.draft = Some(draft);
+            app.preferences.search = search;
+        }
+    }
+    app.ui.preferences_page = page;
+    app.ui.show_preferences = open;
+}
+
+/// Caps the normal fixed window height so its frame and footer remain inside short screens.
+fn preferences_window_height(screen_height: f32) -> f32 {
+    (screen_height - PREFERENCES_SCREEN_MARGIN).clamp(PREFERENCES_MIN_HEIGHT, PREFERENCES_WINDOW_HEIGHT)
+}
+
+/// Draws the fixed-order page tabs and highlights pages containing a search match.
+fn tabs(ui: &mut egui::Ui, page: &mut PreferencesPage, search: &str) {
+    let t = Tokens::current();
+    egui::ScrollArea::horizontal().id_salt("preferences_tabs").show(ui, |ui| {
+        ui.horizontal(|ui| {
+            for candidate in PreferencesPage::ALL {
+                let hit = page_matches(candidate, search);
+                let text =
+                    if !search.is_empty() && hit { RichText::new(candidate.label()).color(t.meter_yellow) } else { RichText::new(candidate.label()) };
+                if ui.selectable_label(*page == candidate, text).clicked() {
+                    *page = candidate;
+                }
+            }
+        });
+    });
+}
+
+/// Draws one page using its standard two-column section layout.
+fn page_body(ui: &mut egui::Ui, page: PreferencesPage, search: &str, draft: &mut PreferencesDraft) {
+    if page == PreferencesPage::Mixing {
+        mixing_page_body(ui, search, draft);
+        return;
+    }
+
+    let sections = page.sections();
+    ui.columns(2, |columns| {
+        for (index, section) in sections.iter().enumerate() {
+            let column = index % 2;
+            section_ui(&mut columns[column], page, *section, search, draft);
+            columns[column].add_space(6.0);
+        }
+    });
+}
+
+/// Draws the Mixing page whose larger sections use explicit column splits.
+fn mixing_page_body(ui: &mut egui::Ui, search: &str, draft: &mut PreferencesDraft) {
+    ui.columns(2, |columns| {
+        section_ui(&mut columns[0], PreferencesPage::Mixing, MIXING[0], search, draft);
+        section_ui(&mut columns[1], PreferencesPage::Mixing, MIXING[1], search, draft);
+    });
+    ui.add_space(6.0);
+    section_ui_columns(ui, PreferencesPage::Mixing, MIXING[2], 11, search, draft);
+    ui.add_space(6.0);
+    section_ui_columns(ui, PreferencesPage::Mixing, MIXING[3], 1, search, draft);
+    ui.add_space(6.0);
+    section_ui_columns(ui, PreferencesPage::Mixing, MIXING[4], 5, search, draft);
+}
+
+/// Draws every control in one section inside its shared frame.
+fn section_ui(ui: &mut egui::Ui, page: PreferencesPage, section: Section, search: &str, draft: &mut PreferencesDraft) {
+    section_frame(ui, section, search, |ui| {
+        for (index, control) in section.controls.iter().copied().enumerate() {
+            control_ui(ui, page, section.title, index, control, search, draft);
+        }
+    });
+}
+
+/// Draws a section's title and border, including search-match highlighting.
+fn section_frame(ui: &mut egui::Ui, section: Section, search: &str, add_contents: impl FnOnce(&mut egui::Ui)) {
+    let t = Tokens::current();
+    let section_hit = text_matches(section.title, search);
+    let stroke = if !search.is_empty() && section_hit { Stroke::new(1.5, t.meter_yellow) } else { Stroke::new(1.0, t.border) };
+    egui::Frame::new().fill(t.panel_bg2).stroke(stroke).corner_radius(3.0).inner_margin(egui::Margin::same(6)).show(ui, |ui| {
+        ui.set_min_width((ui.available_width() - 12.0).max(280.0));
+        egui::Frame::new().fill(t.toolbar_group).inner_margin(egui::Margin::symmetric(6, 3)).show(ui, |ui| {
+            let title = if !search.is_empty() && section_hit {
+                RichText::new(section.title).strong().color(t.meter_yellow)
+            } else {
+                RichText::new(section.title).strong()
+            };
+            ui.label(title);
+        });
+        ui.add_space(3.0);
+        add_contents(ui);
+    });
+}
+
+/// Draws one long section in two columns at a stable catalog index.
+fn section_ui_columns(ui: &mut egui::Ui, page: PreferencesPage, section: Section, split_at: usize, search: &str, draft: &mut PreferencesDraft) {
+    section_frame(ui, section, search, |ui| {
+        let split_at = split_at.min(section.controls.len());
+        ui.columns(2, |columns| {
+            for (index, control) in section.controls[..split_at].iter().copied().enumerate() {
+                control_ui(&mut columns[0], page, section.title, index, control, search, draft);
+            }
+            for (offset, control) in section.controls[split_at..].iter().copied().enumerate() {
+                control_ui(&mut columns[1], page, section.title, split_at + offset, control, search, draft);
+            }
+        });
+    });
+}
+
+/// Draws an implemented control or a disabled placeholder for unavailable behavior.
+fn control_ui(ui: &mut egui::Ui, page: PreferencesPage, section: &str, index: usize, control: Control, search: &str, draft: &mut PreferencesDraft) {
+    let t = Tokens::current();
+    let hit = text_matches(&control.search_text(), search);
+    let label = |text: &'static str| {
+        if !search.is_empty() && hit { RichText::new(text).color(t.meter_yellow) } else { RichText::new(text) }
+    };
+    let id = ("preference", page, section, index);
+    match control {
+        Control::Theme => {
+            craft_ui::ComboBox::new(id, label("UI Theme")).selected_text(draft.theme.label()).show_ui(ui, |ui| {
+                for mode in ThemeMode::ALL {
+                    ui.selectable_value(&mut draft.theme, mode, mode.label());
+                }
+            });
+        }
+        Control::ReduceVisualEffects => {
+            craft_ui::ComboBox::new(id, label("Reduce Visual Effects")).selected_text(draft.reduce_visual_effects.label()).show_ui(ui, |ui| {
+                for mode in ReduceVisualEffects::ALL {
+                    ui.selectable_value(&mut draft.reduce_visual_effects, mode, mode.label());
+                }
+            });
+        }
+        Control::UndoLevels => {
+            ui.horizontal(|ui| {
+                ui.label(label("Levels of Undo"));
+                ui.add(egui::DragValue::new(&mut draft.undo_levels).range(1..=64));
+            });
+        }
+        Control::Check(text) => {
+            let mut value = false;
+            ui.add_enabled(false, egui::Checkbox::new(&mut value, label(text))).on_disabled_hover_text("This preference is not available yet.");
+        }
+        Control::Choice(text, value) | Control::ChoiceOptions(text, value, _) => {
+            ui.add_enabled_ui(false, |ui| {
+                craft_ui::ComboBox::new(id, label(text)).selected_text(value).show_ui(ui, |_| {});
+            })
+            .response
+            .on_disabled_hover_text("This preference is not available yet.");
+        }
+        Control::Value(text, value) => {
+            ui.add_enabled_ui(false, |ui| {
+                ui.horizontal(|ui| {
+                    let caption = ui.label(label(text));
+                    let mut shown = value.to_string();
+                    ui.add(egui::TextEdit::singleline(&mut shown).desired_width(96.0)).labelled_by(caption.id);
+                });
+            })
+            .response
+            .on_disabled_hover_text("This preference is not available yet.");
+        }
+        Control::Button(text) => {
+            ui.add_enabled(false, egui::Button::new(label(text))).on_disabled_hover_text("This preference is not available yet.");
+        }
+        Control::Radios(text, values) => {
+            ui.add_enabled_ui(false, |ui| {
+                ui.label(label(text));
+                let mut selected = 0usize;
+                ui.indent(id, |ui| {
+                    for (value_index, value) in values.iter().enumerate() {
+                        ui.radio_value(&mut selected, value_index, *value);
+                    }
+                });
+            })
+            .response
+            .on_disabled_hover_text("This preference is not available yet.");
+        }
+    }
+}
+
+/// Selects a page from the platform's primary-modifier plus number shortcuts.
+fn select_page_from_keyboard(ctx: &egui::Context, page: &mut PreferencesPage) {
+    let mac = ctx.os() == egui::os::OperatingSystem::Mac;
+    let keys = [Key::Num1, Key::Num2, Key::Num3, Key::Num4, Key::Num5, Key::Num6, Key::Num7, Key::Num8, Key::Num9];
+    let selected = ctx.input(|input| {
+        input.events.iter().find_map(|event| match event {
+            egui::Event::Key { key, pressed: true, repeat: false, modifiers, .. } if page_modifiers(*modifiers, mac) => {
+                keys.iter().position(|candidate| candidate == key)
+            }
+            _ => None,
+        })
+    });
+    if let Some(index) = selected.and_then(|index| PreferencesPage::ALL.get(index).copied()) {
+        *page = index;
+    }
+}
+
+/// Reports whether modifiers represent the platform-specific page shortcut chord.
+fn page_modifiers(modifiers: Modifiers, mac: bool) -> bool {
+    !modifiers.alt && !modifiers.shift && if mac { modifiers.command && !modifiers.ctrl } else { modifiers.command && modifiers.ctrl }
+}
+
+/// Reports whether a page title, section or control matches the search terms.
+fn page_matches(page: PreferencesPage, search: &str) -> bool {
+    text_matches(page.label(), search)
+        || page.sections().iter().any(|section| {
+            text_matches(section.title, search) || section.controls.iter().copied().any(|control| text_matches(&control.search_text(), search))
+        })
+}
+
+/// Performs a case-insensitive AND match over whitespace-separated search terms.
+fn text_matches(text: &str, search: &str) -> bool {
+    let search = search.trim();
+    if search.is_empty() {
+        return false;
+    }
+    let text = text.to_lowercase();
+    search.split_whitespace().all(|term| text.contains(&term.to_lowercase()))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use egui_kittest::Harness;
+    use egui_kittest::kittest::{NodeT, Queryable};
+    use soundcraft_engine::Engine;
+
+    /// Creates a complete application harness with Preferences already open.
+    fn preferences_harness(size: egui::Vec2) -> Harness<'static, SoundApp> {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        app.ui.show_preferences = true;
+        Harness::builder().with_size(size).build_ui_state(
+            |ui, app: &mut SoundApp| {
+                app.logic(&ui.ctx().clone());
+                app.ui(ui);
+            },
+            app,
+        )
+    }
+
+    /// Search includes page, section, control, choice and value text.
+    #[test]
+    fn search_matches_pages_sections_labels_and_values() {
+        assert!(page_matches(PreferencesPage::Display, "theme"));
+        assert!(page_matches(PreferencesPage::Editing, "zoom selection"));
+        assert!(page_matches(PreferencesPage::Metering, "K-20"));
+        assert!(page_matches(PreferencesPage::Synchronization, "linked systems"));
+        assert!(!page_matches(PreferencesPage::Midi, "waveform cache"));
+    }
+
+    /// Implemented preferences use commands without becoming document edits.
+    #[test]
+    fn applying_live_preferences_uses_ui_commands_without_dirtying_the_session() {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        assert_eq!(app.ui.theme, ThemeMode::Dark);
+        assert!(app.run("ui.theme", json!({"mode": ThemeMode::Light.id()})).is_ok());
+        assert!(app.run("ui.reduce_visual_effects", json!({"mode": "playback"})).is_ok());
+        assert!(app.run("ui.undo_levels", json!({"levels": 32})).is_ok());
+        assert_eq!(app.ui.theme, ThemeMode::Light);
+        assert_eq!(app.ui.reduce_visual_effects, ReduceVisualEffects::Playback);
+        assert_eq!((app.ui.undo_levels, app.engine.history_limit), (32, 32));
+        assert!(!app.engine.is_dirty());
+    }
+
+    /// An unchanged open draft follows newer scripted values instead of restoring stale ones.
+    #[test]
+    fn unchanged_draft_tracks_live_commands_before_apply() {
+        let mut harness = preferences_harness(egui::vec2(1280.0, 1000.0));
+        harness.run_steps(4);
+        {
+            let app = harness.state_mut();
+            assert!(app.run("ui.theme", json!({"mode": ThemeMode::Light.id()})).is_ok());
+            assert!(app.run("ui.reduce_visual_effects", json!({"mode": "always"})).is_ok());
+            assert!(app.run("ui.undo_levels", json!({"levels": 17})).is_ok());
+        }
+        harness.run_steps(2);
+        harness.get_by_label("OK").click();
+        harness.run_steps(2);
+
+        let app = harness.state();
+        assert_eq!(app.ui.theme, ThemeMode::Light);
+        assert_eq!(app.ui.reduce_visual_effects, ReduceVisualEffects::Always);
+        assert_eq!((app.ui.undo_levels, app.engine.history_limit), (17, 17));
+        assert!(!app.ui.show_preferences);
+    }
+
+    /// Fields edited in the window win over later live changes while untouched fields follow them.
+    #[test]
+    fn draft_sync_preserves_only_user_edited_fields() {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        let mut draft = PreferencesDraft::from_live(&app);
+        let before = draft;
+        draft.theme = ThemeMode::Light;
+        draft.remember_user_edits(before);
+
+        app.ui.theme = ThemeMode::System;
+        app.ui.reduce_visual_effects = ReduceVisualEffects::Always;
+        app.ui.undo_levels = 17;
+        draft.sync_unedited_from_live(&app);
+
+        assert_eq!(draft.theme, ThemeMode::Light);
+        assert_eq!(draft.reduce_visual_effects, ReduceVisualEffects::Always);
+        assert_eq!(draft.undo_levels, 17);
+    }
+
+    /// Command inputs reject unknown modes and bound undo depth safely.
+    #[test]
+    fn live_preferences_reject_or_clamp_hostile_values() {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        assert!(app.run("ui.reduce_visual_effects", json!({"mode": "sometimes"})).is_err());
+        assert_eq!(app.run("ui.undo_levels", json!({"levels": 10_000})).unwrap()["levels"], 64);
+    }
+
+    /// The Setup menu maps to the same scriptable command as every other entry point.
+    #[test]
+    fn setup_menu_opens_preferences_through_the_catalog_command() {
+        let aliases = crate::menus::ui_aliases();
+        assert_eq!(crate::menus::command_for_path("Setup > Preferences...", &aliases).as_deref(), Some("setup.preferences"));
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        assert_eq!(app.run("setup.preferences", json!({"value": true})), Ok(json!({"value": true})));
+        assert!(app.ui.show_preferences);
+    }
+
+    /// Closing by command cannot leak staged values into a later window instance.
+    #[test]
+    fn command_closes_discard_staged_preferences_before_a_reopen() {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        app.ui.show_preferences = true;
+        app.preferences.search = "theme".into();
+        let mut draft = PreferencesDraft::from_live(&app);
+        draft.theme = ThemeMode::Light;
+        draft.reduce_visual_effects = ReduceVisualEffects::Always;
+        draft.undo_levels = 1;
+        app.preferences.draft = Some(draft);
+
+        assert_eq!(app.run("setup.preferences", json!({})), Ok(json!({"value": false})));
+        assert!(app.preferences.draft.is_none());
+        assert!(app.preferences.search.is_empty());
+        assert_eq!(app.run("setup.preferences", json!({})), Ok(json!({"value": true})));
+
+        app.preferences.search = "undo".into();
+        app.preferences.draft = Some(draft);
+        assert!(app.run("window.close", json!({})).is_ok());
+        assert!(app.preferences.draft.is_none());
+        assert!(app.preferences.search.is_empty());
+    }
+
+    /// A broad scripted UI-state replacement also discards the closed window before another
+    /// command can reopen it, even when no UI frame runs between the two commands.
+    #[test]
+    fn ui_set_close_then_immediate_reopen_starts_without_the_canceled_draft() {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        app.ui.show_preferences = true;
+        app.preferences.search = "undo".into();
+        let mut draft = PreferencesDraft::from_live(&app);
+        draft.theme = ThemeMode::Light;
+        draft.undo_levels = 1;
+        app.preferences.draft = Some(draft);
+
+        assert!(app.run("ui.set", json!({"show_preferences": false})).is_ok());
+        assert_eq!(app.run("setup.preferences", json!({"value": true})), Ok(json!({"value": true})));
+
+        assert!(app.ui.show_preferences);
+        assert!(app.preferences.draft.is_none());
+        assert!(app.preferences.search.is_empty());
+        assert_eq!(app.ui.theme, ThemeMode::Dark);
+        assert_eq!(app.ui.undo_levels, 64);
+    }
+
+    /// Enter belongs to the focused search field and must not apply its window.
+    #[test]
+    fn enter_in_search_does_not_apply_or_close_preferences() {
+        let mut harness = preferences_harness(egui::vec2(1280.0, 1000.0));
+        harness.run_steps(4);
+        harness.ctx.memory_mut(|memory| memory.request_focus(egui::Id::unique("preferences_search")));
+        harness.run_steps(1);
+        assert!(harness.ctx.memory(|memory| memory.has_focus(egui::Id::unique("preferences_search"))));
+        let state = harness.state_mut();
+        state.preferences.draft.as_mut().unwrap().theme = ThemeMode::Light;
+
+        harness.key_press(Key::Enter);
+        harness.run_steps(2);
+
+        assert!(harness.state().ui.show_preferences);
+        assert_eq!(harness.state().ui.theme, ThemeMode::Dark);
+    }
+
+    /// Enter cannot replace a Cancel button action received in the same frame.
+    #[test]
+    fn enter_and_cancel_in_the_same_frame_discard_the_draft() {
+        let mut harness = preferences_harness(egui::vec2(1280.0, 1000.0));
+        harness.run_steps(4);
+        harness.state_mut().preferences.draft.as_mut().unwrap().theme = ThemeMode::Light;
+
+        harness.get_by_label("Cancel").click();
+        harness.key_press(Key::Enter);
+        harness.run_steps(2);
+
+        assert!(!harness.state().ui.show_preferences);
+        assert_eq!(harness.state().ui.theme, ThemeMode::Dark);
+    }
+
+    /// Escape closes an open choice popup before it can cancel the complete window.
+    #[test]
+    fn escape_closes_a_choice_popup_before_preferences() {
+        let mut harness = preferences_harness(egui::vec2(1280.0, 1000.0));
+        harness.run_steps(4);
+        harness.get_by_value("Dark").click();
+        harness.run_steps(2);
+        assert!(harness.ctx.any_popup_open());
+
+        harness.key_press(Key::Escape);
+        harness.run_steps(2);
+
+        assert!(!harness.ctx.any_popup_open());
+        assert!(harness.state().ui.show_preferences);
+    }
+
+    /// The adaptive fixed height keeps both footer buttons inside a short viewport.
+    #[test]
+    fn short_screens_keep_the_preferences_footer_reachable() {
+        assert_eq!(preferences_window_height(1000.0), PREFERENCES_WINDOW_HEIGHT);
+        assert_eq!(preferences_window_height(800.0), 776.0);
+
+        let mut harness = preferences_harness(egui::vec2(1280.0, 800.0));
+        harness.run_steps(4);
+        let ok = harness.get_by_label("OK").rect();
+        let cancel = harness.get_by_label("Cancel").rect();
+        assert!(ok.max.y <= 800.0, "OK button is outside the screen: {ok:?}");
+        assert!(cancel.max.y <= 800.0, "Cancel button is outside the screen: {cancel:?}");
+    }
+
+    /// The application menu invokes the same Preferences command as Setup.
+    #[test]
+    fn soundcraft_menu_opens_preferences_through_the_same_command() {
+        let app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        let mut harness = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_ui_state(
+            |ui, app: &mut SoundApp| {
+                app.logic(&ui.ctx().clone());
+                app.ui(ui);
+            },
+            app,
+        );
+        harness.run_steps(4);
+        harness.get_by_label("SoundCraft").click();
+        harness.run_steps(2);
+        harness.get_by_label("Preferences...").click();
+        harness.run_steps(2);
+        assert!(harness.state().ui.show_preferences);
+    }
+
+    /// The in-window application menu visibly disables Preferences during playback.
+    #[test]
+    fn soundcraft_menu_disables_preferences_during_playback() {
+        let mut app = SoundApp::new(Engine::default(), None, crate::Services::default());
+        app.engine.transport.playing = true;
+        let mut harness = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_ui_state(
+            |ui, app: &mut SoundApp| {
+                app.logic(&ui.ctx().clone());
+                app.ui(ui);
+            },
+            app,
+        );
+        harness.run_steps(4);
+        harness.get_by_label("SoundCraft").click();
+        harness.run_steps(2);
+        assert!(harness.get_by_label("Preferences...").accesskit_node().is_disabled());
+    }
+
+    /// Page declaration order remains identical to number-shortcut order.
+    #[test]
+    fn page_order_matches_the_platform_shortcuts() {
+        assert_eq!(PreferencesPage::ALL[0], PreferencesPage::Display);
+        assert_eq!(PreferencesPage::ALL[4], PreferencesPage::Metering);
+        assert_eq!(PreferencesPage::ALL[8], PreferencesPage::Synchronization);
+    }
+
+    /// An empty query does not mark every page and section as a hit.
+    #[test]
+    fn empty_search_does_not_highlight_everything() {
+        assert!(!text_matches("Display", ""));
+        assert!(!text_matches("Display", "   "));
+    }
+
+    /// Page shortcuts accept only the documented primary modifier for each platform.
+    #[test]
+    fn page_shortcuts_require_the_platform_primary_modifier_only() {
+        let command = Modifiers { command: true, ..Modifiers::NONE };
+        let off_mac_control = Modifiers { command: true, ctrl: true, ..Modifiers::NONE };
+        assert!(page_modifiers(command, true));
+        assert!(!page_modifiers(command, false));
+        assert!(page_modifiers(off_mac_control, false));
+        assert!(!page_modifiers(off_mac_control, true));
+    }
+}
diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs
--- a/crates/ui-egui/src/shortcuts.rs
+++ b/crates/ui-egui/src/shortcuts.rs
@@ -59,7 +59,7 @@ fn search_chord(m: Modifiers, mac: bool) -> bool {
 pub fn handle(app: &mut SoundApp, ctx: &egui::Context) {
     // Cmd+= and Cmd+0 are Mix/Edit and Quantize to Grid, not egui's built-in interface zoom.
     ctx.options_mut(|o| o.zoom_with_keyboard = false);
-    if ctx.egui_wants_keyboard_input() || app.dialogs.open.is_some() {
+    if ctx.egui_wants_keyboard_input() || app.dialogs.open.is_some() || app.ui.show_preferences {
         return;
     }
     // A scripted press is identified by its exact position in this pass's events, not by matching
diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs
--- a/crates/ui-egui/src/tests.rs
+++ b/crates/ui-egui/src/tests.rs
@@ -366,6 +366,8 @@ fn a_preset_changed_in_an_effect_editor_is_stored_once() {
 fn preferences_saved_before_the_window_was_remembered_keep_full_scale() {
     let ui: super::UiState = serde_json::from_value(json!({"show_transport": true})).unwrap();
     assert_eq!((ui.window_size, ui.window_maximized, ui.ui_scale), ([1600.0, 1000.0], false, 1.0));
+    assert_eq!(ui.reduce_visual_effects, crate::preferences::ReduceVisualEffects::Never);
+    assert_eq!(ui.undo_levels, 64);
 }
 
 #[test]
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -70,6 +70,8 @@ Legend: ✅ matches · 🟡 partial · ❌ missing.
 | Date | Change | Summary |
 |---|---|---|
 | 2026-10-25 | minor | Edit and Mix windows: Tracks and Groups and Clips in full-height dock columns. Readiness estimate unchanged |
+| 2026-10-20 | minor | Preferences uses one fixed non-scrolling size with compact stacked choices, removes the duplicate meter-type list, and command-backs visual-effects reduction and undo depth alongside the UI theme |
+| 2026-10-19 | minor | Preferences opens from Setup with nine searchable pages, compact grouped controls, staged apply/cancel, platform tab shortcuts and disabled placeholders for unavailable settings |
 | 2026-10-16 | minor | Track name menu: Scroll Into View brings the track to the top of the Edit window and the left of the Mix. Readiness estimate unchanged |
 | 2026-10-13 | minor | A plugin opens when it's inserted (#486) |
 | 2026-10-13 | minor | Drag across checkboxes (#399) |
