diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -397,6 +397,7 @@ Newest first.
 - **2026-10-24** · Text frames: a custom baseline grid's Relative To (Text Frame Options › Baseline Options, `object.textFrameOptions {baselineGrid: {relativeTo}}`, IDML `BaselineFrameGridRelativeOption`) measures the grid's start from the top of the page, the top margin, the top of the frame or its top inset. Text Frame Options had no custom baseline grid controls, imports dropped the setting and every frame grid started from the page top, so grid-aligned text in a frame measured from its own top sat on the wrong lines. No readiness percentage change.
 - **2026-10-24** · Paragraph Style Options › Indents and Spacing has an Align to Grid menu: None, All Lines or First Line Only. It shows the style's current setting. A First Line Only style could be set only in the CJK Grid Settings section, which shows only with CJK features on, or through commands. No readiness percentage change.
 - **2026-10-24** · Anchored text frames, such as marginal notes, compose where their host story places them: they align to the baseline grid at that position, and page-number markers and spine-relative alignment inside them read the host's page. They composed against a grid measured from their stored position and on no page, so a note set to Only Align First Line to Grid could sit a fraction of a line off the body text's grid line. No readiness percentage change.
+- **2026-10-22** · Paragraph Style Options › Paragraph Shading and Paragraph Border are laid out as InDesign's: headed with the page's name; Tint is a spinner with InDesign's presets and has Overprint beside it (shading, border and gap; a new attribute each, kept through IDML; PDF export warns it doesn't overprint yet); Corner Size and Shape is InDesign's two-by-two grid of corner icons, sizes and shape menus showing each shape's glyph, with the link in the middle; offsets and weights have spinners. Found in the Learn Skills Daily course, chapter 41. No readiness percentage change.
 - **2026-10-24** · New documents start with Hyphenate Capitalized Words, Hyphenate Last Word and Hyphenate Across Column off in [Basic Paragraph], as in InDesign. Opened and imported files keep the values they carry. No readiness percentage change.
 - **2026-10-24** · The baseline grid's Relative To setting applies: with Top Margin, the grid starts at each page's own top margin in text composition, on the canvas and for snapping. It started at the page top, so grid-aligned text sat on the wrong lines and a page could fit one line fewer. No readiness percentage change.
 - **2026-10-24** · Custom-positioned anchored objects honour Relative to Spine (`anchored.options {spineRelative}`, IDML `SpineRelative`): the settings describe a left-hand page and mirror on a right-hand page, so marginal notes stay in the outside margin on both pages of a spread. On right-hand pages they took the left-hand placement and landed in the inside margin, across the spine or over the body text. No readiness percentage change.
