diff --git a/contributors/people.toml b/contributors/people.toml
--- a/contributors/people.toml
+++ b/contributors/people.toml
@@ -88,3 +88,6 @@ sync_github_name = false
 
 [people.webdevtodayjason]
 display_name = "Jason Brashear"
+
+[people.alexanderalber]
+real_name = "Alexander Alber"
diff --git a/crates/engine/src/graphic_templates.rs b/crates/engine/src/graphic_templates.rs
--- a/crates/engine/src/graphic_templates.rs
+++ b/crates/engine/src/graphic_templates.rs
@@ -763,6 +763,7 @@ fn char_style_p(v: &Value) -> Result<CharStyle> {
             "bold" | "fauxBold" | "faux_bold" => c.faux_bold = x.as_bool(),
             "italic" | "fauxItalic" | "faux_italic" => c.faux_italic = x.as_bool(),
             "underline" => c.underline = x.as_bool(),
+            "strikethrough" | "strike" => c.strikethrough = x.as_bool(),
             "tracking" => c.tracking = x.as_f64().map(|v| v as f32),
             "baselineShift" | "baseline_shift" => c.baseline_shift = x.as_f64().map(|v| v as f32),
             "caps" => {
@@ -1236,7 +1237,7 @@ pub fn commands() -> Vec<CommandSpec> {
             "graphics.setCharStyle",
             "Character Style",
             &[],
-            r##"{"clip":id?,"layer":n|name?,"start":char=0,"end":char=len,"style":{"font":str,"fontStyle":str,"size":px,"color":"#rrggbb","bold":bool,"italic":bool,"underline":bool,"tracking":n,"baselineShift":px,"caps":"normal|all caps|small caps"},"clear":bool? (remove styles from the range),"merge":bool?,"begin":bool? (start a new undo gesture)}"##,
+            r##"{"clip":id?,"layer":n|name?,"start":char=0,"end":char=len,"style":{"font":str,"fontStyle":str,"size":px,"color":"#rrggbb","bold":bool,"italic":bool,"underline":bool,"strikethrough":bool,"tracking":n,"baselineShift":px,"caps":"normal|all caps|small caps"},"clear":bool? (remove styles from the range),"merge":bool?,"begin":bool? (start a new undo gesture)}"##,
             has_graphic,
             set_char_style,
         ),
diff --git a/crates/engine/src/graphic_templates/tests.rs b/crates/engine/src/graphic_templates/tests.rs
--- a/crates/engine/src/graphic_templates/tests.rs
+++ b/crates/engine/src/graphic_templates/tests.rs
@@ -285,6 +285,29 @@ fn character_styles_follow_edits() {
     assert!(s.execute("graphics.setCharStyle", json!({"clip": c.0, "style": {"sparkle": true}})).is_err());
 }
 
+#[test]
+fn strikethrough_per_character_and_per_layer() {
+    let mut s = demo("gt-strike");
+    let c = clip_of(&s.execute("graphics.newText", json!({"text": "old new"})).unwrap());
+    s.execute("graphics.setCharStyle", json!({"clip": c.0, "start": 0, "end": 3, "style": {"strikethrough": true}})).unwrap();
+    let st = s.execute("graphics.list", json!({"clip": c.0})).unwrap()["layers"][0]["styles"].clone();
+    assert_eq!(st, json!([{"start": 0, "end": 3, "strikethrough": true}]));
+    let layout = |s: &Session| {
+        let it = item(s, c);
+        let sp = &item_layer_specs(&it, it.source_in, canvas(s))[0].1;
+        let LayerContent::Text(t) = &sp.content else { panic!() };
+        filmcraft_render::graphic_clip::text_layout(t)
+    };
+    let l = layout(&s);
+    assert_eq!(l.underline_runs, vec![1], "one stroke, over the styled word");
+    assert!(l.underlines[0][2] <= l.caret(3).0 + 0.01);
+    s.execute("graphics.setCharStyle", json!({"clip": c.0, "clear": true})).unwrap();
+    s.execute("graphics.set", json!({"clip": c.0, "props": {"strikethrough": true}})).unwrap();
+    let l = layout(&s);
+    assert_eq!(l.underlines.len(), 1);
+    assert!((l.underlines[0][2] - l.underlines[0][0] - l.lines[0].width).abs() < 0.01, "the whole line");
+}
+
 #[test]
 fn upgrade_caption_to_graphic() {
     let mut s = demo("gt-caption");
diff --git a/crates/project/src/graphic.rs b/crates/project/src/graphic.rs
--- a/crates/project/src/graphic.rs
+++ b/crates/project/src/graphic.rs
@@ -170,6 +170,7 @@ pub(crate) fn layer_defs() -> Vec<EffectDef> {
                 bo("faux_italic", "Faux Italic", false, X),
                 chc("caps", "Capitalisation", CAPS_OPTS, 0, X),
                 bo("underline", "Underline", false, X),
+                bo("strikethrough", "Strikethrough", false, X),
                 fl("box_width", "Text Box Width", 0.0, (0.0, 100_000.0), (0.0, 3840.0), "", X),
                 fl("box_height", "Text Box Height", 0.0, (0.0, 100_000.0), (0.0, 2160.0), "", X),
                 bo("vertical", "Vertical Text", false, X),
@@ -251,6 +252,7 @@ pub struct TextProps {
     /// 0 normal, 1 all caps, 2 small caps.
     pub caps: u32,
     pub underline: bool,
+    pub strikethrough: bool,
     /// Paragraph-text box width (0 = point text).
     pub box_width: f32,
     /// Paragraph-text box height: lines that do not fit are not shown (0 = as tall as the text).
@@ -346,6 +348,7 @@ pub fn eval_layer(e: &EffectInstance, t: Tick, frame: (u32, u32)) -> Option<Laye
                 faux_italic: bb(e, "faux_italic", t),
                 caps: ch(e, "caps", t),
                 underline: bb(e, "underline", t),
+                strikethrough: bb(e, "strikethrough", t),
                 box_width: ff(e, "box_width", t).max(0.0),
                 box_height: ff(e, "box_height", t).max(0.0),
                 vertical: bb(e, "vertical", t),
diff --git a/crates/project/src/graphic_design.rs b/crates/project/src/graphic_design.rs
--- a/crates/project/src/graphic_design.rs
+++ b/crates/project/src/graphic_design.rs
@@ -1,7 +1,7 @@
 //! Responsive design and per-character styles of graphic clips (M10.7).
 //!
 //! - **Per-character styles** ([`StyleRun`], [`CharStyle`]): a text layer's runs override font,
-//!   style, size, fill, faux bold / italic, underline, tracking, baseline shift and caps for a
+//!   style, size, fill, faux bold / italic, underline, strikethrough, tracking, baseline shift and caps for a
 //!   range of characters. Ranges are in *characters* (not bytes), sorted and non-overlapping;
 //!   [`apply_char_style`] merges a new style into a range and [`adjust_runs`] keeps runs on the
 //!   right characters when the text is edited (typed characters take the style of the character
@@ -69,6 +69,8 @@ pub struct CharStyle {
     pub faux_italic: Option<bool>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub underline: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub strikethrough: Option<bool>,
     /// Tracking in 1/1000 em.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub tracking: Option<f32>,
@@ -94,6 +96,7 @@ impl CharStyle {
             faux_bold: over.faux_bold.or(self.faux_bold),
             faux_italic: over.faux_italic.or(self.faux_italic),
             underline: over.underline.or(self.underline),
+            strikethrough: over.strikethrough.or(self.strikethrough),
             tracking: over.tracking.or(self.tracking),
             baseline_shift: over.baseline_shift.or(self.baseline_shift),
             caps: over.caps.or(self.caps),
diff --git a/crates/render/src/graphic_clip.rs b/crates/render/src/graphic_clip.rs
--- a/crates/render/src/graphic_clip.rs
+++ b/crates/render/src/graphic_clip.rs
@@ -49,6 +49,7 @@ pub fn text_styles(t: &TextProps) -> (TextStyle, ParagraphStyle) {
             faux_italic: t.faux_italic,
             caps: caps_of(t.caps),
             underline: t.underline,
+            strikethrough: t.strikethrough,
         },
         ParagraphStyle {
             align: match t.align {
@@ -91,6 +92,7 @@ pub fn text_runs(t: &TextProps, base: &TextStyle) -> Vec<StyleRun> {
                 faux_italic: c.faux_italic.unwrap_or(base.faux_italic),
                 caps: c.caps.map_or(base.caps, caps_of),
                 underline: c.underline.unwrap_or(base.underline),
+                strikethrough: c.strikethrough.unwrap_or(base.strikethrough),
                 ..base.clone()
             },
         })
diff --git a/crates/render/tests/it/main.rs b/crates/render/tests/it/main.rs
--- a/crates/render/tests/it/main.rs
+++ b/crates/render/tests/it/main.rs
@@ -1,4 +1,5 @@
 mod median_radius;
 mod noise_frame_cadence;
 mod sharpen_preview_res;
+mod strikethrough;
 mod underline_direction;
diff --git a/crates/render/tests/it/strikethrough.rs b/crates/render/tests/it/strikethrough.rs
new file mode 100644
--- /dev/null
+++ b/crates/render/tests/it/strikethrough.rs
@@ -0,0 +1,49 @@
+//! Graphic text with strikethrough (letter o, 60 px): the stroke crosses the letter.
+use filmcraft_geom::{Affine, Vec2};
+use filmcraft_project::graphic::{new_text_layer, new_vertical_text_layer};
+use filmcraft_project::{EffectInstance, ParamValue};
+use filmcraft_render::graphic_clip::render_graphic_layers;
+use filmcraft_render::image::Image;
+use filmcraft_time::Tick;
+
+fn set(e: &mut EffectInstance, k: &str, v: ParamValue) {
+    e.params.get_mut(k).unwrap_or_else(|| panic!("{k}")).value = v;
+}
+
+fn draw(layer: EffectInstance) -> Image {
+    let mut img = Image::new(320, 200);
+    render_graphic_layers(&[layer], Tick::ZERO, (320, 200), &Affine::IDENTITY, &mut img);
+    img
+}
+
+fn ink(img: &Image) -> Vec<(usize, usize)> {
+    (0..img.h).flat_map(|y| (0..img.w).map(move |x| (x, y))).filter(|&(x, y)| img.get(x, y)[3] > 0.4).collect()
+}
+
+fn ink_box(img: &Image) -> [usize; 4] {
+    ink(img).into_iter().fold([usize::MAX, usize::MAX, 0, 0], |b, (x, y)| [b[0].min(x), b[1].min(y), b[2].max(x), b[3].max(y)])
+}
+
+#[test]
+fn strikethrough_crosses_the_letter() {
+    let mut h = new_text_layer("o", Vec2::new(80.0, 100.0), 60.0);
+    let h_plain = draw(h.clone());
+    set(&mut h, "strikethrough", ParamValue::Bool(true));
+    let h_st = draw(h);
+    let mut v = new_vertical_text_layer("o", Vec2::new(200.0, 100.0), 60.0);
+    let v_plain = draw(v.clone());
+    set(&mut v, "strikethrough", ParamValue::Bool(true));
+    let v_st = draw(v);
+
+    let (hp, hs) = (ink_box(&h_plain), ink_box(&h_st));
+    assert!(ink(&h_st).len() > ink(&h_plain).len() + 20, "horizontal strikethrough adds ink");
+    assert!(hs[1] >= hp[1] && hs[3] <= hp[3], "the stroke stays within the letter's height: plain {hp:?} struck {hs:?}");
+    // the counter of the o is empty in its middle row; the stroke fills it
+    let cx = (hp[0] + hp[2]) / 2;
+    let row = (hp[1]..=hp[3]).filter(|&y| h_st.get(cx, y)[3] > 0.4 && h_plain.get(cx, y)[3] <= 0.4).count();
+    assert!(row > 0, "ink inside the counter");
+
+    let (vp, vs) = (ink_box(&v_plain), ink_box(&v_st));
+    assert!(ink(&v_st).len() > ink(&v_plain).len() + 20, "vertical strikethrough adds ink");
+    assert!(vs[0] >= vp[0] && vs[2] <= vp[2], "the vertical stroke stays within the letter's width: plain {vp:?} struck {vs:?}");
+}
diff --git a/crates/text/README.md b/crates/text/README.md
--- a/crates/text/README.md
+++ b/crates/text/README.md
@@ -7,8 +7,8 @@ and the Timecode / Clip Name effects. Pure Rust; it builds for `wasm32-unknown-u
 | Module | What |
 |---|---|
 | `fonts` | Font database. Bundled faces (always present, also on the web): Inter Regular / Medium / SemiBold / Bold / Italic, JetBrains Mono Regular, Noto Serif Regular — all SIL OFL 1.1, unmodified, in `assets/fonts/` with `.attribution` sidecars. More faces come from `FontSource`s: `DirectorySource::system()` scans the platform font folders on first need (only the `name` and `OS/2` tables are read, via `sfnt`); on wasm the scan finds nothing and hosts can call `add_font_data`. `resolve(family, style)` picks a face, synthesises bold/italic when the family lacks them, and falls back to Inter (flagged `missing`). `fallback_for` finds a face for characters the chosen face lacks: bundled faces first, then the optional craft-fonts faces (`CRAFT_FONTS`, embedded by `build.rs` when built with `CRAFT_FONTS_DIR`; Mincho for serif text, Gothic otherwise, nearest weight), then loaded system faces. |
-| `layout` | Paragraph layout: per-character font fallback → bidi levels (UAX #9) → items (runs of one face, direction and size) shaped with harfrust (OpenType kerning, ligatures, marks, complex scripts) → tracking → line breaking at UAX #14 opportunities (area text) → visual reordering per line → alignment (left / centre / right / justify), leading (added to the natural line height), baseline shift, all caps / synthesised small caps (78 %), underline. Every character boundary gets a caret stop (ligatures are split evenly), so editors can place carets, hit-test and draw selections. Layouts are cached by text + style (512 entries, LRU). |
-| `layout_rich` | Per-character style runs (`StyleRun`: a byte range with its own `TextStyle`): runs split shaping items (font, size, tracking, caps, faux styles), baseline shift and underline apply per run, a line holding a larger run is pushed down from the line above (baseline distance = max(base line height, previous descent + this line's ascent + leading)), and every glyph / underline carries its style index so renderers can colour runs (`render::draw_run`). |
+| `layout` | Paragraph layout: per-character font fallback → bidi levels (UAX #9) → items (runs of one face, direction and size) shaped with harfrust (OpenType kerning, ligatures, marks, complex scripts) → tracking → line breaking at UAX #14 opportunities (area text) → visual reordering per line → alignment (left / centre / right / justify), leading (added to the natural line height), baseline shift, all caps / synthesised small caps (78 %), underline, strikethrough (OS/2 strikeout metrics; half the x-height when a font leaves them out). Every character boundary gets a caret stop (ligatures are split evenly), so editors can place carets, hit-test and draw selections. Layouts are cached by text + style (512 entries, LRU). |
+| `layout_rich` | Per-character style runs (`StyleRun`: a byte range with its own `TextStyle`): runs split shaping items (font, size, tracking, caps, faux styles), baseline shift, underline and strikethrough apply per run, a line holding a larger run is pushed down from the line above (baseline distance = max(base line height, previous descent + this line's ascent + leading)), and every glyph / underline / strikethrough carries its style index so renderers can colour runs (`render::draw_run`). |
 | `raster` | Our anti-aliased rasteriser (non-zero winding, 5 sub-scanlines, exact horizontal coverage, active-edge list), contour paths with transforms, rounded rectangles, ellipses, polygons and outline emboldening. Coverage is linear, so compositing in linear light is correct. |
 | `render` | Glyph outlines (cached per face + glyph) and glyph masks (cached per size in ⅛ px, ¼-px horizontal phase, synthetic style). Axis-aligned uniform transforms use the mask cache with sub-pixel x positioning; rotated / skewed / non-uniform transforms fill the transformed outlines of the whole run in one pass. Synthetic bold grows outlines by 2.5 % of the size; synthetic italic slants by 12°. |
 | `mask` | Outer / centre / inner strokes from an exact Euclidean distance transform (Felzenszwalb–Huttenlocher) with sub-pixel refinement, a three-pass box blur (≈ Gaussian) for shadows, offsets. |
diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs
--- a/crates/text/src/fonts.rs
+++ b/crates/text/src/fonts.rs
@@ -201,7 +201,7 @@ impl FaceBytes {
 }
 
 /// Vertical metrics at a pixel size (all positive distances, y down from the baseline for the
-/// underline position).
+/// underline and strikethrough positions, which are the top edges of their strokes).
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
 pub struct VMetrics {
     pub ascent: f32,
@@ -211,6 +211,8 @@ pub struct VMetrics {
     pub x_height: f32,
     pub underline_pos: f32,
     pub underline_thickness: f32,
+    pub strikethrough_pos: f32,
+    pub strikethrough_thickness: f32,
 }
 
 impl Face {
@@ -284,18 +286,27 @@ impl Face {
                 x_height: px * 0.5,
                 underline_pos: px * 0.1,
                 underline_thickness: px * 0.06,
+                strikethrough_pos: -px * 0.28,
+                strikethrough_thickness: px * 0.06,
                 ..Default::default()
             };
         };
         let m = f.metrics(Size::new(px), LocationRef::default());
+        let x_height = m.x_height.unwrap_or(m.ascent * 0.5);
+        // OS/2 strikeout: offset of the stroke's top above the baseline. Fonts that leave it zero
+        // (or put it below the baseline) get a stroke centred on half the x-height.
+        let strike_t = m.strikeout.map_or(px * 0.06, |d| d.thickness).max(px * 0.03);
+        let strike_top = m.strikeout.map(|d| d.offset).filter(|o| *o > 0.0 && o.is_finite()).unwrap_or(x_height * 0.5 + strike_t * 0.5);
         VMetrics {
             ascent: m.ascent,
             descent: -m.descent,
             line_gap: m.leading,
             cap_height: m.cap_height.unwrap_or(m.ascent * 0.7),
-            x_height: m.x_height.unwrap_or(m.ascent * 0.5),
+            x_height,
             underline_pos: m.underline.map_or(px * 0.1, |u| -u.offset),
             underline_thickness: m.underline.map_or(px * 0.06, |u| u.thickness.max(px * 0.03)),
+            strikethrough_pos: -strike_top,
+            strikethrough_thickness: strike_t,
         }
     }
     /// Whether the face's GSUB offers an OpenType feature (e.g. `smcp`).
diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs
--- a/crates/text/src/layout.rs
+++ b/crates/text/src/layout.rs
@@ -1,9 +1,10 @@
 //! Paragraph layout: font fallback, bidi (UAX #9), shaping (OpenType via harfrust: kerning,
 //! ligatures, mark positioning, complex scripts), line breaking (UAX #14), alignment, leading,
-//! tracking, baseline shift, all caps / small caps, underline. Results are cached.
+//! tracking, baseline shift, all caps / small caps, underline, strikethrough. Results are cached.
 //!
 //! **Per-character styles** ([`layout_rich`]): [`StyleRun`]s give byte ranges their own
-//! [`TextStyle`] (font, size, tracking, baseline shift, faux bold / italic, caps, underline).
+//! [`TextStyle`] (font, size, tracking, baseline shift, faux bold / italic, caps, underline,
+//! strikethrough).
 //! Runs split shaping items, lines grow to fit the largest run, and every [`Glyph`] carries the
 //! index of the style it was set in (`run`: 0 = the base style, `i + 1` = `runs[i]`), so renderers
 //! can colour runs.
@@ -60,6 +61,7 @@ pub struct TextStyle {
     pub faux_italic: bool,
     pub caps: Caps,
     pub underline: bool,
+    pub strikethrough: bool,
 }
 
 impl Default for TextStyle {
@@ -76,6 +78,7 @@ impl Default for TextStyle {
             faux_italic: false,
             caps: Caps::Normal,
             underline: false,
+            strikethrough: false,
         }
     }
 }
@@ -109,7 +112,7 @@ impl Hash for TextStyle {
         hf(h, self.size);
         hf(h, self.tracking);
         hf(h, self.baseline_shift);
-        (self.kerning, self.ligatures, self.faux_bold, self.faux_italic, self.caps, self.underline).hash(h);
+        (self.kerning, self.ligatures, self.faux_bold, self.faux_italic, self.caps, self.underline, self.strikethrough).hash(h);
     }
 }
 
@@ -208,9 +211,9 @@ pub struct Line {
 pub struct Layout {
     pub glyphs: Vec<Glyph>,
     pub lines: Vec<Line>,
-    /// Underline rectangles `[x0, y0, x1, y1]`.
+    /// Decoration rectangles `[x0, y0, x1, y1]`: per line its underlines, then its strikethroughs.
     pub underlines: Vec<[f32; 4]>,
-    /// Style index of each underline (parallel to `underlines`; see [`Glyph::run`]).
+    /// Style index of each decoration (parallel to `underlines`; see [`Glyph::run`]).
     pub underline_runs: Vec<u16>,
     /// Logical bounds `[x0, y0, x1, y1]` (line boxes; area text spans the box width).
     pub bounds: [f32; 4],
@@ -374,6 +377,8 @@ struct ParaLine {
     descent: f32,
     /// Underlined segments `(x0, x1, style)` in line coordinates (rich layouts only).
     underlines: Vec<(f32, f32, u16)>,
+    /// Struck-through segments, like `underlines`.
+    strikes: Vec<(f32, f32, u16)>,
 }
 
 /// Lay out one paragraph (no newlines) whose text starts at byte `base` of the whole string.
@@ -391,6 +396,7 @@ fn paragraph(text: &str, base: usize, sty: &Styles, para: &ParagraphStyle) -> Ve
             ascent: pm.ascent,
             descent: pm.descent,
             underlines: Vec::new(),
+            strikes: Vec::new(),
         }];
     }
     let default_level = para.rtl.map(|r| if r { Level::rtl() } else { Level::ltr() });
@@ -563,25 +569,32 @@ fn paragraph(text: &str, base: usize, sty: &Styles, para: &ParagraphStyle) -> Ve
                 carets.push((base + b, x));
             }
         }
-        // underlined segments of rich layouts: clusters set in an underlined style, merged
-        let mut underlines: Vec<(f32, f32, u16)> = Vec::new();
-        if sty.rich() {
+        // decorated segments of rich layouts: clusters set in an underlined (struck-through)
+        // style, merged
+        let segments = |on: fn(&TextStyle) -> bool| {
+            let mut merged: Vec<(f32, f32, u16)> = Vec::new();
+            if !sty.rich() {
+                return merged;
+            }
             let mut segs: Vec<(f32, f32, u16)> = cluster_starts
                 .iter()
                 .filter(|&&cs| cs < ce)
                 .filter_map(|&cs| {
                     let si = sty.at(base + cs);
-                    sty.list[si].underline.then(|| (extents[&cs].0, extents[&cs].1, si as u16))
+                    on(sty.list[si]).then(|| (extents[&cs].0, extents[&cs].1, si as u16))
                 })
                 .collect();
             segs.sort_by(|a, b| a.0.total_cmp(&b.0));
             for sg in segs {
-                match underlines.last_mut() {
+                match merged.last_mut() {
                     Some(l) if l.2 == sg.2 && (sg.0 - l.1).abs() < 0.01 => l.1 = l.1.max(sg.1),
-                    _ => underlines.push(sg),
+                    _ => merged.push(sg),
                 }
             }
-        }
+            merged
+        };
+        let underlines = segments(|s| s.underline);
+        let strikes = segments(|s| s.strikethrough);
         // end of line caret
         let end_x = match chars.iter().rev().find(|c| c.0 < r.end && c.0 >= r.start) {
             Some(c) => {
@@ -621,6 +634,7 @@ fn paragraph(text: &str, base: usize, sty: &Styles, para: &ParagraphStyle) -> Ve
             ascent: asc,
             descent: desc,
             underlines,
+            strikes,
         });
     }
     out
@@ -689,10 +703,24 @@ pub fn layout_rich_uncached(text: &str, style: &TextStyle, runs: &[StyleRun], pa
                 lay.underlines.push([x0 + shift, y, x1 + shift, y + um.underline_thickness]);
                 lay.underline_runs.push(*si);
             }
-        } else if style.underline && pl.content > 0.0 {
-            let y = baseline - style.baseline_shift + pm.underline_pos;
-            lay.underlines.push([x, y, x + pl.content, y + pm.underline_thickness]);
-            lay.underline_runs.push(0);
+            for (x0, x1, si) in &pl.strikes {
+                let st = sty.list[*si as usize];
+                let sm = fonts::face(sty.prim[*si as usize].face).metrics(st.size);
+                let y = baseline - st.baseline_shift + sm.strikethrough_pos;
+                lay.underlines.push([x0 + shift, y, x1 + shift, y + sm.strikethrough_thickness]);
+                lay.underline_runs.push(*si);
+            }
+        } else if pl.content > 0.0 {
+            if style.underline {
+                let y = baseline - style.baseline_shift + pm.underline_pos;
+                lay.underlines.push([x, y, x + pl.content, y + pm.underline_thickness]);
+                lay.underline_runs.push(0);
+            }
+            if style.strikethrough {
+                let y = baseline - style.baseline_shift + pm.strikethrough_pos;
+                lay.underlines.push([x, y, x + pl.content, y + pm.strikethrough_thickness]);
+                lay.underline_runs.push(0);
+            }
         }
         lay.lines.push(Line {
             range: pl.range,
@@ -771,7 +799,8 @@ fn layout_vertical(text: &str, sty: &Styles, para: &ParagraphStyle, col_w: f32,
                 }
                 g
             }));
-            // Underline runs down the column, to the right of the cell (the "below" side of vertical-rl).
+            // Underline runs down the column, to the right of the cell (the "below" side of vertical-rl);
+            // strikethrough runs down the middle of the cell.
             let y_top = baseline - pl.ascent;
             if rich {
                 for (_x0, x1, si) in &pl.underlines {
@@ -781,10 +810,25 @@ fn layout_vertical(text: &str, sty: &Styles, para: &ParagraphStyle, col_w: f32,
                     lay.underlines.push([x, y_top, x + um.underline_thickness, y_top + row]);
                     lay.underline_runs.push(*si);
                 }
-            } else if style.underline && pl.content > 0.0 {
-                let x = shift + pl.left_trim + pl.content + pm.underline_pos;
-                lay.underlines.push([x, y_top, x + pm.underline_thickness, y_top + row]);
-                lay.underline_runs.push(0);
+                for (x0, x1, si) in &pl.strikes {
+                    let st = sty.list[*si as usize];
+                    let t = fonts::face(sty.prim[*si as usize].face).metrics(st.size).strikethrough_thickness;
+                    let x = (x0 + x1 - t) / 2.0 + shift - st.baseline_shift;
+                    lay.underlines.push([x, y_top, x + t, y_top + row]);
+                    lay.underline_runs.push(*si);
+                }
+            } else if pl.content > 0.0 {
+                if style.underline {
+                    let x = shift + pl.left_trim + pl.content + pm.underline_pos;
+                    lay.underlines.push([x, y_top, x + pm.underline_thickness, y_top + row]);
+                    lay.underline_runs.push(0);
+                }
+                if style.strikethrough {
+                    let t = pm.strikethrough_thickness;
+                    let x = shift + pl.left_trim + (pl.content - t) / 2.0;
+                    lay.underlines.push([x, y_top, x + t, y_top + row]);
+                    lay.underline_runs.push(0);
+                }
             }
             lay.lines.push(Line {
                 range: pl.range,
diff --git a/crates/text/src/layout/tests.rs b/crates/text/src/layout/tests.rs
--- a/crates/text/src/layout/tests.rs
+++ b/crates/text/src/layout/tests.rs
@@ -188,6 +188,50 @@ fn vertical_underline_runs_down_the_column() {
     assert_eq!(rich.underline_runs, vec![1]);
 }
 
+#[test]
+fn strikethrough_crosses_the_glyphs() {
+    let s = TextStyle { strikethrough: true, ..st(60.0) };
+    let l = layout("Hx", &s, &ParagraphStyle::default());
+    assert_eq!(l.underlines.len(), 1);
+    let [x0, y0, x1, y1] = l.underlines[0];
+    let line = &l.lines[0];
+    assert!((x0 - line.x).abs() < 0.01 && (x1 - line.x - line.width).abs() < 0.01, "spans the line");
+    // above the baseline (y down) and below the x-height
+    let xh = fonts::face(fonts::resolve(&s.family, &s.style).face).metrics(60.0).x_height;
+    assert!(y1 < line.baseline && y0 > line.baseline - xh, "{y0} {y1} baseline {} x-height {xh}", line.baseline);
+    assert!(y1 - y0 > 0.5, "has a thickness");
+    // with underline: two strips, the underline below the baseline
+    let both = layout("Hx", &TextStyle { underline: true, ..s.clone() }, &ParagraphStyle::default());
+    assert_eq!(both.underlines.len(), 2);
+    assert!(both.underlines[0][1] > line.baseline && both.underlines[1][3] < line.baseline);
+    // vertical: a tall strip through the middle of the cell
+    let v = layout("A", &s, &ParagraphStyle { vertical: true, ..Default::default() });
+    let [vx0, vy0, vx1, vy1] = v.underlines[0];
+    let vl = &v.lines[0];
+    assert!(vy1 - vy0 > vx1 - vx0, "vertical strikethrough is a tall strip");
+    assert!(((vx0 + vx1) / 2.0 - (vl.x + vl.width / 2.0)).abs() < 0.5, "centred on the cell");
+}
+
+#[test]
+fn style_runs_strike_through_only_their_range() {
+    let base = st(40.0);
+    let runs = [StyleRun { range: 3..5, style: TextStyle { strikethrough: true, ..base.clone() } }];
+    let l = layout_rich("ab cd ef", &base, &runs, &ParagraphStyle::default());
+    assert_eq!(l.underlines.len(), 1, "only the struck-through run");
+    assert_eq!(l.underline_runs, vec![1]);
+    let u = l.underlines[0];
+    assert!(u[0] >= l.caret(3).0 - 0.01 && u[2] <= l.caret(5).0 + 0.01, "{u:?}");
+    assert!(u[3] < l.lines[0].baseline, "above the baseline");
+    let v = layout_rich(
+        "ab",
+        &base,
+        &[StyleRun { range: 0..1, style: TextStyle { strikethrough: true, ..base.clone() } }],
+        &ParagraphStyle { vertical: true, ..Default::default() },
+    );
+    assert_eq!(v.underlines.len(), 1);
+    assert_eq!(v.underline_runs, vec![1]);
+}
+
 #[test]
 fn vertical_cells_keep_combining_marks_with_their_base() {
     let style = st(32.0);
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -1075,6 +1075,7 @@
 	All Caps	Großbuchstaben
 	Small Caps	Kapitälchen
 	Underline	Unterstrichen
+	Strikethrough	Durchgestrichen
 	OpenType	OpenType
 	Kerning	Unterschneidung
 	Ligatures	Ligaturen
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -1048,6 +1048,7 @@
 	All Caps	Todo mayúsculas
 	Small Caps	Versalitas
 	Underline	Subrayado
+	Strikethrough	Tachado
 	OpenType	OpenType
 	Kerning	Ajuste entre pares
 	Ligatures	Ligaduras
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -1040,6 +1040,7 @@
 	All Caps	Tout en capitales
 	Small Caps	Petites capitales
 	Underline	Souligné
+	Strikethrough	Barré
 	OpenType	OpenType
 	Kerning	Crénage
 	Ligatures	Ligatures
diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv
--- a/crates/ui-egui/src/i18n/id.tsv
+++ b/crates/ui-egui/src/i18n/id.tsv
@@ -1045,6 +1045,7 @@
 	All Caps	Semua Kapital
 	Small Caps	Kapital Kecil
 	Underline	Garis Bawah
+	Strikethrough	Coret
 	OpenType	OpenType
 	Kerning	Kerning
 	Ligatures	Ligatur
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -1037,6 +1037,7 @@
 	All Caps	Tutto maiuscolo
 	Small Caps	Maiuscoletto
 	Underline	Sottolineato
+	Strikethrough	Barrato
 	OpenType	OpenType
 	Kerning	Crenatura
 	Ligatures	Legature
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -1042,6 +1042,7 @@
 	All Caps	すべて大文字
 	Small Caps	スモールキャップ
 	Underline	下線
+	Strikethrough	取り消し線
 	OpenType	OpenType
 	Kerning	カーニング
 	Ligatures	合字
diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv
--- a/crates/ui-egui/src/i18n/ko.tsv
+++ b/crates/ui-egui/src/i18n/ko.tsv
@@ -3297,6 +3297,7 @@
 	Ultra Key	울트라 키
 	Uncompressed (PCM)	비압축(PCM)
 	Underline	밑줄
+	Strikethrough	취소선
 	Undo	실행 취소
 	Undock Panel	패널 도킹 해제
 	Ungroup	그룹 해제
diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv
--- a/crates/ui-egui/src/i18n/pt-br.tsv
+++ b/crates/ui-egui/src/i18n/pt-br.tsv
@@ -1042,6 +1042,7 @@
 	All Caps	Tudo em maiúsculas
 	Small Caps	Versaletes
 	Underline	Sublinhado
+	Strikethrough	Tachado
 	OpenType	OpenType
 	Kerning	Kerning
 	Ligatures	Ligaduras
diff --git a/crates/ui-egui/src/i18n/zh-cn.tsv b/crates/ui-egui/src/i18n/zh-cn.tsv
--- a/crates/ui-egui/src/i18n/zh-cn.tsv
+++ b/crates/ui-egui/src/i18n/zh-cn.tsv
@@ -1036,6 +1036,7 @@
 	All Caps	全大写
 	Small Caps	小型大写
 	Underline	下划线
+	Strikethrough	删除线
 	OpenType	OpenType
 	Kerning	字距调整
 	Ligatures	连字
diff --git a/crates/ui-egui/src/panels/graphics.rs b/crates/ui-egui/src/panels/graphics.rs
--- a/crates/ui-egui/src/panels/graphics.rs
+++ b/crates/ui-egui/src/panels/graphics.rs
@@ -227,6 +227,7 @@ mod ime_byte_delete_tests {
             faux_italic: false,
             caps: 0,
             underline: false,
+            strikethrough: false,
             box_width: 0.0,
             box_height: 0.0,
             vertical: false,
diff --git a/crates/ui-egui/src/panels/graphics/layer_identity.rs b/crates/ui-egui/src/panels/graphics/layer_identity.rs
--- a/crates/ui-egui/src/panels/graphics/layer_identity.rs
+++ b/crates/ui-egui/src/panels/graphics/layer_identity.rs
@@ -116,6 +116,7 @@ const LAYOUT_PARAMS: &[&str] = &[
     "faux_italic",
     "caps",
     "underline",
+    "strikethrough",
     "box_width",
     "box_height",
     "vertical",
@@ -245,6 +246,7 @@ fn layout_fingerprint(effect: &EffectInstance, t: Tick) -> u64 {
             hash_option(&mut h, run.style.faux_bold, |h, v| hash_bytes(h, &[u8::from(v)]));
             hash_option(&mut h, run.style.faux_italic, |h, v| hash_bytes(h, &[u8::from(v)]));
             hash_option(&mut h, run.style.underline, |h, v| hash_bytes(h, &[u8::from(v)]));
+            hash_option(&mut h, run.style.strikethrough, |h, v| hash_bytes(h, &[u8::from(v)]));
             hash_option(&mut h, run.style.tracking, hash_f32);
             hash_option(&mut h, run.style.baseline_shift, hash_f32);
             hash_option(&mut h, run.style.caps, |h, v| hash_bytes(h, &u64::from(v).to_le_bytes()));
diff --git a/crates/ui-egui/src/panels/graphics/properties.rs b/crates/ui-egui/src/panels/graphics/properties.rs
--- a/crates/ui-egui/src/panels/graphics/properties.rs
+++ b/crates/ui-egui/src/panels/graphics/properties.rs
@@ -48,6 +48,7 @@ fn char_style_key(id: &str) -> Option<&'static str> {
         "faux_bold" => "bold",
         "faux_italic" => "italic",
         "underline" => "underline",
+        "strikethrough" => "strikethrough",
         "tracking" => "tracking",
         "baseline_shift" => "baselineShift",
         "caps" => "caps",
@@ -593,6 +594,7 @@ pub fn properties(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
                 ("all_caps", "TT", tl!("All Caps"), caps == 1, Tokens::ui(12.0)),
                 ("small_caps", "Tt", tl!("Small Caps"), caps == 2, Tokens::ui(12.0)),
                 ("underline", "U", tl!("Underline"), flag("underline"), Tokens::ui(13.0)),
+                ("strikethrough", "S", tl!("Strikethrough"), flag("strikethrough"), Tokens::ui(13.0)),
             ] {
                 let r = letter_button(&mut vui, label, tip, on, &t, font);
                 if id == "faux_italic" {
@@ -600,6 +602,12 @@ pub fn properties(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
                     let c = r.rect.center();
                     vui.painter().line_segment([c + vec2(-5.0, 6.0), c + vec2(5.0, -6.0)], Stroke::new(0.8, t.text_faint));
                 }
+                if id == "strikethrough" {
+                    // the stroke through the letter
+                    let c = r.rect.center();
+                    let col = if on || r.hovered() { t.text } else { t.text_dim };
+                    vui.painter().line_segment([c + vec2(-6.0, 0.5), c + vec2(6.0, 0.5)], Stroke::new(1.0, col));
+                }
                 cx.auto(id, r.rect, tip);
                 if r.clicked() {
                     match id {
diff --git a/docs/graphics.md b/docs/graphics.md
--- a/docs/graphics.md
+++ b/docs/graphics.md
@@ -26,7 +26,7 @@ kerning and ligatures, bidi, line breaking, all drawn as vectors in linear light
 
 | Group | Properties (parameter ids) |
 |---|---|
-| Text | `text` (Source Text), `font`, `font_style`, `size` (px), `align` (Left/Center/Right/Justify), `tracking` (1/1000 em), `kerning`, `ligatures`, `leading` (px added to the natural line height), `baseline_shift`, `faux_bold`, `faux_italic`, `caps` (Normal/All Caps/Small Caps), `underline`, `box_width` (0 = point text, else paragraph text wrapped at that width), `box_height` (paragraph text: lines that do not fit are not shown; 0 = as tall as the text) |
+| Text | `text` (Source Text), `font`, `font_style`, `size` (px), `align` (Left/Center/Right/Justify), `tracking` (1/1000 em), `kerning`, `ligatures`, `leading` (px added to the natural line height), `baseline_shift`, `faux_bold`, `faux_italic`, `caps` (Normal/All Caps/Small Caps), `underline`, `strikethrough`, `box_width` (0 = point text, else paragraph text wrapped at that width), `box_height` (paragraph text: lines that do not fit are not shown; 0 = as tall as the text) |
 | Shape | `shape` (Rectangle/Ellipse/Polygon/Path), `size` [w, h], `sides`, `corner_radius` (one radius for all four corners, at most half the shorter side; on the Program monitor, drag one of the four small circles just inside a selected rectangle's corners), `points` (path vertices relative to the layer origin) |
 | Appearance | `fill`, `fill_color`; `stroke`, `stroke_color`, `stroke_width`, `stroke_type` (Outer/Center/Inner) and the same for `stroke2`; `background`, `background_color`, `background_opacity`, `background_size` (padding), `background_radius`; `shadow`, `shadow_color`, `shadow_opacity`, `shadow_angle` (135° = down-right), `shadow_distance`, `shadow_size`, `shadow_blur` |
 | Transform | `position` (graphic canvas px), `anchor` (layer px), `scale`, `scale_width`, `uniform_scale`, `rotation`, `opacity` |
@@ -139,7 +139,7 @@ With a graphic clip selected, the Properties panel (and Essential Graphics) show
 **Align and Transform** (six align buttons, two distribute buttons, position, anchor, scale,
 rotation, opacity), **Text** (Source Text field, font family and style, size, paragraph alignment,
 tracking, leading, baseline shift, box width and height, faux bold / faux italic / all caps /
-small caps / underline, kerning and ligatures; the wrench in the section's header opens **Text
+small caps / underline / strikethrough, kerning and ligatures; the wrench in the section's header opens **Text
 Properties**: Text Layer Type and Ligatures), **Shape**, and **Appearance** (fill, two strokes, background,
 shadow). Automation ids: `graphics.layers.<n>`, `graphics.prop.<parameter id>`,
 `graphics.align.<how>`, `graphics.distribute.<axis>`, `graphics.sourceText`, `graphics.NewTextLayer`,
@@ -150,7 +150,7 @@ shadow). Automation ids: `graphics.layers.<n>`, `graphics.prop.<parameter id>`,
 ## Per-character styles
 
 Select characters with the Type tool and change font, style, size, colour, faux bold / italic,
-underline, tracking, baseline shift or caps in the Properties panel: the change applies to the
+underline, strikethrough, tracking, baseline shift or caps in the Properties panel: the change applies to the
 selection only (`graphics.setCharStyle`). Styles are stored on the layer as runs of characters
 (`EffectInstance::layer.runs`, character offsets, sorted and non-overlapping) and follow the text
 when it is edited (typed characters take the style of the character before them). The text engine
@@ -159,7 +159,7 @@ lines grow to fit the largest run, and each run can have its own fill colour.
 
 | Command | Params |
 |---|---|
-| `graphics.setCharStyle` | `clip`, `layer`, `start`, `end` (characters; default the whole text), `style` {`font`, `fontStyle`, `size`, `color`, `bold`, `italic`, `underline`, `tracking`, `baselineShift`, `caps`}, `clear` (remove styles from the range) |
+| `graphics.setCharStyle` | `clip`, `layer`, `start`, `end` (characters; default the whole text), `style` {`font`, `fontStyle`, `size`, `color`, `bold`, `italic`, `underline`, `strikethrough`, `tracking`, `baselineShift`, `caps`}, `clear` (remove styles from the range) |
 
 ## Responsive design
 
