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

commit9f2775e3d20b0dd069940e71c7c2c9fc5ac2623d
parent342a57490b
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 13:59
refactor: retire the flat frost keys — plate { frost … } is the one spelling

`style.surface.plate.blur` / `.radius` / `.backdrop_compression` /
`.refraction` were read as aliases of the `plate { frost radius=
compression= refraction= }` block since the block arrived earlier
today, and `backdrop_compression` was accepted inside any `frost` child
as an alias of `compression`. An alias that keeps working is a second
spelling forever, so the loader no longer reads any of them: a config
still carrying one is reported once by path (`color::retired_frost_keys`,
a warning that names the block spelling) and the key does nothing —
`blur=true` alone is sharp, and the warning is what says why. The
`plate_blur` flatten arm goes with them (no registry reader existed).

cce-relief writes the block — only for a frosted plate, since the block
is what frosts one — seeds its Frost sliders from the block, else from a
retired key it still finds (seed only), honours a legacy `blur` for the
frosted switch the same way, and removes the four retired keys on Save,
so a file migrates the first time it is saved. The two material
fixtures move onto the block; the frost-block test is now the
retirement's test (the retired keys flip neither the switch nor the
recipe), and `retired_frost_keys_are_named_by_path_and_the_block_is_not`
covers the report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md             |  21 ++++++----
 src/bin/cce-relief.rs |  63 +++++++++++++++++++----------
 src/color.rs          | 107 +++++++++++++++++++++++++++++++++++---------------
 src/config.rs         |   4 +-
 src/layout.rs         |   1 -
 src/scene/material.rs |  35 +++++++++++------
 6 files changed, 158 insertions(+), 73 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 46214c2..1522538 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -404,10 +404,17 @@ What this buys, and where the code is heading:
   default plate material's recipe is spelled `style.surface.plate { frost
   radius=5.5 compression=0 refraction=0 }` — the same `frost` child a named
   material has — with a bare `frost` frosted at the defaults and `frost
-  (bool)false` off. The four keys it replaces (`blur` as the switch,
-  `radius`, `backdrop_compression`, `refraction`) are still read as aliases,
-  and the block governs when both are present; cce-relief writes whichever
-  spelling the config already uses. The enum variant is `Frost::Unfrosted`
+  (bool)false` off. The four keys it replaced (`blur` as the switch,
+  `radius`, `backdrop_compression`, `refraction`) were aliases for the rest
+  of that day and are RETIRED: the loader reports one it finds
+  (`color::retired_frost_keys`, a warning naming the path) and does not read
+  it — so a config with `blur=true` and no block is sharp, and the warning
+  is what says why. A material's `frost` child spells its compression
+  `compression` too; its `backdrop_compression` alias went with them.
+  cce-relief writes the block (only for a frosted plate, since the block is
+  what frosts one), seeds from a retired key it still finds, and removes
+  the retired keys on Save, so a file migrates the first time it is saved.
+  The enum variant is `Frost::Unfrosted`
   (was `Opaque`): it means the plate never samples its backdrop, and a
   translucent tint stays translucent with a SHARP view through it — which
   is what the old name kept reading as "covers everything". The pane tint
@@ -427,7 +434,7 @@ What this buys, and where the code is heading:
   "liquid glass" legibility problem, and why refraction and specular cannot
   help: they are shape cues, and legibility is a luminance budget.
 