@@ -426,6 +427,8 @@ Newest first.
 - **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is an undo step, is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change.
 - **2026-10-16** · IDML: a page keeps column guides that were dragged off centre (InDesign's Custom columns). They were divided evenly on import, so the gutter sprang back to the middle; the imported positions now place the columns, with the canvas guides, snapping and text placement following them, and they are written back on export. Layout › Margins and Columns re-divides them evenly when the count or gutter changes, as InDesign's does. No readiness percentage change.
 - **2026-10-16** · IDML: custom column positions are checked before they are used (#1096). A set must give two edges per column, ascending from the left margin to the right one; any other (a wrong count, unsorted or non-numeric values, a list longer than 216 columns allow, or positions left behind by a margin or page-size change) is the even split, in the canvas, on import and on export, instead of columns drawn outside the margins. No readiness percentage change.
+- **2026-10-20** · Properties › Appearance: the corner shape list shows each shape's glyph beside its name, drawn as InDesign draws them (Fancy has its own glyph now), and the stroke type list shows each type's line; both dropdowns are named for screen readers. No readiness percentage change.
+- **2026-10-21** · Paragraph Border and Paragraph Shading link their four sides as InDesign's: the stroke weights, offsets and corners have the "Make all settings the same" link, on when the four are the same, so a side typed in sets all four; with the link off a side is set alone (a border with only a thick left side). Found in the Learn Skills Daily course, chapter 40. No readiness percentage change.
 - **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
 - **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
 - **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs
--- a/crates/compose/src/lib.rs
+++ b/crates/compose/src/lib.rs
@@ -268,6 +268,8 @@ pub struct Deco {
     pub path: Option<designcraft_geom::BezPath>,
     /// Shown on screen only (not printed or exported).
     pub nonprinting: bool,
+    /// Set to overprint (paragraph shading or border); PDF export doesn't overprint yet.
+    pub overprint: bool,
 }
 
 #[derive(Clone, Debug, Default)]
diff --git a/crates/compose/src/parabox.rs b/crates/compose/src/parabox.rs
--- a/crates/compose/src/parabox.rs
+++ b/crates/compose/src/parabox.rs
@@ -32,6 +32,7 @@ struct Shading {
     bottom: BoxBottom,
     clip: bool,
     nonprinting: bool,
+    overprint: bool,
 }
 
 #[derive(Clone, Debug, PartialEq)]
@@ -51,6 +52,8 @@ struct Border {
     display_if_splits: bool,
     gap_color: String,
     gap_tint: f32,
+    overprint: bool,
+    gap_overprint: bool,
 }
 
 fn finite(v: f64) -> f64 {
@@ -72,6 +75,7 @@ impl ParaBox {
             bottom: pp.shading_bottom,
             clip: pp.shading_clip,
             nonprinting: pp.shading_nonprinting,
+            overprint: pp.shading_overprint,
         });
         let border = pp.border_on.then(|| Border {
             color: pp.border_color.clone(),
@@ -88,6 +92,8 @@ impl ParaBox {
             display_if_splits: pp.border_display_if_splits,
             gap_color: pp.border_gap_color.clone(),
             gap_tint: pp.border_gap_tint,
+            overprint: pp.border_overprint,
+            gap_overprint: pp.border_gap_overprint,
         });
         Some(ParaBox { shading, border, merge: pp.border_merge })
     }
@@ -136,7 +142,7 @@ fn shading_deco(s: &Shading, part: &Part<'_>, frames: &[FrameSpec], first_part:
         // Corners stay square where the box is cut by a split.
         let corners = cut_corners(s.corners, !first_part, !last_part);
         let path = (!corners.iter().all(Corner::is_none)).then(|| box_outline(rect, &corners, [true; 4]));
-        return Some(Deco { rect, color: s.color.clone(), tint: s.tint, path, nonprinting: s.nonprinting });
+        return Some(Deco { rect, color: s.color.clone(), tint: s.tint, path, nonprinting: s.nonprinting, overprint: s.overprint });
     }
     None
 }
@@ -149,15 +155,14 @@ fn border_decos(bd: &Border, part: &Part<'_>, first_part: bool, last_part: bool,
     let open_top = !first_part && !bd.display_if_splits;
     let open_bottom = !last_part && !bd.display_if_splits;
     for (path, gap) in border_paths(bd, o, open_top, open_bottom) {
-        let (color, tint) = if gap { (bd.gap_color.clone(), bd.gap_tint) } else { (bd.color.clone(), bd.tint) };
-        decos.push((part.fi, false, Deco { rect: path.bounding_box(), color, tint, path: Some(path), nonprinting: false }));
+        let (color, tint, overprint) =
+            if gap { (bd.gap_color.clone(), bd.gap_tint, bd.gap_overprint) } else { (bd.color.clone(), bd.tint, bd.overprint) };
+        decos.push((part.fi, false, Deco { rect: path.bounding_box(), color, tint, path: Some(path), nonprinting: false, overprint }));
     }
     if is_plain(bd, open_top, open_bottom) {
-        decos.extend(
-            plain_edges(bd, o, open_top, open_bottom)
-                .into_iter()
-                .map(|rect| (part.fi, false, Deco { rect, color: bd.color.clone(), tint: bd.tint, path: None, nonprinting: false })),
-        );
+        decos.extend(plain_edges(bd, o, open_top, open_bottom).into_iter().map(|rect| {
+            (part.fi, false, Deco { rect, color: bd.color.clone(), tint: bd.tint, path: None, nonprinting: false, overprint: bd.overprint })
+        }));
     }
 }
 
diff --git a/crates/doc/src/attrs.rs b/crates/doc/src/attrs.rs
--- a/crates/doc/src/attrs.rs
+++ b/crates/doc/src/attrs.rs
@@ -1341,6 +1341,9 @@ attr_set! {
         shading_on: bool = false,
         shading_color: String = "[Black]".into(),
         shading_tint: f32 = 0.2,
+        /// Overprint (InDesign's Paragraph Shading › Overprint): stored and kept through IDML;
+        /// PDF export doesn't overprint yet.
+        shading_overprint: bool = false,
         shading_offsets: [f64; 4] = [0.0; 4],
         /// Paragraph border: stroke weights (top, left, bottom, right), colour, and offsets past the
         /// text (top, left, bottom, right).
@@ -1348,6 +1351,8 @@ attr_set! {
         border_weights: [f64; 4] = [1.0; 4],
         border_color: String = "[Black]".into(),
         border_tint: f32 = 1.0,
+        /// Overprint the border's stroke (stored and kept through IDML, as `ParagraphBorderOverprint`).
+        border_overprint: bool = false,
         border_offsets: [f64; 4] = [0.0; 4],
         /// The border's stroke type (a document stroke style by name, or a built-in type), cap and
         /// join.
@@ -1367,6 +1372,8 @@ attr_set! {
         /// The colour between the dashes, dots or stripes of a border that isn't solid.
         border_gap_color: String = "[None]".into(),
         border_gap_tint: f32 = 1.0,
+        /// Overprint the border's gap colour (stored and kept through IDML).
+        border_gap_overprint: bool = false,
         /// Shading corners: top left, top right, bottom right, bottom left.
         shading_corners: [designcraft_geom::corners::Corner; 4] = [designcraft_geom::corners::Corner::default(); 4],
         shading_width: BoxWidth = BoxWidth::Column,
diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs
--- a/crates/idml/src/export.rs
+++ b/crates/idml/src/export.rs
@@ -1621,6 +1621,7 @@ impl<'a> Ex<'a> {
             props.push(p("ParagraphShadingColor", "object", self.sw(c)));
         }
         n!(shading_tint, "ParagraphShadingTint", pct);
+        b!(shading_overprint, "ParagraphShadingOverprint");
         const SIDES: [&str; 4] = ["Top", "Left", "Bottom", "Right"];
         if let Some(o) = a.shading_offsets {
             for (i, side) in SIDES.iter().enumerate() {
@@ -1632,6 +1633,7 @@ impl<'a> Ex<'a> {
             props.push(p("ParagraphBorderColor", "object", self.sw(c)));
         }
         n!(border_tint, "ParagraphBorderTint", pct);
+        b!(border_overprint, "ParagraphBorderOverprint");
         if let Some(w) = a.border_weights {
             for (i, side) in SIDES.iter().enumerate() {
                 el.set(&format!("ParagraphBorder{side}LineWeight"), num(w[i]));
@@ -1675,6 +1677,7 @@ impl<'a> Ex<'a> {
             props.push(p("ParagraphBorderGapColor", "object", self.sw(c)));
         }
         n!(border_gap_tint, "ParagraphBorderGapTint", pct);
+        b!(border_gap_overprint, "ParagraphBorderGapOverprint");
         if let Some(w) = a.shading_width {
             el.set("ParagraphShadingWidth", names::box_width_out(w));
         }
diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs
--- a/crates/idml/src/import.rs
+++ b/crates/idml/src/import.rs
@@ -1902,6 +1902,7 @@ impl<'r> Importer<'r> {
             a.shading_color = Some(self.swatch_ref(c.trim()));
         }
         a.shading_tint = tint(e.num("ParagraphShadingTint"));
+        a.shading_overprint = e.boolean("ParagraphShadingOverprint");
         let sides = |f: &dyn Fn(&str) -> String| -> Option<[f64; 4]> {
             let v = ["Top", "Left", "Bottom", "Right"].map(|s| e.num(&f(s)));
             v.iter().any(Option::is_some).then(|| v.map(|x| x.unwrap_or(0.0)))
@@ -1912,6 +1913,7 @@ impl<'r> Importer<'r> {
             a.border_color = Some(self.swatch_ref(c.trim()));
         }
         a.border_tint = tint(e.num("ParagraphBorderTint"));
+        a.border_overprint = e.boolean("ParagraphBorderOverprint");
         a.border_weights = sides(&|s| format!("ParagraphBorder{s}LineWeight"));
         a.border_offsets = sides(&|s| format!("ParagraphBorder{s}Offset"));
         a.border_type = e.prop("ParagraphBorderType").map(|v| names::stroke_type_in(v.trim()));
@@ -1927,6 +1929,7 @@ impl<'r> Importer<'r> {
             a.border_gap_color = Some(self.swatch_ref(c.trim()));
         }
         a.border_gap_tint = tint(e.num("ParagraphBorderGapTint"));
+        a.border_gap_overprint = e.boolean("ParagraphBorderGapOverprint");
         a.shading_corners = corners(e, "ParagraphShading");
         a.shading_width = e.prop("ParagraphShadingWidth").map(|v| names::box_width_in(v.trim()));
         a.shading_top = e.prop("ParagraphShadingTopOrigin").map(|v| names::box_top_in(v.trim()));
diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs
--- a/crates/idml/src/tests.rs
+++ b/crates/idml/src/tests.rs
@@ -336,6 +336,9 @@ fn indesign_text_defaults() -> Vec<AttrRow> {
         "ParagraphBorderOn": "false" => false, "true" => true, |r| r.0.border_on;
         "ParagraphBorderColor": "Color/Black" => "[Black]", "Swatch/None" => "[None]", |r| r.0.border_color;
         "ParagraphBorderTint": "-1" => 1.0f32, "50" => 0.5f32, |r| r.0.border_tint;
+        "ParagraphShadingOverprint": "false" => false, "true" => true, |r| r.0.shading_overprint;
+        "ParagraphBorderOverprint": "false" => false, "true" => true, |r| r.0.border_overprint;
+        "ParagraphBorderGapOverprint": "false" => false, "true" => true, |r| r.0.border_gap_overprint;
         "FontStyle": "Regular" => "Regular", "Bold" => "Bold", |r| r.1.font_style;
         "PointSize": "12" => 12.0, "9" => 9.0, |r| r.1.size;
         "Leading": "Auto" => Leading::Auto, "14" => Leading::Points(14.0), |r| r.1.leading;
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
@@ -237,6 +237,9 @@ impl Exporter<'_> {
         // Paragraph shading and rules under the text.
         self.as_artifact(s, |me, s| {
             for d in ft.decos.iter().filter(|d| !d.nonprinting) {
+                if d.overprint {
+                    me.warn("overprint is not exported yet");
+                }
                 if let Some(c) = me.swatch_color(&d.color, d.tint) {
                     match &d.path {
                         Some(p) => me.fill_path(s, p, c),
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
@@ -4167,8 +4167,13 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
             "openType" => open_type_features(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }),
             "underline" => line_options(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }, &doc, "underline"),
             "strikethrough" => line_options(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }, &doc, "strikethrough"),
-            "border" => border_shading(&app.ui.language, ui, d, &pv, &doc, true),
-            "shading" => border_shading(&app.ui.language, ui, d, &pv, &doc, false),
+            "border" | "shading" => {
+                let border = d.s("section") == "border";
+                let title = if border { "Paragraph Border" } else { "Paragraph Shading" };
+                crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, title)).size(14.0));
+                ui.add_space(6.0);
+                border_shading(&app.ui.language, ui, d, &pv, &doc, border);
+            }
             "keep" => keep_options(&app.ui.language, ui, d, &pv),
             "span" => span_columns(&app.ui.language, ui, d, &pv, &doc),
             "bullets" => {
@@ -4431,9 +4436,14 @@ fn para_measure(ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, unit:
 
 /// A paragraph attribute chosen from `opts` (serialized value, label).
 fn para_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, opts: &[(&str, &str)]) {
+    para_choice_width(lang, ui, d, pv, key, opts, 200.0);
+}
+
+/// [`para_choice`] `width` wide.
+fn para_choice_width(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, opts: &[(&str, &str)], width: f32) {
     let cur = para_value(d, pv, key);
     let shown = opts.iter().find(|o| cur == o.0).map_or("", |o| crate::i18n::tr(lang, o.1));
-    egui::ComboBox::from_id_salt(("pschoice", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
+    egui::ComboBox::from_id_salt(("pschoice", key)).selected_text(crate::rtl::widget(ui, shown)).width(width).show_ui(ui, |ui| {
         for (v, label) in opts {
             if ui.selectable_label(cur == *v, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() {
                 para_set(d, key, json!(v));
@@ -4446,18 +4456,32 @@ fn para_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &
 /// edited side stores the whole array.
 fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, field: &dyn Fn(&mut egui::Ui, &str, Option<f64>) -> Option<f64>) {
     let mut sides: [f64; 4] = serde_json::from_value(para_value(d, pv, key)).unwrap_or_default();
+    let linked = linked(d, key, sides.iter().all(|v| (v - sides[0]).abs() < 1e-9));
     let mut changed = false;
-    egui::Grid::new(("pssides", key)).num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| {
+    // As InDesign's: Top and Bottom, the link, Left and Right.
+    egui::Grid::new(("pssides", key)).num_columns(5).spacing([8.0, 6.0]).show(ui, |ui| {
         for (i, label) in ["Top:", "Left:", "Bottom:", "Right:"].into_iter().enumerate() {
             crate::rtl::label(ui, crate::i18n::tr(lang, label));
-            if let Some(side) = sides.get_mut(i)
-                && let Some(v) = field(ui, &format!("ps{key}{i}"), Some(*side))
-            {
-                *side = v;
+            if let Some(v) = field(ui, &format!("ps{key}{i}"), sides.get(i).copied()) {
+                // Linked, a side typed in sets all four.
+                if linked {
+                    sides = [v; 4];
+                } else if let Some(side) = sides.get_mut(i) {
+                    *side = v;
+                }
                 changed = true;
             }
-            if i % 2 == 1 {
-                ui.end_row();
+            match i {
+                // Linked again, the four take Top's value.
+                0 if link_toggle(lang, ui, d, key, linked) => {
+                    sides = [sides[0]; 4];
+                    changed = true;
+                }
+                0 => {}
+                2 => {
+                    ui.label("");
+                }
+                _ => ui.end_row(),
             }
         }
     });
@@ -4466,6 +4490,23 @@ fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &s
     }
 }
 
+/// Whether the four values of `key` are linked ("Make all settings the same"): as the user left
+/// the link, else when the four are the same, as InDesign starts it.
+fn linked(d: &Dialog, key: &str, same: bool) -> bool {
+    d.fields.get(&format!("_linked.{key}")).and_then(Value::as_bool).unwrap_or(same)
+}
+
+/// The link button between a border's or shading's four sides or corners; true when it was
+/// clicked to link them, and the caller makes the four the same.
+fn link_toggle(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, key: &str, linked: bool) -> bool {
+    let tip = crate::i18n::tr(lang, "Make all settings the same");
+    let clicked = crate::widgets::icon_toggle_sized(ui, if linked { "link" } else { "link-broken" }, linked, tip, egui::vec2(22.0, 21.0)).clicked();
+    if clicked {
+        d.fields.insert(format!("_linked.{key}"), json!(!linked));
+    }
+    clicked && !linked
+}
+
 /// A swatch menu with a colour chip per swatch (hidden swatches left out); returns the swatch
 /// picked.
 fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui::AsIdSalt, current: &str, width: f32) -> Option<String> {
@@ -4516,49 +4557,96 @@ fn border_type_menu(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, d
 }
 
 /// Corner size and shape of a border or shading (`key` holds top left, top right, bottom right,
-/// bottom left); an edited corner stores all four.
+/// bottom left), as InDesign lays them out: Top Left and Top Right over Bottom Left and Bottom
+/// Right, each a corner icon, a size and a shape menu showing the shapes' glyphs, with the link in
+/// the middle. An edited corner stores all four.
 fn para_corners(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, units: Unit) {
     use designcraft_geom::corners::{Corner, CornerShape};
     let mut corners: [Corner; 4] = serde_json::from_value(para_value(d, pv, key)).unwrap_or_default();
-    let mut changed = false;
-    egui::Grid::new(("pscorners", key)).num_columns(3).spacing([8.0, 6.0]).show(ui, |ui| {
-        for (i, label) in ["Top Left:", "Top Right:", "Bottom Right:", "Bottom Left:"].into_iter().enumerate() {
-            let Some(c) = corners.get_mut(i) else { continue };
-            crate::rtl::label(ui, crate::i18n::tr(lang, label));
-            if let Some(v) = crate::widgets::measure(ui, &format!("ps{key}{i}"), Some(c.size), units, 70.0) {
-                c.size = if v.is_finite() { v.clamp(0.0, 10_000.0) } else { 0.0 };
-                changed = true;
-            }
-            egui::ComboBox::from_id_salt(("pscornershape", key, i))
-                .selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, c.shape.label())))
-                .width(130.0)
-                .show_ui(ui, |ui| {
-                    for shape in CornerShape::ALL {
-                        if ui.selectable_label(c.shape == shape, crate::rtl::widget(ui, crate::i18n::tr(lang, shape.label()))).clicked() {
-                            c.shape = shape;
-                            changed = true;
-                        }
-                    }
+    let linked = linked(d, key, corners.iter().all(|c| *c == corners[0]));
+    let mut changed = None;
+    let mut relinked = false;
+    let mut corner = |ui: &mut egui::Ui, i: usize, icon: &str, tip: &str, changed: &mut Option<usize>| {
+        let Some(c) = corners.get_mut(i) else { return };
+        let tip = crate::i18n::tr(lang, tip);
+        let (r, resp) = ui.allocate_exact_size(egui::vec2(18.0, 18.0), egui::Sense::hover());
+        crate::icons::paint(ui.painter(), r, icon, crate::theme::Tokens::get(ui.ctx()).icon);
+        resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, true, tip));
+        resp.on_hover_text(tip);
+        let id = format!("ps{key}{i}");
+        if let Some(v) = crate::widgets::NumField::measure(&id, Some(c.size), units).width(84.0).spinner().range(0.0, 10_000.0).show(ui) {
+            c.size = if v.is_finite() { v.clamp(0.0, 10_000.0) } else { 0.0 };
+            *changed = Some(i);
+        }
+        let shape_label = crate::i18n::tr(lang, "Corner Shape");
+        let shown = c.shape;
+        let resp = crate::panels::properties::preview_dropdown(ui, 52.0, shape_label, |p, r| {
+            crate::panels::properties::paint_corner_shape(p, r.shrink(2.0), shown, egui::Color32::BLACK)
+        });
+        egui::Popup::menu(&resp).show(|ui| {
+            for shape in CornerShape::ALL {
+                let row = crate::panels::properties::preview_row(ui, c.shape == shape, crate::i18n::tr(lang, shape.label()), 22.0, |p, r, ink| {
+                    crate::panels::properties::paint_corner_shape(p, r, shape, ink)
                 });
+                if row.clicked() {
+                    c.shape = shape;
+                    *changed = Some(i);
+                    ui.close();
+                }
+            }
+        });
+    };
+    egui::Grid::new(("pscorners", key)).num_columns(3).spacing([14.0, 6.0]).show(ui, |ui| {
+        for (row, [(li, licon, ltip), (ri, ricon, rtip)]) in [
+            [(0, "corner-tl", "Top Left Corner"), (1, "corner-tr", "Top Right Corner")],
+            [(3, "corner-bl", "Bottom Left Corner"), (2, "corner-br", "Bottom Right Corner")],
+        ]
+        .into_iter()
+        .enumerate()
+        {
+            ui.horizontal(|ui| corner(ui, li, licon, ltip, &mut changed));
+            // Linked again, the four take Top Left's size and shape.
+            if row == 0 {
+                if link_toggle(lang, ui, d, key, linked) {
+                    relinked = true;
+                }
+            } else {
+                ui.label("");
+            }
+            ui.horizontal(|ui| corner(ui, ri, ricon, rtip, &mut changed));
             ui.end_row();
         }
     });
-    if changed {
+    if relinked {
+        corners = [corners[0]; 4];
+        changed = Some(0);
+    }
+    if let Some(i) = changed {
+        // Linked, the corner changed sets all four.
+        if linked {
+            corners = [corners[i]; 4];
+        }
         para_set(d, key, serde_json::to_value(corners).unwrap_or_default());
     }
 }
 
-/// The colour and tint rows of a border, its gap or shading (`keys`: colour, tint), in a grid.
+/// Tint presets (%), as InDesign's tint menus list them.
+const TINTS: &[f64] = &[0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
+
+/// The colour and tint rows of a border, its gap or shading (`keys`: colour, tint, overprint), in
+/// a grid; Overprint follows the tint, as InDesign's.
+#[allow(clippy::too_many_arguments)]
 fn para_color_tint(
+    lang: &str,
     ui: &mut egui::Ui,
     d: &mut Dialog,
     pv: &Value,
     doc: &designcraft_doc::Document,
-    keys: (&str, &str),
+    keys: (&str, &str, &str),
     labels: (&str, &str),
     enabled: bool,
 ) {
-    let (color_key, tint_key) = keys;
+    let (color_key, tint_key, overprint_key) = keys;
     crate::rtl::label(ui, labels.0);
     let color = para_value(d, pv, color_key).as_str().unwrap_or("").to_string();
     if let Some(w) = ui.add_enabled_ui(enabled, |ui| swatch_menu(ui, doc, ("pscolor", color_key), &color, 160.0)).inner {
@@ -4566,10 +4654,18 @@ fn para_color_tint(
     }
     ui.end_row();
     crate::rtl::label(ui, labels.1);
-    let tint = para_value(d, pv, tint_key).as_f64().map(|t| t * 100.0);
-    if let Some(v) = ui.add_enabled_ui(enabled, |ui| crate::widgets::number(ui, &format!("ps{tint_key}"), tint, "%", 60.0, 0)).inner {
-        para_set(d, tint_key, json!(if v.is_finite() { (v / 100.0).clamp(0.0, 1.0) } else { 1.0 }));
-    }
+    let tint = para_value(d, pv, tint_key).as_f64().map(|t| (t * 1000.0).round() / 10.0);
+    ui.add_enabled_ui(enabled, |ui| {
+        ui.horizontal(|ui| {
+            let id = format!("ps{tint_key}");
+            let field = crate::widgets::NumField::number(&id, tint, "%", 0).width(96.0).spinner().presets(TINTS).step(10.0).range(0.0, 100.0);
+            if let Some(v) = field.show(ui) {
+                para_set(d, tint_key, json!(if v.is_finite() { (v / 100.0).clamp(0.0, 1.0) } else { 1.0 }));
+            }
+            ui.add_space(8.0);
+            para_check(ui, d, pv, overprint_key, crate::i18n::tr(lang, "Overprint"));
+        });
+    });
     ui.end_row();
 }
 
@@ -4587,12 +4683,17 @@ fn border_shading(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc
         crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, t)).font(semibold(12.0)));
     };
     ui.add_space(4.0);
-    egui::ScrollArea::vertical().id_salt(("psbsscroll", border)).max_height(330.0).show(ui, |ui| {
+    egui::ScrollArea::vertical().id_salt(("psbsscroll", border)).max_height(400.0).show(ui, |ui| {
         ui.add_enabled_ui(on, |ui| {
             if border {
                 head(ui, "Stroke");
                 para_sides(lang, ui, d, pv, &key("Weights"), &|ui: &mut egui::Ui, id: &str, v: Option<f64>| {
-                    crate::widgets::number(ui, id, v, " pt", 60.0, 2).map(|w| if w.is_finite() { w.clamp(0.0, 1000.0) } else { 0.0 })
+                    crate::widgets::NumField::number(id, v, " pt", 2)
+                        .width(84.0)
+                        .spinner()
+                        .range(0.0, 1000.0)
+                        .show(ui)
+                        .map(|w| if w.is_finite() { w.clamp(0.0, 1000.0) } else { 0.0 })
                 });
                 ui.add_space(4.0);
             } else {
@@ -4605,12 +4706,12 @@ fn border_shading(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc
                     ui.end_row();
                 }
                 let (color, tint) = (crate::i18n::tr(lang, "Color:"), crate::i18n::tr(lang, "Tint:"));
-                para_color_tint(ui, d, pv, doc, (&key("Color"), &key("Tint")), (color, tint), true);
+                para_color_tint(lang, ui, d, pv, doc, (&key("Color"), &key("Tint"), &key("Overprint")), (color, tint), true);
                 if border {
                     // The gap colour shows between the dashes, dots or stripes.
                     let solid = para_value(d, pv, "borderType").get("kind").and_then(Value::as_str).is_none_or(|k| k == "solid");
                     let (gap, gap_tint) = (crate::i18n::tr(lang, "Gap Color:"), crate::i18n::tr(lang, "Gap Tint:"));
-                    para_color_tint(ui, d, pv, doc, ("borderGapColor", "borderGapTint"), (gap, gap_tint), !solid);
+                    para_color_tint(lang, ui, d, pv, doc, ("borderGapColor", "borderGapTint", "borderGapOverprint"), (gap, gap_tint), !solid);
                     crate::rtl::label(ui, crate::i18n::tr(lang, "Cap:"));
                     para_choice(lang, ui, d, pv, "borderCap", &[("butt", "Butt"), ("round", "Round"), ("projecting", "Projecting")]);
                     ui.end_row();
@@ -4621,28 +4722,45 @@ fn border_shading(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc
             });
             head(ui, "Corner Size and Shape");
             para_corners(lang, ui, d, pv, &key("Corners"), units);
-            head(ui, "Offsets");
-            para_sides(lang, ui, d, pv, &key("Offsets"), &|ui: &mut egui::Ui, id: &str, v: Option<f64>| {
-                crate::widgets::measure(ui, id, v, units, 60.0)
-            });
-            ui.add_space(4.0);
-            egui::Grid::new(("psbsedges", border)).num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
-                crate::rtl::label(ui, crate::i18n::tr(lang, "Top Edge:"));
-                para_choice(
-                    lang,
-                    ui,
-                    d,
-                    pv,
-                    &key("Top"),
-                    &[("ascent", "Ascent"), ("baseline", "Baseline"), ("emBox", "Em Box Top"), ("leading", "Leading")],
-                );
-                ui.end_row();
-                crate::rtl::label(ui, crate::i18n::tr(lang, "Bottom Edge:"));
-                para_choice(lang, ui, d, pv, &key("Bottom"), &[("descent", "Descent"), ("baseline", "Baseline"), ("emBox", "Em Box Bottom")]);
-                ui.end_row();
-                crate::rtl::label(ui, crate::i18n::tr(lang, "Width:"));
-                para_choice(lang, ui, d, pv, &key("Width"), &[("column", "Column"), ("text", "Text")]);
-                ui.end_row();
+            // The offsets, and beside them the edges and width the box follows, as InDesign's.
+            ui.horizontal_top(|ui| {
+                ui.vertical(|ui| {
+                    head(ui, "Offsets");
+                    para_sides(lang, ui, d, pv, &key("Offsets"), &|ui: &mut egui::Ui, id: &str, v: Option<f64>| {
+                        crate::widgets::spin_measure(ui, id, v, units, 72.0)
+                    });
+                });
+                ui.add_space(12.0);
+                ui.vertical(|ui| {
+                    head(ui, "");
+                    egui::Grid::new(("psbsedges", border)).num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
+                        crate::rtl::label(ui, crate::i18n::tr(lang, "Top Edge:"));
+                        para_choice_width(
+                            lang,
+                            ui,
+                            d,
+                            pv,
+                            &key("Top"),
+                            &[("ascent", "Ascent"), ("baseline", "Baseline"), ("emBox", "Em Box Top"), ("leading", "Leading")],
+                            92.0,
+                        );
+                        ui.end_row();
+                        crate::rtl::label(ui, crate::i18n::tr(lang, "Bottom Edge:"));
+                        para_choice_width(
+                            lang,
+                            ui,
+                            d,
+                            pv,
+                            &key("Bottom"),
+                            &[("descent", "Descent"), ("baseline", "Baseline"), ("emBox", "Em Box Bottom")],
+                            92.0,
+                        );
+                        ui.end_row();
+                        crate::rtl::label(ui, crate::i18n::tr(lang, "Width:"));
+                        para_choice_width(lang, ui, d, pv, &key("Width"), &[("column", "Column"), ("text", "Text")], 92.0);
+                        ui.end_row();
+                    });
+                });
             });
             ui.add_space(4.0);
             if border {
@@ -9845,6 +9963,134 @@ mod tests {
         }
     }
 
+    /// Replace the text of the field beside the `nth` label `label` (on its row, to its right) with
+    /// `new`, and leave the field.
+    fn retype_beside(h: &mut egui_kittest::Harness<'static, crate::test_window::Window>, label: &str, nth: usize, new: &str) {
+        use egui_kittest::kittest::Queryable;
+        let l = h.get_all_by_label(label).nth(nth).unwrap_or_else(|| panic!("no label {label} #{nth}")).rect();
+        let field = h
+            .get_all_by(|n| n.role() == egui::accesskit::Role::TextInput)
+            .map(|n| n.rect())
+            .filter(|r| r.center().x > l.max.x && (r.center().y - l.center().y).abs() < l.height().max(8.0))
+            .min_by(|a, b| a.min.x.total_cmp(&b.min.x))
+            .unwrap_or_else(|| panic!("no field beside {label} #{nth}"))
+            .center();
+        crate::test_window::click_at(h, field);
+        h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
+        h.event(egui::Event::Text(new.into()));
+        h.run_steps(1);
+        // Tab leaves the field (Enter would be the dialog's OK).
+        h.key_press(egui::Key::Tab);
+        h.run_steps(3);
+    }
+
+    /// The course's chapter 40: Paragraph Border's four weights are linked as InDesign's, so a
+    /// side typed in sets all four; with the link off ("Make all settings the same") a side is set
+    /// alone, as the lesson gives the border only a thick left side. The corners link the same way.
+    #[test]
+    fn paragraph_border_sides_and_corners_link_as_indesigns() {
+        use egui_kittest::kittest::Queryable;
+        let (app, _) = ruled_paragraph();
+        let mut h = crate::test_window::open(app, egui::vec2(1400.0, 900.0));
+        h.state_mut().app.ui.dialog =
+            Some(Dialog::new("paragraphStyleOptions", json!({"name": "Ruled Child", "section": "border", "p.borderOn": true})));
+        h.run_steps(4);
+        let field = |h: &egui_kittest::Harness<'static, crate::test_window::Window>, key: &str| {
+            h.state().app.ui.dialog.as_ref().and_then(|d| d.fields.get(&format!("p.{key}")).cloned()).unwrap_or_default()
+        };
+        // The four weights start the same, so linked: Top sets them all.
+        retype_beside(&mut h, "Top:", 0, "3");
+        assert_eq!(field(&h, "borderWeights"), json!([3.0, 3.0, 3.0, 3.0]));
+        // Unlinked, Left alone takes 10 pt.
+        // The link of the weights (0), corners (1) or offsets (2), where it is now: the dialog
+        // scrolls as fields take focus.
+        let link = |h: &egui_kittest::Harness<'static, crate::test_window::Window>, n: usize| {
+            let links: Vec<egui::Rect> = h.get_all_by_label("Make all settings the same").map(|n| n.rect()).collect();
+            assert_eq!(links.len(), 3, "weights, corners and offsets each have a link");
+            links[n].center()
+        };
+        let at = link(&h, 0);
+        crate::test_window::click_at(&mut h, at);
+        retype_beside(&mut h, "Left:", 0, "10");
+        assert_eq!(field(&h, "borderWeights"), json!([3.0, 10.0, 3.0, 3.0]));
+        // Linked again, the four take Top's weight; unlinked once more for the thick left side.
+        let at = link(&h, 0);
+        crate::test_window::click_at(&mut h, at);
+        assert_eq!(field(&h, "borderWeights"), json!([3.0, 3.0, 3.0, 3.0]));
+        let at = link(&h, 0);
+        crate::test_window::click_at(&mut h, at);
+        retype_beside(&mut h, "Left:", 0, "10");
+        assert_eq!(field(&h, "borderWeights"), json!([3.0, 10.0, 3.0, 3.0]));
+        // The corners start the same too: Top Left's size goes to all four.
+        retype_beside(&mut h, "Top Left Corner", 0, "0p4");
+        let corners = field(&h, "borderCorners");
+        assert!((0..4).all(|i| corners[i]["size"] == json!(4.0)), "{corners}");
+        // Unlinked, Top Right alone takes 0p8; linked again, all four take Top Left's 4 pt.
+        let at = link(&h, 1);
+        crate::test_window::click_at(&mut h, at);
+        retype_beside(&mut h, "Top Right Corner", 0, "0p8");
+        assert_eq!(field(&h, "borderCorners")[1]["size"], json!(8.0));
+        assert_eq!(field(&h, "borderCorners")[0]["size"], json!(4.0));
+        let at = link(&h, 1);
+        crate::test_window::click_at(&mut h, at);
+        let corners = field(&h, "borderCorners");
+        assert!((0..4).all(|i| corners[i] == corners[0] && corners[i]["size"] == json!(4.0)), "{corners}");
+        confirm(&mut h.state_mut().app).unwrap();
+        let style = h.state().app.session.doc().unwrap().doc.styles.para("Ruled Child").cloned().unwrap();
+        assert_eq!(style.para.border_weights, Some([3.0, 10.0, 3.0, 3.0]));
+    }
+
+    /// The course's chapter 41: Paragraph Shading as InDesign's. The page is headed with its name;
+    /// the tint has Overprint beside it; the corners are a two-by-two grid whose shape menus show
+    /// the shapes. The lesson rounds the top two corners at 10 pt, unlinked, and moves the
+    /// offsets.
+    #[test]
+    fn paragraph_shading_page_is_laid_out_as_indesigns() {
+        use designcraft_geom::corners::CornerShape;
+        use egui_kittest::kittest::Queryable;
+        let (app, _) = ruled_paragraph();
+        let mut h = crate::test_window::open(app, egui::vec2(1400.0, 900.0));
+        h.state_mut().app.ui.dialog =
+            Some(Dialog::new("paragraphStyleOptions", json!({"name": "Ruled Child", "section": "shading", "p.shadingOn": true})));
+        h.run_steps(4);
+        let field = |h: &egui_kittest::Harness<'static, crate::test_window::Window>, key: &str| {
+            h.state().app.ui.dialog.as_ref().and_then(|d| d.fields.get(&format!("p.{key}")).cloned()).unwrap_or_default()
+        };
+        assert!(h.get_all_by_label("Paragraph Shading").count() >= 2, "the section list and the page's heading");
+        h.get_by_label("Overprint").click();
+        h.run_steps(2);
+        assert_eq!(field(&h, "shadingOverprint"), json!(true));
+        // The corners' link (0) and the offsets' (1).
+        let link = |h: &egui_kittest::Harness<'static, crate::test_window::Window>, n: usize| {
+            let links: Vec<egui::Rect> = h.get_all_by_label("Make all settings the same").map(|n| n.rect()).collect();
+            assert_eq!(links.len(), 2, "corners and offsets each have a link");
+            links[n].center()
+        };
+        let at = link(&h, 0);
+        crate::test_window::click_at(&mut h, at);
+        for (corner, nth) in [("Top Left Corner", 0), ("Top Right Corner", 1)] {
+            retype_beside(&mut h, corner, 0, "10 pt");
+            h.get_all_by_label("Corner Shape").nth(nth).unwrap().click();
+            h.run_steps(2);
+            h.get_by_label("Rounded").click();
+            h.run_steps(2);
+        }
+        let corners: [designcraft_geom::corners::Corner; 4] = serde_json::from_value(field(&h, "shadingCorners")).unwrap();
+        assert_eq!(
+            corners.map(|c| (c.shape, c.size)),
+            [(CornerShape::Rounded, 10.0), (CornerShape::Rounded, 10.0), (CornerShape::None, 0.0), (CornerShape::None, 0.0)]
+        );
+        // The offsets, unlinked: Top alone.
+        let at = link(&h, 1);
+        crate::test_window::click_at(&mut h, at);
+        retype_beside(&mut h, "Top:", 0, "3 pt");
+        assert_eq!(field(&h, "shadingOffsets"), json!([3.0, 0.0, 0.0, 0.0]));
+        confirm(&mut h.state_mut().app).unwrap();
+        let style = h.state().app.session.doc().unwrap().doc.styles.para("Ruled Child").cloned().unwrap();
+        assert_eq!((style.para.shading_on, style.para.shading_overprint), (Some(true), Some(true)));
+        assert_eq!(style.para.shading_corners.map(|c| c[1].shape), Some(CornerShape::Rounded));
+    }
+
     #[test]
     fn paragraph_style_options_tabs_section_adds_a_stop_on_its_ruler() {
         use egui_kittest::kittest::Queryable;
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
@@ -164,6 +164,8 @@ const TABLE: &[(&str, [&str; 5])] = &[
     ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]),
     ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]),
     ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]),
+    ("Stroke Type", ["Stroke Type", "Type de contour", "Tipo de trazo", "Stroke Type", "描边类型"]),
+    ("Corner Shape", ["Corner Shape", "Forme d'angle", "Forma de esquina", "Corner Shape", "角形状"]),
     ("Hide Rulers", ["Lineale ausblenden", "Masquer les règles", "Ocultar reglas", "定規を隠す", "隐藏标尺"]),
     ("Inches Decimal", ["Zoll dezimal", "Pouces décimaux", "Pulgadas decimales", "インチ（10 進）", "十进制英寸"]),
     (
@@ -1077,6 +1079,11 @@ const TABLE: &[(&str, [&str; 5])] = &[
         ],
     ),
     ("Load Styles", ["Formate laden", "Charger les styles", "Cargar estilos", "スタイルを読み込み", "加载样式"]),
+    ("Overprint", ["Überdrucken", "Surimpression", "Sobreimprimir", "オーバープリント", "叠印"]),
+    ("Top Left Corner", ["Ecke oben links", "Angle supérieur gauche", "Esquina superior izquierda", "左上角", "左上角"]),
+    ("Top Right Corner", ["Ecke oben rechts", "Angle supérieur droit", "Esquina superior derecha", "右上角", "右上角"]),
+    ("Bottom Left Corner", ["Ecke unten links", "Angle inférieur gauche", "Esquina inferior izquierda", "左下角", "左下角"]),
+    ("Bottom Right Corner", ["Ecke unten rechts", "Angle inférieur droit", "Esquina inferior derecha", "右下角", "右下角"]),
     ("Restart Numbering", ["Nummerierung neu beginnen", "Recommencer la numérotation", "Reiniciar numeración", "番号の振り直し", "重新开始编号"]),
     ("Continue Numbering", ["Nummerierung fortführen", "Poursuivre la numérotation", "Continuar numeración", "番号の継続", "继续编号"]),
     ("Keep Options…", ["Umbruchoptionen …", "Options d'enchaînement…", "Opciones de separación…", "段落分離禁止オプション…", "保持选项…"]),
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
@@ -30,6 +30,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "قص"),
     ("Copy", "نسخ"),
     ("Paste", "لصق"),
+    ("Overprint", "طباعة فوقية"),
+    ("Top Left Corner", "الزاوية العلوية اليسرى"),
+    ("Top Right Corner", "الزاوية العلوية اليمنى"),
+    ("Bottom Left Corner", "الزاوية السفلية اليسرى"),
+    ("Bottom Right Corner", "الزاوية السفلية اليمنى"),
+    ("Stroke Type", "نوع الحد"),
+    ("Corner Shape", "شكل الزاوية"),
     ("Restart Numbering", "إعادة الترقيم"),
     ("Continue Numbering", "متابعة الترقيم"),
     ("Keep Options…", "خيارات الإبقاء…"),
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
@@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Vyjmout"),
     ("Copy", "Kopírovat"),
     ("Paste", "Vložit"),
+    ("Overprint", "Přetisk"),
+    ("Top Left Corner", "Levý horní roh"),
+    ("Top Right Corner", "Pravý horní roh"),
+    ("Bottom Left Corner", "Levý dolní roh"),
+    ("Bottom Right Corner", "Pravý dolní roh"),
+    ("Stroke Type", "Typ tahu"),
+    ("Corner Shape", "Tvar rohu"),
     ("Restart Numbering", "Začít číslování znovu"),
     ("Continue Numbering", "Pokračovat v číslování"),
     ("Keep Options…", "Volby zachování…"),
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
@@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Ausschneiden"),
     ("Copy", "Kopieren"),
     ("Paste", "Einfügen"),
+    ("Overprint", "Überdrucken"),
+    ("Top Left Corner", "Ecke oben links"),
+    ("Top Right Corner", "Ecke oben rechts"),
+    ("Bottom Left Corner", "Ecke unten links"),
+    ("Bottom Right Corner", "Ecke unten rechts"),
+    ("Stroke Type", "Konturtyp"),
+    ("Corner Shape", "Eckenform"),
     ("Restart Numbering", "Nummerierung neu beginnen"),
     ("Continue Numbering", "Nummerierung fortführen"),
     ("Keep Options…", "Umbruchoptionen …"),
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
@@ -28,6 +28,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Taglia"),
     ("Copy", "Copia"),
     ("Paste", "Incolla"),
+    ("Overprint", "Sovrastampa"),
+    ("Top Left Corner", "Angolo superiore sinistro"),
+    ("Top Right Corner", "Angolo superiore destro"),
+    ("Bottom Left Corner", "Angolo inferiore sinistro"),
+    ("Bottom Right Corner", "Angolo inferiore destro"),
+    ("Stroke Type", "Tipo traccia"),
+    ("Corner Shape", "Forma angolo"),
     ("Restart Numbering", "Riavvia numerazione"),
     ("Continue Numbering", "Continua numerazione"),
     ("Keep Options…", "Opzioni di separazione…"),
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
@@ -30,6 +30,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "カット"),
     ("Copy", "コピー"),
     ("Paste", "ペースト"),
+    ("Overprint", "オーバープリント"),
+    ("Top Left Corner", "左上角"),
+    ("Top Right Corner", "右上角"),
+    ("Bottom Left Corner", "左下角"),
+    ("Bottom Right Corner", "右下角"),
+    ("Stroke Type", "線の種類"),
+    ("Corner Shape", "角のシェイプ"),
     ("Restart Numbering", "番号の振り直し"),
     ("Continue Numbering", "番号の継続"),
     ("Keep Options…", "段落分離禁止オプション…"),
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
@@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "ამოჭრა"),
     ("Copy", "კოპირება"),
     ("Paste", "ჩასმა"),
+    ("Overprint", "ზედაბეჭდვა"),
+    ("Top Left Corner", "ზედა მარცხენა კუთხე"),
+    ("Top Right Corner", "ზედა მარჯვენა კუთხე"),
+    ("Bottom Left Corner", "ქვედა მარცხენა კუთხე"),
+    ("Bottom Right Corner", "ქვედა მარჯვენა კუთხე"),
+    ("Stroke Type", "კონტურის ტიპი"),
+    ("Corner Shape", "კუთხის ფორმა"),
     ("Restart Numbering", "ნუმერაციის თავიდან დაწყება"),
     ("Continue Numbering", "ნუმერაციის გაგრძელება"),
     ("Keep Options…", "შენარჩუნების პარამეტრები…"),
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
@@ -29,6 +29,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Wytnij"),
     ("Copy", "Kopiuj"),
     ("Paste", "Wklej"),
+    ("Overprint", "Nadruk"),
+    ("Top Left Corner", "Lewy górny narożnik"),
+    ("Top Right Corner", "Prawy górny narożnik"),
+    ("Bottom Left Corner", "Lewy dolny narożnik"),
+    ("Bottom Right Corner", "Prawy dolny narożnik"),
+    ("Stroke Type", "Rodzaj obrysu"),
+    ("Corner Shape", "Kształt narożnika"),
     ("Restart Numbering", "Zacznij numerowanie od nowa"),
     ("Continue Numbering", "Kontynuuj numerowanie"),
     ("Keep Options…", "Opcje utrzymywania…"),
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
@@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Recortar"),
     ("Copy", "Copiar"),
     ("Paste", "Colar"),
+    ("Overprint", "Superimprimir"),
+    ("Top Left Corner", "Canto superior esquerdo"),
+    ("Top Right Corner", "Canto superior direito"),
+    ("Bottom Left Corner", "Canto inferior esquerdo"),
+    ("Bottom Right Corner", "Canto inferior direito"),
+    ("Stroke Type", "Tipo de traçado"),
+    ("Corner Shape", "Forma do canto"),
     ("Restart Numbering", "Reiniciar numeração"),
     ("Continue Numbering", "Continuar numeração"),
     ("Keep Options…", "Opções de manutenção…"),
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
@@ -33,6 +33,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Вырезать"),
     ("Copy", "Копировать"),
     ("Paste", "Вставить"),
+    ("Overprint", "Наложение"),
+    ("Top Left Corner", "Левый верхний угол"),
+    ("Top Right Corner", "Правый верхний угол"),
+    ("Bottom Left Corner", "Левый нижний угол"),
+    ("Bottom Right Corner", "Правый нижний угол"),
+    ("Stroke Type", "Тип обводки"),
+    ("Corner Shape", "Форма угла"),
     ("Restart Numbering", "Начать нумерацию заново"),
     ("Continue Numbering", "Продолжить нумерацию"),
     ("Keep Options…", "Параметры удержания…"),
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
@@ -29,6 +29,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
     ("Cut", "Вирізати"),
     ("Copy", "Копіювати"),
     ("Paste", "Вставити"),
+    ("Overprint", "Накладання"),
+    ("Top Left Corner", "Лівий верхній кут"),
+    ("Top Right Corner", "Правий верхній кут"),
+    ("Bottom Left Corner", "Лівий нижній кут"),
+    ("Bottom Right Corner", "Правий нижній кут"),
+    ("Stroke Type", "Тип обведення"),
+    ("Corner Shape", "Форма кута"),
     ("Restart Numbering", "Почати нумерацію знову"),
     ("Continue Numbering", "Продовжити нумерацію"),
     ("Keep Options…", "Параметри утримання…"),
diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs
--- a/crates/ui-egui/src/icons.rs
+++ b/crates/ui-egui/src/icons.rs
@@ -944,6 +944,18 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32) {
                 pen.line(&[(15.0, 13.5), (17.0, 15.5), (15.0, 17.5)]);
             }
         }
+        // A box with one corner marked (Top Left, Top Right, Bottom Left, Bottom Right Corner).
+        "corner-tl" | "corner-tr" | "corner-bl" | "corner-br" => {
+            pen.w = 1.0;
+            pen.rect(4.0, 4.0, 16.0, 16.0);
+            let (x, y) = match name {
+                "corner-tl" => (2.5, 2.5),
+                "corner-tr" => (13.5, 2.5),
+                "corner-bl" => (2.5, 13.5),
+                _ => (13.5, 13.5),
+            };
+            pen.frect(x, y, x + 4.0, y + 4.0);
+        }
         "vscale" => {
             pen.text(13.0, 10.5, "T", 15.0);
             pen.line(&[(4.0, 3.0), (4.0, 17.0)]);
diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs
--- a/crates/ui-egui/src/panels/properties.rs
+++ b/crates/ui-egui/src/panels/properties.rs
@@ -705,11 +705,11 @@ pub(crate) fn stroke_type_dropdown(app: &mut DesignApp, ui: &mut Ui, cur: &str,
         ("Thin - Thin", "thinThin"),
         ("Thick - Thick", "thickThick"),
     ];
-    let resp = preview_dropdown(ui, w, |p, r| paint_stroke_type(p, r, cur));
+    let resp = preview_dropdown(ui, w, crate::i18n::tr(&app.ui.language, "Stroke Type"), |p, r| paint_stroke_type(p, r, cur, egui::Color32::BLACK));
     egui::Popup::menu(&resp).show(|ui| {
-        ui.set_min_width(140.0);
+        ui.set_min_width(170.0);
         for (label, kind) in TYPES {
-            if ui.selectable_label(*kind == cur, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
+            if preview_row(ui, *kind == cur, crate::i18n::tr(&app.ui.language, label), 40.0, |p, r, c| paint_stroke_type(p, r, kind, c)).clicked() {
                 let ty = if *kind == "dashed" { json!({"kind": "dashed", "pattern": [12.0, 4.0]}) } else { json!({"kind": kind}) };
                 let _ = app.run("object.stroke", json!({"type": ty}));
                 ui.close();
@@ -718,8 +718,7 @@ pub(crate) fn stroke_type_dropdown(app: &mut DesignApp, ui: &mut Ui, cur: &str,
     });
 }
 
-fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str) {
-    let c = egui::Color32::BLACK;
+fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str, c: egui::Color32) {
     let (x0, x1, y) = (r.min.x + 5.0, r.max.x - 5.0, r.center().y);
     match kind {
         "dashed" => {
@@ -752,10 +751,44 @@ fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str) {
     }
 }
 
+/// A row of a preview dropdown's list, as InDesign's: a small preview of the choice (`preview_w`
+/// wide), then its name. Selected rows are highlighted.
+pub(crate) fn preview_row(
+    ui: &mut Ui,
+    selected: bool,
+    label: &str,
+    preview_w: f32,
+    paint: impl FnOnce(&egui::Painter, Rect, egui::Color32),
+) -> egui::Response {
+    let t = Tokens::get(ui.ctx());
+    // As wide as the preview and its name, translated (at least 130 pt for the name), and as the
+    // popup once its widest row has set it: the rows' highlights then end together.
+    let font = egui::FontId::proportional(12.0);
+    let text_w = ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), font, egui::Color32::WHITE).size().x);
+    let w = (preview_w + 12.0 + text_w + 12.0).max(preview_w + 130.0).max(ui.available_width().min(600.0));
+    let (r, resp) = ui.allocate_exact_size(vec2(w, 22.0), Sense::click());
+    if selected || resp.hovered() {
+        ui.painter().rect_filled(r, 2.0, if selected { t.row_selected } else { t.hover });
+    }
+    let ink = if selected { t.text_strong } else { t.text };
+    paint(ui.painter(), Rect::from_min_size(r.min + vec2(4.0, 0.0), vec2(preview_w, 22.0)), ink);
+    crate::rtl::paint(
+        ui.painter(),
+        pos2(r.min.x + preview_w + 12.0, r.center().y),
+        egui::Align2::LEFT_CENTER,
+        label,
+        egui::FontId::proportional(12.0),
+        ink,
+    );
+    resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, selected, label));
+    resp
+}
+
 /// A dropdown whose face is a light preview box (stroke type, corner shape).
-fn preview_dropdown(ui: &mut Ui, w: f32, paint: impl FnOnce(&egui::Painter, Rect)) -> egui::Response {
+pub(crate) fn preview_dropdown(ui: &mut Ui, w: f32, label: &str, paint: impl FnOnce(&egui::Painter, Rect)) -> egui::Response {
     let t = Tokens::get(ui.ctx());
     let (r, resp) = ui.allocate_exact_size(vec2(w, FIELD_H), Sense::click());
+    resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, true, label));
     let face = Rect::from_min_max(r.min, pos2(r.max.x - widgets::SEG_W, r.max.y));
     ui.painter().rect_filled(face, 0.0, egui::Color32::from_gray(0xdf));
     paint(ui.painter(), face);
@@ -766,38 +799,56 @@ fn preview_dropdown(ui: &mut Ui, w: f32, paint: impl FnOnce(&egui::Painter, Rect
     resp
 }
 
-fn paint_corner_shape(p: &egui::Painter, r: Rect, shape: CornerShape) {
-    let c = egui::Color32::BLACK;
-    let s = Stroke::new(1.5, c);
-    let (x0, y0, x1, y1) = (r.min.x + 4.0, r.min.y + 4.0, r.max.x - 9.0, r.max.y - 3.0);
-    let k = 8.0;
-    let pts: Vec<egui::Pos2> = match shape {
-        CornerShape::None => vec![pos2(x0, y0), pos2(x1, y0), pos2(x1, y1)],
-        CornerShape::Bevel => vec![pos2(x0, y0), pos2(x1 - k, y0), pos2(x1, y0 + k), pos2(x1, y1)],
-        CornerShape::Inset => vec![pos2(x0, y0), pos2(x1 - k, y0), pos2(x1 - k, y0 + k), pos2(x1, y0 + k), pos2(x1, y1)],
-        CornerShape::Rounded | CornerShape::InverseRounded | CornerShape::Fancy => {
-            let mut v = vec![pos2(x0, y0)];
-            for i in 0..=8 {
-                let a = i as f32 / 8.0 * std::f32::consts::FRAC_PI_2;
-                v.push(if shape == CornerShape::InverseRounded {
-                    pos2(x1 - k * a.cos(), y0 + k * a.sin())
-                } else {
-                    pos2(x1 - k + k * a.sin(), y0 + k - k * a.cos())
-                });
-            }
-            v.push(pos2(x1, y1));
-            v
-        }
+/// A corner shape's glyph, as InDesign draws it in its corner lists: the top edge running into the
+/// corner and the right edge leaving it, in a square centred in `r`.
+pub(crate) fn paint_corner_shape(p: &egui::Painter, r: Rect, shape: CornerShape, c: egui::Color32) {
+    let side = (r.width() - 3.0).min(r.height() - 3.0).max(4.0);
+    let o = pos2(r.center().x - side / 2.0, r.center().y - side / 2.0);
+    let at = |x: f32, y: f32| pos2(o.x + x * side, o.y + y * side);
+    let quarter = |cx: f32, cy: f32, rad: f32, from: f32, to: f32| -> Vec<egui::Pos2> {
+        (0..=10).map(|i| from + (to - from) * i as f32 / 10.0).map(|a| at(cx + rad * a.cos(), cy + rad * a.sin())).collect()
     };
-    p.add(egui::Shape::line(pts, s));
+    use std::f32::consts::{FRAC_PI_2, PI};
+    let stroke = Stroke::new(1.6, c);
+    let line = |pts: Vec<egui::Pos2>| {
+        p.add(egui::Shape::line(pts, stroke));
+    };
+    match shape {
+        CornerShape::None => line(vec![at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0)]),
+        // The corner cut by a diagonal, half the side.
+        CornerShape::Bevel => line(vec![at(0.0, 0.0), at(0.5, 0.0), at(1.0, 0.5), at(1.0, 1.0)]),
+        // A square notch.
+        CornerShape::Inset => line(vec![at(0.0, 0.0), at(0.5, 0.0), at(0.5, 0.45), at(1.0, 0.45), at(1.0, 1.0)]),
+        // A concave quarter circle centred on the corner.
+        CornerShape::InverseRounded => {
+            let mut v = vec![at(0.0, 0.0)];
+            v.extend(quarter(1.0, 0.0, 0.5, PI, FRAC_PI_2));
+            v.push(at(1.0, 1.0));
+            line(v)
+        }
+        // A large convex arc.
+        CornerShape::Rounded => {
+            let mut v = vec![at(0.0, 0.0)];
+            v.extend(quarter(0.08, 0.92, 0.92, -FRAC_PI_2, 0.0));
+            v.push(at(1.0, 1.0));
+            line(v)
+        }
+        // Steps into the corner with a small square loop between them.
+        CornerShape::Fancy => {
+            line(vec![at(0.0, 0.0), at(0.38, 0.0), at(0.38, 0.2), at(0.52, 0.2)]);
+            p.rect_stroke(Rect::from_min_max(at(0.52, 0.06), at(0.76, 0.3)), 0.0, stroke, StrokeKind::Middle);
+            line(vec![at(0.76, 0.3), at(0.76, 0.48), at(1.0, 0.48), at(1.0, 1.0)]);
+        }
+    }
 }
 
 pub(crate) fn corner_type_dropdown(app: &mut DesignApp, ui: &mut Ui, shape: CornerShape, size: f64, width: f32) {
-    let resp = preview_dropdown(ui, width, |p, r| paint_corner_shape(p, r, shape));
+    let resp =
+        preview_dropdown(ui, width, crate::i18n::tr(&app.ui.language, "Corner Shape"), |p, r| paint_corner_shape(p, r, shape, egui::Color32::BLACK));
     egui::Popup::menu(&resp).show(|ui| {
-        ui.set_min_width(140.0);
+        ui.set_min_width(150.0);
         for s in CornerShape::ALL {
-            if ui.selectable_label(s == shape, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, s.label()))).clicked() {
+            if preview_row(ui, s == shape, crate::i18n::tr(&app.ui.language, s.label()), 22.0, |p, r, c| paint_corner_shape(p, r, s, c)).clicked() {
                 let _ = app.run("object.cornerOptions", json!({"shape": s, "size": if size > 0.0 { size } else { 12.0 }}));
                 ui.close();
             }
@@ -3876,6 +3927,56 @@ mod tests {
         assert!((r.x0 - 200.0).abs() < 0.5 && (r.y1 - 200.0).abs() < 0.5 && (r.x1 - 300.0).abs() < 0.5, "{r:?}");
     }
 
+    /// A row is as wide as its translated name needs: a long one isn't cut at a fixed width.
+    #[test]
+    fn preview_rows_fit_long_translated_names() {
+        let ctx = egui::Context::default();
+        let long = "Скругленный прямоугольник с обратным углом";
+        let mut widths = (0.0, 0.0);
+        let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
+            egui::Area::new(egui::Id::new("rows")).show(ui.ctx(), |ui| {
+                ui.set_max_width(10.0);
+                let r = preview_row(ui, false, long, 40.0, |_, _, _| {});
+                let text = ui.fonts_mut(|f| f.layout_no_wrap(long.to_string(), egui::FontId::proportional(12.0), egui::Color32::WHITE).size().x);
+                widths = (r.rect.width(), 40.0 + 12.0 + text);
+            });
+        });
+        out.textures_delta.clear();
+        assert!(widths.0 >= widths.1, "{widths:?}");
+    }
+
+    /// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks
+    /// the shape and the current one is marked.
+    #[test]
+    fn the_corner_shape_list_has_glyph_rows_and_picks_a_shape() {
+        let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+        app.run("file.new", json!({})).unwrap();
+        let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "shape": "rectangle"})).unwrap()["id"].as_u64().unwrap();
+        app.run("selection.set", json!({"ids": [id]})).unwrap();
+        let mut h = panel(app);
+        h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click();
+        h.run_steps(3);
+        let rows = ["None", "Fancy", "Bevel", "Inset", "Inverse Rounded", "Rounded"];
+        for r in rows {
+            let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, r);
+        }
+        h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel").click();
+        h.run_steps(3);
+        let it = h.state().session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().clone();
+        assert!(it.corners.corners.iter().all(|c| c.shape == designcraft_geom::corners::CornerShape::Bevel), "{:?}", it.corners);
+        h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click();
+        h.run_steps(3);
+        use egui_kittest::kittest::NodeT;
+        let n = h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel");
+        assert_eq!(n.accesskit_node().toggled(), Some(egui::accesskit::Toggled::True), "the current shape is marked");
+        // The stroke type list has the same rows, by name.
+        h.key_press(egui::Key::Escape);
+        h.run_steps(2);
+        h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Stroke Type").click();
+        h.run_steps(3);
+        let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, "Thick - Thin");
+    }
+
     const CYAN: &str = "C=100 M=0 Y=0 K=0";
 
     #[test]
@@ -4279,7 +4380,7 @@ fn flex_section(app: &mut DesignApp, ui: &mut Ui) {
         let cur = serde_json::to_value(cur).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
         let icon = FLEX_JUSTIFY.iter().find(|j| j.0 == cur).map(|j| j.1).unwrap_or("flex-start");
         let resp = place(ui, sub(b, 0.0, 0.0, 58.0, FIELD_H), |ui| {
-            preview_dropdown(ui, 58.0, |p, r| icons::paint(p, r.shrink2(vec2(6.0, 2.0)), icon, egui::Color32::from_gray(60)))
+            preview_dropdown(ui, 58.0, &tr("Justification"), |p, r| icons::paint(p, r.shrink2(vec2(6.0, 2.0)), icon, egui::Color32::from_gray(60)))
         });
         egui::Popup::menu(&resp).show(|ui| {
             ui.set_min_width(150.0);
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
@@ -337,7 +337,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
 - Drop caps (`type.para {dropCapLines, dropCapChars, dropCapStyle, dropCapAlignLeft, dropCapScaleDescenders}`, Paragraph panel, Paragraph Style Options › Drop Caps and Nested Styles; IDML DropCapLines/DropCapCharacters/DropcapDetail, the drop cap style as the leading "through 1 Dropcap" nested style): the first characters (code points, as InDesign counts them, never splitting a letter from its combining marks; up to 150) are enlarged from the first line's cap height down N − 1 leadings of the text after them (up to 25 lines; grid steps on a baseline grid), the lines beside them are indented by their advance (on the right in right-to-left paragraphs), spaces right after them are set at their size without widening the indent, baselines don't move; a one-line drop cap keeps a smaller size of its own and sets its tabs at the paragraph's tab stops; Align Left Edge (on by default) moves the drop cap left by its first letter's side bearing so its ink starts at the indent; Scale for Descenders is kept but doesn't change the layout yet; a drop cap character style, and nested styles through the drop cap (Dropcap delimiter). Drawn on screen and in PDF (text in order), with caret, hit testing and selection over the enlarged letters. Vertical text sets no drop caps.
 - User dictionary hyphenation exceptions (`hyphenation.addException/removeException/list`, Edit ▸ Spelling ▸ User Dictionary): `ex~am~ple` breaks only at `~`, a plain word never hyphenates.
 - Frame Fitting Options (`object.fittingOptions`, Object ▸ Fitting ▸ Frame Fitting Options…): Auto-Fit refits on resize, fitting, Align From reference point, crop amounts; IDML FrameFittingOption.
-- Paragraph borders (per-side weights, colour, tint, offsets) and shading offsets, drawn per column part of split paragraphs (also when overset); Paragraph panel controls; IDML ParagraphBorder*/ParagraphShading*Offset.
+- Paragraph borders (per-side weights, colour, tint, offsets) and shading offsets, drawn per column part of split paragraphs (also when overset); Paragraph panel controls; IDML ParagraphBorder*/ParagraphShading*Offset. Overprint for the shading, the border and its gap (`shadingOverprint`, `borderOverprint`, `borderGapOverprint`; IDML ParagraphShadingOverprint, ParagraphBorderOverprint, ParagraphBorderGapOverprint) is kept; PDF export warns that it does not overprint yet, as for objects. Paragraph Style Options › Paragraph Border and Paragraph Shading are laid out as InDesign's: the page's heading, tint spinners with Overprint, corners in a two-by-two grid with shape glyph menus and the link between, spinner offsets.
 - Font menu laid out like InDesign's: search, a Filters row (classification: serif, slab serif, sans serif, script, monospaced, decorative, symbol, read from PANOSE, the OS/2 family class, the name, else the shape of the "l"; Show Favorites Only; Show Recently Added, fonts added in the last 30 days), the sample text (the selected text, Sample, pangrams) in three sizes, Recently Used fonts at the top (`recentFonts` preference), favourites (stars, `favoriteFonts`), and each family previewed in its own face with its format (TrueType, OpenType, variable, colour). A family of several styles shows how many and opens to list them, each previewed in its own face, and a style picked is applied as it is; Show Similar Fonts (≈) lists the families of the same class nearest by PANOSE. Western (and other-script) families come first, then Japanese, Simplified Chinese, Traditional Chinese and Korean ones, each group alphabetical and set off by a separator; the group comes from the font's own data (`meta` languages, family names in a CJK language, OS/2 code pages, else what it covers), read once (installed fonts by the font scan, without loading them). CJK families show their native names, the English name on hover; Preferences › Type › Show Font Names in English (`showFontNamesInEnglish`) shows English names throughout. Search matches either name; documents store the English name. Every family list (Character panel, Control bar, Properties, style dialogs, Find Font, Glyphs) follows this order.
 - Application preferences (engine `Prefs`) now persist across launches (desktop: prefs.json beside ui.json).
 - View ▸ Fit Selection in Window (⌥⌘=) and Find/Change ▸ Object (`find.objects`, `find.changeObjects`: by fill, stroke, weight, opacity, kind, layer, label).
