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/docbin/src/sections.rs b/crates/docbin/src/sections.rs
--- a/crates/docbin/src/sections.rs
+++ b/crates/docbin/src/sections.rs
@@ -82,6 +82,9 @@ pub(crate) fn parse(word: &[u8], table: &[u8], fib: &Fib) -> Vec<Section> {
 /// carry 16-bit twip operands ([MS-DOC] §2.6.4).
 fn apply_section(s: &mut SectionProps, prl: &Prl) {
     let tw = |v: i32| (v as f32 / 20.0).clamp(0.0, 1584.0);
+    // A page side is not one of the lengths the 1,584 pt (22 in) cap is for: the document model
+    // takes up to 600 cm, so convert it straight from twips and bound it there.
+    let page_tw = |v: i32| (v as f32 / 20.0).clamp(7.2, wordcraft_doc::section::MAX_PAGE_SIDE);
     let s16 = |prl: &Prl| -> Option<i16> {
         match prl.operand {
             [b0, b1, ..] => Some(i16::from_le_bytes([*b0, *b1])),
@@ -130,12 +133,12 @@ fn apply_section(s: &mut SectionProps, prl: &Prl) {
         }
         S_XA_PAGE => {
             if let Some(v) = s16(prl) {
-                s.page_w = tw(v as i32).max(7.2);
+                s.page_w = page_tw(v as i32);
             }
         }
         S_YA_PAGE => {
             if let Some(v) = s16(prl) {
-                s.page_h = tw(v as i32).max(7.2);
+                s.page_h = page_tw(v as i32);
             }
         }
         S_DXA_LEFT => {
diff --git a/crates/docx/src/read/props.rs b/crates/docx/src/read/props.rs
--- a/crates/docx/src/read/props.rs
+++ b/crates/docx/src/read/props.rs
@@ -697,11 +697,15 @@ pub fn sectpr(e: &El) -> (SectionProps, Vec<HfRef>) {
                 }
             }
             "w:pgSz" => {
-                if let Some(w) = tw(k, "w:w").filter(|v| *v >= 36.0) {
-                    s.page_w = w;
+                // A page is at least half an inch and at most 600 cm (LibreOffice's limit): a
+                // hostile size can't reach layout, and the 600 cm pages WordCraft writes back
+                // still open.
+                let side = |v: f32| v.clamp(36.0, wordcraft_doc::section::MAX_PAGE_SIDE);
+                if let Some(w) = crate::units::tw_page(k, "w:w").filter(|v| *v >= 36.0) {
+                    s.page_w = side(w);
                 }
-                if let Some(h) = tw(k, "w:h").filter(|v| *v >= 36.0) {
-                    s.page_h = h;
+                if let Some(h) = crate::units::tw_page(k, "w:h").filter(|v| *v >= 36.0) {
+                    s.page_h = side(h);
                 }
                 s.landscape = match k.attr("w:orient") {
                     Some("landscape") => true,
diff --git a/crates/docx/src/units.rs b/crates/docx/src/units.rs
--- a/crates/docx/src/units.rs
+++ b/crates/docx/src/units.rs
@@ -9,6 +9,12 @@ pub const MAX_LEN_PT: f32 = 14_400.0;
 /// `3mm`, `1pc`, `1pi`). `per_pt` is how many file units make a point when there's no unit
 /// suffix (20 for twips, 2 for half-points, 8 for eighths, 12700 for EMU). Returns points.
 pub fn measure(s: &str, per_pt: f32) -> Option<f32> {
+    measure_capped(s, per_pt, MAX_LEN_PT)
+}
+
+/// `measure` with an explicit cap, for the few measures that may exceed a normal length: a page
+/// side goes up to 600 cm (LibreOffice's custom size), past the 200" bound of other lengths.
+fn measure_capped(s: &str, per_pt: f32, cap: f32) -> Option<f32> {
     let t = s.trim();
     if t.is_empty() || t.len() > 40 {
         return None;
@@ -57,7 +63,17 @@ pub fn measure(s: &str, per_pt: f32) -> Option<f32> {
         _ => return None,
     };
     let pt = pt as f32;
-    pt.is_finite().then_some(pt.clamp(-MAX_LEN_PT, MAX_LEN_PT))
+    pt.is_finite().then_some(pt.clamp(-cap, cap))
+}
+
+/// `w:pgSz`'s `w:w`/`w:h`: a page side, up to 600 cm.
+pub fn tw_page(e: &El, attr: &str) -> Option<f32> {
+    e.attr(attr).and_then(|v| measure_capped(v, 20.0, wordcraft_doc::section::MAX_PAGE_SIDE))
+}
+
+/// Twips for a page side, which may be 600 cm — beyond [`twips`]'s 200" bound.
+pub fn twips_page(pt: f32) -> String {
+    wordcraft_geom::to_twips(pt.clamp(-wordcraft_doc::section::MAX_PAGE_SIDE, wordcraft_doc::section::MAX_PAGE_SIDE)).to_string()
 }
 
 /// `w:line` under `lineRule="auto"`: the multiple of single line spacing, in 240ths (240 =
diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs
--- a/crates/docx/src/write/story.rs
+++ b/crates/docx/src/write/story.rs
@@ -1413,11 +1413,15 @@ fn sectpr_body(w: &mut W, s: &SectionProps) {
             SectionStart::NextColumn => "nextColumn",
         };
         w.val("w:type", start);
-        let (pw, ph) = (s.page_w.clamp(36.0, crate::units::MAX_LEN_PT), s.page_h.clamp(36.0, crate::units::MAX_LEN_PT));
+        // A page may be up to 600 cm (LibreOffice's custom size): the size goes into the file as
+        // it is, so WordCraft and Writer read it back. Word itself stops at 22" when it opens the
+        // file — a Word limit, shown in the app's save note, not something to truncate here.
+        let page_side = |v: f32| v.clamp(36.0, wordcraft_doc::section::MAX_PAGE_SIDE);
+        let (pw, ph) = (page_side(s.page_w), page_side(s.page_h));
         if s.landscape {
-            w.empty("w:pgSz", &[("w:w", &twips(pw)), ("w:h", &twips(ph)), ("w:orient", "landscape")]);
+            w.empty("w:pgSz", &[("w:w", &crate::units::twips_page(pw)), ("w:h", &crate::units::twips_page(ph)), ("w:orient", "landscape")]);
         } else {
-            w.empty("w:pgSz", &[("w:w", &twips(pw)), ("w:h", &twips(ph))]);
+            w.empty("w:pgSz", &[("w:w", &crate::units::twips_page(pw)), ("w:h", &crate::units::twips_page(ph))]);
         }
         w.empty(
             "w:pgMar",
diff --git a/crates/docx/tests/it/roundtrip.rs b/crates/docx/tests/it/roundtrip.rs
--- a/crates/docx/tests/it/roundtrip.rs
+++ b/crates/docx/tests/it/roundtrip.rs
@@ -14,6 +14,19 @@ use wordcraft_doc::styles::{Style, StyleKind};
 use wordcraft_doc::table::{Cell, Table};
 use wordcraft_doc::{Block, Blocks, Comment, Document, InlineObject, Paragraph, PartKind, Revision, RevisionKind, Source, Watermark, para_block};
 
+/// A 600 cm page (LibreOffice's custom size) goes through DOCX too: Word's own 22" limit is a
+/// Word limit, not WordCraft's, and WordCraft must read back what it wrote.
+#[test]
+fn a_six_hundred_centimetre_page_roundtrips() {
+    let cm = 72.0 / 2.54;
+    let mut d = Document::from_text("Body");
+    d.last_section.page_w = cm * 19.0;
+    d.last_section.page_h = cm * 600.0;
+    let back = rt(&d);
+    assert!((back.last_section.page_h - cm * 600.0).abs() < 0.5, "{}", back.last_section.page_h);
+    assert!((back.last_section.page_w - cm * 19.0).abs() < 0.5);
+}
+
 fn rt(doc: &Document) -> Document {
     let bytes = wordcraft_docx::write(doc).expect("write");
     wordcraft_docx::read(&bytes).expect("read")
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()) {
@@ -542,9 +542,11 @@ pub fn margins_fit(page: (f32, f32), (t, b, l, r): (f32, f32, f32, f32), gutter:
     l + r + gutter <= page.0 - MIN_TEXT_ROOM && t + b <= page.1 - MIN_TEXT_ROOM
 }
 
-/// The smallest and largest page side, points (0.1" and 22", as Word allows).
+/// The smallest page side, points (0.1", as Word allows).
 pub const MIN_PAGE_SIDE: f32 = 7.2;
-pub const MAX_PAGE_SIDE: f32 = 1584.0;
+/// The largest page side, points: 600 cm (LibreOffice's custom page limit), so a Writer document
+/// with a 600 cm page keeps its height. Word's own 22" limit only matters when Word opens it.
+pub const MAX_PAGE_SIDE: f32 = wordcraft_doc::section::MAX_PAGE_SIDE;
 
 fn size(s: &mut Session, v: &Value) -> CmdResult {
     let (w, h) = if let Some(n) = p::str(v, "name") {
@@ -779,6 +781,37 @@ mod tests {
         json!({"section": v})
     }
 
+    /// A 600 cm page (LibreOffice's custom size) is a real page: accepted as-is by the model and
+    /// by `layout.pageSetup`; only a value beyond the maximum is clamped.
+    #[test]
+    fn a_six_hundred_centimetre_page_is_accepted() {
+        let cm = 72.0 / 2.54;
+        let mut s = doc();
+        let tall = cm * 600.0;
+        s.run(
+            "layout.pageSetup",
+            &with_section(|v| {
+                v["pageW"] = json!(cm * 19.0);
+                v["pageH"] = json!(tall);
+                v["marginTop"] = json!(11.34);
+                v["marginBottom"] = json!(11.34);
+                v["marginLeft"] = json!(11.34);
+                v["marginRight"] = json!(11.34);
+            }),
+        )
+        .unwrap();
+        let got = sect(&s);
+        assert!((got.page_h - tall).abs() < 0.01, "600 cm kept: {}", got.page_h);
+        assert!((got.page_w - cm * 19.0).abs() < 0.01, "the width is untouched");
+
+        // Just past the maximum: clamped to it, not kept and not refused.
+        s.run("layout.pageSetup", &with_section(|v| v["pageH"] = json!(tall + 50.0))).unwrap();
+        assert_eq!(sect(&s).page_h, wordcraft_doc::section::MAX_PAGE_SIDE);
+        // Junk is still refused.
+        assert!(s.run("layout.pageSetup", &with_section(|v| v["pageH"] = json!(f64::INFINITY))).is_err());
+        assert!(s.run("layout.pageSetup", &with_section(|v| v["pageH"] = json!(-1.0))).is_err());
+    }
+
     /// `layout.pageSetup {section}` checks the values like the dedicated commands do.
     #[test]
     fn page_setup_section_is_validated() {
@@ -860,7 +893,7 @@ mod tests {
         )
         .unwrap();
         let x = sect(&s);
-        assert_eq!(x.page_w, 1584.0);
+        assert_eq!(x.page_w, wordcraft_doc::section::MAX_PAGE_SIDE, "clamped to 600 cm");
         assert!(x.landscape, "wider than tall is landscape");
         assert_eq!(x.columns.widths.len(), 12);
         assert_eq!(x.columns.count, 12, "the count follows the widths");
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
@@ -2635,7 +2635,11 @@ fn layout_size_takes_a_custom_size_and_clamps_it() {
     assert_eq!(size(&s), (300.0, 500.0));
     run(&mut s, "layout.margins", json!({"top": 0.0, "bottom": 0.0, "left": 0.0, "right": 0.0}));
     run(&mut s, "layout.size", json!({"width": 40.0, "height": 5000.0}));
-    assert_eq!(size(&s), (40.0, 1584.0), "clamped to 22\"");
+    // A 600 cm page is a real page now: 5000 pt stays, and only past the model's maximum is it
+    // clamped.
+    assert_eq!(size(&s), (40.0, 5000.0), "5000 pt is under 600 cm");
+    run(&mut s, "layout.size", json!({"width": 40.0, "height": 1e6}));
+    assert_eq!(size(&s), (40.0, wordcraft_doc::section::MAX_PAGE_SIDE), "clamped to 600 cm");
     for junk in [
         json!({"width": 1e300, "height": 400.0}),
         json!({"width": "NaN", "height": 400.0}),
@@ -2646,7 +2650,7 @@ fn layout_size_takes_a_custom_size_and_clamps_it() {
         json!({"width": 1.0, "height": 400.0}),
     ] {
         assert!(s.run("layout.size", &junk).is_err(), "{junk}");
-        assert_eq!(size(&s), (40.0, 1584.0), "{junk} leaves the size alone");
+        assert_eq!(size(&s), (40.0, wordcraft_doc::section::MAX_PAGE_SIDE), "{junk} leaves the size alone");
     }
     // A landscape section keeps its orientation: the long side goes across.
     run(&mut s, "layout.orientation", json!({"value": "landscape"}));
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
@@ -24,7 +24,7 @@ use std::sync::Arc;
 use quick_xml::XmlVersion;
 use quick_xml::events::{BytesStart, Event};
 use wordcraft_doc::props::VAlign;
-use wordcraft_doc::section::{Columns, SectionStart};
+use wordcraft_doc::section::{Columns, MAX_PAGE_SIDE, SectionStart};
 use wordcraft_doc::{Align, Document, RevisionKind, Rgb, SectionProps};
 
 use crate::model::{self, Cell, FBlock, FTable, Flow, Fmt, Inline, Kind, ListInfo, Meta, Para, RevMark, make_img, mime_of};
@@ -2717,9 +2717,10 @@ fn apply_page_layout(xml: &[u8], sec: &mut SectionProps) {
 /// A `style:page-layout-properties` element's page size, orientation and margins into `sec`.
 fn apply_layout(a: &Attrs, sec: &mut SectionProps) {
     let len = |k: &str| get(a, k).and_then(length).filter(|v| *v >= 0.0);
-    // Like the DOCX reader: a page is at least half an inch; at most 22 inches (Word's limit), so a hostile
-    // `1e30pt` can't reach layout.
-    let page = |k: &str| len(k).filter(|v| *v >= 36.0).map(|v| v.min(1584.0));
+    // Like the DOCX reader: a page is at least half an inch; at most 600 cm (the model's limit,
+    // LibreOffice's custom size), so a hostile `1e30pt` can't reach layout but a Writer page of
+    // 600 cm still opens instead of being truncated to Word's 22 inches.
+    let page = |k: &str| len(k).filter(|v| *v >= 36.0).map(|v| v.min(MAX_PAGE_SIDE));
     if let (Some(w), Some(h)) = (page("fo:page-width"), page("fo:page-height")) {
         sec.page_w = w;
         sec.page_h = h;
@@ -3033,11 +3034,13 @@ mod tests {
                 </office:document-styles>"#
             )
         };
-        // Huge values: the page is capped at 22 inches and margins at the page's longer side.
+        // Huge values: the page is capped at the model's 600 cm and margins at the page's
+        // longer side.
+        let max = wordcraft_doc::section::MAX_PAGE_SIDE;
         let mut sec = wordcraft_doc::SectionProps::default();
         apply_page_layout(styles("1e30pt", "1e30in", "1e30pt").as_bytes(), &mut sec);
-        assert_eq!((sec.page_w, sec.page_h), (1584.0, 1584.0));
-        assert_eq!((sec.margin_top, sec.margin_left), (1584.0, 1584.0));
+        assert_eq!((sec.page_w, sec.page_h), (max, max));
+        assert_eq!((sec.margin_top, sec.margin_left), (max, max));
         // Negative, zero, tiny and non-numeric values leave the defaults alone.
         let def = wordcraft_doc::SectionProps::default();
         for (w, h, m) in [("-10in", "-5in", "-1in"), ("0pt", "0pt", "NaNpt"), ("1pt", "2pt", "infpt"), ("NaN", "inf", "x")] {
@@ -3697,6 +3700,20 @@ mod tests {
         assert_eq!(marks(&back), want, "after a save");
     }
 
+    /// A 600 cm page (LibreOffice's custom size) survives ODT: the reader used to clamp page
+    /// sides to 22 inches (55.88 cm), which truncated the Writer document.
+    #[test]
+    fn a_six_hundred_centimetre_page_survives_odt() {
+        let cm = 72.0 / 2.54;
+        let mut d = wordcraft_doc::Document::new();
+        d.ensure_nonempty();
+        d.last_section.page_w = cm * 19.0;
+        d.last_section.page_h = cm * 600.0;
+        let back = super::import(&super::export(&d).unwrap()).unwrap();
+        assert!((back.last_section.page_h - cm * 600.0).abs() < 0.5, "{}", back.last_section.page_h);
+        assert!((back.last_section.page_w - cm * 19.0).abs() < 0.5);
+    }
+
     #[test]
     fn insertions_end_before_a_table_and_inside_a_cell() {
         let regions = region("i1", "insertion", "Alice", "2024-03-01T10:00:00", "") + &region("i2", "insertion", "Bob", "2024-03-02T10:00:00", "");
