diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -380,6 +380,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
 
 Newest first.
 
+- **2026-10-24** · Table › Cell Options › Diagonal Lines… (`table.setCell` `diagonal`, `table.getCellDiagonal`): as in InDesign, a cell can carry a diagonal line from the top left corner to the bottom right, from the top right to the bottom left, or both, with the weight, type, color, tint, gap color and gap tint that cell edges take, drawn in front of the cell's contents or behind them. The lines draw on screen and in PDF and are saved in the native format. No readiness percentage change.
 - **2026-10-20** · Edit › Keyboard Shortcuts is craft-keymap's shared dialog: commands grouped by menu with Show and Filter, recording that asks before taking a key from another command, undo and redo, named shortcut sets (edits of Default become Custom; an imported set becomes a set of its own), an on-screen US keyboard, InDesign Show Set files reviewed before they apply, JSON export and an HTML summary. ui.json keeps the `shortcuts` map earlier versions read, for the active set. No readiness percentage change.
 - **2026-10-19** · Import InDesign Set… runs on craft-keymap's importer: a set's Window menu panel items (Layers F7, Pages F12, Swatches F5) and Tools items set the panel and tool shortcuts, all of an item's Default keys apply, and the report adds numeric keypad keys read as main keys and likely commands for unmatched items (`window.importShortcuts`). On InDesign's Windows defaults it matches 366 of 1,629 items (was 233) and changes 23 commands. No readiness percentage change.
 - **2026-10-19** · Keyboard shortcuts run on the shared craft-keymap crate. Panels take shortcuts, with InDesign's defaults where the key is free (Swatches F5, Color F6, Layers F7, Info F8, Stroke F10, Paragraph Styles F11, Character Styles Shift+F11, Pages F12, Align Shift+F7); tool keys are editable in Edit › Keyboard Shortcuts and `window.setShortcut`, and tools sharing a key take turns (B). Saved shortcuts load unchanged. No readiness percentage change.
diff --git a/crates/compose/src/table.rs b/crates/compose/src/table.rs
--- a/crates/compose/src/table.rs
+++ b/crates/compose/src/table.rs
@@ -33,6 +33,10 @@ pub struct PlacedCell {
     pub repeated: bool,
     /// Graphic cell content (drawn at `clip`'s top left, clipped to it).
     pub graphic: Option<designcraft_doc::Graphic>,
+    /// The cell's diagonal lines (Cell Options › Diagonal Lines), corner to corner of `rect`.
+    pub diagonals: Vec<StrokeSeg>,
+    /// Whether the diagonals are drawn over the cell's contents (otherwise under them).
+    pub diagonals_in_front: bool,
 }
 
 /// One cell edge (or the table border) to stroke.
@@ -87,6 +91,10 @@ impl TableFrag {
             c.rect = c.rect + v;
             c.clip = c.clip + v;
             c.origin += v;
+            for s in &mut c.diagonals {
+                s.a += v;
+                s.b += v;
+            }
         }
         for s in &mut self.strokes {
             s.a += v;
@@ -440,6 +448,14 @@ fn emit(
                 overset,
                 repeated,
                 graphic: cell.graphic.clone(),
+                diagonals: cell
+                    .diagonal
+                    .segments(rect)
+                    .into_iter()
+                    .filter(|_| cell.diagonal.stroke.is_visible())
+                    .map(|(a, b)| StrokeSeg { a, b, stroke: cell.diagonal.stroke.clone() })
+                    .collect(),
+                diagonals_in_front: cell.diagonal.in_front,
             });
         }
     }
@@ -1130,6 +1146,46 @@ mod border_tests {
         assert_eq!(right.stroke.weight, 2.0, "unoverridden right edge keeps the table border");
     }
 
+    #[test]
+    fn diagonal_lines_cross_the_placed_cell_corner_to_corner() {
+        let mut table = Table::new(77, 2, 2, 0, 0, 100.0);
+        let stroke = CellStroke { weight: 3.0, ..Default::default() };
+        let cell = table.cell_mut(0, 0).unwrap();
+        cell.col_span = 2;
+        cell.diagonal = designcraft_doc::CellDiagonal { lines: designcraft_doc::DiagonalLines::Crossed, stroke: stroke.clone(), in_front: false };
+        table.cell_mut(1, 0).unwrap().diagonal.lines = designcraft_doc::DiagonalLines::TopLeft;
+        table.cell_mut(1, 0).unwrap().diagonal.stroke.color = designcraft_color::swatch::NONE.into();
+        let composed = compose_table(table);
+        let fragment = &composed.frames[0].tables[0];
+        let merged = fragment.cells.iter().find(|c| (c.row, c.col) == (0, 0)).unwrap();
+        let r = merged.rect;
+        let ends: Vec<_> = merged.diagonals.iter().map(|s| (s.a, s.b)).collect();
+        assert_eq!(
+            ends,
+            [(Point::new(r.x0, r.y0), Point::new(r.x1, r.y1)), (Point::new(r.x1, r.y0), Point::new(r.x0, r.y1))],
+            "across the merged cell"
+        );
+        assert!(merged.diagonals.iter().all(|s| s.stroke == stroke) && !merged.diagonals_in_front);
+        assert!(fragment.cells.iter().filter(|c| c.row == 1).all(|c| c.diagonals.is_empty()), "none, or an invisible stroke, draws nothing");
+        assert!(fragment.strokes.iter().all(|s| s.a.x == s.b.x || s.a.y == s.b.y), "the edges stay edges");
+    }
+
+    #[test]
+    fn diagonal_lines_move_with_their_cell_in_a_bottom_justified_frame() {
+        let mut table = Table::new(77, 1, 1, 0, 0, 100.0);
+        table.cell_mut(0, 0).unwrap().diagonal.lines = designcraft_doc::DiagonalLines::TopLeft;
+        let mut doc = Document::new(&NewDocument::default());
+        let layer = doc.default_layer();
+        let (fid, sid) = doc.add_text_frame(SpreadRef::Doc(0), Rect::new(0.0, 0.0, 200.0, 300.0), layer, "", ParaFormat::default()).unwrap();
+        doc.item_mut(fid).unwrap().text_frame_mut().unwrap().options.vertical_justification = VerticalJustification::Bottom;
+        doc.story_mut(sid).unwrap().insert_table(0, table);
+        let composed = crate::compose_story(&doc, sid, &ComposeOptions::default());
+        let cell = &composed.frames[0].tables[0].cells[0];
+        assert!(cell.rect.y0 > 100.0, "the table sits at the bottom: {:?}", cell.rect);
+        let s = &cell.diagonals[0];
+        assert_eq!((s.a, s.b), (Point::new(cell.rect.x0, cell.rect.y0), Point::new(cell.rect.x1, cell.rect.y1)));
+    }
+
     #[test]
     fn merged_cell_keeps_explicit_border_suppression() {
         let mut table = Table::new(77, 2, 2, 0, 0, 100.0);
diff --git a/crates/doc/src/swatch_refs.rs b/crates/doc/src/swatch_refs.rs
--- a/crates/doc/src/swatch_refs.rs
+++ b/crates/doc/src/swatch_refs.rs
@@ -167,6 +167,7 @@ impl Names<'_> {
             for s in &mut c.strokes {
                 self.cell_stroke(s);
             }
+            self.cell_stroke(&mut c.diagonal.stroke);
         }
     }
     /// One story's own text, objects and tables (cell and note text are visited separately).
@@ -231,7 +232,7 @@ impl Document {
 
     /// Point every reference to swatch `from` at `to`: tints based on it, colour groups, inks,
     /// styles, page items (fills, strokes, gaps, effects, column rules), text, kenten and ruby
-    /// colours, rules, shading, borders and border gaps, tables (with stroke gaps), footnotes,
+    /// colours, rules, shading, borders and border gaps, tables (with stroke gaps and diagonal lines), footnotes,
     /// gradient stops,
     /// endnotes and the footnote rule. The swatch itself is renamed by the
     /// caller.
diff --git a/crates/doc/src/table.rs b/crates/doc/src/table.rs
--- a/crates/doc/src/table.rs
+++ b/crates/doc/src/table.rs
@@ -89,6 +89,55 @@ impl Default for CellStroke {
     }
 }
 