-  `style.surface.plate.backdrop_compression` (0..1, `color::plate_backdrop_
+  `style.surface.plate { frost compression=… }` (0..1, `color::plate_backdrop_
   compression`, **default 0** — every existing config keeps today's look)
   remaps the blurred backdrop's luminance toward the plate's own key before
   the tint, holding its chromaticity. It is not opacity and not "darken": it
@@ -483,12 +490,12 @@ What this buys, and where the code is heading:
   the rung's legacy material (unset fields fall back; no `frost` child = opaque; a
   binding wins over the legacy keys; an undefined name warns and degrades to legacy).
   The DE finish's three fixed terms are `style.surface.relief.spec / shininess /
-  curvature`; the default frost's blur sigma is `style.surface.plate.radius`. cce-relief
+  curvature`; the default frost's blur sigma is `plate { frost radius=… }`. cce-relief
   edits them (Finish and Frost columns) and writes into the bound material's node or the
   DE keys — never restructuring an unbound config. KDL trap when writing fixtures: two
   nodes on one line need a `;`, and `a { b }` on one line is a parse error the loader
   swallows into an empty document.
-- **`style.surface.plate.refraction` (0..1, default 0) is the rim, and it buys
+- **`plate { frost refraction=… }` (0..1, default 0) is the rim, and it buys
   no legibility.** It is the answer to the other half of the question — not
   "can I read this" but "is this an object". The roll is a real surface with a
   real tilt, and `sv_rim` IS that tilt (the unnormalized normal's horizontal
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 7701932..628fa5c 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1155,22 +1155,27 @@ impl BevelPopup {
                             & w(&format!("{m}.frost.radius"), &radius)))
             }
             None => {
-                // Into the `plate { frost … }` block when the config spells
-                // it that way (the block governs, so a write to the old
-                // scattered keys would be read and then overridden), else
-                // the scattered keys the config already uses.
-                let block = cce_ui::config::cached_config().pointer("/style/surface/plate/frost").is_some();
-                let (kc, kr, kd) = if block {
-                    ("style.surface.plate.frost.compression", "style.surface.plate.frost.refraction", "style.surface.plate.frost.radius")
-                } else {
-                    ("style.surface.plate.backdrop_compression", "style.surface.plate.refraction", "style.surface.plate.radius")
-                };
+                // 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"]
+                    .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)
-                    & w(kc, &comp)
-                    & w(kr, &refr)
-                    & w(kd, &radius)
+                    & (!frosted
+                        || (w("style.surface.plate.frost.compression", &comp)
+                            & w("style.surface.plate.frost.refraction", &refr)
+                            & w("style.surface.plate.frost.radius", &radius)))
+                    & migrated
             }
         }
     }
@@ -1518,12 +1523,26 @@ impl Application for BevelPopup {
             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() && 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,
-                )),
+                || (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())),
@@ -1541,13 +1560,17 @@ impl Application for BevelPopup {
                 cce_ui::scene::Frost::Unfrosted => (0.0, 0.0, cce_ui::scene::Frost::DEFAULT_RADIUS),
             },
         };
-        let frost_seed = |k: &str, plate_key: &str, process: f32| -> f32 {
+        // 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/{plate_key}")))
+                .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("backdrop_compression", "backdrop_compression", pcomp);
+        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);
diff --git a/src/color.rs b/src/color.rs
index ac58098..efd3a10 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -681,35 +681,29 @@ fn parse_and_set_colors(content: &str) {
         if let Ok(mut lock) = PARAM_BG_COLOR.write() { *lock = c; }
         if let Ok(mut lock) = PANE_COLOR_WHOLE.write() { *lock = true; }
     }
