diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -365,6 +365,8 @@ Newest first.
 - **2026-10-23** · `designcraft-cli mcp` takes `--automation-read-root` and `--automation-write-root` more than once, as the suite's other apps do (#1552): a path inside any root of its kind passes, and the roots' common parent, `..` or a link out of them and the other kind's roots stay refused. No readiness percentage change.
 - **2026-10-23** · Layout › Go Back and Go Forward (Cmd+Page Up / Cmd+Page Down, Ctrl on Windows and Linux) retrace the jumps between pages (Go to Page, the page and spread commands, the Pages panel) as InDesign's do, returning to where the view was on each page at the current zoom; a new jump after going back drops the pages ahead, scrolling adds no entry, and pages deleted since are skipped. No readiness percentage change.
 - **2026-10-24** · The Story Editor has InDesign's vertical depth ruler between the style column and the text, and View › Story Editor › Show/Hide Depth Ruler (`view.storyEditorDepthRuler`) shows or hides it in the Story Editor, remembered with the preferences. The ruler is marked in the document's vertical ruler units with how deep the story's text fills its frames, counted through its columns and threaded frames (lines set side by side, in the sub-columns of a split paragraph or on both sides of a wrap object, count once), and is shaded down to the overset point, where a red line now crosses the editor. No readiness percentage change.
+- **2026-10-24** · View › Story Editor › Show/Hide Style Name Column (`view.storyEditorStyleColumn`) shows or hides the Story Editor's column of paragraph style names, as in InDesign, and dragging the bar between the column and the text makes the column wider or narrower (40 to 400 points; the command's `width` sets it too). Both settings are remembered with the preferences, so the editor opens with the same column the next time. No readiness percentage change.
+- **2026-10-24** · View › Story Editor › Show/Hide Paragraph Break Marks (`view.storyEditorParagraphMarks`) shows or hides a ¶ after each paragraph that a paragraph break ends in the Story Editor, as in InDesign. The marks are off until turned on, and the setting is remembered with the preferences. No readiness percentage change.
 - **2026-10-24** · Type › Notes › Previous Note and Next Note (`note.previous`, `note.next`): as in InDesign, they move the insertion point from note to note in the story, putting the caret after the note's anchor. Each item is unavailable when no note lies in its direction in the story, or when the insertion point is in a table cell or a footnote. No readiness percentage change.
 - **2026-10-24** · Type › Notes › Convert to Note (`note.convertFromText {author?}`): as in InDesign, the selected text leaves the story and becomes the text of a new note, whose anchor takes its place; Convert Note to Text puts it back. DesignCraft refuses a selection in a table cell, or one that holds a note, footnote, endnote, anchored object, index marker, text anchor, cross-reference or table. No readiness percentage change.
 - **2026-10-23** · Effects dialog: with Use Global Light on, the Angle field (and the Bevel and Emboss one) stays editable and shows the document's global light angle; a change there sets the global light for every effect that uses it, applied with the effects in one undo step (`object.effects` takes `globalLight`). Before, the field was greyed out and the angle couldn't be changed in the dialog. No readiness percentage change.
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
@@ -2287,7 +2287,27 @@ const TABLE: &[(&str, [&str; 5])] = &[
             "显示/隐藏面板(工具除外)",
         ],
     ),
+    (
+        "Show/Hide Paragraph Break Marks",
+        [
+            "Show/Hide Paragraph Break Marks",
+            "Afficher/Masquer les marques de fin de paragraphe",
+            "Show/Hide Paragraph Break Marks",
+            "Show/Hide Paragraph Break Marks",
+            "显示/隐藏段落结束标记",
+        ],
+    ),
     ("Show/Hide Rulers", ["Show/Hide Rulers", "Afficher/Masquer les règles", "Show/Hide Rulers", "Show/Hide Rulers", "显示/隐藏标尺"]),
