git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitc0872607067ecd5b1ae825c5474e9aa7385d4cb0
parent19c9a48bd6
authorLucas Galante <[email protected]>
date2026-07-19 00:47
fix: paint bevel shading as light/shadow overlays with a curvature term

Three defects traced to one root cause — bevels resolved final surface colors
instead of modulating what lies beneath:

  - a step band crossing the plate's rim stamped a flat strip over the rim
    gradient, leaving hard rectangular seams at both ends of every bar;
  - rim and step bands re-painted the surface color at the plate's own alpha
    on top of the fill, so a translucent plate was more opaque at its edges
    than its middle;
  - Recess had to be told the color it was carving.

Shading is now emitted as translucent overlays and composes over anything:
fills, gradients, blur. Two passes with fixed RGB — white with alpha
max(v,0)·gain, black with alpha max(-v,0)·gain — never one signed color: a
band whose value crosses zero inside a primitive would interpolate
white→black through mid-gray at real alpha, a brightening artifact on a dark
base exactly where shading should vanish (observed as a full-width bright
line at the foot of the statusbar wall). The gains are asymmetric the
opposite way from intuition: on dark bases white-over moves a pixel far more
per unit alpha than black-over, so light damps (0.7) and dark runs hot (3.0).

The shading itself gains a light-independent curvature term, the second depth
cue: convex catches ambient (bright), concave is self-occluded (dark). Rims
get a crest line hugging the silhouette — the line that makes glass read as
glass, kept below the directional amplitude so the shadow side keeps its
shadow with a faint bright edge over it. Steps get an antisymmetric
shoulder/fillet pair, zero at both plateaus. Because curvature does not
rotate with the light, no edge vanishes where the directional term dies.

Prim::Bevel and push_widget_vertices' bevel branch now fill at full size (the
lip no longer paints their outer ring), which makes Bevel's geometry identical
to Plate's. Prim::Recess loses its `surface` field; MenuBar and StatusBar drop
the page_low/backplate-opacity plumbing that fed it.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

 src/backend/window_runner.rs     | 229 ++++++++++++++++++++++++++++-----------
 src/scene/paint.rs               |  22 ++--
 src/widget/container/menu.rs     |  16 +--
 src/widget/display/status_bar.rs |  12 +-
 src/widget/model.rs              |   2 +-
 5 files changed, 186 insertions(+), 95 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 41bf4a5..f6f7cd0 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -932,6 +932,83 @@ fn bevel_profile(kind: EdgeKind, d: f32, t: f32) -> f32 {
     }
 }
 
