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

commit3877dd667994f9a5139ef78b8c8d8b081bc886d1
parentc857837993
authorLucas Galante <[email protected]>
date2026-07-20 12:49
feat: per-pixel SDF-lit plate system — CSG carves, squircle corners, control reliefs

Replaces the banded vertex-overlay bevels with a per-pixel lit plate branch
in shader2d (the legacy path stays behind the new bevel_shader config
toggle). A plate is one composite height field — the host's rolled edge
minus surface-relative carves — lit ONCE per pixel from summed analytic
slope vectors, so junctions (a band meeting the plate's perimeter roll) are
arithmetic instead of per-pair fade heuristics, and features never blend in
color space.

- shader2d: analytic rounded-box SDF+gradient (no dpdx), decoupled
  profile×azimuth specular (Blinn-Phong crushes edge lines vs corner
  glints and blobs the corner meeting), superellipse corners AND roll/step
  profiles from one corner_shape exponent (plan-corner span is
  curvature-matched for window Plates only), overlay modes for free
  recess/boss/ridge with host-roll fade
- carve grouping: Recess prims inside a preceding Plate/Bevel become CSG
  features of that plate's draw via a per-frame feature UBO (binding 3,
  double-buffered by frame in flight; offsets rebased at record time)
- new prims: Boss (raised plateau, edges only) and Ridge (raised rim in ONE
  profile evaluation — a boss+recess stack double-counts specular at the
  crest); both degrade to overlays when ungrouped
- widget silhouettes follow corner_shape: superellipse corner fans in the
  rounded-rect tessellator and border strokes (arcs stay circular)
- control relief styles (opt-in): Button/Toggle/Dropdown with_raised (lit
  Bevel plate replacing flat fill + border; transparent fills degrade to
  edges-only Boss), TextBox with_recessed (carved well), Slider with_raised
  (ridge around a channel, fill inset into the valley, circular knob sized
  to the channel floor)
- demo: lit window Plate, carved menubar/status bands (heights derived from
  the chrome fonts), all controls on the relief styles
- config knobs: bevel_shader (A/B), corner_shape (2 = circular, ~4.5 = Apple
  continuous curvature)

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/backend/window_runner.rs | 377 +++++++++++++++++++++++++++++++++++++------
 src/layout.rs                |  20 +++
 src/main.rs                  |  96 ++++++++---
 src/scene/paint.rs           |  54 +++++++
 src/vk/mod.rs                |   2 +-
 src/vk/renderer.rs           | 133 +++++++++++++--
 src/vk/shader2d.wgsl         | 317 +++++++++++++++++++++++++++++++++---
 src/widget/input/button.rs   |  22 +++
 src/widget/input/checkbox.rs |  22 ++-
 src/widget/input/dropdown.rs |  28 +++-
 src/widget/input/slider.rs   |  81 ++++++++--
 src/widget/input/text_box.rs |  39 ++++-
 src/widget/model.rs          |   2 +
 13 files changed, 1067 insertions(+), 126 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 8e6bbb8..a2e9bd1 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -562,6 +562,19 @@ pub fn rounded_rect_vertices_corners(
     verts
 }
 