+    (
+        "Show/Hide Style Name Column",
+        [
+            "Show/Hide Style Name Column",
+            "Afficher/Masquer la colonne des noms de style",
+            "Show/Hide Style Name Column",
+            "Show/Hide Style Name Column",
+            "显示/隐藏样式名称栏",
+        ],
+    ),
     (
         "Show/Hide Tagged Frames",
         [
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
@@ -1560,7 +1560,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "إظهار/إخفاء الأحرف المخفية"),
     ("Show/Hide Panels", "إظهار/إخفاء اللوحات"),
     ("Show/Hide Panels Except Tools", "إظهار/إخفاء اللوحات باستثناء الأدوات"),
+    ("Show/Hide Paragraph Break Marks", "إظهار/إخفاء علامات نهاية الفقرات"),
     ("Show/Hide Rulers", "إظهار/إخفاء المساطر"),
+    ("Show/Hide Style Name Column", "إظهار/إخفاء عمود أسماء الأنماط"),
     ("Show/Hide Tagged Frames", "إظهار/إخفاء الإطارات الموسومة"),
     ("Show/Hide Text Threads", "إظهار/إخفاء روابط الإطارات النصية"),
     ("Smooth", "تنعيم"),
diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs
--- a/crates/ui-egui/src/i18n/cs.rs
+++ b/crates/ui-egui/src/i18n/cs.rs
@@ -715,7 +715,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Zobrazit/skrýt skryté znaky"),
     ("Show/Hide Panels", "Zobrazit/skrýt panely"),
     ("Show/Hide Panels Except Tools", "Zobrazit/skrýt panely kromě nástrojů"),
+    ("Show/Hide Paragraph Break Marks", "Zobrazit/skrýt značky konce odstavce"),
     ("Show/Hide Rulers", "Zobrazit/skrýt pravítka"),
+    ("Show/Hide Style Name Column", "Zobrazit/skrýt sloupec názvů stylů"),
     ("Show/Hide Tagged Frames", "Zobrazit/skrýt otagované rámečky"),
     ("Show/Hide Text Threads", "Zobrazit/skrýt zřetězení textu"),
     ("Smooth", "Vyhladit"),
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
@@ -752,7 +752,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Verborgene Zeichen ein-/ausblenden"),
     ("Show/Hide Panels", "Bedienfelder ein-/ausblenden"),
     ("Show/Hide Panels Except Tools", "Bedienfelder außer Werkzeugen ein-/ausblenden"),
+    ("Show/Hide Paragraph Break Marks", "Absatzendemarken ein-/ausblenden"),
     ("Show/Hide Rulers", "Lineale ein-/ausblenden"),
+    ("Show/Hide Style Name Column", "Spalte mit Formatnamen ein-/ausblenden"),
     ("Show/Hide Tagged Frames", "Getaggte Rahmen ein-/ausblenden"),
     ("Show/Hide Text Threads", "Textverkettungen ein-/ausblenden"),
     ("Smooth", "Glätten"),
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
@@ -844,7 +844,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Mostra/nascondi caratteri nascosti"),
     ("Show/Hide Panels", "Mostra/nascondi pannelli"),
     ("Show/Hide Panels Except Tools", "Mostra/nascondi pannelli tranne Strumenti"),
+    ("Show/Hide Paragraph Break Marks", "Mostra/nascondi segni di fine paragrafo"),
     ("Show/Hide Rulers", "Mostra/nascondi righelli"),
+    ("Show/Hide Style Name Column", "Mostra/nascondi colonna nomi stili"),
     ("Show/Hide Tagged Frames", "Mostra/nascondi cornici con tag"),
     ("Show/Hide Text Threads", "Mostra/nascondi concatenamenti testo"),
     ("Smooth", "Arrotonda"),
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
@@ -945,7 +945,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "制御文字を表示・非表示"),
     ("Show/Hide Panels", "パネルを表示・非表示"),
     ("Show/Hide Panels Except Tools", "ツール以外のパネルを表示・非表示"),
+    ("Show/Hide Paragraph Break Marks", "段落の終わりのマークを表示・非表示"),
     ("Show/Hide Rulers", "定規を表示・非表示"),
+    ("Show/Hide Style Name Column", "スタイル名の列を表示・非表示"),
     ("Show/Hide Tagged Frames", "タグ付きフレームを表示・非表示"),
     ("Show/Hide Text Threads", "テキストフレームの連結を表示・非表示"),
     ("Smooth", "平滑化"),
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
@@ -771,7 +771,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "დამალული სიმბოლოების ჩვენება/დამალვა"),
     ("Show/Hide Panels", "პანელების ჩვენება/დამალვა"),
     ("Show/Hide Panels Except Tools", "პანელების ჩვენება/დამალვა, ხელსაწყოების გარდა"),
+    ("Show/Hide Paragraph Break Marks", "აბზაცის დასასრულის ნიშნების ჩვენება/დამალვა"),
     ("Show/Hide Rulers", "სახაზავების ჩვენება/დამალვა"),
+    ("Show/Hide Style Name Column", "სტილების სახელების სვეტის ჩვენება/დამალვა"),
     ("Show/Hide Tagged Frames", "ტეგირებული ჩარჩოების ჩვენება/დამალვა"),
     ("Show/Hide Text Threads", "ტექსტის კავშირების ჩვენება/დამალვა"),
     ("Smooth", "გლუვი"),
diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs
--- a/crates/ui-egui/src/i18n/pl.rs
+++ b/crates/ui-egui/src/i18n/pl.rs
@@ -722,7 +722,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Pokaż/ukryj ukryte znaki"),
     ("Show/Hide Panels", "Pokaż/ukryj panele"),
     ("Show/Hide Panels Except Tools", "Pokaż/ukryj panele oprócz narzędzi"),
+    ("Show/Hide Paragraph Break Marks", "Pokaż/ukryj znaczniki końca akapitu"),
     ("Show/Hide Rulers", "Pokaż/ukryj linijki"),
+    ("Show/Hide Style Name Column", "Pokaż/ukryj kolumnę nazw stylów"),
     ("Show/Hide Tagged Frames", "Pokaż/ukryj ramki z tagami"),
     ("Show/Hide Text Threads", "Pokaż/ukryj powiązania tekstu"),
     ("Smooth", "Wygładź"),
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
@@ -1690,7 +1690,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Mostrar/ocultar caracteres ocultos"),
     ("Show/Hide Panels", "Mostrar/ocultar painéis"),
     ("Show/Hide Panels Except Tools", "Mostrar/ocultar painéis, exceto ferramentas"),
+    ("Show/Hide Paragraph Break Marks", "Mostrar/ocultar marcas de fim de parágrafo"),
     ("Show/Hide Rulers", "Mostrar/ocultar réguas"),
+    ("Show/Hide Style Name Column", "Mostrar/ocultar coluna de nomes de estilo"),
     ("Show/Hide Tagged Frames", "Mostrar/ocultar quadros marcados"),
     ("Show/Hide Text Threads", "Mostrar/ocultar encadeamentos de texto"),
     ("Sort", "Ordenar"),
diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs
--- a/crates/ui-egui/src/i18n/ru.rs
+++ b/crates/ui-egui/src/i18n/ru.rs
@@ -702,7 +702,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Показать/скрыть скрытые символы"),
     ("Show/Hide Panels", "Показать/скрыть панели"),
     ("Show/Hide Panels Except Tools", "Показать/скрыть панели, кроме инструментов"),
+    ("Show/Hide Paragraph Break Marks", "Показать/скрыть знаки конца абзаца"),
     ("Show/Hide Rulers", "Показать/скрыть линейки"),
+    ("Show/Hide Style Name Column", "Показать/скрыть столбец имен стилей"),
     ("Show/Hide Tagged Frames", "Показать/скрыть рамки с тегами"),
     ("Show/Hide Text Threads", "Показать/скрыть связи текстовых рамок"),
     ("Smooth", "Сгладить"),
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
@@ -847,7 +847,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Show/Hide Hidden Characters", "Показати/приховати приховані символи"),
     ("Show/Hide Panels", "Показати/приховати панелі"),
     ("Show/Hide Panels Except Tools", "Показати/приховати панелі, крім інструментів"),
+    ("Show/Hide Paragraph Break Marks", "Показати/приховати знаки кінця абзацу"),
     ("Show/Hide Rulers", "Показати/приховати лінійки"),