+/// Cell Options › Diagonal Lines: the diagonals drawn across a cell.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum DiagonalLines {
+    #[default]
+    None,
+    /// From the top left corner to the bottom right corner.
+    TopLeft,
+    /// From the top right corner to the bottom left corner.
+    TopRight,
+    /// Both diagonals.
+    Crossed,
+}
+
+/// A cell's diagonal lines, their stroke, and whether they are drawn in front of the cell's
+/// contents (Draw › Diagonal In Front) or behind them (Content In Front).
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase", default)]
+pub struct CellDiagonal {
+    pub lines: DiagonalLines,
+    pub stroke: CellStroke,
+    pub in_front: bool,
+}
+
+impl Default for CellDiagonal {
+    fn default() -> Self {
+        CellDiagonal { lines: DiagonalLines::None, stroke: CellStroke::default(), in_front: true }
+    }
+}
+
+impl CellDiagonal {
+    pub fn is_default(&self) -> bool {
+        *self == CellDiagonal::default()
+    }
+
+    /// The diagonals' ends in the cell rectangle `r`, as (start, end) pairs.
+    pub fn segments(&self, r: designcraft_geom::Rect) -> Vec<(designcraft_geom::Point, designcraft_geom::Point)> {
+        use designcraft_geom::Point;
+        let down = (Point::new(r.x0, r.y0), Point::new(r.x1, r.y1));
+        let up = (Point::new(r.x1, r.y0), Point::new(r.x0, r.y1));
+        match self.lines {
+            DiagonalLines::None => vec![],
+            DiagonalLines::TopLeft => vec![down],
+            DiagonalLines::TopRight => vec![up],
+            DiagonalLines::Crossed => vec![down, up],
+        }
+    }
+}
+
 impl CellStroke {
     pub fn none() -> Self {
         CellStroke { weight: 0.0, ..Default::default() }
@@ -216,6 +265,9 @@ pub struct Cell {
     /// content box, origin at the box's top left).
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub graphic: Option<crate::Graphic>,
+    /// Cell Options › Diagonal Lines.
+    #[serde(default, skip_serializing_if = "CellDiagonal::is_default")]
+    pub diagonal: CellDiagonal,
 }
 
 impl Default for Cell {
@@ -237,6 +289,7 @@ impl Default for Cell {
             stroke_defined: [false; 4],
             style: String::new(),
             graphic: None,
+            diagonal: CellDiagonal::default(),
         }
     }
 }
@@ -1509,6 +1562,29 @@ mod tests {
         assert_eq!(back, explicit);
         assert_eq!(back.blank_like().border_overrides, explicit.border_overrides);
     }
+
+    #[test]
+    fn cell_diagonal_lines_roundtrip_and_run_corner_to_corner() {
+        let plain = serde_json::to_value(Cell::default()).unwrap();
+        assert!(plain.get("diagonal").is_none(), "a cell without diagonal lines saves none");
+        let diagonal = CellDiagonal { lines: DiagonalLines::Crossed, stroke: CellStroke { weight: 2.0, ..Default::default() }, in_front: false };
+        let cell = Cell { diagonal: diagonal.clone(), ..Cell::default() };
+        let back: Cell = serde_json::from_str(&serde_json::to_string(&cell).unwrap()).unwrap();
+        assert_eq!(back.diagonal, diagonal);
+        assert_eq!(back.blank_like().diagonal, diagonal, "new cells take the cell's formatting");
+        let r = designcraft_geom::Rect::new(10.0, 20.0, 110.0, 70.0);
+        let p = designcraft_geom::Point::new;
+        let down = (p(10.0, 20.0), p(110.0, 70.0));
+        let up = (p(110.0, 20.0), p(10.0, 70.0));
+        for (lines, want) in [
+            (DiagonalLines::None, vec![]),
+            (DiagonalLines::TopLeft, vec![down]),
+            (DiagonalLines::TopRight, vec![up]),
+            (DiagonalLines::Crossed, vec![down, up]),
+        ] {
+            assert_eq!(CellDiagonal { lines, ..Default::default() }.segments(r), want, "{lines:?}");
+        }
+    }
 }
 
 #[cfg(test)]
diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs
--- a/crates/engine/src/cmd/style.rs
+++ b/crates/engine/src/cmd/style.rs
@@ -3181,6 +3181,7 @@ mod swatch_options_tests {
         s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap();
         s.execute("table.select", &json!({"what": "table"})).unwrap();
         s.execute("table.setCell", &json!({"stroke": {"weight": 2, "type": "dashed", "gapColor": "Brand", "edges": ["all"]}})).unwrap();
+        s.execute("table.setCell", &json!({"diagonal": {"lines": "crossed", "type": "dashed", "color": "Brand", "gapColor": "Brand"}})).unwrap();
         s.execute("swatch.newColorGroup", &json!({"name": "Brand Colors", "swatches": ["Brand"]})).unwrap();
         s.execute("style.character.create", &json!({"name": "Accent"})).unwrap();
         s.edit(|d, _| {
@@ -3226,6 +3227,7 @@ mod swatch_options_tests {
             basic.chars.ruby_stroke.as_deref() == Some(name),
             d.styles.char_style("Accent").unwrap().chars.kenten_fill.as_deref() == Some(name),
             story.tables.values().any(|t| t.cells.iter().all(|c| c.strokes.iter().all(|e| e.gap_color == name))),
+            story.tables.values().any(|t| t.cells.iter().all(|c| c.diagonal.stroke.color == name && c.diagonal.stroke.gap_color == name)),
         ]
     }
 
@@ -3238,10 +3240,10 @@ mod swatch_options_tests {
         let d = &s.doc().unwrap().doc;
         assert!(d.swatch("Brand").is_none());
         assert!(matches!(d.swatch("Signal Orange").unwrap().value, SwatchValue::Color { color_type: designcraft_color::ColorType::Spot, .. }));
-        assert_eq!(uses(&s, id, sid, "Signal Orange"), vec![true; 12]);
+        assert_eq!(uses(&s, id, sid, "Signal Orange"), vec![true; 13]);
         s.execute("edit.undo", &json!({})).unwrap();
         assert!(s.doc().unwrap().doc.swatch("Brand").is_some());
-        assert_eq!(uses(&s, id, sid, "Brand"), vec![true; 12]);
+        assert_eq!(uses(&s, id, sid, "Brand"), vec![true; 13]);
     }
 
     #[test]
@@ -3262,7 +3264,7 @@ mod swatch_options_tests {
             assert!(s.execute("swatch.options", &p).is_err(), "{p}");
         }
         assert_eq!(s.doc().unwrap().doc.swatches, before.swatches);
-        assert_eq!(uses(&s, id, sid, "Brand"), vec![true; 12]);
+        assert_eq!(uses(&s, id, sid, "Brand"), vec![true; 13]);
         // [Paper]'s colour can change; its name can't.
         s.execute("swatch.options", &json!({"name": "[Paper]", "to": "[Paper]", "color": "#fff8e0"})).unwrap();
     }
@@ -3272,7 +3274,7 @@ mod swatch_options_tests {
         let (mut s, id, sid) = brand_doc();
         let r = s.execute("swatch.options", &json!({"name": "Brand", "nameWithValue": true, "color": {"c": 10, "m": 20, "y": 30, "k": 40}})).unwrap();
         assert_eq!(r["name"], "C=10 M=20 Y=30 K=40");
-        assert_eq!(uses(&s, id, sid, "C=10 M=20 Y=30 K=40"), vec![true; 12]);
+        assert_eq!(uses(&s, id, sid, "C=10 M=20 Y=30 K=40"), vec![true; 13]);
         // Later colour edits keep the name in step until the swatch is named by hand.
         let r = s.execute("swatch.options", &json!({"name": "C=10 M=20 Y=30 K=40", "color": {"c": 0, "m": 0, "y": 0, "k": 50}})).unwrap();
         assert_eq!(r["name"], "C=0 M=0 Y=0 K=50");
diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs
--- a/crates/engine/src/cmd/table.rs
+++ b/crates/engine/src/cmd/table.rs
@@ -9,8 +9,8 @@
 use designcraft_compose as compose;
 use designcraft_doc::story::floor_char_boundary;
 use designcraft_doc::{
-    AltFills, CellAddr, CellRange, CellStroke, Document, RowHeightMode, RowKind, Selection, StoryId, StrokeType, Table, TableSel, TextSel,
-    VerticalJustification,
+    AltFills, CellAddr, CellRange, CellStroke, DiagonalLines, Document, RowHeightMode, RowKind, Selection, StoryId, StrokeType, Table, TableSel,
+    TextSel, VerticalJustification,
 };
 use serde_json::{Value, json};
 
@@ -297,7 +297,7 @@ fn table_specs() -> Vec<CommandSpec> {
             "Cell Options",
             [],
             None,
-            "{fill?: swatch, tint?: 0..1, insets?: n | [t,l,b,r], vj? | verticalJustification?: top|center|bottom|justify, firstBaseline?: ascent|capHeight|leading|xHeight|fixed|emboxHeight, firstBaselineMin?: pt, rotation?: 0|90|180|270 (Cell Options › Text › Rotation; kept and written back to IDML, composed horizontally for now), text?, stroke?: {weight?, color?: swatch, tint?: 0..1, type?: solid|dashed|dotted|thickThin|thinThick|thinThin|thickThick|thinThickThin|thickThinThick|wavy|hashed | {kind:…}, gapColor?: swatch, gapTint?: 0..1, edges?: name | [names] (all|outer|inner|top|left|bottom|right|innerHorizontal|innerVertical; default all)}, story?, table?, rows?: [a, b] | row?, cols?: [a, b] | col? (the target cells; default: the selection)} — top/left/bottom/right are the target range's outer sides, innerHorizontal/innerVertical the lines between its rows/columns",
+            "{fill?: swatch, tint?: 0..1, insets?: n | [t,l,b,r], vj? | verticalJustification?: top|center|bottom|justify, firstBaseline?: ascent|capHeight|leading|xHeight|fixed|emboxHeight, firstBaselineMin?: pt, rotation?: 0|90|180|270 (Cell Options › Text › Rotation; kept and written back to IDML, composed horizontally for now), text?, stroke?: {weight?, color?: swatch, tint?: 0..1, type?: solid|dashed|dotted|thickThin|thinThick|thinThin|thickThick|thinThickThin|thickThinThick|wavy|hashed | {kind:…}, gapColor?: swatch, gapTint?: 0..1, edges?: name | [names] (all|outer|inner|top|left|bottom|right|innerHorizontal|innerVertical; default all)}, diagonal?: {lines?: none|topLeft|topRight|crossed (Cell Options › Diagonal Lines: top left to bottom right, top right to bottom left, or both), weight?, color?, tint?, type?, gapColor?, gapTint? (as stroke), inFront?: bool (true: Diagonal In Front, false: Content In Front)}, story?, table?, rows?: [a, b] | row?, cols?: [a, b] | col? (the target cells; default: the selection)} — top/left/bottom/right are the target range's outer sides, innerHorizontal/innerVertical the lines between its rows/columns",
             in_table,
             set_cell
         ),
@@ -310,6 +310,15 @@ fn table_specs() -> Vec<CommandSpec> {
             in_table_or_ids,
             get_cell_stroke
         ),
+        cmd!(
+            query "table.getCellDiagonal",
+            "Get Cell Diagonal Lines",
+            [],
+            None,
+            "{story?, table?, rows?, cols?} → {lines, weight, color, tint, type, gapColor, gapTint, inFront} of the target cells' diagonal lines; a value that differs between them is null",
+            in_table_or_ids,
+            get_cell_diagonal
+        ),
         cmd!(
             "table.setRowHeight",
             "Row Height",
@@ -925,6 +934,48 @@ impl StrokeEdit {
     }
 }
 
+/// A `diagonal` edit of `table.setCell`: the lines, their stroke attributes and Draw.
+struct DiagonalEdit {
+    lines: Option<DiagonalLines>,
+    stroke: StrokeEdit,
+    in_front: Option<bool>,
+}
+
+impl DiagonalEdit {
+    fn parse(doc: &Document, v: &Value, cmd: &str) -> Result<DiagonalEdit> {
+        if !v.is_object() {
+            return Err(bad(cmd, "diagonal must be an object"));
+        }
+        let lines = match v.get("lines").filter(|l| !l.is_null()) {
+            Some(Value::String(name)) => match name.as_str() {
+                "none" => Some(DiagonalLines::None),
+                "topLeft" => Some(DiagonalLines::TopLeft),
+                "topRight" => Some(DiagonalLines::TopRight),
+                "crossed" => Some(DiagonalLines::Crossed),
+                other => return Err(bad(cmd, format!("unknown diagonal lines `{other}`: none, topLeft, topRight or crossed"))),
+            },
+            Some(other) => return Err(bad(cmd, format!("diagonal lines must be a string, not {other}"))),
+            None => None,
+        };
+        let in_front = match v.get("inFront").filter(|f| !f.is_null()) {
+            Some(Value::Bool(b)) => Some(*b),
+            Some(other) => return Err(bad(cmd, format!("inFront must be true or false, not {other}"))),
+            None => None,
+        };
+        Ok(DiagonalEdit { lines, stroke: StrokeEdit::parse(doc, v, cmd)?, in_front })
+    }
+
+    fn apply(&self, d: &mut designcraft_doc::CellDiagonal) {
+        if let Some(l) = self.lines {
+            d.lines = l;
+        }
+        d.stroke = self.stroke.apply(d.stroke.clone());
+        if let Some(f) = self.in_front {
+            d.in_front = f;
+        }
+    }
+}
+
 /// The stroke drawn on side `side` of the owner cell at `(r, c)` (where that side meets its
 /// first neighbour), as rendering resolves it; the cell's own copy if the side has no stretch.
 fn drawn_side(t: &Table, owners: &[(usize, usize)], (r, c): (usize, usize), side: usize) -> Option<CellStroke> {
@@ -985,6 +1036,10 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
         Some(sv) => Some((StrokeEdit::parse(doc, sv, ID)?, EdgeSel::parse(sv.get("edges"), ID)?)),
         None => None,
     };
+    let diagonal = match p.get("diagonal").filter(|v| !v.is_null()) {
+        Some(dv) => Some(DiagonalEdit::parse(doc, dv, ID)?),
+        None => None,
+    };
     edit_table(s, &g, ID, |t| {
         let owners = t.owners();
         let nc = t.ncols();
@@ -1037,6 +1092,9 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
                 let len = cell.text.len();
                 cell.text.replace(0..len, tx);
             }
+            if let Some(edit) = &diagonal {
+                edit.apply(&mut cell.diagonal);
+            }
             if let Some((edit, _)) = &stroke {
                 for i in 0..4 {
                     if let Some(base) = bases.remove(&(r, c, i)) {
@@ -1141,6 +1199,31 @@ fn get_cell_stroke(s: &mut Session, p: &Value) -> Result<Value> {
     Ok(Value::Object(out))
 }
 
+/// The diagonal lines of the target cells and their stroke; values that differ are null.
+fn get_cell_diagonal(s: &mut Session, p: &Value) -> Result<Value> {
+    const ID: &str = "table.getCellDiagonal";
+    let g = target(s, p, ID)?;
+    let st = s.doc()?;
+    let t = st.doc.story(g.story).and_then(|x| x.tables.get(&g.table)).ok_or_else(|| bad(ID, "no table"))?;
+    let keys = ["lines", "weight", "color", "tint", "type", "gapColor", "gapTint", "inFront"];
+    let mut slots: [Option<Value>; 8] = Default::default();
+    for (r, c) in t.selected_owners(g.range) {
+        let Some(cell) = t.cell(r, c) else { continue };
+        let d = &cell.diagonal;
+        let e = &d.stroke;
+        let values =
+            [json!(d.lines), json!(e.weight), json!(e.color), json!(e.tint), json!(e.kind), json!(e.gap_color), json!(e.gap_tint), json!(d.in_front)];
+        for (slot, v) in slots.iter_mut().zip(values) {
+            match slot {
+                None => *slot = Some(v),
+                Some(cur) if *cur != v => *cur = Value::Null,
+                _ => {}
+            }
+        }
+    }
+    Ok(Value::Object(keys.iter().zip(slots).map(|(k, v)| ((*k).to_string(), v.unwrap_or(Value::Null))).collect()))
+}
+
 /// Only saturation needs rebasing. Preserve every distinct priority's order and keep
 /// zero/negative priorities untouched; the work is bounded by the table's edges.
 fn next_cell_stroke_priority(t: &mut Table) -> i32 {
@@ -1574,7 +1657,7 @@ fn get(s: &mut Session, p: &Value) -> Result<Value> {
         .filter_map(|(r, c)| {
             let cell = t.cell(r, c)?;
             Some(json!({"row": r, "col": c, "rowSpan": cell.row_span, "colSpan": cell.col_span, "text": cell.text.text, "fill": cell.fill,
-                "insets": cell.insets, "vj": cell.vj, "firstBaseline": cell.first_baseline, "firstBaselineMin": cell.first_baseline_min, "rotation": cell.rotation, "style": cell.style}))
+                "insets": cell.insets, "vj": cell.vj, "firstBaseline": cell.first_baseline, "firstBaselineMin": cell.first_baseline_min, "rotation": cell.rotation, "style": cell.style, "diagonal": cell.diagonal}))
         })
         .collect();
     Ok(json!({
@@ -1877,6 +1960,48 @@ mod style_tests {
         assert_eq!(t.cell(1, 0).unwrap().first_baseline, designcraft_doc::FirstBaseline::Ascent);
     }
 
+    #[test]
+    fn diagonal_lines_are_a_cell_option_of_the_target_cells() {
+        use designcraft_doc::DiagonalLines;
+        let mut s = Session::new();
+        s.execute("file.new", &json!({})).unwrap();
+        let r = s.execute("frame.create", &json!({"rect": [72, 72, 500, 400], "content": "text", "text": ""})).unwrap();
+        let sid = designcraft_doc::StoryId(r["story"].as_u64().unwrap());
+        s.execute("text.select", &json!({"story": sid.0, "anchor": 0, "focus": 0})).unwrap();
+        s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap();
+        let fresh = s.execute("table.getCellDiagonal", &json!({})).unwrap();
+        assert_eq!((&fresh["lines"], &fresh["weight"], &fresh["inFront"]), (&json!("none"), &json!(1.0), &json!(true)));
+        let p = json!({"rows": [0, 0], "cols": [0, 1], "diagonal": {"lines": "crossed", "weight": 2, "color": "[Paper]", "type": "dotted", "inFront": false}});
+        s.execute("table.setCell", &p).unwrap();
+        let cell = |s: &Session, r: usize, c: usize| {
+            s.doc().unwrap().doc.story(sid).unwrap().tables.values().next().unwrap().cell(r, c).unwrap().diagonal.clone()
+        };
+        let d = cell(&s, 0, 1);
+        assert_eq!(
+            (d.lines, d.stroke.weight, d.stroke.color.as_str(), &d.stroke.kind, d.in_front),
+            (DiagonalLines::Crossed, 2.0, "[Paper]", &designcraft_doc::StrokeType::Dotted, false)
+        );
+        assert!(cell(&s, 1, 0).is_default(), "other cells keep none");
+        // Only the attributes given change.
+        s.execute("table.setCell", &json!({"row": 0, "col": 0, "diagonal": {"lines": "topRight"}})).unwrap();
+        let d = cell(&s, 0, 0);
+        assert_eq!((d.lines, d.stroke.weight, d.in_front), (DiagonalLines::TopRight, 2.0, false));
+        let got = s.execute("table.getCellDiagonal", &json!({"rows": [0, 0], "cols": [0, 1]})).unwrap();
+        assert_eq!(
+            (&got["lines"], &got["weight"], &got["color"], &got["inFront"]),
+            (&serde_json::Value::Null, &json!(2.0), &json!("[Paper]"), &json!(false)),
+            "{got}"
+        );
+        let shown = s.execute("table.get", &json!({})).unwrap()["cells"][1]["diagonal"].clone();
+        assert_eq!((&shown["lines"], &shown["inFront"]), (&json!("crossed"), &json!(false)));
+        for bad in [json!({"lines": "x"}), json!({"lines": 1}), json!({"inFront": "yes"}), json!({"weight": -1}), json!("crossed")] {
+            assert!(s.execute("table.setCell", &json!({"row": 0, "col": 0, "diagonal": bad})).is_err(), "{bad}");
+        }
+        assert_eq!(cell(&s, 0, 0).lines, DiagonalLines::TopRight, "a refused edit changes nothing");
+        s.execute("edit.undo", &json!({})).unwrap();
+        assert_eq!(cell(&s, 0, 0).lines, DiagonalLines::Crossed, "one undo step per edit");
+    }
+
     #[test]
     fn cell_and_table_styles_apply_and_follow_edits() {
         let mut s = Session::new();
diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs
--- a/crates/pdf/src/tests.rs
+++ b/crates/pdf/src/tests.rs
@@ -236,6 +236,29 @@ fn tables_export_cell_text() {
     }
 }
 
+#[test]
+fn cell_diagonal_lines_export_in_front_of_or_behind_the_cell_text() {
+    let export = |lines, in_front| {
+        let mut d = Document::new(&NewDocument::default());
+        let lid = d.default_layer();
+        let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 400.0), lid, "", ParaFormat::default()).unwrap();
+        let mut t = designcraft_doc::Table::new(9, 1, 1, 0, 0, 200.0);
+        let cell = t.cell_mut(0, 0).unwrap();
+        cell.text.insert(0, "Ink");
+        let stroke = designcraft_doc::CellStroke { weight: 3.0, ..Default::default() };
+        cell.diagonal = designcraft_doc::CellDiagonal { lines, stroke, in_front };
+        d.story_mut(sid).unwrap().insert_table(0, t);
+        let bytes = export_pdf(&d, &Cache::new(), &PdfOptions::default()).unwrap();
+        // The table's edges are 1 pt; the diagonals are the 3 pt strokes.
+        paint_order(&bytes).into_iter().filter(|(op, w)| op.ends_with("text") || *w == 3.0).map(|(op, _)| op).collect::<Vec<_>>()
+    };
+    use designcraft_doc::DiagonalLines;
+    let (text, line) = ("fill text".to_string(), "stroke path".to_string());
+    assert_eq!(export(DiagonalLines::None, true), [text.clone(), text.clone(), text.clone()]);
+    assert_eq!(export(DiagonalLines::Crossed, true), [text.clone(), text.clone(), text.clone(), line.clone(), line.clone()], "Diagonal In Front");
+    assert_eq!(export(DiagonalLines::TopLeft, false), [line.clone(), text.clone(), text.clone(), text.clone()], "Content In Front");
+}
+
 #[test]
 fn drop_cap_text_extracts_in_order() {
     let mut d = Document::new(&NewDocument::default());
diff --git a/crates/pdf/src/text.rs b/crates/pdf/src/text.rs
--- a/crates/pdf/src/text.rs
+++ b/crates/pdf/src/text.rs
@@ -345,72 +345,88 @@ impl Exporter<'_> {
                 }
             }
             for c in &t.cells {
+                if !c.diagonals_in_front {
+                    self.cell_diagonals(s, c);
+                }
                 self.table_cell(s, c);
+                if c.diagonals_in_front {
+                    self.cell_diagonals(s, c);
+                }
             }
             for seg in &t.strokes {
-                let st = &seg.stroke;
-                let Some(col) = self.swatch_color(&st.color, st.tint) else { continue };
-                let kind = self.doc.stroke_kind(&st.kind);
-                let mut bp = BezPath::new();
-                bp.move_to(seg.a);
-                bp.line_to(seg.b);
-                let Some(p) = to_path(&bp) else { continue };
-                // Butt-ended fills close the table's corners the way square caps do.
-                let (ca, cb) = t.corner_capped(seg);
-                let mut capped = BezPath::new();
-                capped.move_to(ca);
-                capped.line_to(cb);
-                let Some(cp) = to_path(&capped) else { continue };
-                // Gap colour under dashes, dots and stripes: the whole edge, solid.
-                if !matches!(kind, designcraft_doc::StrokeType::Solid)
-                    && st.gap_color != designcraft_color::swatch::NONE
-                    && let Some(gap) = self.swatch_color(&st.gap_color, st.gap_tint)
-                {
-                    s.set_fill(None);
-                    s.set_stroke(Some(Stroke {
-                        paint: gap.into(),
-                        width: st.weight as f32,
-                        line_cap: krilla::paint::LineCap::Butt,
-                        ..Default::default()
-                    }));
-                    s.draw_path(&cp);
-                    s.set_stroke(None);
-                }
-                // Stripes, wavy and hash types are fills along the edge.
-                if let Some(o) = kind.outline(&capped, st.weight, 0.05)
-                    && let Some(op) = to_path(&o)
-                {
-                    s.set_stroke(None);
-                    s.set_fill(Some(solid_fill(col, 1.0)));
-                    s.draw_path(&op);
-                    s.set_fill(None);
-                    continue;
-                }
-                let (cap, dash) = match &kind {
-                    designcraft_doc::StrokeType::Dashed { pattern }
-                        if designcraft_doc::StrokeType::expandable_dashes(pattern, seg.a.distance(seg.b)) =>
-                    {
-                        let mut pat: Vec<f32> = pattern.iter().map(|v| v.max(0.0) as f32).collect();
-                        if pat.len() % 2 == 1 {
-                            pat.extend(pat.clone());
-                        }
-                        (krilla::paint::LineCap::Butt, Some(krilla::paint::StrokeDash { array: pat, offset: 0.0 }))
-                    }
-                    designcraft_doc::StrokeType::Dotted
-                        if designcraft_doc::StrokeType::expandable_dashes(&[0.0, st.weight * 2.0], seg.a.distance(seg.b)) =>
-                    {
-                        (krilla::paint::LineCap::Round, Some(krilla::paint::StrokeDash { array: vec![0.0, (st.weight * 2.0) as f32], offset: 0.0 }))
-                    }
-                    _ => (krilla::paint::LineCap::Square, None),
-                };
-                s.set_fill(None);
-                s.set_stroke(Some(Stroke { paint: col.into(), width: st.weight as f32, line_cap: cap, dash, ..Default::default() }));
-                s.draw_path(&p);
-                s.set_stroke(None);
+                self.cell_stroke(s, seg, t.corner_capped(seg));
             }
         }
     }
 
+    /// A cell's diagonal lines, clipped to the cell so their caps stay inside it.
+    fn cell_diagonals(&mut self, s: &mut Surface, c: &designcraft_compose::PlacedCell) {
+        if c.diagonals.is_empty() {
+            return;
+        }
+        let Some(clip) = to_path(&c.rect.to_path(0.1)) else { return };
+        s.push_clip_path(&clip, &krilla::paint::FillRule::NonZero);
+        for seg in &c.diagonals {
+            self.cell_stroke(s, seg, (seg.a, seg.b));
+        }
+        s.pop();
+    }
+
+    /// One cell edge or diagonal: the gap color, then the stroke type. `capped` holds the ends the
+    /// butt-ended fills (stripes, wavy, hash, the gap color) run between.
+    fn cell_stroke(&mut self, s: &mut Surface, seg: &designcraft_compose::StrokeSeg, capped: (designcraft_geom::Point, designcraft_geom::Point)) {
+        let st = &seg.stroke;
+        let Some(col) = self.swatch_color(&st.color, st.tint) else { return };
+        let kind = self.doc.stroke_kind(&st.kind);
+        let mut bp = BezPath::new();
+        bp.move_to(seg.a);
+        bp.line_to(seg.b);
+        let Some(p) = to_path(&bp) else { return };
+        // Butt-ended fills close the table's corners the way square caps do.
+        let (ca, cb) = capped;
+        let mut capped = BezPath::new();
+        capped.move_to(ca);
+        capped.line_to(cb);
+        let Some(cp) = to_path(&capped) else { return };
+        // Gap colour under dashes, dots and stripes: the whole edge, solid.
+        if !matches!(kind, designcraft_doc::StrokeType::Solid)
+            && st.gap_color != designcraft_color::swatch::NONE
+            && let Some(gap) = self.swatch_color(&st.gap_color, st.gap_tint)
+        {
+            s.set_fill(None);
+            s.set_stroke(Some(Stroke { paint: gap.into(), width: st.weight as f32, line_cap: krilla::paint::LineCap::Butt, ..Default::default() }));
+            s.draw_path(&cp);
+            s.set_stroke(None);
+        }
+        // Stripes, wavy and hash types are fills along the edge.
+        if let Some(o) = kind.outline(&capped, st.weight, 0.05)
+            && let Some(op) = to_path(&o)
+        {
+            s.set_stroke(None);
+            s.set_fill(Some(solid_fill(col, 1.0)));
+            s.draw_path(&op);
+            s.set_fill(None);
+            return;
+        }
+        let (cap, dash) = match &kind {
+            designcraft_doc::StrokeType::Dashed { pattern } if designcraft_doc::StrokeType::expandable_dashes(pattern, seg.a.distance(seg.b)) => {
+                let mut pat: Vec<f32> = pattern.iter().map(|v| v.max(0.0) as f32).collect();
+                if pat.len() % 2 == 1 {
+                    pat.extend(pat.clone());
+                }
+                (krilla::paint::LineCap::Butt, Some(krilla::paint::StrokeDash { array: pat, offset: 0.0 }))
+            }
+            designcraft_doc::StrokeType::Dotted if designcraft_doc::StrokeType::expandable_dashes(&[0.0, st.weight * 2.0], seg.a.distance(seg.b)) => {
+                (krilla::paint::LineCap::Round, Some(krilla::paint::StrokeDash { array: vec![0.0, (st.weight * 2.0) as f32], offset: 0.0 }))
+            }
+            _ => (krilla::paint::LineCap::Square, None),
+        };
+        s.set_fill(None);
+        s.set_stroke(Some(Stroke { paint: col.into(), width: st.weight as f32, line_cap: cap, dash, ..Default::default() }));
+        s.draw_path(&p);
+        s.set_stroke(None);
+    }
+
     fn fill_rect(&self, s: &mut Surface, r: Rect, c: krilla::color::Color) {
         self.fill_path(s, &r.to_path(0.1), c);
     }
diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs
--- a/crates/render/src/lib.rs
+++ b/crates/render/src/lib.rs
@@ -2093,6 +2093,55 @@ mod tests {
         assert!(column.iter().any(|p| ink(*p)) && column.iter().any(|p| cyan(*p)), "stripes and gap: {column:?}");
     }
 
+    #[test]
+    fn cell_diagonal_lines_draw_in_front_of_or_behind_the_cell_text() {
+        let render = |lines, in_front| {
+            let mut d = Document::new(&NewDocument::default());
+            let lid = d.default_layer();
+            let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 400.0), lid, "", ParaFormat::default()).unwrap();
+            let mut t = designcraft_doc::Table::new(1, 1, 1, 0, 0, 200.0);
+            t.rows[0].height = 60.0;
+            t.rows[0].mode = designcraft_doc::RowHeightMode::Exactly;
+            let cell = t.cell_mut(0, 0).unwrap();
+            cell.text.insert(0, "WWWWWWWWWWWWWWWWWWWW");
+            cell.text.format_chars(0..20, |f| f.over.size = Some(30.0));
+            let stroke = designcraft_doc::CellStroke { weight: 6.0, color: "C=100 M=0 Y=0 K=0".into(), ..Default::default() };
+            cell.diagonal = designcraft_doc::CellDiagonal { lines, stroke, in_front };
+            d.story_mut(sid).unwrap().insert_table(0, t);
+            let cache = Cache::new();
+            let rect = cache.get(&d, sid, None).frames[0].tables[0].cell(0, 0).unwrap().rect;
+            let mut r = Renderer::new();
+            r.threads = 0;
+            let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
+            let cyan = |p: [u8; 4]| p[0] < 80 && p[2] > 150;
+            let inside = |x: f64, y: f64| x > rect.x0 + 4.0 && x < rect.x1 - 4.0 && y > rect.y0 + 4.0 && y < rect.y1 - 4.0;
+            let count = |a: (f64, f64), b: (f64, f64)| {
+                (0..=200)
+                    .map(|i| (a.0 + (b.0 - a.0) * i as f64 / 200.0, a.1 + (b.1 - a.1) * i as f64 / 200.0))
+                    .filter(|&(x, y)| inside(x, y) && cyan(img.pixel(x as u32, y as u32)))
+                    .count()
+            };
+            let down = count((rect.x0, rect.y0), (rect.x1, rect.y1));
+            let up = count((rect.x1, rect.y0), (rect.x0, rect.y1));
+            // Just past the corner, outside the cell.
+            let outside = img.pixel((rect.x1 + 2.0) as u32, (rect.y1 + 2.0) as u32);
+            (down, up, cyan(outside))
+        };
+        use designcraft_doc::DiagonalLines;
+        assert_eq!(render(DiagonalLines::None, true), (0, 0, false));
+        let (down, up, outside) = render(DiagonalLines::Crossed, true);
+        assert!(down > 150 && up > 150 && !outside, "crossed: {down} {up} {outside}");
+        let (down, up, _) = render(DiagonalLines::TopLeft, true);
+        // The other diagonal meets this one only where they cross, near the middle.
+        assert!(down > 150 && up < 40, "top left to bottom right: {down} {up}");
+        let (down, up, _) = render(DiagonalLines::TopRight, true);
+        assert!(down < 40 && up > 150, "top right to bottom left: {down} {up}");
+        // The text covers part of the line drawn behind it.
+        let (behind, _, _) = render(DiagonalLines::TopLeft, false);
+        let (front, _, _) = render(DiagonalLines::TopLeft, true);
+        assert!(behind + 10 < front, "content in front: {behind} vs {front}");
+    }
+
     #[test]
     fn degenerate_dash_patterns_draw_solid_and_fill_types_close_table_corners() {
         let fine = designcraft_doc::StrokeType::Dashed { pattern: vec![1e-6, 1e-6] };
diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs
--- a/crates/render/src/text.rs
+++ b/crates/render/src/text.rs
@@ -483,6 +483,9 @@ impl Renderer {
                 }
             }
             for c in &t.cells {
+                if !c.diagonals_in_front {
+                    draw_cell_diagonals(ctx, f, xf, c);
+                }
                 if let Some(g) = &c.graphic {
                     ctx.set_transform(f.view * xf);
                     ctx.push_clip_layer(&c.clip.to_path(0.1));
@@ -492,36 +495,58 @@ impl Renderer {
                 } else if let Some(cft) = c.text.frames.first() {
                     self.draw_text(ctx, f, &c.text, cft, xf * Affine::translate(c.origin.to_vec2()));
                 }
+                if c.diagonals_in_front {
+                    draw_cell_diagonals(ctx, f, xf, c);
+                }
             }
             ctx.set_transform(f.view * xf);
             for s in &t.strokes {
-                let Some(col) = doc.resolve_color(&s.stroke.color, s.stroke.tint) else { continue };
-                let kind = doc.stroke_kind(&s.stroke.kind);
-                let line = kurbo::Line::new(s.a, s.b).to_path(0.1);
-                // Butt-ended fills close the table's corners the way square caps do.
-                let (ca, cb) = t.corner_capped(s);
-                let capped = kurbo::Line::new(ca, cb).to_path(0.1);
-                // Gap colour under dashes, dots and stripes: the whole edge, solid.
-                if !matches!(kind, designcraft_doc::StrokeType::Solid)
-                    && let Some(gap) = doc.resolve_color(&s.stroke.gap_color, s.stroke.gap_tint)
-                {
-                    ctx.set_paint(color_of(&gap, 1.0));
-                    ctx.set_stroke(kurbo::Stroke::new(s.stroke.weight).with_caps(kurbo::Cap::Butt));
-                    ctx.stroke_path(&capped);
-                }
-                ctx.set_paint(color_of(&col, 1.0));
-                // Stripes, wavy and hash types are fills along the edge.
-                if let Some(o) = kind.outline(&capped, s.stroke.weight, 0.05 * f.px) {
-                    ctx.fill_path(&o);
-                    continue;
-                }
-                ctx.set_stroke(cell_stroke(&kind, s.stroke.weight, s.a.distance(s.b)));
-                ctx.stroke_path(&line);
+                draw_cell_stroke(ctx, f, s, t.corner_capped(s));
             }
         }
     }
 }
 