+/// Sample of the unit superellipse |x|^n + |y|^n = 1 at circle parameter θ —
+/// the (cos θ, sin θ) replacement the corner fans use. Exactly the circle at
+/// n = 2; higher `corner_shape` exponents give the DE's continuous-curvature
+/// corners, so widget silhouettes follow the same corner family as the
+/// SDF-lit plates. `e` is 2/n, hoisted by callers. Tangent points at the
+/// quadrant ends are unchanged, so fans still tile exactly against the body
+/// rects and edge strips.
+#[inline]
+fn superellipse_pt(theta: f32, e: f32) -> (f32, f32) {
+    let (s, c) = theta.sin_cos();
+    (c.signum() * c.abs().powf(e), s.signum() * s.abs().powf(e))
+}
+
 pub fn push_rounded_rect_vertices_corners(
     x: f32, y: f32, ww: f32, h: f32,
     radii: crate::widget::CornerRadii,
@@ -571,6 +584,7 @@ pub fn push_rounded_rect_vertices_corners(
     clip_rect: Option<(f32, f32, f32, f32)>,
     out: &mut Vec<Vertex>,
 ) {
+    let corner_e = 2.0 / crate::layout::corner_shape();
     let mut r_tl = radii.top_left.max(0.0);
     let mut r_tr = radii.top_right.max(0.0);
     let mut r_br = radii.bottom_right.max(0.0);
@@ -672,12 +686,14 @@ pub fn push_rounded_rect_vertices_corners(
             let theta1 = start + (i as f32) * (end - start) / (segments as f32);
             let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
             
+            let (c1, s1) = superellipse_pt(theta1, corner_e);
+            let (c2, s2) = superellipse_pt(theta2, corner_e);
             let x0 = clamp_x(cx);
             let y0 = clamp_y(cy);
-            let x1 = clamp_x(cx + r_tl * theta1.cos());
-            let y1 = clamp_y(cy + r_tl * theta1.sin());
-            let x2 = clamp_x(cx + r_tl * theta2.cos());
-            let y2 = clamp_y(cy + r_tl * theta2.sin());
+            let x1 = clamp_x(cx + r_tl * c1);
+            let y1 = clamp_y(cy + r_tl * s1);
+            let x2 = clamp_x(cx + r_tl * c2);
+            let y2 = clamp_y(cy + r_tl * s2);
             
             let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
             let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
@@ -707,12 +723,14 @@ pub fn push_rounded_rect_vertices_corners(
             let theta1 = start + (i as f32) * (end - start) / (segments as f32);
             let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
             
+            let (c1, s1) = superellipse_pt(theta1, corner_e);
+            let (c2, s2) = superellipse_pt(theta2, corner_e);
             let x0 = clamp_x(cx);
             let y0 = clamp_y(cy);
-            let x1 = clamp_x(cx + r_tr * theta1.cos());
-            let y1 = clamp_y(cy + r_tr * theta1.sin());
-            let x2 = clamp_x(cx + r_tr * theta2.cos());
-            let y2 = clamp_y(cy + r_tr * theta2.sin());
+            let x1 = clamp_x(cx + r_tr * c1);
+            let y1 = clamp_y(cy + r_tr * s1);
+            let x2 = clamp_x(cx + r_tr * c2);
+            let y2 = clamp_y(cy + r_tr * s2);
             
             let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
             let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
@@ -742,12 +760,14 @@ pub fn push_rounded_rect_vertices_corners(
             let theta1 = start + (i as f32) * (end - start) / (segments as f32);
             let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
             
+            let (c1, s1) = superellipse_pt(theta1, corner_e);
+            let (c2, s2) = superellipse_pt(theta2, corner_e);
             let x0 = clamp_x(cx);
             let y0 = clamp_y(cy);
-            let x1 = clamp_x(cx + r_br * theta1.cos());
-            let y1 = clamp_y(cy + r_br * theta1.sin());
-            let x2 = clamp_x(cx + r_br * theta2.cos());
-            let y2 = clamp_y(cy + r_br * theta2.sin());
+            let x1 = clamp_x(cx + r_br * c1);
+            let y1 = clamp_y(cy + r_br * s1);
+            let x2 = clamp_x(cx + r_br * c2);
+            let y2 = clamp_y(cy + r_br * s2);
             
             let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
             let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
@@ -777,12 +797,14 @@ pub fn push_rounded_rect_vertices_corners(
             let theta1 = start + (i as f32) * (end - start) / (segments as f32);
             let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
             
+            let (c1, s1) = superellipse_pt(theta1, corner_e);
+            let (c2, s2) = superellipse_pt(theta2, corner_e);
             let x0 = clamp_x(cx);
             let y0 = clamp_y(cy);
-            let x1 = clamp_x(cx + r_bl * theta1.cos());
-            let y1 = clamp_y(cy + r_bl * theta1.sin());
-            let x2 = clamp_x(cx + r_bl * theta2.cos());
-            let y2 = clamp_y(cy + r_bl * theta2.sin());
+            let x1 = clamp_x(cx + r_bl * c1);
+            let y1 = clamp_y(cy + r_bl * s1);
+            let x2 = clamp_x(cx + r_bl * c2);
+            let y2 = clamp_y(cy + r_bl * s2);
             
             let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
             let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
@@ -1306,38 +1328,45 @@ pub fn push_plate_solid_border_vertices(
     out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r_tr, t, h - r_tr - r_br, sw, sh, color, clip_circle));
 
     let segments = 16;
+    let corner_e = 2.0 / crate::layout::corner_shape();
+
+    // Corner strokes as annulus strips between the outer superellipse (radius
+    // r) and its inner scaled copy (r - t): at 1px thickness the scaled inner
+    // curve is indistinguishable from the true parallel curve, and at
+    // corner_shape 2 this is exactly the circular arc annulus. NOT
+    // push_arc_background_vertices — that stays circular for genuine arcs.
+    let mut corner = |cx: f32, cy: f32, r: f32, start: f32, end: f32, out: &mut Vec<Vertex>| {
+        let r_in = (r - t).max(0.0);
+        let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
+        for i in 0..segments {
+            let t1 = start + (i as f32) * (end - start) / segments as f32;
+            let t2 = start + ((i + 1) as f32) * (end - start) / segments as f32;
+            let (c1, s1) = superellipse_pt(t1, corner_e);
+            let (c2, s2) = superellipse_pt(t2, corner_e);
+            let o1 = ndc(cx + r * c1, cy + r * s1);
+            let o2 = ndc(cx + r * c2, cy + r * s2);
+            let i1 = ndc(cx + r_in * c1, cy + r_in * s1);
+            let i2 = ndc(cx + r_in * c2, cy + r_in * s2);
+            out.push(Vertex { position: o1, color, clip_circle });
+            out.push(Vertex { position: o2, color, clip_circle });
+            out.push(Vertex { position: i1, color, clip_circle });
+            out.push(Vertex { position: o2, color, clip_circle });
+            out.push(Vertex { position: i2, color, clip_circle });
+            out.push(Vertex { position: i1, color, clip_circle });
+        }
+    };
 
     if r_tl > 0.1 {
-        push_arc_background_vertices(
-            x + r_tl, y + r_tl, r_tl, t,
-            std::f32::consts::PI, 1.5 * std::f32::consts::PI,
-            sw, sh, color, segments, clip_circle,
-            out,
-        );
+        corner(x + r_tl, y + r_tl, r_tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI, out);
     }
     if r_tr > 0.1 {
-        push_arc_background_vertices(
-            x + ww - r_tr, y + r_tr, r_tr, t,
-            1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
-            sw, sh, color, segments, clip_circle,
-            out,
-        );
+        corner(x + ww - r_tr, y + r_tr, r_tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI, out);
     }
     if r_br > 0.1 {
-        push_arc_background_vertices(
-            x + ww - r_br, y + h - r_br, r_br, t,
-            0.0, 0.5 * std::f32::consts::PI,
-            sw, sh, color, segments, clip_circle,
-            out,
-        );
+        corner(x + ww - r_br, y + h - r_br, r_br, 0.0, 0.5 * std::f32::consts::PI, out);
     }
     if r_bl > 0.1 {
-        push_arc_background_vertices(
-            x + r_bl, y + h - r_bl, r_bl, t,
-            0.5 * std::f32::consts::PI, std::f32::consts::PI,
-            sw, sh, color, segments, clip_circle,
-            out,
-        );
+        corner(x + r_bl, y + h - r_bl, r_bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI, out);
     }
 }
 
@@ -1390,6 +1419,9 @@ pub struct DlBatch {
     pub clip_rrect: Option<[f32; 5]>,
     pub start: u32,
     pub end: u32,
+    /// When set, this batch is one SDF-lit plate cover quad (see
+    /// [`crate::vk::PlatePush`]; already in physical px). Never merged.
+    pub plate: Option<crate::vk::PlatePush>,
 }
 
 /// An image draw from the display list: `at` is the vertex index it sorts
@@ -1414,14 +1446,36 @@ pub fn tessellate_display_list(
     sw: f32,
     sh: f32,
     scale: f32,
-) -> (Vec<Vertex>, Vec<DlBatch>, Vec<DlImage>) {
+) -> (Vec<Vertex>, Vec<DlBatch>, Vec<DlImage>, Vec<[f32; 12]>) {
     use crate::scene::paint::{Cap, Prim};
     let mut verts: Vec<Vertex> = Vec::new();
     let mut batches: Vec<DlBatch> = Vec::new();
     let mut images: Vec<DlImage> = Vec::new();
+    // Carves CSG'd into plates (see Frame2D::plate_features), plus the plate
+    // they group into: the most recent Plate/Bevel batch, provided only Text
+    // and Image prims (which draw through separate paths anyway) intervene.
+    let mut features: Vec<[f32; 12]> = Vec::new();
+    let mut last_plate: Option<(usize, crate::scene::layout::Rect)> = None;
+
+    // SDF-lit plate path (shader2d's plate branch) vs the legacy banded vertex
+    // shading, plus the frame-constant lighting inputs it pushes per plate.
+    let shader_plates = crate::layout::bevel_shader();
+    let plate_light = {
+        let az = crate::layout::light_source_position();
+        let el = std::f32::consts::FRAC_PI_4; // light elevation above the screen plane
+        [az.cos() * el.cos(), -az.sin() * el.cos(), el.sin()]
+    };
+    // [shading strength (1.0 at the default bevel_depth), specular strength,
+    // shininess, curvature/AO strength] — the plastic material. Curvature is
+    // kept near the raised path's crest amplitude: the recess shoulder's
+    // brightening lands on the same pixels as its specular line, and the two
+    // stack — at 0.5 the step read several times hotter than a plate roll.
+    let plate_mat = [crate::layout::bevel_depth() / 0.15, 0.4, 24.0, 0.2];
 
     for item in &dl.items {
         let start = verts.len() as u32;
+        let mut plate: Option<crate::vk::PlatePush> = None;
+        let mut made_plate: Option<crate::scene::layout::Rect> = None;
         // Logical [cx, cy, r] → the physical-pixel triple the vertex attribute carries.
         let no = item
             .clip_circle
@@ -1459,6 +1513,129 @@ pub fn tessellate_display_list(
                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, *fill, no, None, &mut verts);
                 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 } if shader_plates => {
+                // SDF-lit raised plate: one cover quad; the shader owns fill,
+                // roll shading, corners, and silhouette AA. Nominal corner
+                // radii (scale_corners false): a Bevel is a WIDGET-scale plate
+                // whose silhouette must match the nominal-radius squircles of
+                // the controls around it — only window-scale `Plate`s get the
+                // curvature-matched span.
+                verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
+                plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false));
+                made_plate = Some(*rect);
+            }
+            Prim::Plate { rect, radii, color, depth } if shader_plates => {
+                // Same lit-plate branch; the cover quad is the exact rect so the
+                // silhouette and the compositor's rounded window corners agree.
+                verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
+                plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, true));
+                made_plate = Some(*rect);
+            }
+            Prim::Recess { rect, radii, depth, edges }
+            | Prim::Boss { rect, radii, depth, edges }
+            | Prim::Ridge { rect, radii, depth, edges }
+                if shader_plates =>
+            {
+                // Recess carves down into the surface; Boss raises a plateau out
+                // of it (same machinery, depth sign flipped); Ridge is a raised
+                // rim straddling the boundary (its own overlay profile — never
+                // grouped, the CSG features only model monotonic steps).
+                let mode = match &item.prim {
+                    Prim::Boss { .. } => 3.0f32,
+                    Prim::Ridge { .. } => 4.0,
+                    _ => 2.0,
+                };
+                let raised = mode > 2.5;
+                // Grouped into the enclosing plate whenever one is live: the
+                // carve becomes a CSG feature of that plate's single draw —
+                // exact composite shading, real junctions at the plate's rolled
+                // perimeter — instead of a shading overlay (the fallback below).
+                if let Some((bi, prect)) = last_plate.filter(|_| mode < 3.5) {
+                    let inside = rect.x >= prect.x - 0.5
+                        && rect.y >= prect.y - 0.5
+                        && rect.x + rect.width <= prect.x + prect.width + 0.5
+                        && rect.y + rect.height <= prect.y + prect.height + 0.5;
+                    if inside && features.len() < crate::vk::MAX_PLATE_FEATURES {
+                        // A wall the carve shares with the plate's edge extends
+                        // past the plate, so the carve has no wall there.
+                        let ext = *depth + 4.0;
+                        let (mut x0, mut y0) = (rect.x, rect.y);
+                        let (mut x1, mut y1) = (rect.x + rect.width, rect.y + rect.height);
+                        if !edges.0 { y0 -= ext; }
+                        if !edges.1 { x1 += ext; }
+                        if !edges.2 { y1 += ext; }
+                        if !edges.3 { x0 -= ext; }
+                        let t_px = *depth * scale;
+                        // Negative depth = raised (Boss); the shader's summed
+                        // slope vectors and curvature sign follow it.
+                        let k_px =
+                            if raised { -RECESS_DEPTH_RATIO * t_px } else { RECESS_DEPTH_RATIO * t_px };
+                        if let Some(p) = batches[bi].plate.as_mut() {
+                            if p.host[1] == 0.0 {
+                                p.host[0] = features.len() as f32;
+                            }
+                            p.host[1] += 1.0;
+                        }
+                        features.push([
+                            (x0 + x1) * 0.5 * scale,
+                            (y0 + y1) * 0.5 * scale,
+                            (x1 - x0) * 0.5 * scale,
+                            (y1 - y0) * 0.5 * scale,
+                            radii.0 * scale,
+                            radii.1 * scale,
+                            radii.2 * scale,
+                            radii.3 * scale,
+                            t_px,
+                            k_px,
+                            0.0,
+                            0.0,
+                        ]);
+                        continue;
+                    }
+                }
+                // Overlay-only carve: the cover quad inflates by half the roll
+                // width (the step straddles the boundary) and carries no color —
+                // the shader emits translucent white/black over what's beneath.
+                let infl = *depth * 0.5 + 2.0;
+                verts.extend(quad_vertices(
+                    rect.x - infl, rect.y - infl,
+                    rect.width + 2.0 * infl, rect.height + 2.0 * infl,
+                    sw, sh, [0.0; 4],
+                ));
+                // A suppressed wall is pushed past the cover quad, so its
+                // shading falls outside the drawn pixels (see Prim::Recess on
+                // why a flush region is a step, not a trough).
+                let ext = *depth + 4.0;
+                let (mut x0, mut y0) = (rect.x, rect.y);
+                let (mut x1, mut y1) = (rect.x + rect.width, rect.y + rect.height);
+                if !edges.0 { y0 -= ext; }
+                if !edges.1 { x1 += ext; }
+                if !edges.2 { y1 += ext; }
+                if !edges.3 { x0 -= ext; }
+                let sdf_rect = crate::scene::layout::Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
+                let mut p = plate_push_raised(&sdf_rect, *radii, *depth, scale, plate_light, plate_mat, false);
+                p.mode = mode;
+                // Host-plate box for the roll fade: a suppressed wall means the
+                // recess runs flush to the host's edge there, so that side of
+                // the box sits at the original rect edge; enabled walls face
+                // host interior, pushed to ±1e5 so no fade applies.
+                const FAR: f32 = 1e5;
+                let (hx0, hy0) = (
+                    if edges.3 { rect.x - FAR } else { rect.x },
+                    if edges.0 { rect.y - FAR } else { rect.y },
+                );
+                let (hx1, hy1) = (
+                    if edges.1 { rect.x + rect.width + FAR } else { rect.x + rect.width },
+                    if edges.2 { rect.y + rect.height + FAR } else { rect.y + rect.height },
+                );
+                p.host = [
+                    (hx0 + hx1) * 0.5 * scale,
+                    (hy0 + hy1) * 0.5 * scale,
+                    (hx1 - hx0) * 0.5 * scale,
+                    (hy1 - hy0) * 0.5 * scale,
+                ];
+                plate = Some(p);
+            }
             Prim::Bevel { rect, radii, color, depth } => {
                 // 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
@@ -1499,6 +1676,34 @@ pub fn tessellate_display_list(
                     EdgeKind::Step, &mut verts,
                 );
             }
+            Prim::Boss { rect, radii, depth, edges } => {
+                // Legacy raised step: the recess overlay with the light sign upright.
+                push_bevel_edge_vertices_banded(
+                    rect.x, rect.y, rect.width, rect.height, *radii, *depth,
+                    sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(*depth), *edges,
+                    EdgeKind::Step, &mut verts,
+                );
+            }
+            Prim::Ridge { rect, radii, depth, edges } => {
+                // Legacy approximation: a raised step up at the boundary plus a
+                // recessed step down half a width in (the banded machinery has no
+                // bump profile; the double-pass hot crest is accepted here — the
+                // legacy path exists only for A/B comparison).
+                let half = *depth * 0.5;
+                push_bevel_edge_vertices_banded(
+                    rect.x, rect.y, rect.width, rect.height, *radii, half,
+                    sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), *edges,
+                    EdgeKind::Step, &mut verts,
+                );
+                let ir = (radii.0 - half).max(0.0);
+                push_bevel_edge_vertices_banded(
+                    rect.x + half, rect.y + half,
+                    rect.width - *depth, rect.height - *depth,
+                    (ir, ir, ir, ir), half,
+                    sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), *edges,
+                    EdgeKind::Step, &mut verts,
+                );
+            }
             Prim::Arc { cx, cy, radius, thickness, start: sa, end: ea, color } => {
                 push_arc_background_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *color, segs(*radius), no, &mut verts);
             }
