diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,10 @@ on:
     types: [opened, synchronize, reopened, labeled]
   workflow_dispatch:
     inputs:
+      color_range:
+        description: "Compare cropped Color Range on 24 MP (Linux, exact outputs)"
+        type: boolean
+        default: false
       selection_morphology:
         description: "Compare selection morphology on 36 MP (Linux, exact outputs)"
         type: boolean
@@ -424,6 +428,36 @@ jobs:
           name: fuzz-${{ matrix.target.crate }}-${{ matrix.target.name }}
           path: crates/${{ matrix.target.crate }}/fuzz/artifacts
 
+  color-range-performance:
+    if: inputs.color_range == true
+    runs-on: ubuntu-latest
+    timeout-minutes: 45
+    env:
+      RAYON_NUM_THREADS: "4"
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          persist-credentials: false
+      - name: Install Linux build dependencies
+        uses: storytold/craft-common-actions/actions/install-apt-packages@main
+        with:
+          packages: >-
+            libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev
+            libgtk-3-dev
+      - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
+      - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
+        with:
+          key: color-range-perf
+      - name: Compare exact outputs and release timings on 24 MP
+        run: |
+          set -o pipefail
+          cargo test --release -p photocraft-engine --lib color_range_release_comparison -- --ignored --nocapture | tee color-range-perf.txt
+      - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+        if: always()
+        with:
+          name: color-range-performance
+          path: color-range-perf.txt
+
   selection-morphology:
     if: inputs.selection_morphology == true
     runs-on: ubuntu-latest
@@ -459,7 +493,7 @@ jobs:
           path: selection-morphology-perf.txt
 
   selection-morphology-quick-perf:
-    if: inputs.selection_morphology == true
+    if: inputs.selection_morphology == true || inputs.color_range == true
     # Match perf-nightly.yml: Linux llvmpipe lacks the float render targets needed by
     # bevel_bench, so its required GPU rows cannot be measured there.
     runs-on: macos-15
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
@@ -13,6 +13,11 @@ use crate::{EngineError, Result, Session};
 
 mod modify_window;
 
+#[cfg(test)]
+mod color_range_performance;
+#[cfg(test)]
+mod color_range_window_tests;
+
 fn has_doc(s: &Session) -> std::result::Result<(), String> {
     s.active().map(|_| ()).ok_or_else(|| "no document open".into())
 }
@@ -45,15 +50,18 @@ fn scope_color_range(mask: &mut [f32], doc: &Document, area: Rect, m: SelectionM
 }
 
 /// RGBA pixels the tools sample: the composite, or the active layer.