+/// A cell's diagonal lines, clipped to the cell so their caps stay inside it.
+fn draw_cell_diagonals(ctx: &mut RenderContext, f: &Frame, xf: Affine, c: &designcraft_compose::PlacedCell) {
+    if c.diagonals.is_empty() {
+        return;
+    }
+    ctx.set_transform(f.view * xf);
+    ctx.push_clip_layer(&c.rect.to_path(0.1));
+    for s in &c.diagonals {
+        draw_cell_stroke(ctx, f, s, (s.a, s.b));
+    }
+    ctx.pop_layer();
+}
+
+/// One cell edge or diagonal: the gap color, then the stroke type. `capped` holds the ends the
+/// butt-ended fills (stripes, wavy, hash, the gap color) run between.
+fn draw_cell_stroke(ctx: &mut RenderContext, f: &Frame, s: &designcraft_compose::StrokeSeg, capped: (kurbo::Point, kurbo::Point)) {
+    let doc = f.doc;
+    let Some(col) = doc.resolve_color(&s.stroke.color, s.stroke.tint) else { return };
+    let kind = doc.stroke_kind(&s.stroke.kind);
+    let line = kurbo::Line::new(s.a, s.b).to_path(0.1);
+    // Butt-ended fills close the table's corners the way square caps do.
+    let capped = kurbo::Line::new(capped.0, capped.1).to_path(0.1);
+    // Gap colour under dashes, dots and stripes: the whole edge, solid.
+    if !matches!(kind, designcraft_doc::StrokeType::Solid)
+        && let Some(gap) = doc.resolve_color(&s.stroke.gap_color, s.stroke.gap_tint)
+    {
+        ctx.set_paint(color_of(&gap, 1.0));
+        ctx.set_stroke(kurbo::Stroke::new(s.stroke.weight).with_caps(kurbo::Cap::Butt));
+        ctx.stroke_path(&capped);
+    }
+    ctx.set_paint(color_of(&col, 1.0));
+    // Stripes, wavy and hash types are fills along the edge.
+    if let Some(o) = kind.outline(&capped, s.stroke.weight, 0.05 * f.px) {
+        ctx.fill_path(&o);
+        return;
+    }
+    ctx.set_stroke(cell_stroke(&kind, s.stroke.weight, s.a.distance(s.b)));
+    ctx.stroke_path(&line);
+}
+
 /// A run's stroke colour, if it has one.
 fn stroke_of(doc: &designcraft_doc::Document, st: &designcraft_compose::RunStyle) -> Option<TextPaint> {
     if st.stroke == designcraft_color::swatch::NONE {
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
@@ -2201,7 +2201,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
         "frameSize" => crate::i18n::tr(&app.ui.language, "Rectangle"),
         "goToPage" => crate::i18n::tr(&app.ui.language, "Go to Page"),
         "insertTable" => crate::i18n::tr(&app.ui.language, "Create Table"),
-        "cellOptions" => crate::i18n::tr(&app.ui.language, "Cell Options"),
+        "cellOptions" | "diagonalLines" => crate::i18n::tr(&app.ui.language, "Cell Options"),
         "createGuides" => crate::i18n::tr(&app.ui.language, "Create Guides"),
         "marginsAndColumns" => crate::i18n::tr(&app.ui.language, "Margins and Columns"),
         "textFrameOptions" => crate::i18n::tr(&app.ui.language, "Text Frame Options"),
@@ -2975,6 +2975,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
                         });
                     }
                     "cellOptions" => cell_options(app, ui, &mut d),
