diff --git a/crates/engine/src/cmd/selection_cmds.rs b/crates/engine/src/cmd/selection_cmds.rs
--- a/crates/engine/src/cmd/selection_cmds.rs
+++ b/crates/engine/src/cmd/selection_cmds.rs
@@ -458,16 +458,16 @@ fn modify(s: &mut Session, p: &Value, op: &str) -> Result<Value> {
     if canvas.size().area() > MAX_MODIFY_PIXELS as u64 {
         return Err(EngineError::BadParams { cmd: format!("select.modify.{op}"), msg: "the selection is too large to modify".into() });
     }
+    // Feather reaches ⌈3σ⌉ (σ = r, as in Photoshop); the others reach at most ceil(r).
+    let reach = if op == "feather" { sel::feather_reach(r) } else { r.ceil() as usize };
     // Keep both mask conversion and tiled replacement within the affected region. The
     // zero halo makes internal window edges inert; nonzero edges can only be canvas edges,
     // so the existing Apply effect at canvas bounds padding still has the same meaning.
-    let area = if matches!(op, "expand" | "contract" | "border") { modify_window::area(doc, r) } else { canvas };
+    let area = modify_window::area(doc, reach as f32);
     let m = sel::mask_from_surface(doc.selection.as_ref(), area);
     let (w, h) = (area.width() as usize, area.height() as usize);
-    // Feather reaches ⌈3σ⌉ (σ = r, as in Photoshop); the others reach r. A margin past that
-    // keeps the canvas edge out of reach. A selection clear of the canvas edge needs none: both
-    // readings agree there.
-    let reach = if op == "feather" { sel::feather_reach(r) } else { r.ceil() as usize };
+    // A margin past the reach keeps the padded edge out of reach. A selection clear of the
+    // canvas edge needs none: both readings agree there.
     let pad = if touches_edge(&m, w, h) { reach.saturating_add(2) } else { 0 };
     let grown = |n: usize| pad.checked_mul(2).and_then(|p| n.checked_add(p));
     let (Some(pw), Some(ph)) = (grown(w), grown(h)) else {
diff --git a/crates/engine/src/cmd/selection_cmds/modify_window/tests.rs b/crates/engine/src/cmd/selection_cmds/modify_window/tests.rs
--- a/crates/engine/src/cmd/selection_cmds/modify_window/tests.rs
+++ b/crates/engine/src/cmd/selection_cmds/modify_window/tests.rs
@@ -17,13 +17,17 @@ fn full_canvas_reference(doc: &Document, op: &str, radius: f32, at_bounds: bool)
     let canvas = doc.bounds();
     let m = sel::mask_from_surface(doc.selection.as_ref(), canvas);
     let (w, h) = (canvas.width() as usize, canvas.height() as usize);
-    let pad = if touches_edge(&m, w, h) { radius.ceil() as usize + 2 } else { 0 };
+    let reach = if op == "feather" { sel::feather_reach(radius) } else { radius.ceil() as usize };
+    let pad = if touches_edge(&m, w, h) { reach + 2 } else { 0 };
     let (pw, ph) = (w + 2 * pad, h + 2 * pad);
     let padded = if pad == 0 { m } else { pad_mask(&m, w, h, pad, op == "border" || at_bounds) };
     let out = match op {
         "expand" => sel::expand(&padded, pw, ph, radius),
         "contract" => sel::contract(&padded, pw, ph, radius),
-        _ => sel::border(&padded, pw, ph, radius),
+        "border" => sel::border(&padded, pw, ph, radius),
+        "smooth" => sel::smooth(&padded, pw, ph, radius),
+        "feather" => sel::feather(&padded, pw, ph, radius),
+        _ => panic!("unknown modify operation: {op}"),
     };
     let out = if pad == 0 { out } else { crop_mask(&out, pw, pad, w, h) };
     sel::combine(None, &out, canvas, SelectionMode::Replace)
@@ -36,9 +40,11 @@ fn sparse_36mp_command_only_materializes_the_selection_halo() {
     let doc = document(6000, 6000, selection);
     assert_eq!(area(&doc, 5.0), Rect::new(2994, 2994, 3056, 3056));
     assert_eq!(area(&doc, 5.0).size().area(), 62 * 62);
-    let mut session = Session::new();
-    session.add_document(doc, None);
-    for op in ["expand", "contract", "border"] {
+    assert_eq!(area(&doc, sel::feather_reach(5.0) as f32), Rect::new(2984, 2984, 3066, 3066));
+    assert_eq!(area(&doc, sel::feather_reach(5.0) as f32).size().area(), 82 * 82);
+    for op in ["expand", "contract", "border", "smooth", "feather"] {
+        let mut session = Session::new();
+        session.add_document(doc.clone(), None);
         session.execute(&format!("select.modify.{op}"), json!({"radius": 5})).unwrap();
         assert!(session.active().unwrap().doc.selection.is_some());
     }
@@ -71,8 +77,11 @@ fn cropped_commands_match_full_canvas_reads_padding_and_writeback() {
             selection.write_pixel(-10, -10, &[1.0]);
         }
         let doc = document(75, 61, selection);
-        for radius in [1.0, 5.0, 9.5] {
-            for op in ["expand", "contract", "border"] {
+        for radius in [0.1, 1.0, 1.5, 5.0, 9.5, 25.0] {
+            for op in ["expand", "contract", "border", "smooth", "feather"] {
+                if radius < 1.0 && op != "feather" {
+                    continue;
+                }
                 for at_bounds in [false, true] {
                     let expected = full_canvas_reference(&doc, op, radius, at_bounds);
                     let mut session = Session::new();
@@ -91,21 +100,79 @@ fn cropped_commands_match_full_canvas_reads_padding_and_writeback() {
     }
 }
 
+#[test]
+fn cropped_blurs_preserve_soft_masks_at_every_depth() {
+    for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
+        let format = PixelFormat { sample: depth, ..PixelFormat::GRAY8 };
+        for default in [0.0, 0.25] {
+            let mut selection = Surface::with_default(format, &[default]);
+            for y in 21..39 {
+                for x in 28..47 {
+                    selection.write_pixel(x, y, &[((x * 13 + y * 7) % 17) as f32 / 16.0]);
+                }
+            }
+            let mut doc = Document::new("soft modify", Size::new(83, 67), ColorMode::Rgb, depth);
+            doc.selection = Some(selection);
+            for op in ["smooth", "feather"] {
+                for radius in [1.0, 2.5, 9.5] {
+                    for at_bounds in [false, true] {
+                        let expected = full_canvas_reference(&doc, op, radius, at_bounds);
+                        let mut session = Session::new();
+                        session.add_document(doc.clone(), None);
+                        session.execute(&format!("select.modify.{op}"), json!({"radius": radius, "applyAtCanvasBounds": at_bounds})).unwrap();
+                        assert_eq!(
+                            sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), doc.bounds()),
+                            sel::mask_from_surface(expected.as_ref(), doc.bounds()),
+                            "{depth:?} default {default} {op} radius {radius} bounds {at_bounds}"
+                        );
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[test]
+fn cropped_blurs_preserve_single_pixel_and_thin_canvases() {
+    for (w, h) in [(1, 1), (1, 19), (23, 1)] {
+        let mut selection = Surface::new(PixelFormat::GRAY8);
+        selection.write_pixel(w as i32 / 2, h as i32 / 2, &[1.0]);
+        let doc = document(w, h, selection);
+        for op in ["smooth", "feather"] {
+            for radius in [1.0, 9.5] {
+                for at_bounds in [false, true] {
+                    let expected = full_canvas_reference(&doc, op, radius, at_bounds);
+                    let mut session = Session::new();
+                    session.add_document(doc.clone(), None);
+                    session.execute(&format!("select.modify.{op}"), json!({"radius": radius, "applyAtCanvasBounds": at_bounds})).unwrap();
+                    assert_eq!(
+                        sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), doc.bounds()),
+                        sel::mask_from_surface(expected.as_ref(), doc.bounds()),
+                        "{w}x{h} {op} radius {radius} bounds {at_bounds}"
+                    );
+                }
+            }
+        }
+    }
+}
+
 #[test]
 fn cropped_replacement_keeps_undo_and_redo() {
     let canvas = Rect::new(0, 0, 75, 61);
     let selection = sel::mask_to_surface(&vec![1.0; 10 * 10], Rect::new(30, 25, 40, 35));
     let doc = document(75, 61, selection);
     let before = sel::mask_from_surface(doc.selection.as_ref(), canvas);
-    let mut session = Session::new();
-    session.add_document(doc, None);
-    session.execute("select.modify.expand", json!({"radius": 5})).unwrap();
-    let after = sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), canvas);
-    assert_ne!(before, after);
-    session.execute("edit.undo", json!({})).unwrap();
-    assert_eq!(sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), canvas), before);
-    session.execute("edit.redo", json!({})).unwrap();
-    assert_eq!(sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), canvas), after);
+    for op in ["expand", "smooth", "feather"] {
+        let mut session = Session::new();
+        session.add_document(doc.clone(), None);
+        session.execute(&format!("select.modify.{op}"), json!({"radius": 5})).unwrap();
+        let after = sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), canvas);
+        assert_ne!(before, after, "{op}");
+        session.execute("edit.undo", json!({})).unwrap();
+        assert_eq!(sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), canvas), before, "{op}");
+        session.execute("edit.redo", json!({})).unwrap();
+        assert_eq!(sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), canvas), after, "{op}");
+    }
 }
 
 /// Isolate command-window savings: both paths use the optimized algorithm kernels, while