+/// The light-independent curvature term at signed distance `d` — the second depth cue,
+/// on top of the directional one. Curvature shading is what ambient light does: convex
+/// surface catches it from everywhere (bright), concave is self-occluded (dark). Because
+/// it does not rotate with the light, it survives exactly where the directional term
+/// dies — walls parallel to the light vector — so no edge ever vanishes entirely.
+///
+/// `high_sign` is +1 when the rect interior is the HIGH side of the transition and -1
+/// when it is the low side (a recess). Geometry, not lighting: it does not flip with
+/// `light_sign`... except that for these 2.5D shapes the two are the same number, since
+/// a raised shape is lit like a plateau and shaded like one.
+#[inline]
+fn bevel_curvature(kind: EdgeKind, d: f32, t: f32, high_sign: f32) -> f32 {
+    if t <= 0.0 {
+        return 0.0;
+    }
+    match kind {
+        // A rim is convex everywhere, tightest right at the silhouette: a bright crest
+        // line hugging the boundary and dying fast inward. This is the line that makes
+        // glass read as glass — the edge catches ambient light all the way around, even
+        // (dimmer, via the gain asymmetry below) on the side facing away from the light.
+        EdgeKind::Rim => {
+            let u = (d / t).clamp(0.0, 1.0);
+            let f = 1.0 - u;
+            CREST_RATIO * f * f * f
+        }
+        // An S-curve step is convex on its high half (the shoulder) and concave on its
+        // low half (the fillet, where the wall meets the floor): antisymmetric, zero at
+        // the ends (no seam against either plateau) and at the midpoint.
+        EdgeKind::Step => {
+            let s = (d / t + 0.5).clamp(0.0, 1.0);
+            let outer_is_high = -high_sign; // d < 0 is outside the rect
+            // sin(2πs) is positive on the outer half — the shoulder when the outside is
+            // the high side — and negative on the inner (fillet) half.
+            AO_RATIO * outer_is_high * (s * std::f32::consts::TAU).sin()
+        }
+    }
+}
+
+/// Crest amplitude as a fraction of `bevel_depth` — how much brighter a rim's silhouette
+/// line is than flat surface under even light. Must stay clearly below ~0.7 (the
+/// projection of a 135° light onto an axis edge), or it cancels the directional shadow
+/// on the dark side and the rim goes flat there instead of showing a faint bright line
+/// over a shadowed roll.
+const CREST_RATIO: f32 = 0.4;
+/// Shoulder/fillet amplitude as a fraction of `bevel_depth`.
+const AO_RATIO: f32 = 0.6;
+/// Per-sign overlay gains. These are asymmetric the opposite way from intuition: on the
+/// dark bases this DE runs, white-over blending (`b + a(1-b)`) moves the pixel far more
+/// per unit alpha than black-over (`b(1-a)`) — a dark surface has little brightness for
+/// black to take away. The old subtractive shading effectively crushed shadow sides to
+/// black in linear space; the black overlay needs a high gain to keep shadows reading
+/// at all, while white needs damping to keep highlights from blowing out.
+const LIGHT_GAIN: f32 = 0.7;
+const DARK_GAIN: f32 = 3.0;
+
+/// A shading value (already scaled by `bevel_depth`) as the two overlay passes: the lit
+/// pass is translucent white, the shadow pass translucent black. Painting the
+/// *modulation* instead of a resolved surface color is what lets bevels compose — a step
+/// crossing a rim shades the rim's gradient instead of stamping a flat band over it, a
+/// lip on a translucent plate no longer doubles its opacity, and a recess needs no
+/// knowledge of the surface color it carves.
+///
+/// Why two passes with fixed RGB rather than one signed color: a primitive whose value
+/// crosses zero inside a band would interpolate white→black through mid-gray at
+/// non-negligible alpha — on a dark base a *brightening* artifact right where the
+/// shading should vanish. With per-pass alphas clamped at the crossing, each pass fades
+/// to zero there and the hue can never be wrong. Alphas also stay non-negative on every
+/// vertex, which the renderer requires (negative alpha is the blur sentinel).
+#[inline]
+fn overlay_light(v: f32) -> [f32; 4] {
+    [1.0, 1.0, 1.0, (v.max(0.0) * LIGHT_GAIN).min(1.0)]
+}
+#[inline]
+fn overlay_dark(v: f32) -> [f32; 4] {
+    [0.0, 0.0, 0.0, ((-v).max(0.0) * DARK_GAIN).min(1.0)]
+}
+
 /// The signed distance range an edge's shading occupies, relative to the boundary.
 #[inline]
 fn bevel_span(kind: EdgeKind, t: f32) -> (f32, f32) {
@@ -942,11 +1019,12 @@ fn bevel_span(kind: EdgeKind, t: f32) -> (f32, f32) {
 }
 
 /// How many gradient bands to slice a lip of thickness `t` into. Vertex colors interpolate
-/// linearly, so each band is a chord of [`bevel_profile`]'s curve; one band per ~1.5px
-/// keeps the error under a shade step without emitting geometry finer than the display
-/// resolves. A 2px lip stays a single ramp; a 12px rolled edge gets eight.
+/// linearly, so each band is a chord of the shading curve; one band per ~1.25px keeps the
+/// error under a shade step without emitting geometry finer than the display resolves.
+/// The cap rose with the curvature term: a step now has two features across its width
+/// (shoulder and fillet), so it needs double the samples a single bump did.
 fn default_bevel_bands(t: f32) -> usize {
-    ((t / 1.5).ceil() as usize).clamp(1, 8)
+    ((t / 1.25).ceil() as usize).clamp(1, 12)
 }
 
 /// As [`push_bevel_edge_vertices_radii`], with the band count forced and the walls
@@ -989,22 +1067,25 @@ pub fn push_bevel_edge_vertices_banded(
     let ly = -rad.sin() * light_sign;
     let depth = crate::layout::bevel_depth();
 
-    // Only RGB moves; alpha is held at the base value on every vertex. The renderer reads
-    // a negative alpha as the blur-behind sentinel (shader2d.wgsl), so interpolating alpha
-    // could cross zero and flip part of a triangle into blur mode.
-    let shade = |factor: f32| -> [f32; 4] {
-        let offset = factor.clamp(-1.0, 1.0) * depth;
+    // `base_color` is no longer painted: shading is an overlay (see `overlay_color`), so
+    // the surface below shows through with its own gradients and translucency intact.
+    let _ = base_color;
+    // Shading (directional + curvature, scaled by bevel_depth) at signed distance `d`,
+    // for an edge whose outward flat normal is `dir`. A `Step` band runs negative — it
+    // straddles the boundary into the plateau outside the rect, which is exactly what
+    // removes the seam.
+    let value = |dot: f32, d: f32| {
+        depth * (bevel_profile(kind, d, t) * dot + bevel_curvature(kind, d, t, light_sign))
+    };
+    // The (up to two) overlay color pairs for a band running from value `v0` to `v1`:
+    // one white pair and/or one black pair, each pass fading to zero alpha wherever the
+    // value has the other sign. Both fire only when the band straddles the terminator.
+    let passes = |v0: f32, v1: f32| -> [Option<([f32; 4], [f32; 4])>; 2] {
         [
-            (base_color[0] + offset).clamp(0.0, 1.0),
-            (base_color[1] + offset).clamp(0.0, 1.0),
-            (base_color[2] + offset).clamp(0.0, 1.0),
-            base_color[3],
+            (v0 > 0.0 || v1 > 0.0).then(|| (overlay_light(v0), overlay_light(v1))),
+            (v0 < 0.0 || v1 < 0.0).then(|| (overlay_dark(v0), overlay_dark(v1))),
         ]
     };
-    // Color at signed distance `d` from the boundary, for an edge whose outward normal is
-    // `dir`. A `Step` band runs negative — it straddles the boundary into the plateau
-    // outside the rect, which is exactly what removes the seam.
-    let band = |dir: (f32, f32), d: f32| shade(bevel_profile(kind, d, t) * (dir.0 * lx + dir.1 * ly));
     let (span_lo, span_hi) = bevel_span(kind, t);
 
     // Each flat edge spans between its two adjoining corner radii, not a single uniform
@@ -1026,31 +1107,39 @@ pub fn push_bevel_edge_vertices_banded(
 
         // Top: outward normal (0,-1); the gradient runs downward, into the surface.
         if top_w > 0.0 && edges.0 {
-            let (c0, c1) = (band((0.0, -1.0), d0), band((0.0, -1.0), d1));
-            out.extend_from_slice(&quad_vertices_shaded(
-                x + tl, y + d0, top_w, bw, sw, sh, c0, c0, c1, c1, clip_circle,
-            ));
+            let (v0, v1) = (value(-ly, d0), value(-ly, d1));
+            for (c0, c1) in passes(v0, v1).into_iter().flatten() {
+                out.extend_from_slice(&quad_vertices_shaded(
+                    x + tl, y + d0, top_w, bw, sw, sh, c0, c0, c1, c1, clip_circle,
+                ));
+            }
         }
         // Bottom: outward normal (0,1); gradient runs upward.
         if bottom_w > 0.0 && edges.2 {
-            let (c0, c1) = (band((0.0, 1.0), d0), band((0.0, 1.0), d1));
-            out.extend_from_slice(&quad_vertices_shaded(
-                x + bl, y + h - d1, bottom_w, bw, sw, sh, c1, c1, c0, c0, clip_circle,
-            ));
+            let (v0, v1) = (value(ly, d0), value(ly, d1));
+            for (c0, c1) in passes(v0, v1).into_iter().flatten() {
+                out.extend_from_slice(&quad_vertices_shaded(
+                    x + bl, y + h - d1, bottom_w, bw, sw, sh, c1, c1, c0, c0, clip_circle,
+                ));
+            }
         }
         // Left: outward normal (-1,0); gradient runs rightward.
         if left_h > 0.0 && edges.3 {
-            let (c0, c1) = (band((-1.0, 0.0), d0), band((-1.0, 0.0), d1));
-            out.extend_from_slice(&quad_vertices_shaded(
-                x + d0, y + left_top, bw, left_h, sw, sh, c0, c1, c1, c0, clip_circle,
-            ));
+            let (v0, v1) = (value(-lx, d0), value(-lx, d1));
+            for (c0, c1) in passes(v0, v1).into_iter().flatten() {
+                out.extend_from_slice(&quad_vertices_shaded(
+                    x + d0, y + left_top, bw, left_h, sw, sh, c0, c1, c1, c0, clip_circle,
+                ));
+            }
         }
         // Right: outward normal (1,0); gradient runs leftward.
         if right_h > 0.0 && edges.1 {
-            let (c0, c1) = (band((1.0, 0.0), d0), band((1.0, 0.0), d1));
-            out.extend_from_slice(&quad_vertices_shaded(
-                x + ww - d1, y + right_top, bw, right_h, sw, sh, c1, c0, c0, c1, clip_circle,
-            ));
+            let (v0, v1) = (value(lx, d0), value(lx, d1));
+            for (c0, c1) in passes(v0, v1).into_iter().flatten() {
+                out.extend_from_slice(&quad_vertices_shaded(
+                    x + ww - d1, y + right_top, bw, right_h, sw, sh, c1, c0, c0, c1, clip_circle,
+                ));
+            }
         }
     }
 
@@ -1079,8 +1168,6 @@ pub fn push_bevel_edge_vertices_banded(
             let theta1 = start_angle + ((j + 1) as f32) * (end_angle - start_angle) / (segments as f32);
             let (cos0, sin0) = (theta0.cos(), theta0.sin());
             let (cos1, sin1) = (theta1.cos(), theta1.sin());
-            let f0 = cos0 * lx + sin0 * ly;
-            let f1 = cos1 * lx + sin1 * ly;
             for k in 0..bands {
                 let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
                 let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
@@ -1093,13 +1180,33 @@ pub fn push_bevel_edge_vertices_banded(
                         1.0 - ((cy + rho * s) / sh) * 2.0,
                     ]
                 };
-                // Outer/inner × the two sweep ends; each vertex gets its own shade.
-                let (p0, p1) = (bevel_profile(kind, d0, t), bevel_profile(kind, d1, t));
-                let v00 = Vertex { position: p(r0, cos0, sin0), color: shade(p0 * f0), clip_circle };
-                let v10 = Vertex { position: p(r0, cos1, sin1), color: shade(p0 * f1), clip_circle };
-                let v11 = Vertex { position: p(r1, cos1, sin1), color: shade(p1 * f1), clip_circle };
-                let v01 = Vertex { position: p(r1, cos0, sin0), color: shade(p1 * f0), clip_circle };
-                out.extend_from_slice(&[v00, v10, v11, v00, v11, v01]);
+                // Outer/inner × the two sweep ends; each vertex gets its own value, and
+                // the cell is drawn once per overlay pass that has any coverage.
+                let vals = [
+                    value(cos0 * lx + sin0 * ly, d0),
+                    value(cos1 * lx + sin1 * ly, d0),
+                    value(cos1 * lx + sin1 * ly, d1),
+                    value(cos0 * lx + sin0 * ly, d1),
+                ];
+                let geo = [
+                    p(r0, cos0, sin0),
+                    p(r0, cos1, sin1),
+                    p(r1, cos1, sin1),
+                    p(r1, cos0, sin0),
+                ];
+                let mut cells: [Option<fn(f32) -> [f32; 4]>; 2] = [None, None];
+                if vals.iter().any(|&v| v > 0.0) {
+                    cells[0] = Some(overlay_light);
+                }
+                if vals.iter().any(|&v| v < 0.0) {
+                    cells[1] = Some(overlay_dark);
+                }
+                for f in cells.into_iter().flatten() {
+                    let c: Vec<Vertex> = (0..4)
+                        .map(|i| Vertex { position: geo[i], color: f(vals[i]), clip_circle })
+                        .collect();
+                    out.extend_from_slice(&[c[0], c[1], c[2], c[0], c[2], c[3]]);
+                }
             }
         }
     }
@@ -1210,13 +1317,9 @@ pub fn push_widget_vertices(w: &dyn crate::widget::WidgetHost, sw: f32, sh: f32,
     let radii = w.corner_radii();
     if let Some(thickness) = w.plate_bevel() {
         let t = thickness;
-        let inner_radii = crate::widget::CornerRadii {
-            top_left: (radii.top_left - t).max(0.0),
-            top_right: (radii.top_right - t).max(0.0),
-            bottom_right: (radii.bottom_right - t).max(0.0),
-            bottom_left: (radii.bottom_left - t).max(0.0),
-        };
-        push_rounded_rect_vertices_corners(x + t, y + t, ww - 2.0 * t, h - 2.0 * t, inner_radii, sw, sh, w.color(), clip_circle, None, out);
+        // Full-size fill: the bevel lip is a shading overlay now, not a paint of the
+        // outer ring, so an inset fill would leave the ring unfilled.
+        push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
         push_plate_bevel_vertices(x, y, ww, h, radii.top_left, t, sw, sh, w.color(), clip_circle, out);
     } else {
         push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
@@ -1309,16 +1412,15 @@ pub fn tessellate_display_list(
                 push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
             }
             Prim::Bevel { rect, radii, color, depth } => {
-                // Mirror push_widget_vertices' bevel branch: inset rounded fill + bevel edges.
-                let t = *depth;
-                let inner = crate::widget::CornerRadii::new(
-                    (radii.0 - t).max(0.0),
-                    (radii.1 - t).max(0.0),
-                    (radii.2 - t).max(0.0),
-                    (radii.3 - t).max(0.0),
-                );
-                push_rounded_rect_vertices_corners(rect.x + t, rect.y + t, rect.width - 2.0 * t, rect.height - 2.0 * t, inner, sw, sh, *color, no, None, &mut verts);
-                push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, t, sw, sh, *color, no, &mut verts);
+                // Full-size fill: the lip is now a shading overlay, not a paint of the
+                // outer ring, so the fill must cover the whole rect (the old inset fill
+                // would leave the ring showing whatever lay beneath).
+                let corners = crate::widget::CornerRadii {
+                    top_left: radii.0, top_right: radii.1,
+                    bottom_right: radii.2, bottom_left: radii.3,
+                };
+                push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, *color, no, None, &mut verts);
+                push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, *color, no, &mut verts);
             }
             Prim::Plate { rect, radii, color, depth } => {
                 // Fill at full size (no inset — see Prim::Plate), then roll the perimeter.
@@ -1337,13 +1439,14 @@ pub fn tessellate_display_list(
                     sw, sh, *color, no, 1.0, &mut verts,
                 );
             }