+                    "diagonalLines" => diagonal_lines(app, ui, &mut d),
                     "createGuides" => create_guides_dialog(app, ui, &mut d),
                     "marginsAndColumns" => margins_and_columns_dialog(app, ui, &mut d),
                     "textFrameOptions" => {
@@ -3181,6 +3182,105 @@ fn confirm_cell_options(app: &mut DesignApp, d: &Dialog) -> Result<Value, String
     app.run("table.setCell", Value::Object(params))
 }
 
+/// The line buttons of Cell Options › Diagonal Lines: the `diagonal.lines` name and its label.
+const DIAGONAL_LINES: [(&str, &str); 4] = [
+    ("none", "No Diagonal Lines"),
+    ("topLeft", "Diagonal Line from Top Left to Bottom Right"),
+    ("topRight", "Diagonal Line from Top Right to Bottom Left"),
+    ("crossed", "Crossed Diagonal Lines"),
+];
+
+/// Cell Options › Diagonal Lines keys (`table.getCellDiagonal` names).
+const DIAGONAL_KEYS: [&str; 8] = ["lines", "weight", "type", "color", "tint", "gapColor", "gapTint", "inFront"];
+
+/// Table › Cell Options › Diagonal Lines… for the target cells. The fields start from the
+/// cells' values (`seed`); OK applies the fields that differ from it.
+pub fn open_diagonal_lines(app: &mut DesignApp) -> Result<Value, String> {
+    let cur = app.session.execute("table.getCellDiagonal", &json!({})).map_err(|e| e.to_string())?;
+    let mut fields = Map::new();
+    for k in DIAGONAL_KEYS {
+        fields.insert(k.into(), cur[k].clone());
+    }
+    fields.insert("seed".into(), Value::Object(fields.clone()));
+    app.ui.dialog = Some(Dialog::new("diagonalLines", Value::Object(fields)));
+    Ok(Value::Null)
+}
+
+/// Cell Options › Diagonal Lines: the four line buttons, the Line Stroke fields and Draw.
+fn diagonal_lines(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
+    let lang = app.ui.language.clone();
+    let t = crate::theme::Tokens::get(ui.ctx());
+    ui.horizontal(|ui| {
+        for (name, label) in DIAGONAL_LINES {
+            let on = d.fields.get("lines").and_then(Value::as_str) == Some(name);
+            let text = crate::i18n::tr(&lang, label).to_string();
+            let (r, resp) = ui.allocate_exact_size(egui::vec2(32.0, 32.0), egui::Sense::click());
+            resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, on, &text));
+            let resp = resp.on_hover_text(&text);
+            if resp.clicked() {
+                d.fields.insert("lines".into(), json!(name));
+            }
+            // Enter on a focused line button picks that line and leaves the dialog open.
+            if resp.has_focus() {
+                ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter));
+            }
+            let p = ui.painter();
+            p.rect_filled(r, 2.0, if resp.hovered() { t.hover } else { t.input });
+            let cell = r.shrink(7.0);
+            p.rect_stroke(cell, 0.0, egui::Stroke::new(1.0, t.text_dim), egui::StrokeKind::Inside);
+            let ink = egui::Stroke::new(1.5, if on { t.accent } else { t.icon });
+            if matches!(name, "topLeft" | "crossed") {
+                p.line_segment([cell.left_top(), cell.right_bottom()], ink);
+            }
+            if matches!(name, "topRight" | "crossed") {
+                p.line_segment([cell.right_top(), cell.left_bottom()], ink);
+            }
+            let rim = if on { egui::Stroke::new(2.0, t.accent) } else { egui::Stroke::new(1.0, t.field_border) };
+            p.rect_stroke(r, 2.0, rim, egui::StrokeKind::Inside);
+        }
+    });
+    ui.add_space(10.0);
+    crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Line Stroke")).font(semibold(12.0)));
+    let values = Value::Object(d.fields.clone());
+    if let Some((k, v)) = crate::panels::cell_stroke::stroke_fields(app, ui, "diagonal_lines", &values) {
+        d.fields.insert(k.into(), v);
+    }
+    ui.add_space(10.0);
+    egui::Grid::new("diagonal_draw").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
+        crate::rtl::label(ui, crate::i18n::tr(&lang, "Draw:"));
+        let shown = match d.fields.get("inFront").and_then(Value::as_bool) {
+            Some(true) => crate::i18n::tr(&lang, "Diagonal In Front"),
+            Some(false) => crate::i18n::tr(&lang, "Content In Front"),
+            None => "",
+        }
+        .to_string();
+        egui::ComboBox::from_id_salt("diagonal_draw_order").selected_text(crate::rtl::widget(ui, &shown)).width(140.0).show_ui(ui, |ui| {
+            for (front, label) in [(true, "Diagonal In Front"), (false, "Content In Front")] {
+                let text = crate::i18n::tr(&lang, label);
+                if ui.selectable_label(shown == text, crate::rtl::widget(ui, text)).clicked() {
+                    d.fields.insert("inFront".into(), json!(front));
+                }
+            }
+        });
+        ui.end_row();
+    });
+}
+
+/// Apply the Diagonal Lines fields that differ from the cells' values, as one `table.setCell`.
+fn confirm_diagonal_lines(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
+    let seed = d.fields.get("seed").cloned().unwrap_or(Value::Null);
+    let mut diagonal = Map::new();
+    for k in DIAGONAL_KEYS {
+        if let Some(v) = d.fields.get(k).filter(|v| !v.is_null() && seed.get(k) != Some(*v)) {
+            diagonal.insert(k.into(), v.clone());
+        }
+    }
+    if diagonal.is_empty() {
+        return Ok(Value::Null);
+    }
+    app.run("table.setCell", json!({"diagonal": diagonal}))
+}
+
 /// Delete absolute pages `pages` as the Pages panel and Layout › Pages › Delete Page do: when
 /// they hold objects, InDesign's warning asks first, unless it was told not to.
 pub fn delete_pages(app: &mut DesignApp, pages: Vec<usize>) -> Result<Value, String> {
@@ -3317,6 +3417,15 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
             }
             r
         }
