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
@@ -574,7 +574,9 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
             // starts one, and each line folds at the column width.
             if st.align.wrap {
                 let text = crate::display::cell_text(wb, sh, CellRef::new(row, c));
-                let w = sh.col_width(c).max(1.0) - 6.0;
+                // Hidden columns have zero display width, but their cells still wrap at
+                // the stored (or sheet-default) width when fitting a row.
+                let w = sh.cols.get(&c).and_then(|info| info.size).unwrap_or(sh.default_col_width).max(1.0) - 6.0;
                 lines = text.lines().map(|l| wrapped_line_count(l, w.max(1.0), size, st.font.bold)).sum::<f32>().max(1.0);
             }
         } else if st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE {
diff --git a/crates/engine/tests/it/row_autofit.rs b/crates/engine/tests/it/row_autofit.rs
--- a/crates/engine/tests/it/row_autofit.rs
+++ b/crates/engine/tests/it/row_autofit.rs
@@ -1,5 +1,8 @@
 //! Whole-row and whole-column formatting must re-fit row heights (issue #833).
 
+use gridcraft_core::{CellRef, Value};
+use gridcraft_engine::{DocState, Session, Trust};
+use gridcraft_model::{StyleId, Workbook};
 use serde_json::json;
 
 fn sheet_mut(s: &mut gridcraft_engine::Session) -> &mut gridcraft_engine::model::Sheet {
@@ -59,3 +62,84 @@ fn whole_row_format_keeps_hidden_rows_hidden() {
     assert!(info.size.unwrap_or(before) > before + 10.0, "hidden row still grows to its font: before={before}, size={:?}", info.size);
     assert!(info.hidden, "formatting a hidden row must not unhide it");
 }
+
+// Build the input without commands, which would already fit the row. The wrapped
+// cell in B1 must contribute even when hidden, using its stored or default width.
+fn wrapped_column_workbook(hidden: bool, width: Option<f32>, custom_height: bool) -> Workbook {
+    let mut wb = Workbook::new();
+    let wrapped = wb.styles.derive(StyleId::DEFAULT, |style| style.align.wrap = true);
+    let sh = wb.sheet_mut(0).unwrap();
+    sh.default_col_width = 80.0;
+    sh.set_value(CellRef::new(0, 0), Value::text("Label"));
+    sh.set_value(CellRef::new(0, 1), Value::text("Wrapped text spans lines"));
+    sh.set_style(CellRef::new(0, 1), wrapped);
+    let col = sh.cols.entry(1).or_default();
+    col.hidden = hidden;
+    col.size = width;
+    let row = sh.rows.entry(0).or_default();
+    row.size = Some(20.0);
+    row.custom = custom_height;
+    wb
+}
+
+fn open_wrapped_column(hidden: bool, width: Option<f32>, custom_height: bool) -> Session {
+    let bytes = gridcraft_xlsx::write_xlsx(&wrapped_column_workbook(hidden, width, custom_height)).unwrap();
+    let mut s = Session::new();
+    s.execute("file.open", json!({"name": "wrapped-column.xlsx", "base64": gridcraft_engine::io::base64_encode(&bytes)})).unwrap();
+    s
+}
+
+#[test]
+fn imported_wrapped_rows_use_hidden_columns_stored_width() {
+    let mut heights = Vec::new();
+    for width in [None, Some(56.0), Some(120.0)] {
+        let visible = open_wrapped_column(false, width, false);
+        let hidden = open_wrapped_column(true, width, false);
+        let expected = visible.doc().unwrap().wb.active().unwrap().row_height(0);
+        let sh = hidden.doc().unwrap().wb.active().unwrap();
+        assert_eq!(sh.row_height(0), expected, "hiding a column must not change the imported row's wrap width: {width:?}");
+        assert!(expected > 20.0, "the wrapped hidden cell must still contribute to the row height: {width:?}");
+        assert!(sh.cols.get(&1).unwrap().hidden, "fitting must not unhide the column");
+        heights.push(expected);
+    }
+    assert!(heights[1] > heights[2], "a narrow stored width must wrap onto more lines than a wide one");
+}
+
+#[test]
+fn explicit_row_autofit_uses_hidden_columns_stored_or_default_width() {
+    let mut heights = Vec::new();
+    for width in [None, Some(56.0), Some(120.0)] {
+        let mut fitted = Vec::new();
+        for hidden in [false, true] {
+            let mut s = Session::new();
+            s.add_document(DocState::new(wrapped_column_workbook(hidden, width, true), None, "Wrapped column".into(), Trust::Local));
+            assert_eq!(s.doc().unwrap().wb.active().unwrap().row_height(0), 20.0);
+            s.execute("home.autofitRowHeight", json!({"rows": "1:1"})).unwrap();
+            let sh = s.doc().unwrap().wb.active().unwrap();
+            fitted.push(sh.row_height(0));
+            assert!(sh.row_height(0) > 20.0, "AutoFit must include the wrapped cell, including when hidden");
+            assert!(!sh.rows.get(&0).unwrap().custom, "explicit AutoFit releases the custom height");
+            assert_eq!(sh.cols.get(&1).unwrap().hidden, hidden);
+            assert_eq!(sh.cols.get(&1).unwrap().size, width, "AutoFit must not change the stored column width");
+            s.execute("edit.undo", json!({})).unwrap();
+            let sh = s.doc().unwrap().wb.active().unwrap();
+            assert_eq!(sh.row_height(0), 20.0, "undo restores the explicit row height");
+            assert!(sh.rows.get(&0).unwrap().custom);
+            assert_eq!(sh.cols.get(&1).unwrap().hidden, hidden);
+        }
+        assert_eq!(fitted[0], fitted[1], "hidden and visible columns must have the same AutoFit wrap width: {width:?}");
+        heights.push(fitted[0]);
+    }
+    assert!(heights[1] > heights[2], "AutoFit must use the stored width when present");
+}
+
+#[test]
+fn imported_wrapped_hidden_columns_keep_custom_row_heights() {
+    for hidden in [false, true] {
+        let s = open_wrapped_column(hidden, Some(56.0), true);
+        let sh = s.doc().unwrap().wb.active().unwrap();
+        assert_eq!(sh.row_height(0), 20.0, "import must preserve an explicit custom height");
+        assert!(sh.rows.get(&0).unwrap().custom);
+        assert_eq!(sh.cols.get(&1).unwrap().hidden, hidden);
+    }
+}
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,6 +1,6 @@
 # UI parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (AutoSum over a selected row puts its total to the right) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (row AutoFit uses hidden columns' stored width) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
 
 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
 **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
@@ -47,6 +47,12 @@ uses that allowance in its grid units; its existing approximate font measurement
 Header text aligns and clips before the button. Command and header-double-click paths share
 this logic, with one undo step.
 
+Row AutoFit, including fitting on open, measures wrapped text in hidden columns at their
+stored width, or the sheet's default width when absent. Hidden and visible columns therefore
+contribute equally to the row height, as observed with a synthetic workbook in Windows Excel
+16.0 build 20527 on 2026-10-26; hiding a column no longer makes its wrapped text inflate the row. Custom row
+heights stay intact on open until AutoFit is requested. Font measurement remains approximate.
+
 | Behaviour | Status |
 |---|---|
 | Click/drag/Shift/Ctrl selection, multi-area selection | rectangular drag works in one direction only in some cases (#28) |
@@ -187,6 +193,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-26 | minor | Row AutoFit and fitting on open use a hidden column's stored or default width for wrapped text, instead of its zero display width. Hidden and visible columns contribute equally; custom row heights remain intact on open and explicit AutoFit remains undoable |
 | 2026-10-25 | minor | AutoSum (Alt+=, Home › AutoSum) over a selected row puts its total to the right of the row, or in its last cell when that is empty, as in Excel; it wrote a one-cell total under each cell. A selection's empty last column takes each row's total and its empty last row each column's, with the block's total in the corner when both are empty |
 | 2026-10-25 | minor | Paste Name lays out only the names in view, so a workbook with thousands of names opens it at once; the arrow keys still scroll the chosen name into view. Go To matches a `Sheet2!Name` reference and hides a workbook name behind the active sheet's own name in any case and script (`Größe` = `GRÖSSE`), as formulas do. The two Page Layout view rows are one; the 0.7 in side margin used when Align with Page Margins is off is marked unverified |
 | 2026-10-24 | minor | Go To lists the named ranges on every sheet, as Excel's does ("The Go to popup window shows named ranges on every worksheet in your workbook"): a name scoped to another sheet appears as `Sheet2!Name`, and hidden names, constants and formulas are left out. It keeps the references entered (newest first, ten at most), and a double-click on an entry goes there. The Reference box and `edit.goTo` take several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30,H21:H29`) and select them all; `edit.goTo {list: true}` returns the list |