-            Prim::Recess { rect, radii, surface, depth, edges } => {
-                // Edges only — no fill, so the surface already painted below shows through
-                // the middle of the carve. `light_sign = -1.0` shadows the lit-facing edges,
+            Prim::Recess { rect, radii, depth, edges } => {
+                // Edges only — no fill: the shading is an overlay, so whatever is painted
+                // below (fill, rim gradient, blur) shows through the carve modulated
+                // rather than repainted. `light_sign = -1.0` shadows the lit-facing edges,
                 // which is the raised->recessed inversion.
                 push_bevel_edge_vertices_banded(
                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
-                    sw, sh, *surface, no, -1.0, default_bevel_bands(*depth), *edges,
+                    sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), *edges,
                     EdgeKind::Step, &mut verts,
                 );
             }
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 76d631e..1eee4dd 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -40,8 +40,8 @@ pub enum Prim {
     /// A rounded fill plus a solid border stroke — a widget's own "plate" (mirrors
     /// `push_widget_vertices`' non-bevel branch: rounded bg + `push_plate_solid_border_vertices`).
     Border { rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32 },
-    /// A beveled plate: an inset rounded fill plus lightened/darkened edges (mirrors
-    /// `push_widget_vertices`' bevel branch).
+    /// A beveled plate: a rounded fill at full size plus a light/shadow overlay lip
+    /// (mirrors `push_widget_vertices`' bevel branch).
     Bevel { rect: Rect, radii: Radii, color: [f32; 4], depth: f32 },
     /// A recess carved into whatever is already painted underneath — the inverse of
     /// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
@@ -50,13 +50,13 @@ pub enum Prim {
     /// `light_source_position` fall into shadow and the far edges catch the light —
     /// which is what reads as "lower" instead of "raised".
     ///
-    /// `surface` is the color of what lies beneath (the thing being carved); it is only
-    /// the base for the edge shading, and is never filled. Its alpha carries through to
-    /// the edges, so a recess in a translucent plate stays translucent.
+    /// The shading is a translucent light/shadow overlay, so the carve needs no knowledge
+    /// of what it carves: fills, gradients, and translucency below all show through
+    /// modulated rather than repainted.
     /// `edges` is (top, right, bottom, left): which walls of the carve actually exist.
     /// A region flush with the plate's own edge is a step, not a trough — see
     /// `push_bevel_edge_vertices_banded`.
-    Recess { rect: Rect, radii: Radii, surface: [f32; 4], depth: f32, edges: (bool, bool, bool, bool) },
+    Recess { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
     /// The window's glass slab: a rounded fill plus a rolled, lit edge around its whole
     /// perimeter, drawn at full size. Distinct from `Bevel`, which insets its fill by
     /// `depth` — a plate must fill the window exactly, or the compositor's rounded window
@@ -347,18 +347,18 @@ impl PaintCtx {
     }
 
     /// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
-    /// nothing — `surface` is the color being carved, used only to shade the edges.
-    pub fn recess(&mut self, rect: Rect, radii: Radii, surface: [f32; 4], depth: f32) {
-        self.recess_edges(rect, radii, surface, depth, (true, true, true, true));
+    /// nothing — the shading is an overlay, so it composes over whatever was painted.
+    pub fn recess(&mut self, rect: Rect, radii: Radii, depth: f32) {
+        self.recess_edges(rect, radii, depth, (true, true, true, true));
     }
 
     /// [`PaintCtx::recess`] with only some of the walls — see `Prim::Recess`.
     pub fn recess_edges(
-        &mut self, rect: Rect, radii: Radii, surface: [f32; 4], depth: f32,
+        &mut self, rect: Rect, radii: Radii, depth: f32,
         edges: (bool, bool, bool, bool),
     ) {
         let rect = self.apply_offset(rect);
-        self.push(Prim::Recess { rect, radii, surface, depth, edges });
+        self.push(Prim::Recess { rect, radii, depth, edges });
     }
 
     /// The window's glass slab: rounded fill at full size plus a rolled, lit perimeter.
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index b2ce4fc..0eb3830 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -465,15 +465,9 @@ impl Paint for MenuBar {
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         if self.recessed {
-            // No background of our own: carve the backplate instead, so the plate below is
-            // what shows through the bar. The shading base has to be the plate's own color
-            // (matching how the window emits it: page_low tinted by the active-backplate
-            // opacity), because the recess edges are that surface catching/losing light —
-            // shading a transparent color would just produce transparent edges.
-            let mut surface = colors::page_low_color();
-            if surface[3] > 0.001 {
-                surface[3] = colors::active_backplate_opacity();
-            }
+            // No background of our own: carve the backplate instead. The recess shading is
+            // a light/shadow overlay, so whatever the plate painted here (fill, rim
+            // gradient, blur) shows through modulated.
             // `depth` is the roll-off width in px (the shading amplitude is separate: the
             // renderer applies `bevel_depth` itself), capped so a deep DE-wide setting can
             // never swallow a short bar — the two walls would meet in the middle and the
@@ -484,11 +478,11 @@ impl Paint for MenuBar {
                 // trough, so its only wall is the one facing the content. The other three
                 // sides are the plate's outer edge, where the plate's own roll already lives
                 // — carving there too would cut a second lip into the same pixels.
-                ctx.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), surface, depth, (false, false, true, false));
+                ctx.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), depth, (false, false, true, false));
             } else {
                 // Inset from the plate edge: a real trough, walled all round, its corners
                 // rounded by the roll itself.
-                ctx.recess(rect, (depth, depth, depth, depth), surface, depth);
+                ctx.recess(rect, (depth, depth, depth, depth), depth);
             }
         } else {
             // Background: always the plain quad — the rounded-against-parent variant required a
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index 180a1f6..d6bbf28 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -138,17 +138,11 @@ impl Paint for StatusBar {
     /// body; deliberately no `widget_font`, see module docs).
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         if self.recessed {
-            // The shading base is the plate's own color (page_low at the active backplate
-            // opacity, matching how the window emits it) — these edges are that surface
-            // catching and losing light, so shading a transparent color would just produce
-            // transparent edges.
-            let mut surface = colors::page_low_color();
-            if surface[3] > 0.001 {
-                surface[3] = colors::active_backplate_opacity();
-            }
+            // The recess shading is a light/shadow overlay — whatever the plate painted
+            // here shows through modulated, so no surface color is needed.
             // Capped against the bar's own height so a deep DE-wide roll can't swallow it.
             let depth = crate::layout::bevel_width().min(rect.height * 0.4);
-            ctx.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), surface, depth, (true, false, false, false));
+            ctx.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), depth, (true, false, false, false));
         } else {
             // Always the plain background quad — the rounded-against-parent variant required a
             // backplate parent, which no longer exists.
diff --git a/src/widget/model.rs b/src/widget/model.rs
index fd894ca..e9bcba9 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1235,7 +1235,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Prim::RoundedRect { rect, radius, corners, color } => ctx.rounded_rect(rect, radius, corners, color),
                 Prim::Border { rect, radii, fill, border, thickness } => ctx.border(rect, radii, fill, border, thickness),
                 Prim::Bevel { rect, radii, color, depth } => ctx.bevel(rect, radii, color, depth),
-                Prim::Recess { rect, radii, surface, depth, edges } => ctx.recess_edges(rect, radii, surface, depth, edges),
+                Prim::Recess { rect, radii, depth, edges } => ctx.recess_edges(rect, radii, depth, edges),
                 Prim::Plate { rect, radii, color, depth } => ctx.plate(rect, radii, color, depth),
                 Prim::Arc { cx, cy, radius, thickness, start, end, color } => ctx.arc(cx, cy, radius, thickness, start, end, color),
                 Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => ctx.vector(x1, y1, x2, y2, thickness, color, cap),