+        "diagonalLines" => {
+            // On an error the dialog stays open to be corrected.
+            let r = confirm_diagonal_lines(app, &d);
+            if let Err(e) = &r {
+                app.status(e.clone());
+                app.ui.dialog = Some(d);
+            }
+            r
+        }
         "textFrameOptions" => {
             // On an error the dialog stays open to be corrected.
             let mut d = d;
@@ -7839,6 +7948,101 @@ mod tests {
         assert_eq!([t.cell(0, 0).unwrap().strokes[2].weight, t.cell(1, 1).unwrap().strokes[0].weight], [1.0; 2]);
     }
 
+    #[test]
+    fn diagonal_lines_dialog_draws_and_applies_its_values_but_not_a_mixed_one() {
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+        app.run("file.new", json!({})).unwrap();
+        app.run("frame.create", json!({"rect": [72, 72, 400, 400], "content": "text", "caret": true})).unwrap();
+        let tid = app.run("table.insert", json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap();
+        app.run("table.setCell", json!({"row": 0, "col": 0, "diagonal": {"color": "[Paper]"}})).unwrap();
+        app.run("table.select", json!({"what": "table"})).unwrap();
+        app.run("app.diagonalLinesDialog", json!({})).unwrap();
+        let ctx = dialog_context("");
+        let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, 700.0));
+        // The first frame sizes the window without painting it.
+        dialog_frame(&mut app, &ctx, screen, vec![]);
+        let labels = dialog_frame(&mut app, &ctx, screen, vec![]);
+        for shown in ["Line Stroke", "Weight:", "Draw:", "Diagonal In Front"] {
+            visible_label(&labels, screen, shown);
+        }
+        let d = app.ui.dialog.as_ref().unwrap();
+        assert_eq!(d.id, "diagonalLines");
+        assert_eq!((&d.fields["lines"], &d.fields["color"], &d.fields["inFront"]), (&json!("none"), &Value::Null, &json!(true)));
+        // The fourth line button, in the row above Line Stroke, is Crossed Diagonal Lines.
+        let caption = visible_label(&labels, screen, "Line Stroke");
+        let gap = ctx.global_style().spacing.item_spacing;
+        let crossed = egui::pos2(caption.left() + 3.0 * (32.0 + gap.x) + 16.0, caption.top() - 10.0 - gap.y - 16.0);
+        click_dialog(&mut app, &ctx, screen, crossed);
+        let d = app.ui.dialog.as_mut().unwrap();
+        assert_eq!(d.fields["lines"], json!("crossed"));
+        d.fields.insert("inFront".into(), json!(false));
+        d.fields.insert("weight".into(), json!(0.5));
+        confirm(&mut app).unwrap();
+        let st = app.session.active().unwrap();
+        let t = st.doc.stories.values().find_map(|s| s.tables.get(&tid)).unwrap();
+        assert!(
+            t.cells
+                .iter()
+                .all(|c| c.diagonal.lines == designcraft_doc::DiagonalLines::Crossed && !c.diagonal.in_front && c.diagonal.stroke.weight == 0.5)
+        );
+        let colors: Vec<_> = t.cells.iter().map(|c| c.diagonal.stroke.color.as_str()).collect();
+        assert_eq!(colors, ["[Paper]", "[Black]", "[Black]", "[Black]"], "a mixed value is left as it is");
+    }
+
+    #[test]
+    fn cell_options_menu_items_need_a_table_target() {
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+        app.run("file.new", json!({})).unwrap();
+        app.run("frame.create", json!({"rect": [72, 72, 400, 400], "content": "text", "caret": true})).unwrap();
+        let items = ["app.cellOptionsDialog", "app.diagonalLinesDialog", "app.graphicCell"];
+        for id in items {
+            assert!(!crate::menus::menu_enabled(&app, id), "{id} without a table");
+        }
+        app.run("table.insert", json!({"rows": 2, "cols": 2})).unwrap();
+        for id in items {
+            assert!(crate::menus::menu_enabled(&app, id), "{id} in a table");
+        }
+    }
+
+    #[test]
+    fn enter_on_a_diagonal_line_button_picks_it_and_leaves_the_dialog_open() {
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+        app.run("file.new", json!({})).unwrap();
+        app.run("frame.create", json!({"rect": [72, 72, 400, 400], "content": "text", "caret": true})).unwrap();
+        let tid = app.run("table.insert", json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap();
+        app.run("table.select", json!({"what": "table"})).unwrap();
+        app.run("app.diagonalLinesDialog", json!({})).unwrap();
+        let ctx = dialog_context("");
+        let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, 700.0));
+        dialog_frame(&mut app, &ctx, screen, vec![]);
+        dialog_frame(&mut app, &ctx, screen, vec![]);
+        let key = |key| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE };
+        // Tab past the title bar and the first line button to Diagonal Line from Top Left to Bottom
+        // Right, then press Enter.
+        for _ in 0..3 {
+            dialog_frame(&mut app, &ctx, screen, vec![key(egui::Key::Tab)]);
+            dialog_frame(&mut app, &ctx, screen, vec![]);
+        }
+        dialog_frame(&mut app, &ctx, screen, vec![key(egui::Key::Enter)]);
+        dialog_frame(&mut app, &ctx, screen, vec![]);
+        let d = app.ui.dialog.as_ref().expect("Enter on a line button leaves the dialog open");
+        assert_eq!(d.fields["lines"], json!("topLeft"));
+        let st = app.session.active().unwrap();
+        let t = st.doc.stories.values().find_map(|s| s.tables.get(&tid)).unwrap();
+        assert!(t.cells.iter().all(|c| c.diagonal.lines == designcraft_doc::DiagonalLines::None), "nothing is applied yet");
+        // Enter with no line button focused still confirms.
+        ctx.memory_mut(|m| {
+            if let Some(id) = m.focused() {
+                m.surrender_focus(id);
+            }
+        });
+        dialog_frame(&mut app, &ctx, screen, vec![key(egui::Key::Enter)]);
+        assert!(app.ui.dialog.is_none());
+        let st = app.session.active().unwrap();
+        let t = st.doc.stories.values().find_map(|s| s.tables.get(&tid)).unwrap();
+        assert!(t.cells.iter().all(|c| c.diagonal.lines == designcraft_doc::DiagonalLines::TopLeft));
+    }
+
     #[test]
     fn preferences_ok_saves_a_story_editor_size_that_loads_back() {
         // `ui.dialog.set` takes any value; a size that isn't finite is saved as `null`.
diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs
--- a/crates/ui-egui/src/i18n.rs
+++ b/crates/ui-egui/src/i18n.rs
@@ -4538,6 +4538,36 @@ const TABLE: &[(&str, [&str; 5])] = &[
     ("Start At", ["Beginnen bei", "Commencer à", "Empezar en", "開始番号", "起始编号"]),
     // Cell strokes.
     ("Strokes and Fills…", ["Konturen und Flächen …", "Contours et fonds…", "Trazos y rellenos…", "線と塗り…", "描边和填色…"]),
+    ("Diagonal Lines…", ["Diagonale Linien …", "Lignes diagonales…", "Líneas diagonales…", "斜線…", "对角线…"]),
+    ("No Diagonal Lines", ["Keine diagonalen Linien", "Aucune ligne diagonale", "Sin líneas diagonales", "斜線なし", "无对角线"]),
+    (
+        "Diagonal Line from Top Left to Bottom Right",
+        [
+            "Diagonale Linie von links oben nach rechts unten",
+            "Ligne diagonale du coin supérieur gauche au coin inférieur droit",
+            "Línea diagonal de la esquina superior izquierda a la inferior derecha",
+            "左上から右下への斜線",
+            "从左上到右下的对角线",
+        ],
+    ),
+    (
+        "Diagonal Line from Top Right to Bottom Left",
+        [
+            "Diagonale Linie von rechts oben nach links unten",
+            "Ligne diagonale du coin supérieur droit au coin inférieur gauche",
+            "Línea diagonal de la esquina superior derecha a la inferior izquierda",
+            "右上から左下への斜線",
+            "从右上到左下的对角线",
+        ],
+    ),
+    (
+        "Crossed Diagonal Lines",
+        ["Gekreuzte diagonale Linien", "Lignes diagonales croisées", "Líneas diagonales cruzadas", "交差する斜線", "交叉对角线"],
+    ),
+    ("Line Stroke", ["Linienkontur", "Contour de ligne", "Trazo de línea", "斜線の線", "线条描边"]),
+    ("Draw:", ["Zeichnen:", "Dessin :", "Dibujar:", "描画:", "绘制："]),
+    ("Diagonal In Front", ["Diagonale im Vordergrund", "Diagonale au premier plan", "Diagonal delante", "斜線を前面へ", "对角线置前"]),
+    ("Content In Front", ["Inhalt im Vordergrund", "Contenu au premier plan", "Contenido delante", "内容を前面へ", "内容置前"]),
     ("Cell Stroke", ["Zellkontur", "Contour de cellule", "Trazo de celda", "セルの線", "单元格描边"]),
     ("Cell Fill", ["Zellfläche", "Fond de cellule", "Relleno de celda", "セルの塗り", "单元格填色"]),
     ("Cell edges", ["Zellkanten", "Bords de cellule", "Bordes de celda", "セルの境界線", "单元格边缘"]),
diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs
--- a/crates/ui-egui/src/i18n/ar.rs
+++ b/crates/ui-egui/src/i18n/ar.rs
@@ -1802,6 +1802,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Continue from Previous Number", "المتابعة من الرقم السابق"),
     ("Start At", "البدء عند"),
     ("Strokes and Fills…", "الحدود والتعبئة…"),
+    ("Diagonal Lines…", "الخطوط القطرية…"),
+    ("No Diagonal Lines", "بلا خطوط قطرية"),
+    ("Diagonal Line from Top Left to Bottom Right", "خط قطري من أعلى اليسار إلى أسفل اليمين"),
+    ("Diagonal Line from Top Right to Bottom Left", "خط قطري من أعلى اليمين إلى أسفل اليسار"),
+    ("Crossed Diagonal Lines", "خطوط قطرية متقاطعة"),
+    ("Line Stroke", "حد الخط"),
+    ("Draw:", "الرسم:"),
+    ("Diagonal In Front", "القطري في الأمام"),
+    ("Content In Front", "المحتوى في الأمام"),
     ("Cell Stroke", "حد الخلية"),
     ("Cell Fill", "تعبئة الخلية"),
     ("Cell edges", "حواف الخلية"),
diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs
--- a/crates/ui-egui/src/i18n/de.rs
+++ b/crates/ui-egui/src/i18n/de.rs
@@ -1898,6 +1898,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Fancy", "Verziert"),
     ("Gap Tint:", "Lückenfarbton:"),
     ("Strokes and Fills…", "Konturen und Flächen …"),
+    ("Diagonal Lines…", "Diagonale Linien …"),
+    ("No Diagonal Lines", "Keine diagonalen Linien"),
+    ("Diagonal Line from Top Left to Bottom Right", "Diagonale Linie von links oben nach rechts unten"),
+    ("Diagonal Line from Top Right to Bottom Left", "Diagonale Linie von rechts oben nach links unten"),
+    ("Crossed Diagonal Lines", "Gekreuzte diagonale Linien"),
+    ("Line Stroke", "Linienkontur"),
+    ("Draw:", "Zeichnen:"),
+    ("Diagonal In Front", "Diagonale im Vordergrund"),
+    ("Content In Front", "Inhalt im Vordergrund"),
     ("Cell Stroke", "Zellkontur"),
     ("Cell Fill", "Zellfläche"),
     ("Cell edges", "Zellkanten"),
diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs
--- a/crates/ui-egui/src/i18n/fr.rs
+++ b/crates/ui-egui/src/i18n/fr.rs
@@ -145,6 +145,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Generate Index", "Générer l'index"),
     ("Generate Live Caption", "Générer une légende dynamique"),
     ("Get Cell Stroke", "Obtenir le contour de cellule"),
+    ("Get Cell Diagonal Lines", "Obtenir les lignes diagonales de cellule"),
     ("Get Tabs", "Obtenir les tabulations"),
     ("Global Light", "Lumière globale"),
     ("Grab bag", "Liste"),
diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs
--- a/crates/ui-egui/src/i18n/it.rs
+++ b/crates/ui-egui/src/i18n/it.rs
@@ -1854,6 +1854,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Continue from Previous Number", "Continua dal numero precedente"),
     ("Start At", "Inizia da"),
     ("Strokes and Fills…", "Tracce e riempimenti…"),
+    ("Diagonal Lines…", "Linee diagonali…"),
+    ("No Diagonal Lines", "Nessuna linea diagonale"),
+    ("Diagonal Line from Top Left to Bottom Right", "Linea diagonale dall'angolo superiore sinistro a quello inferiore destro"),
+    ("Diagonal Line from Top Right to Bottom Left", "Linea diagonale dall'angolo superiore destro a quello inferiore sinistro"),
+    ("Crossed Diagonal Lines", "Linee diagonali incrociate"),
+    ("Line Stroke", "Traccia linea"),
+    ("Draw:", "Disegna:"),
+    ("Diagonal In Front", "Diagonale in primo piano"),
+    ("Content In Front", "Contenuto in primo piano"),
     ("Cell Stroke", "Traccia cella"),
     ("Cell Fill", "Riempimento cella"),
     ("Cell edges", "Bordi cella"),
diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs
--- a/crates/ui-egui/src/i18n/ja.rs
+++ b/crates/ui-egui/src/i18n/ja.rs
@@ -2112,6 +2112,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Bounding Box (Visible Layers Only)", "バウンディングボックス（表示レイヤーのみ）"),
     ("Bounding Box (All Layers)", "バウンディングボックス（すべてのレイヤー）"),
     ("Strokes and Fills…", "線と塗り…"),
+    ("Diagonal Lines…", "斜線…"),
+    ("No Diagonal Lines", "斜線なし"),
+    ("Diagonal Line from Top Left to Bottom Right", "左上から右下への斜線"),
+    ("Diagonal Line from Top Right to Bottom Left", "右上から左下への斜線"),
+    ("Crossed Diagonal Lines", "交差する斜線"),
+    ("Line Stroke", "斜線の線"),
+    ("Draw:", "描画:"),
+    ("Diagonal In Front", "斜線を前面へ"),
+    ("Content In Front", "内容を前面へ"),
     ("Cell Stroke", "セルの線"),
     ("Cell Fill", "セルの塗り"),
     ("Cell edges", "セルの境界線"),
diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs
--- a/crates/ui-egui/src/i18n/ka.rs
+++ b/crates/ui-egui/src/i18n/ka.rs
@@ -1924,6 +1924,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Clear Recent Files", "ბოლო ფაილების სიის გასუფთავება"),
     ("Transparency…", "გამჭვირვალობა…"),
     ("Strokes and Fills…", "კონტურები და შევსებები…"),
+    ("Diagonal Lines…", "დიაგონალური ხაზები…"),
     ("Show/Hide Document Grid", "დოკუმენტის ბადის ჩვენება/დამალვა"),
     ("% of all commits", "ყველა კომიტის %"),
     ("(Paragraph Font)", "(აბზაცის შრიფტი)"),
diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs
--- a/crates/ui-egui/src/i18n/pt_br.rs
+++ b/crates/ui-egui/src/i18n/pt_br.rs
@@ -1406,6 +1406,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Continue from Previous Number", "Continuar do número anterior"),
     ("Start At", "Iniciar em"),
     ("Strokes and Fills…", "Traçados e preenchimentos…"),
+    ("Diagonal Lines…", "Linhas diagonais…"),
+    ("No Diagonal Lines", "Sem linhas diagonais"),
+    ("Diagonal Line from Top Left to Bottom Right", "Linha diagonal do canto superior esquerdo ao inferior direito"),
+    ("Diagonal Line from Top Right to Bottom Left", "Linha diagonal do canto superior direito ao inferior esquerdo"),
+    ("Crossed Diagonal Lines", "Linhas diagonais cruzadas"),
+    ("Line Stroke", "Traçado da linha"),
+    ("Draw:", "Desenhar:"),
+    ("Diagonal In Front", "Diagonal à frente"),
+    ("Content In Front", "Conteúdo à frente"),
     ("Cell Stroke", "Traçado da célula"),
     ("Cell Fill", "Preenchimento da célula"),
     ("Cell edges", "Bordas da célula"),
diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs
--- a/crates/ui-egui/src/i18n/uk.rs
+++ b/crates/ui-egui/src/i18n/uk.rs
@@ -1822,6 +1822,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Continue from Previous Number", "Продовжити з попереднього номера"),
     ("Start At", "Почати з"),
     ("Strokes and Fills…", "Обведення та заливки…"),
+    ("Diagonal Lines…", "Діагональні лінії…"),
+    ("No Diagonal Lines", "Без діагональних ліній"),
+    ("Diagonal Line from Top Left to Bottom Right", "Діагональна лінія з верхнього лівого кута в нижній правий"),
+    ("Diagonal Line from Top Right to Bottom Left", "Діагональна лінія з верхнього правого кута в нижній лівий"),
+    ("Crossed Diagonal Lines", "Перехресні діагональні лінії"),
+    ("Line Stroke", "Обведення лінії"),
+    ("Draw:", "Малювати:"),
+    ("Diagonal In Front", "Діагональ попереду"),
+    ("Content In Front", "Вміст попереду"),
     ("Cell Stroke", "Обведення клітинки"),
     ("Cell Fill", "Заливка клітинки"),
     ("Cell edges", "Краї клітинки"),
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -120,6 +120,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
         None,
         "{} — Cell Options › Strokes and Fills of the target cells: edge proxy, stroke weight, type, colour, tint, gap colour, gap tint, and cell fill (table.setCell)",
     ),
+    (
+        "app.diagonalLinesDialog",
+        "Diagonal Lines…",
+        None,
+        "{} — Cell Options › Diagonal Lines of the target cells: the line type, its weight, type, color, tint, gap color and gap tint, and Draw (table.setCell {diagonal})",
+    ),
     ("app.footnoteOptionsDialog", "Document Footnote Options…", None, "{} — numbering, formatting and layout of footnotes"),
     ("app.rubyDialog", "Ruby…", None, "{} — the reading set over the selected text"),
     (
@@ -822,6 +828,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
             "-",
             ">Cell Options",
             "ui:app.cellOptionsDialog",
+            "ui:app.diagonalLinesDialog",
             "<",
             "cmd:table.merge",
             "cmd:table.unmerge",
@@ -1210,6 +1217,7 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
             Ok(Value::Null)
         }
         "app.cellOptionsDialog" => crate::dialogs::open_cell_options(app),
+        "app.diagonalLinesDialog" => crate::dialogs::open_diagonal_lines(app),
         "window.hideMenuItem" => {
             let Some(item) = p.get("item").and_then(Value::as_str) else { return Some(Err("missing item".into())) };
             let hide = p.get("hidden").and_then(Value::as_bool).unwrap_or(true);
@@ -2644,6 +2652,12 @@ pub fn menu_enabled(app: &DesignApp, id: &str) -> bool {
     if matches!(id, "app.justificationDialog" | "app.hyphenationDialog") {
         return enabled(app, "type.para");
     }
+    if matches!(id, "app.cellOptionsDialog" | "app.diagonalLinesDialog") {
+        return enabled(app, "table.setCell");
+    }
+    if id == "app.graphicCell" {
+        return enabled(app, "table.placeGraphic");
+    }
     if id.starts_with("fit.") {
         return enabled(app, "object.fit");
     }
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -151,7 +151,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format ·
 
 ### 12. Tables in depth (F)
 - **Missing:** anchored objects in cells not drawn (#163); Table ▸ Table Options and Cell Options
-  submenus as InDesign lays them out, Diagonal Lines, Edit Header/Footer,
+  submenus as InDesign lays them out, diagonal lines in IDML, Edit Header/Footer,
   Go to Row; general reliability (#20).
 - **Estimate:** 25–40 h.
 
diff --git a/docs/roadmap.md b/docs/roadmap.md
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -65,7 +65,7 @@ set exists; depth gaps are in gaps.md.
 | M5 | Graphics & links | ✅ core; EPS preview only |
 | M6 | Files & export (native, IDML, PDF, PNG/JPEG) | IDML ✅, PDF ✅ (X-4, A-2b, tagged); INDD read ✅ (via indd-utils); PDF/X-1a ✅; PDF/X-3 and presets ✗ |
 | M7 | Long documents (sections, TOC, index, footnotes, books) | ✅ except Books (shallow) |
-| M8 | Tables | ✅ core (table/cell styles, rotation); diagonal lines, anchored objects in cells (#163) |
+| M8 | Tables | ✅ core (table/cell styles, rotation); diagonal lines in IDML, anchored objects in cells (#163) |
 | M9 | Performance (MT composition, tiles) | in progress: parallel compose 6.7×, damage regions; incremental composition next |
 | M10 | Find/Change, spelling, Preflight | ✅ (spelling English only) |
 | M11 | Layout power features (liquid/alternate layouts, data merge) | present, shallow; Flex Layout (new in InDesign 2026) ✗ |
diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md
--- a/docs/target-app-parity.md
+++ b/docs/target-app-parity.md
@@ -214,7 +214,7 @@ table is the one summed.
 | Type & text frames | 99% | 45% | 40–60 | Primary Text Frame pages users can't delete (alpha blocker, #165); autoflow sets new frames on following pages instead of flowing into their primary text frame; caret formatting discarded (#289); Notes/Track Changes menus; frame grids |
 | Typography | 100% | 45% | (typography dimension) | see [typography-parity.md](typography-parity.md) |
 | Styles | 100% | 60% | 15–25 | Paragraph Styles edit/delete reported failing (#169); style options depth |
-| Tables | 100% | 55% | 25–40 | anchored objects in cells not drawn (#163); Table ▸ Table Options submenus, Diagonal Lines, Edit Header/Footer |
+| Tables | 100% | 55% | 25–40 | anchored objects in cells not drawn (#163); Table ▸ Table Options submenus, Edit Header/Footer |
 | Long documents | 100% | 45% | 30–50 | Books are shallow (sync, book pagination, book PDF/TOC/index); Table of Contents Styles |
 | Interactivity & digital | 100% | 40% | 30–50 | HTML export basic (#314); fixed-layout EPUB is page images; no animation, timing, media playback |
 | Output & production | 92% | 55% | 40–70 | PDF/X-4 only (no X-1a/X-3), with visible unembedded font detection (below); no PDF/print presets or `.joboptions`, print via craft-print (CUPS/Windows), no trapping; Output Preview panel |
@@ -370,6 +370,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
 - Track Changes: per-change Accept / Reject (`changes.accept`, `changes.reject`) and a Track Changes panel listing every change.
 - Move table rows and columns (`table.moveRow`, `table.moveColumn`, Table panel buttons; with whole rows or columns selected, drag them with the Type tool — `table.dropCells`).
 - Cell strokes: with cells selected (or the cursor in a cell) the Stroke panel edits the chosen cell edges, picked on a drawn edge proxy (top, bottom, left, right, inner horizontal, inner vertical); Table › Cell Options › Strokes and Fills… does the same with the cell fill. Weight, every stroke type, colour, tint, gap colour and gap tint draw on screen and in PDF and round-trip through IDML (`table.setCell` `stroke.edges`, `table.getCellStroke`).
+- Diagonal lines in cells: Table › Cell Options › Diagonal Lines… draws a line from the top left corner to the bottom right, from the top right to the bottom left, or both, with the same weight, type, color, tint, gap color and gap tint as cell edges, in front of the cell's contents or behind them (Draw: Diagonal In Front or Content In Front). They draw on screen and in PDF and are saved in the native format; IDML import and export do not carry them (`table.setCell` `diagonal`, `table.getCellDiagonal`).
 - Distribute Rows Evenly (`table.distributeRows`, Table menu and panel): selected rows receive equal fixed heights preserving their composed total height, with undo/redo. Row-spanning merges and rows outside the composed frames are rejected; shrinking tall cells can overset their text.
 - Object States (`states.create/show/rename/release/list`, Object States panel): multi-state objects show one state on screen and in PDF.
 - Buttons (`button.set/clear/list`, Buttons and Forms panel): go to page / next / previous / first / last / URL as PDF link annotations. Form fields too (`form.set/clear/list`: text fields, check boxes, combo and list boxes, signature fields; required, multiline, defaults) exported as AcroForm widgets through an incremental update (not in PDF/X or PDF/A).
