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

commit402e088b1981db8438b3660b317a275da2a36b54
parent1707187463
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 14:13
refactor: the toolkit names no app

- The runner no longer reads the app id. Anything whose app_id began
  "cce-status" lost its client-side move, resize and resize cursors and had
  its input region pinned to its launch size; the status bar and its OSD now
  say so through Application::standard_csd, and the input region is gone (a
  segment's menu grows the surface past it, and a row below the old region
  took the click before and after, checked in a shadow).
- SplitterLayout and CircularPaneLayout, the designer's pane geometry, moved
  into cce-designer.
- The relief editors cce-relief and cce-ramp, bins of this crate until now,
  are the cce-relief crate (github.com/lsgalante/cce-relief); their names and
  launchers are unchanged, and both draw identically to the pixel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md                    |   23 +-
 src/backend/driver.rs        |   16 +-
 src/backend/window_runner.rs |   16 -
 src/bin/cce-ramp.rs          |  426 --------
 src/bin/cce-relief.rs        | 2285 ------------------------------------------
 src/layout/mod.rs            |    2 -
 src/layout/panes.rs          |   96 --
 7 files changed, 20 insertions(+), 2844 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index d2c5c7a..684b2be 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -42,7 +42,8 @@ set outright.
 ## Build, test, run
 
 Use cargo directly (the `Makefile` wraps `cargo build --release` and
-`ccebuild install --no-build cce-ui`, which installs every bin: the demo, `cce-relief`, `cce-ramp`). Prefer `-p cce-ui` from anywhere in the workspace so you don't rebuild the compositor.
+`ccebuild install --no-build cce-ui`, which installs its one bin, the demo; the relief
+editors `cce-relief` and `cce-ramp` are the `cce-relief` crate since 2026-10-08). Prefer `-p cce-ui` from anywhere in the workspace so you don't rebuild the compositor.
 
 ```sh
 cargo build -p cce-ui                          # build the toolkit (+ demo binary)
@@ -608,6 +609,21 @@ its modules exited, the launcher's backoff grew while nobody was logged in, and
 StatusNotifierWatcher came back seconds after the next login — Dropbox, starting into the
 gap, reported no tray.
 
+### The toolkit names no app (since 2026-10-08)
+
+Nothing in cce-ui branches on WHICH app is running. The runner used to read the app id:
+anything whose `app_id` began `cce-status` lost its client-side move, resize and resize
+cursors, and had its input region pinned to its launch size. The status bar and its OSD
+now say so through the contract — `Application::standard_csd` returns false — and the
+input region went (the segments' menus grow the surface past it, and a row below the old
+region took the click before and after; checked in a shadow). The same sweep moved the
+designer's pane geometry (`SplitterLayout`, `CircularPaneLayout`) into cce-designer and
+the relief editors (`cce-relief`, `cce-ramp`, until then bins of this crate) into the
+`cce-relief` crate. When an app needs the runner to behave differently, add a defaulted
+`Application` hook; never test the app id. (Two widgets LAUNCH a DE app by name — the
+colour selector `cce-color-editor`, a default its host can replace, and the font selector
+`cce-fonts` — which is the DE's toolkit using the DE, not a branch on the app.)
+
 ### The runner works out each frame's damage (`backend::frame::derive_damage`, 2026-10-06)
 
 An app that does not report its own damage (`take_damage`, which only cce-grid does) no
@@ -1791,7 +1807,6 @@ cce-system-interface) to confirm behavior, not just the test suite.
     browse page is a `scene::layout` column now, and the container layouts
     (`widget::ContainerLayout`, the gallery's Layout exhibit) are a trait of their own,
     `layout` and `measure`, without the cursor. New layout is `scene::layout`.
-  - `panes.rs` — `SplitterLayout` and `CircularPaneLayout`, cce-designer's pane geometry.
 - `color/` — the colour model and named colours (`colors` re-export module in `lib.rs`),
   split the same way: `mod.rs` the constants, statics and getters; `load.rs` reading the
   config into them (and `retired_surface_keys`); `math.rs` sRGB/linear, OKLab and the
@@ -2160,7 +2175,7 @@ key nothing but the editor read. They live in that app's own
 `~/.config/cce/cce-relief/state.kdl` now, one `knobs` node per Save target
 (`shared`, a retargeted file's path, or `<path>#<key>` for a `(relief)`
 value — two materials must not seed each other's sliders; `knob_state` in
-`cce-relief.rs`). The registry keys `bevel_profile_knobs` /
+cce-relief's `src/main.rs`). The registry keys `bevel_profile_knobs` /
 `roll_profile_knobs` and their flatten arms are gone; a config that still
 carries a knob key seeds the editor once, off the raw file, and the next
 Save takes the key off under either spelling. The `(bevel)` type and
@@ -2177,7 +2192,7 @@ slider verbatim, a moved one converted through the same metric that seeded
 it — so a headless session never turns `(mm)0.3` into `(px)1.1339`, which
 it did until 2026-09-28), choosing a unit only for a height the config
 never had: `(mm)` when the metric is real, px otherwise
