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

commitbdf62986819a11c0d060d9d9d119735e14c09be8
parentda6d345474
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 12:33
refactor: Frost::Unfrosted, a `plate { frost … }` block, and plate.pane.color

Three renamings that were each a trap:

Frost::Opaque is Frost::Unfrosted. The variant means the plate never
samples its backdrop; a translucent tint stays translucent with a sharp
view through it, which "opaque" kept reading as "solid".

The default plate material's frost is one block, `style.surface.plate
{ frost radius=5.5 compression=0 refraction=0 }` — the shape a named
material's `frost` child already had. A bare `frost` is frosted at the
defaults, `frost (bool)false` is not. The four scattered 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, and spells
a named material's knob `compression` (both are read).

The pane tint has a clear spelling, `style.surface.plate.pane.color`,
whose alpha is the whole tint strength; the legacy `param.color` is
still multiplied by the top-level `plate_opacity` line as it always was
(color::pane_color_is_whole says which was used).

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

 CLAUDE.md             |  15 +++++++
 src/bin/cce-relief.rs |  22 +++++++---
 src/color.rs          |  53 ++++++++++++++++++++++++
 src/scene/material.rs | 109 ++++++++++++++++++++++++++++++++++++++------------
 src/scene/paint.rs    |   2 +-
 5 files changed, 168 insertions(+), 33 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index ac8ffef..e8bfa32 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -400,6 +400,21 @@ What this buys, and where the code is heading:
   **6-digit** hex sets alpha to **1.0**, so dropping the last byte off a
   translucent plate colour makes it fully opaque rather than leaving it
   alone.
+- **Frost is one block, and "unfrosted" is not "solid"** (2026-09-28). The
+  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`
+  (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
+  has a clear spelling too, `style.surface.plate.pane.color`, whose alpha
+  is the whole tint strength; the legacy `style.surface.param.color` is
+  still multiplied by the top-level `plate_opacity` line, as it always was
+  (`color::pane_color_is_whole`).
 - **A frosted plate's legibility is `backdrop_compression`, not opacity.**
   Blur destroys a backdrop's spatial DETAIL and preserves its mean LUMINANCE,
   and text contrast is a mean-luminance property — so `resolve_blur`'s closing
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 9e92714..61b4085 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1138,17 +1138,27 @@ impl BevelPopup {
                     & w(&format!("{m}.finish.shininess"), &shine)
                     & w(&format!("{m}.finish.curvature"), &curv)
                     & (!frosted
-                        || (w(&format!("{m}.frost.backdrop_compression"), &comp)
+                        || (w(&format!("{m}.frost.compression"), &comp)
                             & w(&format!("{m}.frost.refraction"), &refr)
                             & 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")
+                };
                 w("style.surface.relief.spec", &spec)
                     & w("style.surface.relief.shininess", &shine)
                     & w("style.surface.relief.curvature", &curv)
-                    & w("style.surface.plate.backdrop_compression", &comp)
-                    & w("style.surface.plate.refraction", &refr)
-                    & w("style.surface.plate.radius", &radius)
+                    & w(kc, &comp)
+                    & w(kr, &refr)
+                    & w(kd, &radius)
             }
         }
     }
@@ -1499,9 +1509,9 @@ impl Application for BevelPopup {
         };
         let (pcomp, prefr, pradius) = match pane.frost {
             cce_ui::scene::Frost::Frosted { compression, refraction, radius } => (compression, refraction, radius),
-            cce_ui::scene::Frost::Opaque => match cce_ui::scene::Frost::from_style() {
+            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::Opaque => (0.0, 0.0, cce_ui::scene::Frost::DEFAULT_RADIUS),
+                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 {
diff --git a/src/color.rs b/src/color.rs
index fc253e4..1ae7f6c 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -664,6 +664,13 @@ fn parse_and_set_colors(content: &str) {
     }
     if let Some(c) = get_color("/style/surface/param/color") {
         if let Ok(mut lock) = PARAM_BG_COLOR.write() { *lock = c; }
+        if let Ok(mut lock) = PANE_COLOR_WHOLE.write() { *lock = false; }
+    }
+    // The clear spelling of the same tint: the PANE plate's colour, whole —
+    // its alpha is the tint strength and nothing multiplies it.
+    if let Some(c) = get_color("/style/surface/plate/pane/color") {
+        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")
@@ -682,6 +689,35 @@ fn parse_and_set_colors(content: &str) {
     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.
+    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 Ok(mut lock) = PLATE_BACKDROP_COMPRESSION.write() { *lock = c.clamp(0.0, 1.0); }
+            }
+            if let Some(r) = num("refraction") {
+                if let Ok(mut lock) = PLATE_REFRACTION.write() { *lock = r.clamp(0.0, 1.0); }
+            }
+            if let Some(r) = num("radius") {
+                if let Ok(mut lock) = PLATE_FROST_RADIUS.write() { *lock = r.max(0.0); }
+            }
+        }
+        Some(serde_json::Value::Null) => {
+            if let Ok(mut lock) = PLATE_BLUR.write() { *lock = true; }
+        }
+        Some(serde_json::Value::Bool(on)) => {
+            if let Ok(mut lock) = PLATE_BLUR.write() { *lock = *on; }
+        }
+        _ => {}
+    }
 
     // The DE finish beyond its strength (`relief.depth` / `light`, which
     // lives in the style registry): the three terms that were literals.
@@ -965,12 +1001,25 @@ pub const PARAM_BG: [f32; 4] = [0.10, 0.10, 0.14, 0.25];
 /// runtime (the designer's Style section "Plate Color"). Alpha doubles as the
 /// frost strength under plate blur.
 static PARAM_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PARAM_BG);
+/// Whether the pane tint came in as `style.surface.plate.pane.color` — the
+/// spelling whose alpha IS the tint strength — rather than the legacy
+/// `style.surface.param.color`, which the top-level `plate_opacity` line
+/// still multiplies (`Material::pane_legacy`). Two spellings, one tint.
+static PANE_COLOR_WHOLE: RwLock<bool> = RwLock::new(false);
 
 pub fn param_bg_color() -> [f32; 4] {
     load_colors_once();
     style_read(&PARAM_BG_COLOR)
 }
 
+/// Whether the pane tint was spelled `style.surface.plate.pane.color`, in
+/// which case its alpha is the whole tint strength and `plate_opacity` does
+/// not multiply it. See `PANE_COLOR_WHOLE`.
+pub fn pane_color_is_whole() -> bool {
+    load_colors_once();
+    style_read(&PANE_COLOR_WHOLE)
+}
+
 pub fn set_param_bg_color(color: [f32; 4]) {
     style_write(&PARAM_BG_COLOR, color);
 }
@@ -1845,6 +1894,10 @@ pub fn set_plate_refraction(r: f32) {
     style_write(&PLATE_REFRACTION, r.clamp(0.0, 1.0));
 }
 
+/// 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.
 static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
 
 /// The finish's three fixed terms — specular strength, shininess exponent,
diff --git a/src/scene/material.rs b/src/scene/material.rs
index 6fe26fb..56ed111 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -63,8 +63,12 @@ impl Finish {
 /// question.
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum Frost {
-    /// The tint alone, composited at its alpha. Not a sample of the backdrop.
-    Opaque,
+    /// No frost: the tint alone, composited at its ALPHA — a translucent
+    /// tint is still translucent, and what shows through is sharp. This
+    /// says nothing about coverage; it says the plate never samples its
+    /// backdrop. (Named `Opaque` until 2026-09-28, which read as "solid"
+    /// and was not.)
+    Unfrosted,
     /// Frosted glass: the backdrop blurred, luminance-compressed toward the
     /// tint's key, tinted at the tint's alpha; the rim refracts.
     Frosted {
@@ -104,11 +108,11 @@ impl Frost {
     /// The recipe as the plate branch reads it: `[p_host.z, p_host.w]` —
     /// compression and refraction packed in `z`, the blur radius in `w` as
     /// the kernel sigma in PHYSICAL px (the shader samples the backdrop in
-    /// physical px). `Opaque` packs to zeros: nothing reads them, and a
+    /// physical px). `Unfrosted` packs to zeros: nothing reads them, and a
     /// plate that was never frosted pushes the bytes it always did.
     pub fn pack(&self, scale: f32) -> [f32; 2] {
         match *self {
-            Frost::Opaque => [0.0, 0.0],
+            Frost::Unfrosted => [0.0, 0.0],
             Frost::Frosted { compression, refraction, radius } => {
                 let q = |v: f32| (v.clamp(0.0, 1.0) * Self::PACK_MAX).round();
                 [q(compression) * Self::PACK_BASE + q(refraction), radius.max(0.0) * scale]
@@ -138,10 +142,10 @@ impl Frost {
         }
     }
 
-    /// [`Frost::from_style`] when `on`, else [`Frost::Opaque`] — the shape of
+    /// [`Frost::from_style`] when `on`, else [`Frost::Unfrosted`] — the shape of
     /// every `blur: bool` the toolkit carries today.
     pub fn from_flag(on: bool) -> Self {
-        if on { Self::from_style() } else { Frost::Opaque }
+        if on { Self::from_style() } else { Frost::Unfrosted }
     }
 
     pub fn is_frosted(&self) -> bool {
@@ -205,7 +209,7 @@ impl MaterialDef {
                 refraction: f.refraction.unwrap_or(0.0).clamp(0.0, 1.0),
                 radius: f.radius.unwrap_or(Frost::DEFAULT_RADIUS).max(0.0),
             },
-            None => Frost::Opaque,
+            None => Frost::Unfrosted,
         };
         let mut finish = base.finish;
         if let Some(l) = self.light {
@@ -249,7 +253,7 @@ pub struct Material {
 impl Material {
     /// An opaque material of `tint` under the DE's finish.
     pub fn opaque(tint: [f32; 4]) -> Self {
-        Self { tint, frost: Frost::Opaque, finish: Finish::from_style() }
+        Self { tint, frost: Frost::Unfrosted, finish: Finish::from_style() }
     }
 
     pub fn with_tint(mut self, tint: [f32; 4]) -> Self {
@@ -272,7 +276,7 @@ impl Material {
     /// The root rung: `plate { root material="…" }` when bound, else the
     /// window's background, `style.surface.plate.root.color`
     /// (`color::page_low_color`, whose alpha IS `root_plate_opacity`).
-    /// `Frost::Opaque` on the client side by construction: a root plate's
+    /// `Frost::Unfrosted` on the client side by construction: a root plate's
     /// frost is the COMPOSITOR's (`plate.root.blur`, which the client never
     /// reads), and [`Material::fill`] under [`PlateRole::Root`] would ignore
     /// a `Frosted` here anyway.
@@ -315,7 +319,12 @@ impl Material {
 
     fn pane_legacy() -> Self {
         let mut tint = crate::color::param_bg_color();
-        tint[3] *= crate::layout::plate_opacity();
+        // `style.surface.plate.pane.color` is the whole tint, alpha
+        // included; the legacy `param.color` is still multiplied by the
+        // top-level `plate_opacity` line, as it always was.
+        if !crate::color::pane_color_is_whole() {
+            tint[3] *= crate::layout::plate_opacity();
+        }
         let frost = if crate::color::plate_blur() {
             Frost::Frosted {
                 compression: crate::color::plate_backdrop_compression(),
@@ -323,7 +332,7 @@ impl Material {
                 radius: crate::color::plate_frost_radius(),
             }
         } else {
-            Frost::Opaque
+            Frost::Unfrosted
         };
         Self { tint, frost, finish: Finish::from_style() }
     }
@@ -356,7 +365,7 @@ impl Material {
 
     /// The legacy bridge: a fill as the renderer consumed it before this
     /// type existed. A negative alpha is the frost sentinel — `Frosted` at
-    /// the DE recipe, tint alpha `|a|`; otherwise `Opaque` with the colour
+    /// the DE recipe, tint alpha `|a|`; otherwise `Unfrosted` with the colour
     /// as is. `from_fill(c).fill(Nested) == c` for every `c` a caller could
     /// hand the old API. For a call site that holds an encoded colour; a
     /// site that knows what it means says `Material::opaque` / `with_frost`.
@@ -380,7 +389,7 @@ impl Material {
     /// the tessellator encodes every prim as nested.
     pub fn for_role(&self, role: PlateRole) -> Self {
         match role {
-            PlateRole::Root => Material { frost: Frost::Opaque, ..*self },
+            PlateRole::Root => Material { frost: Frost::Unfrosted, ..*self },
             PlateRole::Nested => *self,
         }
     }
@@ -458,7 +467,7 @@ impl Material {
     ///   plate's frost is the compositor's blur-behind, never the in-app pass.
     /// - nested + [`Frost::Frosted`] → the in-app frost pass's negative-alpha
     ///   sentinel, `-|alpha|`.
-    /// - nested + [`Frost::Opaque`] → the tint as is.
+    /// - nested + [`Frost::Unfrosted`] → the tint as is.
     ///
     /// Static so a caller holding a tint and a flag (today's `PlateSpec`)
     /// encodes through the same rule without resolving a finish it does not
@@ -488,12 +497,12 @@ mod tests {
     fn fill_encodes_by_role() {
         let tint = [0.1, 0.2, 0.3, 0.8];
         assert_eq!(Material::fill_tint(tint, frosted(), PlateRole::Root)[3], 0.8, "root frost is the compositor's");
-        assert_eq!(Material::fill_tint(tint, Frost::Opaque, PlateRole::Root)[3], 0.8);
+        assert_eq!(Material::fill_tint(tint, Frost::Unfrosted, PlateRole::Root)[3], 0.8);
         assert_eq!(Material::fill_tint(tint, frosted(), PlateRole::Nested)[3], -0.8, "nested frost = sentinel");
-        assert_eq!(Material::fill_tint(tint, Frost::Opaque, PlateRole::Nested), tint, "no frost, no encoding");
+        assert_eq!(Material::fill_tint(tint, Frost::Unfrosted, PlateRole::Nested), tint, "no frost, no encoding");
         // A caller that hands a negative alpha in is normalised, not doubled.
         assert_eq!(Material::fill_tint([0.0, 0.0, 0.0, -0.5], frosted(), PlateRole::Nested)[3], -0.5);
-        assert_eq!(Material::fill_tint([0.0, 0.0, 0.0, -0.5], Frost::Opaque, PlateRole::Root)[3], 0.5);
+        assert_eq!(Material::fill_tint([0.0, 0.0, 0.0, -0.5], Frost::Unfrosted, PlateRole::Root)[3], 0.5);
         let m = Material::opaque(tint).with_frost(frosted());
         assert_eq!(m.fill(PlateRole::Nested), Material::fill_tint(tint, frosted(), PlateRole::Nested));
     }
@@ -542,7 +551,7 @@ mod tests {
         crate::color::set_plate_backdrop_compression(0.6);
         crate::color::set_plate_refraction(0.3);
         assert_eq!(Frost::from_style(), frosted());
-        assert_eq!(Frost::from_flag(false), Frost::Opaque);
+        assert_eq!(Frost::from_flag(false), Frost::Unfrosted);
         assert!(Frost::from_flag(true).is_frosted());
         crate::color::set_plate_backdrop_compression(0.0);
         crate::color::set_plate_refraction(0.0);
@@ -600,7 +609,7 @@ mod tests {
         // 0.6 / 0.3 (the designer's recipe) survive to better than a 1/255 step.
         let (c, r) = Frost::unpack(Frost::Frosted { compression: 0.6, refraction: 0.3, radius: 0.0 }.pack(1.0)[0]);
         assert!((c - 0.6).abs() < 1.0 / 510.0 && (r - 0.3).abs() < 1.0 / 510.0);
-        assert_eq!(Frost::Opaque.pack(2.0), [0.0, 0.0]);
+        assert_eq!(Frost::Unfrosted.pack(2.0), [0.0, 0.0]);
         assert_eq!(Frost::Frosted { compression: 0.0, refraction: 0.0, radius: 0.0 }.pack(2.0), [0.0, 0.0]);
 
         let wgsl = include_str!("../vk/shader2d.wgsl");
@@ -684,9 +693,57 @@ mod tests {
         assert_eq!(crate::color::material_binding(PlateRung::Pane), None);
     }
 
-    /// A binding wins over the legacy keys, a node without `frost` is opaque
-    /// whatever `plate blur` says, unset fields fall back to the rung, and a
-    /// binding to an undefined name degrades to the legacy material.
+    /// The default material's frost as 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.
+    #[test]
+    fn the_frost_block_is_one_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 load = |plate: &str| {
+            crate::color::reload_colors(&format!("style {{\n surface {{\n plate {{\n {plate}\n }}\n }}\n}}\n"));
+            (crate::color::plate_blur(), Frost::from_style())
+        };
+        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_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");
+        crate::color::reload_colors("");
+    }
+
+    /// `style.surface.plate.pane.color` is the pane tint WHOLE — its alpha is
+    /// the tint strength — where the legacy `param.color` is still multiplied
+    /// by the top-level `plate_opacity` line.
+    #[test]
+    fn the_pane_colour_spelling_is_the_whole_tint() {
+        let _lock = crate::color::test_color_state_lock();
+        crate::layout::lazy_init_style_registry();
+        let _ = crate::color::plate_blur();
+        let was = crate::layout::plate_opacity();
+        crate::layout::set_plate_opacity(0.5);
+        crate::color::reload_colors("style {\n surface {\n param color=(rgba)\"#10101880\"\n }\n}\n");
+        assert!(!crate::color::pane_color_is_whole());
+        let a = Material::pane().tint[3];
+        assert!((a - 0.25).abs() < 1e-2, "legacy: alpha 0.5 x plate_opacity 0.5, got {a}");
+        crate::color::reload_colors("style {\n surface {\n plate {\n pane color=(rgba)\"#10101880\"\n }\n }\n}\n");
+        assert!(crate::color::pane_color_is_whole());
+        let a = Material::pane().tint[3];
+        assert!((a - 0.5).abs() < 1e-2, "spelled whole: alpha 0.5 as written, got {a}");
+        crate::layout::set_plate_opacity(was);
+        crate::color::reload_colors("");
+    }
+
+    /// A binding wins over the legacy keys, a node without `frost` is
+    /// unfrosted whatever `plate blur` says, unset fields fall back to the
+    /// rung, and a binding to an undefined name degrades to the legacy
+    /// material.
     #[test]
     fn binding_semantics() {
         let _lock = crate::color::test_color_state_lock();
@@ -711,7 +768,7 @@ mod tests {
             }
         "##);
         let pane = Material::pane();
-        assert_eq!(pane.frost, Frost::Opaque, "no frost child = opaque, blur flag or not");
+        assert_eq!(pane.frost, Frost::Unfrosted, "no frost child = unfrosted, blur flag or not");
         assert_eq!(pane.tint, Material::legacy(PlateRung::Pane).tint, "no color = the rung's tint");
         assert_eq!(pane.finish.spec, 0.25);
         assert_eq!(pane.finish.shininess, 12.0);
@@ -759,9 +816,9 @@ mod tests {
     fn control_face_is_opaque_or_none() {
         let face = Material::control_face([0.2, 0.3, 0.4, 0.5]).expect("a fill is a face");
         assert_eq!(face.tint, [0.2, 0.3, 0.4, 1.0]);
-        assert_eq!(face.frost, Frost::Opaque);
+        assert_eq!(face.frost, Frost::Unfrosted);
         assert!(Material::control_face([0.2, 0.3, 0.4, 0.0]).is_none());
-        assert_eq!(Material::control().frost, Frost::Opaque);
-        assert_eq!(Material::root().frost, Frost::Opaque);
+        assert_eq!(Material::control().frost, Frost::Unfrosted);
+        assert_eq!(Material::root().frost, Frost::Unfrosted);
     }
 }
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 3c23c61..e1cf75f 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -1840,7 +1840,7 @@ mod tests {
         assert!(!spec.is_root());
         assert_eq!(spec.role(), PlateRole::Nested);
         assert!(spec.fill()[3] < 0.0, "nested frost = negative-alpha sentinel");
-        spec.material.frost = Frost::Opaque;
+        spec.material.frost = Frost::Unfrosted;
         assert_eq!(spec.fill()[3], 0.8, "no frost, no encoding");
 
         // The detach role flip (RFC 7c): a frosted nested pane becomes a