-fn sample_pixels(s: &Session, all_layers: bool) -> Result<(Rect, Vec<[f32; 4]>)> {
+fn sample_pixels(s: &Session, all_layers: bool, window: Option<Rect>) -> Result<(Rect, Vec<[f32; 4]>)> {
     let d = s.active().ok_or(EngineError::NoDocument)?;
-    let area = d.doc.bounds();
+    let area = window.map_or(d.doc.bounds(), |r| r.intersect(&d.doc.bounds()));
     // Match the decoders' raster budget before either the layer or composite path allocates.
     let len = area.size().area();
     let max_bytes: u64 = if cfg!(target_pointer_width = "64") { 8 << 30 } else { 2 << 30 };
     if len > max_bytes / std::mem::size_of::<[f32; 4]>() as u64 {
         return Err(EngineError::Other("the canvas is too large to sample for a selection".into()));
     }
+    if area.is_empty() {
+        return Ok((area, Vec::new()));
+    }
     let layer = d.active_layer.and_then(|id| d.doc.layer(id)).and_then(|l| l.surface());
     match (all_layers, layer) {
         (false, Some(surf)) => {
@@ -149,31 +157,6 @@ fn parse_color(p: &Value) -> [f32; 3] {
     }
 }
 
-/// Eyedropper samples from `key` (`[[x, y], …]` in document pixels): the colour under each point
-/// and where it was picked (area-local, for Localized Color Clusters).
-fn eyedropper_samples(p: &Value, key: &str, area: Rect, px: &[[f32; 4]], bad: &impl Fn(String) -> EngineError) -> Result<Vec<sel::RangeSample>> {
-    let Some(pts) = p.get(key) else { return Ok(Vec::new()) };
-    let pts = pts.as_array().ok_or_else(|| bad(format!("`{key}` must be [[x, y], …]")))?;
-    let w = area.width() as usize;
-    let mut out = Vec::with_capacity(pts.len().min(4096));
-    for pt in pts {
-        let xy = pt.as_array().and_then(|a| match a.as_slice() {
-            [x, y] => Some((x.as_f64()?, y.as_f64()?)),
-            _ => None,
-        });
-        let Some((x, y)) = xy.filter(|(x, y)| x.is_finite() && y.is_finite()) else {
-            return Err(bad(format!("bad point {pt} in `{key}` (want [x, y])")));
-        };
-        let (xi, yi) = (x.floor() as i32, y.floor() as i32);
-        let q = if area.contains(xi, yi) { px.get((yi - area.y0) as usize * w + (xi - area.x0) as usize) } else { None };
-        let Some(&[r, g, b, _]) = q else {
-            return Err(bad(format!("point [{x}, {y}] in `{key}` is outside the canvas")));
-        };
-        out.push(sel::RangeSample { color: [r, g, b], at: Some(((xi - area.x0) as f32, (yi - area.y0) as f32)) });
-    }
-    Ok(out)
-}
-
 /// Composite RGB (the compositor's output profile for the document) → CIE Lab D50.
 fn lab_transform(s: &Session) -> Result<std::sync::Arc<photocraft_cms::Transform>> {
     let d = s.active().ok_or(EngineError::NoDocument)?;
@@ -346,6 +329,23 @@ impl ColorRangeSamples {
     }
 }
 
+/// Replace refines the existing selection; no output outside its nonzero bounds survives.
+fn color_range_area(doc: &Document, m: SelectionMode) -> Rect {
+    let canvas = doc.bounds();
+    let Some(selection) = doc.selection.as_ref().filter(|_| m == SelectionMode::Replace) else { return canvas };
+    // content_bounds describes differences from the default, not implicit selected pixels.
+    if selection.default_pixel().first().is_some_and(|v| *v != 0.0) {
+        return canvas;
+    }
+    // content_bounds orders raw tile coordinates by their differences. Reject coordinates
+    // outside the pixel-addressable range before those differences can overflow.
+    let valid = (i32::MIN / photocraft_geom::TILE_SIZE)..=(i32::MAX / photocraft_geom::TILE_SIZE);
+    if selection.tiles().any(|(tile, _)| !valid.contains(&tile.tx) || !valid.contains(&tile.ty)) {
+        return canvas;
+    }
+    selection.content_bounds().intersect(&canvas)
+}
+
 fn color_range(s: &mut Session, p: &Value) -> Result<Value> {
     const CMD: &str = "select.colorRange";
     let bad = |msg: String| EngineError::BadParams { cmd: CMD.into(), msg };
@@ -357,17 +357,16 @@ fn color_range(s: &mut Session, p: &Value) -> Result<Value> {
     if b(p, "detectFaces", false) {
         return Err(bad("detectFaces: face detection isn't available yet; select without it".into()));
     }
+    let area = color_range_area(&s.active().ok_or(EngineError::NoDocument)?.doc, selection_mode);
     let (area, mut mask) = match preset {
         "sampledColors" => {
-            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true))?;
+            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true), Some(area))?;
             let w = area.width() as usize;
             // The dialog's eyedroppers: colours picked on the image, at their positions.
             // Like Photoshop's, they may sample anywhere on the canvas: an active selection only
             // limits the result (scope_color_range), not where colours are picked (#5244).
-            let plus = eyedropper_samples(p, "points", area, &px, &bad)?;
-            let minus = eyedropper_samples(p, "subtractPoints", area, &px, &bad)?;
-            let query = ColorRangeSamples::with_samples(s, p, area, &plus, &minus)?;
-            let mask = query.coverage(&px, w, 1.0, [0.0, 0.0]);
+            let query = ColorRangeSamples::new(s, p)?;
+            let mask = query.coverage(&px, w, 1.0, [area.x0 as f32, area.y0 as f32]);
             (area, mask)
         }
         "reds" | "yellows" | "greens" | "cyans" | "blues" | "magentas" => {
@@ -379,11 +378,11 @@ fn color_range(s: &mut Session, p: &Value) -> Result<Value> {
                 "blues" => 240.0,
                 _ => 300.0,
             };
-            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true))?;
+            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true), Some(area))?;
             (area, sel::hue_range(&px, center))
         }
         "skinTones" => {
-            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true))?;
+            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true), Some(area))?;
             (area, sel::skin_tones(&px, fuzz(40.0).min(200.0)))
         }
         "highlights" | "midtones" | "shadows" => {
@@ -406,7 +405,7 @@ fn color_range(s: &mut Session, p: &Value) -> Result<Value> {
                     .filter(|(l, h)| l <= h)
                     .ok_or_else(|| bad(format!("tonalRange: want [low, high] levels 0..255 (got {v})")))?,
             };
-            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true))?;
+            let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", true), Some(area))?;
             (area, sel::tone_range(&px, lo, hi, fuzz(20.0).min(100.0) / 100.0 * 255.0))
         }
         "outOfGamut" => {
@@ -545,7 +544,7 @@ fn crop_mask(m: &[f32], pw: usize, pad: usize, w: usize, h: usize) -> Vec<f32> {
 /// the selected pixels' colour range widened by the tolerance.
 fn grow_similar(s: &mut Session, p: &Value, contiguous: bool) -> Result<Value> {
     let tol = f(p, "tolerance", 32.0) / 255.0;
-    let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", false))?;
+    let (area, px) = sample_pixels(s, b(p, "sampleAllLayers", false), None)?;
     let (_, m) = current_mask(&s.active().ok_or(EngineError::NoDocument)?.doc);
     let (mut lo, mut hi) = ([f32::MAX; 4], [f32::MIN; 4]);
     for (q, v) in px.iter().zip(&m) {
diff --git a/crates/engine/src/cmd/selection_cmds/color_range_performance.rs b/crates/engine/src/cmd/selection_cmds/color_range_performance.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/src/cmd/selection_cmds/color_range_performance.rs
@@ -0,0 +1,70 @@
+//! Opt-in release comparison; full-canvas buffers belong on a CI runner.
+use super::*;
+use photocraft_color::{ColorMode, PixelFormat, SampleType};
+use photocraft_doc::{Layer, LayerContent, Size};
+use std::time::Instant;
+
+fn reference(s: &mut Session, p: &Value) {
+    let (canvas, px) = sample_pixels(s, true, None).unwrap();
+    // Previous path: eyedropper colour came from the full composite already in memory.
+    let [r, g, b, _] = px[1500 * canvas.width() as usize + 2000];
+    let point = sel::RangeSample { color: [r, g, b], at: Some((2000.0, 1500.0)) };
+    let query = ColorRangeSamples::with_samples(s, p, canvas, &[point], &[]).unwrap();
+    let mut mask = query.coverage(&px, canvas.width() as usize, 1.0, [0.0, 0.0]);
+    drop(px);
+    scope_color_range(&mut mask, &s.active().unwrap().doc, canvas, SelectionMode::Replace);
+    set_selection(s, "Color Range", canvas, mask, SelectionMode::Replace).unwrap();
+}
+
+#[test]
+#[ignore = "24 MP release comparison: run on GitHub Actions"]
+fn color_range_release_comparison() {
+    let mut doc = Document::new("Color Range performance", Size::new(6000, 4000), ColorMode::Rgb, SampleType::U8);
+    let mut pixels = Surface::with_default(PixelFormat::RGBA8, &[0.65, 0.3, 0.2, 1.0]);
+    pixels.fill_rect(Rect::new(3000, 2000, 3600, 2400), &[0.2, 0.3, 0.65, 1.0]);
+    doc.layers.push(Layer::new("image", LayerContent::Raster(pixels)));
+    let p = json!({"points": [[2000, 1500]], "fuzziness": 40, "localized": true, "range": 80});
+    // Small, 1%, 4%, and full selections: include dense fallback cost in the comparison.
+    for bounds in [Rect::new(2975, 1975, 3025, 2025), Rect::new(2700, 1800, 3300, 2200), Rect::new(2400, 1600, 3600, 2400), doc.bounds()] {
+        let mut selection = Surface::new(PixelFormat::GRAY8);
+        selection.fill_rect(bounds, &[128.0 / 255.0]);
+        doc.selection = Some(selection);
+        let mut old_times = Vec::new();
+        let mut new_times = Vec::new();
+        let mut expected = None;
+        // One warmup pair, followed by three alternating measured pairs.
+        for round in 0..4 {
+            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 {
+                    reference(&mut session, &p);
+                } else {
+                    session.execute("select.colorRange", p.clone()).unwrap();
+                }
+                let elapsed = now.elapsed().as_secs_f64() * 1000.0;
+                if round > 0 {
+                    if old { old_times.push(elapsed) } else { new_times.push(elapsed) }
+                }
+                // Outside timing, compare every stored sample over the whole canvas.
+                let actual = sel::mask_from_surface(session.active().unwrap().doc.selection.as_ref(), doc.bounds());
+                if let Some(ref expected) = expected {
+                    assert_eq!(&actual, expected);
+                } else {
+                    expected = Some(actual);
+                }
+            }
+        }
+        old_times.sort_by(f64::total_cmp);
+        new_times.sort_by(f64::total_cmp);
+        let (old, new) = (old_times[1], new_times[1]);
+        println!(
+            "24 MP Color Range {}x{}: full_canvas={old:.3}ms cropped={new:.3}ms speedup={:.2}x rgba_bytes={} exact=true",
+            bounds.width(),
+            bounds.height(),
+            old / new,
+            bounds.size().area() * 16
+        );
+    }
+}
diff --git a/crates/engine/src/cmd/selection_cmds/color_range_window_tests.rs b/crates/engine/src/cmd/selection_cmds/color_range_window_tests.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/src/cmd/selection_cmds/color_range_window_tests.rs
@@ -0,0 +1,211 @@
+use super::*;
+use photocraft_color::{ColorMode, PixelFormat, SampleType};
+use photocraft_geom::Size;
+use serde_json::json;
+
+fn fixture(depth: SampleType, p3: bool) -> Session {
+    let mut s = Session::new();
+    let mut doc = Document::with_background("color range", Size::new(64, 48), ColorMode::Rgb, depth, photocraft_doc::Color::WHITE);
+    if p3 {
+        doc.icc_profile = Some(photocraft_cms::Builtin::DisplayP3.profile().to_bytes());
+    } else {
+        doc.icc_profile = Some(photocraft_cms::Builtin::Srgb.profile().to_bytes());
+    }
+    let bg = doc.layers[0].surface_mut().unwrap();
+    bg.fill_rect(Rect::new(0, 0, 64, 48), &[0.92, 0.92, 0.92, 1.0]);
+    bg.fill_rect(Rect::new(7, 8, 22, 23), &[0.85, 0.12, 0.08, 1.0]);
+    bg.fill_rect(Rect::new(35, 8, 50, 23), &[0.12, 0.72, 0.18, 1.0]);
+    bg.fill_rect(Rect::new(7, 29, 22, 44), &[0.12, 0.20, 0.86, 1.0]);
+    bg.fill_rect(Rect::new(35, 29, 50, 44), &[0.72, 0.48, 0.31, 1.0]);
+    s.add_document(doc, None);
+    s
+}
+
+fn coverage_at(s: &Session, x: i32, y: i32) -> f32 {
+    s.active().and_then(|d| d.doc.selection.as_ref()).map_or(0.0, |m| m.sample_channel(x, y, 0))
+}
+
+fn full_sampled_reference(s: &Session, params: &Value) -> Vec<f32> {
+    let doc = &s.active().unwrap().doc;
+    let canvas = doc.bounds();
+    let (_, px) = sample_pixels(s, b(params, "sampleAllLayers", true), None).unwrap();
+    // Reproduce the previous command: read eyedropper colours from the full composite.
+    let samples = |key: &str| -> Vec<sel::RangeSample> {
+        params
+            .get(key)
+            .and_then(Value::as_array)
+            .into_iter()
+            .flatten()
+            .map(|point| {
+                let x = point[0].as_f64().unwrap().floor() as usize;
+                let y = point[1].as_f64().unwrap().floor() as usize;
+                let pixel = px[y * canvas.width() as usize + x];
+                sel::RangeSample { color: [pixel[0], pixel[1], pixel[2]], at: Some((x as f32, y as f32)) }
+            })
+            .collect()
+    };
+    let query = ColorRangeSamples::with_samples(s, params, canvas, &samples("points"), &samples("subtractPoints")).unwrap();
+    let mut mask = query.coverage(&px, canvas.width() as usize, 1.0, [canvas.x0 as f32, canvas.y0 as f32]);
+    if b(params, "invert", false) {
+        mask.iter_mut().for_each(|v| *v = 1.0 - *v);
+    }
+    scope_color_range(&mut mask, doc, canvas, mode(params));
+    sel::mask_from_surface(sel::combine(doc.selection.as_ref(), &mask, canvas, mode(params)).as_ref(), canvas)
+}
+
+#[test]
+fn sampled_color_cropped_results_match_full_canvas_for_depths_profiles_and_sample_options() {
+    for (depth, p3) in [SampleType::U8, SampleType::U16, SampleType::F32].into_iter().flat_map(|depth| [false, true].map(|p3| (depth, p3))) {
+        for params in [
+            json!({"points": [[40, 15]], "fuzziness": 12}),
+            json!({"points": [[40, 15]], "subtractPoints": [[10, 15]], "order": ["-", "+"], "fuzziness": 18}),
+            json!({"points": [[40, 15]], "localized": true, "range": 80, "invert": true}),
+            json!({"points": [[10, 15]], "localized": true, "range": 25}),
+            json!({"points": [[40, 15]], "sampleAllLayers": false, "fuzziness": 0}),
+        ] {
+            let mut s = fixture(depth, p3);
+            s.execute("select.rect", json!({"x": 3, "y": 5, "width": 22, "height": 25})).unwrap();
+            let expected = full_sampled_reference(&s, &params);
+            s.execute("select.colorRange", params.clone()).unwrap();
+            let actual = sel::mask_from_surface(s.active().unwrap().doc.selection.as_ref(), s.active().unwrap().doc.bounds());
+            assert_eq!(actual, expected, "depth={depth:?} p3={p3} params={params}");
+        }
+    }
+}
+
+#[test]
+fn range_presets_are_scoped_and_non_replace_modes_keep_full_canvas_semantics() {
+    for preset in ["reds", "yellows", "greens", "cyans", "blues", "magentas", "highlights", "midtones", "shadows", "skinTones"] {
+        let mut s = fixture(SampleType::U8, false);
+        s.execute("select.rect", json!({"x": 2, "y": 4, "width": 24, "height": 25})).unwrap();
+        s.execute("select.colorRange", json!({"select": preset})).unwrap();
+        assert_eq!(coverage_at(&s, 42, 15), 0.0, "preset {preset} escaped selection");
+    }
+    let mut s = fixture(SampleType::U8, false);
+    s.execute("select.rect", json!({"x": 2, "y": 4, "width": 24, "height": 25})).unwrap();
+    s.execute("select.colorRange", json!({"points": [[40, 15]], "fuzziness": 0, "mode": "add"})).unwrap();
+    assert_eq!(coverage_at(&s, 42, 15), 1.0);
+    s.execute("edit.undo", json!({})).unwrap();
+    assert_eq!(coverage_at(&s, 42, 15), 0.0);
+    s.execute("edit.redo", json!({})).unwrap();
+    assert_eq!(coverage_at(&s, 42, 15), 1.0);
+}
+
+#[test]
+fn window_defaults_and_hostile_tiles_fall_back_and_empty_selection_is_safe() {
+    let canvas = Rect::new(0, 0, 64, 48);
+    let mut doc = Document::new("window", Size::new(64, 48), ColorMode::Rgb, SampleType::U8);
+    doc.selection = Some(Surface::with_default(PixelFormat::GRAY8, &[1.0]));
+    assert_eq!(color_range_area(&doc, SelectionMode::Replace), canvas);
+    doc.selection = Some(Surface::new(PixelFormat::GRAY8));
+    assert_eq!(color_range_area(&doc, SelectionMode::Replace), Rect::new(0, 0, 0, 0));
+    use photocraft_geom::TileCoord;
+    use std::sync::Arc;
+    let mut hostile = Surface::new(PixelFormat::GRAY8);
+    let tile = hostile.solid_tile(&[1.0]);
+    hostile.put_tiles([(TileCoord::new(i32::MIN, i32::MIN), Arc::clone(&tile)), (TileCoord::new(i32::MAX, i32::MAX), tile)]);
+    doc.selection = Some(hostile);
+    assert_eq!(color_range_area(&doc, SelectionMode::Replace), canvas);
+    assert_eq!(color_range_area(&doc, SelectionMode::Add), canvas);
+    doc.selection = Some(Surface::new(PixelFormat::GRAY8));
+    let mut s = Session::new();
+    s.add_document(doc, None);
+    assert!(sample_pixels(&s, true, Some(color_range_area(&s.active().unwrap().doc, SelectionMode::Replace))).unwrap().1.is_empty());
+    let err = s.execute("select.colorRange", json!({"select": "shadows", "tonalRange": -1})).unwrap_err();
+    assert!(err.to_string().contains("tonalRange"));
+    for params in [json!({"points": [[64, 0]]}), json!({"points": "invalid"}), json!({"localized": true}), json!({"order": ["+"]})] {
+        assert!(s.execute("select.colorRange", params).is_err());
+    }
+    s.execute("select.colorRange", json!({"points": [[1, 1]], "invert": true})).unwrap();
+    assert!(s.active().unwrap().doc.selection.is_none());
+}
+
+#[test]
+fn sparse_large_selection_samples_only_its_clipped_window() {
+    let mut doc = Document::new("sparse", Size::new(6000, 4000), ColorMode::Rgb, SampleType::U8);
+    let selection = sel::mask_to_surface(&vec![1.0; 24 * 20], Rect::new(2990, 1990, 3014, 2010));
+    doc.selection = Some(selection);
+    let mut s = Session::new();
+    s.add_document(doc, None);
+    let win = color_range_area(&s.active().unwrap().doc, SelectionMode::Replace);
+    assert_eq!(win, Rect::new(2990, 1990, 3014, 2010));
+    let (area, pixels) = sample_pixels(&s, true, Some(win)).unwrap();
+    assert_eq!(pixels.len(), area.size().area() as usize);
+    s.execute("select.colorRange", json!({"select": "highlights"})).unwrap();
+}
+
+#[test]
+fn cropped_range_matches_full_canvas_combinations_with_soft_holes_and_clipped_masks() {
+    let canvas = Rect::new(0, 0, 64, 48);
+    for case in 0..6 {
+        for selection_mode in ["replace", "add", "subtract", "intersect"] {
+            for preset in ["sampledColors", "reds", "greens", "blues", "highlights", "midtones", "shadows", "skinTones"] {
+                for invert in [false, true] {
+                    let mut s = fixture(SampleType::U16, true);
+                    let mut selection = match case {
+                        0 => Surface::new(PixelFormat::GRAY8),
+                        1 => Surface::with_default(PixelFormat::GRAY8, &[128.0 / 255.0]),
+                        _ => sel::mask_to_surface(&vec![128.0 / 255.0; 30 * 30], Rect::new(-5, 5, 25, 35)),
+                    };
+                    if case == 3 {
+                        selection.fill_rect(Rect::new(8, 10, 15, 20), &[0.0]);
+                    }
+                    if case == 5 {
+                        selection = Surface::new(PixelFormat::GRAY8);
+                    }
+                    if case == 4 {
+                        selection = sel::mask_to_surface(&[1.0; 4], Rect::new(-8, -8, -6, -6));
+                    }
+                    let original = (case != 0).then_some(selection);
+                    let mut prior = s.active().unwrap().doc.as_ref().clone();
+                    prior.selection = original;
+                    let before = sel::mask_from_surface(prior.selection.as_ref(), canvas);
+                    let mut unselected = prior.clone();
+                    unselected.selection = None;
+                    let mut full = Session::new();
+                    full.add_document(unselected, None);
+                    let params = json!({"select": preset, "points": [[40, 15]], "fuzziness": 40, "invert": invert});
+                    full.execute("select.colorRange", params.clone()).unwrap();
+                    let mut mask = sel::mask_from_surface(full.active().unwrap().doc.selection.as_ref(), canvas);
+                    let m = SelectionMode::parse(selection_mode);
+                    scope_color_range(&mut mask, &prior, canvas, m);
+                    let expected = sel::combine(prior.selection.as_ref(), &mask, canvas, m);
+                    s.edit("prior selection", |doc, _| {
+                        doc.selection = prior.selection.clone();
+                        Ok(())
+                    })
+                    .unwrap();
+                    let mut params = params;
+                    params["mode"] = json!(selection_mode);
+                    s.execute("select.colorRange", params).unwrap();
+                    let actual = sel::mask_from_surface(s.active().unwrap().doc.selection.as_ref(), canvas);
+                    assert_eq!(actual, sel::mask_from_surface(expected.as_ref(), canvas), "case {case}, {selection_mode}, {preset}, invert {invert}");
+                    s.execute("edit.undo", json!({})).unwrap();
+                    assert_eq!(sel::mask_from_surface(s.active().unwrap().doc.selection.as_ref(), canvas), before);
+                    s.execute("edit.redo", json!({})).unwrap();
+                    assert_eq!(sel::mask_from_surface(s.active().unwrap().doc.selection.as_ref(), canvas), actual);
+                }
+            }
+        }
+    }
+}
+
+#[test]
+fn cropped_samples_match_full_composite_with_translucent_layers_and_active_layer_sampling() {
+    use photocraft_doc::{Layer, LayerContent};
+    for all_layers in [false, true] {
+        let mut s = fixture(SampleType::F32, true);
+        s.edit("overlay", |doc, _| {
+            let mut pixels = Surface::new(PixelFormat::RGBA32F);
+            pixels.fill_rect(Rect::new(0, 0, 64, 48), &[0.3, 0.1, 0.6, 0.35]);
+            doc.layers.push(Layer::new("overlay", LayerContent::Raster(pixels)));
+            Ok(())
+        })
+        .unwrap();
+        s.execute("select.rect", json!({"x": 5, "y": 6, "width": 20, "height": 30})).unwrap();
+        let params = json!({"points": [[40, 15], [10, 15]], "subtractPoints": [[40, 35]], "sampleAllLayers": all_layers, "localized": true, "range": 70});
+        let expected = full_sampled_reference(&s, &params);
+        s.execute("select.colorRange", params).unwrap();
+        assert_eq!(sel::mask_from_surface(s.active().unwrap().doc.selection.as_ref(), s.active().unwrap().doc.bounds()), expected);
+    }
+}
diff --git a/crates/engine/src/cmd/selection_cmds/tests.rs b/crates/engine/src/cmd/selection_cmds/tests.rs
--- a/crates/engine/src/cmd/selection_cmds/tests.rs
+++ b/crates/engine/src/cmd/selection_cmds/tests.rs
@@ -60,7 +60,7 @@ fn selection_sampling_refuses_oversized_canvases_before_reading_pixels() {
         None,
     );
     for all_layers in [false, true] {
-        assert!(sample_pixels(&s, all_layers).unwrap_err().to_string().contains("too large to sample"));
+        assert!(sample_pixels(&s, all_layers, None).unwrap_err().to_string().contains("too large to sample"));
     }
     assert!(!s.active().unwrap().history.can_undo());
 }
@@ -275,7 +275,7 @@ fn color_range_prepared_query_samples_visible_layers_not_the_active_layer() {
         clear_selection(&mut s);
         let p = json!({"points": [[7, 7]], "sampleAllLayers": all_layers, "fuzziness": 0});
         let query = ColorRangeSamples::new(&s, &p).unwrap();
-        let (area, px) = sample_pixels(&s, all_layers).unwrap();
+        let (area, px) = sample_pixels(&s, all_layers, None).unwrap();
         let shown = query.coverage(&px, area.width() as usize, 1.0, [0.0, 0.0]);
         s.execute("select.colorRange", p).unwrap();
         assert_eq!(shown[7 * 40 + 7], coverage(&s, 7, 7));
diff --git a/docs/color-range-performance.md b/docs/color-range-performance.md
new file mode 100644
--- /dev/null
+++ b/docs/color-range-performance.md
@@ -0,0 +1,42 @@
+# Color Range selection sampling
+
+`select.colorRange` in Replace mode refines an existing selection. Its sampled-colour,
+hue, skin-tone and tonal queries render/read only `selection.content_bounds()` clipped
+to the canvas. RGBA buffers, Lab conversion, coverage, selection clipping and replacement
+use that same rectangle. Invert happens before clipping to the old coverage, including
+soft edges and holes.
+
+Eyedropper points and subtract points still sample anywhere on the original canvas.
+Localized Color Clusters retain the full canvas's range and sample positions; the
+coverage query receives the sampled rectangle's origin. Without a selection, or with
+Add, Subtract or Intersect, sampling covers the full canvas. Nonzero implicit selection
+defaults and malformed tile coordinates also use the full canvas. An explicitly empty
+selection produces an empty result while still validating query parameters.
+
+The Out of Gamut preset uses the existing proof gamut-mask path and is not cropped.
+Interactive preview resolution and scheduling are unchanged.
+
+On a 6000×4000 canvas, the RGBA f32 buffer normally contains 24,000,000 pixels
+(384,000,000 bytes). A 50×50 selection uses 2,500 pixels (40,000 bytes); a 600×400
+selection uses 240,000 pixels (3,840,000 bytes). Lab input/output and mask buffers
+shrink by the same area ratio. These are buffer sizes, not measured peak RSS.
+
+## Reproduce the comparison
+
+On a CI runner, run:
+
+```sh
+RAYON_NUM_THREADS=4 cargo test --release -p photocraft-engine --lib color_range_release_comparison -- --ignored --nocapture
+```
+
+Or dispatch `ci.yml` with `color_range=true`. It runs the Linux release comparison,
+full normal CI, and the general quick performance suite on macOS. The release fixture
+is 24 MP with two colour regions and localized eyedropper sampling. It compares the
+full-canvas reference with the cropped command for 50×50, 600×400, 1200×800 and
+full-canvas soft selections. Each has a warm-up pair and three alternating timed
+pairs; reported timings are medians. Both include the undoable selection edit, and the
+cropped path includes command dispatch. Every stored coverage sample is compared over
+the whole canvas outside timing. No GUI frame latency is measured.
+
+This optimization does not change an existing scorecard scenario or baseline. The
+request's P5 label is unrelated to scorecard P5 (layer visibility).
