diff --git a/crates/compose/src/effects.rs b/crates/compose/src/effects.rs
--- a/crates/compose/src/effects.rs
+++ b/crates/compose/src/effects.rs
@@ -1142,6 +1142,16 @@ fn paint(dst: &mut Buffer, m: &Map, color: impl Fn(usize) -> [f32; 4], blend: Bl
             }
             continue;
         }
+        if psblend::fill_is_special(blend) {
+            // Photoshop's "special eight" (Vivid Light, Color Dodge, ...): an effect's strength
+            // (coverage x Opacity), like a layer's Fill, pulls the colour toward the mode's
+            // neutral and blends it at full strength, instead of mixing the full-strength
+            // result in linearly. Linear mixing turned a Vivid Light white inner shadow on a
+            // near-black fill into mid grey, and a Vivid Light white satin on gold into white,
+            // where Photoshop keeps the fill dark / the gold saturated.
+            *p = psblend::composite_fill(blend, *p, [c[0], c[1], c[2], c[3]], 1.0, a.min(1.0), 1.0);
+            continue;
+        }
         *p = psblend::composite(blend, *p, [c[0], c[1], c[2], c[3] * a], 1.0);
     }
 }
diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs
--- a/crates/compose/src/tests.rs
+++ b/crates/compose/src/tests.rs
@@ -1248,6 +1248,43 @@ fn stroke(size: f32, position: StrokePosition) -> Effect {
     })
 }
 