@@ -1526,16 +1731,88 @@ pub fn tessellate_display_list(
             }
         }
         // Merge into the previous batch if it shares this clip pair and is contiguous.
-        if let Some(last) = batches.last_mut() {
-            if last.scissor == item.clip && last.clip_rrect == item.clip_rrect && last.end == start {
-                last.end = end;
-                continue;
+        // Plate batches carry per-draw push constants, so they never merge.
+        if plate.is_none() {
+            // Ordinary geometry painted after a plate ends its carve-grouping
+            // window: a recess emitted later must overlay this geometry (the
+            // fallback path), not shade beneath it inside the plate's draw.
+            last_plate = None;
+            if let Some(last) = batches.last_mut() {
+                if last.plate.is_none()
+                    && last.scissor == item.clip
+                    && last.clip_rrect == item.clip_rrect
+                    && last.end == start
+                {
+                    last.end = end;
+                    continue;
+                }
             }
         }
-        batches.push(DlBatch { scissor: item.clip, clip_rrect: item.clip_rrect, start, end });
+        batches.push(DlBatch { scissor: item.clip, clip_rrect: item.clip_rrect, start, end, plate });
+        if let Some(prect) = made_plate {
+            last_plate = Some((batches.len() - 1, prect));
+        }
     }
 
-    (verts, batches, images)
+    (verts, batches, images, features)
+}
+
+/// A carve's depth as a fraction of its transition width — must match the
+/// shader's `RECESS_DEPTH` (used by the mode-2 overlay fallback).
+const RECESS_DEPTH_RATIO: f32 = 0.6;
+
+/// The push-constant block for a raised SDF-lit plate over `rect` (logical px in,
+/// physical px out). Corner radii clamp to the half-extent cap the SDF needs.
+fn plate_push_raised(
+    rect: &crate::scene::layout::Rect,
+    radii: (f32, f32, f32, f32),
+    width: f32,
+    scale: f32,
+    light: [f32; 3],
+    material: [f32; 4],
+    scale_corners: bool,
+) -> crate::vk::PlatePush {
+    let cap = rect.width.min(rect.height) * 0.5;
+    let shape = crate::layout::corner_shape();
+    // A raw superellipse of exponent n at the circle's nominal radius turns
+    // TIGHTER at the diagonal than that circle — its radius of curvature there
+    // is √2·r / (2^(1/n)·(n − 1)) — and once the roll inset exceeds it, the
+    // offset curve the specular band follows creases into a visible square
+    // corner. For PLATES (`scale_corners`), scale the corner span so the
+    // diagonal curvature radius equals the configured radius: the corner reads
+    // as the same size, entered and exited smoothly (the same reason Apple's
+    // continuous corners run ~1.5·r along the edge), and every inset ≤ r stays
+    // crease-free. Continuous at n = 2, where the factor is exactly 1.
+    // Widget-scale overlay reliefs (recess/boss/ridge fallbacks) pass false:
+    // their radii must MATCH the nominal-radius squircles of the widget
+    // silhouettes around them, and at their few-px roll widths the offset
+    // crease is subpixel.
+    let rscale = if scale_corners && shape > 2.001 {
+        (shape - 1.0) * 2f32.powf(1.0 / shape) / std::f32::consts::SQRT_2
+    } else {
+        1.0
+    };
+    crate::vk::PlatePush {
+        rect: [
+            (rect.x + rect.width * 0.5) * scale,
+            (rect.y + rect.height * 0.5) * scale,
+            rect.width * 0.5 * scale,
+            rect.height * 0.5 * scale,
+        ],
+        radii: [
+            (radii.0 * rscale).clamp(0.0, cap) * scale,
+            (radii.1 * rscale).clamp(0.0, cap) * scale,
+            (radii.2 * rscale).clamp(0.0, cap) * scale,
+            (radii.3 * rscale).clamp(0.0, cap) * scale,
+        ],
+        light: [light[0], light[1], light[2], width * scale],
+        material,
+        // Mode-1 semantics: [feature offset, feature count] — no carves yet;
+        // the tessellator fills these in as recesses group into this plate.
+        host: [0.0, 0.0, 0.0, 0.0],
+        mode: 1.0,
+        shape,
+    }
 }
 
 pub fn extra_quad_vertices(
@@ -2328,12 +2605,12 @@ impl<A: Application> EngineState<A> {
             }
         }
 
-        let (mut verts, mut dl_batches, dl_images) = tessellate_display_list(&dl, logical_w, logical_h, scale_factor as f32);
+        let (mut verts, mut dl_batches, dl_images, plate_features) = tessellate_display_list(&dl, logical_w, logical_h, scale_factor as f32);
         // custom_vertices (e.g. graph geometry) is appended as a final unclipped batch drawn on top.
         let pre_custom = verts.len() as u32;
         self.inner.as_mut().unwrap().custom_vertices(&mut verts, LogicalSize::new(logical_w, logical_h), scale_factor);
         if (verts.len() as u32) > pre_custom {
-            dl_batches.push(DlBatch { scissor: None, clip_rrect: None, start: pre_custom, end: verts.len() as u32 });
+            dl_batches.push(DlBatch { scissor: None, clip_rrect: None, start: pre_custom, end: verts.len() as u32, plate: None });
         }
 
         // 1b. Overlay quads (drawn after the text pass).
@@ -2471,6 +2748,7 @@ impl<A: Application> EngineState<A> {
                     .map(|c| [c[0] * scale_f32, c[1] * scale_f32, c[2] * scale_f32, c[3] * scale_f32, c[4] * scale_f32]),
                 start: batch.start,
                 end: batch.end,
+                plate: batch.plate,
             })
             .collect();
 
@@ -2496,6 +2774,7 @@ impl<A: Application> EngineState<A> {
             batches: &batches,
             overlay_verts: &overlay_verts,
             images: &image_quads,
+            plate_features: &plate_features,
             clear_color,
         });
     }
diff --git a/src/layout.rs b/src/layout.rs
index 695d061..bc4a467 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -109,6 +109,8 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "window_manager.light_source_position" => "light_source_position",
                 "window_manager.bevel_depth" => "bevel_depth",
                 "window_manager.bevel_width" => "bevel_width",
+                "window_manager.bevel_shader" => "bevel_shader",
+                "window_manager.corner_shape" => "corner_shape",
                 "style.control.ramp.height" => "ramp_height",
                 "style.layout.column.gap" => "column_gap",
                 "style.control.control_panel.padding" => "control_panel_padding",
@@ -1488,6 +1490,24 @@ pub fn bevel_depth() -> f32 {
     get_style_registry().read().unwrap().get_float("bevel_depth").unwrap_or(0.15)
 }
 
+/// Corner shape exponent for SDF-lit plates: 2.0 (the default) is a circular
+/// arc; higher values are superellipse "squircle" corners with continuous
+/// curvature — ~4.5 is the Apple-like look. Clamped to [2, 16]: below 2 the
+/// Lp construction degenerates toward a chamfer, above 16 it is visually a
+/// square corner and the pow() terms start flirting with f32 range.
+pub fn corner_shape() -> f32 {
+    lazy_init_style_registry();
+    get_style_registry().read().unwrap().get_float("corner_shape").unwrap_or(2.0).clamp(2.0, 16.0)
+}
+
+/// Whether plates/bevels/recesses render through shader2d's per-pixel SDF-lit
+/// plate branch (the default) or the legacy banded vertex shading. `bevel_shader 0`
+/// in config flips back to the old look for A/B comparison.
+pub fn bevel_shader() -> bool {
+    lazy_init_style_registry();
+    get_style_registry().read().unwrap().get_float("bevel_shader").map(|v| v != 0.0).unwrap_or(true)
+}
+
 /// How wide a rolled edge is, in logical px — the distance over which a plate's perimeter
 /// or a recess wall curves away from the flat surface. `bevel_depth` is the companion
 /// knob: it sets how hard the light falls across that distance. Wide and shallow reads as
diff --git a/src/main.rs b/src/main.rs
index f715a62..358f377 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,6 +36,18 @@ enum DemoMessage {
     Exit,
 }
 
+/// Chrome typography: the title/status font sizes, and the layout leaves that
+/// hold them derived as one line-height (×1.2, the toolkit convention) — so the
+/// header and status bands, which anchor to those solved rects, resize with the
+/// typography instead of relying on magic leaf heights.
+const TITLE_FONT_SIZE: f32 = 15.0;
+const STATUS_FONT_SIZE: f32 = 12.0;
+/// Vertical padding on each side of the status band's text line.
+const STATUS_BAND_PAD: f32 = 5.0;
+fn text_leaf_height(font_size: f32) -> f32 {
+    (font_size * 1.2).ceil()
+}
+
 struct DemoApp {
     // ── Widgets: app-owned values on the narrow-trait adapter. Their addresses must be
     // stable across frames (plain struct fields, not Vec elements): the UiContext
@@ -129,16 +141,23 @@ impl Application for DemoApp {
     ) -> Self {
         cce_ui::scale::set_scale_factor(1.0);
         Self {
-            button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Click me"),
-            toggle: Toggle::new(),
+            button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Click me").with_raised(true),
+            toggle: Toggle::new().with_raised(true),
             // Slider `value` is NORMALIZED 0..1; `with_range` only scales the readout
             // (`get_scaled_value`). Wheel nudging is an explicit opt-in.
-            slider: Slider::new().with_range(0.0, 100.0).with_value(0.4).with_scroll(true),
-            name_box: TextBox::new(String::new()).with_placeholder("Type a name..."),
+            slider: Slider::new()
+                .with_range(0.0, 100.0)
+                .with_value(0.4)
+                .with_scroll(true)
+                .with_raised(true),
+            name_box: TextBox::new(String::new())
+                .with_placeholder("Type a name...")
+                .with_recessed(true),
             theme_dropdown: Dropdown::new(
                 vec!["Forest".into(), "Ocean".into(), "Ember".into()],
                 0,
-            ),
+            )
+            .with_raised(true),
             toggle_on: false,
             clicks: 0,
             status: "Ready.".to_string(),
@@ -215,7 +234,17 @@ impl Application for DemoApp {
             let root = arena.insert(LayoutBox::container(
                 Style::column().padding(16.0).gap(14.0).cross_align(CrossAlign::Stretch),
             ));
-            let title = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 22.0)));
+            let title = arena.insert(LayoutBox::leaf(
+                Style::row(),
+                LSize::new(0.0, text_leaf_height(TITLE_FONT_SIZE)),
+            ));
+            // Clearance under the header band: the recess step rolls over `bevel_width`
+            // past the band's bottom edge, so the first content row must stand off by at
+            // least that or it crowds the carve.
+            let band_gap = arena.insert(LayoutBox::leaf(
+                Style::row(),
+                LSize::new(0.0, cce_ui::layout::bevel_width()),
+            ));
             let controls = arena.insert(LayoutBox::container(
                 Style::row().gap(14.0).height(Length::Fixed(28.0)),
             ));