-(`height_len_for` in `cce-relief.rs`).
+(`height_len_for` in cce-relief's `src/main.rs`).
 
 **There is one roll width.** `style.surface.relief.width` is the run of every
 roll and wall: the root plate's perimeter (`PlateSpec::window`), a `PlateSpec`
diff --git a/src/backend/driver.rs b/src/backend/driver.rs
index 6b239e5..1838bf0 100644
--- a/src/backend/driver.rs
+++ b/src/backend/driver.rs
@@ -174,9 +174,6 @@ pub struct Driver {
     /// the plate-navigation group jump, for apps that opt in.
     pub group_next_chord: String,
     pub group_prev_chord: String,
-    /// Whether the app is the status bar, asked once (see
-    /// [`is_status_bar`](Self::is_status_bar)).
-    status_bar: std::cell::OnceCell<bool>,
 }
 
 impl Default for Driver {
@@ -227,18 +224,9 @@ impl Driver {
             redo_chord: crate::input::app_chord("redo", "ctrl+shift+z"),
             group_next_chord: crate::input::app_chord("focus_next_group", "ctrl+tab"),
             group_prev_chord: crate::input::app_chord("focus_prev_group", "ctrl+shift+tab"),
-            status_bar: std::cell::OnceCell::new(),
         }
     }
 
-    /// The app is the status bar (`app_id` `cce-status…`): no CSD move,
-    /// resize or resize cursors. Read from `settings()` once per session —
-    /// the checks it serves run on every pointer motion and press, and
-    /// `settings()` builds two `String`s each call.
-    fn is_status_bar<A: Application>(&self, app: &A) -> bool {
-        *self.status_bar.get_or_init(|| app.settings().app_id.starts_with("cce-status"))
-    }
-
     /// Copy the modifiers into the app's widget context, where widgets read them.
     fn sync_mods<A: Application>(&self, app: &mut A) {
         if let Some(ctx) = app.ui_context_mut() {
@@ -263,8 +251,7 @@ impl Driver {
         if let Some(icon) = app.cursor_icon(lx, ly) {
             return icon;
         }
-        if self.is_status_bar(app)
-            || !app.standard_csd()
+        if !app.standard_csd()
             || !app.csd_resize_borders()
         {
             return CursorIcon::Default;
@@ -358,7 +345,6 @@ impl Driver {
         if site.can_grab
             && btn == MouseButton::Left
             && !site.on_popup
-            && !self.is_status_bar(t.app)
             && t.app.standard_csd()
         {
             // Resize borders off: the compositor's own band outside the
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index c017427..65ebd79 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -201,12 +201,6 @@ pub struct EngineState<A: Application> {
     /// [`Application::display_list_text`]).
     pub(crate) dl_text_items: Vec<DlText>,
 
-    /// The app is the status bar (`app_id` `cce-status…`): no CSD move,
-    /// resize or resize cursors. Read from `settings()` once per session —
-    /// the checks it serves run on every pointer motion and press, and
-    /// `settings()` builds two `String`s each call.
-    pub(crate) is_status_bar: bool,
-
     /// Drag-and-drop destination state (see [`crate::backend::dnd`]). The
     /// manager is absent when the compositor exposes no wl_data_device_manager;
     /// every drop path then no-ops.
@@ -2135,7 +2129,6 @@ fn run_session<'l, A: Application>(
         touch_offset: (0.0, 0.0),
         touch_scroll_at: None,
         dl_text_items: Vec::new(),
-        is_status_bar: false,
     };
 
     if let Err(e) = event_queue.roundtrip(&mut engine_state) {
@@ -2160,7 +2153,6 @@ fn run_session<'l, A: Application>(
     }
     let settings = inner.settings();
     crate::scale::set_app_id(settings.app_id.clone());
-    engine_state.is_status_bar = settings.app_id.starts_with("cce-status");
     engine_state.logical_width = settings.width as f32;
     engine_state.logical_height = settings.height as f32;
     engine_state.inner = Some(inner);
@@ -2185,14 +2177,6 @@ fn run_session<'l, A: Application>(
     surface.set_buffer_scale(buffer_scale);
     engine_state.committed_buffer_scale = buffer_scale;
 
-    if engine_state.is_status_bar {
-        let compositor = engine_state.compositor_state.wl_compositor();
-        let region = compositor.create_region(&qh, ());
-        region.add(0, 0, settings.width as i32, settings.height as i32);
-        surface.set_input_region(Some(&region));
-        region.destroy();
-    }
-
     let layer_settings = engine_state.inner.as_ref().unwrap().layer();
     if let Some(ls) = layer_settings {
         engine_state.is_layer_app = true;
diff --git a/src/bin/cce-ramp.rs b/src/bin/cce-ramp.rs
deleted file mode 100644
index 595be7d..0000000
--- a/src/bin/cce-ramp.rs
+++ /dev/null
@@ -1,426 +0,0 @@
-//! `cce-ramp` — a minimal popup hosting the [`Ramp`] widget in isolation, for
-//! iterating on the widget's look without driving a full client around it.
-//! `make install` puts it on PATH; run it inside a Wayland session. Edits print
-//! their ramp spec to stdout, so the popup doubles as a curve scratchpad.
-//!
-//! `--key <dotted.key>` (with optional `--config <path>`, default the shared
-//! config.kdl) turns the scratchpad into the `(ramp)` VALUE editor: the curve
-//! seeds from that key's ramp spec, Save writes the spec back to that one key
-//! with the `(ramp)` annotation, and Cancel closes without saving — the
-//! `cce-relief --key` convention. cce-data-editor spawns it this way from a
-//! ramp value's inline preview.
-//!
-//! Architecture mirrors the reference `DemoApp` (`src/main.rs`): display-list
-//! frame, routed events, in-frame popovers.
-
-use cce_ui::widget::Owned;
-use cce_ui::engine::{Application, AppSender, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::scene::layout::Rect;
-use cce_ui::scene::paint::{DisplayList, PaintCtx};
-use cce_ui::widget::{
-    Adapted, Button, ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, Ramp,
-    WidgetHost,
-};
-
-/// Transparent rim between the surface edge and the plate: room for the ramp's
-/// key pegs (r=28, +45 selected halo) to render outside the window frame
-/// instead of being clipped at the buffer edge.
-const OVERFLOW_MARGIN: f32 = 40.0;
-
-#[derive(Debug, Clone)]
-enum RampMsg {
-    Exit,
-}
-
-struct RampPopup {
-    ramp: Owned<Adapted<Ramp>>,
-    /// Last spec printed to stdout — edits log their curve for copy/paste.
-    last_spec: String,
-    /// `--key` mode only; parked off-screen in the scratchpad.
-    save_button: Owned<Adapted<Button>>,
-    cancel_button: Owned<Adapted<Button>>,
-    /// `--key <dotted.key>`: Save writes the spec as a `(ramp)` value at
-    /// this key; the curve seeds from it. None = the stdout scratchpad.
-    target_key: Option<String>,
-    config_path: std::path::PathBuf,
-    /// Status line under the buttons (key mode): what the last save did.
-    status: String,
-    /// Set by the cancel click in `drain_changes` (no exit access there);
-    /// `handle_mouse_input` turns it into `RampMsg::Exit`.
-    exit_requested: bool,
-    ui_context: cce_ui::context::UiContext,
-    width: u32,
-    height: u32,
-    scale_factor: f64,
-    needs_rebuild: bool,
-    registered: bool,
-}
-
-impl RampPopup {
-    fn drain_changes(&mut self) {
-        let spec = self.ramp.inner().spec_string();
-        if spec != self.last_spec {
-            println!("{spec}");
-            self.last_spec = spec;
-            self.needs_rebuild = true;
-        }
-        if self.save_button.take_click() {
-            self.save_to_key();
-            self.needs_rebuild = true;
-        }
-        if self.cancel_button.take_click() {
-            // Discard-and-close: nothing persisted without Save.
-            self.exit_requested = true;
-        }
-    }
-
-    /// Persist the current curve as a `(ramp)` value at the target key.
-    fn save_to_key(&mut self) {
-        let Some(key) = self.target_key.clone() else { return };
-        let p = self.config_path.to_string_lossy().into_owned();
-        let spec = self.ramp.inner().spec_string();
-        let ok = cce_ui::config::write_config_value_typed(&p, &key, &spec, "style", Some("ramp"));
-        self.status = if ok {
-            println!("saved {key} -> {p}");
-            format!("Saved — {key} holds this curve.")
-        } else {
-            "Save FAILED — see config permissions.".to_string()
-        };
-    }
-}
-
-impl Application for RampPopup {
-    type Message = RampMsg;
-
-    fn create(_sender: AppSender<Self::Message>) -> Self {
-        cce_ui::scale::set_scale_factor(1.0);
-        let mut ramp = Ramp::new();
-
-        // `--key <dotted.key>` / `--config <path>`: edit one `(ramp)` value
-        // in place — seed the curve from it, Save writes it back.
-        let mut config_path = cce_ui::config::get_config_path();
-        let mut target_key: Option<String> = None;
-        let args: Vec<String> = std::env::args().collect();
-        let mut i = 1;
-        while i < args.len() {
-            if args[i] == "--config" && i + 1 < args.len() {
-                config_path = std::path::PathBuf::from(&args[i + 1]);
-                i += 1;
-            } else if args[i] == "--key" && i + 1 < args.len() {
-                target_key = Some(args[i + 1].clone());
-                i += 1;
-            }
-            i += 1;
-        }
-        if let Some(key) = &target_key {
-            let seed = std::fs::read_to_string(&config_path)
-                .ok()
-                .map(|c| cce_ui::config::parse_kdl_to_json(&c))
-                .and_then(|v| v.pointer(&format!("/{}", key.replace('.', "/"))).cloned())
-                .and_then(|v| v.as_str().map(String::from));
-            if let Some(spec) = seed {
-                ramp.inner_mut().set_spec(&spec);
-            }
-        }
-
-        let last_spec = ramp.inner().spec_string();
-        Self {
-            ramp: Owned::new(ramp),
-            last_spec,
-            save_button: Owned::new(Button::new(0.0, 0.0, 0.0, 0.0).with_label("Save")),
-            cancel_button: Owned::new(Button::new(0.0, 0.0, 0.0, 0.0).with_label("Cancel")),
-            status: match &target_key {
-                Some(k) => format!("Edits are live in the curve; Save writes the {k} key."),
-                None => String::new(),
-            },
-            target_key,
-            config_path,
-            exit_requested: false,
-            ui_context: cce_ui::context::UiContext::new(),
-            width: 540,
-            height: 420,
-            scale_factor: 1.0,
-            needs_rebuild: true,
-            registered: false,
-        }
-    }
-
-    // Buffer-larger-than-geometry mode: the runner publishes the plate rect
-    // as the xdg window geometry + input region, so key pegs painted on the
-    // rim render outside the window frame and clicks there fall through.
-    fn overflow_margin(&self) -> u32 {
-        OVERFLOW_MARGIN as u32
-    }
-
-    fn settings(&self) -> WindowSettings {
-        let title = match &self.target_key {
-            Some(k) => {
-                let parts: Vec<&str> = k.split('.').collect();
-                format!("Ramp — {}", parts[parts.len().saturating_sub(2)..].join("."))
-            }
-            None => "Ramp".to_string(),
-        };
-        // The key mode adds a Save/Cancel row under the curve.
-        let extra = if self.target_key.is_some() { 56 } else { 0 };
-        WindowSettings {
-            title,
-            app_id: "cce-ramp".to_string(),
-            width: 460,
-            height: 340 + extra,
-            fullscreen: false,
-            min_size: Some((420, 300 + extra)),
-        }
-    }
-
-    fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
-        match msg {
-            RampMsg::Exit => *exit = true,
-        }
-    }
-
-    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
-        if self.ui_context.tick(dt) {
-            // Tick-driven edits (hover-scroll glide) log their spec too.
-            self.drain_changes();
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
-        if !self.registered {
-            self.registered = true;
-            self.ui_context.register_host(&mut self.ramp);
-            self.ui_context.register_host(&mut self.save_button);
-            self.ui_context.register_host(&mut self.cancel_button);
-        }
-
-        let size_changed = self.width != size.width as u32
-            || self.height != size.height as u32
-            || self.scale_factor != scale;
-        if self.needs_rebuild || size_changed {
-            self.width = size.width as u32;
-            self.height = size.height as u32;
-            self.scale_factor = scale;
-            cce_ui::scale::set_scale_factor(scale as f32);
-
-            // One widget, one rect: the ramp fills the plate inside half the
-            // DE pad (this popup runs tighter than a full client). The plate
-            // itself is inset by OVERFLOW_MARGIN so key pegs can render past
-            // the window frame into the transparent surface rim. Key mode
-            // reserves a Save/Cancel band under the curve.
-            let pad = OVERFLOW_MARGIN + cce_ui::layout::root_plate_padding() / 2.0;
-            let band = if self.target_key.is_some() { 56.0 } else { 0.0 };
-            self.ramp.set_rect(
-                pad,
-                pad,
-                (self.width as f32 - 2.0 * pad).max(0.0),
-                (self.height as f32 - 2.0 * pad - band).max(0.0),
-            );
-            if self.target_key.is_some() {
-                let by = self.height as f32 - pad - 30.0;
-                self.save_button.set_rect(pad, by, 96.0, 28.0);
-                self.cancel_button.set_rect(pad + 96.0 + 12.0, by, 96.0, 28.0);
-            } else {
-                self.save_button.set_rect(-1000.0, -1000.0, 1.0, 1.0);
-                self.cancel_button.set_rect(-1000.0, -1000.0, 1.0, 1.0);
-            }
-
-            self.needs_rebuild = false;
-            self.ui_context.rebuild_spatial_grid();
-        }
-
-        self.ui_context.clear_popovers();
-        if self.ramp.popover_rect().is_some() {
-            self.ui_context.register_popover(&mut self.ramp);
-        }
-
-        let mut pc = PaintCtx::new();
-        let (w, h) = (self.width as f32, self.height as f32);
-
-        // The window plate (the DemoApp idiom): page-low color at the configured
-        // opacity, config corner radius, rolled perimeter — inset by the
-        // overflow margin so widget content (the ramp's key pegs) can spill
-        // past the frame onto the transparent rim.
-        let mut plate = cce_ui::color::page_low_color();
-        if plate[3] > 0.001 {
-            // Half the DE opacity: this popup reads better mostly-glass.
-            plate[3] = cce_ui::color::root_plate_opacity() * 0.5;
-        }
-        let radius = cce_ui::colors::root_plate_corner_radius();
-        let bevel = cce_ui::layout::bevel_width();
-        let m = OVERFLOW_MARGIN;
-        pc.plate(
-            Rect { x: m, y: m, width: w - 2.0 * m, height: h - 2.0 * m },
-            (radius, radius, radius, radius),
-            &cce_ui::scene::Material::from_fill(plate),
-            bevel,
-        );
-
-        cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.ramp, &mut pc);
-        if self.target_key.is_some() {
-            cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.save_button, &mut pc);
-            cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.cancel_button, &mut pc);
-            if !self.status.is_empty() {
-                let pad = OVERFLOW_MARGIN + cce_ui::layout::root_plate_padding() / 2.0;
-                pc.text_with(
-                    self.status.clone(),
-                    pad + 2.0 * (96.0 + 12.0),
-                    self.height as f32 - pad - 24.0,
-                    12.0,
-                    [0x9a, 0x9a, 0xa4],
-                    None,
-                    None,
-                );
-            }
-        }
-
-        // The ramp's field-dropdown popover, drawn into the frame on top.
-        if let Some((px, py, pw, ph)) = self.ramp.popover_rect() {
-            let mut coll = cce_ui::layout::PopoverCollector::new();
-            self.ramp.inner().preset_dropdown.render_popover(&mut coll);
-            self.ramp.inner().line_type_dropdown.render_popover(&mut coll);
-            for &(c, x, y, qw, qh) in &coll.rects {
-                pc.quad(Rect { x, y, width: qw, height: qh }, c);
-            }
-            let bounds = Some([px, py, px + pw, py + ph]);
-            for (content, size, tx, ty, color, font, _b) in coll.texts {
-                let color_u8 = [
-                    (color[0] * 255.0).clamp(0.0, 255.0) as u8,
-                    (color[1] * 255.0).clamp(0.0, 255.0) as u8,
-                    (color[2] * 255.0).clamp(0.0, 255.0) as u8,
-                ];
-                pc.text_with(content, tx, ty, size, color_u8, font, bounds);
-            }
-        }
-
-        // The shared context menu (right-click on the graph), drawn last, on
-        // top of everything. Its labels carry the menu rect as bounds — the
-        // runner exempts them from the menu's own text occlusion that way.
-        // The lit plate and the menu font, in one call — the flat quads and
-        // a family-less label loop drew this menu square, opaque and in the
-        // default sans, unlike every app's.
-        cce_ui::widget::context_menu::paint_with_labels(&mut pc);
-
-        Some(pc.finish())
-    }
-
-    fn display_list_text(&self) -> bool {
-        true
-    }
-
-    fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
-        Some(&self.ui_context)
-    }
-
-    fn is_movable_root_plate_at(&self, px: f32, py: f32) -> bool {
-        self.ui_context.drag_allowed_at(px, py)
-    }
-
-    fn clear_color(&self) -> [f32; 4] {
-        [0.0, 0.0, 0.0, 0.0]
-    }
-
-    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
-        if self.ui_context.cursor_moved_context_menu(pos.x, pos.y) {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-        let ev = Event::PointerMove { x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
-        let changed = self.ui_context.propagate_event(&ev, self.ramp.id());
-        self.drain_changes();
-        if changed || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn handle_mouse_input(
-        &mut self,
-        button: MouseButton,
-        state: ElementState,
-        pos: LogicalPosition,
-        needs_rebuild: &mut bool,
-    ) -> Option<Self::Message> {
-        // The open context menu owns the press (item dispatch / dismiss).
-        if self.ui_context.mouse_input_context_menu(button, state, pos.x, pos.y) {
-            self.drain_changes();
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-            return None;
-        }
-        let ev = Event::MouseButton {
-            button,
-            state,
-            x: pos.x,
-            y: pos.y,
-            local_x: pos.x,
-            local_y: pos.y,
-        };
-        let mut changed = self.ui_context.propagate_event(&ev, self.ramp.id());
-        changed |= self.ui_context.propagate_event(&ev, self.save_button.id());
-        changed |= self.ui_context.propagate_event(&ev, self.cancel_button.id());
-        self.drain_changes();
-        if self.exit_requested {
-            return Some(RampMsg::Exit);
-        }
-        if changed || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-        None
-    }
-
-    fn handle_mouse_wheel(
-        &mut self,
-        delta: &MouseScrollDelta,
-        pos: LogicalPosition,
-        needs_rebuild: &mut bool,
-    ) {
-        let ev = Event::MouseWheel {
-            delta: *delta,
-            x: pos.x,
-            y: pos.y,
-            local_x: pos.x,
-            local_y: pos.y,
-        };
-        let changed = self.ui_context.propagate_event(&ev, self.ramp.id());
-        self.drain_changes();
-        if changed || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn handle_key_input(
-        &mut self,
-        event: &KeyEvent,
-        needs_rebuild: &mut bool,
-    ) -> Option<Self::Message> {
-        use cce_ui::widget::{Key, NamedKey};
-        if event.state == ElementState::Pressed {
-            if event.logical_key == Key::Named(NamedKey::Escape) {
-                return Some(RampMsg::Exit);
-            }
-            if event.ctrl {
-                if let Key::Character(ref c) = event.logical_key {
-                    if c == "q" {
-                        return Some(RampMsg::Exit);
-                    }
-                }
-            }
-        }
-        let ev = Event::KeyInput(event.clone());
-        let handled = self.ui_context.propagate_event(&ev, self.ramp.id());
-        self.drain_changes();
-        if handled || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-        None
-    }
-}
-
-fn main() {
-    cce_ui::engine::run::<RampPopup>();
-}
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
deleted file mode 100644
index 857f19d..0000000
--- a/src/bin/cce-relief.rs
+++ /dev/null
@@ -1,2285 +0,0 @@
-//! `cce-relief` — the relief-material control interface. A SHAPE from the
-//! `scene::paint` relief family (recess, boss, ridge, trough, groove, fillet,
-//! plate edge roll, and the composed inset plate) is shown as a lit
-//! CROSS-SECTION of the actual edge, and the curve its walls are cut with is
-//! shaped by three semantic sliders (Shoulder / Base / Bias) instead of a
-//! free-form ramp.
-//!
-//! Named for the family, not for one member: `scene::paint`'s `Prim` doc
-//! reserves "bevel" for the `Bevel` prim and the shared edge treatment, and
-//! calls the family relief primitives — which is also what `control_relief`
-//! gates and what the `relief` config node is called. This was `cce-bevel`
-//! until the shape picker landed and made the mismatch untenable.
-//!
-//! Under the section runs the SHADING STRIP: the same shape put through
-//! `scene::relief_shade`, the shader's own arithmetic in Rust, composited one
-//! carve at a time in emission order. The section says what the shape is; the
-//! strip says what it will look like. A composed shape gets one pass per carve
-//! there, which is how a stack whose geometry looks reasonable can still read
-//! hot.
-//!
-//! The knobs under the section: the wall curve's Shoulder / Base / Bias,
-//! then **Light** (`style.surface.relief.light`, the `bevel_depth` registry
-//! slot — how hard the light falls across the wall; not a length), **Width**
-//! (the wall's run, logical px) and
-//! **Height** (the wall's drop, logical px; 0 = follow the width at the
-//! analytic ratio). Height is the fabrication axis: the section's depth
-//! numbers read in millimetres whenever the display metric is real
-//! (`cce_ui::units`), and Save writes `style.surface.relief.wall.height` in
-//! the UNIT the config already spells — a `(mm)0.3` stays millimetres on a
-//! headless session, and an untouched slider writes the seed back verbatim
-//! — choosing a unit only for a height the config never had: `(mm)` when
-//! the metric is real, px when it is only assumed (`height_len_for`).
-//!
-//! Every edit applies live to this process (the
-//! popup's own plate, wells, and buttons ARE the preview) and logs the
-//! sampled spec to stdout; Save persists to `~/.config/cce/config.kdl`
-//! (`style.surface.relief`) so every cce app starts with the material —
-//! or, with `--config <path>`, to that file instead (a per-app override
-//! like cce-designer's), which also seeds the depth/width on open.
-//!
-//! The Shoulder / Base / Bias knob triples are NOT material: they are this
-//! editor's slider positions behind the profile specs, which nothing else
-//! reads. Since 2026-09-28 they live in this app's own state file
-//! (`~/.config/cce/cce-relief/state.kdl`, one `knobs` node per Save
-//! target — see `knob_state`), not in the style block beside the values
-//! that draw. A config that still carries them (`relief.wall.knobs` /
-//! `edge.knobs`, or the older `profile_knobs` / `edge_knobs`) seeds the
-//! sliders once, and the next Save takes the keys off the file.
-//!
-//! `--key <dotted.key>` edits a single `(relief)` VALUE in place instead
-//! (`cce_ui::relief_spec::ReliefSpec` — width/depth/wall knobs/wall
-//! profile folded into one string), e.g.
-//! `cce-relief --key style.surface.desktop.line_relief` for the desktop
-//! grid's lines. Seeds come from that key, edits still preview live, and
-//! Save rewrites only that key — the DE-wide material is untouched. The
-//! edge section is not part of a `(relief)` value; a feature material has
-//! one wall curve.
-//!
-//! The curve family is the two-exponent rational ease
-//! `h(w) = w^a / (w^a + (1-w)^b)` over a bias pre-warp `w = v^g` — monotone,
-//! endpoint-exact, with the shoulder (a) and base fillet (b) shaped
-//! independently. Slider midpoints give a=b=2, g=1: the analytic smoothstep.
-//! Curves are sampled into ramp-spec keys, so the config format and the
-//! DE-wide loader are unchanged — free-form specs from cce-designer or a
-//! hand-edited config still load everywhere.
-
-use cce_ui::widget::Owned;
-use cce_ui::engine::{Application, AppSender, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::layout::RELIEF_PROFILE_IDENTITY_SPEC as IDENTITY_SPEC;
-use cce_ui::scene::layout::Rect;
-use cce_ui::scene::paint::{Cap, DisplayList, PaintCtx};
-use cce_ui::scene::relief_shade::{self, CarveMode};
-use cce_ui::widget::{
-    Adapted, Button, Dropdown, ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta,
-    Slider, WidgetHost, WidgetId,
-};
-
-const HEADER_FONT_SIZE: f32 = 13.0;
-const HEADER_COLOR: [u8; 3] = [0x9a, 0x9a, 0xa4];
-
-/// Knob ranges: light (how hard the light falls across the wall — the
-/// `bevel_depth` slot, NOT a length), width (how far the wall runs, logical
-/// px) and height (how far it drops, logical px; 0 = follow the width at
-/// the analytic ratio, the pre-height look). Defaults per
-/// `layout::bevel_depth` / `bevel_width` / `bevel_height`.
-const DEPTH_RANGE: (f32, f32) = (0.0, 0.6);
-const WIDTH_RANGE: (f32, f32) = (2.0, 24.0);
-const HEIGHT_RANGE: (f32, f32) = (0.0, 24.0);
-/// The finish's three fixed terms and the frost recipe — the material
-/// sections (RFC material step 4). Specular strength, shininess exponent,
-/// curvature/AO strength; luminance compression, rim refraction, blur sigma
-/// in logical px.
-const SPEC_RANGE: (f32, f32) = (0.0, 1.5);
-const SHINE_RANGE: (f32, f32) = (1.0, 64.0);
-const CURV_RANGE: (f32, f32) = (0.0, 1.0);
-const COMP_RANGE: (f32, f32) = (0.0, 1.0);
-const REFR_RANGE: (f32, f32) = (0.0, 1.0);
-const RADIUS_RANGE: (f32, f32) = (0.0, 20.0);
-
-/// Sample count for the spec written to config — enough that the 32-slot
-/// renderer LUT sees the curve, few enough that the config line stays sane.
-const SPEC_SAMPLES: usize = 17;
-
-/// Cutaway metrics, shared by `draw_section` and the layout's shrink-wrap:
-/// inner margin, the axis gutters (depth numbers left of the slab's cut
-/// face, run numbers under its underside), the plateau/floor minimum band,
-/// and the slab's underside room.
-const CUT_MARGIN: f32 = 8.0;
-const GUTTER_L: f32 = 34.0;
-const GUTTER_B: f32 = 16.0;
-const MIN_BAND: f32 = 36.0;
-const UNDERSIDE: f32 = 18.0;
-/// Height of the shading strip under the section — the band that shows what
-/// the shape will actually LOOK like, as opposed to what it is.
-const SHADE_STRIP_H: f32 = 14.0;
-
-/// The height this window WANTS at a given width: every fixed row, plus the
-/// cutaway at its natural (shrink-wrapped) height.
-///
-/// The window is CONTENT-SIZED. There is nothing here to drag to a different
-/// shape — the rows are fixed and the cutaway shrink-wraps its section — so a
-/// user-chosen height only ever adds dark space or squeezes the section's wall
-/// to a sliver, because the proportional axes are height-bound. Kept as one
-/// function so `settings()` asks for the same number the layout will use.
-///
-/// (The compositor may still restore a saved size over this; see the Utility
-/// window-mode proposal. Until then the request is only a request.)
-fn content_height(width: f32) -> f32 {
-    let pad = cce_ui::layout::root_plate_padding();
-    let w = (width - 2.0 * pad).max(0.0);
-    let gap = 14.0;
-    let strip = {
-        let (_, fsize) = cce_ui::layout::control_label_font_detached_parsed();
-        fsize + cce_ui::layout::control_label_margin()
-    };
-    let knob_h = 22.0 + strip;
-    let button_h = 26.0;
-    let status_h = HEADER_FONT_SIZE + 4.0;
-    let fixed = knob_h + gap
-        + 9.0 * (knob_h + gap)
-        + button_h + 8.0 + status_h + 2.0 * gap;
-    let natural = (w - 2.0 * CUT_MARGIN - GUTTER_L - 2.0 * MIN_BAND)
-        + 2.0 * CUT_MARGIN
-        + UNDERSIDE
-        + GUTTER_B
-        + 2.0 * SHADE_STRIP_H
-        + 6.0
-        + GUTTER_B;
-    2.0 * pad + fixed + natural
-}
-
-#[derive(Debug, Clone)]
-enum BevelMsg {
-    Exit,
-}
-
-/// Which of the two profile curves a shape's walls are drawn with — the thing
-/// the knobs actually edit. Several shapes share one curve (every box carve and
-/// both straddling shapes are the Wall curve), so the picker selects a SHAPE and
-/// the curve follows; the caption says which one you are editing.
-#[derive(Clone, Copy, PartialEq, Eq)]
-enum Curve {
-    Wall,
-    Edge,
-}
-
-/// Which wall of the rect the section is taken through.
-///
-/// Not cosmetic. A carve's shading depends on the angle between the wall's
-/// outward normal and the DE light, so ONE profile reads four different ways
-/// around a control — the reason a seam's two rims never match (measured on
-/// cce-files' pane gap: 187 grey one side, 125 the other, same carve).
-#[derive(Clone, Copy, PartialEq, Eq)]
-enum Edge {
-    Left,
-    Right,
-    Top,
-    Bottom,
-}
-
-impl Edge {
-    const ALL: [Edge; 4] = [Edge::Left, Edge::Right, Edge::Top, Edge::Bottom];
-
-    fn label(self) -> &'static str {
-        match self {
-            Edge::Left => "Left wall",
-            Edge::Right => "Right wall",
-            Edge::Top => "Top wall",
-            Edge::Bottom => "Bottom wall",
-        }
-    }
-
-    /// The SDF gradient at the wall: the unit vector pointing OUT of the carve.
-    fn facing(self) -> [f32; 2] {
-        match self {
-            Edge::Left => [-1.0, 0.0],
-            Edge::Right => [1.0, 0.0],
-            Edge::Top => [0.0, -1.0],
-            Edge::Bottom => [0.0, 1.0],
-        }
-    }
-
-    /// Does the wall run horizontally? Then its shading varies DOWN the band
-    /// rather than along it, and both bands must be sampled that way to stay
-    /// comparable to each other.
-    fn horizontal_wall(self) -> bool {
-        matches!(self, Edge::Top | Edge::Bottom)
-    }
-}
-
-/// A relief shape, as a cross-section. This is the `Prim` family from
-/// `scene::paint` — the point of the picker is that the section shows the shape
-/// you will actually emit, not an idealised wall standing in for all of them.
-///
-/// Two of these are COMPOSED rather than primitive, and they are the reason the
-/// picker exists: `InsetPlate` is what `PaintCtx::inset_plate` emits today (one
-/// `Trough`), and `InsetStacked` is what it emitted before cce-ui@`35f5183` (a
-/// `Recess` on an outset rect plus a `Boss` on the rect). Put them side by side
-/// and the old one's fault is visible as geometry: same dip depth, 1.5× the run,
-/// and a flat floor where the single evaluation comes to a point.
-#[derive(Clone, Copy, PartialEq, Eq)]
-enum Shape {
-    Recess,
-    Boss,
-    EdgeRoll,
-    Ridge,
-    Trough,
-    Groove,
-    Fillet,
-    InsetPlate,
-    InsetStacked,
-}
-
-impl Shape {
-    const ALL: [Shape; 9] = [
-        Shape::Recess,
-        Shape::Boss,
-        Shape::EdgeRoll,
-        Shape::Ridge,
-        Shape::Trough,
-        Shape::Groove,
-        Shape::Fillet,
-        Shape::InsetPlate,
-        Shape::InsetStacked,
-    ];
-
-    fn label(self) -> &'static str {
-        match self {
-            Shape::Recess => "Recess — carve, interior one step down",
-            Shape::Boss => "Boss — plateau, interior one step up",
-            Shape::EdgeRoll => "Plate edge — perimeter roll to the silhouette",
-            Shape::Ridge => "Ridge — raised crest on the boundary",
-            Shape::Trough => "Trough — sunken valley on the boundary",
-            Shape::Groove => "Groove — slab valley (width 0 = a V)",
-            Shape::Fillet => "Fillet — concave inside corner",
-            Shape::InsetPlate => "Inset plate — flush control seam",
-            Shape::InsetStacked => "Inset plate, STACKED (pre-35f5183)",
-        }
-    }
-
-    /// The curve whose knobs shape this section.
-    fn curve(self) -> Curve {
-        match self {
-            Shape::EdgeRoll => Curve::Edge,
-            _ => Curve::Wall,
-        }
-    }
-
-    /// Does the section end in a floor (material continues), or in air (past a
-    /// silhouette)? Only the plate's edge roll ends in air.
-    fn has_floor(self) -> bool {
-        self != Shape::EdgeRoll
-    }
-
-    /// The run the shape's walls occupy, in WALL WIDTHS. Everything but the
-    /// stacked inset is one wall wide; the stack is 1.5 because its two walls
-    /// only overlap by half.
-    fn run(self) -> f32 {
-        match self {
-            Shape::InsetStacked => 1.5,
-            Shape::Groove => 1.0 + GROOVE_FLOOR,
-            _ => 1.0,
-        }
-    }
-
-    /// Surface height at run position `t` (in wall widths, 0 at the first wall's
-    /// start). Units: 1.0 = one full wall drop DOWN into the material, negative
-    /// = up out of it, 0 = the surrounding surface. `None` is air.
-    fn height(self, p: &ProfileKnobs, t: f32) -> Option<f32> {
-        // The profile's height curve, clamped to its plateaus.
-        let h = |u: f32| -> f32 {
-            if u <= 0.0 {
-                0.0
-            } else if u >= 1.0 {
-                1.0
-            } else {
-                p.eval(u)
-            }
-        };
-        // A bump straddling the run: up the first half, back down the second,
-        // amplitude halved — the shader's ridge/trough construction, where one
-        // profile evaluation on the folded coordinate yields both walls.
-        let bump = |t: f32| -> f32 {
-            let w = (2.0 * t).min(2.0 - 2.0 * t).clamp(0.0, 1.0);
-            0.5 * h(w)
-        };
-        Some(match self {
-            Shape::Recess => h(t),
-            Shape::Boss => -h(t),
-            Shape::EdgeRoll => {
-                if t > 1.0 {
-                    return None;
-                }
-                h(t)
-            }
-            Shape::Ridge => -bump(t),
-            Shape::Trough | Shape::InsetPlate => bump(t),
-            Shape::Groove => {
-                // The trough with a flat floor of GROOVE_FLOOR spliced in at the
-                // bottom — `width` in `Prim::Groove`, which is 0 for a pure V.
-                if t < 0.5 {
-                    bump(t)
-                } else if t < 0.5 + GROOVE_FLOOR {
-                    0.5 * h(1.0)
-                } else {
-                    bump(t - GROOVE_FLOOR)
-                }
-            }
-            // Quarter arc, concave: the inside corner a box SDF cannot express.
-            Shape::Fillet => {
-                let u = t.clamp(0.0, 1.0);
-                1.0 - (1.0 - u * u).max(0.0).sqrt()
-            }
-            // The composed stack: a recess wall stepping DOWN over [0,1] and a
-            // boss wall stepping UP over [0.5,1.5]. They overlap across the
-            // middle half, which is where the old code shaded twice.
-            Shape::InsetStacked => h(t) - h(t - 0.5),
-        })
-    }
-
-    /// The carve(s) this shape emits, as (mode, run start, run end). The
-    /// shading strip composites these IN ORDER, exactly as the renderer
-    /// composites one prim's cover quad over another's — which is the whole
-    /// reason a stacked shape can read hot while its geometry looks fine.
-    ///
-    /// The plate's edge roll returns nothing: it is the shader's plate branch
-    /// (fill + roll + CSG features), not the free-carve branch this models, so
-    /// the strip says so rather than inventing a number.
-    fn walls(self) -> Vec<(CarveMode, f32, f32)> {
-        match self {
-            Shape::Recess => vec![(CarveMode::Recess, 0.0, 1.0)],
-            Shape::Boss => vec![(CarveMode::Boss, 0.0, 1.0)],
-            Shape::Ridge => vec![(CarveMode::Ridge, 0.0, 1.0)],
-            Shape::Trough | Shape::InsetPlate => vec![(CarveMode::Trough, 0.0, 1.0)],
-            // The groove rejoins the free-carve path as a recess on |distance to
-            // the line| - halfwidth; across the section that is a trough spread
-            // over the wider run.
-            Shape::Groove => vec![(CarveMode::Trough, 0.0, 1.0 + GROOVE_FLOOR)],
-            // The fillet rejoins the shared path as its flat equivalent.
-            Shape::Fillet => vec![(CarveMode::Recess, 0.0, 1.0)],
-            Shape::EdgeRoll => Vec::new(),
-            // The pre-35f5183 pair, in emission order.
-            Shape::InsetStacked => {
-                vec![(CarveMode::Recess, 0.0, 1.0), (CarveMode::Boss, 0.5, 1.5)]
-            }
-        }
-    }
-
-    /// For a composed shape, the run interval where TWO walls are live at once
-    /// — the band that gets two lighting evaluations instead of one, and so the
-    /// band that reads hot. `None` for a primitive shape, which has one wall
-    /// everywhere by construction.
-    ///
-    /// Drawn as a band rather than as the two contributing curves on purpose:
-    /// plotted raw, the second wall's own excursion runs a full drop the other
-    /// way and leaves the section entirely, which forces the view to zoom out
-    /// far enough to shrink the composite you actually came to look at.
-    fn overlap(self) -> Option<(f32, f32)> {
-        match self {
-            Shape::InsetStacked => Some((0.5, 1.0)),
-            _ => None,
-        }
-    }
-}
-
-/// Flat floor spliced into the `Groove` section, in wall widths — `Prim::Groove`'s
-/// `width`, shown non-zero so the knob's effect is visible (0 would render as a V
-/// identical to `Trough`, whose difference is the slab SDF, not the section).
-const GROOVE_FLOOR: f32 = 0.45;
-
-/// One profile section's shape state: the three knob sliders plus whether
-/// the profile has diverged from the analytic default.
-struct ProfileKnobs {
-    shoulder: Owned<Adapted<Slider>>,
-    base: Owned<Adapted<Slider>>,
-    bias: Owned<Adapted<Slider>>,
-    /// False until a knob moves or config installed a real (non-identity)
-    /// profile for this section: the DE renders its analytic profile and
-    /// Save writes the identity sentinel.
-    custom: bool,
-    /// Last spec applied+logged.
-    last_spec: String,
-}
-
-impl ProfileKnobs {
-    /// `installed` is whether the live material actually carries a custom
-    /// LUT for this profile (`layout::*_profile_slopes().is_some()` — the
-    /// shader's own condition). It is NOT the same as "the state file
-    /// carried saved knobs": Save remembers the knob triples even for an
-    /// untouched section (so the editor reopens where it was left), while
-    /// writing the identity SPEC — which installs nothing. Seeding `custom`
-    /// from the knobs' presence made the prediction follow the knob curve
-    /// while the renderer ran analytic. The wall curve hid it (the knob
-    /// midpoints ARE the analytic smoothstep); the roll exposed it (the knob
-    /// family is nothing like the superellipse quadrant) — and a Save from
-    /// that state would have installed a smoothstep roll DE-wide unasked.
-    fn new(seed: Option<(f32, f32, f32)>, installed: bool) -> Self {
-        let (s, b, c) = seed.unwrap_or((0.5, 0.5, 0.5));
-        let knob = |v: f32, label: &str| {
-            Slider::new()
-                .with_label(label)
-                .with_value(v.clamp(0.0, 1.0))
-                .with_scroll(true)
-        };
-        let mut this = Self {
-            shoulder: Owned::new(knob(s, "Shoulder")),
-            base: Owned::new(knob(b, "Base")),
-            bias: Owned::new(knob(c, "Bias")),
-            custom: installed,
-            last_spec: String::new(),
-        };
-        this.last_spec = if this.custom { this.spec() } else { IDENTITY_SPEC.to_string() };
-        this
-    }
-
-    fn values(&self) -> (f32, f32, f32) {
-        (self.shoulder.inner().value(), self.base.inner().value(), self.bias.inner().value())
-    }
-
-    /// The section's height curve `h(v)` — the shared `(bevel)` curve family
-    /// (`cce_ui::widget::bevel_ease`; the BevelPreview swatch draws the same).
-    fn eval(&self, v: f32) -> f32 {
-        let (s, b, c) = self.values();
-        cce_ui::widget::bevel_ease(s, b, c, v)
-    }
-
-    /// The curve sampled as linear ramp-spec keys — what the renderer LUT
-    /// and the config carry.
-    fn keys(&self) -> Vec<(f32, f32)> {
-        (0..SPEC_SAMPLES)
-            .map(|i| {
-                let v = i as f32 / (SPEC_SAMPLES - 1) as f32;
-                (v, self.eval(v))
-            })
-            .collect()
-    }
-
-    fn spec(&self) -> String {
-        cce_ui::widget::format_ramp_spec(&self.keys(), false)
-    }
-
-    fn take_change(&mut self) -> bool {
-        // Bitwise-or on purpose: every slider's flag must drain.
-        self.shoulder.take_change() | self.base.take_change() | self.bias.take_change()
-    }
-}
-
-struct BevelPopup {
-    /// Which SHAPE the section shows; the curve it edits follows from it.
-    profile_dropdown: Owned<Adapted<Dropdown>>,
-    /// Which wall of the rect the section is through — see [`Edge`].
-    edge_dropdown: Owned<Adapted<Dropdown>>,
-    /// The carve wall — what `carve_slope` renders on every
-    /// recess/boss/ridge in the DE.
-    wall: ProfileKnobs,
-    /// The plate perimeter roll — `roll_slope`'s descent profile.
-    edge: ProfileKnobs,
-    depth_slider: Owned<Adapted<Slider>>,
-    width_slider: Owned<Adapted<Slider>>,
-    height_slider: Owned<Adapted<Slider>>,
-    /// The wall height as the Save target spelled it when this window
-    /// opened — value AND unit — or `None` for a config with no height. The
-    /// unit Save writes back in (`height_len`); the value is what an
-    /// untouched slider writes verbatim, so a `(mm)0.3` does not come back
-    /// as `(px)1.1339` from a headless session whose metric is a guess.
-    height_seed: Option<cce_ui::units::Len>,
-    /// The Finish column: what the surface does under light beyond its
-    /// strength (`Light` above) — `scene::material::Finish`'s spec /
-    /// shininess / curvature. Applied live to the DE finish, or to the pane
-    /// rung's bound material when config binds one.
-    spec_slider: Owned<Adapted<Slider>>,
-    shine_slider: Owned<Adapted<Slider>>,
-    curv_slider: Owned<Adapted<Slider>>,
-    /// The Frost column: the pane material's recipe — compression,
-    /// refraction, blur radius (`scene::material::Frost`). Same live target.
-    comp_slider: Owned<Adapted<Slider>>,
-    refr_slider: Owned<Adapted<Slider>>,
-    radius_slider: Owned<Adapted<Slider>>,
-    save_button: Owned<Adapted<Button>>,
-    /// The material the Save target binds its pane rung to, if any — read
-    /// from the `--config` file (this process's own config is not the
-    /// target's), else this process's binding. `material_frosted` says
-    /// whether that material (or the target's legacy pane) is frosted, i.e.
-    /// whether the Frost column has anything to write.
-    material_target: Option<String>,
-    material_frosted: bool,
-    /// Cancel = discard-and-close: edits are live only in THIS process, so
-    /// with nothing persisted, closing IS the discard (same as Escape).
-    cancel_button: Owned<Adapted<Button>>,
-    /// Set by the cancel click in `drain_widget_changes` (no exit access
-    /// there); `handle_mouse_input` turns it into `BevelMsg::Exit`.
-    exit_requested: bool,
-    /// The window plate's alpha — seeded from this app's own config
-    /// (`~/.config/cce/cce-relief/config.kdl`, `window { opacity }`, falling
-    /// back to the pre-rename `cce-bevel` path), falling
-    /// back to the DE root plate opacity. Edited on the file directly, the
-    /// DE way — no dedicated control.
-    plate_opacity: f32,
-    /// Status line under the buttons: what the last save/reset did.
-    status: String,
-    /// Save/seed target: `--config <path>` retargets the editor at a
-    /// specific config file (a per-app override like cce-designer's), else
-    /// the shared config.kdl. Knob/depth/width seeds prefer this file.
-    config_path: std::path::PathBuf,
-    /// `--key <dotted.key>`: edit a single `(relief)` VALUE in place —
-    /// Save serializes width/depth/wall knobs/wall profile into that one
-    /// key instead of the `style.surface.relief.*` material keys, and the
-    /// seeds come from it. The edge section still previews but is not part
-    /// of a `(relief)` value (a feature material has one wall curve).
-    target_key: Option<String>,
-    /// The knob state's key for this Save target (`knob_state::target_id`):
-    /// which `knobs` node of `state.kdl` remembers this window's sliders.
-    state_target: String,
-    /// Short label for a retargeted config ("cce-designer"), shown in the
-    /// title and status so it's obvious which material is being edited.
-    target_label: Option<String>,
-    ui_context: cce_ui::context::UiContext,
-    width: u32,
-    height: u32,
-    scale_factor: f64,
-    needs_rebuild: bool,
-    registered: bool,
-    status_pos: (f32, f32),
-    cut_rect: Rect,
-}
-
-/// Parse a saved "shoulder,base,bias" knob triple — the `(bevel)` value
-/// format, shared with the BevelPreview swatch.
-use cce_ui::widget::parse_bevel_knobs as parse_knobs;
-
-/// Draw one profile as a lit cutaway: the material slab (plate color) inside
-/// a dark opening, its surface stroked with segment lighting from the DE's
-/// light azimuth. `has_floor` distinguishes the carve (wall meets a floor
-/// inside the material) from the roll (the surface drops to the silhouette
-/// and the material simply ends — air beyond the edge).
-fn draw_section(pc: &mut PaintCtx, rect: Rect, profile: &ProfileKnobs, shape: Shape, edge: Edge) {
-    let has_floor = shape.has_floor();
-    let radius = 6.0f32;
-    let radii = (radius, radius, radius, radius);
-    pc.rounded_rect(rect, radius, (true, true, true, true), [0.08, 0.08, 0.10, 1.0]);
-
-    // The section geometry: a flat band, the shape's walls over `run` wall
-    // widths, then a closing band. PROPORTIONAL AXES: one unit of run is one
-    // unit of drop in pixels, whatever the shape or the window — so a 45°
-    // chamfer draws at 45°, and a shape whose run is 1.5 wall widths draws
-    // half again as wide as one that runs 1. That comparison is the whole
-    // point of putting the composed shapes in here, so the scale must not be
-    // renormalised per shape.
-    let m = CUT_MARGIN;
-    let x_l = rect.x + m + GUTTER_L;
-    let x_r = rect.x + rect.width - m;
-    let avail_w = x_r - x_l;
-    // The shading strip owns a reserved band along the bottom of the opening,
-    // and the SECTION lays out in what is left. Reserving it up front is what
-    // keeps the slab from expanding over it — the slab grows to the bottom of
-    // whatever area it is given.
-    let strip_band = 2.0 * SHADE_STRIP_H + 6.0;
-    let sec_h = rect.height - strip_band;
-    // stroke + slab-underside room, plus the bottom gutter
-    let avail_h = sec_h - 2.0 * m - UNDERSIDE - GUTTER_B;
-
-    let run = shape.run();
-    // Drop over run: the material's pinned height against the DE roll width
-    // (`layout::carve_depth_ratio`), the roll's own rise for the edge roll.
-    // Everything below scales the shape's unit drop by it, so the section
-    // shows the geometry the shader shades — a 0.5 mm drop over a 2 mm wall
-    // draws at that slope, not at the analytic 0.6.
-    let ratio = if matches!(shape, Shape::EdgeRoll) {
-        cce_ui::layout::roll_height_ratio()
-    } else {
-        cce_ui::layout::carve_depth_ratio()
-    };
-    // Vertical extent of THIS shape, sampled — a ridge lives above the surface,
-    // a recess below, a trough only half a drop down.
-    let (mut h_lo, mut h_hi) = (0.0f32, 0.0f32);
-    for i in 0..=64 {
-        let t = run * i as f32 / 64.0;
-        if let Some(hv) = shape.height(profile, t) {
-            h_lo = h_lo.min(hv * ratio);
-            h_hi = h_hi.max(hv * ratio);
-        }
-    }
-    let h_span = (h_hi - h_lo).max(0.25);
-
-    // One scale for both axes (see above), the binding constraint whichever it is.
-    let unit = ((avail_w - 2.0 * MIN_BAND) / run)
-        .min(avail_h / h_span)
-        .max(16.0);
-    let wall_w = run * unit;
-    let leftover = (avail_w - wall_w).max(2.0 * MIN_BAND);
-    let plateau_frac = if has_floor { 0.45 } else { 0.62 };
-    let plateau_w = leftover * plateau_frac;
-    // y of h = 0 (the surrounding surface), placed so the whole excursion fits.
-    let drawn_h = h_span * unit;
-    let y_zero = rect.y
-        + ((sec_h - drawn_h - UNDERSIDE - GUTTER_B) / 2.0).max(m)
-        - h_lo * unit;
-    let y_top = y_zero + h_lo * unit;
-    let y_bot = y_zero + h_hi * unit;
-    let x0 = x_l + plateau_w;
-    let x1 = x0 + wall_w;
-
-    // The axes: faint gridlines over the shape's run × depth domain, drawn
-    // before the slab so the material occludes them (grid in the void only),
-    // with the numbers in the gutters. x is run in wall widths, y is depth in
-    // drops — 0 at the surrounding surface, positive down into the material.
-    let grid = [0.25f32, 0.25, 0.28, 0.6];
-    let num_color = [0x84u8, 0x84, 0x92];
-    // The depth numbers are REAL lengths: one wall width of drop is
-    // `bevel_width` logical px, shown in millimetres when the display metric
-    // is measured or configured, in px when it is only assumed.
-    let metric = cce_ui::units::metric();
-    let wall_px = cce_ui::layout::bevel_width();
-    let (axis_unit, per_wall) = if metric.is_real() {
-        ("mm", wall_px * metric.mm_per_px())
-    } else {
-        ("px", wall_px)
-    };
-    let mut gh = (h_lo / 0.25).round() * 0.25;
-    while gh <= h_hi + 1e-3 {
-        let gy = y_zero + gh * unit;
-        pc.quad(Rect { x: x0, y: gy, width: wall_w, height: 1.0 }, grid);
-        pc.text_with(
-            format!("{:.2}", gh * per_wall),
-            rect.x + m + 2.0,
-            gy - 5.0,
-            10.0,
-            num_color,
-            Some("monospace".to_string()),
-            None,
-        );
-        gh += 0.25;
-    }
-    let mut gt = 0.0f32;
-    while gt <= run + 1e-3 {
-        let gx = x0 + gt * unit;
-        pc.quad(Rect { x: gx, y: y_top, width: 1.0, height: (y_bot - y_top).max(1.0) }, grid);
-        gt += 0.25;
-    }
-
-    // A Right or Bottom wall genuinely draws MIRRORED: a wall's outward normal
-    // always points at its plateau, so "plateau on the left" IS the left/top
-    // wall. Flipping the run keeps the drawing, the predicted band and the real
-    // swatch all describing the same physical wall — without it the prediction
-    // and the swatch disagree by a reflection, which reads as a shading bug.
-    let flip = matches!(edge, Edge::Right | Edge::Bottom);
-    let t_of = |x: f32| -> f32 {
-        if flip { (x1 - x) / unit } else { (x - x0) / unit }
-    };
-
-    // Surface height at a section x.
-    let surface_y = |x: f32| -> Option<f32> {
-        let inside = if flip { x >= x0 } else { x <= x1 };
-        let outside = if flip { x >= x1 } else { x <= x0 };
-        if outside {
-            Some(y_zero)
-        } else if inside {
-            shape
-                .height(profile, t_of(x))
-                .map(|hv| y_zero + hv * ratio * unit)
-        } else if has_floor {
-            shape.height(profile, run).map(|hv| y_zero + hv * ratio * unit)
-        } else {
-            None // past the silhouette: air
-        }
-    };
-
-    // A composed shape's double-shaded band: where two walls are live at once.
-    // Drawn under the slab so the material still occludes it, like the grid.
-    if let Some((ot0, ot1)) = shape.overlap() {
-        pc.quad(
-            Rect {
-                x: x0 + ot0 * unit,
-                y: y_top,
-                width: (ot1 - ot0) * unit,
-                height: (y_bot - y_top).max(1.0),
-            },
-            [0.55, 0.30, 0.30, 0.30],
-        );
-    }
-
-    // The slab: the plate material itself, filled from the surface down to
-    // the cut's bottom edge. Columns share exact edges (opaque fill, but the
-    // ramp-fill rule keeps seams clean under AA).
-    let mut slab = cce_ui::color::page_low_color();
-    slab = [slab[0] * 1.25 + 0.03, slab[1] * 1.25 + 0.03, slab[2] * 1.25 + 0.03, 1.0];
-    // A fixed slab thickness under the lowest surface, so vertical centering
-    // doesn't grow a bottomless block of material.
-    let slab_bot = (y_bot + 16.0).min(rect.y + sec_h - 8.0);
-    let step = 2.0f32;
-    let mut x = x_l;
-    while x < x_r {
-        let xm = (x + step / 2.0).min(x_r);
-        if let Some(sy) = surface_y(xm) {
-            let w = step.min(x_r - x);
-            pc.quad(Rect { x, y: sy, width: w, height: (slab_bot - sy).max(0.0) }, slab);
-        }
-        x += step;
-    }
-
-    // Run numbers under the slab's underside, in wall widths — so a 1.5-wide
-    // shape reads "…1.25 1.50" and its extra run is a number, not just a
-    // feeling.
-    let mut rt = 0.0f32;
-    while rt <= run + 1e-3 {
-        pc.text_with(
-            format!("{rt:.2}"),
-            (if flip { x1 - rt * unit } else { x0 + rt * unit }) - 11.0,
-            slab_bot + 3.0,
-            10.0,
-            num_color,
-            Some("monospace".to_string()),
-            None,
-        );
-        rt += 0.25;
-    }
-    // The depth axis's unit, in the left gutter on the run-number row —
-    // the one spot no excursion of the section can reach.
-    pc.text_with(
-        axis_unit.to_string(),
-        rect.x + m + 2.0,
-        slab_bot + 3.0,
-        10.0,
-        num_color,
-        Some("monospace".to_string()),
-        None,
-    );
-
-    // THE SHADING STRIP: what the shader will actually put on screen along
-    // this section, as opposed to the geometry drawn above it.
-    //
-    // Each of the shape's carves is evaluated with the real model and
-    // composited in emission order onto the surface colour, so a shape that
-    // emits two overlapping walls gets two passes here exactly as it would in
-    // the frame. That is the difference the geometry cannot show: the stacked
-    // inset's dip is only half again as deep as the trough's, but its strip is
-    // visibly hotter, because the overlap region is lit twice.
-    let strip_h = SHADE_STRIP_H;
-    let strip_y = rect.y + rect.height - strip_band + 2.0;
-    {
-        let walls = shape.walls();
-        let light = relief_shade::light_vector();
-        let mat = cce_ui::scene::material::Finish::from_style();
-        // Follow the knobs ONLY when a custom profile is actually installed.
-        // Until one is, the shader runs its analytic branch, and predicting
-        // from the knob curve instead quietly disagrees with it. The carve case
-        // hides this — the knob midpoints ARE the analytic smoothstep — but the
-        // roll's analytic form is a superellipse quadrant, nothing like the
-        // knob family, and there the two differ by ~20 grey levels.
-        let custom = profile.custom;
-        let knob_slope = |v: f32| -> f32 {
-            let d = 1.0 / 32.0;
-            let (a, b) = ((v - d * 0.5).clamp(0.0, 1.0), (v + d * 0.5).clamp(0.0, 1.0));
-            let taper = (v.min(1.0 - v) * 32.0 * 0.667).clamp(0.0, 1.0);
-            if b <= a { 0.0 } else { (profile.eval(b) - profile.eval(a)) / (b - a) * taper }
-        };
-        let slope_at = |v: f32| -> f32 {
-            if custom { knob_slope(v) } else { relief_shade::analytic_carve_slope(v) }
-        };
-        // The DE's own plate colour, NOT a swatch grey. The composite is
-        // asymmetric — brightening screens toward white, darkening multiplies
-        // toward black — so which lobe dominates depends on how light the
-        // surface under it is, and it INVERTS between a dark plate and a light
-        // one. Drawn over the wrong base the strip reverses the very thing you
-        // came to judge: on this plate a wall's bright side out-measures its
-        // dark side about 4:1, and over a pale swatch it reads the other way.
-        let plate = cce_ui::color::page_low_color();
-        let surface = [plate[0], plate[1], plate[2]];
-        // A HORIZONTAL wall's shading varies down the band, not along it: the
-        // run axis is y there, so the band becomes rows of one colour rather
-        // than columns. Both bands do this, so they still compare to each
-        // other — and at 14px the vertical version is roughly life size, since
-        // a real wall is 4.8-9.3 logical px.
-        let horiz = edge.horizontal_wall();
-        let step = 1.0f32;
-        let (scan_lo, scan_hi) = if horiz { (strip_y, strip_y + strip_h) } else { (x_l, x_r) };
-        let mut x = scan_lo;
-        while x < scan_hi {
-            let t = if horiz {
-                // One wall across the band's height, centred like the swatch's.
-                // Bottom mirrors for the same reason Right does.
-                let raw = (x - (strip_y + strip_h * 0.5)) / strip_h + 0.5;
-                if flip { 1.0 - raw } else { raw }
-            } else {
-                t_of(x)
-            };
-            // The plate's edge roll is the OTHER shader branch: it emits its
-            // own material rather than an overlay, and it ends at a silhouette
-            // rather than a floor, so past f = 1 there is nothing to draw.
-            if matches!(shape, Shape::EdgeRoll) {
-                let roll_slope_at = |f: f32| -> f32 {
-                    if !custom {
-                        return relief_shade::analytic_roll_slope(f);
-                    }
-                    let d = 1.0 / 32.0;
-                    let (a, b) = ((f - d * 0.5).clamp(0.0, 1.0), (f + d * 0.5).clamp(0.0, 1.0));
-                    // Taper at the FACE end only; the silhouette keeps whatever
-                    // slope the curve was drawn ending on (roll_slope's `win`).
-                    let taper = (f * 32.0 * 0.667).clamp(0.0, 1.0);
-                    if b <= a { 0.0 } else { (profile.eval(b) - profile.eval(a)) / (b - a) * taper }
-                };
-                // Past the silhouette (f > 1) there is nothing; INSIDE the
-                // face (f < 0) there is the plate's own colour, untouched —
-                // that is the whole point of expressing plate shading relative
-                // to the flat face. Drawing nothing there, as the first cut
-                // did, leaves the band empty over most of its length and looks
-                // like the model failing.
-                //
-                // The section draws this shape with the face on the plateau
-                // side and AIR past the wall — so the outward normal at the
-                // drawn silhouette points along +x, which is the direction fed
-                // to the model here.
-                let c = if t < 0.0 {
-                    Some(surface)
-                } else {
-                    relief_shade::plate_surface(surface, t, [1.0, 0.0], &roll_slope_at, light, &mat)
-                };
-                if let Some(c) = c {
-                    let band = if horiz {
-                        Rect { x: x_l, y: x, width: x_r - x_l, height: step.min(scan_hi - x) }
-                    } else {
-                        Rect { x, y: strip_y, width: step.min(scan_hi - x), height: strip_h }
-                    };
-                    pc.quad(band, [c[0], c[1], c[2], 1.0]);
-                }
-                x += step;
-                continue;
-            }
-            let mut c = surface;
-            for (mode, w0, w1) in &walls {
-                let u = if horiz { t } else { (t - w0) / (w1 - w0) };
-                if !(-0.02..=1.02).contains(&u) {
-                    continue;
-                }
-                // Facing: the SDF gradient at this wall, pointing out of the
-                // carve. THIS is what the edge selector changes, and it is the
-                // whole of the difference between the four walls.
-                let v = relief_shade::carve_shade(*mode, u, edge.facing(), &slope_at, light, &mat);
-                c = relief_shade::composite(c, v);
-            }
-            let band = if horiz {
-                Rect { x: x_l, y: x, width: x_r - x_l, height: step.min(scan_hi - x) }
-            } else {
-                Rect { x, y: strip_y, width: step.min(scan_hi - x), height: strip_h }
-            };
-            pc.quad(band, [c[0], c[1], c[2], 1.0]);
-            x += step;
-        }
-        // THE LIVE SWATCH, directly under the prediction and on the same
-        // x-scale: the shape emitted as REAL prims, through the same PaintCtx
-        // as everything else, so the renderer draws it with the actual shader.
-        //
-        // The two bands are the anti-drift device with teeth. A shared constant
-        // and a parsing test say the numbers agree; these say the PICTURES do.
-        // Any divergence between them is either a bug in relief_shade or a
-        // change in the shader that relief_shade has not tracked, and it shows
-        // up as a visible seam between the bands rather than as silence.
-        //
-        // Alignment: a carve's wall straddles its box edge by ±depth/2, so
-        // emitting at depth = `unit` with the edge at the section's own wall
-        // centre puts the real wall over the predicted one, column for column.
-        let swatch_y = strip_y + strip_h + 2.0;
-        let edge_x = x0 + unit * 0.5;
-        // Tall and wide: only the LEFT wall is meant to land in the band, so
-        // the box's other three edges are pushed well outside it. The clip is
-        // what keeps the cover quad — which inflates past the box — off the
-        // section above and the sliders below.
-        pc.clip(
-            Rect { x: x_l, y: swatch_y, width: x_r - x_l, height: strip_h },
-            |pc| {
-                // OPAQUE: page_low_color carries the plate's own alpha, and
-                // over the near-black opening that lands ~4 grey levels below
-                // the predicted band, which then reads as a constant model
-                // error it is not. The two bands must differ ONLY by shading.
-                // A carve needs a surface to cut into; a PLATE brings its own
-                // fill and ends at a silhouette. Backing the plate with a full
-                // band of its own colour paints over the air past that
-                // silhouette, so the band reads as uniform material and the
-                // roll vanishes — the swatch has to be left empty for it.
-                if !matches!(shape, Shape::EdgeRoll) {
-                    pc.quad(
-                        Rect { x: x_l, y: swatch_y, width: x_r - x_l, height: strip_h },
-                        [plate[0], plate[1], plate[2], 1.0],
-                    );
-                }
-                // The box is placed so the SELECTED wall is the one crossing
-                // the band, and its other three edges are pushed far outside
-                // it. For a horizontal wall the box spans the band's width and
-                // its top/bottom edge sits at the band's mid-height, so the
-                // wall runs across the band at depth = strip_h.
-                let (tall, d_sw) = match edge {
-                    Edge::Left => (
-                        Rect { x: edge_x, y: swatch_y - 400.0, width: 4000.0, height: strip_h + 800.0 },
-                        unit,
-                    ),
-                    Edge::Right => (
-                        Rect { x: edge_x - 4000.0, y: swatch_y - 400.0, width: 4000.0, height: strip_h + 800.0 },
-                        unit,
-                    ),
-                    Edge::Top => (
-                        Rect { x: x_l - 400.0, y: swatch_y + strip_h * 0.5, width: (x_r - x_l) + 800.0, height: 4000.0 },
-                        strip_h,
-                    ),
-                    Edge::Bottom => (
-                        Rect { x: x_l - 400.0, y: swatch_y + strip_h * 0.5 - 4000.0, width: (x_r - x_l) + 800.0, height: 4000.0 },
-                        strip_h,
-                    ),
-                };
-                let unit = d_sw;
-                let sq = (0.0, 0.0, 0.0, 0.0);
-                match shape {
-                    Shape::Recess | Shape::Fillet => pc.recess(tall, sq, unit),
-                    Shape::Boss => pc.boss(tall, sq, unit),
-                    Shape::Ridge => pc.ridge(tall, sq, unit),
-                    Shape::Trough => pc.trough(tall, sq, unit),
-                    Shape::InsetPlate => pc.inset_plate(tall, sq, None, unit),
-                    Shape::Groove => {
-                        let cx = x0 + unit * (0.5 + GROOVE_FLOOR * 0.5);
-                        pc.groove(
-                            (cx, swatch_y - 400.0),
-                            (cx, swatch_y + strip_h + 400.0),
-                            GROOVE_FLOOR * unit,
-                            unit,
-                            tall,
-                        );
-                    }
-                    // The pair as inset_plate used to emit it, in order.
-                    Shape::InsetStacked => {
-                        let g = unit * 0.5;
-                        let inner = Rect { x: edge_x + g, ..tall };
-                        pc.recess(
-                            Rect { x: inner.x - g, y: inner.y, width: inner.width + 2.0 * g, height: inner.height },
-                            sq,
-                            unit,
-                        );
-                        pc.boss(inner, sq, unit);
-                    }
-                    // A plate's roll runs INWARD from its silhouette, where a
-                    // carve's wall straddles its edge — so this box is placed
-                    // by its silhouette at the section's x1, not by a wall
-                    // centre at edge_x. Half a roll of misalignment otherwise.
-                    Shape::EdgeRoll => pc.plate(
-                        Rect { x: x1 - 4000.0, y: swatch_y - 400.0, width: 4000.0, height: strip_h + 800.0 },
-                        sq,
-                        &cce_ui::scene::Material::from_fill([plate[0], plate[1], plate[2], 1.0]),
-                        unit,
-                    ),
-                }
-            },
-        );
-
-        if walls.is_empty() {
-            pc.text_with(
-                "".to_string(),
-                x_l + 4.0,
-                strip_y + 1.0,
-                10.0,
-                num_color,
-                Some("monospace".to_string()),
-                None,
-            );
-        }
-    }
-
-    // The surface stroke, lit per segment: outward normal (material below)
-    // against the DE light azimuth — the same light the real walls shade by.
-    let az = cce_ui::layout::light_source_position();
-    let (lx, ly) = (az.cos(), -az.sin());
-    let base = [0.60f32, 0.65, 0.74];
-    let n_seg = 56usize;
-    let seg_end = if has_floor { x_r } else { x1 };
-    let mut prev = (x_l, surface_y(x_l).unwrap_or(y_top));
-    for i in 1..=n_seg {
-        let x = x_l + (seg_end - x_l) * i as f32 / n_seg as f32;
-        let Some(y) = surface_y(x) else { break };
-        let (dx, dy) = (x - prev.0, y - prev.1);
-        let len = (dx * dx + dy * dy).sqrt().max(1e-3);
-        let (nx, ny) = (dy / len, -dx / len);
-        let lit = (nx * lx + ny * ly) * 0.35;
-        let c = [
-            (base[0] + lit).clamp(0.0, 1.0),
-            (base[1] + lit).clamp(0.0, 1.0),
-            (base[2] + lit).clamp(0.0, 1.0),
-            1.0,
-        ];
-        pc.vector(prev.0, prev.1, x, y, 2.5, c, Cap::Round);
-        prev = (x, y);
-    }
-    // The roll's cut face: a dimmer vertical edge closing the slab at the
-    // silhouette.
-    if !has_floor {
-        pc.vector(x1, y_bot, x1, slab_bot, 2.0, [0.36, 0.39, 0.46, 1.0], Cap::Round);
-    }
-
-    // The opening's rim, drawn last so its shading falls over the slab edges.
-    let depth = cce_ui::layout::bevel_width().min(rect.height * 0.2);
-    pc.recess(rect, radii, depth);
-}
-
-impl BevelPopup {
-    /// The pinned drop as the length Save writes — see [`height_len_for`].
-    fn height_len(&self, px: f32) -> cce_ui::units::Len {
-        height_len_for(self.height_seed, px, &cce_ui::units::metric())
-    }
-
-    /// The selected shape. The dropdown index is the ONLY source; read it
-    /// through here so layout and paint cannot disagree about it.
-    fn active_shape(&self) -> Shape {
-        Shape::ALL
-            .get(self.profile_dropdown.selected)
-            .copied()
-            .unwrap_or(Shape::Recess)
-    }
-
-    /// The wall the section is taken through.
-    fn active_edge(&self) -> Edge {
-        Edge::ALL.get(self.edge_dropdown.selected).copied().unwrap_or(Edge::Left)
-    }
-
-    /// The curve the knobs are editing for the selected shape.
-    fn active_curve(&self) -> Curve {
-        self.active_shape().curve()
-    }
-
-    fn root_ids(&self) -> [WidgetId; 19] {
-        [
-            self.profile_dropdown.id(),
-            self.edge_dropdown.id(),
-            self.wall.shoulder.id(),
-            self.wall.base.id(),
-            self.wall.bias.id(),
-            self.edge.shoulder.id(),
-            self.edge.base.id(),
-            self.edge.bias.id(),
-            self.depth_slider.id(),
-            self.width_slider.id(),
-            self.height_slider.id(),
-            self.spec_slider.id(),
-            self.shine_slider.id(),
-            self.curv_slider.id(),
-            self.comp_slider.id(),
-            self.refr_slider.id(),
-            self.radius_slider.id(),
-            self.save_button.id(),
-            self.cancel_button.id(),
-        ]
-    }
-
-    /// Register every dispatch root by reference: the registry keeps a pointer to each
-    /// and resolves it only while the widget lives, so register once `self` is at its
-    /// final address (see `display_list`).
-    fn register_roots(&mut self) {
-        let ctx = &mut self.ui_context;
-        ctx.register_host(&mut self.profile_dropdown);
-        ctx.register_host(&mut self.edge_dropdown);
-        ctx.register_host(&mut self.wall.shoulder);
-        ctx.register_host(&mut self.wall.base);
-        ctx.register_host(&mut self.wall.bias);
-        ctx.register_host(&mut self.edge.shoulder);
-        ctx.register_host(&mut self.edge.base);
-        ctx.register_host(&mut self.edge.bias);
-        ctx.register_host(&mut self.depth_slider);
-        ctx.register_host(&mut self.width_slider);
-        ctx.register_host(&mut self.height_slider);
-        ctx.register_host(&mut self.spec_slider);
-        ctx.register_host(&mut self.shine_slider);
-        ctx.register_host(&mut self.curv_slider);
-        ctx.register_host(&mut self.comp_slider);
-        ctx.register_host(&mut self.refr_slider);
-        ctx.register_host(&mut self.radius_slider);
-        ctx.register_host(&mut self.save_button);
-        ctx.register_host(&mut self.cancel_button);
-    }
-
-    /// The pane rung's bound material name, when config binds one — the
-    /// target the material sliders edit and Save writes; `None` = the DE
-    /// keys (`style.surface.relief.*` for the finish, `style.surface.plate.*`
-    /// for the frost).
-    fn bound_material() -> Option<String> {
-        cce_ui::color::material_binding(cce_ui::scene::PlateRung::Pane)
-    }
-
-    /// Push the six material sliders into the live style: the bound
-    /// material's node when there is one (so the panes made of it follow),
-    /// else the DE keys every unbound rung reads.
-    fn apply_material_live(&self) {
-        use cce_ui::scene::{FrostDef, MaterialDef};
-        let spec = self.spec_slider.inner().get_scaled_value();
-        let shine = self.shine_slider.inner().get_scaled_value();
-        let curv = self.curv_slider.inner().get_scaled_value();
-        let comp = self.comp_slider.inner().get_scaled_value();
-        let refr = self.refr_slider.inner().get_scaled_value();
-        let radius = self.radius_slider.inner().get_scaled_value();
-        match self.material_target.clone() {
-            Some(name) => {
-                let mut def: MaterialDef = cce_ui::color::named_material(&name).unwrap_or_default();
-                def.spec = Some(spec);
-                def.shininess = Some(shine);
-                def.curvature = Some(curv);
-                // Only a frosted material has a recipe to edit; an opaque
-                // node stays opaque (the sliders read as "when frosted").
-                if def.frost.is_some() || self.material_frosted {
-                    def.frost = Some(FrostDef { compression: Some(comp), refraction: Some(refr), radius: Some(radius) });
-                }
-                cce_ui::color::set_named_material(&name, Some(def));
-            }
-            None => {
-                cce_ui::color::set_finish_spec(spec);
-                cce_ui::color::set_finish_shininess(shine);
-                cce_ui::color::set_finish_curvature(curv);
-                cce_ui::color::set_plate_backdrop_compression(comp);
-                cce_ui::color::set_plate_refraction(refr);
-                cce_ui::color::set_plate_frost_radius(radius);
-            }
-        }
-    }
-
-    /// Persist the material sliders: into the bound material's node
-    /// (`style.surface.material.<name>.finish` / `.frost`) when the pane rung
-    /// is bound, else the DE keys. Never restructures a config that has no
-    /// materials — the named form is opted into by writing the binding.
-    fn save_material(&self, p: &str) -> bool {
-        let f = |v: f32| format!("{v:.3}");
-        let w = |key: &str, value: &str| cce_ui::config::write_config_value(p, key, value, "style");
-        let spec = f(self.spec_slider.inner().get_scaled_value());
-        let shine = f(self.shine_slider.inner().get_scaled_value());
-        let curv = f(self.curv_slider.inner().get_scaled_value());
-        let comp = f(self.comp_slider.inner().get_scaled_value());
-        let refr = f(self.refr_slider.inner().get_scaled_value());
-        let radius = f(self.radius_slider.inner().get_scaled_value());
-        match self.material_target.clone() {
-            Some(name) => {
-                let m = format!("style.surface.material.{name}");
-                let frosted = self.material_frosted;
-                w(&format!("{m}.finish.spec"), &spec)
-                    & w(&format!("{m}.finish.shininess"), &shine)
-                    & w(&format!("{m}.finish.curvature"), &curv)
-                    & (!frosted
-                        || (w(&format!("{m}.frost.compression"), &comp)
-                            & w(&format!("{m}.frost.refraction"), &refr)
-                            & w(&format!("{m}.frost.radius"), &radius)))
-            }
-            None => {
-                // Into the `plate { frost … }` block — the one spelling the
-                // loader reads since the flat `blur` / `radius` /
-                // `backdrop_compression` / `refraction` keys were retired —
-                // and only for a FROSTED plate: writing the block is what
-                // frosts a plate, so an unfrosted config keeps its sharp
-                // view and the three sliders read as "when frosted", as they
-                // do for an opaque material node. A file that still carries
-                // a retired key had it read as the seed (below) and loses it
-                // here, so a config migrates on its first save.
-                let frosted = self.material_frosted;
-                let migrated = ["blur", "radius", "backdrop_compression", "refraction", "bevel_width"]
-                    .iter()
-                    .all(|k| cce_ui::config::remove_config_value(p, &format!("style.surface.plate.{k}")));
-                w("style.surface.relief.spec", &spec)
-                    & w("style.surface.relief.shininess", &shine)
-                    & w("style.surface.relief.curvature", &curv)
-                    & (!frosted
-                        || (w("style.surface.plate.frost.compression", &comp)
-                            & w("style.surface.plate.frost.refraction", &refr)
-                            & w("style.surface.plate.frost.radius", &radius)))
-                    & migrated
-            }
-        }
-    }
-
-    /// `take_*` plumbing after any routed dispatch — state-gated, so it does
-    /// not matter which propagate call consumed the event.
-    fn drain_widget_changes(&mut self) {
-        if self.edge_dropdown.take_change() {
-            self.needs_rebuild = true;
-        }
-        if self.profile_dropdown.take_change() {
-            // Switch which profile the section shows — re-arrange parks the
-            // other set's knobs off-screen.
-            self.needs_rebuild = true;
-        }
-        if self.wall.take_change() {
-            self.wall.custom = true;
-            let keys = self.wall.keys();
-            cce_ui::layout::set_bevel_profile_keys(&keys, false);
-            let spec = self.wall.spec();
-            println!("wall {spec}");
-            self.wall.last_spec = spec;
-            self.needs_rebuild = true;
-        }
-        if self.edge.take_change() {
-            self.edge.custom = true;
-            let keys = self.edge.keys();
-            cce_ui::layout::set_roll_profile_keys(&keys, false);
-            let spec = self.edge.spec();
-            println!("edge {spec}");
-            self.edge.last_spec = spec;
-            self.needs_rebuild = true;
-        }
-        if self.depth_slider.take_change() {
-            let v = self.depth_slider.inner().get_scaled_value();
-            if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
-                reg.set_float("bevel_depth", v);
-            }
-            println!("depth {v:.3}");
-            self.needs_rebuild = true;
-        }
-        if self.width_slider.take_change() {
-            let v = self.width_slider.inner().get_scaled_value();
-            if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
-                reg.set_float("bevel_width", v);
-            }
-            println!("width {v:.2}");
-            self.needs_rebuild = true;
-        }
-        if self.height_slider.take_change() {
-            let v = self.height_slider.inner().get_scaled_value();
-            // 0 = follow the width (`layout::bevel_height` reads 0 as unset).
-            if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
-                reg.set_float("bevel_height", v);
-            }
-            let m = cce_ui::units::metric();
-            println!("height {v:.2}px = {:.3}mm ({})", v * m.mm_per_px(), m.source.as_str());
-            self.needs_rebuild = true;
-        }
-        let material_moved = self.spec_slider.take_change()
-            | self.shine_slider.take_change()
-            | self.curv_slider.take_change()
-            | self.comp_slider.take_change()
-            | self.refr_slider.take_change()
-            | self.radius_slider.take_change();
-        if material_moved {
-            self.apply_material_live();
-            println!(
-                "material spec {:.3} shininess {:.1} curvature {:.3} | compression {:.3} refraction {:.3} radius {:.1}",
-                self.spec_slider.inner().get_scaled_value(),
-                self.shine_slider.inner().get_scaled_value(),
-                self.curv_slider.inner().get_scaled_value(),
-                self.comp_slider.inner().get_scaled_value(),
-                self.refr_slider.inner().get_scaled_value(),
-                self.radius_slider.inner().get_scaled_value(),
-            );
-            self.needs_rebuild = true;
-        }
-        if self.save_button.take_click() {
-            self.save_to_config();
-            self.needs_rebuild = true;
-        }
-        if self.cancel_button.take_click() {
-            self.exit_requested = true;
-        }
-    }
-
-    /// Persist the current material to the shared config
-    /// (`style.surface.relief` — the same keys every app reads at startup).
-    /// Untouched sections write the identity sentinel (= analytic); the knob
-    /// triples go to this app's own state file (`knob_state`) so the editor
-    /// reopens where you left it without the config carrying editor state.
-    fn save_to_config(&mut self) {
-        let p = self.config_path.to_string_lossy().into_owned();
-        // The sliders' positions, whichever target the material goes to.
-        let knobs_ok = knob_state::save(&self.state_target, self.wall.values(), self.edge.values());
-        // `--key` mode: the whole material folds into ONE `(relief)` value
-        // at that key — width, depth, the wall curve, and the knob triple
-        // behind it (so reopening with --key seeds these sliders). The edge
-        // section is not part of a feature material; an untouched analytic
-        // wall writes no profile at all.
-        if let Some(key) = self.target_key.clone() {
-            let h = self.height_slider.inner().get_scaled_value();
-            let spec = cce_ui::relief_spec::ReliefSpec {
-                width: self.width_slider.inner().get_scaled_value(),
-                height: (h > 0.0).then(|| self.height_len(h)),
-                light: Some(self.depth_slider.inner().get_scaled_value()),
-                knobs: Some(self.wall.values()),
-                profile: self.wall.custom.then(|| self.wall.last_spec.clone()),
-            };
-            let ok = knobs_ok
-                & cce_ui::config::write_config_value_typed(
-                    &p,
-                    &key,
-                    &spec.serialize(),
-                    "style",
-                    Some("relief"),
-                );
-            self.status = if ok {
-                println!("saved {key} -> {p}");
-                format!("Saved — {key} holds this material.")
-            } else {
-                "Save FAILED — see config.kdl permissions.".to_string()
-            };
-            return;
-        }
-        let depth = format!("{:.3}", self.depth_slider.inner().get_scaled_value());
-        let width = format!("{:.2}", self.width_slider.inner().get_scaled_value());
-        let w = &mut |key: &str, value: &str| {
-            cce_ui::config::write_config_value(&p, key, value, "style")
-        };
-        // The pinned drop is a LENGTH: written in millimetres when the
-        // display metric is real (fabrication reads it straight), in logical
-        // px when it is only assumed; 0 = follow the width.
-        let h = self.height_slider.inner().get_scaled_value();
-        let height_ok = if h > 0.0 {
-            let len = self.height_len(h);
-            cce_ui::config::write_config_value_typed(
-                &p,
-                "style.surface.relief.wall.height",
-                &cce_ui::units::fmt_num(len.value),
-                "style",
-                Some(len.unit.suffix()),
-            )
-        } else {
-            w("style.surface.relief.wall.height", "0")
-        };
-        let material_ok = self.save_material(&p);
-        // The two shapes are nodes — `relief { wall … ; edge … }` — and a
-        // save MIGRATES: the flat spellings this editor wrote until
-        // 2026-09-28 (`height`, `profile`, `profile_knobs` for the wall,
-        // `edge_*` for the edge, and `depth` for the light) come off the
-        // file, or a current spelling would shadow a stale line forever; and
-        // the knob triples come off under either spelling, since they live
-        // in the state file now.
-        // `remove_config_value` is true for a key that is not there, so a
-        // clean file costs nothing.
-        let ok = height_ok
-            & material_ok
-            & knobs_ok
-            & w("style.surface.relief.light", &depth)
-            & w("style.surface.relief.width", &width)
-            & w("style.surface.relief.wall.profile", &self.wall.last_spec)
-            & w("style.surface.relief.edge.profile", &self.edge.last_spec);
-        let migrated = [
-            "depth",
-            "height", "edge_height", "profile", "edge_profile",
-            "profile_knobs", "edge_knobs", "wall.knobs", "edge.knobs",
-        ]
-        .iter()
-        .all(|k| cce_ui::config::remove_config_value(&p, &format!("style.surface.relief.{k}")))
-            & ["bevel_depth", "bevel_width"]
-                .iter()
-                .all(|k| cce_ui::config::remove_config_value(&p, &format!("window_manager.{k}")));
-        // The shader toggle is not one of this editor's knobs, but its old
-        // home is one of the retired keys a save cleans up: carry the value
-        // across to `relief.shader` rather than drop it.
-        let shader_moved = match std::fs::read_to_string(&p)
-            .ok()
-            .map(|c| cce_ui::config::parse_kdl_to_json(&c))
-            .and_then(|v| v.pointer("/window_manager/bevel_shader").cloned())
-        {
-            Some(v) => {
-                let on = v.as_f64().map_or(v.as_bool().unwrap_or(true), |f| f != 0.0);
-                w("style.surface.relief.shader", if on { "true" } else { "false" })
-                    & cce_ui::config::remove_config_value(&p, "window_manager.bevel_shader")
-            }
-            None => true,
-        };
-        let ok = ok & migrated & shader_moved;
-        self.status = if ok {
-            println!("saved {p}");
-            "Saved — apps pick the material up on start.".to_string()
-        } else {
-            "Save FAILED — see config.kdl permissions.".to_string()
-        };
-    }
-
-}
-
-impl Application for BevelPopup {
-    type Message = BevelMsg;
-
-    fn create(_sender: AppSender<Self::Message>) -> Self {
-        cce_ui::scale::set_scale_factor(1.0);
-        // Force the lazy config load BEFORE reading the registry: the knob
-        // strings are read directly (no getter wraps them), so nothing else
-        // has triggered it yet this early in startup.
-        cce_ui::layout::lazy_init_style_registry();
-
-        // `--config <path>`: retarget Save (and the seeds below) at a
-        // specific config file instead of the shared config.kdl.
-        let shared_path = cce_ui::config::get_config_path();
-        let mut config_path = shared_path.clone();
-        let args: Vec<String> = std::env::args().collect();
-        let mut target_key: Option<String> = None;
-        let mut i = 1;
-        while i < args.len() {
-            if args[i] == "--config" && i + 1 < args.len() {
-                config_path = std::path::PathBuf::from(&args[i + 1]);
-                i += 1;
-            } else if args[i] == "--key" && i + 1 < args.len() {
-                target_key = Some(args[i + 1].clone());
-                i += 1;
-            }
-            i += 1;
-        }
-        let target_label = (config_path != shared_path).then(|| {
-            // An app override (~/.config/cce/<app>/config.kdl) reads best as
-            // the app name; anything else as the file name.
-            let parent = config_path.parent().and_then(|d| d.file_name()).map(|n| n.to_string_lossy().into_owned());
-            match parent {
-                Some(dir) if dir.starts_with("cce-") => dir,
-                _ => config_path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| config_path.display().to_string()),
-            }
-        });
-
-        // Seeds prefer the target file's own relief keys, falling back to
-        // the DE-wide registry for anything it lacks. The shared config is
-        // read too (until 2026-09-28 only a retargeted one was), because a
-        // legacy knob key still in it has no registry slot to fall back to.
-        let target_json = std::fs::read_to_string(&config_path)
-            .ok()
-            .map(|c| cce_ui::config::parse_kdl_to_json(&c));
-        let state_target = knob_state::target_id(&config_path, &shared_path, target_key.as_deref());
-        let target_relief = target_json.as_ref().and_then(|v| v.pointer("/style/surface/relief").cloned());
-        let rel_f32 = |k: &str| {
-            target_relief.as_ref().and_then(|r| r.get(k)).and_then(|v| v.as_f64()).map(|f| f as f32)
-        };
-        // A key of one of the relief's two SHAPES: `relief { wall k=… }` first,
-        // then the flat legacy spelling a file saved before 2026-09-28 still
-        // carries (`height` / `profile` / `profile_knobs` for the wall,
-        // `edge_*` for the edge). The loader does not read those any more
-        // (retired, reported); this is the one-time seed, and Save writes
-        // the node spelling and removes the flat one.
-        let rel_shape = |node: &str, k: &str, legacy: &str| {
-            let r = target_relief.as_ref()?;
-            r.get(node).and_then(|n| n.get(k)).or_else(|| r.get(legacy)).cloned()
-        };
-        let rel_shape_str = |node: &str, k: &str, legacy: &str| {
-            rel_shape(node, k, legacy).and_then(|v| v.as_str().map(String::from))
-        };
-        // A length key, unit and all: a bare number is logical px, a
-        // `(mm)`-annotated one arrives as the string "0.3mm".
-        let rel_shape_units = |node: &str, k: &str, legacy: &str| {
-            rel_shape(node, k, legacy).and_then(|v| {
-                v.as_f64()
-                    .map(|f| cce_ui::units::Len::px(f as f32))
-                    .or_else(|| v.as_str().and_then(cce_ui::units::Len::parse))
-            })
-        };
-        // `--key` seeds: the single `(relief)` value at that key wins over
-        // both the target file's material keys and the registry. Installing
-        // it live BEFORE the knob structs are built means the preview shows
-        // the key's material from the first frame, and the wall's
-        // `installed` flag reads the truth from the registry as usual.
-        let key_spec = target_key.as_ref().and_then(|k| {
-            std::fs::read_to_string(&config_path)
-                .ok()
-                .map(|c| cce_ui::config::parse_kdl_to_json(&c))
-                .and_then(|v| v.pointer(&format!("/{}", k.replace('.', "/"))).cloned())
-                .and_then(|v| v.as_str().map(String::from))
-                .and_then(|s| cce_ui::relief_spec::ReliefSpec::parse(&s))
-        });
-        if let Some(ks) = &key_spec {
-            if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
-                reg.set_float("bevel_width", ks.width);
-                if let Some(d) = ks.light {
-                    reg.set_float("bevel_depth", d);
-                }
-                if let Some(h) = ks.height {
-                    reg.set_len("bevel_height", h);
-                }
-            }
-            cce_ui::layout::install_wall_profile_spec(ks.profile.as_deref());
-        }
-        // The sliders reopen where the last Save of THIS target left them:
-        // the state file first, then a `(relief)` value's own `k=` ride-along,
-        // then a knob key a config still carries from before the move.
-        let (saved_wall, saved_edge) = knob_state::load(&state_target);
-        let (wall_seed, edge_seed) = (
-            saved_wall
-                .or_else(|| key_spec.as_ref().and_then(|s| s.knobs))
-                .or_else(|| rel_shape_str("wall", "knobs", "profile_knobs").as_deref().and_then(parse_knobs)),
-            saved_edge.or_else(|| rel_shape_str("edge", "knobs", "edge_knobs").as_deref().and_then(parse_knobs)),
-        );
-
-        // `depth` and the `window_manager.bevel_*` spellings are retired
-        // (the loader does not read them); read HERE as seeds only, so a
-        // file saved before the renames opens on its own values and Save
-        // writes them back under `relief`.
-        let wm_f32 = |k: &str| {
-            target_json.as_ref().and_then(|v| v.pointer(&format!("/window_manager/{k}"))).and_then(|v| v.as_f64()).map(|f| f as f32)
-        };
-        let depth = key_spec
-            .as_ref()
-            .and_then(|s| s.light)
-            .or_else(|| rel_f32("light"))
-            .or_else(|| rel_f32("depth"))
-            .or_else(|| wm_f32("bevel_depth"))
-            .unwrap_or_else(cce_ui::layout::bevel_depth);
-        // The height as the target SPELLS it (value and unit), kept so Save
-        // writes the same unit back; the registry fallback carries no unit
-        // and seeds the slider only.
-        let height_seed: Option<cce_ui::units::Len> = key_spec
-            .as_ref()
-            .and_then(|s| s.height)
-            .or_else(|| rel_shape_units("wall", "height", "height"))
-            .filter(|l| l.value > 0.0);
-        let height = height_seed
-            .map(|l| l.to_px())
-            .or_else(cce_ui::layout::bevel_height)
-            .unwrap_or(0.0);
-        let width = key_spec
-            .as_ref()
-            .map(|s| s.width)
-            .or_else(|| rel_f32("width"))
-            .or_else(|| wm_f32("bevel_width"))
-            .unwrap_or_else(cce_ui::layout::bevel_width);
-        // The seeds ARE the material this window previews: install them so
-        // the section, the strip and the popup's own plate show the target
-        // file's width / light / height from the first frame, not the
-        // DE-wide registry's until a knob moves. (A `--key` spec was
-        // installed above already; this repeats it harmlessly.)
-        if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
-            reg.set_float("bevel_depth", depth);
-            reg.set_float("bevel_width", width);
-            reg.set_float("bevel_height", height);
-        }
-        // A key target labels the window by the key, not the file.
-        let target_label = match &target_key {
-            Some(k) => {
-                let parts: Vec<&str> = k.split('.').collect();
-                Some(parts[parts.len().saturating_sub(2)..].join("."))
-            }
-            None => target_label,
-        };
-        let (dmin, dmax) = DEPTH_RANGE;
-        let (wmin, wmax) = WIDTH_RANGE;
-        let (hmin, hmax) = HEIGHT_RANGE;
-        // The material sliders seed from the pane rung's effective material
-        // — the bound node when config binds one, else the DE keys — so they
-        // open on what the panes actually wear. With `--config`, that is the
-        // TARGET file's material, read from the file: this process loads its
-        // own config, and seeding from that would make a Save write this
-        // app's values over the target's (the designer's 0.6 / 0.3 became 0
-        // / 0 that way once).
-        let pane = cce_ui::scene::Material::pane();
-        let tj = target_json.as_ref();
-        let tnum = |p: &str| tj.and_then(|v| v.pointer(p)).and_then(|v| v.as_f64()).map(|f| f as f32);
-        let material_target: Option<String> = match tj {
-            Some(v) => v
-                .pointer("/style/surface/plate/material")
-                .and_then(|b| b.as_str())
-                .filter(|s| !s.is_empty())
-                .map(String::from),
-            None => Self::bound_material(),
-        };
-        let tdef = |k: &str| material_target.as_deref().map(|n| format!("/style/surface/material/{n}/{k}"));
-        // A `frost` block (anything but `frost (bool)false`) is frosted. The
-        // retired `plate.blur` is still honoured HERE, as a seed only: a
-        // file that has not been saved since the retirement opens frosted,
-        // and Save writes the block and drops the old key.
-        let block_frosted = |v: &serde_json::Value| {
-            match v.pointer("/style/surface/plate/frost") {
-                Some(serde_json::Value::Bool(on)) => *on,
-                Some(_) => true,
-                None => false,
-            }
-        };
-        let legacy_blur = |v: &serde_json::Value| {
-            tnum("/style/surface/plate/blur").map_or(
-                v.pointer("/style/surface/plate/blur").and_then(|b| b.as_bool()).unwrap_or(false),
-                |f| f > 0.001,
-            )
-        };
-        let material_frosted = match tj {
-            Some(v) => tdef("frost").is_some_and(|p| v.pointer(&p).is_some())
-                || (material_target.is_none() && (block_frosted(v) || legacy_blur(v))),
-            None => material_target
-                .as_deref()
-                .map_or(pane.frost.is_frosted(), |n| cce_ui::color::named_material(n).is_some_and(|d| d.frost.is_some())),
-        };
-        let finish_seed = |k: &str, process: f32| -> f32 {
-            tdef(&format!("finish/{k}"))
-                .and_then(|p| tnum(&p))
-                .or_else(|| tnum(&format!("/style/surface/relief/{k}")))
-                .unwrap_or(process)
-        };
-        let (pcomp, prefr, pradius) = match pane.frost {
-            cce_ui::scene::Frost::Frosted { compression, refraction, radius } => (compression, refraction, radius),
-            cce_ui::scene::Frost::Unfrosted => match cce_ui::scene::Frost::from_style() {
-                cce_ui::scene::Frost::Frosted { compression, refraction, radius } => (compression, refraction, radius),
-                cce_ui::scene::Frost::Unfrosted => (0.0, 0.0, cce_ui::scene::Frost::DEFAULT_RADIUS),
-            },
-        };
-        // A frost knob seeds from the bound material's `frost` child, else
-        // the plate's `frost` block, else — seed only, so a not-yet-saved
-        // file opens where it was — the retired flat key under `plate`.
-        let frost_seed = |k: &str, legacy_key: &str, process: f32| -> f32 {
-            tdef(&format!("frost/{k}"))
-                .and_then(|p| tnum(&p))
-                .or_else(|| tnum(&format!("/style/surface/plate/frost/{k}")))
-                .or_else(|| tnum(&format!("/style/surface/plate/{legacy_key}")))
-                .unwrap_or(process)
-        };
-        let comp0 = frost_seed("compression", "backdrop_compression", pcomp);
-        let refr0 = frost_seed("refraction", "refraction", prefr);
-        let radius0 = frost_seed("radius", "radius", pradius);
-        let spec0 = finish_seed("spec", pane.finish.spec);
-        let shine0 = finish_seed("shininess", pane.finish.shininess);
-        let curv0 = finish_seed("curvature", pane.finish.curvature);
-        let norm = |v: f32, (lo, hi): (f32, f32)| ((v - lo) / (hi - lo)).clamp(0.0, 1.0);
-        let material_slider = |label: &str, v: f32, range: (f32, f32), decimals: usize| {
-            Slider::new()
-                .with_label(label)
-                .with_range(range.0, range.1)
-                .with_value(norm(v, range))
-                .with_readout(true)
-                .with_decimals(decimals)
-                .with_scroll(true)
-        };
-        // The persisted per-app plate opacity, falling back to the DE look.
-        // New path first, then the pre-rename one, so an existing opacity
-        // setting keeps working without a migration step.
-        let plate_opacity = std::fs::read_to_string(cce_ui::config::get_app_config_path("cce-relief"))
-            .or_else(|_| std::fs::read_to_string(cce_ui::config::get_app_config_path("cce-bevel")))
-            .ok()
-            .map(|c| cce_ui::config::parse_kdl_to_json(&c))
-            .and_then(|v| {
-                v.get("window")
-                    .and_then(|w| w.get("opacity"))
-                    .and_then(|o| o.as_f64())
-                    .map(|f| (f as f32).clamp(0.0, 1.0))
-            })
-            .unwrap_or_else(cce_ui::color::root_plate_opacity);
-        Self {
-            profile_dropdown: Owned::new(Dropdown::new(
-                Shape::ALL.iter().map(|s| s.label().to_string()).collect(),
-                0,
-            )
-            .with_label("Shape")),
-            edge_dropdown: Owned::new(Dropdown::new(
-                Edge::ALL.iter().map(|e| e.label().to_string()).collect(),
-                0,
-            )
-            .with_label("Edge")),
-            wall: ProfileKnobs::new(wall_seed, cce_ui::layout::bevel_profile_slopes().is_some()),
-            edge: ProfileKnobs::new(edge_seed, cce_ui::layout::roll_profile_slopes().is_some()),
-            depth_slider: Owned::new(Slider::new()
-                .with_label("Light")
-                .with_range(dmin, dmax)
-                .with_value(((depth - dmin) / (dmax - dmin)).clamp(0.0, 1.0))
-                .with_readout(true)
-                .with_decimals(2)
-                .with_scroll(true)),
-            width_slider: Owned::new(Slider::new()
-                .with_label("Width")
-                .with_range(wmin, wmax)
-                .with_value(((width - wmin) / (wmax - wmin)).clamp(0.0, 1.0))
-                .with_readout(true)
-                .with_decimals(1)
-                .with_scroll(true)),
-            height_slider: Owned::new(Slider::new()
-                .with_label("Height")
-                .with_range(hmin, hmax)
-                .with_value(((height - hmin) / (hmax - hmin)).clamp(0.0, 1.0))
-                .with_readout(true)
-                .with_decimals(1)
-                .with_scroll(true)),
-            spec_slider: Owned::new(material_slider("Specular", spec0, SPEC_RANGE, 2)),
-            shine_slider: Owned::new(material_slider("Shininess", shine0, SHINE_RANGE, 0)),
-            curv_slider: Owned::new(material_slider("Curvature", curv0, CURV_RANGE, 2)),
-            comp_slider: Owned::new(material_slider("Compression", comp0, COMP_RANGE, 2)),
-            refr_slider: Owned::new(material_slider("Refraction", refr0, REFR_RANGE, 2)),
-            radius_slider: Owned::new(material_slider("Blur radius", radius0, RADIUS_RANGE, 1)),
-            save_button: Owned::new(Button::new(0.0, 0.0, 0.0, 0.0).with_label("Save")),
-            cancel_button: Owned::new(Button::new(0.0, 0.0, 0.0, 0.0).with_label("Cancel")),
-            material_target,
-            material_frosted,
-            exit_requested: false,
-            plate_opacity,
-            status: match (&target_key, &target_label) {
-                (Some(_), Some(l)) => format!("Edits apply live; Save writes the {l} key."),
-                (None, Some(l)) => format!("Edits apply live; Save writes {l}'s config."),
-                _ => "Edits apply live; Save writes config.kdl.".to_string(),
-            },
-            config_path,
-            target_key,
-            state_target,
-            height_seed,
-            target_label,
-            ui_context: cce_ui::context::UiContext::new(),
-            width: 520,
-            height: 480,
-            scale_factor: 1.0,
-            needs_rebuild: true,
-            registered: false,
-            status_pos: (0.0, 0.0),
-            cut_rect: Rect::ZERO,
-        }
-    }
-
-    fn settings(&self) -> WindowSettings {
-        WindowSettings {
-            title: match &self.target_label {
-                Some(l) => format!("Relief — {l}"),
-                None => "Relief".to_string(),
-            },
-            app_id: "cce-relief".to_string(),
-            width: 520,
-            // Asked for, not guessed: the exact height the content occupies at
-            // this width (see `content_height`).
-            height: content_height(520.0).round() as u32,
-            fullscreen: false,
-            // The floor is the same content height — this window has no useful
-            // smaller shape, and shrinking it only eats the cutaway.
-            min_size: Some((440, content_height(440.0).round() as u32)),
-        }
-    }
-
-    /// The motivating case for the mode: this window's shape IS
-    /// `content_height`, so nothing — not a drag, not a remembered size —
-    /// should ever dictate a different one.
-    fn utility(&self) -> bool {
-        true
-    }
-
-    fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
-        match msg {
-            BevelMsg::Exit => *exit = true,
-        }
-    }
-
-    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
-        if self.ui_context.tick(dt) {
-            self.drain_widget_changes();
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
-        if !self.registered {
-            self.registered = true;
-            self.register_roots();
-        }
-
-        let size_changed = self.width != size.width as u32
-            || self.height != size.height as u32
-            || self.scale_factor != scale;
-        if self.needs_rebuild || size_changed {
-            self.width = size.width as u32;
-            self.height = size.height as u32;
-            self.scale_factor = scale;
-            cce_ui::scale::set_scale_factor(scale as f32);
-
-            // Manual column layout: the profile selector, ONE cutaway, the
-            // selected profile's knobs, then the global rows. The unselected
-            // profile's knobs park off-screen.
-            let pad = cce_ui::layout::root_plate_padding();
-            let x = pad;
-            let w = (self.width as f32 - 2.0 * pad).max(0.0);
-            let gap = 14.0;
-            let strip = {
-                let (_, fsize) = cce_ui::layout::control_label_font_detached_parsed();
-                fsize + cce_ui::layout::control_label_margin()
-            };
-            let knob_h = 22.0 + strip;
-            let button_h = 26.0;
-            let status_h = HEADER_FONT_SIZE + 4.0;
-            // Rows above/below the cutaway: the selector row, then SIX
-            // stacked sliders, then buttons and status. Stacked rather than
-            // gridded because a slider's label and readout want the full width
-            // — three to a row truncated both, and the two-wide Depth/Width row
-            // set a different rhythm again for no reason.
-            let fixed = knob_h + gap                      // selector row
-                + 6.0 * (knob_h + gap)                    // Shoulder..Height
-                + button_h + 8.0 + status_h + 2.0 * gap;  // buttons + status
-            // The cutaway absorbs spare height — but only up to its NATURAL
-            // height for this width (the proportional square domain plus
-            // gutters), so the opening shrink-wraps the section instead of
-            // floating it in dark space.
-            let natural = (w - 2.0 * CUT_MARGIN - GUTTER_L - 2.0 * MIN_BAND)
-                + 2.0 * CUT_MARGIN
-                + UNDERSIDE
-                + GUTTER_B
-                + 2.0 * SHADE_STRIP_H
-                + 6.0
-                + GUTTER_B;
-            let cut_h = (self.height as f32 - 2.0 * pad - fixed).min(natural).max(90.0);
-
-            let knob_row = |k: &mut ProfileKnobs, x: f32, y: f32| {
-                k.shoulder.set_rect(x, y, w, knob_h);
-                k.base.set_rect(x, y + (knob_h + gap), w, knob_h);
-                k.bias.set_rect(x, y + 2.0 * (knob_h + gap), w, knob_h);
-            };
-            let park = |k: &mut ProfileKnobs| {
-                k.shoulder.set_rect(-1000.0, -1000.0, 0.0, 0.0);
-                k.base.set_rect(-1000.0, -1000.0, 0.0, 0.0);
-                k.bias.set_rect(-1000.0, -1000.0, 0.0, 0.0);
-            };
-
-            let mut y = pad;
-            // Shape takes the row's left portion, Edge the rest — one row,
-            // because the cutaway is what should get the spare height.
-            let edge_w = (w * 0.32).max(120.0).min(w * 0.5);
-            let shape_w = (w - edge_w - gap).max(140.0);
-            self.profile_dropdown.set_rect(x, y, shape_w, knob_h);
-            self.edge_dropdown.set_rect(x + shape_w + gap, y, edge_w, knob_h);
-            y += knob_h + gap;
-            self.cut_rect = Rect { x, y, width: w, height: cut_h };
-            y += cut_h + gap;
-            // Keyed off the SHAPE's curve, never the dropdown index — several
-            // shapes share the Wall curve, and this has to agree with the paint
-            // side's `wall_active` or the row is laid out for one set and drawn
-            // from the other, which parks every knob off-screen and looks like
-            // the sliders vanished.
-            if self.active_curve() == Curve::Wall {
-                knob_row(&mut self.wall, x, y);
-                park(&mut self.edge);
-            } else {
-                knob_row(&mut self.edge, x, y);
-                park(&mut self.wall);
-            }
-            y += 3.0 * (knob_h + gap);
-            self.depth_slider.set_rect(x, y, w, knob_h);
-            y += knob_h + gap;
-            self.width_slider.set_rect(x, y, w, knob_h);
-            y += knob_h + gap;
-            self.height_slider.set_rect(x, y, w, knob_h);
-            y += knob_h + gap;
-            // The material columns: Finish left, Frost right, three rows.
-            let half = ((w - gap) * 0.5).max(60.0);
-            for (l, r) in [
-                (&mut self.spec_slider, &mut self.comp_slider),
-                (&mut self.shine_slider, &mut self.refr_slider),
-                (&mut self.curv_slider, &mut self.radius_slider),
-            ] {
-                l.set_rect(x, y, half, knob_h);
-                r.set_rect(x + half + gap, y, half, knob_h);
-                y += knob_h + gap;
-            }
-            self.save_button.set_rect(x, y, 96.0, button_h);
-            self.cancel_button.set_rect(x + 96.0 + 12.0, y, 96.0, button_h);
-            y += button_h + 8.0;
-            self.status_pos = (x, y);
-
-            self.needs_rebuild = false;
-            self.ui_context.rebuild_spatial_grid();
-        }
-
-        // BOTH selectors, not just the shape one. A dropdown whose popover is
-        // neither registered nor rendered still OPENS on a press — it just
-        // opens invisibly, so its items cannot be hit and the widget reads as
-        // completely dead. That is what adding the Edge selector looked like
-        // until this list grew: paint, layout, registration and routing were
-        // all correct and the thing still did nothing.
-        self.ui_context.clear_popovers();
-        if self.profile_dropdown.popover_rect().is_some() {
-            self.ui_context.register_popover(&mut self.profile_dropdown);
-        }
-        if self.edge_dropdown.popover_rect().is_some() {
-            self.ui_context.register_popover(&mut self.edge_dropdown);
-        }
-
-        let mut pc = PaintCtx::new();
-        let (w, h) = (self.width as f32, self.height as f32);
-
-        // The window plate at full opacity on purpose: its rolled perimeter
-        // previews the edge profile, and the wells/buttons preview the wall
-        // profile — the popup is its own material sample.
-        let mut plate = cce_ui::color::page_low_color();
-        plate[3] = self.plate_opacity;
-        let radius = cce_ui::colors::root_plate_corner_radius();
-        let bevel = cce_ui::layout::bevel_width();
-        pc.plate(
-            Rect { x: 0.0, y: 0.0, width: w, height: h },
-            (radius, radius, radius, radius),
-            &cce_ui::scene::Material::from_fill(plate),
-            bevel,
-        );
-
-        pc.text_with(
-            self.status.clone(),
-            self.status_pos.0,
-            self.status_pos.1,
-            HEADER_FONT_SIZE,
-            HEADER_COLOR,
-            Some("monospace".to_string()),
-            None,
-        );
-
-        let shape = self.active_shape();
-        let wall_active = shape.curve() == Curve::Wall;
-        let active = if wall_active { &self.wall } else { &self.edge };
-        draw_section(&mut pc, self.cut_rect, active, shape, self.active_edge());
-
-        let knobs = if wall_active { &self.wall } else { &self.edge };
-        for s in [
-            &knobs.shoulder,
-            &knobs.base,
-            &knobs.bias,
-            &self.depth_slider,
-            &self.width_slider,
-            &self.height_slider,
-            &self.spec_slider,
-            &self.shine_slider,
-            &self.curv_slider,
-            &self.comp_slider,
-            &self.refr_slider,
-            &self.radius_slider,
-        ] {
-            cce_ui::scene::painter::paint_root_into(&self.ui_context, s, &mut pc);
-        }
-        cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.edge_dropdown, &mut pc);
-        cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.profile_dropdown, &mut pc);
-        cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.save_button, &mut pc);
-        cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.cancel_button, &mut pc);
-
-        // The selector's popover, drawn into the frame on top of everything
-        // below it (its labels carry the popover rect as bounds).
-        if self.profile_dropdown.popover_rect().is_some() {
-            // PaintCtx is a RenderTarget: the popover draws its real prims (the
-            // dropdown's expanded inset-plate surface) with its own bounds.
-            self.profile_dropdown.render_popover(&mut pc);
-        }
-        if self.edge_dropdown.popover_rect().is_some() {
-            self.edge_dropdown.render_popover(&mut pc);
-        }
-
-        // The shared context menu (slider Copy/Paste), last, on top.
-        // The lit plate and the menu font, in one call — the flat quads and
-        // a family-less label loop drew this menu square, opaque and in the
-        // default sans, unlike every app's.
-        cce_ui::widget::context_menu::paint_with_labels(&mut pc);
-
-        Some(pc.finish())
-    }
-
-    fn display_list_text(&self) -> bool {
-        true
-    }
-
-    fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
-        Some(&self.ui_context)
-    }
-
-    // Engine-driven animation frames for the dropdown expand/contract.
-    fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
-        Some(&mut self.ui_context)
-    }
-
-    fn is_movable_root_plate_at(&self, px: f32, py: f32) -> bool {
-        self.ui_context.drag_allowed_at(px, py)
-    }
-
-    fn clear_color(&self) -> [f32; 4] {
-        [0.0, 0.0, 0.0, 0.0]
-    }
-
-    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
-        if self.ui_context.cursor_moved_context_menu(pos.x, pos.y) {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-        let ev = Event::PointerMove { x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
-        let mut changed = false;
-        for root in self.root_ids() {
-            if self.ui_context.propagate_event(&ev, root) {
-                changed = true;
-            }
-        }
-        self.drain_widget_changes();
-        if changed || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn handle_mouse_input(
-        &mut self,
-        button: MouseButton,
-        state: ElementState,
-        pos: LogicalPosition,
-        needs_rebuild: &mut bool,
-    ) -> Option<Self::Message> {
-        // The open context menu owns the press (item dispatch / dismiss).
-        if self.ui_context.mouse_input_context_menu(button, state, pos.x, pos.y) {
-            self.drain_widget_changes();
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-            return None;
-        }
-        let ev = Event::MouseButton {
-            button,
-            state,
-            x: pos.x,
-            y: pos.y,
-            local_x: pos.x,
-            local_y: pos.y,
-        };
-        let mut changed = false;
-        for root in self.root_ids() {
-            if self.ui_context.propagate_event(&ev, root) {
-                changed = true;
-            }
-        }
-        self.drain_widget_changes();
-        if self.exit_requested {
-            return Some(BevelMsg::Exit);
-        }
-        if changed || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-        None
-    }
-
-    fn handle_mouse_wheel(
-        &mut self,
-        delta: &MouseScrollDelta,
-        pos: LogicalPosition,
-        needs_rebuild: &mut bool,
-    ) {
-        let ev = Event::MouseWheel {
-            delta: *delta,
-            x: pos.x,
-            y: pos.y,
-            local_x: pos.x,
-            local_y: pos.y,
-        };
-        let mut changed = false;
-        for root in self.root_ids() {
-            if self.ui_context.propagate_event(&ev, root) {
-                changed = true;
-            }
-        }
-        self.drain_widget_changes();
-        if changed || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn handle_key_input(
-        &mut self,
-        event: &KeyEvent,
-        needs_rebuild: &mut bool,
-    ) -> Option<Self::Message> {
-        use cce_ui::widget::{Key, NamedKey};
-        if event.state == ElementState::Pressed {
-            // Escape exits — unless the context menu is up (the toolkit-wide
-            // Escape-dismiss should win the first press).
-            if event.logical_key == Key::Named(NamedKey::Escape)
-                && !self.ui_context.is_context_menu_visible()
-            {
-                return Some(BevelMsg::Exit);
-            }
-            if event.ctrl {
-                if let Key::Character(ref c) = event.logical_key {
-                    if c == "q" {
-                        return Some(BevelMsg::Exit);
-                    }
-                }
-            }
-        }
-        let ev = Event::KeyInput(event.clone());
-        let mut handled = false;
-        for root in self.root_ids() {
-            if self.ui_context.propagate_event(&ev, root) {
-                handled = true;
-                break;
-            }
-        }
-        self.drain_widget_changes();
-        if handled || self.needs_rebuild {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-        None
-    }
-}
-
-fn main() {
-    cce_ui::engine::run::<BevelPopup>();
-}
-
-/// The wall height Save writes for a slider at `px`, given the height the
-/// target spelled when the window opened (`seed`: value and unit, `None`
-/// for a config that had none) and the display metric.
-///
-/// **The configured unit is kept.** A `(mm)0.3` is written back in
-/// millimetres whatever the metric's source, converted through the same
-/// metric that resolved it on seed — so on a headless session, where the
-/// metric is the assumed 96 ppi, an untouched slider round-trips exactly
-/// and a moved one is a millimetre value derived through the same guess
-/// the slider itself was showing. Until 2026-09-28 the rule was "mm when
-/// the metric is real, px otherwise", which rewrote a `(mm)0.3` as
-/// `(px)1.1339` on every save from a session without EDID — a unit the
-/// user chose, lost to a metric the machine lacked. An untouched slider
-/// writes the seed VERBATIM, not a round trip of it, so a saved file that
-/// was not edited is byte-identical in that key. Only a height the config
-/// never had picks a unit from the metric: `(mm)` when it is real, px when
-/// it is only assumed, since a millimetre value nobody typed is honest only
-/// when the millimetres are.
-fn height_len_for(seed: Option<cce_ui::units::Len>, px: f32, metric: &cce_ui::units::Metric) -> cce_ui::units::Len {
-    use cce_ui::units::{Len, Unit};
-    match seed {
-        Some(seed) if (seed.resolve(metric) - px).abs() < 1e-4 => seed,
-        Some(seed) if seed.unit == Unit::Px => Len::px(px),
-        Some(seed) => Len::new((metric.from_px(px, seed.unit) * 1000.0).round() / 1000.0, seed.unit),
-        None if metric.is_real() => Len::mm(((px * metric.mm_per_px()) * 1000.0).round() / 1000.0),
-        None => Len::px(px),
-    }
-}
-
-#[cfg(test)]
-mod height_unit_tests {
-    use super::height_len_for;
-    use cce_ui::units::{Len, Metric, MetricSource};
-
-    fn real() -> Metric {
-        Metric::from_px_per_mm(1.0, 5.0, MetricSource::Measured).unwrap()
-    }
-
-    /// A `(mm)` height keeps its unit on an assumed metric: untouched it
-    /// writes back verbatim, moved it converts through the same guess.
-    #[test]
-    fn a_configured_unit_survives_a_headless_save() {
-        let assumed = Metric::assumed(1.0);
-        let seed = Len::mm(0.3);
-        let px = seed.resolve(&assumed);
-        assert_eq!(height_len_for(Some(seed), px, &assumed), seed, "untouched: verbatim");
-        assert_eq!(height_len_for(Some(seed), px + 0.00005, &assumed), seed, "a float wobble is untouched");
-        let moved = height_len_for(Some(seed), px * 2.0, &assumed);
-        assert_eq!(moved.unit, seed.unit);
-        assert!((moved.value - 0.6).abs() < 1e-3, "{moved:?}");
-        // A px height stays px even on a real display.
-        assert_eq!(height_len_for(Some(Len::px(4.0)), 6.0, &real()), Len::px(6.0));
-        // A `(cm)` height keeps centimetres.
-        assert_eq!(height_len_for(Some(Len::cm(0.1)), 10.0, &real()), Len::cm(0.2));
-    }
-
-    /// A height the config never had picks its unit from the metric.
-    #[test]
-    fn a_new_height_takes_its_unit_from_the_metric() {
-        assert_eq!(height_len_for(None, 1.5, &Metric::assumed(1.0)), Len::px(1.5));
-        assert_eq!(height_len_for(None, 1.5, &real()), Len::mm(0.3));
-    }
-}
-
-/// The editor's slider positions, kept in this app's own state file —
-/// `~/.config/cce/cce-relief/state.kdl` (`$XDG_CONFIG_HOME` honoured through
-/// `cce_config_dir`) — one `knobs` node per Save target:
-///
-/// ```kdl
-/// knobs target="shared" wall=(bevel)"0.500,0.500,0.500" edge=(bevel)"0.500,0.500,0.500"
-/// knobs target="/home/me/.config/cce/cce-designer/config.kdl" wall=… edge=…
-/// knobs target="/home/me/.config/cce/config.kdl#style.surface.desktop.line_relief" wall=…
-/// ```
-///
-/// Per target, because a per-app override and the shared material are two
-/// materials with two curves each, and reopening one must not seed it with
-/// the other's sliders. The triples used to ride in the style block as
-/// `relief.wall.knobs` / `edge.knobs` (before that `profile_knobs` /
-/// `edge_knobs`), `(bevel)`-typed so the data editor could preview them —
-/// editor state beside the values that draw, and the one relief key nothing
-/// but this editor read. The `(bevel)` type and `parse_bevel_knobs` stay:
-/// a `(relief)` value still carries its own `k=` ride-along, which is what
-/// the data editor's preview of such a value draws.
-mod knob_state {
-    use cce_ui::widget::parse_bevel_knobs as parse_knobs;
-
-    pub fn path() -> std::path::PathBuf {
-        cce_ui::config::cce_config_dir().join("cce-relief").join("state.kdl")
-    }
-
-    /// Which `knobs` node a Save target owns: `shared` for the DE-wide
-    /// config.kdl, a retargeted file by its path, a `--key` value by
-    /// `<path>#<key>`.
-    pub fn target_id(config_path: &std::path::Path, shared_path: &std::path::Path, key: Option<&str>) -> String {
-        match key {
-            Some(k) => format!("{}#{k}", config_path.display()),
-            None if config_path == shared_path => "shared".to_string(),
-            None => config_path.display().to_string(),
-        }
-    }
-
-    /// The saved (wall, edge) triples for `target`, each absent when the
-    /// state file has none — or a triple that does not parse, which reads
-    /// as unsaved rather than as midpoints, so the seed chain can go on to
-    /// a config's legacy key.
-    pub fn load(target: &str) -> (Option<(f32, f32, f32)>, Option<(f32, f32, f32)>) {
-        std::fs::read_to_string(path()).ok().map_or((None, None), |c| lookup(&c, target))
-    }
-
-    /// Remember `wall` and `edge` for `target`, keeping every other target's
-    /// node. `true` when the file was written.
-    pub fn save(target: &str, wall: (f32, f32, f32), edge: (f32, f32, f32)) -> bool {
-        let p = path();
-        let current = std::fs::read_to_string(&p).unwrap_or_default();
-        let Some(next) = upsert(&current, target, wall, edge) else {
-            return false;
-        };
-        if let Some(dir) = p.parent() {
-            let _ = std::fs::create_dir_all(dir);
-        }
-        // Temp-and-rename, so a crash mid-write leaves the old file whole.
-        let tmp = p.with_extension("kdl.tmp");
-        std::fs::write(&tmp, next).is_ok() && std::fs::rename(&tmp, &p).is_ok()
-    }
-
-    /// The (wall, edge) triples of `target`'s node in `content`.
-    pub fn lookup(content: &str, target: &str) -> (Option<(f32, f32, f32)>, Option<(f32, f32, f32)>) {
-        let Ok(doc) = content.parse::<kdl::KdlDocument>() else {
-            return (None, None);
-        };
-        let Some(node) = doc.nodes().iter().find(|n| is_target(n, target)) else {
-            return (None, None);
-        };
-        let triple = |k: &str| node.get(k).and_then(|e| e.value().as_string()).and_then(parse_knobs);
-        (triple("wall"), triple("edge"))
-    }
-
-    /// `content` with `target`'s node replaced (or added), every other node
-    /// kept as it was. `None` when `content` is not KDL — the file is this
-    /// app's own, so a corrupt one is reported by the Save rather than
-    /// silently replaced.
-    pub fn upsert(content: &str, target: &str, wall: (f32, f32, f32), edge: (f32, f32, f32)) -> Option<String> {
-        let mut doc = content.parse::<kdl::KdlDocument>().ok()?;
-        doc.nodes_mut().retain(|n| !is_target(n, target));
-        let fmt = |(s, b, c): (f32, f32, f32)| format!("{s:.3},{b:.3},{c:.3}");
-        let line = format!(
-            "knobs target={} wall=(bevel){} edge=(bevel){}\n",
-            kdl::KdlValue::String(target.to_string()),
-            kdl::KdlValue::String(fmt(wall)),
-            kdl::KdlValue::String(fmt(edge)),
-        );
-        doc.nodes_mut().push(line.parse::<kdl::KdlNode>().ok()?);
-        Some(doc.to_string())
-    }
-
-    fn is_target(node: &kdl::KdlNode, target: &str) -> bool {
-        node.name().value() == "knobs" && node.get("target").and_then(|e| e.value().as_string()) == Some(target)
-    }
-
-    #[cfg(test)]
-    mod tests {
-        use super::*;
-
-        /// A Save adds or replaces exactly its own target's node and leaves
-        /// the others alone; a lookup reads back what was saved, and a
-        /// target with no node (or a corrupt triple) reads as unsaved.
-        #[test]
-        fn knob_state_keeps_one_node_per_target() {
-            let a = (0.2, 0.7, 0.9);
-            let b = (0.5, 0.5, 0.5);
-            let c = (0.1, 0.1, 0.1);
-            let s1 = upsert("", "shared", a, b).unwrap();
-            let s2 = upsert(&s1, "/x/config.kdl", c, c).unwrap();
-            let s3 = upsert(&s2, "shared", b, a).unwrap();
-            assert_eq!(s3.matches("knobs ").count(), 2, "{s3}");
-            assert_eq!(lookup(&s3, "shared"), (Some(b), Some(a)));
-            assert_eq!(lookup(&s3, "/x/config.kdl"), (Some(c), Some(c)));
-            assert_eq!(lookup(&s3, "/y/config.kdl#some.key"), (None, None));
-            assert!(s3.contains("(bevel)\"0.200,0.700,0.900\""), "the (bevel) value format: {s3}");
-            assert_eq!(lookup("knobs target=\"shared\" wall=\"junk\"", "shared"), (None, None));
-            assert!(upsert("not kdl {{{", "shared", a, b).is_none(), "a corrupt file is refused, not replaced");
-        }
-
-        #[test]
-        fn a_target_is_the_shared_file_a_path_or_a_key() {
-            let shared = std::path::Path::new("/home/me/.config/cce/config.kdl");
-            let app = std::path::Path::new("/home/me/.config/cce/cce-designer/config.kdl");
-            assert_eq!(target_id(shared, shared, None), "shared");
-            assert_eq!(target_id(app, shared, None), app.display().to_string());
-            assert_eq!(target_id(shared, shared, Some("style.surface.desktop.line_relief")), format!("{}#style.surface.desktop.line_relief", shared.display()));
-        }
-    }
-}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 0d0e0b0..c4574bb 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -76,8 +76,6 @@ mod bridge;
 pub use bridge::*;
 mod section;
 pub use section::*;
-mod panes;
-pub use panes::*;
 
 static SECTION_PADDING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SECTION_PADDING, |s| &mut s.layout.SECTION_PADDING);
 
diff --git a/src/layout/panes.rs b/src/layout/panes.rs
deleted file mode 100644
index 963b9c2..0000000
--- a/src/layout/panes.rs
+++ /dev/null
@@ -1,96 +0,0 @@
-//! Pane geometry for a window of three resizable columns, and for a circular pane:
-//! cce-designer's. Plain arithmetic over rects and points, no layout pass.
-
-pub struct SplitterLayout {
-    pub splitter1_x: f32,
-    pub splitter2_x: f32,
-    pub splitter_width: f32,
-    pub min_column_width: f32,
-}
-
-impl SplitterLayout {
-    pub fn new(width: f32, splitter_width: f32, min_column_width: f32) -> Self {
-        let s1 = (width - 2.0 * splitter_width) / 3.0;
-        let s2 = s1 + splitter_width + (width - 2.0 * splitter_width) / 3.0;
-        Self {
-            splitter1_x: s1,
-            splitter2_x: s2,
-            splitter_width,
-            min_column_width,
-        }
-    }
-
-    pub fn clamp(&mut self, total_width: f32, detached_circular_network: bool) {
-        if detached_circular_network {
-            let min_s2 = self.min_column_width;
-            let max_s2 = (total_width - self.min_column_width).max(min_s2);
-            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
-        } else {
-            let min_s1 = self.min_column_width;
-            let max_s1 = (self.splitter2_x - self.splitter_width - self.min_column_width).max(min_s1);
-            self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
-            let min_s2 = self.splitter1_x + self.splitter_width + self.min_column_width;
-            let max_s2 = (total_width - self.min_column_width).max(min_s2);
-            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
-        }
-    }
-
-    pub fn scale(&mut self, factor: f32) {
-        self.splitter1_x *= factor;
-        self.splitter2_x *= factor;
-    }
-
-    pub fn left_col(&self) -> (f32, f32) { // (x, width)
-        (0.0, self.splitter1_x)
-    }
-
-    pub fn center_col(&self) -> (f32, f32) { // (x, width)
-        let x = self.splitter1_x + self.splitter_width;
-        (x, self.splitter2_x - x)
-    }
-
-    pub fn right_col(&self, total_width: f32) -> (f32, f32) { // (x, width)
-        let x = self.splitter2_x + self.splitter_width;
-        (x, (total_width - x).max(0.0))
-    }
-}
-
-pub struct CircularPaneLayout {
-    pub x: f32,
-    pub y: f32,
-    pub r: f32,
-}
-
-impl CircularPaneLayout {
-    pub fn new(x: f32, y: f32, r: f32) -> Self {
-        Self { x, y, r }
-    }
-
-    pub fn hit_test_content(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist_sq = dx * dx + dy * dy;
-        dist_sq <= self.r * self.r && cy >= self.y - self.r + 45.0 + menubar_h + breadcrumb_h
-    }
-
-    pub fn hit_test_menubar(&self, cx: f32, cy: f32, menubar_h: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist = (dx * dx + dy * dy).sqrt();
-        dist >= self.r - menubar_h && dist <= self.r && cy < self.y
-    }
-
-    pub fn hit_test_breadcrumb(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist_sq = dx * dx + dy * dy;
-        dist_sq <= self.r * self.r && cy >= self.y - self.r + menubar_h && cy < self.y - self.r + 45.0 + menubar_h + breadcrumb_h
-    }
-
-    pub fn hit_test_border(&self, cx: f32, cy: f32, border_thickness: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist = (dx * dx + dy * dy).sqrt();
-        dist >= self.r - border_thickness && dist <= self.r
-    }
-}