diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs
--- a/crates/engine/src/cmd/edit.rs
+++ b/crates/engine/src/cmd/edit.rs
@@ -183,7 +183,7 @@ fn copy(s: &mut Session, _: &Value) -> CmdResult {
 /// and for a copy of one equation alone, its MathML (#1297).
 pub(crate) fn copy_result(s: &mut Session) -> Value {
     s.clipboard_html = match &s.clipboard {
-        Some(f) => wordcraft_formats::html::export_fragment(&f.to_document()),
+        Some(f) => wordcraft_formats::html::export_fragment(&clipboard_document(s, f)),
         None => String::new(),
     };
     let mut r = json!({"text": s.clipboard_text});
@@ -200,6 +200,53 @@ pub(crate) fn copy_result(s: &mut Session) -> Value {
     r
 }
 
+/// The copy as a document, for the HTML Copy puts on the system clipboard: the fragment with the
+/// source's styles and list definitions, so its numbered and bulleted paragraphs stay lists when
+/// another program pastes them (Word turns the HTML list back into numbering), and with each
+/// copied list starting at the number it has in the source, counted as the page shows it: a copy
+/// of items 3 and 4 reads 3 and 4, as Word's does. The first copied item of each list and level
+/// gets a list of its own that starts there, so a later list at that level, under the next item,
+/// still starts at its own start.
+fn clipboard_document(s: &Session, f: &Fragment) -> wordcraft_doc::Document {
+    let mut d = f.to_document();
+    d.styles = s.doc.styles.clone();
+    d.numbering = s.doc.numbering.clone();
+    let (a, b) = s.sel.ordered();
+    // Copied body blocks map one to one onto the source's from the first: only a copy that starts
+    // in a top-level paragraph of the body.
+    let Some(first) = (a.story == StoryRef::Body && a.path.depth() == 0).then(|| a.path.last()) else { return d };
+    let mut counters = wordcraft_doc::numbering::Counters::default();
+    let mut seen = std::collections::HashSet::new();
+    for path in s.doc.para_paths(StoryRef::Body) {
+        if path > b.path {
+            break;
+        }
+        let Some(p) = s.doc.para(StoryRef::Body, &path) else { continue };
+        let Some(n) = s.doc.styles.resolve_para(&p.props).numbering.filter(|n| n.num != 0) else { continue };
+        if counters.next_label(&s.doc.numbering, n.num, n.level).is_none() || path < a.path || !seen.insert((n.num, n.level)) {
+            continue;
+        }
+        let Some(value) = counters.values(&s.doc.numbering, n.num).and_then(|v| v.get(n.level as usize).copied()) else { continue };
+        let Some(own) = d.numbering.restart(n.num) else { continue };
+        // Its level starts at its number, and the levels above it at their current numbers: they
+        // stand for the items above it the copy leaves out.
+        let current = counters.values(&s.doc.numbering, n.num).unwrap_or_default();
+        if let Some(num) = d.numbering.nums.iter_mut().find(|x| x.id == own) {
+            num.start_overrides.retain(|(l, _)| *l > n.level);
+            num.start_overrides.extend((0..n.level).filter_map(|k| current.get(k as usize).map(|v| (k, (*v).max(1)))));
+            num.start_overrides.push((n.level, value));
+        }
+        let mut at = path.clone();
+        if let Some(top) = at.0.first_mut() {
+            *top -= first as u32;
+        }
+        if let Ok(q) = d.para_mut(StoryRef::Body, &at) {
+            q.props.numbering = Some(wordcraft_doc::props::NumRef { num: own, level: n.level });
+        }
+    }
+    d
+}
+
 fn cut(s: &mut Session, v: &Value) -> CmdResult {
     let r = copy(s, v)?;
     // A tracked cut of a paragraph or sentence may become a move when pasted.
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
@@ -1309,6 +1309,40 @@ fn copying_bold_text_puts_html_on_the_clipboard() {
     assert_eq!(s.clipboard_html, html);
 }
 
+/// Copied numbered paragraphs reach other programs as an HTML list (Word turns it back into
+/// numbering when it pastes; it pasted them unnumbered): with the levels' number formats, and
+/// starting at the numbers the copied items have, the levels above a copy that starts inside a
+/// sub-list included, as Word's own copy does.
+#[test]
+fn copying_numbered_paragraphs_puts_an_html_list_on_the_clipboard() {
+    let mut s = s();
+    run(&mut s, "document.setText", json!({"text": "One\nTwo\nSub A\nSub B\nThree"}));
+    run(&mut s, "select.all", json!({}));
+    run(&mut s, "para.numbering", json!({}));
+    for k in [2, 3] {
+        s.sel = crate::Selection::caret(Pos::body(k, 0));
+        run(&mut s, "para.listLevel", json!({"level": 1}));
+    }
+    let copied = |s: &mut Session, from: usize| {
+        s.sel = crate::Selection { anchor: Pos::body(from, 0), focus: Pos::body(4, 5) };
+        let r = run(s, "edit.copy", json!({}));
+        r["html"].as_str().unwrap_or_default().replace('\n', "")
+    };
+    let num = s.doc.para(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap().props.numbering.unwrap().num;
+    let sub = match s.doc.numbering.level(num, 1).map(|l| l.format) {
+        Some(wordcraft_doc::section::NumFormat::LowerLetter) => " type=\"a\"",
+        Some(wordcraft_doc::section::NumFormat::LowerRoman) => " type=\"i\"",
+        _ => "",
+    };
+    let all = copied(&mut s, 0);
+    assert!(all.contains(&format!("<ol><li>One</li><li>Two<ol{sub}><li>Sub A</li><li>Sub B</li></ol></li><li>Three</li></ol>")), "{all}");
+    let from_two = copied(&mut s, 1);
+    assert!(from_two.contains(&format!("<ol start=\"2\"><li>Two<ol{sub}><li>Sub A</li>")), "{from_two}");
+    // From the second sub-item: under item 2, starting at its second number, then item 3.
+    let from_sub_b = copied(&mut s, 3);
+    assert!(from_sub_b.contains(&format!("<ol start=\"2\"><li><ol{sub} start=\"2\"><li>Sub B</li></ol></li><li>Three</li></ol>")), "{from_sub_b}");
+}
+
 #[test]
 fn copying_symbol_font_text_puts_the_unicode_symbols_on_the_clipboard() {
     let mut s = s();
diff --git a/crates/formats/src/html.rs b/crates/formats/src/html.rs
--- a/crates/formats/src/html.rs
+++ b/crates/formats/src/html.rs
@@ -16,6 +16,7 @@
 use std::sync::Arc;
 
 use wordcraft_doc::props::VAlign;
+use wordcraft_doc::section::NumFormat;
 use wordcraft_doc::{Align, Document};
 
 use crate::model::{
@@ -1858,7 +1859,19 @@ fn blocks_html(blocks: &[FBlock], out: &mut String, depth: usize) {
                             out.push_str("<li>");
                             top.1 = true;
                         }
-                        out.push_str(if li.ordered { "<ol>" } else { "<ul>" });
+                        // The innermost list opens with the item's number format and start, as
+                        // a browser and Word read them (`<ol type="a" start="3">`).
+                        let innermost = stack.len() == lv;
+                        // A list opening above it (the copy leaves its item out) takes that
+                        // level's start and format.
+                        out.push_str(&match (li.ordered, innermost) {
+                            (true, true) => format!("<ol{}>", ol_attrs(li.start, q.list_format)),
+                            (true, false) => match q.list_outer.get(stack.len()) {
+                                Some((start, fmt)) => format!("<ol{}>", ol_attrs(*start, *fmt)),
+                                None => "<ol>".to_string(),
+                            },
+                            (false, _) => "<ul>".to_string(),
+                        });
                         stack.push((li.ordered, false));
                     }
                     if let Some(top) = stack.last_mut() {
@@ -1914,6 +1927,23 @@ fn blocks_html(blocks: &[FBlock], out: &mut String, depth: usize) {
     }
 }
 
+/// An ordered list's `type` (letters and roman numerals; decimal is the default) and `start`
+/// (when not 1).
+fn ol_attrs(start: u32, format: Option<NumFormat>) -> String {
+    let ty = match format {
+        Some(NumFormat::LowerLetter) => Some("a"),
+        Some(NumFormat::UpperLetter) => Some("A"),
+        Some(NumFormat::LowerRoman) => Some("i"),
+        Some(NumFormat::UpperRoman) => Some("I"),
+        _ => None,
+    };
+    let mut a = ty.map(|t| format!(" type=\"{t}\"")).unwrap_or_default();
+    if start != 1 {
+        a.push_str(&format!(" start=\"{start}\""));
+    }
+    a
+}
+
 fn close_list(stack: &mut Vec<(bool, bool)>, out: &mut String) {
     if let Some((ordered, li_open)) = stack.pop() {
         if li_open {
diff --git a/crates/formats/src/model.rs b/crates/formats/src/model.rs
--- a/crates/formats/src/model.rs
+++ b/crates/formats/src/model.rs
@@ -241,6 +241,12 @@ pub struct Spacing {
 pub struct Para {
     pub kind: Kind,
     pub list: Option<ListInfo>,
+    /// The number format of an ordered list item that a document's numbering gives it (letters,
+    /// roman numerals…): HTML writes it as the list's `type`, so a paste keeps "a." or "i.".
+    pub list_format: Option<NumFormat>,
+    /// For an ordered list item, the start and number format of each level above it, for a list
+    /// that opens straight at a deeper level (a copy that starts under an item it leaves out).
+    pub list_outer: Vec<(u32, Option<NumFormat>)>,
     pub align: Option<Align>,
     pub page_break: bool,
     /// Keep the paragraph on the same page as the next one (ODT `fo:keep-with-next`).
@@ -1030,7 +1036,9 @@ fn props_kind(doc: &Document, props: &ParaProps, blank: bool) -> (Kind, Option<L
     };
     let list = rp.numbering.and_then(|n| {
         let lv = doc.numbering.level(n.num, n.level)?;
-        Some(ListInfo { ordered: !matches!(lv.format, NumFormat::Bullet | NumFormat::None), level: n.level.min(8), start: lv.start })
+        // A list's own start override for the level wins over its definition's start.
+        let start = doc.numbering.num(n.num).and_then(|x| x.start_overrides.iter().find(|(l, _)| *l == n.level)).map_or(lv.start, |(_, s)| *s);
+        Some(ListInfo { ordered: !matches!(lv.format, NumFormat::Bullet | NumFormat::None), level: n.level.min(8), start })
     });
     (kind, list)
 }
@@ -1058,9 +1066,22 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
     let (kind, list) = para_kind(doc, p);
     let mut done: Vec<Para> = Vec::new();
     let borders = if kind == Kind::Rule { None } else { p.props.borders.filter(|b| b.any_visible()) };
+    let numbered = list.filter(|l| l.ordered).and_then(|_| doc.styles.resolve_para(&p.props).numbering);
+    let list_format = numbered.and_then(|n| doc.numbering.level(n.num, n.level).map(|lv| lv.format));
+    let list_outer = numbered.map_or_else(Vec::new, |n| {
+        (0..n.level.min(8))
+            .map(|k| {
+                let lv = doc.numbering.level(n.num, k);
+                let own = doc.numbering.num(n.num).and_then(|x| x.start_overrides.iter().find(|(l, _)| *l == k)).map(|(_, s)| *s);
+                (own.or(lv.map(|l| l.start)).unwrap_or(1), lv.map(|l| l.format))
+            })
+            .collect()
+    });
     let mut out = Para {
         kind,
         list,
+        list_format,
+        list_outer,
         align: p.props.align,
         page_break: p.props.page_break_before.unwrap_or(false),
         keep_next: p.props.keep_next.unwrap_or(false),
@@ -1092,6 +1113,8 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
                     let next = Para {
                         kind,
                         list: None,
+                        list_format: None,
+                        list_outer: Vec::new(),
                         align: p.props.align,
                         page_break: true,
                         keep_next: p.props.keep_next.unwrap_or(false),