@@ -114,43 +181,46 @@ fn cropped_replacement_keeps_undo_and_redo() {
 #[ignore = "36 MP release benchmark; run on GitHub Actions"]
 fn sparse_modify_release_comparison() {
     use std::time::Instant;
-    let selection = sel::mask_to_surface(&vec![1.0; 50 * 50], Rect::new(3000, 3000, 3050, 3050));
-    let doc = document(6000, 6000, selection);
-    for radius in [3.0, 50.0] {
-        for op in ["expand", "contract", "border"] {
-            let reference = full_canvas_reference(&doc, op, radius, false);
-            let expected = sel::mask_from_surface(reference.as_ref(), doc.bounds());
-            let mut old_times = Vec::new();
-            let mut new_times = Vec::new();
-            for round in 0..3 {
-                for old in if round % 2 == 0 { [true, false] } else { [false, true] } {
-                    let mut session = Session::new();
-                    session.add_document(doc.clone(), None);
-                    let now = Instant::now();
-                    if old {
-                        let result = full_canvas_reference(&doc, op, radius, false);
-                        session
-                            .edit("Modify selection", |doc, _| {
-                                doc.selection = result;
-                                Ok(())
-                            })
-                            .unwrap();
-                    } else {
-                        session.execute(&format!("select.modify.{op}"), json!({"radius": radius})).unwrap();
-                    }
-                    let elapsed = now.elapsed().as_secs_f64() * 1000.0;
-                    if old {
-                        old_times.push(elapsed)
-                    } else {
-                        new_times.push(elapsed)
+    for (kind, bounds) in [("sparse", Rect::new(3000, 3000, 3050, 3050)), ("dense", Rect::new(1000, 1000, 5000, 5000)), ("full", Rect::new(0, 0, 6000, 6000))] {
+        let selection = sel::mask_to_surface(&vec![1.0; bounds.size().area() as usize], bounds);
+        let doc = document(6000, 6000, selection);
+        let ops: &[&str] = if kind == "sparse" { &["expand", "contract", "border", "smooth", "feather"] } else { &["smooth", "feather"] };
+        for radius in [3.0, 50.0] {
+            for &op in ops {
+                let reference = full_canvas_reference(&doc, op, radius, false);
+                let expected = sel::mask_from_surface(reference.as_ref(), doc.bounds());
+                let mut old_times = Vec::new();
+                let mut new_times = Vec::new();
+                for round in 0..3 {
+                    for old in if round % 2 == 0 { [true, false] } else { [false, true] } {
+                        let mut session = Session::new();
+                        session.add_document(doc.clone(), None);
+                        let now = Instant::now();
+                        if old {
+                            let result = full_canvas_reference(&doc, op, radius, false);
+                            session
+                                .edit("Modify selection", |doc, _| {
+                                    doc.selection = result;
+                                    Ok(())
+                                })
+                                .unwrap();
+                        } else {
+                            session.execute(&format!("select.modify.{op}"), json!({"radius": radius})).unwrap();
+                        }
+                        let elapsed = now.elapsed().as_secs_f64() * 1000.0;
+                        if old {
+                            old_times.push(elapsed)
+                        } else {
+                            new_times.push(elapsed)
+                        }
+                        assert_eq!(sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), doc.bounds()), expected);
                     }
-                    assert_eq!(sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), doc.bounds()), expected);
                 }
+                old_times.sort_by(f64::total_cmp);
+                new_times.sort_by(f64::total_cmp);
+                let (old, new) = (old_times[1], new_times[1]);
+                println!("36 MP {kind} command {op} r={radius}: full_canvas={old:.3}ms cropped={new:.3}ms speedup={:.2}x exact=true", old / new);
             }
-            old_times.sort_by(f64::total_cmp);
-            new_times.sort_by(f64::total_cmp);
-            let (old, new) = (old_times[1], new_times[1]);
-            println!("36 MP command {op} r={radius}: full_canvas={old:.3}ms cropped={new:.3}ms speedup={:.2}x exact=true", old / new);
         }
     }
 }
@@ -165,7 +235,7 @@ fn corrupt_tile_coordinates_do_not_panic_while_finding_the_window() {
     let doc = document(75, 61, selection);
     // Without the guard, content_bounds overflows while sorting these coordinates.
     assert_eq!(area(&doc, 5.0), doc.bounds());
-    for op in ["expand", "contract", "border"] {
+    for op in ["expand", "contract", "border", "smooth", "feather"] {
         let mut session = Session::new();
         session.add_document(doc.clone(), None);
         session.execute(&format!("select.modify.{op}"), json!({"radius": 5})).unwrap();
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where PhotoCraft falls short of Photoshop
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (cropped selection morphology and command mask conversion performance evidence added; G9: 12 of 178 preferences do nothing, as the scorecard prefs audit counts them; G6: Art History Brush landed as partial, TOOL-213-20, #1610, so no toolbar tool is missing; G4: five hard-round built-in presets use 5% spacing to remove stacked-tip outlines and reduce pressure-opacity bands, #8842; G9: Crop tool canvas menu, #6561; G2: Content-Aware Scale seam carving 1.65–3.6× faster with unchanged output, P28 row 3.3 → 1.6 s on a 4-core VM, #5236; G9: UI scale presets done, 105–120% steps, #3222; G14: lossy DNG, raw sidecars, Camera Raw Defaults live, Calibration and Auto, CR3 assessed as not clean-room feasible; G8: PSD type on a path opens on its path and saves back into the file's `Txt2`, #7962; G11: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430; adjustment clipping stays visible in compact Properties, #5231; G9: Color panel modes, ramp and copy commands, the same in every theme, UI-217-17, #6000; G17: Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; G10: `.asl` style libraries landed, #1051; G8: area type (TYPE-218-10); G1: smart objects keep Photoshop's Cylinder warp, #4112; G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: 32-bit Hue/Saturation and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; G11: compound layer properties export as separate Photoshop Set events, rename last, #8304; G11: Batch and droplets take the result at the action's Save step, so actions ending in Save As and Close work; G11: `.atn` steps that switch to another open document import, play and save back, #7103; G14: the Nikon D4 white level follows the sample depth, so 12-bit D4 NEFs no longer open 2.6 stops dark, #5978; G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E), #5987; Apply Layer Comp action mapping, #7101; G10: JPEG Options dialog landed, FILE-215-19 partial (no Matte, progressive Scans or size preview); G10: Cineon write; G10: DPX read and write, #1049, Cineon and Amiga IFF read; Classical Object Finder improvement; learned segmentation remains open; G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed; G10: Amiga IFF write; G10: PSD spot channels in Photoshop's polarity, earlier PhotoCraft PSDs keep their ink, #7415 split; G10: TIFF alpha and spot channels written and read as extra samples, FILE-215-7 done, FILE-215-17's TIFF part, #7415) · **Target:** Adobe Photoshop 2026
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (Smooth and Feather command masks cropped to selection bounds plus blur reach; cropped selection morphology and command mask conversion performance evidence added; G9: 12 of 178 preferences do nothing, as the scorecard prefs audit counts them; G6: Art History Brush landed as partial, TOOL-213-20, #1610, so no toolbar tool is missing; G4: five hard-round built-in presets use 5% spacing to remove stacked-tip outlines and reduce pressure-opacity bands, #8842; G9: Crop tool canvas menu, #6561; G2: Content-Aware Scale seam carving 1.65–3.6× faster with unchanged output, P28 row 3.3 → 1.6 s on a 4-core VM, #5236; G9: UI scale presets done, 105–120% steps, #3222; G14: lossy DNG, raw sidecars, Camera Raw Defaults live, Calibration and Auto, CR3 assessed as not clean-room feasible; G8: PSD type on a path opens on its path and saves back into the file's `Txt2`, #7962; G11: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430; adjustment clipping stays visible in compact Properties, #5231; G9: Color panel modes, ramp and copy commands, the same in every theme, UI-217-17, #6000; G17: Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; G10: `.asl` style libraries landed, #1051; G8: area type (TYPE-218-10); G1: smart objects keep Photoshop's Cylinder warp, #4112; G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: 32-bit Hue/Saturation and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; G11: compound layer properties export as separate Photoshop Set events, rename last, #8304; G11: Batch and droplets take the result at the action's Save step, so actions ending in Save As and Close work; G11: `.atn` steps that switch to another open document import, play and save back, #7103; G14: the Nikon D4 white level follows the sample depth, so 12-bit D4 NEFs no longer open 2.6 stops dark, #5978; G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E), #5987; Apply Layer Comp action mapping, #7101; G10: JPEG Options dialog landed, FILE-215-19 partial (no Matte, progressive Scans or size preview); G10: Cineon write; G10: DPX read and write, #1049, Cineon and Amiga IFF read; Classical Object Finder improvement; learned segmentation remains open; G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed; G10: Amiga IFF write; G10: PSD spot channels in Photoshop's polarity, earlier PhotoCraft PSDs keep their ink, #7415 split; G10: TIFF alpha and spot channels written and read as extra samples, FILE-215-7 done, FILE-215-17's TIFF part, #7415) · **Target:** Adobe Photoshop 2026
 
 Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
 on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
@@ -294,6 +294,12 @@ what an agent can't finish alone.
 - **Estimate:** 150–250 h.
 
 ### G2. Interactive performance
+- **Progress (2026-10-27):** Smooth and Feather also crop command mask reads and
+  replacement to the nonzero selection bounds plus their blur reach and a zero margin.
+  A 50×50 selection at radius 5 uses 62×62 and 82×82 samples respectively on a 36 MP
+  canvas. Full-canvas reference tests and remote release benchmarks cover both commands;
+  GUI budgets and readiness percentages are unchanged.
+  [Method and limits](selection-distance-performance.md#cropped-smooth-and-feather-commands-2026-10-27).
 - **Measured progress (2026-10-26):** Expand, Contract and Border restrict distance work to
   the nonzero bounds plus a radius halo. Small radii use capped distances; Contract avoids
   inverted float masks and Border retains one distance plane. On 36 MP, eight synthetic
@@ -395,6 +401,7 @@ what an agent can't finish alone.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-27 | minor | Smooth/Feather command reads and replacement use selection bounds plus blur reach; full-canvas reference and release comparisons extended; percentages and scorecard budgets unchanged |
 | 2026-10-26 | minor | Expand/Contract/Border distance work, mask reads and replacement cropped to nonzero bounds; bounded distances for radii ≤5; 36 MP exact-output comparisons improve 1.54–37.82× across eight cases; percentages, estimates and scorecard budgets unchanged |
 | 2026-10-25 | minor | G9: the count of preferences that do nothing is the scorecard's again, 12 of 178 (it still said 24 of 171, from 2026-10-15); no estimate changed |
 | 2026-10-25 | minor | G6: Art History Brush landed as partial (TOOL-213-20, #1610), so no toolbar tool is missing; no percentage or estimate changed |
diff --git a/docs/selection-distance-performance.md b/docs/selection-distance-performance.md
--- a/docs/selection-distance-performance.md
+++ b/docs/selection-distance-performance.md
@@ -145,8 +145,8 @@ selections clear normally. The existing oversized-canvas safety limit remains.
 
 Replacement already accepts a rectangle and builds a fresh sparse selection surface,
 so it writes only affected tiles and retains the existing undoable transaction. The
-shared `current_mask` helper and Grow/Similar are unchanged. Smooth and Feather keep
-their existing command paths.
+shared `current_mask` helper and Grow/Similar are unchanged. Smooth and Feather also
+use command windows as described below.
 
 Command tests compare against full-canvas reads, padding and replacement for empty/full
 selections, both canvas-bounds settings, edge-touching masks, holes, fractional coverage,
@@ -176,3 +176,32 @@ Border also skips distance planes and coverage passes when its binary mask is un
 there is no edge inside the supplied rectangle. Extreme library radii retain the finite
 no-seed sentinel behavior; the command's canvas-edge padding still creates a real edge
 for Select All.
+
+
+## Cropped Smooth and Feather commands (2026-10-27)
+
+All five Select › Modify commands now use the same bounded mask read and replacement.
+Smooth grows the nonzero selection bounds by `ceil(radius) + 1`. Feather takes the
+truncated Gaussian reach from `selection::feather_reach(radius)` and adds the same
+one-pixel zero margin: `ceil(3 * radius) + 1` for the accepted command radii. Using the
+blur's shared reach keeps command windows and canvas-edge padding in sync with the
+kernel. Both windows are clipped to the canvas before any mask allocation.
+
+For a 50×50 selection at radius 5 on a 6000×6000 canvas, the command supplies 62×62
+samples to Smooth and 82×82 to Feather, instead of 36 million samples. Nonzero implicit
+defaults and malformed tile coordinates retain the full-canvas fallback. The existing
+canvas-size safety limit still applies.
+
+The command comparisons above now include Smooth and Feather, fractional radii down
+to Feather's 0.1 px minimum, both canvas-bound settings, soft coverage at 8/16/32-bit
+sample depths, one-pixel/thin canvases, and undo/redo. They compare stored selection
+coverage with the previous full-canvas path.
+
+Dispatch the fork's CI with `selection_morphology=true` to run the release comparison
+and the general quick performance suite. The existing
+`sparse_modify_release_comparison` test now measures both blurs on sparse 50×50,
+dense 4000×4000 and fully selected masks in a 36 MP canvas, at radii 3 and 50. It
+alternates execution order for three rounds, reports median command times and verifies
+every output sample outside timing. The full-canvas reference uses the same optimized
+blur kernels and includes the undoable edit. GUI rendering and live-preview scheduling
+are excluded; these measurements do not update the P26/P27 scorecard budgets.
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
@@ -1,6 +1,6 @@
 # PhotoCraft vs Adobe Photoshop: target-app parity
 
-> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-26 · **Change:** minor (cropped selection morphology and command mask conversion performance evidence added; 12 of 178 preferences do nothing, from the scorecard; the Camera Raw row names per-camera colour for non-DNG cameras as missing, not the Calibration panel, which landed 2026-10-13; Content-Aware Scale measurements added, #5236; Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; area type, TYPE-218-10 partial, Type scorecard 5 / 5 / 0; compound layer properties export as separate Photoshop Set events, #8304; JPEG Options dialog landed, FILE-215-19 partial; Print › Functions › Bleed through `file.print`; Content-Aware Move live preview for every selection size, TOOL-213-4 done, #6680; retouching evidence: Pattern Stamp and Background Eraser draw live, only the Mixer Brush is left of TOOL-213-7; fill and adjustment layers copy and paste whole, UI-217-23 partial, #2344; shape layers take Density and Feather, TOOL-214-12 done, #5846; on-canvas corner radius widgets, TOOL-214-12; `.atn` import maps 112 events; Free Transform H/V skew fields; stale rows corrected against `main` and the scorecard, estimates unchanged; Photoshop Actions `.atn` load and save; 2026-10-10: Tilt-Shift measurements added; earlier the same day: major, full re-measure against Photoshop 2026 27.11.0; merged `parity-estimate.md` and the roadmap's honest assessment; stale missing-tool rows corrected: Color Replacement and Red Eye are live toolbar tools, Frame landed; percentages unchanged; PSD spot plates in Photoshop's polarity, #7415 split; TIFF spot plates, FILE-215-17's TIFF part, #7415) · **Target:** Adobe Photoshop 2026
+> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-27 · **Change:** minor (Smooth and Feather command masks cropped to selection bounds plus blur reach; cropped selection morphology and command mask conversion performance evidence added; 12 of 178 preferences do nothing, from the scorecard; the Camera Raw row names per-camera colour for non-DNG cameras as missing, not the Calibration panel, which landed 2026-10-13; Content-Aware Scale measurements added, #5236; Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; area type, TYPE-218-10 partial, Type scorecard 5 / 5 / 0; compound layer properties export as separate Photoshop Set events, #8304; JPEG Options dialog landed, FILE-215-19 partial; Print › Functions › Bleed through `file.print`; Content-Aware Move live preview for every selection size, TOOL-213-4 done, #6680; retouching evidence: Pattern Stamp and Background Eraser draw live, only the Mixer Brush is left of TOOL-213-7; fill and adjustment layers copy and paste whole, UI-217-23 partial, #2344; shape layers take Density and Feather, TOOL-214-12 done, #5846; on-canvas corner radius widgets, TOOL-214-12; `.atn` import maps 112 events; Free Transform H/V skew fields; stale rows corrected against `main` and the scorecard, estimates unchanged; Photoshop Actions `.atn` load and save; 2026-10-10: Tilt-Shift measurements added; earlier the same day: major, full re-measure against Photoshop 2026 27.11.0; merged `parity-estimate.md` and the roadmap's honest assessment; stale missing-tool rows corrected: Color Replacement and Red Eye are live toolbar tools, Frame landed; percentages unchanged; PSD spot plates in Photoshop's polarity, #7415 split; TIFF spot plates, FILE-215-17's TIFF part, #7415) · **Target:** Adobe Photoshop 2026
 
 The authoritative answer to "how close is PhotoCraft to Photoshop, and how much work is left". The
 one-page summary is [`ROADMAP.md`](../ROADMAP.md); the ranked work list is [`gaps.md`](gaps.md);
@@ -363,6 +363,12 @@ i7-9750H, 12 workers, 24 MP RGBA8, Good quality, amount 1: Spin 66.10 → 16.43
 (4.02×), Zoom 19.19 → 5.35 s (3.59×), one paired run each, exact output equality.
 These are local measurements at amount 1; see [method and limits](radial-blur-performance.md).
 
+2026-10-27: Smooth and Feather command reads and replacement also use selection bounds
+plus blur reach and a zero margin. A 50×50 selection at radius 5 supplies 62×62 and
+82×82 samples respectively on a 36 MP canvas. Full-canvas reference comparisons and
+remote release benchmarks cover both commands; GUI budgets and percentages are unchanged.
+See [method and limits](selection-distance-performance.md#cropped-smooth-and-feather-commands-2026-10-27).
+
 2026-10-26: cropped Expand, Contract and Border measure 1.54–37.82× faster across eight
 36 MP synthetic mask cases on GitHub Actions, with exact output equality. Radii up to 5 px
 use capped distances; Contract avoids inverted float masks and Border retains one distance
@@ -580,6 +586,7 @@ See [method, raw measurements and reproduction](five-algorithm-performance.md).
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-27 | minor | Smooth/Feather command reads and replacement use selection bounds plus blur reach; full-canvas reference and release comparisons extended; percentages and scorecard budgets unchanged |
 | 2026-10-26 | minor | Expand/Contract/Border distance work, mask reads and replacement cropped to nonzero bounds; bounded distances for radii ≤5; 36 MP exact-output comparisons improve 1.54–37.82× across eight cases; percentages, estimates and scorecard budgets unchanged |
 | 2026-10-25 | minor | UI / UX and Workspace rows: 12 of 178 preferences do nothing, the scorecard's count. Camera Raw row: the Calibration panel landed on 2026-10-13 (#5128); what is missing is per-camera colour for non-DNG cameras ([gaps.md](gaps.md) G14). Percentages and estimates unchanged |
 | 2026-10-23 | minor | Colour management and modes: PSD spot plates are stored as Photoshop stores them (ink 0 = solid), and PSDs from earlier PhotoCraft versions keep their ink (#7415 split, part 4); estimates unchanged |
