diff --git a/crates/formats/src/odt.rs b/crates/formats/src/odt.rs
--- a/crates/formats/src/odt.rs
+++ b/crates/formats/src/odt.rs
@@ -841,7 +841,7 @@ fn columns_xml(c: &Columns) -> String {
 
 /// One master page per entry of `pages` (name, setup), each with its own page layout `pm1`, `pm2`…
 /// (§16.5, §16.9); the first is the default one, "Standard".
-fn styles_xml(pages: &[(String, wordcraft_doc::SectionProps)], body_font: &str) -> String {
+fn styles_xml(pages: &[(String, wordcraft_doc::SectionProps)], body_font: &str, page_color: Option<Rgb>) -> String {
     let mut s = format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<office:document-styles {NS}><office:styles>");
     s.push_str(&format!(
         "<style:default-style style:family=\"paragraph\"><style:paragraph-properties fo:margin-bottom=\"8pt\"/><style:text-properties fo:font-family=\"'{}'\" fo:font-size=\"12pt\"/></style:default-style>",
@@ -861,11 +861,13 @@ fn styles_xml(pages: &[(String, wordcraft_doc::SectionProps)], body_font: &str)
     s.push_str("<style:style style:name=\"Quotations\" style:family=\"paragraph\" style:parent-style-name=\"Standard\" style:class=\"html\"><style:paragraph-properties fo:margin-left=\"36pt\" fo:margin-right=\"36pt\"/><style:text-properties fo:font-style=\"italic\" fo:color=\"#404040\"/></style:style>");
     s.push_str(&format!("<style:style style:name=\"Preformatted_20_Text\" style:display-name=\"Preformatted Text\" style:family=\"paragraph\" style:parent-style-name=\"Standard\" style:class=\"html\"><style:paragraph-properties fo:margin-top=\"0pt\" fo:margin-bottom=\"0pt\"/><style:text-properties fo:font-family=\"'{}'\" style:font-family-generic=\"modern\" style:font-pitch=\"fixed\" fo:font-size=\"10pt\"/></style:style>", model::MONO_FONT));
     s.push_str("</office:styles><office:automatic-styles>");
+    // The page background is a document setting: write it on the page layouts so Writer shows it.
+    let bg = page_color.map(|c| format!(" fo:background-color=\"#{}\"", c.hex().to_ascii_lowercase())).unwrap_or_default();
     for (i, (_, sec)) in pages.iter().enumerate() {
         // A page of several columns carries them in its layout.
         let cols = if sec.columns.count > 1 { columns_xml(&sec.columns) } else { String::new() };
         s.push_str(&format!(
-            "<style:page-layout style:name=\"pm{}\"><style:page-layout-properties fo:page-width=\"{}\" fo:page-height=\"{}\" fo:margin-top=\"{}\" fo:margin-bottom=\"{}\" fo:margin-left=\"{}\" fo:margin-right=\"{}\" style:print-orientation=\"{}\">{cols}</style:page-layout-properties></style:page-layout>",
+            "<style:page-layout style:name=\"pm{}\"><style:page-layout-properties fo:page-width=\"{}\" fo:page-height=\"{}\" fo:margin-top=\"{}\" fo:margin-bottom=\"{}\" fo:margin-left=\"{}\" fo:margin-right=\"{}\" style:print-orientation=\"{}\"{}>{cols}</style:page-layout-properties></style:page-layout>",
             i + 1,
             pt(sec.page_w),
             pt(sec.page_h),
@@ -873,7 +875,8 @@ fn styles_xml(pages: &[(String, wordcraft_doc::SectionProps)], body_font: &str)
             pt(sec.margin_bottom),
             pt(sec.margin_left),
             pt(sec.margin_right),
-            if sec.page_w > sec.page_h { "landscape" } else { "portrait" }
+            if sec.page_w > sec.page_h { "landscape" } else { "portrait" },
+            bg
         ));
     }
     s.push_str("</office:automatic-styles><office:master-styles>");
@@ -987,7 +990,7 @@ pub fn export(doc: &Document) -> Result<Vec<u8>, String> {
         w.automatic_styles()
     );
     let body_font = doc.styles.default_chr.font.clone().unwrap_or_else(|| wordcraft_doc::styles::BODY_FONT.to_string());
-    let styles = styles_xml(&pages, &body_font);
+    let styles = styles_xml(&pages, &body_font, doc.settings.page_color);
     let meta = meta_xml(&flow.meta);
     let mut manifest = String::from(
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest:manifest xmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\" manifest:version=\"1.3\">\n<manifest:file-entry manifest:full-path=\"/\" manifest:version=\"1.3\" manifest:media-type=\"application/vnd.oasis.opendocument.text\"/>\n<manifest:file-entry manifest:full-path=\"content.xml\" manifest:media-type=\"text/xml\"/>\n<manifest:file-entry manifest:full-path=\"styles.xml\" manifest:media-type=\"text/xml\"/>\n<manifest:file-entry manifest:full-path=\"meta.xml\" manifest:media-type=\"text/xml\"/>\n",
@@ -2582,7 +2585,7 @@ fn parse_full(bytes: &[u8]) -> Result<(Flow, SectionProps), String> {
 }
 
 /// [`parse_full`], plus whether change tracking is on (`None`: the document has no tracked changes).
-fn parse_tracked(bytes: &[u8]) -> Result<(Flow, SectionProps, Option<bool>, Option<String>, page_parts::Parts), String> {
+fn parse_tracked(bytes: &[u8]) -> Result<(Flow, SectionProps, Option<bool>, Option<String>, page_parts::Parts, Option<Rgb>), String> {
     let mut z = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| format!("ODT: not a valid package ({e})"))?;
     if let Some(mt) = read_entry(&mut z, "mimetype") {
         let mt = String::from_utf8_lossy(&mt);
@@ -2622,13 +2625,13 @@ fn parse_tracked(bytes: &[u8]) -> Result<(Flow, SectionProps, Option<bool>, Opti
         blocks.push(FBlock::Para(Para::default()));
     }
     let track = body.track;
-    Ok((Flow { blocks, meta }, last, track, styles.doc_font(), parts))
+    Ok((Flow { blocks, meta }, last, track, styles.doc_font(), parts, pages.background))
 }
 
 /// Parse an `.odt` package into a document.
 pub fn import(bytes: &[u8]) -> Result<Document, String> {
     model::begin_import();
-    let (flow, last, track, font, parts) = parse_tracked(bytes)?;
+    let (flow, last, track, font, parts, page_color) = parse_tracked(bytes)?;
     let mut doc = model::to_doc(&flow);
     if font.is_some() {
         doc.styles.default_chr.font = font;
@@ -2638,6 +2641,7 @@ pub fn import(bytes: &[u8]) -> Result<Document, String> {
     if let Some(t) = track {
         doc.settings.track_changes = t;
     }
+    doc.settings.page_color = page_color;
     Ok(doc)
 }
 
@@ -2648,6 +2652,9 @@ struct Pages {
     masters: HashMap<String, SectionProps>,
     first: Option<SectionProps>,
     first_name: Option<String>,
+    /// The page background of the default master page: a document-wide setting in WordCraft's
+    /// model, as it is in the DOCX reader (`w:background`).
+    background: Option<Rgb>,
 }
 
 fn read_pages(xml: &[u8]) -> Pages {
@@ -2700,6 +2707,7 @@ fn read_pages(xml: &[u8]) -> Pages {
         if pages.first.is_none() {
             pages.first = Some(sec.clone());
             pages.first_name = Some(name.clone());
+            pages.background = get(a, "fo:background-color").and_then(model::parse_color);
         }
         pages.masters.entry(name).or_insert(sec);
     }
@@ -2746,6 +2754,23 @@ fn apply_layout(a: &Attrs, sec: &mut SectionProps) {
 mod tests {
     use super::*;
 
+    /// The page background (`fo:background-color` of the page layout) is a document setting, as in
+    /// the DOCX reader: the reference Writer document has `#181818`, and WordCraft keeps it.
+    #[test]
+    fn a_page_background_survives_a_round_trip() {
+        let mut d = Document::new();
+        d.ensure_nonempty();
+        d.settings.page_color = Some(Rgb(0x18, 0x18, 0x18));
+        let bytes = super::export(&d).unwrap();
+        let back = super::import(&bytes).unwrap();
+        assert_eq!(back.settings.page_color, Some(Rgb(0x18, 0x18, 0x18)));
+
+        let mut plain = Document::new();
+        plain.ensure_nonempty();
+        let plain = super::import(&super::export(&plain).unwrap()).unwrap();
+        assert_eq!(plain.settings.page_color, None, "no background, none in the file");
+    }
+
     #[test]
     fn escaper_drops_noncharacters() {
         assert_eq!(x("a\u{FFFF}b\u{FFFE}c\u{1}d\te"), "abcd\te");
diff --git a/crates/layout/src/display.rs b/crates/layout/src/display.rs
--- a/crates/layout/src/display.rs
+++ b/crates/layout/src/display.rs
@@ -929,9 +929,11 @@ fn lines(
         StoryRef::Part(id) => doc.parts.get(&id).and_then(|p| p.body.auto_color),
         StoryRef::Body => None,
     };
+    // Writer's "Automatic" text follows the page: white on a dark page, black on a light one.
+    let page_bg = doc.settings.page_color;
     let ink = |c: &TextColor, background: Option<Rgb>| match (c, auto) {
         (TextColor::Auto, Some(rgb)) => rgb,
-        _ => text_color(c, background),
+        _ => text_color(c, background.or(page_bg)),
     };
     for li in l0..l1 {
         let Some(line) = pl.lines.get(li) else { continue };
@@ -1153,7 +1155,7 @@ fn lines(
             // Moved text marked (none) is plain text, its colour setting aside (Word for Mac).
             let plain_move = (moved_from && m.move_from_mark == DeleteMark::None) || (moved_to && m.move_to_mark == InsertMark::None);
             let color = match (ins, del) {
-                _ if plain_move => text_color(&rc.color, rc.shading.or(rc.highlight)),
+                _ if plain_move => ink(&rc.color, rc.shading.or(rc.highlight)),
                 (_, Some(r)) if moved_from => m.move_from_color.unwrap_or_else(|| author(r)),
                 (Some(r), _) if moved_to => m.move_to_color.unwrap_or_else(|| author(r)),
                 (Some(r), _) => m.insert_color.unwrap_or_else(|| author(r)),
diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -1112,14 +1112,6 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
         app.canvas.read_target_page = None;
         app.canvas.scroll_request = Some(vec2(page as f32 * step, 0.0));
     }
-    // View › Switch Modes: dark paper, inverted document colours.
-    // In Read Mode the Page color setting is used instead: Inverse draws the dark page, Sepia
-    // draws sepia paper and None draws white paper.
-    use wordcraft_engine::cmd::view::ReadPageColor;
-    let read_mode = app.session.view.read_mode;
-    let dark_page = if read_mode { app.session.view.read_page_color == ReadPageColor::Inverse } else { app.session.view.dark_mode };
-    let sepia_page = read_mode && app.session.view.read_page_color == ReadPageColor::Sepia;
-    let dark_paper = wordcraft_render::DARK_PAPER;
     let show_ruler = app.session.view.ruler
         && app.session.view.mode == wordcraft_layout::ViewMode::Print
         && !app.session.view.read_mode
@@ -1420,17 +1412,12 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
             } else {
                 painter.with_clip_rect(painter.clip_rect().intersect(band))
             };
-            // Shadow and paper.
+            // Shadow and paper. The paper under the raster is the one the renderer paints
+            // (`paper_of`: the document's own colour, Read Mode's paper, or the theme's), so no
+            // other colour flashes at the edges while a page or tile is re-rendered.
+            let paper_opts = page_render_options(app, dim_body);
             painter.rect_filled(visual_sr.translate(vec2(0.0, 2.0)).expand(1.5), 1.0, t.page_shadow);
-            let paper = if dark_page {
-                Color32::from_gray(dark_paper)
-            } else if sepia_page {
-                let s = wordcraft_render::SEPIA_PAPER;
-                Color32::from_rgb(s.0, s.1, s.2)
-            } else {
-                Color32::WHITE
-            };
-            painter.rect_filled(visual_sr, 0.0, paper);
+            painter.rect_filled(visual_sr, 0.0, crate::theme::c32(paper_opts.paper_of(&app.session.doc)));
             if tiled {
                 // Render before painting to keep the frame sharp immediately after an edit or
                 // zoom. Tiles are fixed to raster-pixel boundaries, independent of scrolling.
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
@@ -273,6 +273,9 @@ pub enum Dialog {
         /// first page (the section's), applied only if changed (#1230).
         odd_even: bool,
         first_page: bool,
+        /// The sheet's own colour (#RRGGBB, or empty for none); `page_color_was` as opened.
+        page_color: String,
+        page_color_was: String,
     },
     Link {
         url: String,
@@ -1334,7 +1337,10 @@ impl Dialog {
                 let size = app.session.doc.styles.resolve_char(Some("Normal"), &Default::default()).size;
                 let grid = (kind, sp.grid_lines_per_page().min(1000), sp.grid_chars_per_line(size).min(1000));
                 let paper = (sp.page_w / crate::options::unit().pt_per_unit(), sp.page_h / crate::options::unit().pt_per_unit());
+                let page_color = app.session.doc.settings.page_color.map(|c| c.hex()).unwrap_or_default();
                 Dialog::PageSetup {
+                    page_color_was: page_color.clone(),
+                    page_color,
                     paper,
                     paper_was: paper,
                     paper_custom: false,
@@ -1817,6 +1823,49 @@ fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &mut bool, c
     });
 }
 
+/// Page Setup › Page Color: the sheet's own colour (the document's `settings.pageColor`), with
+/// the palette, a hex field and RGB values; empty means no colour, and the page picture or the
+/// theme's paper shows instead. It is a document property, independent of the interface theme.
+fn page_color_group(ui: &mut Ui, theme: &[wordcraft_doc::Rgb], value: &mut String) {
+    let t = Tokens::get(ui.ctx());
+    ui.horizontal(|ui| {
+        color_field(ui, theme, value, "No Color", "");
+        let c = wordcraft_doc::Rgb::parse(value);
+        // Preview: the sheet as it will be painted, and a crossed swatch for no colour.
+        let (r, _) = ui.allocate_exact_size(vec2(28.0, 18.0), Sense::hover());
+        match c {
+            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);
+    });
+    ui.horizontal(|ui| {
+        let hex_label = ui.label(tl!("Hex:"));
+        ui.label("#");
+        ui.add(egui::TextEdit::singleline(value).desired_width(76.0).char_limit(8)).labelled_by(hex_label.id);
+        if let Some(c) = wordcraft_doc::Rgb::parse(value) {
+            let mut rgb = [c.0, c.1, c.2];
+            let mut changed = false;
+            for (label, v) in ["Red:", "Green:", "Blue:"].into_iter().zip(rgb.iter_mut()) {
+                ui.label(tl!(label));
+                changed |= ui.add(egui::DragValue::new(v).range(0..=255).speed(1.0)).changed();
+            }
+            if changed {
+                *value = wordcraft_doc::Rgb(rgb[0], rgb[1], rgb[2]).hex();
+            }
+        } else {
+            // Nothing to show for a half-typed or invalid hex: the sheet keeps no colour until
+            // the field parses, and OK stays disabled (see the dialog's OK).
+            ui.label(tl!("No Color")).on_hover_text(value.as_str());
+        }
+    });
+}
+
 /// 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)))
@@ -2415,6 +2464,8 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
             valign,
             odd_even,
             first_page,
+            page_color,
+            page_color_was,
         } => {
             ui.label(egui::RichText::new(tl!("Margins")).font(semibold(12.5)));
             egui::Grid::new("ps").num_columns(4).show(ui, |ui| {
@@ -2443,6 +2494,10 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
             ui.label(egui::RichText::new(tl!("Pages")).font(semibold(12.5)));
             ui.checkbox(mirror, tl!("Mirror margins"));
             page_paper_group(ui, paper, landscape, paper_custom);
+            ui.add_space(2.0);
+            ui.label(egui::RichText::new(tl!("Page Color")).font(semibold(12.5)));
+            page_color_group(ui, &app.session.doc.settings.theme_colors.clone(), page_color);
+            ui.add_space(2.0);
             ui.label(egui::RichText::new(tl!("Document Grid")).font(semibold(12.5)));
             ui.horizontal_wrapped(|ui| {
                 ui.radio_value(&mut grid.0, 0, tl!("No grid"));
@@ -2470,7 +2525,9 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
             ui.label(egui::RichText::new(tl!("Headers and footers")).font(semibold(12.5)));
             ui.checkbox(odd_even, tl!("Different Odd & Even Pages"));
             ui.checkbox(first_page, tl!("Different First Page"));
+            let color_ok = page_color.is_empty() || wordcraft_doc::Rgb::parse(page_color).is_some();
             let (mut ok, cancel) = buttons(ui, tl!("OK"));
+            ok &= color_ok;
             if ok {
                 // Everything OK changes is one undo step; a refused command applies nothing.
                 let result = page_layout_commands(app, (*paper, *paper_was), *landscape, (*top, *bottom, *left, *right))
@@ -2503,6 +2560,10 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
                         if *first_page != wordcraft_engine::cmd::page::sect(&app.session).title_page {
                             commands.push(("layout.differentFirstPage", json!({"value": *first_page})));
                         }
+                        if page_color != page_color_was {
+                            let c = wordcraft_doc::Rgb::parse(page_color).map(|c| c.hex());
+                            commands.push(("design.pageColor", json!({"color": c})));
+                        }
                         run_as_one_step(app, commands)
                     });
                 if let Err(e) = result {
@@ -5303,6 +5364,54 @@ mod tests {
         ((paper.0 * ppu, paper.1 * ppu), paper_entry(paper))
     }
 
+    /// Page Setup's Page Color: the sheet's own colour lives in the document (not the theme),
+    /// applies on OK, stays in the document on Cancel, and an invalid hex keeps OK from applying.
+    #[test]
+    fn page_setup_sets_the_pages_own_colour() {
+        use egui_kittest::kittest::Queryable;
+        let mut h = app_harness(app_with("Page"));
+        h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
+        h.run_steps(3);
+        assert!(matches!(h.state().dialog, Some(Dialog::PageSetup { ref page_color, .. }) if page_color.is_empty()));
+        if let Some(Dialog::PageSetup { page_color, .. }) = h.state_mut().dialog.as_mut() {
+            *page_color = "181818".into();
+        }
+        h.run_steps(2);
+        h.get_by_label("OK").click();
+        h.run_steps(3);
+        assert_eq!(h.state().session.doc.settings.page_color, Some(wordcraft_doc::Rgb(0x18, 0x18, 0x18)), "applied on OK");
+        // Reopening shows the document's colour; Cancel leaves it as it was.
+        h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
+        h.run_steps(3);
+        assert!(matches!(h.state().dialog, Some(Dialog::PageSetup { ref page_color, .. }) if page_color == "181818"));
+        if let Some(Dialog::PageSetup { page_color, .. }) = h.state_mut().dialog.as_mut() {
+            *page_color = "FFFFFF".into();
+        }
+        h.run_steps(2);
+        h.get_by_label("Cancel").click();
+        h.run_steps(3);
+        assert_eq!(h.state().session.doc.settings.page_color, Some(wordcraft_doc::Rgb(0x18, 0x18, 0x18)), "Cancel changes nothing");
+        // A half-typed hex doesn't apply: the dialog stays open with the document untouched.
+        h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
+        h.run_steps(3);
+        if let Some(Dialog::PageSetup { page_color, .. }) = h.state_mut().dialog.as_mut() {
+            *page_color = "18".into();
+        }
+        h.run_steps(2);
+        h.get_by_label("OK").click();
+        h.run_steps(3);
+        assert!(h.state().dialog.is_some(), "an invalid colour leaves the dialog open");
+        assert_eq!(h.state().session.doc.settings.page_color, Some(wordcraft_doc::Rgb(0x18, 0x18, 0x18)));
+        // Clearing it gives the document no page colour again.
+        if let Some(Dialog::PageSetup { page_color, .. }) = h.state_mut().dialog.as_mut() {
+            page_color.clear();
+        }
+        h.run_steps(2);
+        h.get_by_label("OK").click();
+        h.run_steps(3);
+        assert_eq!(h.state().session.doc.settings.page_color, None, "No Color restores the default");
+    }
+
     /// #1433: Page Setup's Paper size shows the named size the section has, in either
     /// orientation, and Custom size (None) for anything else.
     #[test]