+/// The middle pixel of a grey square carrying `effect`, in a white document of `depth`.
+fn effect_on_grey(depth: SampleType, grey: f32, effect: Effect) -> [f32; 4] {
+    let mut d = Document::with_background("t", Size::new(40, 40), ColorMode::Rgb, depth, Color::WHITE);
+    let fmt = d.pixel_format();
+    let mut l = Layer::raster("sq", fmt);
+    l.surface_mut().unwrap().fill_rect(Rect::new(10, 10, 30, 30), &photocraft_raster::from_rgba(&fmt, [grey, grey, grey, 1.0]));
+    l.effects.items = vec![effect];
+    d.layers.push(l);
+    px(&d, 20, 20)
+}
+
+#[test]
+fn special_eight_effect_strength_acts_like_fill() {
+    // Photoshop shows a white Vivid Light effect at 56 % on a dark grey (16/255) as
+    // 16 / (1 - 0.56) = 36.4 / 255: the strength pulls the colour toward the mode's neutral and
+    // the blend runs at full strength, as with a layer's Fill. Mixing the full-strength result
+    // in linearly gave about 150.
+    let grey = 16.0 / 255.0;
+    let want = grey / (1.0 - 0.56);
+    let overlay = |mode, opacity| Effect::ColorOverlay { common: FxCommon::new(mode, opacity), color: Color::WHITE };
+    for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
+        let got = effect_on_grey(depth, grey, overlay(BlendMode::VividLight, 0.56));
+        let tolerance = if depth == SampleType::U8 { 1.5 / 255.0 } else { 0.5 / 255.0 };
+        assert!((got[0] - want).abs() <= tolerance, "{depth:?}: {got:?} vs {want}");
+        assert!(close4(got, [got[0], got[0], got[0], 1.0]), "{depth:?}: stays neutral {got:?}");
+        // Full strength is the plain mode; no strength leaves the fill alone.
+        let full = effect_on_grey(depth, grey, overlay(BlendMode::VividLight, 1.0));
+        let plain = blend_px(depth, BlendMode::VividLight, [grey; 3], [1.0; 3]);
+        assert!(close4(full, plain), "{depth:?}: {full:?} vs {plain:?}");
+        let none = effect_on_grey(depth, grey, overlay(BlendMode::VividLight, 0.0));
+        assert!((none[0] - grey).abs() <= 0.5 / 255.0, "{depth:?}: {none:?}");
+        // Modes outside the special eight still mix linearly.
+        let soft = effect_on_grey(depth, grey, overlay(BlendMode::Normal, 0.56));
+        let linear = grey + (1.0 - grey) * 0.56;
+        assert!((soft[0] - linear).abs() <= 1.0 / 255.0, "{depth:?}: {soft:?} vs {linear}");
+    }
+}
 #[test]
 fn outside_stroke_width() {
     let d = fx_doc(vec![stroke(3.0, StrokePosition::Outside)]);
diff --git a/crates/gpu/src/compose.wgsl b/crates/gpu/src/compose.wgsl
--- a/crates/gpu/src/compose.wgsl
+++ b/crates/gpu/src/compose.wgsl
@@ -1171,6 +1171,15 @@ fn fx_composite(d: vec2<i32>, dst: vec4<f32>, s: vec4<f32>) -> vec4<f32> {
     return composite(op.mode, dst, s, 1.0);
 }
 
+// The same with the effect's strength `k` (coverage x opacity) kept apart from the colour's own
+// alpha `s.a`: Photoshop's special-eight modes treat it like a layer's Fill (effects::paint).
+fn fx_composite_k(d: vec2<i32>, dst: vec4<f32>, s: vec4<f32>, k: f32) -> vec4<f32> {
+    if (op.mode != M_DISSOLVE && fill_is_special(op.mode)) {
+        return composite_fill(op.mode, dst, s, 1.0, min(k, 1.0), false);
+    }
+    return fx_composite(d, dst, vec4(s.rgb, s.a * k));
+}
+
 // Effect chain steps (`kind`): 0 A at `opacity` × alpha; 1 an opaque copy of A (a clipping base
 // for clipped layers' effects); 2 A's colour with alpha `opacity` inside B's shape (the layer
 // before its interior effects); 3 A with its alpha × B's alpha (the shape's own alpha); 4 the
@@ -1239,12 +1248,12 @@ fn fs_fxpaint(in: VOut) -> @location(0) vec4<f32> {
         let k = min(m * op.p0.x, 1.0) * op.opacity;
         if (k <= 0.0) { return dst; }
         let t = 1.0 - clamp(m, 0.0, 1.0);
-        return fx_composite(d, dst, vec4(lut(0, t), lut(1, t), lut(2, t), lut(3, t) * k));
+        return fx_composite_k(d, dst, vec4(lut(0, t), lut(1, t), lut(2, t), lut(3, t)), k);
     }
     let k = m * op.opacity;
     if (k <= 0.0) { return dst; }
     let c = fx_color(d);
-    return fx_composite(d, dst, vec4(c.rgb, c.a * k));
+    return fx_composite_k(d, dst, c, k);
 }
 
 // Outside strokes (A = the exterior result before them, B = the layer, C = coverage so far in
diff --git a/crates/gpu/tests/it/parity.rs b/crates/gpu/tests/it/parity.rs
--- a/crates/gpu/tests/it/parity.rs
+++ b/crates/gpu/tests/it/parity.rs
@@ -1738,6 +1738,23 @@ fn effect_cases() -> Vec<(&'static str, Vec<Effect>)> {
                 Effect::Stroke(stroke(6.0, StrokePosition::Outside, FxPaint::Color(Color::rgb(0.0, 0.0, 0.0)))),
             ],
         ),
+        (
+            "special-eight effects act like Fill",
+            vec![
+                Effect::InnerShadow(shadow(BlendMode::VividLight, 0.56, 120.0, 4.0, 6.0, 0.0)),
+                Effect::Satin(Satin {
+                    common: FxCommon::new(BlendMode::VividLight, 1.0),
+                    color: Color::rgb(1.0, 1.0, 1.0),
+                    angle: 19.0,
+                    distance: 7.0,
+                    size: 8.0,
+                    contour: Contour::Linear,
+                    anti_alias: false,
+                    invert: false,
+                }),
+                Effect::ColorOverlay { common: FxCommon::new(BlendMode::LinearDodge, 0.35), color: Color::rgb(0.9, 0.5, 0.1) },
+            ],
+        ),
         (
             "multiple instances",
             vec![