+    ("Show/Hide Style Name Column", "Показати/приховати стовпець назв стилів"),
     ("Show/Hide Tagged Frames", "Показати/приховати фрейми з тегами"),
     ("Show/Hide Text Threads", "Показати/приховати зв’язки текстових фреймів"),
     ("Smooth", "Згладити"),
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -243,6 +243,13 @@ pub struct UiState {
     pub story_editor_size: f32,
     /// View › Story Editor › Show/Hide Depth Ruler.
     pub story_editor_depth_ruler: bool,
+    /// View › Story Editor › Show/Hide Style Name Column.
+    pub story_editor_style_column: bool,
+    /// Width of the Story Editor's style name column, in screen points (set by dragging its bar or by
+    /// `view.storyEditorStyleColumn` `width`).
+    pub story_editor_style_width: f32,
+    /// View › Story Editor › Show/Hide Paragraph Break Marks.
+    pub story_editor_paragraph_marks: bool,
     /// Preferences › General › Interface Language: supported codes are listed in `i18n::LANGUAGES`.
     pub language: String,
     /// Edit › Transparency Flattener Presets: "" (none), "high", "medium" or "low" for PDF export.
@@ -394,6 +401,9 @@ impl Default for UiState {
             show_full_menus: false,
             story_editor_size: 14.0,
             story_editor_depth_ruler: true,
+            story_editor_style_column: true,
+            story_editor_style_width: story_editor::STYLE_COLUMN_WIDTH,
+            story_editor_paragraph_marks: false,
             scripts: vec![
                 ("Number the pages".into(), "# A page-number frame at the bottom of the current page.\nframe.create {\"rect\": [288, 740, 324, 760], \"content\": \"text\", \"text\": \"\"}\ntext.select {\"story\": \"$0.story\", \"anchor\": 0, \"focus\": 0}\ntext.insert {\"text\": \"\\ue000\", \"raw\": true}".into()),
                 ("Two-column grid".into(), "# Four text frames in a 2 × 2 grid.\nframe.grid {\"rect\": [36, 36, 576, 756], \"cols\": 2, \"rows\": 2, \"gutter\": 12}".into()),
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
@@ -215,12 +215,24 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
     ("view.textThreads", "Show/Hide Text Threads", Some("Cmd+Alt+Y"), "{}"),
     ("view.hiddenCharacters", "Show Hidden Characters", Some("Cmd+Alt+I"), "{}"),
     ("view.taggedFrames", "Show/Hide Tagged Frames", None, "{} — XML-tagged frames outlined in their tag colour"),
+    (
+        "view.storyEditorStyleColumn",
+        "Show/Hide Style Name Column",
+        None,
+        "{on?: bool, width?: 40–400 (points)} — the Story Editor's column of paragraph style names; toggles without either → {on, width}",
+    ),
     (
         "view.storyEditorDepthRuler",
         "Show/Hide Depth Ruler",
         None,
         "{} — the Story Editor's vertical ruler of how deep the text reaches in its frames",
     ),
+    (
+        "view.storyEditorParagraphMarks",
+        "Show/Hide Paragraph Break Marks",
+        None,
+        "{} — a ¶ after each paragraph that a paragraph break ends, in the Story Editor",
+    ),
     ("window.hidePanels", "Show/Hide Panels", Some("Tab"), "{} — every panel, the Tools panel included"),
     ("window.hidePanelsExceptTools", "Show/Hide Panels Except Tools", Some("Shift+Tab"), "{}"),
     ("window.nextDocument", "Next Document", Some("Cmd+F6"), "{}"),
@@ -867,7 +879,9 @@ pub const MENUS: &[(&str, &[&str])] = &[
             "ui:view.tagMarkers",
             "<",
             ">Story Editor",
+            "ui:view.storyEditorStyleColumn",
             "ui:view.storyEditorDepthRuler",
+            "ui:view.storyEditorParagraphMarks",
             "<",
         ],
     ),
@@ -1587,7 +1601,25 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
         "view.textThreads" => flag(&mut app.ui.text_threads),
         "view.hiddenCharacters" => flag(&mut app.ui.hidden_characters),
         "view.taggedFrames" => flag(&mut app.ui.tagged_frames),
+        "view.storyEditorStyleColumn" => {
+            let on = p.get("on");
+            let width = p.get("width");
+            if on.is_some_and(|v| !v.is_boolean()) || width.is_some_and(|v| !v.is_number()) {
+                return Some(Err(format!("{id}: `on` is a bool and `width` a number")));
+            }
+            match (on.and_then(Value::as_bool), width.and_then(Value::as_f64)) {
+                (None, None) => app.ui.story_editor_style_column = !app.ui.story_editor_style_column,
+                (on, width) => {
+                    app.ui.story_editor_style_column = on.unwrap_or(app.ui.story_editor_style_column);
+                    if let Some(w) = width {
+                        app.ui.story_editor_style_width = crate::story_editor::style_column_width(w as f32);
+                    }
+                }
+            }
+            Ok(json!({"on": app.ui.story_editor_style_column, "width": app.ui.story_editor_style_width}))
+        }
         "view.storyEditorDepthRuler" => flag(&mut app.ui.story_editor_depth_ruler),
+        "view.storyEditorParagraphMarks" => flag(&mut app.ui.story_editor_paragraph_marks),
         "window.hidePanels" | "window.hidePanelsExceptTools" => {
             let mode = if id == "window.hidePanels" { 1 } else { 2 };
             app.ui.hidden_panels = if app.ui.hidden_panels == mode { 0 } else { mode };
@@ -2475,7 +2507,9 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> {
         "view.textThreads" => app.ui.text_threads,
         "view.hiddenCharacters" => app.ui.hidden_characters,
         "view.taggedFrames" => app.ui.tagged_frames,
+        "view.storyEditorStyleColumn" => app.ui.story_editor_style_column,
         "view.storyEditorDepthRuler" => app.ui.story_editor_depth_ruler,
+        "view.storyEditorParagraphMarks" => app.ui.story_editor_paragraph_marks,
         "changes.track" => app.session.active().is_some_and(|d| d.doc.settings.track_changes),
         // Pressed, as the Bulleted and Numbered List buttons: every paragraph the command applies
         // to (selected text, cells or text frames) has it.
@@ -3415,7 +3449,9 @@ mod tests {
             "view.separations",
             "view.taggedFrames",
             "view.tagMarkers",
+            "view.storyEditorStyleColumn",
             "view.storyEditorDepthRuler",
+            "view.storyEditorParagraphMarks",
         ] {
             assert!(view.contains(&id), "{id} is in View");
             assert!(!typ.contains(&id), "{id} isn't in Type");
diff --git a/crates/ui-egui/src/story_editor.rs b/crates/ui-egui/src/story_editor.rs
--- a/crates/ui-egui/src/story_editor.rs
+++ b/crates/ui-egui/src/story_editor.rs
@@ -11,12 +11,21 @@ use designcraft_doc::StoryId;
 use crate::DesignApp;
 use crate::theme::Tokens;
 
+/// Width of the style name column before its bar is dragged, in screen points.
+pub const STYLE_COLUMN_WIDTH: f32 = 130.0;
+/// Narrowest and widest the style name column can be dragged, in screen points.
+const STYLE_COLUMN_RANGE: (f32, f32) = (40.0, 400.0);
+/// Width of the bar between the style name column and the text, in screen points.
+const STYLE_BAR_WIDTH: f32 = 6.0;
 /// Width of the depth ruler, in screen points.
 const RULER_WIDTH: f32 = 34.0;
 /// Least distance between two of the depth ruler's labels, in screen points.
 const RULER_LABEL_GAP: f32 = 10.0;
 /// Color of the overset line and label.
 const OVERSET_RED: egui::Color32 = egui::Color32::from_rgb(230, 40, 40);
+/// The mark after each paragraph that a paragraph break ends while View › Story Editor › Show Paragraph
+/// Break Marks is on, as Type › Show Hidden Characters marks paragraph ends on the canvas.
+const PARAGRAPH_MARK: &str = "¶";
 
 /// The story open in the Story Editor. Story ids are counted per document, so the editor is bound
 /// to the document the story belongs to.
@@ -51,8 +60,10 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
     let overset = cs.overset_at;
     let depths = app.ui.story_editor_depth_ruler.then(|| line_depths(&cs));
     let unit = st.doc.settings.vertical_units;
+    let style_width = app.ui.story_editor_style_column.then(|| style_column_width(app.ui.story_editor_style_width));
     let mut open = true;
     let mut edit: Option<(usize, usize, String)> = None;
+    let mut dragged_width: Option<f32> = None;
     let words = story.text.split_whitespace().count().to_string();
     egui::Window::new(crate::i18n::tr(&app.ui.language, "Story Editor — {count} words").replace("{count}", &words))
         .id(egui::Id::new("story_editor"))
@@ -61,27 +72,55 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
         .show(ctx, |ui| {
             egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
                 ui.horizontal_top(|ui| {
-                    // Style column: the labels are painted after the text, at the rows the text really wraps to.
-                    let (column, _) = ui.allocate_exact_size(vec2(130.0, 0.0), egui::Sense::hover());
-                    ui.separator();
+                    // Style column: the labels are painted after the text, at the rows the text really wraps to,
+                    // and so is the bar that resizes it.
+                    let column = style_width.map(|w| {
+                        let column = ui.allocate_exact_size(vec2(w, 0.0), egui::Sense::hover()).0;
+                        let bar = ui.allocate_exact_size(vec2(STYLE_BAR_WIDTH, 0.0), egui::Sense::hover()).0;
+                        (column, bar)
+                    });
                     // Depth ruler: painted after the text too, beside the rows the lines start on.
                     let ruler = depths.as_ref().map(|_| ui.allocate_exact_size(vec2(RULER_WIDTH, 0.0), egui::Sense::hover()).0);
                     let mut text = story.text.clone();
+                    let font = egui::FontId::proportional(app.ui.story_editor_size.clamp(8.0, 36.0));
+                    // With paragraph break marks shown, the text wraps one text size short of the edge, so a
+                    // mark after a full row stays within the width the text has without marks.
+                    let width = if app.ui.story_editor_paragraph_marks { (ui.available_width() - font.size).max(font.size) } else { f32::INFINITY };
+                    // The text field has a fixed id, so it keeps its focus and caret when the columns before it
+                    // are shown or hidden.
                     let out = egui::TextEdit::multiline(&mut text)
-                        .font(egui::FontId::proportional(app.ui.story_editor_size.clamp(8.0, 36.0)))
-                        .desired_width(f32::INFINITY)
+                        .id(egui::Id::new("story_editor_text"))
+                        .font(font.clone())
+                        .desired_width(width)
                         .desired_rows(20)
                         .frame(egui::Frame::NONE)
                         .show(ui);
                     let r = out.response;
-                    let starts = paragraph_start_offsets(out.galley.rows.iter().map(|row| (row.pos.y, row.ends_with_newline)));
-                    for (i, y) in starts.into_iter().enumerate() {
-                        let Some(para) = story.paras.get(i) else { break };
-                        let rect = egui::Rect::from_min_size(egui::pos2(column.min.x, out.galley_pos.y + y), vec2(130.0, 14.0));
-                        ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
-                            let name = crate::i18n::style_name(&app.ui.language, &para.style);
-                            crate::rtl::label(ui, egui::RichText::new(name).size(11.0).color(t.text_dim));
-                        });
+                    if let Some((column, bar)) = column {
+                        let starts = paragraph_start_offsets(out.galley.rows.iter().map(|row| (row.pos.y, row.ends_with_newline)));
+                        for (i, y) in starts.into_iter().enumerate() {
+                            let Some(para) = story.paras.get(i) else { break };
+                            let rect = egui::Rect::from_min_size(egui::pos2(column.min.x, out.galley_pos.y + y), vec2(column.width(), 14.0));
+                            ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
+                                let name = crate::i18n::style_name(&app.ui.language, &para.style);
+                                crate::rtl::label(ui, egui::RichText::new(name).size(11.0).color(t.text_dim));
+                            });
+                        }
+                        // The bar between the column and the text: drag it to make the column wider or narrower.
+                        let bar = egui::Rect::from_x_y_ranges(bar.x_range(), r.rect.y_range());
+                        let drag = ui.interact(bar, egui::Id::new("story_editor_style_bar"), egui::Sense::drag());
+                        let line = egui::Rect::from_center_size(bar.center(), vec2(1.0, bar.height()));
+                        ui.painter().rect_filled(line, 0.0, if drag.hovered() || drag.dragged() { t.text_dim } else { t.section_divider });
+                        let drag = drag.on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
+                        if drag.dragged() {
+                            dragged_width = Some(style_column_width(column.width() + drag.drag_delta().x));
+                        }
+                        drag.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "Style name column divider"));
+                    }
+                    if app.ui.story_editor_paragraph_marks {
+                        for spot in paragraph_mark_spots(&out.galley) {
+                            ui.painter().text(out.galley_pos + spot.to_vec2(), egui::Align2::LEFT_TOP, PARAGRAPH_MARK, font.clone(), t.text_dim);
+                        }
                     }
                     let overset_y = overset.map(|o| {
                         let ci = char_offsets(&story.text, std::iter::once(o));
@@ -141,6 +180,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
                 }
             });
         });
+    if let Some(w) = dragged_width {
+        let _ = app.run("view.storyEditorStyleColumn", json!({"width": w}));
+    }
     if let Some((a, b, text)) = edit {
         let _ = app.run("story.replaceRange", json!({"story": sid.0, "start": a, "end": b, "text": text}));
     }
@@ -149,6 +191,18 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
     }
 }
 
