diff --git a/apps/wordcraft/src/main.rs b/apps/wordcraft/src/main.rs
--- a/apps/wordcraft/src/main.rs
+++ b/apps/wordcraft/src/main.rs
@@ -466,9 +466,10 @@ fn main() -> eframe::Result {
             app.set_startup_update_check(true);
             load_prefs(&mut app);
             load_user_dictionary(&mut app);
-            // A region chosen in File › Options replaces the system's for the first blank
-            // document too (#838).
-            if !sample && app.session.prefs.region.is_some() {
+            // The window's first document is made once the preferences are in: the person's
+            // defaults (styles, page setup) and, when chosen, their region's paper (#838) must
+            // both be in place.
+            if !sample {
                 let doc = app.session.blank_document();
                 app.session.set_document(doc);
             }
diff --git a/crates/doc/src/defaults.rs b/crates/doc/src/defaults.rs
new file mode 100644
--- /dev/null
+++ b/crates/doc/src/defaults.rs
@@ -0,0 +1,222 @@
+//! Defaults for new documents (File › Options › Defaults): the styles and page setup a person
+//! adopts from a document. They live in the app's preferences (`UiState::defaults`, saved in
+//! `ui.json`) and are applied by [`Document::apply_defaults`] when a blank document is made —
+//! never to a document being opened, which keeps its own styles and page.
+
+use serde::{Deserialize, Serialize};
+
+use crate::Document;
+use crate::props::Rgb;
+use crate::section::{MAX_PAGE_SIDE, MIN_PAGE_SIDE, SectionProps};
+use crate::styles::{Style, StyleSheet};
+
+/// The page setup adopted for new documents.
+#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct PageDefaults {
+    pub page_w: f32,
+    pub page_h: f32,
+    pub landscape: bool,
+    pub margin_top: f32,
+    pub margin_bottom: f32,
+    pub margin_left: f32,
+    pub margin_right: f32,
+    /// Mirror margins (a document-wide setting, like the page colour).
+    pub mirror: bool,
+    /// The sheet's own colour; `None` leaves the theme's paper.
+    pub page_color: Option<Rgb>,
+}
+
+impl Default for PageDefaults {
+    fn default() -> Self {
+        let s = SectionProps::default();
+        PageDefaults {
+            page_w: s.page_w,
+            page_h: s.page_h,
+            landscape: s.landscape,
+            margin_top: s.margin_top,
+            margin_bottom: s.margin_bottom,
+            margin_left: s.margin_left,
+            margin_right: s.margin_right,
+            mirror: false,
+            page_color: None,
+        }
+    }
+}
+
+impl PageDefaults {
+    /// The page setup a document's section has (its size, orientation and margins), with the
+    /// document's mirror margins and page colour.
+    pub fn from_section(s: &SectionProps, mirror: bool, page_color: Option<Rgb>) -> Self {
+        PageDefaults {
+            page_w: s.page_w,
+            page_h: s.page_h,
+            landscape: s.landscape,
+            margin_top: s.margin_top,
+            margin_bottom: s.margin_bottom,
+            margin_left: s.margin_left,
+            margin_right: s.margin_right,
+            mirror,
+            page_color,
+        }
+    }
+
+    /// Keep the values inside the page model's limits: a hand-edited or older `ui.json` must not
+    /// put an impossible page into every new document.
+    pub fn sanitize(&mut self) {
+        let side = |v: f32, fallback: f32| if v.is_finite() { v.clamp(MIN_PAGE_SIDE, MAX_PAGE_SIDE) } else { fallback };
+        let default = SectionProps::default();
+        self.page_w = side(self.page_w, default.page_w);
+        self.page_h = side(self.page_h, default.page_h);
+        self.margin_top = self.margin_top.clamp(0.0, self.page_h);
+        self.margin_bottom = self.margin_bottom.clamp(0.0, self.page_h);
+        self.margin_left = self.margin_left.clamp(0.0, self.page_w);
+        self.margin_right = self.margin_right.clamp(0.0, self.page_w);
+        // Margins that leave no text room are brought back to the app's margins.
+        if self.margin_left + self.margin_right > self.page_w - 18.0 || self.margin_top + self.margin_bottom > self.page_h - 18.0 {
+            let d = SectionProps::default();
+            self.margin_top = d.margin_top;
+            self.margin_bottom = d.margin_bottom;
+            self.margin_left = d.margin_left;
+            self.margin_right = d.margin_right;
+        }
+    }
+
+    fn apply(&self, doc: &mut Document) {
+        let mut p = *self;
+        p.sanitize();
+        doc.last_section.page_w = p.page_w;
+        doc.last_section.page_h = p.page_h;
+        doc.last_section.landscape = p.landscape;
+        doc.last_section.margin_top = p.margin_top;
+        doc.last_section.margin_bottom = p.margin_bottom;
+        doc.last_section.margin_left = p.margin_left;
+        doc.last_section.margin_right = p.margin_right;
+        // The header/footer distances stay on the page.
+        doc.last_section.header = doc.last_section.header.clamp(0.0, p.page_h);
+        doc.last_section.footer = doc.last_section.footer.clamp(0.0, p.page_h);
+        doc.settings.mirror_margins = p.mirror;
+        doc.settings.page_color = p.page_color;
+    }
+}
+
+/// Defaults for new documents: a whole style sheet (every style adopted), individual style
+/// definitions, and the page setup. Empty means the app's own built-in defaults.
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct DocumentDefaults {
+    /// A whole sheet adopted from a document (`adopt all styles`); replaces the built-in sheet.
+    pub sheet: Option<StyleSheet>,
+    /// Style definitions adopted one by one, merged over the sheet (or the built-in sheet).
+    pub styles: Vec<Style>,
+    /// The page setup for new documents; `None` keeps the app's regional paper.
+    pub page: Option<PageDefaults>,
+}
+
+impl DocumentDefaults {
+    pub fn is_empty(&self) -> bool {
+        self.sheet.is_none() && self.styles.is_empty() && self.page.is_none()
+    }
+
+    /// Keep the defaults inside sane limits (a hand-edited or older `ui.json`).
+    pub fn sanitize(&mut self) {
+        const MAX_STYLES: usize = 5000;
+        self.styles.retain(|s| !s.id.is_empty() && s.id.chars().count() <= 200);
+        self.styles.truncate(MAX_STYLES);
+        if let Some(sheet) = &mut self.sheet {
+            sheet.styles.truncate(MAX_STYLES);
+        }
+        if let Some(page) = &mut self.page {
+            page.sanitize();
+        }
+    }
+
+    /// The style sheet these defaults stand for: the adopted sheet (all styles), or the built-in
+    /// sheet with the adopted styles merged over it. The UI edits and previews this.
+    pub fn styles_sheet(&self) -> StyleSheet {
+        let mut sheet = self.sheet.clone().unwrap_or_default();
+        if self.sheet.is_none() {
+            for style in &self.styles {
+                match sheet.styles.iter_mut().find(|s| s.id == style.id) {
+                    Some(slot) => *slot = style.clone(),
+                    None => sheet.styles.push(style.clone()),
+                }
+            }
+        }
+        sheet
+    }
+
+    /// The ids this default defines, in order (the sheet's, or the adopted ones).
+    pub fn style_ids(&self) -> Vec<String> {
+        match &self.sheet {
+            Some(sheet) => sheet.styles.iter().map(|s| s.id.clone()).collect(),
+            None => self.styles.iter().map(|s| s.id.clone()).collect(),
+        }
+    }
+}
+
+impl Document {
+    /// Start this (new) document from `defaults`: the adopted styles and page setup. An empty
+    /// default leaves the document as it is. Existing documents are never passed here.
+    pub fn apply_defaults(&mut self, defaults: &DocumentDefaults) {
+        let mut d = defaults.clone();
+        d.sanitize();
+        if let Some(sheet) = d.sheet {
+            self.styles = sheet;
+        }
+        for style in d.styles {
+            match self.styles.styles.iter_mut().find(|s| s.id == style.id) {
+                Some(slot) => *slot = style,
+                None => self.styles.styles.push(style),
+            }
+        }
+        if let Some(page) = d.page {
+            page.apply(self);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn defaults_replace_styles_and_page_of_a_new_document() {
+        let mut d = Document::new();
+        let mut style = d.styles.styles.iter().find(|s| s.id == "Normal").cloned().unwrap();
+        style.chr.size = Some(15.0);
+        let mut page = PageDefaults::from_section(&d.last_section, false, Some(Rgb(0x18, 0x18, 0x18)));
+        page.margin_top = 40.0;
+        page.page_h = 600.0 * 72.0 / 2.54;
+        let defaults = DocumentDefaults { sheet: None, styles: vec![style], page: Some(page) };
+        d.apply_defaults(&defaults);
+        let normal = d.styles.styles.iter().find(|s| s.id == "Normal").unwrap();
+        assert_eq!(normal.chr.size, Some(15.0));
+        assert_eq!(d.last_section.margin_top, 40.0);
+        assert_eq!(d.last_section.page_h, page.page_h);
+        assert_eq!(d.settings.page_color, Some(Rgb(0x18, 0x18, 0x18)));
+        // Other styles keep the built-in sheet.
+        assert!(d.styles.styles.iter().any(|s| s.id == "Heading1"));
+    }
+
+    #[test]
+    fn an_empty_default_leaves_a_document_alone() {
+        let mut d = Document::new();
+        let before = d.clone();
+        d.apply_defaults(&DocumentDefaults::default());
+        assert_eq!(d.styles, before.styles);
+        assert_eq!(d.last_section, before.last_section);
+    }
+
+    #[test]
+    fn junk_defaults_are_brought_back_into_limits() {
+        let mut d = Document::new();
+        let page = PageDefaults { page_w: f32::NAN, page_h: 1e30, margin_left: -5.0, margin_right: 1e9, ..Default::default() };
+        let defaults = DocumentDefaults { sheet: None, styles: vec![], page: Some(page) };
+        d.apply_defaults(&defaults);
+        assert!(d.last_section.page_w.is_finite() && d.last_section.page_w >= MIN_PAGE_SIDE);
+        assert_eq!(d.last_section.page_h, MAX_PAGE_SIDE);
+        assert!(d.last_section.margin_left >= 0.0);
+        assert!(d.last_section.margin_left + d.last_section.margin_right < d.last_section.page_w);
+    }
+}
diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs
--- a/crates/doc/src/lib.rs
+++ b/crates/doc/src/lib.rs
@@ -19,6 +19,7 @@ pub mod cluster;
 pub mod collate;
 pub mod connector;
 pub mod control;
+pub mod defaults;
 pub mod edit;
 pub mod effects;
 pub mod encoding;
diff --git a/crates/doc/src/section.rs b/crates/doc/src/section.rs
--- a/crates/doc/src/section.rs
+++ b/crates/doc/src/section.rs
@@ -717,6 +717,15 @@ impl DocGrid {
     }
 }
 
+/// The smallest page side WordCraft accepts, points (0.1", as Word allows).
+pub const MIN_PAGE_SIDE: f32 = 7.2;
+
+/// The largest page side WordCraft accepts, points: 600 cm, LibreOffice's limit for a custom page
+/// size (Word itself stops at 22 in / 1584 pt). The page stays this size through layout, saving
+/// and export; only the PDF writer, capped by the PDF format at 200 in, and Word itself are
+/// narrower.
+pub const MAX_PAGE_SIDE: f32 = 17_008.0;
+
 /// Page setup and section-level settings.
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 #[serde(default, rename_all = "camelCase")]
diff --git a/crates/engine/src/cmd/defaults.rs b/crates/engine/src/cmd/defaults.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/src/cmd/defaults.rs
@@ -0,0 +1,446 @@
+//! Defaults for new documents (File › Options › Defaults): adopting the current document's styles
+//! or page setup as the starting point of every new document. The values are kept in the app's
+//! preferences (`UiState::defaults` in `ui.json`) and used by `Session::blank_document`; an
+//! existing document is never touched.
+
+use serde_json::{Value, json};
+use wordcraft_doc::defaults::PageDefaults;
+use wordcraft_doc::props::Rgb;
+use wordcraft_doc::section::SectionProps;
+
+use super::page;
+use crate::{CmdError, CmdResult, CommandSpec, Session, p};
+
+pub fn specs() -> Vec<CommandSpec> {
+    vec![
+        CommandSpec::new("defaults.state", "Defaults", "File › Options › Defaults", state)
+            .params(r#"{} → {"styles": {"all": bool, "ids": [string]}, "page": {…} | null}"#)
+            .pure(),
+        CommandSpec::new("defaults.saveStyles", "Set as Default", "File › Options › Defaults", save_styles)
+            .params(r#"{"what": "all" | [style id]}"#),
+        CommandSpec::new("defaults.savePage", "Set as Default", "File › Options › Defaults", save_page)
+            .params(r#"{}"#)
+            .pure(),
+        CommandSpec::new("defaults.savePageValues", "Set as Default", "File › Options › Defaults", save_page_values)
+            .params(r#"{"pageW"?: pt, "pageH"?: pt, "landscape"?: bool, "marginTop"?: pt, "marginBottom"?: pt, "marginLeft"?: pt, "marginRight"?: pt, "mirror"?: bool, "color"?: "RRGGBB" | null}"#),
+        CommandSpec::new("defaults.saveStyle", "Set as Default in the App", "File › Options › Defaults", save_style)
+            .params(r#"{"style": string, "name"?: string, "next"?: string, "chr"?: CharProps, "para"?: ParaProps, "apply"?: bool (also apply it to the open document, for the Modify Style dialog's button)}"#),
+        CommandSpec::new("defaults.resetStyle", "Reset", "File › Options › Defaults", reset_style).params(r#"{"style": string}"#),
+        CommandSpec::new("defaults.reset", "Reset", "File › Options › Defaults", reset)
+            .params(r#"{"what": "all" | "view" | "styles" | "page"}"#),
+    ]
+}
+
+/// What the defaults hold, as agents and the Options dialog read it: the adopted styles (all, or
+/// their ids) and the page setup.
+fn state(s: &mut Session, _: &Value) -> CmdResult {
+    Ok(describe(&s.defaults, &s.view))
+}
+
+fn describe(d: &wordcraft_doc::defaults::DocumentDefaults, view: &crate::ViewState) -> Value {
+    let page = d.page.map(|p| {
+        json!({
+            "pageW": p.page_w,
+            "pageH": p.page_h,
+            "landscape": p.landscape,
+            "marginTop": p.margin_top,
+            "marginBottom": p.margin_bottom,
+            "marginLeft": p.margin_left,
+            "marginRight": p.margin_right,
+            "mirror": p.mirror,
+            "color": p.page_color.map(|c| c.hex()),
+        })
+    });
+    json!({
+        "styles": {"all": d.sheet.is_some(), "ids": d.style_ids()},
+        "page": page,
+        "view": {"ruler": view.ruler, "gridlines": view.gridlines, "tableGridlines": view.table_gridlines, "marks": view.marks},
+    })
+}
+
+fn save_styles(s: &mut Session, v: &Value) -> CmdResult {
+    let what = v.get("what").ok_or_else(|| CmdError::Params("`what` (\"all\" or style ids) is required".into()))?;
+    match what {
+        Value::String(name) if name == "all" => {
+            // The whole sheet, as the document has it (built-in and custom styles).
+            s.defaults.sheet = Some(s.doc.styles.clone());
+            s.defaults.styles.clear();
+        }
+        Value::Array(ids) => {
+            if ids.is_empty() {
+                return Err(CmdError::Params("`what` lists no styles".into()));
+            }
+            if ids.len() > 500 {
+                return Err(CmdError::Params("`what` lists more than 500 styles".into()));
+            }
+            // Validate the whole list first: a bad id must not leave the earlier ones adopted.
+            let mut found = Vec::with_capacity(ids.len());
+            for id in ids {
+                let Some(id) = id.as_str() else {
+                    return Err(CmdError::Params("`what` must hold style ids".into()));
+                };
+                let Some(style) = s.doc.styles.styles.iter().find(|st| st.id == id).cloned() else {
+                    return Err(CmdError::Params(format!("unknown style `{id}`")));
+                };
+                found.push(style);
+            }
+            for style in found {
+                let id = style.id.clone();
+                // With a whole sheet adopted, the sheet is the source of truth: update it there,
+                // so the two lists never disagree.
+                if let Some(sheet) = &mut s.defaults.sheet {
+                    match sheet.styles.iter_mut().find(|st| st.id == id) {
+                        Some(slot) => *slot = style,
+                        None => sheet.styles.push(style),
+                    }
+                } else {
+                    match s.defaults.styles.iter_mut().find(|st| st.id == id) {
+                        Some(slot) => *slot = style,
+                        None => s.defaults.styles.push(style),
+                    }
+                }
+            }
+        }
+        _ => return Err(CmdError::Params("`what` must be \"all\" or a list of style ids".into())),
+    }
+    s.defaults.sanitize();
+    Ok(describe(&s.defaults, &s.view))
+}
+
+/// Adopt the current document's page setup: the caret section's size, orientation and margins,
+/// with the document's mirror margins and page colour.
+fn save_page(s: &mut Session, _: &Value) -> CmdResult {
+    let sect = page::sect(s);
+    s.defaults.page = Some(PageDefaults::from_section(&sect, s.doc.settings.mirror_margins, s.doc.settings.page_color));
+    s.defaults.sanitize();
+    Ok(describe(&s.defaults, &s.view))
+}
+
+/// Adopt page setup values that are not (yet) the document's: the Page Setup dialog's Set as
+/// Default, which must not change the open document.
+fn save_page_values(s: &mut Session, v: &Value) -> CmdResult {
+    // Start from the defaults already adopted (what the editor shows), not from the open
+    // document: fields the caller leaves out keep their defaults, not another document's page.
+    let mut sect = match s.defaults.page {
+        Some(p) => SectionProps {
+            page_w: p.page_w,
+            page_h: p.page_h,
+            landscape: p.landscape,
+            margin_top: p.margin_top,
+            margin_bottom: p.margin_bottom,
+            margin_left: p.margin_left,
+            margin_right: p.margin_right,
+            ..SectionProps::default()
+        },
+        None => page::sect(s),
+    };
+    for (key, slot) in [
+        ("pageW", &mut sect.page_w),
+        ("pageH", &mut sect.page_h),
+        ("marginTop", &mut sect.margin_top),
+        ("marginBottom", &mut sect.margin_bottom),
+        ("marginLeft", &mut sect.margin_left),
+        ("marginRight", &mut sect.margin_right),
+    ] {
+        if let Some(n) = v.get(key) {
+            if n.is_null() {
+                continue;
+            }
+            *slot = p::req_f32(v, key)?;
+        }
+    }
+    if let Some(land) = v.get("landscape").filter(|b| !b.is_null()) {
+        let Some(land) = land.as_bool() else { return Err(CmdError::Params("`landscape` must be true or false".into())) };
+        sect.landscape = land;
+    }
+    let mirror = match v.get("mirror") {
+        None | Some(Value::Null) => s.defaults.page.map(|p| p.mirror).unwrap_or(s.doc.settings.mirror_margins),
+        Some(Value::Bool(b)) => *b,
+        Some(_) => return Err(CmdError::Params("`mirror` must be true or false".into())),
+    };
+    let color = match v.get("color") {
+        None => s.defaults.page.map(|p| p.page_color).unwrap_or(s.doc.settings.page_color),
+        Some(Value::Null) => None,
+        Some(Value::String(hex)) => Some(Rgb::parse(hex).ok_or_else(|| CmdError::Params("`color` is not a colour (RRGGBB)".into()))?),
+        Some(_) => return Err(CmdError::Params("`color` must be a string or null".into())),
+    };
+    // The same rules as Layout › Page Setup: an impossible page is refused, not stored.
+    sect.margin_top = sect.margin_top.max(0.0);
+    sect.margin_bottom = sect.margin_bottom.max(0.0);
+    sect.margin_left = sect.margin_left.max(0.0);
+    sect.margin_right = sect.margin_right.max(0.0);
+    page::validate_section(&mut sect)?;
+    s.defaults.page = Some(PageDefaults::from_section(&sect, mirror, color));
+    s.defaults.sanitize();
+    Ok(describe(&s.defaults, &s.view))
+}
+
+/// Adopt the definition the Modify Style dialog shows for one style as the default for new
+/// documents. With `apply`, the same definition goes to the open document's style too (the
+/// dialog's "Set as Default in the App"), so the two never disagree.
+fn save_style(s: &mut Session, v: &Value) -> CmdResult {
+    let id = p::req_str(v, "style")?;
+    let chr: Option<wordcraft_doc::props::CharProps> =
+        v.get("chr").map(|c| serde_json::from_value(c.clone())).transpose().map_err(|e| CmdError::Params(e.to_string()))?;
+    let para: Option<wordcraft_doc::props::ParaProps> =
+        v.get("para").map(|c| serde_json::from_value(c.clone())).transpose().map_err(|e| CmdError::Params(e.to_string()))?;
+    let name = p::str(v, "name");
+    let next = p::str(v, "next");
+    let apply = p::bool(v, "apply").unwrap_or(false);
+
+    // The definition to adopt: the document's style when it has one (its `basedOn`, next style
+    // and kind ride along), else the current default, else the built-in one.
+    let base = s
+        .doc
+        .styles
+        .get(id)
+        .cloned()
+        .or_else(|| s.defaults.styles_sheet().get(id).cloned())
+        .ok_or_else(|| CmdError::Params(format!("unknown style `{id}`")))?;
+    let mut adopted = base.clone();
+    if let Some(c) = &chr {
+        adopted.chr.overlay(c);
+    }
+    if let Some(pp) = &para {
+        adopted.para.overlay(pp);
+    }
+    if let Some(n) = next {
+        adopted.next = Some(n.to_string());
+    }
+    if let Some(n) = name.filter(|n| !n.trim().is_empty()) {
+        adopted.name = n.to_string();
+    }
+    adopted.id = id.to_string();
+
+    // Apply to the open document first: it is the operation that can fail (an unknown style),
+    // and the defaults then describe exactly what the document has.
+    if apply {
+        super::para::overlay_style(s, id, chr.as_ref(), para.as_ref(), name, next)?;
+    }
+    store_default_style(&mut s.defaults, adopted);
+    s.defaults.sanitize();
+    Ok(describe(&s.defaults, &s.view))
+}
+
+/// Put `style` into the defaults: in the adopted sheet when there is one (it is the source of
+/// truth), otherwise among the individually adopted styles.
+fn store_default_style(d: &mut wordcraft_doc::defaults::DocumentDefaults, style: wordcraft_doc::styles::Style) {
+    if let Some(sheet) = &mut d.sheet {
+        match sheet.styles.iter_mut().find(|s| s.id == style.id) {
+            Some(slot) => *slot = style,
+            None => sheet.styles.push(style),
+        }
+    } else {
+        match d.styles.iter_mut().find(|s| s.id == style.id) {
+            Some(slot) => *slot = style,
+            None => d.styles.push(style),
+        }
+    }
+}
+
+/// Restore one style in the defaults to the application's own definition (a custom style is
+/// simply removed from the defaults).
+fn reset_style(s: &mut Session, v: &Value) -> CmdResult {
+    let id = p::req_str(v, "style")?;
+    let builtin = wordcraft_doc::styles::StyleSheet::builtin().get(id).cloned();
+    if let Some(sheet) = &mut s.defaults.sheet {
+        match builtin {
+            Some(b) => match sheet.styles.iter_mut().find(|s| s.id == id) {
+                Some(slot) => *slot = b,
+                None => sheet.styles.push(b),
+            },
+            None => sheet.styles.retain(|s| s.id != id),
+        }
+    } else {
+        s.defaults.styles.retain(|s| s.id != id);
+    }
+    Ok(describe(&s.defaults, &s.view))
+}
+
+fn reset(s: &mut Session, v: &Value) -> CmdResult {
+    let what = p::str(v, "what").unwrap_or("all");
+    match what {
+        "all" | "view" => {
+            let d = crate::ViewState::default();
+            s.view.ruler = d.ruler;
+            s.view.gridlines = d.gridlines;
+            s.view.table_gridlines = d.table_gridlines;
+            s.view.marks = d.marks;
+        }
+        "styles" | "page" => {}
+        _ => return Err(CmdError::Params("`what` must be all, view, styles or page".into())),
+    }
+    match what {
+        "all" => {
+            s.defaults.sheet = None;
+            s.defaults.styles.clear();
+            s.defaults.page = None;
+        }
+        "styles" => {
+            s.defaults.sheet = None;
+            s.defaults.styles.clear();
+        }
+        "page" => s.defaults.page = None,
+        _ => {}
+    }
+    Ok(describe(&s.defaults, &s.view))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn session() -> Session {
+        Session::new(wordcraft_doc::Document::from_text("one"))
+    }
+
+    #[test]
+    fn adopting_styles_and_page_changes_only_the_defaults() {
+        let mut s = session();
+        let before = s.doc.clone();
+        // One style, by id.
+        s.run("defaults.saveStyles", &json!({"what": ["Normal"]})).unwrap();
+        assert_eq!(s.defaults.style_ids(), ["Normal"]);
+        assert!(s.defaults.sheet.is_none());
+        // The whole sheet.
+        s.run("defaults.saveStyles", &json!({"what": "all"})).unwrap();
+        assert!(s.defaults.sheet.is_some());
+        assert_eq!(s.defaults.style_ids().len(), s.doc.styles.styles.len());
+        // The page setup, from the document.
+        s.run("defaults.savePage", &json!({})).unwrap();
+        assert!(s.defaults.page.is_some());
+        // None of it touched the open document.
+        assert_eq!(s.doc.styles, before.styles);
+        assert_eq!(s.doc.last_section, before.last_section);
+        // An unknown style is refused.
+        assert!(s.run("defaults.saveStyles", &json!({"what": ["NoSuchStyle"]})).is_err());
+    }
+
+    #[test]
+    fn page_values_from_the_dialog_are_checked() {
+        let mut s = session();
+        s.run(
+            "defaults.savePageValues",
+            &json!({"pageW": 600.0, "pageH": 800.0, "marginTop": 40.0, "marginBottom": 40.0, "marginLeft": 40.0, "marginRight": 40.0}),
+        )
+        .unwrap();
+        let p = s.defaults.page.expect("page defaults");
+        assert_eq!((p.page_w, p.page_h, p.margin_top), (600.0, 800.0, 40.0));
+        // Margins that leave no text room are refused, and the defaults stay as they were.
+        assert!(s.run("defaults.savePageValues", &json!({"marginLeft": 590.0, "marginRight": 590.0})).is_err());
+        assert_eq!(s.defaults.page.map(|p| p.margin_left), Some(40.0));
+        // A colour goes with the page setup.
+        s.run("defaults.savePageValues", &json!({"color": "181818"})).unwrap();
+        assert_eq!(s.defaults.page.unwrap().page_color, Some(Rgb(0x18, 0x18, 0x18)));
+    }
+
+    #[test]
+    fn a_new_document_starts_from_the_defaults() {
+        let mut s = session();
+        // Change the Normal style and the page, adopt both, then make a new document.
+        if let Some(normal) = s.doc.styles.styles.iter_mut().find(|st| st.id == "Normal") {
+            normal.chr.size = Some(16.0);
+        }
+        s.run("defaults.saveStyles", &json!({"what": ["Normal"]})).unwrap();
+        s.run(
+            "defaults.savePageValues",
+            &json!({"pageW": 500.0, "pageH": 700.0, "marginTop": 30.0, "marginBottom": 30.0, "marginLeft": 30.0, "marginRight": 30.0, "color": "181818"}),
+        )
+        .unwrap();
+        s.run("file.new", &json!({})).unwrap();
+        let normal = s.doc.styles.styles.iter().find(|st| st.id == "Normal").expect("Normal");
+        assert_eq!(normal.chr.size, Some(16.0), "the adopted style starts the new document");
+        assert_eq!((s.doc.last_section.page_w, s.doc.last_section.page_h), (500.0, 700.0));
+        assert_eq!(s.doc.settings.page_color, Some(Rgb(0x18, 0x18, 0x18)));
+        // A heading not adopted keeps the built-in definition.
+        let h1 = s.doc.styles.styles.iter().find(|st| st.id == "Heading1").expect("Heading 1");
+        let builtin = wordcraft_doc::Document::new();
+        assert_eq!(h1.chr.size, builtin.styles.styles.iter().find(|st| st.id == "Heading1").unwrap().chr.size);
+    }
+
+    #[test]
+    fn opening_a_document_keeps_its_own_styles_and_page() {
+        let dir = std::env::temp_dir().join(format!("wc-defaults-{}", std::process::id()));
+        std::fs::create_dir_all(&dir).unwrap();
+        let path = dir.join("own.docx");
+        // A document with its own Normal size and its own page.
+        let mut own = session();
+        if let Some(normal) = own.doc.styles.styles.iter_mut().find(|st| st.id == "Normal") {
+            normal.chr.size = Some(13.0);
+        }
+        own.doc.last_section.page_w = 612.0;
+        own.run("file.saveAs", &json!({"path": path.to_string_lossy()})).unwrap();
+        // The person now adopts different defaults.
+        let mut s = session();
+        s.run("defaults.saveStyles", &json!({"what": "all"})).unwrap();
+        s.run(
+            "defaults.savePageValues",
+            &json!({"pageW": 400.0, "pageH": 400.0, "marginTop": 13.0, "marginBottom": 13.0, "marginLeft": 13.0, "marginRight": 13.0}),
+        )
+        .unwrap();
+        s.run("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
+        let normal = s.doc.styles.styles.iter().find(|st| st.id == "Normal").expect("Normal");
+        assert_eq!(normal.chr.size, Some(13.0), "the file's own style, not the new defaults");
+        assert_eq!(s.doc.last_section.page_w, 612.0, "the file's own page");
+        let _ = std::fs::remove_dir_all(&dir);
+    }
+
+    /// The Modify Style dialog's definition for one style becomes the default: only that style
+    /// is adopted, and a new document starts with it (the others keep the built-in sheet).
+    #[test]
+    fn a_style_saved_from_the_dialog_is_the_new_default() {
+        let mut s = session();
+        let chr = json!({"font": "Georgia", "size": 15.0, "bold": false, "italic": false, "color": {"Rgb": [0x11, 0x22, 0x33]}});
+        s.run("defaults.saveStyle", &json!({"style": "Normal", "chr": chr, "para": {"spaceBefore": 6.0, "spaceAfter": 4.0}})).unwrap();
+        assert_eq!(s.defaults.style_ids(), ["Normal"], "only the edited style is adopted");
+        s.run("file.new", &json!({})).unwrap();
+        let normal = s.doc.styles.get("Normal").expect("Normal");
+        assert_eq!(normal.chr.font.as_deref(), Some("Georgia"));
+        assert_eq!(normal.chr.size, Some(15.0));
+        assert_eq!(normal.para.space_before, Some(6.0));
+        assert_eq!(normal.para.space_after, Some(4.0));
+        // Heading 1 keeps the application's own definition.
+        let builtin = wordcraft_doc::styles::StyleSheet::builtin();
+        assert_eq!(s.doc.styles.get("Heading1"), builtin.get("Heading1"));
+        // Restoring the style drops the adoption.
+        s.run("defaults.resetStyle", &json!({"style": "Normal"})).unwrap();
+        assert!(s.defaults.styles.is_empty());
+    }
+
+    /// The dialog's "Set as Default in the App" applies the definition to the open document and
+    /// adopts it for new documents, leaving the other styles alone.
+    #[test]
+    fn the_dialogs_button_applies_and_adopts_together() {
+        let mut s = session();
+        let other = s.doc.styles.get("Heading1").cloned();
+        s.run("defaults.saveStyle", &json!({"style": "Normal", "apply": true, "chr": {"font": "Georgia", "size": 15.0}})).unwrap();
+        assert_eq!(s.doc.styles.get("Normal").unwrap().chr.font.as_deref(), Some("Georgia"), "the document's style changed");
+        assert_eq!(s.doc.styles.get("Heading1").cloned(), other, "the other styles didn't");
+        assert_eq!(s.defaults.styles.len(), 1);
+        assert_eq!(s.defaults.styles[0].chr.size, Some(15.0));
+        // An unknown style is refused and nothing is adopted.
+        let before = s.defaults.clone();
+        assert!(s.run("defaults.saveStyle", &json!({"style": "NoSuchStyle", "apply": true})).is_err());
+        assert_eq!(s.defaults, before);
+        // A colour the model can't parse is refused the same way (the dialog only sends colours
+        // it managed to parse).
+        assert!(s.run("defaults.saveStyle", &json!({"style": "Normal", "chr": {"color": "not-a-colour"}})).is_err());
+        assert_eq!(s.defaults, before);
+    }
+
+    #[test]
+    fn reset_restores_the_app_defaults() {
+        let mut s = session();
+        s.view.ruler = false;
+        s.view.gridlines = true;
+        s.run("defaults.saveStyles", &json!({"what": "all"})).unwrap();
+        s.run("defaults.savePage", &json!({})).unwrap();
+        let d = s.run("defaults.reset", &json!({"what": "all"})).unwrap();
+        assert!(s.defaults.is_empty());
+        assert!(s.view.ruler && !s.view.gridlines && s.view.table_gridlines && !s.view.marks);
+        assert_eq!(d["view"]["ruler"], true);
+        // A fresh blank document again has the app's own styles and page.
+        let doc = s.blank_document();
+        assert_eq!(doc.styles, wordcraft_doc::Document::new().styles);
+    }
+}
diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs
--- a/crates/engine/src/cmd/mod.rs
+++ b/crates/engine/src/cmd/mod.rs
@@ -8,6 +8,7 @@ pub mod citations;
 pub mod column;
 pub mod connectors;
 pub mod controls;
+pub mod defaults;
 pub mod design;
 pub mod draw;
 pub mod edit;
@@ -76,6 +77,7 @@ pub fn registry() -> Registry {
     v.extend(insert::specs());
     v.extend(equation::specs());
     v.extend(page::specs());
+    v.extend(defaults::specs());
     v.extend(table::specs());
     v.extend(table_style::specs());
     v.extend(table_draw::specs());
diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs
--- a/crates/engine/src/cmd/options.rs
+++ b/crates/engine/src/cmd/options.rs
@@ -7,7 +7,7 @@ use serde_json::{Value, json};
 use crate::{CmdError, CmdResult, CommandSpec, Prefs, Session, p};
 
 /// Options panes, in the order the page lists them (the `pane` param of `file.options`).
-pub const PANES: [&str; 8] = ["general", "display", "proofing", "save", "language", "accessibility", "advanced", "agents"];
+pub const PANES: [&str; 9] = ["general", "display", "proofing", "save", "language", "accessibility", "advanced", "agents", "defaults"];
 
 /// Longest initials kept, in characters.
 pub const MAX_INITIALS_CHARS: usize = 9;
@@ -22,7 +22,7 @@ pub fn specs() -> Vec<CommandSpec> {
     vec![
         CommandSpec::new("file.options", "Options", "File", options)
             .params(
-                r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas|auto" (auto: the region's), "region"?: string (a locale tag like pl-PL, saved: the paper File › New starts on, the default units and the decimal separator; auto: the system's), "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "dictionariesFolder"?: string (a folder of Hunspell .aff/.dic files searched first, saved; auto: the system locations only), "userTemplatesFolder"?: string (the folder File › New lists personal templates from, saved; auto: the app's own Templates folder; not a network folder), "addWords"?: [string], "removeWords"?: [string]} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#,
+                r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents|defaults", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas|auto" (auto: the region's), "region"?: string (a locale tag like pl-PL, saved: the paper File › New starts on, the default units and the decimal separator; auto: the system's), "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "dictionariesFolder"?: string (a folder of Hunspell .aff/.dic files searched first, saved; auto: the system locations only), "userTemplatesFolder"?: string (the folder File › New lists personal templates from, saved; auto: the app's own Templates folder; not a network folder), "addWords"?: [string], "removeWords"?: [string]} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#,
             )
             .pure(),
         CommandSpec::new("text.overtype", "Overtype", "File › Options › Advanced", |s, v| {
diff --git a/crates/engine/src/cmd/page.rs b/crates/engine/src/cmd/page.rs
--- a/crates/engine/src/cmd/page.rs
+++ b/crates/engine/src/cmd/page.rs
@@ -408,7 +408,7 @@ pub fn columns_preset_at(s: &Session) -> Option<&'static str> {
 /// Checks a whole section handed to `layout.pageSetup` by the rules of Size, Margins, Columns,
 /// Line Numbers, Footnotes and Page Borders: a non-finite number or margins that leave no room
 /// for text is an error; the rest is clamped to the ranges those commands use.
-fn validate_section(x: &mut SectionProps) -> Result<(), CmdError> {
+pub(crate) fn validate_section(x: &mut SectionProps) -> Result<(), CmdError> {
     let bad = |what: &str| CmdError::Params(format!("section: {what} must be a finite number"));
     let finite = [x.page_w, x.page_h, x.margin_top, x.margin_bottom, x.margin_left, x.margin_right, x.header, x.footer, x.gutter, x.columns.space];
     if finite.iter().any(|n| !n.is_finite()) {
diff --git a/crates/engine/src/cmd/para.rs b/crates/engine/src/cmd/para.rs
--- a/crates/engine/src/cmd/para.rs
+++ b/crates/engine/src/cmd/para.rs
@@ -974,22 +974,37 @@ fn modify_style(s: &mut Session, v: &Value) -> CmdResult {
         v.get("para").map(|c| serde_json::from_value(c.clone())).transpose().map_err(|e| CmdError::Params(e.to_string()))?;
     let next = p::str(v, "next").map(str::to_string);
     let name = p::str(v, "name").map(str::to_string);
-    let st = s.doc.styles.get_mut(&id).ok_or_else(|| CmdError::Params("no such style".into()))?;
+    overlay_style(s, &id, chr.as_ref(), para.as_ref(), name.as_deref(), next.as_deref())?;
+    sel_result(s)
+}
+
+/// Apply the Modify Style dialog's properties to a style of the open document: an overlay, so
+/// properties the dialog doesn't show (list numbering, borders, the paragraph after…) stay.
+/// Shared with the defaults, which adopt the same definition for new documents.
+pub(crate) fn overlay_style(
+    s: &mut Session,
+    id: &str,
+    chr: Option<&CharProps>,
+    para: Option<&ParaProps>,
+    name: Option<&str>,
+    next: Option<&str>,
+) -> Result<(), CmdError> {
+    let st = s.doc.styles.get_mut(id).ok_or_else(|| CmdError::Params("no such style".into()))?;
     if let Some(c) = chr {
-        st.chr.overlay(&c);
+        st.chr.overlay(c);
     }
     if let Some(pp) = para {
-        st.para.overlay(&pp);
+        st.para.overlay(pp);
     }
     if let Some(n) = next {
-        st.next = Some(n);
+        st.next = Some(n.to_string());
     }
     if let Some(n) = name.filter(|n| !n.trim().is_empty()) {
-        st.name = n;
+        st.name = n.to_string();
     }
     // Every paragraph's layout depends on styles: bump all revisions.
     touch_all(s);
-    sel_result(s)
+    Ok(())
 }
 
 /// Update Style to Match Selection: the style takes on how the selection looks — its paragraph
diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs
--- a/crates/engine/src/session.rs
+++ b/crates/engine/src/session.rs
@@ -451,6 +451,9 @@ pub struct Session {
     pub doc: Document,
     pub sel: Selection,
     pub view: ViewState,
+    /// Defaults for new documents (File › Options › Defaults): the styles and page setup the
+    /// person adopted. Persisted by the host in `ui.json`; applied only to blank documents.
+    pub defaults: wordcraft_doc::defaults::DocumentDefaults,
     /// Formatting picked with a collapsed caret, applied to the next typed text.
     pub pending: Option<CharProps>,
     /// Last highlighter choice for the shortcut; palette state, outside document undo.
@@ -895,6 +898,7 @@ impl Session {
             doc,
             sel,
             view,
+            defaults: wordcraft_doc::defaults::DocumentDefaults::default(),
             pending,
             last_highlight: wordcraft_doc::props::Highlight::Yellow,
             path,
@@ -1607,8 +1611,12 @@ impl Session {
 
     /// A new blank document (the Normal template) on the region's paper: Letter in the US and
     /// other Letter regions, A4 elsewhere (#838). Margins stay 2.54 cm (1") all round.
+    /// A blank document for File › New (and the window's first document): the app's paper for the
+    /// region, then the person's defaults for new documents (styles, page setup, page colour).
     pub fn blank_document(&self) -> Document {
-        blank_document(&self.region())
+        let mut d = blank_document(&self.region());
+        d.apply_defaults(&self.defaults);
+        d
     }
 
     /// Replace the document (open/new).
@@ -1675,6 +1683,8 @@ impl Session {
             doc,
             sel,
             view: _,
+            // App preferences for future documents, not part of this one's edit state.
+            defaults: _,
             pending: _,
             last_highlight: _,
             path: _,
diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs
--- a/crates/engine/src/session/documents.rs
+++ b/crates/engine/src/session/documents.rs
@@ -196,6 +196,8 @@ impl Session {
             doc: _,
             sel: _,
             view: _,
+            // App preferences for future documents, shared by every open document.
+            defaults: _,
             pending: _,
             path: _,
             file_name: _,
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -21,6 +21,49 @@ fn text(s: &Session) -> String {
     s.doc.plain_text(StoryRef::Body)
 }
 
+/// File › Options › Defaults: the style and the page setup adopted for new documents are what a
+/// new document starts from, and the open document is never touched.
+#[test]
+fn adopted_defaults_start_every_new_document() {
+    let mut s = s();
+    run(&mut s, "defaults.saveStyle", json!({"style": "Normal", "chr": {"font": "Courier New", "size": 11.0}}));
+    run(&mut s, "defaults.savePageValues", json!({"pageW": 420.0, "pageH": 595.0, "marginTop": 42.0}));
+    let state = run(&mut s, "defaults.state", json!({}));
+    assert_eq!(state["styles"]["ids"], json!(["Normal"]));
+    assert_eq!(state["page"]["pageW"], json!(420.0));
+
+    let d = s.blank_document();
+    assert_eq!((d.last_section.page_w, d.last_section.page_h), (420.0, 595.0), "the adopted page");
+    assert_eq!(d.last_section.margin_top, 42.0, "the adopted margin");
+    let normal = d.styles.get("Normal").expect("Normal is a built-in");
+    assert_eq!((normal.chr.font.as_deref(), normal.chr.size), (Some("Courier New"), Some(11.0)), "the adopted style");
+    // A document already open keeps its own page and styles.
+    assert_ne!((s.doc.last_section.page_w, s.doc.last_section.page_h), (420.0, 595.0));
+
+    // Reset takes the app's own page back.
+    run(&mut s, "defaults.reset", json!({"what": "page"}));
+    let d = s.blank_document();
+    assert_ne!((d.last_section.page_w, d.last_section.page_h), (420.0, 595.0), "the app's paper again");
+}
+
+/// A `defaults.saveStyles` naming an unknown style changes nothing: the ids before the bad one
+/// are not adopted either, and a page update keeps the values it does not name.
+#[test]
+fn a_failed_save_styles_changes_nothing() {
+    let mut s = s();
+    run(&mut s, "defaults.savePageValues", json!({"pageW": 420.0, "pageH": 595.0, "marginTop": 30.0}));
+    let before = run(&mut s, "defaults.state", json!({}));
+    let r = s.run("defaults.saveStyles", &json!({"what": ["Normal", "NoSuchStyle"]}));
+    assert!(r.is_err(), "the unknown style is refused");
+    assert_eq!(run(&mut s, "defaults.state", json!({})), before, "nothing was adopted");
+
+    // A page update builds on what is already adopted (not on another document's page).
+    run(&mut s, "defaults.savePageValues", json!({"marginTop": 42.0}));
+    let state = run(&mut s, "defaults.state", json!({}));
+    assert_eq!(state["page"]["pageW"], json!(420.0), "the size it did not name");
+    assert_eq!(state["page"]["marginTop"], json!(42.0), "the margin it did name");
+}
+
 #[test]
 fn doc_extension_dispatches_to_docbin() {
     // Garbage bytes with a .doc name must produce an error through the Word 97-2003
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -1376,6 +1376,10 @@ impl Dialog {
             }
             "newStyle" => Dialog::NewStyle { name: "Style1".into(), based_on: "Normal".into(), back: false },
             "manageStyles" => Dialog::ManageStyles { alphabetical: false, selected: s("style") },
+            "modifyStyle" => {
+                let id = if s("style").is_empty() { "Normal".to_string() } else { s("style") };
+                return Dialog::modify_style(app, &id);
+            }
             "newTableStyle" | "modifyTableStyle" => crate::dialogs_table_style::open(app, name)?,
             "commands" => Dialog::Commands { query: String::new(), swallow: true },
             "icons" => Dialog::Icons { query: String::new(), category: String::new(), selected: String::new() },
@@ -1774,7 +1778,7 @@ fn run_as_one_step(app: &mut WordApp, commands: Vec<(&str, Value)>) -> Result<()
 /// Page Setup's Paper group (#1433): a Paper size list (the named sizes and Custom size) and
 /// the width and height. The fields follow the orientation (the wide side is the width in
 /// landscape), and a typed size that is wider than tall turns the orientation to landscape.
-fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &mut bool, custom: &mut bool) {
+pub(crate) fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &mut bool, custom: &mut bool) {
     if (*landscape && paper.0 < paper.1) || (!*landscape && paper.0 > paper.1) {
         *paper = (paper.1, paper.0);
     }
@@ -1817,6 +1821,64 @@ fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &mut bool, c
     });
 }
 
+/// The property fields the Modify Style dialog and the Defaults pane's style editor share: what
+/// a style says for itself, resolved for display (as the dialog has always shown it).
+pub(crate) struct StyleFields<'a> {
+    pub name: &'a mut String,
+    pub font: &'a mut String,
+    pub size: &'a mut f32,
+    pub bold: &'a mut bool,
+    pub italic: &'a mut bool,
+    pub color: &'a mut String,
+    pub before: &'a mut f32,
+    pub after: &'a mut f32,
+}
+
+/// The style's properties: name, font, size, colour, spacing and emphasis. Shared by Modify Style
+/// and File › Options › Defaults, so both edit a style in exactly the same terms. `name_editable`
+/// is false where renaming would break the `basedOn` links of the default styles.
+pub(crate) fn style_fields(app: &mut WordApp, ui: &mut Ui, f: StyleFields<'_>, name_editable: bool, display_name: Option<&str>) {
+    egui::Grid::new("style_fields").num_columns(2).show(ui, |ui| {
+        let name_label = ui.label(tl!("Name:"));
+        if name_editable {
+            ui.text_edit_singleline(f.name).labelled_by(name_label.id);
+        } else {
+            // A default style keeps its name: other styles are based on that id.
+            ui.label(egui::RichText::new(display_name.unwrap_or(f.name.as_str())).weak());
+        }
+        ui.end_row();
+        let font_label = ui.label(tl!("Font:"));
+        let shown = app.previews.font_shown(f.font);
+        let fams = app.previews.families();
+        let prev = app.previews.font_preview_fn();
+        if let Some(face) = crate::widgets::font_combo(ui, "ms_font", 220.0, &shown, &fams, Some(&*prev)) {
+            if face == crate::widgets::ADD_FONT {
+                let _ = app.run("font.add", json!({}));
+            } else {
+                *f.font = app.previews.font_stored(&face);
+            }
+        }
+        let _ = font_label;
+        ui.end_row();
+        let size_label = ui.label(tl!("Size:"));
+        ui.add(egui::DragValue::new(f.size).range(1.0..=1638.0).speed(0.5)).labelled_by(size_label.id);
+        ui.end_row();
+        let color_label = ui.label(tl!("Color:"));
+        ui.text_edit_singleline(f.color).labelled_by(color_label.id);
+        ui.end_row();
+        ui.label(tl!("Space before/after:"));
+        ui.horizontal(|ui| {
+            ui.add(egui::DragValue::new(f.before).range(0.0..=1584.0)).accessible_name(tl!("Space before"));
+            ui.add(egui::DragValue::new(f.after).range(0.0..=1584.0)).accessible_name(tl!("Space after"));
+        });
+        ui.end_row();
+    });
+    ui.horizontal(|ui| {
+        ui.checkbox(f.bold, tl!("Bold"));
+        ui.checkbox(f.italic, tl!("Italic"));
+    });
+}
+
 /// Cancel key for dialogs: Escape everywhere, plus ⌘. (Command+Period) on macOS.
 pub(crate) fn cancel_key_pressed(ui: &Ui) -> bool {
     ui.input(|i| i.key_pressed(egui::Key::Escape) || (cfg!(target_os = "macos") && i.modifiers.command && i.key_pressed(egui::Key::Period)))
@@ -1835,6 +1897,15 @@ pub(crate) fn ok_cancel_responses(ui: &mut Ui, draw_ok: impl FnOnce(&mut Ui) ->
     }
 }
 
+/// The `styles.modify`/`defaults.saveStyle` parameters the fields stand for.
+pub(crate) fn style_params(id: &str, name: &str, font: &str, size: f32, bold: bool, italic: bool, color: &str, before: f32, after: f32) -> Value {
+    let mut chr = json!({"font": font, "size": size, "bold": bold, "italic": italic});
+    if let Some(c) = wordcraft_doc::Rgb::parse(color) {
+        chr["color"] = json!({"Rgb": [c.0, c.1, c.2]});
+    }
+    json!({"style": id, "name": name, "chr": chr, "para": {"spaceBefore": before, "spaceAfter": after}, "apply": true})
+}
+
 pub(crate) fn buttons(ui: &mut Ui, ok: &str) -> (bool, bool) {
     let mut r = (false, false);
     ui.add_space(8.0);
@@ -2744,57 +2815,38 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
             ok || cancel
         }
         Dialog::ModifyStyle { back, resume, id, name, font, size, bold, italic, color, before, after } => {
-            egui::Grid::new("ms").num_columns(2).show(ui, |ui| {
-                let name_label = ui.label(tl!("Name:"));
-                ui.text_edit_singleline(name).labelled_by(name_label.id);
-                ui.end_row();
-                ui.label(tl!("Font:"));
-                let shown = app.previews.font_shown(font);
-                let fams = app.previews.families();
-                let prev = app.previews.font_preview_fn();
-                if let Some(f) = crate::widgets::font_combo(ui, "ms_font", 220.0, &shown, &fams, Some(&*prev)) {
-                    if f == crate::widgets::ADD_FONT {
-                        let _ = app.run("font.add", json!({}));
-                    } else {
-                        *font = app.previews.font_stored(&f);
+            style_fields(app, ui, StyleFields { name, font, size, bold, italic, color, before, after }, true, None);
+            // "Set as Default in the App": the definition shown becomes the default for new
+            // documents *and* goes to the open document's style, so the two agree. It is one
+            // engine command, so the document isn't changed if adopting the default fails.
+            let mut adopted = false;
+            ui.horizontal(|ui| {
+                if ui.button(tl!("Set as Default in the App")).clicked() {
+                    let params = style_params(id, name, font, *size, *bold, *italic, color, *before, *after);
+                    match app.run("defaults.saveStyle", params) {
+                        Ok(_) => {
+                            let shown = app.session.doc.styles.get(id).map(crate::i18n::style_name).unwrap_or_else(|| id.clone());
+                            app.status(format!("{shown} — {}", tl!("Saved")));
+                            adopted = true;
+                        }
+                        Err(e) => app.status(e),
                     }
                 }
-                ui.end_row();
-                let size_label = ui.label(tl!("Size:"));
-                ui.add(egui::DragValue::new(size).range(1.0..=1638.0).speed(0.5)).labelled_by(size_label.id);
-                ui.end_row();
-                let color_label = ui.label(tl!("Color:"));
-                ui.text_edit_singleline(color).labelled_by(color_label.id);
-                ui.end_row();
-                ui.label(tl!("Space before/after:"));
-                ui.horizontal(|ui| {
-                    ui.add(egui::DragValue::new(before).range(0.0..=1584.0)).accessible_name(tl!("Space before"));
-                    ui.add(egui::DragValue::new(after).range(0.0..=1584.0)).accessible_name(tl!("Space after"));
-                });
-                ui.end_row();
-            });
-            ui.horizontal(|ui| {
-                ui.checkbox(bold, tl!("Bold"));
-                ui.checkbox(italic, tl!("Italic"));
             });
             let (ok, cancel) = buttons(ui, tl!("OK"));
             if ok {
-                let mut chr = json!({"font": font, "size": *size, "bold": *bold, "italic": *italic});
-                if let Some(c) = wordcraft_doc::Rgb::parse(color) {
-                    chr["color"] = json!({"Rgb": [c.0, c.1, c.2]});
-                }
-                let _ =
-                    app.run("styles.modify", json!({"style": id, "name": name, "chr": chr, "para": {"spaceBefore": *before, "spaceAfter": *after}}));
+                let params = style_params(id, name, font, *size, *bold, *italic, color, *before, *after);
+                let _ = app.run("styles.modify", params);
             }
-            if (ok || cancel) && *back {
+            if (ok || cancel || adopted) && *back {
                 app.dialog = Some(Dialog::ManageStyles { alphabetical: false, selected: id.clone() });
             }
-            if (ok || cancel)
+            if (ok || cancel || adopted)
                 && let Some(r) = resume.take()
             {
                 app.dialog = Some(*r);
             }
-            ok || cancel
+            ok || cancel || adopted
         }
         Dialog::ManageStyles { alphabetical, selected } => manage_styles(app, ui, alphabetical, selected),
         Dialog::TableStyle { id, name, based_on, region, regions, basis, pen } => {
@@ -5296,6 +5348,65 @@ mod tests {
         )
     }
 
+    /// closes; the other styles and the undo stack of other edits are untouched.
+    #[test]
+    fn the_modify_style_dialog_adopts_the_style_for_new_documents() {
+        use egui_kittest::kittest::Queryable;
+        let mut h = app_harness(app_with("Test"));
+        let heading = h.state().session.doc.styles.get("Heading1").cloned();
+        h.state_mut().dialog = Some(Dialog::ModifyStyle {
+            back: false,
+            resume: None,
+            id: "Normal".into(),
+            name: "Normal".into(),
+            font: "Georgia".into(),
+            size: 15.0,
+            bold: false,
+            italic: false,
+            color: "112233".into(),
+            before: 4.0,
+            after: 3.0,
+        });
+        h.run_steps(3);
+        h.get_by_label("Set as Default in the App").click();
+        h.run_steps(3);
+        assert!(h.state().dialog.is_none(), "the dialog closes when both writes succeeded");
+        let normal = h.state().session.doc.styles.get("Normal").expect("Normal");
+        assert_eq!(normal.chr.font.as_deref(), Some("Georgia"), "the document's style changed");
+        assert_eq!(normal.chr.size, Some(15.0));
+        assert_eq!(normal.para.space_before, Some(4.0));
+        assert_eq!(h.state().session.defaults.style_ids(), ["Normal"], "only this style was adopted");
+        assert_eq!(h.state().session.defaults.styles[0].chr.size, Some(15.0));
+        assert_eq!(h.state().session.doc.styles.get("Heading1").cloned(), heading, "other styles unchanged");
+    }
+
+    /// Cancel in the Modify Style dialog leaves both the document and the defaults alone.
+    #[test]
+    fn cancelling_modify_style_saves_no_default() {
+        use egui_kittest::kittest::Queryable;
+        let mut h = app_harness(app_with("Test"));
+        let normal = h.state().session.doc.styles.get("Normal").cloned();
+        h.state_mut().dialog = Some(Dialog::ModifyStyle {
+            back: false,
+            resume: None,
+            id: "Normal".into(),
+            name: "Normal".into(),
+            font: "Georgia".into(),
+            size: 15.0,
+            bold: true,
+            italic: false,
+            color: "auto".into(),
+            before: 1.0,
+            after: 1.0,
+        });
+        h.run_steps(3);
+        h.get_by_label("Cancel").click();
+        h.run_steps(3);
+        assert!(h.state().dialog.is_none());
+        assert_eq!(h.state().session.doc.styles.get("Normal").cloned(), normal, "no pending edit applied");
+        assert!(h.state().session.defaults.is_empty(), "and nothing adopted");
+    }
+
     /// The Page Setup paper size as opened, in points, and the Paper size entry it shows.
     fn opened_paper(app: &mut WordApp) -> ((f32, f32), Option<&'static str>) {
         let Some(Dialog::PageSetup { paper, .. }) = Dialog::open("pageSetup", app) else { panic!("no page setup dialog") };
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
@@ -314,6 +314,10 @@ pub struct UiState {
     /// Headings whose sub-headings the Navigation pane hides (#1997); per session, not saved.
     #[serde(skip)]
     pub nav_collapsed: std::collections::HashSet<wordcraft_doc::Path>,
+    /// Defaults for new documents (File › Options › Defaults): styles and page setup. The session
+    /// owns them while running (see [`WordApp::prefs`]); they are never applied to an open
+    /// document.
+    pub defaults: wordcraft_doc::defaults::DocumentDefaults,
     pub show_discord: bool,
     /// User name (File › Options) for comments and tracked changes; empty keeps the default.
     pub author: String,
@@ -437,6 +441,7 @@ impl Default for UiState {
             pane_places: Default::default(),
             pane_order: Vec::new(),
             nav_collapsed: Default::default(),
+            defaults: Default::default(),
             show_discord: true,
             author: String::new(),
             window: None,
@@ -958,6 +963,7 @@ impl WordApp {
         ui.read_aloud_skip_citations = self.session.read_aloud.skip_citations;
         ui.dark_page = self.session.view.dark_mode;
         ui.nav_pane = self.session.view.nav_pane;
+        ui.defaults = self.session.defaults.clone();
         ui.styles_pane_dock = self.session.view.styles_pane_dock;
         ui.check_spelling = self.session.view.proofing;
         ui.keyboard = self.session.keymap.clone();
@@ -1007,6 +1013,10 @@ impl WordApp {
         self.session.read_aloud.skip_citations = self.ui.read_aloud_skip_citations;
         self.session.view.dark_mode = self.ui.dark_page;
         self.session.view.nav_pane = self.ui.nav_pane;
+        // Defaults for new documents: cleaned up, then owned by the session.
+        let mut defaults = std::mem::take(&mut self.ui.defaults);
+        defaults.sanitize();
+        self.session.defaults = defaults;
         self.session.view.styles_pane_dock = self.ui.styles_pane_dock;
         self.session.view.proofing = self.ui.check_spelling;
         // Keys for commands this version doesn't have are dropped.
diff --git a/crates/ui-egui/src/options.rs b/crates/ui-egui/src/options.rs
--- a/crates/ui-egui/src/options.rs
+++ b/crates/ui-egui/src/options.rs
@@ -130,6 +130,7 @@ fn pane_label(id: &str) -> &'static str {
         "accessibility" => "Accessibility",
         "advanced" => "Advanced",
         "agents" => "Agents",
+        "defaults" => "Defaults",
         _ => "General",
     }
 }
@@ -175,6 +176,7 @@ pub fn page(app: &mut WordApp, ui: &mut Ui) {
                 "accessibility" => accessibility(app, ui),
                 "advanced" => advanced(app, ui),
                 "agents" => agents(ui),
+                "defaults" => defaults(app, ui),
                 _ => general(app, ui),
             }
         });
@@ -642,6 +644,462 @@ fn templates_folder(app: &mut WordApp, ui: &mut Ui) {
     note(ui, "File › New lists the .dotx and .dotm templates here under Personal; Save as Word Template starts here.");
 }
 
+/// A "Saved" note under an action for a moment after it ran (the dialog's own feedback that the
+/// defaults were written, without a new message that would have to be translated everywhere).
+fn flash(ui: &mut Ui, action: &str) {
+    // The time is read before the data lock: `Context::input` takes the context's own lock, and
+    // calling it inside `data_mut` deadlocks.
+    let now = ui.input(|i| i.time);
+    ui.data_mut(|d| d.insert_temp(egui::Id::unique(("defaults_saved", action)), now));
+}
+
+fn flashed(ui: &Ui, action: &str) -> bool {
+    let now = ui.input(|i| i.time);
+    ui.data(|d| d.get_temp::<f64>(egui::Id::unique(("defaults_saved", action)))).is_some_and(|t| now - t < 2.0)
+}
+
+fn saved_note(ui: &mut Ui, action: &str) {
+    if flashed(ui, action) {
+        ui.label(RichText::new(tl!("Saved")).small().weak());
+        ui.ctx().request_repaint();
+    }
+}
+
+/// A Reset button that asks once (OK/Cancel) before throwing the person's defaults away: a stray
+/// click on a small button shouldn't lose them.
+fn confirm_reset(ui: &mut Ui, key: &str) -> bool {
+    let id = egui::Id::unique(("defaults_reset", key));
+    if !ui.data(|d| d.get_temp::<bool>(id)).unwrap_or(false) {
+        if ui.button(tl!("Reset")).clicked() {
+            ui.data_mut(|d| d.insert_temp(id, true));
+        }
+        return false;
+    }
+    let mut go = false;
+    ui.label(tl!("Reset"));
+    if ui.button(tl!("OK")).clicked() {
+        go = true;
+    }
+    if ui.button(tl!("Cancel")).clicked() {
+        ui.data_mut(|d| d.insert_temp(id, false));
+    }
+    if go {
+        ui.data_mut(|d| d.insert_temp(id, false));
+    }
+    go
+}
+
+/// File › Options › Defaults: what a new document starts from. The view switches are the person's
+/// preferences (saved with the app); the styles and the page setup are adopted from a document —
+/// or reset — and only ever applied to documents created afterwards.
+/// The style being edited in the Defaults pane: the resolved properties the Modify Style dialog
+/// shows, kept apart from any document.
+#[derive(Clone)]
+struct StyleDraft {
+    /// Style id (the model's key; `basedOn` links use it).
+    id: String,
+    /// The style's own name (not the localized label): what is saved.
+    name: String,
+    /// The label shown in the pane (localized for built-in styles).
+    display: String,
+    font: String,
+    size: f32,
+    bold: bool,
+    italic: bool,
+    color: String,
+    before: f32,
+    after: f32,
+}
+
+/// The page setup being edited in the Defaults pane, in the measurement unit the dialogs show.
+#[derive(Clone, Debug)]
+struct PageDraft {
+    paper: (f32, f32),
+    custom: bool,
+    landscape: bool,
+    top: f32,
+    bottom: f32,
+    left: f32,
+    right: f32,
+    mirror: bool,
+    /// `RRGGBB`, or empty for the theme's paper.
+    color: String,
+}
+
+fn style_draft_id() -> egui::Id {
+    egui::Id::unique("defaults_style_draft")
+}
+
+fn page_draft_id() -> egui::Id {
+    egui::Id::unique("defaults_page_draft")
+}
+
+impl PageDraft {
+    /// The app's own page setup, for the editor's first show and a reset.
+    fn default_page() -> Self {
+        page_draft_from(&wordcraft_doc::SectionProps::default(), false, None)
+    }
+}
+
+fn color_text(value: &str) -> wordcraft_doc::props::TextColor {
+    match wordcraft_doc::Rgb::parse(value) {
+        Some(c) => wordcraft_doc::props::TextColor::Rgb(c),
+        None => wordcraft_doc::props::TextColor::Auto,
+    }
+}
+
+/// A style's properties as the Modify Style dialog shows them: resolved through its `basedOn`
+/// chain, so an edited Normal doesn't hide what Heading 1 inherits.
+fn draft_from_style(sheet: &wordcraft_doc::styles::StyleSheet, id: &str) -> Option<StyleDraft> {
+    let st = sheet.get(id)?;
+    let character = st.kind == wordcraft_doc::StyleKind::Character;
+    let rc = sheet.resolve_char(
+        if character { None } else { Some(id) },
+        &wordcraft_doc::CharProps { style: character.then(|| id.to_string()), ..Default::default() },
+    );
+    let rp = sheet.resolve_para(&wordcraft_doc::ParaProps { style: Some(id.to_string()), ..Default::default() });
+    Some(StyleDraft {
+        id: id.to_string(),
+        name: st.name.clone(),
+        display: crate::i18n::style_name(st),
+        font: rc.font,
+        size: rc.size,
+        bold: rc.bold,
+        italic: rc.italic,
+        color: match rc.color {
+            wordcraft_doc::props::TextColor::Rgb(c) => c.hex(),
+            wordcraft_doc::props::TextColor::Auto => "auto".into(),
+        },
+        before: rp.space_before,
+        after: rp.space_after,
+    })
+}
+
+/// The style the draft describes, for the sample: the default's own definition with the edited
+/// properties laid over it (the preview never touches the document).
+fn draft_style(sheet: &wordcraft_doc::styles::StyleSheet, draft: &StyleDraft) -> wordcraft_doc::styles::Style {
+    let mut st = sheet.get(&draft.id).cloned().unwrap_or_default();
+    st.id = draft.id.clone();
+    st.name = draft.name.clone();
+    st.chr.font = Some(draft.font.clone());
+    st.chr.size = Some(draft.size);
+    st.chr.bold = Some(draft.bold);
+    st.chr.italic = Some(draft.italic);
+    st.chr.color = Some(color_text(&draft.color));
+    st.para.space_before = Some(draft.before);
+    st.para.space_after = Some(draft.after);
+    st
+}
+
+fn page_draft_from_defaults(p: &wordcraft_doc::defaults::PageDefaults) -> PageDraft {
+    page_draft_from(
+        &wordcraft_doc::SectionProps {
+            page_w: p.page_w,
+            page_h: p.page_h,
+            landscape: p.landscape,
+            margin_top: p.margin_top,
+            margin_bottom: p.margin_bottom,
+            margin_left: p.margin_left,
+            margin_right: p.margin_right,
+            ..Default::default()
+        },
+        p.mirror,
+        p.page_color,
+    )
+}
+
+fn page_draft_from(sect: &wordcraft_doc::SectionProps, mirror: bool, color: Option<wordcraft_doc::Rgb>) -> PageDraft {
+    let ppu = crate::options::unit().pt_per_unit();
+    let paper = (sect.page_w / ppu, sect.page_h / ppu);
+    PageDraft {
+        paper,
+        custom: true,
+        landscape: sect.landscape,
+        top: sect.margin_top / ppu,
+        bottom: sect.margin_bottom / ppu,
+        left: sect.margin_left / ppu,
+        right: sect.margin_right / ppu,
+        mirror,
+        color: color.map(|c| c.hex()).unwrap_or_default(),
+    }
+}
+
+/// File › Options › Defaults: what a new document starts from, edited right here — the view
+/// switches, the default styles (each one shown and editable like the Modify Style dialog) and
+/// the default page setup. Only the adopted defaults change: the open document is never touched.
+fn defaults(app: &mut WordApp, ui: &mut Ui) {
+    section(ui, "Defaults");
+
+    // A. View preferences: the same switches as View › Show, saved with the app's preferences.
+    ui.label(RichText::new(tl!("View")).font(semibold(13.5)));
+    let mut ruler = app.session.view.ruler;
+    let mut gridlines = app.session.view.gridlines;
+    let mut table_gridlines = app.session.view.table_gridlines;
+    let mut marks = app.session.view.marks;
+    ui.horizontal_wrapped(|ui| {
+        if ui.checkbox(&mut ruler, tl!("Ruler")).changed() {
+            let _ = app.run("view.ruler", json!({"value": ruler}));
+        }
+        if ui.checkbox(&mut gridlines, tl!("Gridlines")).changed() {
+            let _ = app.run("view.gridlines", json!({"value": gridlines}));
+        }
+        if ui.checkbox(&mut table_gridlines, tl!("View Gridlines")).changed() {
+            let _ = app.run("table.viewGridlines", json!({"value": table_gridlines}));
+        }
+        if ui.checkbox(&mut marks, tl!("Show/Hide ¶")).changed() {
+            let _ = app.run("view.marks", json!({"value": marks}));
+        }
+        if confirm_reset(ui, "view") {
+            let _ = app.run("defaults.reset", json!({"what": "view"}));
+        }
+    });
+
+    // B. Default styles: pick one, edit it, see it, adopt it — without a document as the source.
+    ui.add_space(12.0);
+    ui.label(RichText::new(tl!("Styles")).font(semibold(13.5)));
+    let sheet = app.session.defaults.styles_sheet();
+    let ids: Vec<String> = sheet
+        .styles
+        .iter()
+        .filter(|s| !s.hidden && matches!(s.kind, wordcraft_doc::StyleKind::Paragraph | wordcraft_doc::StyleKind::Character))
+        .map(|s| s.id.clone())
+        .collect();
+    if ids.is_empty() {
+        return;
+    }
+    let Some(mut draft) = ui
+        .data(|d| d.get_temp::<StyleDraft>(style_draft_id()))
+        .filter(|d| ids.contains(&d.id))
+        .or_else(|| draft_from_style(&sheet, "Normal").or_else(|| draft_from_style(&sheet, &ids[0])))
+    else {
+        return;
+    };
+    ui.horizontal(|ui| {
+        let style_label = ui.label(tl!("Style:"));
+        let combo = egui::ComboBox::from_id_salt("defaults_style_pick").selected_text(&draft.display).width(240.0).show_ui(ui, |ui| {
+            for id in &ids {
+                let label = sheet.get(id).map(crate::i18n::style_name).unwrap_or_else(|| id.clone());
+                if ui.selectable_label(draft.id == *id, label.clone()).clicked()
+                    && draft.id != *id
+                    && let Some(next) = draft_from_style(&sheet, id)
+                {
+                    draft = next;
+                }
+            }
+        });
+        combo.response.labelled_by(style_label.id);
+    });
+    let display = draft.display.clone();
+    crate::dialogs::style_fields(
+        app,
+        ui,
+        crate::dialogs::StyleFields {
+            name: &mut draft.name,
+            font: &mut draft.font,
+            size: &mut draft.size,
+            bold: &mut draft.bold,
+            italic: &mut draft.italic,
+            color: &mut draft.color,
+            before: &mut draft.before,
+            after: &mut draft.after,
+        },
+        false,
+        Some(&display),
+    );
+    // The sample: drawn on a temporary document with the defaults' styles (never the open one).
+    let sample = draft_style(&sheet, &draft);
+    let ppp = ui.ctx().pixels_per_point();
+    let key = format!(
+        "defaults-style:{}:{}:{}:{}:{}:{}:{}:{}:{ppp}",
+        draft.id, draft.font, draft.size, draft.bold, draft.italic, draft.color, draft.before, draft.after
+    );
+    let text = tl!("AaBbCcDd").to_string();
+    let tex = app.previews.get_or(ui.ctx(), &key, || crate::previews::style_sample(&sheet, &sample, &text, 260.0, 40.0, ppp));
+    match tex {
+        Some(h) => {
+            let t = Tokens::get(ui.ctx());
+            let sz = h.size_vec2() / ppp;
+            let (r, _) = ui.allocate_exact_size(vec2(268.0, 34.0), egui::Sense::hover());
+            ui.painter().rect_filled(r, 3.0, if t.dark { egui::Color32::from_gray(0xC8) } else { egui::Color32::WHITE });
+            ui.painter().rect_stroke(r, 3.0, egui::Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
+            let inner = egui::Rect::from_min_size(r.min + vec2(4.0, 2.0), sz.min(r.size() - vec2(8.0, 4.0)));
+            ui.painter().image(h.id(), inner, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), egui::Color32::WHITE);
+        }
+        None => {
+            ui.ctx().request_repaint();
+        }
+    }
+    ui.horizontal(|ui| {
+        if ui.button(tl!("Set as Default")).clicked() {
+            let params = style_params_for_draft(&draft, false);
+            if app.run("defaults.saveStyle", params).is_ok() {
+                // Show what was stored (the engine validates and cleans the definition).
+                if let Some(saved) = draft_from_style(&app.session.defaults.styles_sheet(), &draft.id) {
+                    draft = saved;
+                }
+                flash(ui, "style");
+            }
+        }
+        if ui.button(tl!("Import from Document")).clicked() {
+            // Fill the editor from the open document's definition, without adopting it.
+            if let Some(next) = draft_from_style(&app.session.doc.styles, &draft.id) {
+                draft = next;
+            }
+        }
+        if confirm_reset(ui, "style") {
+            let _ = app.run("defaults.resetStyle", json!({"style": draft.id}));
+            if let Some(next) = draft_from_style(&app.session.defaults.styles_sheet(), &draft.id) {
+                draft = next;
+            }
+        }
+        ui.label(tl!("All"));
+        if confirm_reset(ui, "styles_all") {
+            let _ = app.run("defaults.reset", json!({"what": "styles"}));
+            if let Some(next) = draft_from_style(&app.session.defaults.styles_sheet(), &draft.id) {
+                draft = next;
+            }
+        }
+        saved_note(ui, "style");
+    });
+    ui.data_mut(|d| d.insert_temp(style_draft_id(), draft));
+
+    // C. Default page setup: edited here, adopted here; nothing comes from a document unless the
+    // person asks for it (Import).
+    ui.add_space(12.0);
+    ui.label(RichText::new(tl!("Page Setup")).font(semibold(13.5)));
+    let mut page = ui.data(|d| d.get_temp::<PageDraft>(page_draft_id())).unwrap_or_else(|| match app.session.defaults.page {
+        Some(p) => page_draft_from_defaults(&p),
+        None => PageDraft::default_page(),
+    });
+    crate::dialogs::page_paper_group(ui, &mut page.paper, &mut page.landscape, &mut page.custom);
+    ui.horizontal(|ui| {
+        ui.radio_value(&mut page.landscape, false, tl!("Portrait"));
+        ui.radio_value(&mut page.landscape, true, tl!("Landscape"));
+    });
+    egui::Grid::new("defaults_page").num_columns(4).show(ui, |ui| {
+        let ppu = crate::options::unit().pt_per_unit();
+        let unit = crate::options::unit();
+        let speed = 3.6 / ppu;
+        ui.label(tl!("Top:"));
+        ui.add(egui::DragValue::new(&mut page.top).speed(speed).measure(unit).max_decimals(2));
+        ui.label(tl!("Bottom:"));
+        ui.add(egui::DragValue::new(&mut page.bottom).speed(speed).measure(unit).max_decimals(2));
+        ui.end_row();
+        ui.label(if page.mirror { tl!("Inside:") } else { tl!("Left:") });
+        ui.add(egui::DragValue::new(&mut page.left).speed(speed).measure(unit).max_decimals(2));
+        ui.label(if page.mirror { tl!("Outside:") } else { tl!("Right:") });
+        ui.add(egui::DragValue::new(&mut page.right).speed(speed).measure(unit).max_decimals(2));
+        ui.end_row();
+    });
+    ui.checkbox(&mut page.mirror, tl!("Mirror margins"));
+    ui.horizontal(|ui| {
+        ui.label(tl!("Page Color"));
+        let theme = app.session.doc.settings.theme_colors.clone();
+        crate::dialogs::color_field(ui, &theme, &mut page.color, "No Color", "");
+        // Swatch of the chosen sheet colour.
+        let t = Tokens::get(ui.ctx());
+        let (r, _) = ui.allocate_exact_size(vec2(28.0, 18.0), egui::Sense::hover());
+        match wordcraft_doc::Rgb::parse(&page.color) {
+            Some(c) => {
+                ui.painter().rect_filled(r, 2.0, crate::theme::c32(c));
+            }
+            None => {
+                ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE);
+                ui.painter().line_segment([r.left_bottom(), r.right_top()], egui::Stroke::new(1.0, t.text_dim));
+            }
+        }
+        ui.painter().rect_stroke(r, 2.0, egui::Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
+    });
+    page_preview(ui, &page);
+    ui.horizontal(|ui| {
+        if ui.button(tl!("Set as Default")).clicked() {
+            let params = page_params(&page);
+            if app.run("defaults.savePageValues", params).is_ok() {
+                // The controls show what was stored: a value the page model limits (600 cm, a
+                // margin that leaves no room) can't stay on screen as if it had been adopted.
+                if let Some(saved) = app.session.defaults.page {
+                    page = page_draft_from_defaults(&saved);
+                }
+                flash(ui, "page");
+            }
+        }
+        if ui.button(tl!("Import from Document")).clicked() {
+            let sect = wordcraft_engine::cmd::page::sect(&app.session);
+            page = page_draft_from(&sect, app.session.doc.settings.mirror_margins, app.session.doc.settings.page_color);
+        }
+        if confirm_reset(ui, "page") {
+            let _ = app.run("defaults.reset", json!({"what": "page"}));
+            page = page_draft_from(&wordcraft_doc::SectionProps::default(), false, None);
+        }
+        saved_note(ui, "page");
+    });
+    ui.data_mut(|d| d.insert_temp(page_draft_id(), page));
+
+    // All the defaults at once (view preferences included), after a confirmation.
+    ui.add_space(16.0);
+    ui.horizontal(|ui| {
+        ui.label(tl!("All"));
+        if confirm_reset(ui, "all") {
+            let _ = app.run("defaults.reset", json!({"what": "all"}));
+            ui.data_mut(|d| d.remove::<StyleDraft>(style_draft_id()));
+            ui.data_mut(|d| d.remove::<PageDraft>(page_draft_id()));
+        }
+        saved_note(ui, "all");
+    });
+}
+
+/// The `defaults.saveStyle` parameters for the pane's draft (`apply` false: the open document is
+/// not changed by editing the defaults here).
+fn style_params_for_draft(draft: &StyleDraft, apply: bool) -> serde_json::Value {
+    let mut chr = json!({"font": draft.font, "size": draft.size, "bold": draft.bold, "italic": draft.italic});
+    if let Some(c) = wordcraft_doc::Rgb::parse(&draft.color) {
+        chr["color"] = json!({"Rgb": [c.0, c.1, c.2]});
+    }
+    json!({
+        "style": draft.id,
+        "name": draft.name,
+        "chr": chr,
+        "para": {"spaceBefore": draft.before, "spaceAfter": draft.after},
+        "apply": apply,
+    })
+}
+
+/// The `defaults.savePageValues` parameters for the pane's draft (points, as the engine takes).
+fn page_params(page: &PageDraft) -> serde_json::Value {
+    let ppu = crate::options::unit().pt_per_unit();
+    json!({
+        "pageW": page.paper.0 * ppu,
+        "pageH": page.paper.1 * ppu,
+        "landscape": page.landscape,
+        "marginTop": page.top * ppu,
+        "marginBottom": page.bottom * ppu,
+        "marginLeft": page.left * ppu,
+        "marginRight": page.right * ppu,
+        "mirror": page.mirror,
+        "color": wordcraft_doc::Rgb::parse(&page.color).map(|c| c.hex()),
+    })
+}
+
+/// A small, proportional view of the sheet: orientation, margins and colour, scaled to fit.
+fn page_preview(ui: &mut Ui, page: &PageDraft) {
+    let t = Tokens::get(ui.ctx());
+    let ppu = crate::options::unit().pt_per_unit();
+    let (w, h) = (page.paper.0 * ppu, page.paper.1 * ppu);
+    let box_size = vec2(200.0, 150.0);
+    let k = (box_size.x / w.max(1.0)).min(box_size.y / h.max(1.0)).min(1.0);
+    let size = vec2((w * k).max(8.0), (h * k).max(8.0));
+    let (r, _) = ui.allocate_exact_size(size, egui::Sense::hover());
+    let paper = wordcraft_doc::Rgb::parse(&page.color).map(crate::theme::c32).unwrap_or(egui::Color32::WHITE);
+    ui.painter().rect_filled(r, 1.0, paper);
+    ui.painter().rect_stroke(r, 1.0, egui::Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
+    let m = |v: f32| (v * ppu * k).max(0.0);
+    let inner = egui::Rect::from_min_max(
+        egui::pos2(r.min.x + m(page.left), r.min.y + m(page.top)),
+        egui::pos2((r.max.x - m(page.right)).max(r.min.x + 1.0), (r.max.y - m(page.bottom)).max(r.min.y + 1.0)),
+    );
+    ui.painter().rect_stroke(inner, 0.0, egui::Stroke::new(1.0, t.text_disabled), egui::StrokeKind::Inside);
+}
+
 fn agents(ui: &mut Ui) {
     section(ui, "Agents");
     ui.label(tl!("Every command is available to scripts and AI agents: run `wordcraft-cli mcp` for an MCP server, or start the app with `--control <port>` for the JSON control channel."));
@@ -859,7 +1317,7 @@ mod tests {
     use serde_json::json;
     use wordcraft_geom::Unit;
 
-    use super::{decimal, length, parse_length, unit};
+    use super::{PageDraft, StyleDraft, decimal, draft_from_style, length, page_draft_id, parse_length, style_draft_id, unit};
 
     use crate::theme::Appearance;
     use crate::{Services, UiState, WordApp};
@@ -880,6 +1338,111 @@ mod tests {
         WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default())
     }
 
+    fn defaults_harness() -> egui_kittest::Harness<'static, WordApp> {
+        let mut a = app();
+        a.run("file.options", json!({"pane": "defaults"})).unwrap();
+        let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
+            |ui, app: &mut WordApp| {
+                let ctx = ui.ctx().clone();
+                app.logic(&ctx);
+                app.ui(ui);
+            },
+            a,
+        );
+        h.run_steps(4);
+        h
+    }
+
+    /// The Defaults pane edits the default style itself: its "Save" adopts the style shown (the
+    /// open document is untouched), and the pane's draft starts from the app's built-in sheet.
+    #[test]
+    fn the_defaults_pane_edits_and_adopts_a_style() {
+        let mut h = defaults_harness();
+        let doc_style = h.state().session.doc.styles.get("Normal").cloned();
+        // The editor was filled from the built-in default (not from the document's accident).
+        let draft = h.ctx.data(|d| d.get_temp::<StyleDraft>(style_draft_id())).expect("a style draft");
+        assert_eq!(draft.id, "Normal");
+        let builtin = draft_from_style(&wordcraft_doc::styles::StyleSheet::builtin(), "Normal").expect("built-in Normal");
+        assert_eq!((draft.font.clone(), draft.size, draft.color.clone()), (builtin.font, builtin.size, builtin.color));
+        // The styles section's "Set as Default" adopts it.
+        h.query_all_by_label("Set as Default").next().expect("styles Set as Default").click();
+        h.run_steps(3);
+        assert_eq!(h.state().session.defaults.style_ids(), ["Normal"]);
+        assert_eq!(h.state().session.doc.styles.get("Normal").cloned(), doc_style, "the open document is untouched");
+    }
+
+    /// "Import from Document" fills the editor with the document's style, without adopting it.
+    #[test]
+    fn importing_a_style_fills_the_editor_without_adopting_it() {
+        let mut h = defaults_harness();
+        h.state_mut().run("styles.modify", json!({"style": "Normal", "chr": {"font": "Georgia", "size": 15.0}})).unwrap();
+        h.run_steps(3);
+        h.query_all_by_label("Import from Document").next().expect("Import").click();
+        h.run_steps(3);
+        let draft = h.ctx.data(|d| d.get_temp::<StyleDraft>(style_draft_id())).expect("a style draft");
+        assert_eq!(draft.font, "Georgia", "the editor shows the document's definition");
+        assert_eq!(draft.size, 15.0);
+        assert!(h.state().session.defaults.is_empty(), "importing alone adopts nothing");
+    }
+
+    /// The page section adopts the values in its own controls, with no document involved.
+    #[test]
+    fn the_defaults_pane_adopts_the_page_setup_it_shows() {
+        let mut h = defaults_harness();
+        let (w0, m0) = (h.state().session.doc.last_section.page_w, h.state().session.doc.last_section.margin_top);
+        h.ctx.data_mut(|d| {
+            let page = d.get_temp_mut_or_insert_with(page_draft_id(), PageDraft::default_page);
+            page.paper = (8.27, 11.69);
+            page.top = 1.0;
+            page.bottom = 1.0;
+            page.left = 1.0;
+            page.right = 1.0;
+            page.color = "181818".into();
+        });
+        h.run_steps(2);
+        // The page section's "Set as Default" is the second one in the pane.
+        h.query_all_by_label("Set as Default").nth(1).expect("page Set as Default").click();
+        h.run_steps(3);
+        let page = h.state().session.defaults.page.expect("page defaults");
+        let ppu = crate::options::unit().pt_per_unit();
+        assert!((page.page_w - 8.27 * ppu).abs() < 0.1 && (page.page_h - 11.69 * ppu).abs() < 0.1, "{page:?}");
+        assert!((page.margin_top - 1.0 * ppu).abs() < 0.1);
+        // The pane shows what was stored, so the fields can't claim a value the model limited.
+        let shown = h.ctx.data(|d| d.get_temp::<PageDraft>(page_draft_id())).expect("the page draft");
+        assert!((shown.paper.0 - 8.27).abs() < 0.02 && (shown.paper.1 - 11.69).abs() < 0.02, "{shown:?}");
+        assert_eq!(page.page_color, Some(wordcraft_doc::Rgb(0x18, 0x18, 0x18)));
+        assert_eq!(
+            (h.state().session.doc.last_section.page_w, h.state().session.doc.last_section.margin_top),
+            (w0, m0),
+            "the open document is untouched"
+        );
+    }
+
+    /// "Import from Document" in the page section fills the editor with the open document's page
+    /// setup, and adopting it is a separate, explicit step.
+    #[test]
+    fn importing_the_page_does_not_adopt_it() {
+        let mut h = defaults_harness();
+        h.state_mut()
+            .run(
+                "defaults.savePageValues",
+                json!({"pageW": 500.0, "pageH": 700.0, "marginTop": 30.0, "marginBottom": 30.0, "marginLeft": 30.0, "marginRight": 30.0}),
+            )
+            .unwrap();
+        // The document itself has another page setup.
+        h.state_mut().run("layout.size", json!({"width": 400.0, "height": 600.0})).unwrap();
+        h.run_steps(3);
+        // Import the page (the second "Import from Document" in the pane).
+        h.query_all_by_label("Import from Document").nth(1).expect("page Import").click();
+        h.run_steps(3);
+        let ppu = crate::options::unit().pt_per_unit();
+        let draft = h.ctx.data(|d| d.get_temp::<PageDraft>(page_draft_id())).expect("the page draft");
+        assert!((draft.paper.0 * ppu - 400.0).abs() < 0.5 && (draft.paper.1 * ppu - 600.0).abs() < 0.5, "{draft:?}");
+        // The defaults still hold what was adopted before the import, not the document's page.
+        let page = h.state().session.defaults.page.expect("page defaults");
+        assert!((page.page_w - 500.0).abs() < 0.1 && (page.page_h - 700.0).abs() < 0.1, "{page:?}");
+    }
+
     fn nav_harness(language: &str, percent: u16) -> egui_kittest::Harness<'static, WordApp> {
         let mut a = app();
         a.run("ui.language", json!({"value": language})).unwrap();
diff --git a/crates/ui-egui/src/previews.rs b/crates/ui-egui/src/previews.rs
--- a/crates/ui-egui/src/previews.rs
+++ b/crates/ui-egui/src/previews.rs
@@ -423,6 +423,29 @@ pub fn style_snippet(base: &Document, para: Paragraph, w: f32, h: f32, ppp: f32,
     snippet_blocks(base, vec![Block::Para(para)], w, h, ppp, margin, None, dark)
 }
 
+/// A sample of `style` as the defaults would make it: the style drawn on a temporary document
+/// with `sheet`'s styles (never the open document), for the Defaults pane's preview.
+pub fn style_sample(
+    sheet: &wordcraft_doc::styles::StyleSheet,
+    style: &wordcraft_doc::styles::Style,
+    text: &str,
+    w: f32,
+    h: f32,
+    ppp: f32,
+) -> Option<egui::ColorImage> {
+    let mut base = Document::new();
+    base.styles = sheet.clone();
+    match base.styles.styles.iter_mut().find(|s| s.id == style.id) {
+        Some(slot) => *slot = style.clone(),
+        None => base.styles.styles.push(style.clone()),
+    }
+    let mut p = Paragraph::with_text(text, CharProps::default()).styled(&style.id);
+    p.props.space_before = Some(0.0);
+    p.props.space_after = Some(0.0);
+    p.props.align = Some(wordcraft_doc::Align::Left);
+    style_snippet(&base, p, w, h, ppp, 2.0, false)
+}
+
 /// How a gallery draws a page preview: as rendered on a light interface or on Switch Modes' dark
 /// page, and dimmed in the dark theme so a white page isn't the brightest thing on screen (#663).
 pub fn preview_tint(t: &Tokens, dark_page: bool) -> egui::Color32 {
diff --git a/crates/ui-egui/src/window_ui.rs b/crates/ui-egui/src/window_ui.rs
--- a/crates/ui-egui/src/window_ui.rs
+++ b/crates/ui-egui/src/window_ui.rs
@@ -275,6 +275,7 @@ impl WordApp {
             last_caption_label: _,
             ink_pens: _,
             initials: _,
+            defaults: _,
             options_pane: _,
             recent_count: _,
             mini_toolbar: _,