@@ -227,8 +256,12 @@ impl Application for DemoApp {
             let slider = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 24.0)));
             let name_box = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 30.0)));
             let spacer = arena.insert(LayoutBox::container(Style::column().grow(1.0)));
-            let status = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 18.0)));
+            let status = arena.insert(LayoutBox::leaf(
+                Style::row(),
+                LSize::new(0.0, text_leaf_height(STATUS_FONT_SIZE)),
+            ));
             arena.append_child(root, title);
+            arena.append_child(root, band_gap);
             arena.append_child(root, controls);
             arena.append_child(controls, button);
             arena.append_child(controls, toggle);
@@ -274,19 +307,44 @@ impl Application for DemoApp {
         let w = self.width as f32;
         let h = self.height as f32;
 
-        // The window plate — the dissolved root Backplate's exact paint: page-low color
-        // at the configured opacity, config corner radius.
+        // The window plate — the dissolved root Backplate as a lit object: page-low color
+        // at the configured opacity, config corner radius, perimeter rolled over
+        // `bevel_width` so the surface reads as a physical plate rather than a flat fill.
         let mut plate = cce_ui::color::page_low_color();
         if plate[3] > 0.001 {
             plate[3] = cce_ui::color::active_backplate_opacity();
         }
         let radius = cce_ui::colors::backplate_corner_radius();
         let frame = Rect { x: 0.0, y: 0.0, width: w, height: h };
-        if radius > 0.1 {
-            pc.rounded_rect(frame, radius, (true, true, true, true), plate);
-        } else {
-            pc.quad(frame, plate);
-        }
+        let bevel = cce_ui::layout::bevel_width();
+        pc.plate(frame, (radius, radius, radius, radius), plate, bevel);
+
+        // Header band: the title strip carved one step down into the plate. Flush to the
+        // window's top and sides, so its only real wall is the bottom one facing the
+        // content (the recessed-MenuBar idiom — the other three would fight the plate's
+        // own rolled perimeter).
+        let band_h = self.title_rect.y + self.title_rect.height + 10.0;
+        pc.recess_edges(
+            Rect { x: 0.0, y: 0.0, width: w, height: band_h },
+            (0.0, 0.0, 0.0, 0.0),
+            bevel,
+            (false, false, true, false),
+        );
+
+        // Status band: the header's mirror — carved into the bottom of the
+        // plate, flush to the window's bottom and sides, its only wall the top
+        // one facing the content. Emitted here, before any widget geometry, so
+        // it CSG-groups into the plate like the header band does. Sized from
+        // the status font plus a symmetric pad (the layout's status leaf only
+        // reserves the space; the band and its text center independently).
+        let status_h = text_leaf_height(STATUS_FONT_SIZE) + 2.0 * STATUS_BAND_PAD;
+        let status_top = h - status_h;
+        pc.recess_edges(
+            Rect { x: 0.0, y: status_top, width: w, height: status_h },
+            (0.0, 0.0, 0.0, 0.0),
+            bevel,
+            (true, false, false, false),
+        );
 
         // App chrome text: plain prims. `text_with` carries an optional font family and
         // optional bounds; unbounded text is clamped to the surface by the engine.
@@ -294,7 +352,7 @@ impl Application for DemoApp {
             "cce-ui reference gallery".to_string(),
             self.title_rect.x,
             self.title_rect.y,
-            15.0,
+            TITLE_FONT_SIZE,
             [0xdd, 0xdd, 0xe2],
             Some("monospace".to_string()),
             None,
@@ -302,10 +360,12 @@ impl Application for DemoApp {
         pc.text_with(
             self.status.clone(),
             self.status_rect.x,
-            self.status_rect.y,
-            12.0,
+            cce_ui::layout::align_text_y(status_top, status_h, STATUS_FONT_SIZE, 0.0),
+            STATUS_FONT_SIZE,
             [0x9a, 0x9a, 0xa4],
-            None,
+            // None here falls through fontconfig's unbundled sans alias to the
+            // serif fallback — always name a family.
+            Some("monospace".to_string()),
             None,
         );
 
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 1eee4dd..57cf3b2 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -57,6 +57,21 @@ pub enum Prim {
     /// 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, depth: f32, edges: (bool, bool, bool, bool) },
+    /// The inverse of [`Prim::Recess`]: a plateau RAISED out of the surface below.
+    /// Like `Recess` it emits only the shaded edges, never a fill — the face is the
+    /// untouched surface underneath — so a region outlined by raised rolled bumps
+    /// keeps the backplate's own color and translucency. Same wall semantics as
+    /// `Recess` (`edges` = top/right/bottom/left); the lighting is the raised sign,
+    /// so the edges facing `light_source_position` catch the light.
+    Boss { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
+    /// A raised RIM riding the rect's boundary: a bump profile straddling the
+    /// outline (span ±depth/2), rising from the surrounding surface to a crest on
+    /// the boundary and falling back to the same level inside — an elevated border
+    /// around a channel, both faces at the underlying surface's own level. One
+    /// primitive, ONE lighting evaluation per pixel: building the same shape from
+    /// a Boss plus an inset Recess stacks two shading passes (double specular /
+    /// shoulder terms at the crest) and reads far hotter than a plate edge.
+    Ridge { 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
@@ -352,6 +367,45 @@ impl PaintCtx {
         self.recess_edges(rect, radii, depth, (true, true, true, true));
     }
 
+    /// Raise a plateau out of the already-painted surface below — the inverse of
+    /// [`PaintCtx::recess`]. Only the edges are shaded; the face stays the surface
+    /// beneath, so the raised region inherits the backplate's color. `depth` is the
+    /// roll width in px (pass [`crate::layout::bevel_width`] unless the widget
+    /// needs a tighter lip).
+    pub fn boss(&mut self, rect: Rect, radii: Radii, depth: f32) {
+        self.boss_edges(rect, radii, depth, (true, true, true, true));
+    }
+
+    /// [`PaintCtx::boss`] with only some of the walls — see `Prim::Boss`.
+    pub fn boss_edges(
+        &mut self,
+        rect: Rect,
+        radii: Radii,
+        depth: f32,
+        edges: (bool, bool, bool, bool),
+    ) {
+        let rect = self.apply_offset(rect);
+        self.push(Prim::Boss { rect, radii, depth, edges });
+    }
+
+    /// Raise a rim along `rect`'s boundary — see `Prim::Ridge`. `depth` is the
+    /// full width of the bump (it straddles the outline by ±depth/2).
+    pub fn ridge(&mut self, rect: Rect, radii: Radii, depth: f32) {
+        self.ridge_edges(rect, radii, depth, (true, true, true, true));
+    }
+
+    /// [`PaintCtx::ridge`] with only some of the walls — see `Prim::Ridge`.
+    pub fn ridge_edges(
+        &mut self,
+        rect: Rect,
+        radii: Radii,
+        depth: f32,
+        edges: (bool, bool, bool, bool),
+    ) {
+        let rect = self.apply_offset(rect);
+        self.push(Prim::Ridge { rect, radii, depth, edges });
+    }
+
     /// [`PaintCtx::recess`] with only some of the walls — see `Prim::Recess`.
     pub fn recess_edges(
         &mut self, rect: Rect, radii: Radii, depth: f32,
diff --git a/src/vk/mod.rs b/src/vk/mod.rs
index 0558ccd..698a1ee 100644
--- a/src/vk/mod.rs
+++ b/src/vk/mod.rs
@@ -40,7 +40,7 @@ mod text;
 
 pub use core::VkCore;
 pub use image::{free_image, upload_rgba, ImageQuad};
-pub use renderer::{Batch2D, Frame2D, VkRenderer};
+pub use renderer::{Batch2D, Frame2D, PlatePush, VkRenderer, MAX_PLATE_FEATURES};
 pub use rt::{RtCamera, RtMaterial, RtOffscreen, RtTriangle};
 pub use scene::{MeshId, SceneDraw, Vertex3D};
 pub use text::TextSpan;
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index c92b9f3..13433f3 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -30,6 +30,35 @@ pub struct Batch2D {
     pub clip_rrect: Option<[f32; 5]>,
     pub start: u32,
     pub end: u32,
+    /// When set, this batch is a single SDF-lit plate cover quad: the params go
+    /// out as push constants and shader2d's plate branch lights it per pixel.
+    pub plate: Option<PlatePush>,
+}
+
+/// Push-constant block for one SDF-lit plate batch (physical px throughout).
+/// Mirrors the `p_*` fields of shader2d's `RRectClip`.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct PlatePush {
+    /// SDF box: center + half-extents. May extend past the cover quad — that is
+    /// how a recess suppresses a wall.
+    pub rect: [f32; 4],
+    /// Per-corner radii [tl, tr, br, bl].
+    pub radii: [f32; 4],
+    /// xyz = unit vector toward the light (+z out of the screen), w = roll width px.
+    pub light: [f32; 4],
+    /// [shading strength, specular strength, shininess, curvature/AO strength].
+    pub material: [f32; 4],
+    /// Mode 1: `[feature offset, feature count, 0, 0]` into the frame's
+    /// `plate_features` — the carves CSG'd out of this plate (the renderer adds
+    /// the frame slot's base offset at record time). Mode 2: the host-plate box
+    /// (center + half-extents) a free recess fades out against; far-away sides
+    /// (±1e5) disable the fade.
+    pub host: [f32; 4],
+    /// 1.0 = raised lit plate, 2.0 = recess overlay.
+    pub mode: f32,
+    /// Corner shape exponent: 2.0 = circular arcs, > 2 = superellipse
+    /// (continuous-curvature) corners — see shader2d's `plate_sdf_grad`.
+    pub shape: f32,
 }
 
 /// A full 2D frame: the display-list vertices (optionally split into scissored
@@ -41,10 +70,18 @@ pub struct Frame2D<'a> {
     pub overlay_verts: &'a [Vertex],
     /// User images drawn interleaved with `verts` by each quad's `z_before`.
     pub images: &'a [ImageQuad],
+    /// Carves CSG'd into this frame's SDF-lit plates, 12 floats each (rect
+    /// center+half-extents, per-corner radii, [width px, depth px, 0, 0]).
+    /// Plate batches reference them by offset+count in `PlatePush::host`.
+    pub plate_features: &'a [[f32; 12]],
     pub clear_color: [f32; 4],
 }
 
 const FRAMES_IN_FLIGHT: usize = 2;
+/// Max plate-carve features per frame; the shader's UBO holds one slot of this
+/// size per frame in flight.
+pub const MAX_PLATE_FEATURES: usize = 64;
+const PLATE_FEATURE_BYTES: usize = 48;
 
 pub(crate) struct AllocatedBuffer {
     pub(crate) buffer: vk::Buffer,
@@ -152,6 +189,7 @@ pub struct VkRenderer {
     descriptor_set: vk::DescriptorSet,
     backdrop_sampler: vk::Sampler,
     window_info: AllocatedBuffer,
+    plate_features: AllocatedBuffer,
 
     frames: Vec<Frame>,
     frame_index: usize,
@@ -445,6 +483,11 @@ impl VkRenderer {
                 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
                 .descriptor_count(1)
                 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
+            vk::DescriptorSetLayoutBinding::default()
+                .binding(3)
+                .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
+                .descriptor_count(1)
+                .stage_flags(vk::ShaderStageFlags::FRAGMENT),
         ];
         let descriptor_set_layout = device
             .create_descriptor_set_layout(
@@ -454,12 +497,13 @@ impl VkRenderer {
             .expect("Failed to create descriptor set layout");
 
         let set_layouts = [descriptor_set_layout];
-        // Push constants: the per-batch rounded-rect clip (two vec4s — [cx, cy, bx, by]
-        // and [r, enabled, 0, 0]) read by shader2d's fragment stage.
+        // Push constants: the per-batch rounded-rect clip plus the SDF-lit
+        // plate block (seven vec4s, matching shader2d's `RRectClip`), read by
+        // shader2d's fragment stage.
         let push_ranges = [vk::PushConstantRange::default()
             .stage_flags(vk::ShaderStageFlags::FRAGMENT)
             .offset(0)
-            .size(32)];
+            .size(112)];
         let pipeline_layout = device
             .create_pipeline_layout(
                 &vk::PipelineLayoutCreateInfo::default()
@@ -593,6 +637,15 @@ impl VkRenderer {
             vk::BufferUsageFlags::UNIFORM_BUFFER,
             "window-info",
         );
+        // Plate-carve features, one MAX_PLATE_FEATURES slot per frame in
+        // flight so a write never races the previous frame's reads.
+        let plate_features = create_cpu_buffer(
+            &device,
+            allocator,
+            (FRAMES_IN_FLIGHT * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES) as vk::DeviceSize,
+            vk::BufferUsageFlags::UNIFORM_BUFFER,
+            "plate-features",
+        );
 
         let pool_sizes = [
             vk::DescriptorPoolSize::default()
@@ -603,7 +656,7 @@ impl VkRenderer {
                 .descriptor_count(1),
             vk::DescriptorPoolSize::default()
                 .ty(vk::DescriptorType::UNIFORM_BUFFER)
-                .descriptor_count(1),
+                .descriptor_count(2),
         ];
         let descriptor_pool = device
             .create_descriptor_pool(
@@ -629,6 +682,10 @@ impl VkRenderer {
             .buffer(window_info.buffer)
             .offset(0)
             .range(16)];
+        let feature_infos = [vk::DescriptorBufferInfo::default()
+            .buffer(plate_features.buffer)
+            .offset(0)
+            .range((FRAMES_IN_FLIGHT * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES) as vk::DeviceSize)];
         device.update_descriptor_sets(
             &[
                 vk::WriteDescriptorSet::default()
@@ -646,6 +703,11 @@ impl VkRenderer {
                     .dst_binding(2)
                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
                     .buffer_info(&buffer_infos),
+                vk::WriteDescriptorSet::default()
+                    .dst_set(descriptor_set)
+                    .dst_binding(3)
+                    .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
+                    .buffer_info(&feature_infos),
             ],
             &[],
         );
@@ -709,6 +771,7 @@ impl VkRenderer {
             descriptor_set,
             backdrop_sampler,
             window_info,
+            plate_features,
             frames,
             frame_index: 0,
             text,
@@ -1048,6 +1111,7 @@ impl VkRenderer {
             batches: &[],
             overlay_verts: &[],
             images: &[],
+            plate_features: &[],
             clear_color: [0.0; 4],
         })
     }
@@ -1096,6 +1160,18 @@ impl VkRenderer {
 
             self.core.device.reset_fences(&[in_flight]).unwrap();
 
+            // Upload this frame's plate carves into its slot of the feature
+            // UBO (the slot's previous user has fenced, so no race).
+            if !frame2d.plate_features.is_empty() {
+                let n = frame2d.plate_features.len().min(MAX_PLATE_FEATURES);
+                let base = frame_index * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES;
+                if let Some(allocation) = self.plate_features.allocation.as_mut() {
+                    let bytes: &[u8] = bytemuck::cast_slice(&frame2d.plate_features[..n]);
+                    allocation.mapped_slice_mut().unwrap()[base..base + bytes.len()]
+                        .copy_from_slice(bytes);
+                }
+            }
+
             // Upload display-list + overlay vertices into this frame's buffer
             // (its fence has signaled, so the GPU is done with it; growing swaps
             // in a fresh buffer). Overlay verts sit after the main range.
@@ -1312,8 +1388,13 @@ impl VkRenderer {
                 order.sort_by_key(|&k| images[k].z_before);
                 let mut img_i = 0usize;
 
-                let default_batch =
-                    [Batch2D { scissor: None, clip_rrect: None, start: 0, end: frame.vertex_count }];
+                let default_batch = [Batch2D {
+                    scissor: None,
+                    clip_rrect: None,
+                    start: 0,
+                    end: frame.vertex_count,
+                    plate: None,
+                }];
                 let batches: &[Batch2D] =
                     if frame2d.batches.is_empty() { &default_batch } else { frame2d.batches };
 
@@ -1377,10 +1458,28 @@ impl VkRenderer {
                             self.core.device
                                 .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
                             self.core.device.cmd_set_scissor(cmd, 0, &[scissor]);
-                            // Per-batch rounded-rect clip (fragments outside discard).
+                            // Per-batch rounded-rect clip (fragments outside
+                            // discard) + the SDF-lit plate block when this
+                            // batch is a plate cover quad.
                             let rr = batch.clip_rrect.unwrap_or([0.0; 5]);
                             let enabled = if batch.clip_rrect.is_some() { 1.0f32 } else { 0.0 };
-                            let pc = [rr[0], rr[1], rr[2], rr[3], rr[4], enabled, 0.0, 0.0];
+                            let mut pc = [0.0f32; 28];
+                            pc[..5].copy_from_slice(&rr);
+                            pc[5] = enabled;
+                            if let Some(p) = &batch.plate {
+                                pc[6] = p.mode;
+                                pc[7] = p.shape;
+                                pc[8..12].copy_from_slice(&p.rect);
+                                pc[12..16].copy_from_slice(&p.radii);
+                                pc[16..20].copy_from_slice(&p.light);
+                                pc[20..24].copy_from_slice(&p.material);
+                                pc[24..28].copy_from_slice(&p.host);
+                                if p.mode == 1.0 {
+                                    // Rebase the feature offset onto this
+                                    // frame's UBO slot.
+                                    pc[24] += (frame_index * MAX_PLATE_FEATURES) as f32;
+                                }
+                            }
                             self.core.device.cmd_push_constants(
                                 cmd,
                                 self.pipeline_layout,
@@ -1428,8 +1527,9 @@ impl VkRenderer {
                 );
                 self.core.device
                     .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
-                // Push constants persist across binds — clear any batch's rounded clip.
-                let pc = [0.0f32; 8];
+                // Push constants persist across binds — clear any batch's
+                // rounded clip and plate mode.
+                let pc = [0.0f32; 28];
                 self.core.device.cmd_push_constants(
                     cmd,
                     self.pipeline_layout,
@@ -1516,8 +1616,11 @@ impl Drop for VkRenderer {
                 }
             }
             let mut window_info = std::mem::replace(&mut self.window_info, AllocatedBuffer::null());
+            let mut plate_features =
+                std::mem::replace(&mut self.plate_features, AllocatedBuffer::null());
             if let Some(allocator) = self.core.allocator.as_mut() {
                 destroy_cpu_buffer(&self.core.device, allocator, &mut window_info);
+                destroy_cpu_buffer(&self.core.device, allocator, &mut plate_features);
             }
 
             self.core.device.destroy_descriptor_pool(self.descriptor_pool, None);
@@ -1534,3 +1637,13 @@ impl Drop for VkRenderer {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    /// The WGSL shaders compile at process start, so a syntax or validation
+    /// error is a runtime panic in every client — catch it headlessly here.
+    #[test]
+    fn shader2d_compiles() {
+        assert!(!super::shader2d_spirv().is_empty());
+    }
+}
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 5cd6199..b736f02 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -16,6 +16,23 @@ struct WindowInfo {
 
 @group(0) @binding(2) var<uniform> window_info: WindowInfo;
 
+// One carve (recess) belonging to an SDF-lit plate: a rounded box subtracted
+// from the plate's material. rect = center + half-extents, radii per-corner
+// (both physical px; a wall the carve shares with the plate's edge is encoded
+// by extending the box past the plate on that side). params = [transition
+// width px, depth px, 0, 0].
+struct PlateFeature {
+    rect: vec4f,
+    radii: vec4f,
+    params: vec4f,
+}
+// Double-buffered by frame-in-flight: slot k's 64 entries belong to frame
+// index k. The plate's push constants carry the absolute offset.
+struct PlateFeatures {
+    items: array<PlateFeature, 128>,
+}
+@group(0) @binding(3) var<uniform> plate_features: PlateFeatures;
+
 fn is_outside_window_corners(pos: vec2<f32>) -> bool {
     let w = window_info.window_size.x;
     let h = window_info.window_size.y;
@@ -55,14 +72,255 @@ fn is_outside_window_corners(pos: vec2<f32>) -> bool {
     return false;
 }
 
-// Per-batch rounded-rect clip: rect0 = [cx, cy, bx, by] (center + SDF half-extents),
-// rect1 = [corner radius, enabled flag, 0, 0]. Physical pixels, like clip_position.
+// Per-batch push constants (112 bytes). The first two vec4s are the rounded-rect
+// clip: rect0 = [cx, cy, bx, by] (center + SDF half-extents), rect1 = [corner
+// radius, enabled flag, plate mode, plate corner shape]. When plate mode is
+// nonzero the batch is an SDF-lit plate (1 = raised plate, 2 = recess overlay)
+// and the p_* block describes it; the corner shape exponent selects circular
+// (2) vs superellipse (> 2) plate corners — see plate_sdf_grad. Physical
+// pixels, like clip_position.
 struct RRectClip {
     rect0: vec4f,
     rect1: vec4f,
+    // Plate SDF box: center + half-extents. May extend past the drawn cover
+    // quad — that is how a recess suppresses a wall (the edge lies outside the
+    // covered pixels, so its shading never lands).
+    p_rect: vec4f,
+    // Per-corner radii [tl, tr, br, bl].
+    p_radii: vec4f,
+    // xyz = unit vector toward the light (screen space, +z out of the screen),
+    // w = bevel roll width in px.
+    p_light: vec4f,
+    // [shading strength, specular strength, shininess, curvature/AO strength].
+    p_mat: vec4f,
+    // Mode 1 (raised plate): xy = [offset, count] into plate_features — the
+    // carves CSG'd out of this plate's material.
+    // Mode 2 (free recess overlay): the host-plate box (center + half-extents)
+    // the carve fades out against — a wall flush with the host's edge dies
+    // across the host's perimeter roll; far-away sides sit at ±1e5 (no fade).
+    p_host: vec4f,
 }
 var<push_constant> rrect_clip: RRectClip;
 
+const TAU: f32 = 6.28318530718;
+// Ambient floor of the plate lighting model: the fraction of illumination that
+// arrives from everywhere rather than from the directional light. Keeps shadow
+// walls readable instead of crushing to black.
+const PLATE_AMBIENT: f32 = 0.55;
+// Amplitude of the bright crest line hugging a raised plate's silhouette — the
+// ambient-catching convex rim that makes glass read as glass.
+const PLATE_CREST: f32 = 0.25;
+// Recess depth as a fraction of the roll width (a recess is visually shallower
+// than a raised plate's full quarter-round).
+const RECESS_DEPTH: f32 = 0.6;
+
+// Signed distance and gradient of the plate's rounded box at p, as
+// (grad.x, grad.y, distance). Analytic — no dpdx/dpdy — so the clip discards
+// above the plate branch cannot poison derivative quads, and corners need no
+// special casing: the gradient swings continuously around each arc.
+//
+// rect1.w is the corner shape exponent: 2 = circular arcs; > 2 swaps them for
+// superellipse (Lp-norm) corners — Apple-style continuous curvature, where
+// curvature ramps smoothly to zero at the edge join instead of jumping from
+// 1/r, so the lit roll's highlight sweeps a corner without a G2 kink. The Lp
+// gradient is not unit length, so both the direction and the distance carry a
+// first-order |∇| correction — exact on the boundary, and well within a shade
+// step over the roll's few-px band.
+fn rr_sdf_grad(p: vec2f, prect: vec4f, pradii: vec4f) -> vec3f {
+    let c = p - prect.xy;
+    let side = select(pradii.xw, pradii.yz, c.x > 0.0);
+    let r = select(side.x, side.y, c.y > 0.0);
+    let q = abs(c) - prect.zw + vec2f(r, r);
+    let s = vec2f(select(-1.0, 1.0, c.x >= 0.0), select(-1.0, 1.0, c.y >= 0.0));
+    if (q.x > 0.0 && q.y > 0.0) {
+        let shape = rrect_clip.rect1.w;
+        if (shape > 2.001) {
+            let lp = max(pow(pow(q.x, shape) + pow(q.y, shape), 1.0 / shape), 1e-4);
+            let g = vec2f(pow(q.x / lp, shape - 1.0), pow(q.y / lp, shape - 1.0));
+            let gm = max(length(g), 1e-4);
+            return vec3f(s * g / gm, (lp - r) / gm);
+        }
+        let len = max(length(q), 1e-4);
+        return vec3f(s * q / len, len - r);
+    }
+    if (q.x > q.y) {
+        return vec3f(s.x, 0.0, q.x - r);
+    }
+    return vec3f(0.0, s.y, q.y - r);
+}
+
+// Specular of a roll at tilt `slope` whose outward horizontal facing is along
+// `g`: the profile alignment (how close the roll's tilt is to the half-vector's
+// tilt) powered by shininess, times a gentle azimuthal falloff, minus the flat
+// face's baseline so the face contributes zero. Deliberately DECOUPLED rather
+// than Blinn-Phong's pow(dot(n, hv), s): coupled, a straight edge's normal can
+// never fully reach the half-vector (it tilts in one plane only) while a corner
+// diagonal's can, so the power function crushes edge lines relative to corner
+// glints and the meeting fattens into a blob that ignores the corner arc.
+// Decoupled, the band keeps constant inset, width, and peak intensity as it
+// sweeps a corner — the highlight follows the silhouette.
+// `sv` is the surface's slope vector — the horizontal part of the unnormalized
+// normal (-∇height, 1): its magnitude is the tilt, its direction the facing.
+fn roll_spec(sv: vec2f) -> f32 {
+    let m = length(sv);
+    if (m < 1e-5) {
+        return 0.0;
+    }
+    let hv = normalize(rrect_clip.p_light.xyz + vec3f(0.0, 0.0, 1.0));
+    let shininess = rrect_clip.p_mat.z;
+    let facing = sv / m;
+    let cos_t = inverseSqrt(1.0 + m * m);
+    let sin_t = m * cos_t;
+    let hxy = length(hv.xy);
+    let prof = cos_t * hv.z + sin_t * hxy; // cos(tilt - half-vector tilt)
+    let az = clamp(dot(facing, hv.xy) / max(hxy, 1e-4), 0.0, 1.0);
+    return rrect_clip.p_mat.y * max(pow(prof, shininess) - pow(hv.z, shininess), 0.0) * az * az;
+}
+
+// Slope of the raised roll's height profile at f (0 at the face join, 1 at the
+// silhouette). Circular (shape 2): a quarter-round h = sqrt(1 - f²) — tangent-
+// continuous with the face but with a curvature JUMP at the join (1/t → 0), the
+// profile-space twin of a circular plan corner. shape > 2 swaps in the matching
+// superellipse quadrant h = (1 - f^n)^(1/n): its curvature ramps to zero at the
+// join, so the roll's shading fades into the face instead of ending on a line.
+// The slope has the closed form (f/h)^(n-1), which IS the circular formula at
+// n = 2 — the same one-exponent generalization as the plan corners.
+fn roll_slope(f: f32) -> f32 {
+    let shape = rrect_clip.rect1.w;
+    if (shape > 2.001) {
+        let h = pow(max(1.0 - pow(f, shape), 1e-4), 1.0 / shape);
+        return pow(f / h, shape - 1.0);
+    }
+    return f / sqrt(max(1.0 - f * f, 1e-4));
+}
+
+// Slope of a carve's transition profile (0 on the surrounding plateau → 1 on
+// the carve floor) at v in [0, 1] across the wall — always >= 0, zero at both
+// ends. The profile is smoothstep normally; smootherstep (zero SECOND
+// derivative at both plateaus) under a continuous-curvature corner_shape —
+// the step's analog of the superellipse roll.
+fn carve_slope(v: f32) -> f32 {
+    if (rrect_clip.rect1.w > 2.001) {
+        let w = v * (1.0 - v);
+        return 30.0 * w * w;
+    }
+    return 6.0 * v * (1.0 - v);
+}
+
+// Per-pixel lighting of a plate. The plate is one composite height field:
+// the host's rolled-edge surface minus every carve's profile, with the carve
+// depth measured RELATIVE to the local surface (a deboss/etch, not a flat
+// milling plane — a flat tool would swallow the perimeter roll wherever a
+// band overlaps it, deleting the plate's own edge shading there). Heights
+// subtract, so slope vectors ADD: the pixel's normal comes from the summed
+// analytic slopes of every feature over it, and the junction where a carve's
+// wall crosses the plate's perimeter roll is the smooth composite of both
+// tilts, ending in the rim notch a real groove leaves. One lighting
+// evaluation per pixel — features never blend in color space. Shading is
+// expressed relative to the flat face (shade ratio 1.0, specular delta 0.0)
+// so the face keeps exactly the app's chosen color.
+fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
+    let gd = rr_sdf_grad(frag, rrect_clip.p_rect, rrect_clip.p_radii);
+    let d = -gd.z; // positive inside the plate, in px
+    let t = max(rrect_clip.p_light.w, 0.001);
+    let l = rrect_clip.p_light.xyz;
+    let strength = rrect_clip.p_mat.x;
+    let flat_shade = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * l.z;
+
+    if (rrect_clip.rect1.z < 1.5) {
+        let aa = clamp(d + 0.5, 0.0, 1.0); // 1px silhouette anti-aliasing
+        if (aa <= 0.0) {
+            discard;
+        }
+        var base = vcol;
+        if (vcol.a < 0.0) {
+            base = resolve_blur(frag, vcol);
+        }
+        let u = clamp(d / t, 0.0, 1.0);
+        let f = 1.0 - u;
+        // Host roll slope vector: vertical at the silhouette, flat where the
+        // roll meets the face — then every carve's slope adds to it, and its
+        // shoulder/fillet ambient term joins the roll's crest.
+        var sv = gd.xy * roll_slope(f);
+        var extra = PLATE_CREST * f * f * f;
+        let f_off = u32(rrect_clip.p_host.x);
+        let f_cnt = u32(rrect_clip.p_host.y);
+        for (var i = 0u; i < f_cnt; i = i + 1u) {
+            let feat = plate_features.items[f_off + i];
+            let fg = rr_sdf_grad(frag, feat.rect, feat.radii);
+            let ft = max(feat.params.x, 0.001);
+            let v = clamp(-fg.z / ft + 0.5, 0.0, 1.0);
+            if (v <= 0.0) {
+                continue;
+            }
+            // params.y (depth) is signed: positive carves down, negative
+            // raises a boss. The slope vector follows automatically; the
+            // shoulder/fillet ambient term flips with it (a boss's convex
+            // shoulder is at the top of its wall, not the bottom).
+            sv += -(feat.params.y / ft) * carve_slope(v) * fg.xy;
+            extra += rrect_clip.p_mat.w * sin(v * TAU) * sign(feat.params.y);
+        }
+        let n = normalize(vec3f(sv, 1.0));
+        let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
+        let shade = 1.0 + (diff / flat_shade - 1.0 + extra) * strength;
+        let spec = roll_spec(sv);
+        return vec4f(base.rgb * shade + vec3f(spec * strength), abs(base.a) * aa);
+    }
+
+    // Free-floating recess, boss, or ridge (one not grouped into a host plate —
+    // e.g. in a widget's own paint): an overlay over whatever is painted
+    // beneath — no fill, no silhouette. Junction behavior here is the heuristic
+    // host-box fade; grouped features get the exact CSG above.
+    // Mode 2 = recess (interior one step DOWN), mode 3 = boss (interior one
+    // step UP) — the same wall with the height sign flipped. Mode 4 = ridge: a
+    // raised bump straddling the boundary, both sides at the base level — ONE
+    // profile evaluation, so its crest carries a single specular/shoulder term
+    // instead of a boss+recess double-stack.
+    // All profiles straddle the boundary (span [-t/2, t/2]). Darkening is exact
+    // multiplicative shading (black at alpha 1 - shade); brightening is a
+    // translucent white screen.
+    let u = clamp(d / t + 0.5, 0.0, 1.0);
+    var slope = 0.0;
+    var curv = 0.0;
+    if (rrect_clip.rect1.z > 3.5) {
+        // Ridge bump: the carve profile mirrored about the boundary (rising
+        // outer half, falling inner half), amplitude halved so the wall tilt
+        // matches a step's despite the doubled profile rate.
+        let w = clamp(select(2.0 * u, 2.0 - 2.0 * u, u > 0.5), 0.0, 1.0);
+        let rising = select(-1.0, 1.0, u <= 0.5);
+        slope = rising * 0.5 * RECESS_DEPTH * 2.0 * carve_slope(w);
+        // Each half-wall is a boss wall: concave fillet at its base, convex
+        // shoulder toward the crest — and ZERO at the plateaus and crest, so
+        // flat ground composites to exactly nothing (a constant term here
+        // tints the whole cover quad).
+        curv = -rrect_clip.p_mat.w * sin(w * TAU);
+    } else {
+        let dir = select(-1.0, 1.0, rrect_clip.rect1.z > 2.5);
+        // The profile slope is carve_slope's family: smoothstep-derived
+        // normally, smootherstep (zero second derivative at the plateaus)
+        // under a continuous-curvature corner_shape — shading eases in and out
+        // instead of starting on a line.
+        slope = dir * RECESS_DEPTH * carve_slope(u);
+        // Curvature: the convex shoulder catches ambient light, the concave
+        // fillet self-occludes — on the outer half for a recess, inner for a
+        // boss.
+        curv = -dir * rrect_clip.p_mat.w * sin(u * TAU);
+    }
+    let sv = gd.xy * slope;
+    let n = normalize(vec3f(sv, 1.0));
+    let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
+    let spec = roll_spec(sv);
+    // Fade the carve out across the host plate's perimeter roll (see p_host).
+    let hb = rrect_clip.p_host;
+    let host_d = min(hb.z - abs(frag.x - hb.x), hb.w - abs(frag.y - hb.y));
+    let att = clamp(host_d / t, 0.0, 1.0);
+    let v = (diff / flat_shade - 1.0 + curv + spec) * strength * att;
+    if (v >= 0.0) {
+        return vec4f(1.0, 1.0, 1.0, min(v, 1.0));
+    }
+    return vec4f(0.0, 0.0, 0.0, min(-v, 1.0));
+}
+
 struct VertexOutput {
     @builtin(position) clip_position: vec4f,
     @location(0) color: vec4f,
@@ -136,32 +394,43 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
         }
     }
 
+    // SDF-lit plate batch (mode in the push constants; see plate_shade).
+    if (rrect_clip.rect1.z > 0.5) {
+        return plate_shade(in.clip_position.xy, in.color);
+    }
+
     // Blur-behind plate: negative alpha mixes the (blurred) backdrop with the
     // plate color at |alpha| opacity.
     if (in.color.a < 0.0) {
-        let tex_size = vec2f(textureDimensions(t_backdrop));
-        let clean_backdrop = textureSample(t_backdrop, s_backdrop, in.clip_position.xy / tex_size);
-
-        var blurred = vec4f(0.0);
-        var total_weight = 0.0;
-
-        // 7x7 Gaussian blur kernel
-        for (var x = -3.0; x <= 3.0; x += 1.0) {
-            for (var y = -3.0; y <= 3.0; y += 1.0) {
-                let offset = vec2f(x, y) * 2.0; // sample every 2 pixels for a wider blur
-                let sample_uv = (in.clip_position.xy + offset) / tex_size;
-                let weight = exp(-(x*x + y*y) / (2.0 * 2.0 * 2.0));
-                blurred += textureSample(t_backdrop, s_backdrop, sample_uv) * weight;
-                total_weight += weight;
-            }
-        }
-
-        let backdrop_color = blurred / total_weight;
-        let opacity = -in.color.a;
-        let plate_color = vec4f(in.color.rgb, 1.0);
-        let blurred_plate = mix(backdrop_color, plate_color, opacity);
-        return mix(clean_backdrop, blurred_plate, opacity);
+        return resolve_blur(in.clip_position.xy, in.color);
     }
 
     return in.color;
 }
+
+// Blur-behind resolve for a negative-alpha plate color: the (blurred) backdrop
+// mixed with the plate color at |alpha| opacity.
+fn resolve_blur(pos: vec2f, color: vec4f) -> vec4f {
+    let tex_size = vec2f(textureDimensions(t_backdrop));
+    let clean_backdrop = textureSample(t_backdrop, s_backdrop, pos / tex_size);
+
+    var blurred = vec4f(0.0);
+    var total_weight = 0.0;
+
+    // 7x7 Gaussian blur kernel
+    for (var x = -3.0; x <= 3.0; x += 1.0) {
+        for (var y = -3.0; y <= 3.0; y += 1.0) {
+            let offset = vec2f(x, y) * 2.0; // sample every 2 pixels for a wider blur
+            let sample_uv = (pos + offset) / tex_size;
+            let weight = exp(-(x*x + y*y) / (2.0 * 2.0 * 2.0));
+            blurred += textureSample(t_backdrop, s_backdrop, sample_uv) * weight;
+            total_weight += weight;
+        }
+    }
+
+    let backdrop_color = blurred / total_weight;
+    let opacity = -color.a;
+    let plate_color = vec4f(color.rgb, 1.0);
+    let blurred_plate = mix(backdrop_color, plate_color, opacity);
+    return mix(clean_backdrop, blurred_plate, opacity);
+}
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 81a8a9e..e2ec697 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -33,6 +33,9 @@ pub struct Button {
     pub justify: Justification,
     label: Option<String>,
     hovered: bool,
+    /// Raised style: the background is an SDF-lit `Bevel` plate — fill plus a
+    /// rolled, lit edge — instead of a flat fill + border stroke.
+    raised: bool,
 }
 
 impl std::fmt::Debug for Button {
@@ -63,6 +66,7 @@ impl Button {
             justify: Justification::Center,
             label: None,
             hovered: false,
+            raised: false,
         }
     }
 
@@ -121,6 +125,12 @@ impl Button {
 /// `Adapted::with_label`, which syncs the model's copy via `Paint::sync_label`).
 impl Adapted<Button> {
 
+    /// Raised style: see the `raised` field.
+    pub fn with_raised(mut self, raised: bool) -> Self {
+        self.raised = raised;
+        self
+    }
+
     pub fn with_selected(mut self, selected: bool) -> Self {
         self.selected = selected;
         self
@@ -251,6 +261,18 @@ impl Paint for Button {
         let radius = crate::layout::button_corner_radius();
         let color = self.color();
 
+        // Raised style: one lit Bevel plate owns fill and edge — the rolled,
+        // lit rim replaces the flat border stroke. A transparent fill degrades
+        // to a Boss: edges-only, the plate below is the button's face (an
+        // opaque hover_color restores the filled bevel on hover).
+        if self.raised {
+            let depth = crate::layout::bevel_width().min(h * 0.2);
+            if color[3] > 0.001 {
+                ctx.bevel(rect, (radius, radius, radius, radius), color, depth);
+            } else {
+                ctx.boss(rect, (radius, radius, radius, radius), depth);
+            }
+        } else
         // Background (+ optional configured border), split by radius exactly as the legacy
         // `all_rounded_quads` (rounded) / `extra_quads` (square) overrides emitted it.
         if radius > 0.0 {
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 0dd87aa..61d43ff 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -226,6 +226,9 @@ pub struct Toggle {
     /// Where the label sits across the pill. Mirrors `Button::justify` — same enum, same
     /// 8px edge inset — so the two read as one control set wherever they share a column.
     justify: Justification,
+    /// Raised style: the background is an SDF-lit `Bevel` plate (fill + rolled
+    /// lit edge) instead of a flat fill; the state gradient composites on top.
+    raised: bool,
 }
 
 impl Toggle {
@@ -237,6 +240,7 @@ impl Toggle {
             hovered: false,
             focused: false,
             justify: Justification::Center,
+            raised: false,
         })
     }
 
@@ -271,6 +275,12 @@ impl Adapted<Toggle> {
         self.justify = justify;
         self
     }
+
+    /// Raised style: see the `raised` field.
+    pub fn with_raised(mut self, raised: bool) -> Self {
+        self.raised = raised;
+        self
+    }
 }
 
 impl Layout for Toggle {
@@ -311,7 +321,17 @@ impl Paint for Toggle {
         let radius = crate::layout::toggle_corner_radius();
         let bg = colors::toggle_bg_color();
 
-        if radius > 0.0 {
+        if self.raised {
+            // One lit Bevel plate owns fill and edge; the state gradient below
+            // composites over it. Transparent fill degrades to a Boss (edges
+            // only — the plate below is the face).
+            let depth = crate::layout::bevel_width().min(h * 0.2);
+            if bg[3] > 0.001 {
+                ctx.bevel(rect, (radius, radius, radius, radius), bg, depth);
+            } else {
+                ctx.boss(rect, (radius, radius, radius, radius), depth);
+            }
+        } else if radius > 0.0 {
             ctx.rounded_rect(rect, radius, (true, true, true, true), bg);
         } else {
             ctx.quad(rect, bg);
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 8ddc21c..d4b087b 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -109,6 +109,9 @@ pub struct Dropdown {
     /// `Backplate` ancestor — the hook that keeps the adjustment after an app dissolves its
     /// root Backplate (the walk finds nothing once the widget is parentless).
     corner_frame: Option<((f32, f32, f32, f32), f32, (bool, bool, bool, bool))>,
+    /// Raised style: the closed control's background is an SDF-lit `Bevel`
+    /// plate (fill + rolled lit edge) instead of a flat fill + border stroke.
+    raised: bool,
 }
 
 impl Dropdown {
@@ -127,6 +130,7 @@ impl Dropdown {
             label: None,
             hovered: false,
             corner_frame: None,
+            raised: false,
         })
     }
 
@@ -279,11 +283,27 @@ impl Dropdown {
         let y = content.y;
         let visual_h = content.height;
 
-        let mut bg_color = colors::dropdown_background_color();
+        let raw_bg = colors::dropdown_background_color();
+        let mut bg_color = raw_bg;
         bg_color[3] = 1.0; // Force opaque background to prevent subpixel blending artifacts
         let border_color = self.border_color();
 
         let radius = crate::layout::dropdown_corner_radius();
+        // Raised style: one lit Bevel plate owns fill and edge (the concentric
+        // corner_frame adjustment keeps the legacy path — it exists to nest
+        // flat outlines, which a rolled edge replaces). A transparent
+        // configured fill degrades to a Boss: edges only, plate as the face —
+        // judged on the RAW alpha, before the opacity force above.
+        if self.raised && self.corner_frame.is_none() {
+            let depth = crate::layout::bevel_width().min(visual_h * 0.2);
+            let r4 = (radius, radius, radius, radius);
+            if raw_bg[3] > 0.001 {
+                ctx.bevel(Rect { x, y, width: w, height: visual_h }, r4, bg_color, depth);
+            } else {
+                ctx.boss(Rect { x, y, width: w, height: visual_h }, r4, depth);
+            }
+            return;
+        }
         if radius <= 0.0 {
             ctx.quad(Rect { x, y, width: w, height: visual_h }, border_color);
             ctx.quad(
@@ -547,6 +567,12 @@ impl Dropdown {
 }
 
 impl Adapted<Dropdown> {
+    /// Raised style: see the `raised` field.
+    pub fn with_raised(mut self, raised: bool) -> Self {
+        self.raised = raised;
+        self
+    }
+
     pub fn with_custom_display_text(mut self, text: &str) -> Self {
         self.custom_display_text = Some(text.to_string());
         self
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index f84020f..67dca38 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -48,6 +48,10 @@ pub struct Slider {
     pub editor_state: TextEditorState,
     pub just_changed: bool,
     label: Option<String>,
+    /// Raised-track style: the track is drawn as a `Boss` outline — raised
+    /// rolled edges on the surface below — instead of a filled background, so
+    /// the plate's own color shows through the unfilled portion.
+    raised: bool,
 }
 
 impl Slider {
@@ -65,6 +69,7 @@ impl Slider {
             editor_state: TextEditorState::new(String::new()),
             just_changed: false,
             label: None,
+            raised: false,
         })
     }
 
@@ -167,6 +172,12 @@ impl Adapted<Slider> {
         self
     }
 
+    /// Raised-track style: see the `raised` field.
+    pub fn with_raised(mut self, raised: bool) -> Self {
+        self.raised = raised;
+        self
+    }
+
     pub fn with_value(mut self, val: f32) -> Self {
         self.set_value(val);
         self
@@ -224,8 +235,16 @@ impl Paint for Slider {
             }
         };
 
-        // Track.
-        rrect(Rect { x: g.track_x, y: g.y, width: g.track_w, height: g.h }, radius, rc, colors::slider_track(), ctx);
+        // Track. Raised style draws no background at all — the plate below is
+        // the track's surface; the ridge ring after the fill delimits it.
+        let track_rect = Rect { x: g.track_x, y: g.y, width: g.track_w, height: g.h };
+        // Ridge wall width for the raised style: capped well below the bar
+        // height, since the ring needs FOUR wall spans (up+down, top+bottom)
+        // plus a usable channel between them.
+        let ridge_t = crate::layout::bevel_width().min(g.h * 0.2);
+        if !self.raised {
+            rrect(track_rect, radius, rc, colors::slider_track(), ctx);
+        }
 
         // Readout box (+ focus border) and its text.
         if self.show_readout {
@@ -252,25 +271,57 @@ impl Paint for Slider {
             ctx.text(text, rx + 8.0, crate::layout::align_text_y(g.y, g.h, 12.0, 0.0), 12.0, [0xee, 0xee, 0xf0]);
         }
 
-        // Fill up to the thumb center.
+        // Fill up to the thumb center. Raised style insets the fill into the
+        // channel (past the falling inner wall), so the liquid sits in the
+        // valley instead of painting over the ridge.
         let thumb_x = g.track_x + self.value * (g.track_w - g.thumb_size);
         if let Some(fill_color) = colors::slider_fill() {
-            let fill_w = (thumb_x + g.thumb_size / 2.0 - g.track_x).max(0.0).min(g.track_w);
-            let fill_rad = if rounded { radius.min(g.h / 2.0) } else { radius };
-            rrect(Rect { x: g.track_x, y: g.y, width: fill_w, height: g.h }, fill_rad, (true, true, true, true), fill_color, ctx);
+            let (fx, fy, fmax_w, fh) = if self.raised {
+                let inset = 1.5 * ridge_t;
+                (g.track_x + inset, g.y + inset, g.track_w - 2.0 * inset, g.h - 2.0 * inset)
+            } else {
+                (g.track_x, g.y, g.track_w, g.h)
+            };
+            let fill_w = (thumb_x + g.thumb_size / 2.0 - fx).max(0.0).min(fmax_w);
+            let fill_rad = if rounded { radius.min(fh / 2.0) } else { radius };
+            rrect(Rect { x: fx, y: fy, width: fill_w, height: fh }, fill_rad, (true, true, true, true), fill_color, ctx);
+        }
+
+        // Raised rim AFTER the fill so its shading modulates whatever it
+        // crosses: one Ridge prim riding the track boundary — up from the
+        // plate, crest, back down into the channel where the control sits.
+        // A single primitive on purpose: boss + inset recess stacks two
+        // shading passes and the crest reads far hotter than a plate edge.
+        if self.raised {
+            ctx.ridge(track_rect, (radius, radius, radius, radius), 2.0 * ridge_t);
         }
 
-        // Thumb.
+        // Thumb. A real Circle prim, not a full-radius rounded rect: rounded
+        // rects follow the DE-wide corner_shape family, and a squircle knob
+        // reads wrong — the thumb should stay round under any corner style.
         let thumb_y = g.y + (g.h - g.thumb_size) / 2.0;
         let thumb_color = if self.dragging { colors::slider_thumb_drag() } else { colors::slider_thumb() };
-        let thumb_rad = if rounded { g.thumb_size / 2.0 } else { 0.0 };
-        rrect(
-            Rect { x: thumb_x, y: thumb_y, width: g.thumb_size, height: g.thumb_size },
-            thumb_rad,
-            (true, true, true, true),
-            thumb_color,
-            ctx,
-        );
+        if rounded {
+            // Raised style: the knob sits IN the valley, so its diameter is the
+            // flat channel floor between the ridge walls — drawn size only; the
+            // drag/hit geometry keeps the full thumb_size.
+            let diameter =
+                if self.raised { g.h - 2.0 * ridge_t } else { g.thumb_size };
+            ctx.circle(
+                thumb_x + g.thumb_size / 2.0,
+                thumb_y + g.thumb_size / 2.0,
+                diameter / 2.0,
+                thumb_color,
+            );
+        } else {
+            rrect(
+                Rect { x: thumb_x, y: thumb_y, width: g.thumb_size, height: g.thumb_size },
+                0.0,
+                (true, true, true, true),
+                thumb_color,
+                ctx,
+            );
+        }
     }
 }
 
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 059269a..65b67da 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -83,6 +83,9 @@ pub struct TextBox {
     /// The laid-out base rect, cached from [`Layout::rect_assigned`] — the cursor/scroll math
     /// reads geometry between events, which the narrow traits don't otherwise carry.
     rect: Rect,
+    /// Recessed style: a `Recess` overlay is carved over the box's own fill —
+    /// an inset well, the input-direction counterpart of the raised controls.
+    recessed: bool,
 }
 
 impl TextBox {
@@ -122,6 +125,7 @@ impl TextBox {
             label: None,
             hovered: false,
             rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
+            recessed: false,
         })
     }
 
@@ -995,6 +999,12 @@ impl TextBox {
 }
 
 impl Adapted<TextBox> {
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
+
     pub fn with_update_on_type(mut self, update: bool) -> Self {
         self.update_on_type = update;
         self
@@ -1282,13 +1292,28 @@ impl Paint for TextBox {
 
             if self.draw_bg_border {
                 let corners = (true, true, true, true);
-                ctx.rounded_rect(Rect { x, y: self.rect.y + top, width: w, height: visual_h }, radius, corners, border_color);
-                ctx.rounded_rect(
-                    Rect { x: x + border_w, y: self.rect.y + top + border_w, width: w - 2.0 * border_w, height: visual_h - 2.0 * border_w },
-                    (radius - border_w).max(0.0),
-                    corners,
-                    bg_color,
-                );
+                // Recessed + transparent fill: the carve alone defines the
+                // well — the plate below is its floor, so the flat border and
+                // bg rects are skipped entirely. An opaque fill (e.g. the edit
+                // color while editing) draws as usual and gets carved.
+                let bare = self.recessed && bg_color[3] <= 0.001;
+                if !bare {
+                    ctx.rounded_rect(Rect { x, y: self.rect.y + top, width: w, height: visual_h }, radius, corners, border_color);
+                    ctx.rounded_rect(
+                        Rect { x: x + border_w, y: self.rect.y + top + border_w, width: w - 2.0 * border_w, height: visual_h - 2.0 * border_w },
+                        (radius - border_w).max(0.0),
+                        corners,
+                        bg_color,
+                    );
+                }
+                if self.recessed {
+                    let depth = crate::layout::bevel_width().min(visual_h * 0.2);
+                    ctx.recess(
+                        Rect { x, y: self.rect.y + top, width: w, height: visual_h },
+                        (radius, radius, radius, radius),
+                        depth,
+                    );
+                }
             }
 
             let mut quads: Vec<(f32, f32, f32, f32, [f32; 4])> = Vec::new();
diff --git a/src/widget/model.rs b/src/widget/model.rs
index e9bcba9..54ae2f1 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1236,6 +1236,8 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 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, depth, edges } => ctx.recess_edges(rect, radii, depth, edges),
+                Prim::Boss { rect, radii, depth, edges } => ctx.boss_edges(rect, radii, depth, edges),
+                Prim::Ridge { rect, radii, depth, edges } => ctx.ridge_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),