+/// The style name column's width for a saved or dragged `width`: kept between the narrowest and
+/// widest the bar allows, and the starting width when `width` is not a number.
+pub(crate) fn style_column_width(width: f32) -> f32 {
+    if width.is_finite() { width.clamp(STYLE_COLUMN_RANGE.0, STYLE_COLUMN_RANGE.1) } else { STYLE_COLUMN_WIDTH }
+}
+
+/// Where the paragraph break marks go, in galley coordinates (each mark's top left): just after the
+/// last row of every paragraph that a paragraph break ends, past any spaces at its end.
+fn paragraph_mark_spots(galley: &egui::Galley) -> Vec<egui::Pos2> {
+    galley.rows.iter().filter(|row| row.ends_with_newline).map(|row| pos2(row.rect().max.x + 1.0, row.rect().min.y)).collect()
+}
+
 /// Vertical offset of the first laid-out row of each paragraph, from `(row y, row ends with a newline)`
 /// in layout order. A paragraph that wraps spans several rows, so counting characters per line
 /// drifts from what the editor draws; the rows themselves are the truth.
@@ -467,6 +521,212 @@ mod tests {
         assert!(!crate::UiState::from_preferences_json(&saved).unwrap().story_editor_depth_ruler);
     }
 
+    fn story_editor_harness(text: &str) -> egui_kittest::Harness<'static, crate::test_window::Window> {
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());
+        app.run("file.new", json!({})).unwrap();
+        let r = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": text, "caret": false})).unwrap();
+        app.run("app.storyEditor", json!({"story": r["story"].as_u64().unwrap()})).unwrap();
+        let mut h = crate::test_window::open(app, egui::vec2(1440.0, 900.0));
+        h.run_steps(2);
+        h
+    }
+
+    fn text_left(h: &egui_kittest::Harness<'_, crate::test_window::Window>, text: &str) -> f32 {
+        h.get_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput && n.value().as_deref() == Some(text)).rect().min.x
+    }
+
+    /// View › Story Editor › Show/Hide Style Name Column: hiding the column takes away the style
+    /// names and the bar beside them, and the text moves left by their width; the setting is
+    /// remembered.
+    #[test]
+    fn style_name_column_shows_and_hides_from_the_view_menu() {
+        let mut h = story_editor_harness("Column story");
+        assert!(h.state().app.ui.story_editor_style_column, "shown by default");
+        let app = &h.state().app;
+        let para_style = &app.session.active().unwrap().doc.stories.values().next().unwrap().paras[0].style;
+        let style = crate::i18n::style_name(&app.ui.language, para_style).to_string();
+        assert!(h.query_by_label(&style).is_some(), "the column names the paragraph's style {style:?}");
+        assert!(h.query_by_label("Style name column divider").is_some());
+        let with = text_left(&h, "Column story");
+        h.state_mut().app.run("view.storyEditorStyleColumn", json!({})).unwrap();
+        h.run_steps(2);
+        let without = text_left(&h, "Column story");
+        assert!(h.query_by_label(&style).is_none(), "no style names while the column is hidden");
+        assert!(h.query_by_label("Style name column divider").is_none());
+        assert!(
+            (with - without - super::STYLE_COLUMN_WIDTH - super::STYLE_BAR_WIDTH).abs() < 20.0,
+            "the text moves by the column: {with} vs {without}"
+        );
+        let app = &h.state().app;
+        assert_eq!(crate::menus::checked(app, "view.storyEditorStyleColumn", &json!({})), Some(false));
+        let saved = serde_json::to_vec(&app.ui).unwrap();
+        assert!(!crate::UiState::from_preferences_json(&saved).unwrap().story_editor_style_column);
+    }
+
+    /// Dragging the bar between the style names and the text resizes the column; the text moves
+    /// with the bar, and the width is remembered for the next time the editor opens.
+    #[test]
+    fn dragging_the_style_column_bar_resizes_the_column() {
+        use egui::{Event, Modifiers, PointerButton};
+        let mut h = story_editor_harness("Resize story");
+        let before = text_left(&h, "Resize story");
+        let a = h.get_by_label("Style name column divider").rect().center();
+        let b = a + egui::vec2(60.0, 0.0);
+        h.input_mut().events.push(Event::PointerMoved(a));
+        h.input_mut().events.push(Event::PointerButton { pos: a, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
+        h.step();
+        for k in 1..=4 {
+            h.input_mut().events.push(Event::PointerMoved(a + (b - a) * (k as f32 / 4.0)));
+            h.step();
+        }
+        h.input_mut().events.push(Event::PointerButton { pos: b, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
+        h.run_steps(3);
+        let width = h.state().app.ui.story_editor_style_width;
+        assert!((width - super::STYLE_COLUMN_WIDTH - 60.0).abs() < 1.0, "the column is 60 points wider: {width}");
+        let after = text_left(&h, "Resize story");
+        assert!((after - before - 60.0).abs() < 1.0, "the text moves with the bar: {before} → {after}");
+        let saved = serde_json::to_vec(&h.state().app.ui).unwrap();
+        assert_eq!(crate::UiState::from_preferences_json(&saved).unwrap().story_editor_style_width, width);
+    }
+
+    /// A saved width out of range, or not a number, gives a column the bar can still be dragged in.
+    #[test]
+    fn style_column_width_stays_in_range() {
+        assert_eq!(super::style_column_width(200.0), 200.0);
+        assert_eq!(super::style_column_width(-50.0), super::STYLE_COLUMN_RANGE.0);
+        assert_eq!(super::style_column_width(1e9), super::STYLE_COLUMN_RANGE.1);
+        assert_eq!(super::style_column_width(f32::NAN), super::STYLE_COLUMN_WIDTH);
+        assert_eq!(super::style_column_width(f32::INFINITY), super::STYLE_COLUMN_WIDTH);
+    }
+
+    /// Showing or hiding the style name column or the depth ruler keeps the text's focus and caret:
+    /// typing goes on into the story.
+    #[test]
+    fn typing_goes_on_after_the_columns_show_or_hide() {
+        let mut h = story_editor_harness("Focus story");
+        h.get_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput && n.value().as_deref() == Some("Focus story")).focus();
+        h.run_steps(2);
+        h.event(egui::Event::Text("1".into()));
+        h.run_steps(2);
+        for (i, id) in ["view.storyEditorStyleColumn", "view.storyEditorDepthRuler", "view.storyEditorStyleColumn"].into_iter().enumerate() {
+            h.state_mut().app.run(id, json!({})).unwrap();
+            h.run_steps(2);
+            h.event(egui::Event::Text((i + 2).to_string()));
+            h.run_steps(2);
+        }
+        let text = story_text(&h.state().app, 0);
+        assert!(text.contains("1234"), "every key reaches the story at the caret: {text:?}");
+    }
+
+    /// `view.storyEditorStyleColumn` sets the column's width and whether it shows, as the bar and
+    /// the menu item do, and toggles it without either.
+    #[test]
+    fn the_style_column_command_takes_a_width() {
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());
+        assert_eq!(app.run("view.storyEditorStyleColumn", json!({"width": 250})).unwrap(), json!({"on": true, "width": 250.0}));
+        assert_eq!(app.run("view.storyEditorStyleColumn", json!({"width": 1000})).unwrap()["width"], json!(400.0));
+        assert_eq!(app.run("view.storyEditorStyleColumn", json!({"on": false})).unwrap(), json!({"on": false, "width": 400.0}));
+        assert_eq!(app.run("view.storyEditorStyleColumn", json!({})).unwrap()["on"], json!(true));
+        assert!(app.run("view.storyEditorStyleColumn", json!({"width": "wide"})).is_err());
+        assert!(app.run("view.storyEditorStyleColumn", json!({"on": 1})).is_err());
+        assert_eq!((app.ui.story_editor_style_column, app.ui.story_editor_style_width), (true, 400.0));
+    }
+
+    /// Every ¶ painted in the window: its rect and the rect it is clipped to.
+    fn paragraph_marks(h: &egui_kittest::Harness<'_, crate::test_window::Window>) -> Vec<(egui::Rect, egui::Rect)> {
+        fn walk(shape: &egui::Shape, clip: egui::Rect, out: &mut Vec<(egui::Rect, egui::Rect)>) {
+            match shape {
+                egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| walk(s, clip, out)),
+                egui::Shape::Text(t) if t.galley.text() == super::PARAGRAPH_MARK => out.push((t.visual_bounding_rect(), clip)),
+                _ => {}
+            }
+        }
+        let mut out = Vec::new();
+        for c in &h.output().shapes {
+            walk(&c.shape, c.clip_rect, &mut out);
+        }
+        out
+    }
+
+    /// View › Story Editor › Show/Hide Paragraph Break Marks: off at first; on, a ¶ follows each of the
+    /// first two paragraphs and none the last; the setting is remembered.
+    #[test]
+    fn paragraph_break_marks_show_and_hide_from_the_view_menu() {
+        let text = format!("First\n{}\nLast", "A paragraph long enough to wrap over several rows in the editor. ".repeat(12).trim_end());
+        let mut h = story_editor_harness(&text);
+        assert!(!h.state().app.ui.story_editor_paragraph_marks, "hidden by default");
+        let before = paragraph_marks(&h);
+        h.state_mut().app.run("view.storyEditorParagraphMarks", json!({})).unwrap();
+        h.run_steps(2);
+        let field = h.get_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput && n.value().as_deref() == Some(text.as_str())).rect();
+        let marks: Vec<_> = paragraph_marks(&h).into_iter().filter(|m| !before.contains(m)).collect();
+        assert_eq!(marks.len(), 2, "one mark after each of the first two paragraphs: {marks:?}");
+        let (first, second) = (marks[0].0, marks[1].0);
+        assert!(first.min.x > field.min.x && first.min.x < field.center().x, "the first mark follows the short first row: {first:?} in {field:?}");
+        assert!(
+            second.min.y > first.min.y + 2.0 * first.height(),
+            "the second mark is below the first, past the wrapped paragraph's first rows: {first:?}, {second:?}"
+        );
+        let app = &h.state().app;
+        assert_eq!(crate::menus::checked(app, "view.storyEditorParagraphMarks", &json!({})), Some(true));
+        let saved = serde_json::to_vec(&app.ui).unwrap();
+        assert!(crate::UiState::from_preferences_json(&saved).unwrap().story_editor_paragraph_marks);
+        h.state_mut().app.run("view.storyEditorParagraphMarks", json!({})).unwrap();
+        h.run_steps(2);
+        assert_eq!(paragraph_marks(&h), before, "hiding them takes the marks away");
+    }
+
+    /// A paragraph that would just fill the editor's width wraps while the marks show, so its mark is
+    /// drawn within the width the text has without marks, not in the window's margin or cut off.
+    #[test]
+    fn a_mark_after_a_full_row_stays_in_the_window() {
+        let field = |h: &egui_kittest::Harness<'_, crate::test_window::Window>, text: &str| {
+            h.get_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput && n.value().as_deref() == Some(text)).rect()
+        };
+        let h = story_editor_harness("x");
+        let full = field(&h, "x").width();
+        let size = h.state().app.ui.story_editor_size;
+        let width = |s: &str| h.ctx.fonts_mut(|f| f.layout_no_wrap(s.into(), egui::FontId::proportional(size), egui::Color32::WHITE)).size().x;
+        let mut line = String::from("i");
+        while width(&format!("{line}i")) < full - 1.0 {
+            line.push('i');
+        }
+        assert!(full - width(&line) < 4.0, "the row fills the editor: {} of {full}", width(&line));
+        drop(h);
+        let text = format!("{line}\nEnd");
+        let mut h = story_editor_harness(&text);
+        let edge = field(&h, &text).max.x;
+        let before = paragraph_marks(&h);
+        h.state_mut().app.run("view.storyEditorParagraphMarks", json!({})).unwrap();
+        h.run_steps(2);
+        let marks: Vec<_> = paragraph_marks(&h).into_iter().filter(|m| !before.contains(m)).collect();
+        assert_eq!(marks.len(), 1, "{marks:?}");
+        let (rect, clip) = marks[0];
+        assert!(clip.contains_rect(rect), "the mark is not cut off: {rect:?} in {clip:?}");
+        assert!(rect.max.x <= edge, "the mark stays left of the text's right edge {edge}: {rect:?}");
+    }
+
+    /// A mark goes after the last row of a wrapped paragraph and after an empty paragraph, and not
+    /// after the story's last paragraph, which no paragraph break ends.
+    #[test]
+    fn paragraph_marks_go_after_each_paragraph_break() {
+        let ctx = egui::Context::default();
+        let mut galley = None;
+        let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
+            galley = Some(ui.fonts_mut(|f| f.layout("aa\nbbbb bbbb\n\ncc".into(), egui::FontId::proportional(14.0), egui::Color32::WHITE, 40.0)));
+        });
+        output.textures_delta.clear();
+        let galley = galley.expect("laid out");
+        let spots = super::paragraph_mark_spots(&galley);
+        assert_eq!(spots.len(), 3, "{spots:?}");
+        let rows = &galley.rows;
+        assert!(rows.len() >= 5, "the second paragraph wraps: {} rows", rows.len());
+        assert_eq!(spots[0], egui::pos2(rows[0].rect().max.x + 1.0, rows[0].rect().min.y));
+        assert_eq!(spots[1].y, rows[2].rect().min.y, "after the wrapped row, not the paragraph's first");
+        assert!(spots[1].x > rows[2].rect().min.x + 1.0);
+        assert_eq!(spots[2], egui::pos2(rows[3].rect().min.x + 1.0, rows[3].rect().min.y), "the empty paragraph");
+    }
+
     /// Story ids are counted per document: typing in the editor after switching documents edited
     /// the other document's story with the same id.
     #[test]
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -188,7 +188,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format ·
   Profile / Edit Original, File ▸ File Info / Open PDF / Document and Print Presets / User
   Settings, Type ▸ Size presets, Table ▸ Table Options / Cell Options submenus, Paste Before/After,
   Convert Columns, Object ▸ Interactive ▸ Convert to (form fields),