-    if let Some(c) = val
-        .pointer("/style/surface/plate/backdrop_compression")
-        .and_then(|v| v.as_f64())
-    {
-        if let Ok(mut lock) = PLATE_BACKDROP_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
-    }
-    if let Some(r) = val.pointer("/style/surface/plate/refraction").and_then(|v| v.as_f64()) {
-        if let Ok(mut lock) = PLATE_REFRACTION.write() { *lock = (r as f32).clamp(0.0, 1.0); }
-    }
-    if let Some(blur) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_bool()) {
-        if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur; }
-    } else if let Some(blur_val) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_f64()) {
-        if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur_val > 0.001; }
-    }
-    if let Some(r) = val.pointer("/style/surface/plate/radius").and_then(|v| v.as_f64()) {
-        if let Ok(mut lock) = PLATE_FROST_RADIUS.write() { *lock = (r as f32).max(0.0); }
-    }
-    // The one-block spelling (2026-09-28), the same shape a named
-    // material's `frost` child has: `plate { frost radius=5.5
-    // compression=0 refraction=0 }` is frosted with those knobs, a bare
-    // `frost` is frosted at the defaults, `frost (bool)false` is not
-    // frosted. Read AFTER the four scattered keys (`blur`, `radius`,
-    // `backdrop_compression`, `refraction`), which stay as aliases, so a
-    // config that says both is governed by the block.
+    // The default plate's frost is ONE block, the same shape a named
+    // material's `frost` child has: `plate { frost radius=5.5 compression=0
+    // refraction=0 }` is frosted with those knobs, a bare `frost` is frosted
+    // at the defaults, `frost (bool)false` is not frosted. The four scattered
+    // keys it replaced on 2026-09-28 (`blur` as the switch, `radius`,
+    // `backdrop_compression`, `refraction`) were read as aliases for the rest
+    // of that day and are retired: a config still carrying one is reported
+    // (`retired_frost_keys`) and the key does nothing, because an alias that
+    // keeps working is a second spelling forever. The block is absent → not
+    // frosted, which is what `blur=true` alone now amounts to; the report
+    // is what tells the user why their plates went sharp.
+    let retired = retired_frost_keys(&val);
+    if !retired.is_empty() {
+        log::warn!(
+            "retired frost keys in config: {} — spell the frost as `plate {{ frost radius= compression= refraction= }}` (or a material's `frost` child with `compression`)",
+            retired.join(", ")
+        );
+    }
     match val.pointer("/style/surface/plate/frost") {
         Some(serde_json::Value::Object(fo)) => {
             let num = |k: &str| fo.get(k).and_then(|v| v.as_f64()).map(|v| v as f32);
             if let Ok(mut lock) = PLATE_BLUR.write() { *lock = true; }
-            if let Some(c) = num("compression").or_else(|| num("backdrop_compression")) {
+            if let Some(c) = num("compression") {
                 if let Ok(mut lock) = PLATE_BACKDROP_COMPRESSION.write() { *lock = c.clamp(0.0, 1.0); }
             }
             if let Some(r) = num("refraction") {
@@ -752,7 +746,7 @@ fn parse_and_set_colors(content: &str) {
                 let Some(node) = node.as_object() else { continue };
                 let frost = node.get("frost").map(|fr| match fr.as_object() {
                     Some(fo) => FrostDef {
-                        compression: num(fo.get("backdrop_compression").or(fo.get("compression"))),
+                        compression: num(fo.get("compression")),
                         refraction: num(fo.get("refraction")),
                         radius: num(fo.get("radius")),
                     },
@@ -785,6 +779,31 @@ fn parse_and_set_colors(content: &str) {
     }
 }
 
+/// The retired frost spellings a parsed config still carries, as the dotted
+/// paths a user would grep for: the four flat keys under `plate` (`blur`,
+/// `radius`, `backdrop_compression`, `refraction`) and `backdrop_compression`
+/// inside any `frost` child (the plate's, or a named material's), whose one
+/// name is `compression`. Empty for a clean config.
+pub fn retired_frost_keys(val: &serde_json::Value) -> Vec<String> {
+    let mut found = Vec::new();
+    for k in ["blur", "radius", "backdrop_compression", "refraction"] {
+        if val.pointer(&format!("/style/surface/plate/{k}")).is_some() {
+            found.push(format!("style.surface.plate.{k}"));
+        }
+    }
+    if val.pointer("/style/surface/plate/frost/backdrop_compression").is_some() {
+        found.push("style.surface.plate.frost.backdrop_compression".to_string());
+    }
+    if let Some(mats) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
+        for (name, node) in mats {
+            if node.pointer("/frost/backdrop_compression").is_some() {
+                found.push(format!("style.surface.material.{name}.frost.backdrop_compression"));
+            }
+        }
+    }
+    found
+}
+
 fn load_colors_once() {
     use std::sync::Once;
     static INIT: Once = Once::new();
@@ -1921,8 +1940,8 @@ pub fn set_plate_refraction(r: f32) {
 
 /// Whether the default plate material is frosted at all — the switch on
 /// `Frost::from_style`. Spelled `style.surface.plate.frost` (a block, the
-/// knobs inside it; see the loader) or, as an alias, the older
-/// `style.surface.plate.blur` bool.
+/// knobs inside it; see the loader). The older `style.surface.plate.blur`
+/// bool is retired and reported, not read.
 static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
 
 /// The finish's three fixed terms — specular strength, shininess exponent,
@@ -1935,8 +1954,8 @@ static FINISH_SPEC: RwLock<f32> = RwLock::new(0.4);
 static FINISH_SHININESS: RwLock<f32> = RwLock::new(24.0);
 static FINISH_CURVATURE: RwLock<f32> = RwLock::new(0.2);
 
-/// The default material's blur radius (`style.surface.plate.radius`, the
-/// kernel sigma in logical px) — [`crate::scene::Frost::DEFAULT_RADIUS`]
+/// The default material's blur radius (`style.surface.plate.frost.radius`,
+/// the kernel sigma in logical px) — [`crate::scene::Frost::DEFAULT_RADIUS`]
 /// unless config says otherwise.
 static PLATE_FROST_RADIUS: RwLock<f32> = RwLock::new(crate::scene::material::Frost::DEFAULT_RADIUS);
 
@@ -2231,6 +2250,32 @@ mod tests {
         assert_eq!(plate_bevel_width(), crate::layout::bevel_width(), "cleared: back to the relief width");
     }
 
+    /// The four flat frost keys and the `backdrop_compression` spelling
+    /// inside a `frost` child are reported by path; the block itself and a
+    /// material's `compression` are not.
+    #[test]
+    fn retired_frost_keys_are_named_by_path_and_the_block_is_not() {
+        let clean: serde_json::Value = serde_json::json!({ "style": { "surface": {
+            "plate": { "frost": { "radius": 5.5, "compression": 0.0, "refraction": 0.0 }, "color": "#6c6c7bf2" },
+            "material": { "glass": { "frost": { "compression": 0.6 } } }
+        } } });
+        assert!(retired_frost_keys(&clean).is_empty());
+        let old: serde_json::Value = serde_json::json!({ "style": { "surface": {
+            "plate": { "blur": true, "radius": 1.5, "backdrop_compression": 0.85, "refraction": 0.3,
+                       "frost": { "backdrop_compression": 0.2 } },
+            "material": { "glass": { "frost": { "backdrop_compression": 0.6 } } }
+        } } });
+        assert_eq!(retired_frost_keys(&old), vec![
+            "style.surface.plate.blur",
+            "style.surface.plate.radius",
+            "style.surface.plate.backdrop_compression",
+            "style.surface.plate.refraction",
+            "style.surface.plate.frost.backdrop_compression",
+            "style.surface.material.glass.frost.backdrop_compression",
+        ]);
+        assert!(retired_frost_keys(&serde_json::json!({})).is_empty());
+    }
+
     #[test]
     fn plate_refraction_defaults_off_and_clamps() {
         assert_eq!(plate_refraction(), 0.0, "off unless a config asks");
@@ -2247,7 +2292,7 @@ mod tests {
     /// Off is load-bearing: it is the behaviour every config already in the
     /// wild has, and a toolkit-wide default that changed how every frosted
     /// surface in the DE looks would arrive unannounced in eighteen apps.
-    /// Opting in is a per-app `style.surface.plate.backdrop_compression`.
+    /// Opting in is a per-app `style.surface.plate { frost compression=… }`.
     #[test]
     fn backdrop_compression_defaults_off_and_clamps() {
         assert_eq!(plate_backdrop_compression(), 0.0, "off unless a config asks");
diff --git a/src/config.rs b/src/config.rs
index b808e8f..68d0144 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -887,14 +887,14 @@ mod tests {
     fn material_keys_write_as_frost_and_finish_props() {
         use super::{parse_kdl_to_json, update_kdl_in_memory};
         let mut doc = kdl::KdlDocument::new();
-        assert!(update_kdl_in_memory(&mut doc, "style.surface.material.glass.frost.backdrop_compression", "0.6", "style"));
+        assert!(update_kdl_in_memory(&mut doc, "style.surface.material.glass.frost.compression", "0.6", "style"));
         assert!(update_kdl_in_memory(&mut doc, "style.surface.material.glass.frost.refraction", "0.3", "style"));
         assert!(update_kdl_in_memory(&mut doc, "style.surface.material.glass.finish.spec", "0.4", "style"));
         assert!(update_kdl_in_memory(&mut doc, "style.surface.material.glass.color", "#05050840", "style"));
         assert!(update_kdl_in_memory(&mut doc, "style.surface.plate.material", "glass", "style"));
         let text = doc.to_string();
         let val = parse_kdl_to_json(&text);
-        assert_eq!(val.pointer("/style/surface/material/glass/frost/backdrop_compression").and_then(|v| v.as_f64()), Some(0.6), "{text}");
+        assert_eq!(val.pointer("/style/surface/material/glass/frost/compression").and_then(|v| v.as_f64()), Some(0.6), "{text}");
         assert_eq!(val.pointer("/style/surface/material/glass/frost/refraction").and_then(|v| v.as_f64()), Some(0.3));
         assert_eq!(val.pointer("/style/surface/material/glass/finish/spec").and_then(|v| v.as_f64()), Some(0.4));
         assert_eq!(val.pointer("/style/surface/material/glass/color").and_then(|v| v.as_str()), Some("#05050840"));
diff --git a/src/layout.rs b/src/layout.rs
index e443c8b..9437d90 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -363,7 +363,6 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.surface.plate.color" => "plate_color",
                 "style.surface.plate.border_color" => "plate_border_color",
                 "style.surface.plate.border_thickness" => "plate_border_thickness",
-                "style.surface.plate.blur" => "plate_blur",
                 "input.touchpad.natural_scroll" => "touchpad_natural_scroll",
                 
                 other => {
diff --git a/src/scene/material.rs b/src/scene/material.rs
index 56ed111..d4fb58c 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -628,7 +628,8 @@ mod tests {
         style {
             surface {
                 param color=(rgba)"#05050840"
-                plate backdrop_compression=(f64)0.6 refraction=(f64)0.3 bevel_width=(f64)12.0 blur=(bool)true {
+                plate bevel_width=(f64)12.0 {
+                    frost compression=(f64)0.6 refraction=(f64)0.3
                     root corner_radius=(i64)24
                 }
                 relief depth=(f64)0.08
@@ -642,7 +643,7 @@ mod tests {
                 material {
                     glass {
                         color (rgba)"#05050840"
-                        frost backdrop_compression=(f64)0.6 refraction=(f64)0.3
+                        frost compression=(f64)0.6 refraction=(f64)0.3
                     }
                 }
                 plate material="glass" bevel_width=(f64)12.0 {
@@ -693,28 +694,38 @@ mod tests {
         assert_eq!(crate::color::material_binding(PlateRung::Pane), None);
     }
 
-    /// The default material's frost as ONE block — `plate { frost radius=…
+    /// The default material's frost is ONE block — `plate { frost radius=…
     /// compression=… refraction=… }`, the shape a named material's `frost`
-    /// child already had — with the four scattered keys (`blur`, `radius`,
-    /// `backdrop_compression`, `refraction`) still read as aliases: a bare
-    /// `frost` is frosted at the defaults, `frost (bool)false` is not, and
-    /// when both spellings are present the block governs.
+    /// child already had: a bare `frost` is frosted at the defaults, `frost
+    /// (bool)false` is not. The four scattered keys it replaced (`blur`,
+    /// `radius`, `backdrop_compression`, `refraction`) are RETIRED, not
+    /// aliases: a config carrying one is reported by path
+    /// (`color::retired_frost_keys`) and the key does nothing — `blur=true`
+    /// alone is sharp, a flat `radius` moves nothing, and the old knob name
+    /// inside the block is ignored too.
     #[test]
-    fn the_frost_block_is_one_spelling_of_the_default_recipe() {
+    fn the_frost_block_is_the_only_spelling_of_the_default_recipe() {
         let _lock = crate::color::test_color_state_lock();
         crate::layout::lazy_init_style_registry();
         let _ = crate::color::plate_blur();
+        let doc = |plate: &str| format!("style {{\n surface {{\n plate {{\n {plate}\n }}\n }}\n}}\n");
         let load = |plate: &str| {
-            crate::color::reload_colors(&format!("style {{\n surface {{\n plate {{\n {plate}\n }}\n }}\n}}\n"));
+            crate::color::reload_colors(&doc(plate));
             (crate::color::plate_blur(), Frost::from_style())
         };
+        let retired = |plate: &str| crate::color::retired_frost_keys(&crate::config::parse_kdl_to_json(&doc(plate)));
         let frosted = |c: f32, r: f32, rad: f32| Frost::Frosted { compression: c, refraction: r, radius: rad };
         assert_eq!(load("frost radius=(f64)3.0 compression=(f64)0.4 refraction=(f64)0.1"), (true, frosted(0.4, 0.1, 3.0)));
-        assert_eq!(load("frost backdrop_compression=(f64)0.2"), (true, frosted(0.2, 0.1, 3.0)), "the old knob name is accepted inside the block; unset knobs keep their last value");
+        assert!(retired("frost radius=(f64)3.0 compression=(f64)0.4 refraction=(f64)0.1").is_empty());
+        assert_eq!(load("frost backdrop_compression=(f64)0.2"), (true, frosted(0.4, 0.1, 3.0)), "the old knob name inside the block is ignored; unset knobs keep their last value");
+        assert_eq!(retired("frost backdrop_compression=(f64)0.2"), vec!["style.surface.plate.frost.backdrop_compression"]);
         assert_eq!(load("frost").0, true, "a bare `frost` is frosted");
         assert_eq!(load("frost (bool)false").0, false);
-        assert_eq!(load("blur (bool)true\n radius (f64)2.0"), (true, frosted(0.2, 0.1, 2.0)), "the scattered spelling still works");
-        assert_eq!(load("blur (bool)false\n frost compression=(f64)0.7"), (true, frosted(0.7, 0.1, 2.0)), "the block governs");
+        // `from_style` is the RECIPE, `plate_blur` the switch: the retired
+        // keys flip neither — the switch stays off and the radius stays 3.
+        assert_eq!(load("blur (bool)true\n radius (f64)2.0"), (false, frosted(0.4, 0.1, 3.0)), "the retired spelling frosts nothing and moves nothing");
+        assert_eq!(retired("blur (bool)true\n radius (f64)2.0"), vec!["style.surface.plate.blur", "style.surface.plate.radius"]);
+        assert_eq!(load("blur (bool)false\n frost compression=(f64)0.7"), (true, frosted(0.7, 0.1, 3.0)), "the block is read, the retired key is not");
         crate::color::reload_colors("");
     }