-  View ▸ Extras badges and Story Editor options (Show/Hide Depth Ruler is there). Pages panel ▸ Parent Options (Based on Parent is
+  View ▸ Extras badges and Story Editor options (Show/Hide Depth Ruler, Show/Hide Style Name Column and Show/Hide Paragraph Break Marks are there). Pages panel ▸ Parent Options (Based on Parent is
   the `layout.parents.options` command only); overriding a base parent's items on a parent spread
   itself (overrides work on document pages only).
 - **Estimate:** 40–70 h (most are thin wrappers over existing engine features).
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -50,7 +50,7 @@ basics of editing fail for too many users on Windows and Linux.
 | Panels: Pages, Layers, Swatches, Color, Stroke, Character, Paragraph, Styles, Links, Effects, Text Wrap, Align, Pathfinder, Table, Index, TOC, Hyperlinks, Bookmarks, Articles, Tags, Notes, Track Changes, Object States, Buttons and Forms, Preflight, Data Merge, Library, Scripts, Glyphs, Info, Tabs, Story | ✓ | all present | ~ | Navigator, Trap Presets, Animation, Timing missing; pull-down panels can't scroll on Linux (#267) |
 | Dialogs: New Document, Document Setup, Preferences (all pages), Paragraph/Character/Object/Table/Cell Style Options, Export PDF, Print, Package, Find/Change | ✓ | present | ~ | generated command dialogs fill the long tail; depth varies |
 | Workspaces | Essentials, Essentials Classic, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Review, Typography, Touch | ✓ named workspaces (Essentials Classic: Control panel and iconic panel groups) + custom; Window › Workspace; no Review or Touch | ~ | tab bar overlaps menus in Book/Advanced (#182) |
-| Story Editor | ✓ | ✓ | ~ | depth ruler and View › Story Editor › Show/Hide Depth Ruler (`story_editor.rs` `depth_ruler_shows_and_hides_from_the_view_menu`); hiding or resizing the style-name column missing |
+| Story Editor | ✓ | ✓ | ~ | depth ruler and View › Story Editor › Show/Hide Depth Ruler (`story_editor.rs` `depth_ruler_shows_and_hides_from_the_view_menu`); Show/Hide Style Name Column and dragging the column's width (`style_name_column_shows_and_hides_from_the_view_menu`, `dragging_the_style_column_bar_resizes_the_column`); Show/Hide Paragraph Break Marks (`paragraph_break_marks_show_and_hide_from_the_view_menu`); Expand/Collapse All Footnotes missing |
 | Undo granularity (a drag = one step) | ✓ | ✓ (Begin/Preview/Commit) | ✓ | |
 | Closing with unsaved changes asks | ✓ | ✓ File › Close and window/menu quit (#146, #545) | ✓ | macOS Dock Quit still bypasses the prompt (#544) |
 | UI scaling, System appearance | ✓ | scaling ✓ (#281); follows macOS light/dark ✗ (#237) | ~ | |
