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

src/color.rs (80.6K)

   1 pub const HEADER_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
   2 pub const HEADER_ACCENT: [f32; 4] = [0.60, 0.40, 0.20, 1.0];
   3 pub const SIDEBAR_BG: [f32; 4] = [0.10, 0.10, 0.13, 1.0];
   4 pub const CONTENT_BG: [f32; 4] = [0.13, 0.13, 0.16, 0.2];
   5 pub const PANEL_IDLE: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
   6 pub const PANEL_DRAG: [f32; 4] = [0.24, 0.85, 0.50, 1.0];
   7 pub const NODE_IDLE: [f32; 4] = [0.10, 0.45, 0.70, 1.0];
   8 pub const NODE_SELECTED: [f32; 4] = [0.20, 0.65, 0.90, 1.0];
   9 pub const NODE_DRAG: [f32; 4] = [0.30, 0.80, 1.00, 1.0];
  10 pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 0.4];
  11 pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 0.6];
  12 pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 0.8];
  13 pub const STATUS_BG: [f32; 4] = [0.06, 0.06, 0.10, 1.0];
  14 pub const STATUS_ACCENT: [f32; 4] = [0.20, 0.20, 0.25, 1.0];
  15 pub const RESET_BTN_IDLE: [f32; 4] = [0.55, 0.20, 0.20, 0.4];
  16 pub const RESET_BTN_HOVER: [f32; 4] = [0.70, 0.30, 0.30, 0.6];
  17 pub const RESET_BTN_PRESS: [f32; 4] = [0.40, 0.12, 0.12, 0.8];
  18 pub const TOGGLE_OFF: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
  19 pub const TOGGLE_ON: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
  20 pub const TOGGLE_HOVER: [f32; 4] = [0.30, 0.30, 0.35, 1.0];
  21 pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
  22 
  23 use std::sync::RwLock;
  24 
  25 /// Per-thread overrides for runtime colour writes, under `cfg(test)` only.
  26 ///
  27 /// Same defect as the font flake (cce-ui 1dc0ab1) and the style registry
  28 /// beside it: these are process-wide `RwLock` statics, so a test that pins a
  29 /// colour pins it for every test running alongside. It was live —
  30 /// `test_graph_style_configuration` sets a dozen of them and restores none.
  31 ///
  32 /// Keyed by the address of the static itself, so a slot needs no name
  33 /// repeated in two places and cannot be typo'd into a silent miss. Config
  34 /// loading is unaffected: `parse_and_set_colors` writes the statics directly
  35 /// and never goes through these setters, so the loaded palette stays the
  36 /// shared base every test thread reads.
  37 #[cfg(test)]
  38 mod test_overlay {
  39     use std::any::Any;
  40     use std::cell::RefCell;
  41     use std::collections::HashMap;
  42     thread_local! {
  43         static MAP: RefCell<HashMap<usize, Box<dyn Any>>> = RefCell::new(HashMap::new());
  44     }
  45     pub fn get<T: Clone + 'static>(key: usize) -> Option<T> {
  46         MAP.with(|m| m.borrow().get(&key).and_then(|b| b.downcast_ref::<T>()).cloned())
  47     }
  48     pub fn set<T: 'static>(key: usize, val: T) {
  49         MAP.with(|m| m.borrow_mut().insert(key, Box::new(val)));
  50     }
  51 }
  52 
  53 /// Read a style static, preferring this thread's test override.
  54 #[inline]
  55 fn style_read<T: Clone + 'static>(cell: &'static RwLock<T>) -> T {
  56     #[cfg(test)]
  57     if let Some(v) = test_overlay::get::<T>(cell as *const _ as usize) {
  58         return v;
  59     }
  60     cell.read().unwrap().clone()
  61 }
  62 
  63 /// Write a style static: per-thread under `cfg(test)`, process-wide otherwise.
  64 #[inline]
  65 fn style_write<T: Clone + 'static>(cell: &'static RwLock<T>, val: T) {
  66     #[cfg(test)]
  67     test_overlay::set(cell as *const _ as usize, val);
  68     #[cfg(not(test))]
  69     if let Ok(mut lock) = cell.write() {
  70         *lock = val;
  71     }
  72 }
  73 
  74 
  75 static PAGE_LOW_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0600316, 0.0600316, 0.080219, 1.0]);
  76 static COLOR_BORDERS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.2039, 0.2039, 0.2530, 1.0]);
  77 static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
  78 static NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
  79 static NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
  80 static SIDEBAR_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(SIDEBAR_BG);
  81 static HIGHLIGHT_PRIMARY_COLOR: RwLock<[f32; 4]> = RwLock::new(HIGHLIGHT_PRIMARY);
  82 static MENUBAR_TAB_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]); // sRGB [230, 230, 242] linear
  83 static CONTROL_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.61206, 0.61206, 0.68666, 1.0]); // sRGB [204, 204, 212]
  84 static CONTROL_LABEL_COLOR_DETACHED: RwLock<[f32; 4]> = RwLock::new([0.22416, 0.22416, 0.2526, 1.0]); // sRGB [131, 131, 138]
  85 static CONTROL_LABEL_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
  86 static CONTROL_LABEL_FOCUS_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
  87 static OPACITY: RwLock<Option<f32>> = RwLock::new(None);
  88 static ROOT_PLATE_OPACITY: RwLock<Option<f32>> = RwLock::new(None);
  89 static LIST_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
  90 static LIST_ENTRY_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([1.0, 1.0, 1.0, 0.04]);
  91 static LIST_ENTRY_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([1.0, 1.0, 1.0, 0.8]);
  92 static LIST_FONT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.80, 0.80, 0.85, 1.0]);
  93 static BREADCRUMB_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
  94 static POPOVER_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
  95 static PAGE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
  96 static LAYER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
  97 static ROOT_PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
  98 
  99 // Transparent by default (alpha 0): a dropdown picks up the surface it sits
 100 // on, and its closed-state chrome is the flush inset trough alone — the
 101 // cce-files treatment, DE-wide. A configured `dropdown color=` opts a theme
 102 // back into a filled face (the paint path judges the RAW alpha).
 103 static DROPDOWN_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
 104 
 105 static TEXTBOX_PLACEHOLDER_TEXT_COLOR: RwLock<[u8; 3]> = RwLock::new([0x60, 0x60, 0x6a]);
 106 static TEXTBOX_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
 107 static TEXTBOX_BACKGROUND_EDIT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.10, 0.18, 1.0]);
 108 
 109 static ROOT_PLATE_MENUBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
 110 static ROOT_PLATE_MENUBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]);
 111 static ROOT_PLATE_MENUBAR_BLUR: RwLock<bool> = RwLock::new(false);
 112 /// Tint strength of frosted menus/popovers over the blurred backdrop:
 113 /// 1.0 is fully opaque (frost invisible), lower shows more content through.
 114 static MENU_OPACITY: RwLock<f32> = RwLock::new(0.8);
 115 /// Backdrop compression of frosted menus/popovers (`style.surface.menu.
 116 /// compression`, 0..1): how hard the blurred content beneath a menu is
 117 /// pulled toward the menu's own key, so the menu holds its legibility over
 118 /// whatever it opens above. Menu-scoped, overriding the DE recipe's
 119 /// `plate.backdrop_compression` for popovers only; a menu is read while
 120 /// something else is going on beneath it, which a pane is not.
 121 static MENU_COMPRESSION: RwLock<f32> = RwLock::new(0.6);
 122 
 123 static ROOT_PLATE_STATUSBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.06, 0.10, 1.0]);
 124 static ROOT_PLATE_STATUSBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.6666, 0.6666, 0.7333, 1.0]);
 125 static ROOT_PLATE_STATUSBAR_BLUR: RwLock<bool> = RwLock::new(false);
 126 static BUTTON_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new(BUTTON_IDLE);
 127 static RAMP_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
 128 static RAMP_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
 129 static CONTROL_PANEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.075, 0.082, 0.11, 1.0]); // default #13151cff
 130 static CONTROL_PANEL_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.161, 0.173, 0.216, 1.0]); // default #292c37ff
 131 
 132 static PROGRESS_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_BG);
 133 static PROGRESS_FILL_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_FILL);
 134 static SPINBOX_DISPLAY_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_DISPLAY);
 135 static SPINBOX_BUTTON_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON);
 136 static SPINBOX_BUTTON_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON_HOVER);
 137 static SPINBOX_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.8, 0.8, 0.83, 1.0]);
 138 
 139 static BUTTON_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
 140 static BUTTON_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
 141 
 142 static DROPDOWN_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
 143 static DROPDOWN_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.72305, 0.72305, 0.76008, 1.0]);
 144 
 145 static SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
 146 static SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
 147 
 148 static RANGE_SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
 149 static RANGE_SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
 150 
 151 static TREE_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
 152 static TREE_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
 153 static TREE_BORDER_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.25, 0.25, 0.35, 1.0]);
 154 static TREE_BORDER_FOCUS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.30, 0.50, 0.32, 1.0]);
 155 static TREE_OPEN_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
 156 static LIST_OPEN_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
 157 static LIST_CLOSE_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
 158 
 159 static TREE_SECTION_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.07, 0.07, 0.09, 1.0]);
 160 static TREE_SECTION_BG_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.10, 0.12, 0.18, 1.0]);
 161 
 162 static TREE_LEAF_BG_EVEN_COLOR: RwLock<[f32; 4]> = RwLock::new([0.09, 0.09, 0.11, 1.0]);
 163 static TREE_LEAF_BG_ODD_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.10, 1.0]);
 164 static TREE_LEAF_BG_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.12, 0.12, 0.16, 1.0]);
 165 static TREE_LEAF_BG_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.20, 0.30, 1.0]);
 166 
 167 static TREE_SECTION_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.38, 0.69, 0.94, 1.0]);
 168 static TREE_LEAF_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.80, 0.80, 0.83, 1.0]);
 169 static TREE_LEAF_TEXT_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new([0.49, 1.0, 1.0, 1.0]);
 170 
 171 static TREE_TYPE_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.78, 0.47, 0.87, 1.0]);
 172 static TREE_VALUE_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.51, 0.51, 0.54, 1.0]);
 173 static TREE_SEPARATOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.19, 1.0]);
 174 
 175 static SCROLLBAR_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.20, 0.3]);
 176 static SCROLLBAR_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new([0.60, 0.60, 0.65, 0.4]);
 177 
 178 static GRAPH_CELL_COLOR: RwLock<[f32; 3]> = RwLock::new([0.13, 0.13, 0.16]);
 179 static GRAPH_GAP_COLOR: RwLock<[f32; 3]> = RwLock::new([0.07, 0.07, 0.09]);
 180 static GRAPH_OPACITY: RwLock<f32> = RwLock::new(0.95);
 181 /// Opacity of the graph's NODE-domain content (node bodies, wires, connectors,
 182 /// node text) — `style.surface.graph.node.opacity`, deliberately independent of
 183 /// `GRAPH_OPACITY`, which fades only the pane surface (grid cells/gaps).
 184 static GRAPH_NODE_OPACITY: RwLock<f32> = RwLock::new(1.0);
 185 
 186 static GRAPH_NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
 187 static GRAPH_NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
 188 static GRAPH_NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
 189 
 190 static GRAPH_WIRE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
 191 static GRAPH_WIRE_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
 192 
 193 static GRAPH_CONNECTOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
 194 static GRAPH_CONNECTOR_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
 195 
 196 pub fn button_background_color() -> [f32; 4] {
 197     style_read(&BUTTON_BACKGROUND_COLOR)
 198 }
 199 
 200 pub fn set_button_background_color(color: [f32; 4]) {
 201     style_write(&BUTTON_BACKGROUND_COLOR, color);
 202 }
 203 
 204 pub fn button_hover_color() -> [f32; 4] {
 205     let base = button_background_color();
 206     let mut oklab = linear_srgb_to_oklab([base[0], base[1], base[2]]);
 207     oklab[0] = (oklab[0] + 0.05).min(1.0); // increase lightness slightly
 208     let rgb = oklab_to_linear_srgb(oklab);
 209     [
 210         rgb[0].clamp(0.0, 1.0),
 211         rgb[1].clamp(0.0, 1.0),
 212         rgb[2].clamp(0.0, 1.0),
 213         (base[3] + 0.20).min(1.0),
 214     ]
 215 }
 216 
 217 pub fn button_press_color() -> [f32; 4] {
 218     let base = button_background_color();
 219     let mut oklab = linear_srgb_to_oklab([base[0], base[1], base[2]]);
 220     oklab[0] = (oklab[0] - 0.07).max(0.0); // decrease lightness
 221     let rgb = oklab_to_linear_srgb(oklab);
 222     [
 223         rgb[0].clamp(0.0, 1.0),
 224         rgb[1].clamp(0.0, 1.0),
 225         rgb[2].clamp(0.0, 1.0),
 226         (base[3] + 0.40).min(1.0),
 227     ]
 228 }
 229 
 230 pub fn node_color() -> [f32; 4] {
 231     load_colors_once();
 232     style_read(&NODE_COLOR)
 233 }
 234 
 235 pub fn set_node_color(color: [f32; 4]) {
 236     style_write(&NODE_COLOR, color);
 237 }
 238 
 239 pub fn node_selected_color() -> [f32; 4] {
 240     load_colors_once();
 241     style_read(&NODE_SELECTED_COLOR)
 242 }
 243 
 244 pub fn set_node_selected_color(color: [f32; 4]) {
 245     style_write(&NODE_SELECTED_COLOR, color);
 246 }
 247 
 248 pub fn node_drag_color() -> [f32; 4] {
 249     load_colors_once();
 250     style_read(&NODE_DRAG_COLOR)
 251 }
 252 
 253 pub fn set_node_drag_color(color: [f32; 4]) {
 254     style_write(&NODE_DRAG_COLOR, color);
 255 }
 256 
 257 fn read_config() -> Option<String> {
 258     let path = crate::config::get_config_path();
 259     if let Ok(content) = std::fs::read_to_string(&path) {
 260         return Some(content);
 261     }
 262     None
 263 }
 264 
 265 fn parse_and_set_colors(content: &str) {
 266     let val = crate::config::parse_kdl_to_json(content);
 267 
 268     let parse_hex = |hex_str: &str| -> Option<[f32; 4]> {
 269         let hex = hex_str.trim_matches(|c| c == '"' || c == '\'' || c == ' ');
 270         let hex = hex.trim_start_matches('#');
 271         if hex.len() >= 8 {
 272             if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
 273                 u8::from_str_radix(&hex[0..2], 16),
 274                 u8::from_str_radix(&hex[2..4], 16),
 275                 u8::from_str_radix(&hex[4..6], 16),
 276                 u8::from_str_radix(&hex[6..8], 16),
 277             ) {
 278                 let r_f = srgb_to_linear(r as f32 / 255.0);
 279                 let g_f = srgb_to_linear(g as f32 / 255.0);
 280                 let b_f = srgb_to_linear(b as f32 / 255.0);
 281                 let a_f = a as f32 / 255.0;
 282                 return Some([r_f, g_f, b_f, a_f]);
 283             }
 284         } else if hex.len() >= 6 {
 285             if let (Ok(r), Ok(g), Ok(b)) = (
 286                 u8::from_str_radix(&hex[0..2], 16),
 287                 u8::from_str_radix(&hex[2..4], 16),
 288                 u8::from_str_radix(&hex[4..6], 16),
 289             ) {
 290                 let r_f = srgb_to_linear(r as f32 / 255.0);
 291                 let g_f = srgb_to_linear(g as f32 / 255.0);
 292                 let b_f = srgb_to_linear(b as f32 / 255.0);
 293                 return Some([r_f, g_f, b_f, 1.0]);
 294             }
 295         }
 296         None
 297     };
 298 
 299     let get_color = |pointer: &str| -> Option<[f32; 4]> {
 300         val.pointer(pointer).and_then(|v| v.as_str()).and_then(parse_hex)
 301     };
 302 
 303     let get_color_u8 = |pointer: &str| -> Option<[u8; 3]> {
 304         val.pointer(pointer).and_then(|v| v.as_str()).and_then(|hex_str| {
 305             let hex = hex_str.trim_matches(|c| c == '"' || c == '\'' || c == ' ');
 306             let hex = hex.trim_start_matches('#');
 307             if hex.len() >= 6 {
 308                 if let (Ok(r), Ok(g), Ok(b)) = (
 309                     u8::from_str_radix(&hex[0..2], 16),
 310                     u8::from_str_radix(&hex[2..4], 16),
 311                     u8::from_str_radix(&hex[4..6], 16),
 312                 ) {
 313                     return Some([r, g, b]);
 314                 }
 315             }
 316             None
 317         })
 318     };
 319 
 320     if let Some(opacity) = val.pointer("/layout/menubar_opacity").and_then(|v| v.as_f64()) {
 321         if let Ok(mut lock) = OPACITY.write() {
 322             *lock = Some(opacity as f32);
 323         }
 324     }
 325 
 326     // `/style/surface/plate/root/...` is the one spelling of the root-plate
 327     // style (RFC Phase 7a; the legacy `root plate` read-alias was removed
 328     // 2026-09-06 once every live config had migrated).
 329     if let Some(radius) = val.pointer("/style/surface/plate/root/corner_radius")
 330         .and_then(|v| v.as_f64())
 331     {
 332         if let Ok(mut lock) = ROOT_PLATE_CORNER_RADIUS.write() {
 333             *lock = radius as f32;
 334         }
 335      }
 336 
 337     if let Some(c) = get_color("/style/surface/plate/root/color") {
 338         if let Ok(mut lock) = PAGE_LOW_COLOR.write() { *lock = c; }
 339         if let Ok(mut lock) = ROOT_PLATE_OPACITY.write() { *lock = Some(c[3]); }
 340     }
 341 
 342     if let Some(c) = get_color("/style/surface/plate/root/menubar/color") {
 343         if let Ok(mut lock) = ROOT_PLATE_MENUBAR_COLOR.write() { *lock = c; }
 344     }
 345     if let Some(c) = get_color("/style/surface/plate/root/menubar/text_color") {
 346         if let Ok(mut lock) = ROOT_PLATE_MENUBAR_TEXT_COLOR.write() { *lock = c; }
 347     }
 348     if let Some(o) = val.pointer("/style/surface/menu/opacity").and_then(|v| v.as_f64()) {
 349         if let Ok(mut lock) = MENU_OPACITY.write() { *lock = (o as f32).clamp(0.0, 1.0); }
 350     }
 351     if let Some(c) = val.pointer("/style/surface/menu/compression").and_then(|v| v.as_f64()) {
 352         if let Ok(mut lock) = MENU_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
 353     }
 354 
 355     let menubar_blur_ptr = val.pointer("/style/surface/plate/root/menubar/blur");
 356     if let Some(blur) = menubar_blur_ptr.and_then(|v| v.as_bool()) {
 357         if let Ok(mut lock) = ROOT_PLATE_MENUBAR_BLUR.write() { *lock = blur; }
 358     } else if let Some(blur_val) = menubar_blur_ptr.and_then(|v| v.as_f64()) {
 359         if let Ok(mut lock) = ROOT_PLATE_MENUBAR_BLUR.write() { *lock = blur_val > 0.001; }
 360     }
 361 
 362     if let Some(c) = get_color("/style/surface/statusbar/color") {
 363         if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_COLOR.write() { *lock = c; }
 364     }
 365     if let Some(c) = get_color("/style/surface/statusbar/text_color") {
 366         if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_TEXT_COLOR.write() { *lock = c; }
 367     }
 368     if let Some(blur) = val.pointer("/style/surface/statusbar/blur").and_then(|v| v.as_bool()) {
 369         if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_BLUR.write() { *lock = blur; }
 370     } else if let Some(blur_val) = val.pointer("/style/surface/statusbar/blur").and_then(|v| v.as_f64()) {
 371         if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_BLUR.write() { *lock = blur_val > 0.001; }
 372     }
 373     if let Some(c) = get_color("/layout/color_borders_color") {
 374         if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() { *lock = c; }
 375     }
 376     if let Some(c) = get_color("/layout/paginator_sidebar_color") {
 377         if let Ok(mut lock) = SIDEBAR_BG_COLOR.write() { *lock = c; }
 378     }
 379     if let Some(c) = get_color("/style/highlight/primary") {
 380         if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() { *lock = [c[0], c[1], c[2], 0.12]; }
 381     }
 382     if let Some(c) = get_color("/style/control/button/background").or_else(|| get_color("/layout/button_background_color")) {
 383         if let Ok(mut lock) = BUTTON_BACKGROUND_COLOR.write() { *lock = c; }
 384     }
 385     if let Some(c) = get_color("/style/control/dropdown/color") {
 386         if let Ok(mut lock) = DROPDOWN_BACKGROUND_COLOR.write() { *lock = c; }
 387     }
 388     if let Some(c) = get_color_u8("/style/control/textbox/placeholder_text_color")
 389         .or_else(|| get_color_u8("/style/textbox/placeholder_text_color"))
 390         .or_else(|| get_color_u8("/style/data/textbox/placeholder_text_color")) {
 391         if let Ok(mut lock) = TEXTBOX_PLACEHOLDER_TEXT_COLOR.write() { *lock = c; }
 392     }
 393     if let Some(c) = get_color("/style/control/textbox/background_color")
 394         .or_else(|| get_color("/style/textbox/background_color"))
 395         .or_else(|| get_color("/style/data/textbox/background_color")) {
 396         if let Ok(mut lock) = TEXTBOX_BACKGROUND_COLOR.write() { *lock = c; }
 397     }
 398     if let Some(c) = get_color("/style/control/textbox/background_edit_color")
 399         .or_else(|| get_color("/style/textbox/background_edit_color"))
 400         .or_else(|| get_color("/style/data/textbox/background_edit_color")) {
 401         if let Ok(mut lock) = TEXTBOX_BACKGROUND_EDIT_COLOR.write() { *lock = c; }
 402     }
 403     if let Some(c) = get_color("/layout/menubar_tab_label_color").or_else(|| get_color("/layout/paginator_tab_label_color")) {
 404         if let Ok(mut lock) = MENUBAR_TAB_LABEL_COLOR.write() { *lock = c; }
 405     }
 406     if let Some(c) = get_color("/style/control/label/color") {
 407         if let Ok(mut lock) = CONTROL_LABEL_COLOR.write() { *lock = c; }
 408     }
 409     if let Some(c) = get_color("/style/control/label/color_detached") {
 410         if let Ok(mut lock) = CONTROL_LABEL_COLOR_DETACHED.write() { *lock = c; }
 411     }
 412     if let Some(c) = get_color("/style/control/label/hover_color") {
 413         if let Ok(mut lock) = CONTROL_LABEL_HOVER_COLOR.write() { *lock = Some(c); }
 414     }
 415     if let Some(c) = get_color("/style/control/label/focus_color") {
 416         if let Ok(mut lock) = CONTROL_LABEL_FOCUS_COLOR.write() { *lock = Some(c); }
 417     }
 418     // The toggle state colors (enabled/disabled/gradient/background) are not
 419     // read: the control inherits its plate — see the note by the retired
 420     // getters below.
 421     if let Some(c) = get_color("/style/control/ramp/background") {
 422         if let Ok(mut lock) = RAMP_BACKGROUND_COLOR.write() { *lock = c; }
 423     }
 424     if let Some(c) = get_color("/style/control/ramp/border_color") {
 425         if let Ok(mut lock) = RAMP_BORDER_COLOR.write() { *lock = c; }
 426     }
 427     if let Some(c) = get_color("/style/control/control_panel/color") {
 428         if let Ok(mut lock) = CONTROL_PANEL_COLOR.write() { *lock = c; }
 429     }
 430     if let Some(c) = get_color("/style/control/control_panel/border_color") {
 431         if let Ok(mut lock) = CONTROL_PANEL_BORDER_COLOR.write() { *lock = c; }
 432     }
 433     if let Some(c) = get_color("/style/control/spinbox/background_color") {
 434         if let Ok(mut lock) = SPINBOX_DISPLAY_COLOR.write() { *lock = c; }
 435     }
 436     if let Some(c) = get_color("/style/control/spinbox/button_color") {
 437         if let Ok(mut lock) = SPINBOX_BUTTON_COLOR.write() { *lock = c; }
 438         if let Ok(mut lock_hover) = SPINBOX_BUTTON_HOVER_COLOR.write() {
 439             *lock_hover = [
 440                 (c[0] + 0.1).min(1.0),
 441                 (c[1] + 0.1).min(1.0),
 442                 (c[2] + 0.1).min(1.0),
 443                 c[3]
 444             ];
 445         }
 446     }
 447     if let Some(c) = get_color("/style/control/spinbox/button_hover_color") {
 448         if let Ok(mut lock) = SPINBOX_BUTTON_HOVER_COLOR.write() { *lock = c; }
 449     }
 450     if let Some(c) = get_color("/style/control/spinbox/text_color") {
 451         if let Ok(mut lock) = SPINBOX_TEXT_COLOR.write() { *lock = c; }
 452     }
 453     if let Some(c) = get_color("/style/control/button/border_color").or_else(|| get_color("/style/button/border_color")) {
 454         if let Ok(mut lock) = BUTTON_BORDER_COLOR.write() { *lock = Some(c); }
 455     }
 456     if let Some(c) = get_color("/style/control/button/hover_color").or_else(|| get_color("/style/button/hover_color")) {
 457         if let Ok(mut lock) = BUTTON_HOVER_COLOR.write() { *lock = Some(c); }
 458     }
 459     if let Some(c) = get_color("/style/control/dropdown/border_color").or_else(|| get_color("/style/dropdown/border_color")) {
 460         if let Ok(mut lock) = DROPDOWN_BORDER_COLOR.write() { *lock = c; }
 461     }
 462     if let Some(c) = get_color("/style/control/dropdown/text_color").or_else(|| get_color("/style/dropdown/text_color")) {
 463         if let Ok(mut lock) = DROPDOWN_TEXT_COLOR.write() { *lock = c; }
 464     }
 465     if let Some(c) = get_color("/style/control/slider/thumb_color").or_else(|| get_color("/style/slider/thumb_color")) {
 466         if let Ok(mut lock) = SLIDER_THUMB_COLOR.write() { *lock = c; }
 467         if let Ok(mut lock_drag) = SLIDER_THUMB_DRAG_COLOR.write() {
 468             *lock_drag = [
 469                 (c[0] + 0.15).min(1.0),
 470                 (c[1] + 0.15).min(1.0),
 471                 (c[2] + 0.15).min(1.0),
 472                 c[3]
 473             ];
 474         }
 475     }
 476     if let Some(c) = get_color("/style/control/rangeslider/thumb_color").or_else(|| get_color("/style/rangeslider/thumb_color")) {
 477         if let Ok(mut lock) = RANGE_SLIDER_THUMB_COLOR.write() { *lock = c; }
 478         if let Ok(mut lock_drag) = RANGE_SLIDER_THUMB_DRAG_COLOR.write() {
 479             *lock_drag = [
 480                 (c[0] + 0.15).min(1.0),
 481                 (c[1] + 0.15).min(1.0),
 482                 (c[2] + 0.15).min(1.0),
 483                 c[3]
 484             ];
 485         }
 486     }
 487     if let Some(c) = get_color("/style/control/progressbar/background") {
 488         if let Ok(mut lock) = PROGRESS_BG_COLOR.write() { *lock = c; }
 489     }
 490     if let Some(c) = get_color("/style/control/progressbar/fill") {
 491         if let Ok(mut lock) = PROGRESS_FILL_COLOR.write() { *lock = c; }
 492     }
 493     let parsed_list_bg = get_color("/layout/list_bg_color");
 494     let parsed_breadcrumb_bg = get_color("/layout/breadcrumb_bg_color");
 495     let parsed_popover_bg = get_color("/layout/popover_bg_color");
 496 
 497     if let Some(c) = get_color("/layout/page_color") {
 498         if let Ok(mut lock) = PAGE_COLOR.write() { *lock = c; }
 499     }
 500     if let Some(c) = get_color("/layout/layer_color") {
 501         if let Ok(mut lock) = LAYER_COLOR.write() { *lock = c; }
 502     }
 503 
 504     if let Some(c) = parsed_list_bg {
 505         if let Ok(mut lock) = LIST_BG_COLOR.write() {
 506             *lock = [c[0], c[1], c[2], 0.3];
 507         }
 508     }
 509     if let Some(c) = get_color("/layout/list_entry_bg_color") {
 510         if let Ok(mut lock) = LIST_ENTRY_BG_COLOR.write() {
 511             *lock = c;
 512         }
 513     }
 514     if let Some(c) = get_color("/layout/list_entry_highlight_color") {
 515         if let Ok(mut lock) = LIST_ENTRY_HIGHLIGHT_COLOR.write() {
 516             *lock = c;
 517         }
 518     }
 519     if let Some(c) = get_color("/style/data/list/font_color").or_else(|| get_color("/layout/list_font_color")) {
 520         if let Ok(mut lock) = LIST_FONT_COLOR.write() {
 521             *lock = c;
 522         }
 523     }
 524     if let Some(c) = parsed_breadcrumb_bg {
 525         if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
 526             *lock = [c[0], c[1], c[2], 1.0];
 527         }
 528     } else if let Some(c) = parsed_list_bg {
 529         if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
 530             *lock = [c[0], c[1], c[2], 1.0];
 531         }
 532     }
 533     if let Some(c) = parsed_popover_bg {
 534         if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
 535             *lock = [c[0], c[1], c[2], 1.0];
 536         }
 537     } else if let Some(c) = parsed_list_bg {
 538         if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
 539             *lock = [c[0], c[1], c[2], 1.0];
 540         }
 541     }
 542 
 543     if let Some(c) = get_color("/style/surface/graph/cell_color") {
 544         if let Ok(mut lock) = GRAPH_CELL_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
 545     }
 546     if let Some(c) = get_color("/style/surface/graph/gap_color") {
 547         if let Ok(mut lock) = GRAPH_GAP_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
 548     }
 549     if let Some(c) = get_color("/style/surface/graph/node/color") {
 550         if let Ok(mut lock) = GRAPH_NODE_COLOR.write() { *lock = c; }
 551         if let Ok(mut lock) = NODE_COLOR.write() { *lock = c; }
 552     }
 553     if let Some(c) = get_color("/style/surface/graph/node/selected_color") {
 554         if let Ok(mut lock) = GRAPH_NODE_SELECTED_COLOR.write() { *lock = c; }
 555         if let Ok(mut lock) = NODE_SELECTED_COLOR.write() { *lock = c; }
 556     }
 557     if let Some(c) = get_color("/style/surface/graph/node/drag_color") {
 558         if let Ok(mut lock) = GRAPH_NODE_DRAG_COLOR.write() { *lock = c; }
 559         if let Ok(mut lock) = NODE_DRAG_COLOR.write() { *lock = c; }
 560     }
 561     if let Some(c) = get_color("/style/surface/graph/node/wire_color") {
 562         if let Ok(mut lock) = GRAPH_WIRE_COLOR.write() { *lock = c; }
 563     }
 564     if let Some(c) = get_color("/style/surface/graph/node/wire_highlight_color") {
 565         if let Ok(mut lock) = GRAPH_WIRE_HIGHLIGHT_COLOR.write() { *lock = c; }
 566     }
 567     if let Some(c) = get_color("/style/surface/graph/node/connector_color") {
 568         if let Ok(mut lock) = GRAPH_CONNECTOR_COLOR.write() { *lock = c; }
 569     }
 570     if let Some(c) = get_color("/style/surface/graph/node/connector_highlight_color") {
 571         if let Ok(mut lock) = GRAPH_CONNECTOR_HIGHLIGHT_COLOR.write() { *lock = c; }
 572     }
 573     if let Some(opacity) = val.pointer("/style/surface/graph/opacity").and_then(|v| v.as_f64()) {
 574         if let Ok(mut lock) = GRAPH_OPACITY.write() { *lock = opacity as f32; }
 575     }
 576     if let Some(opacity) = val.pointer("/style/surface/graph/node/opacity").and_then(|v| v.as_f64()) {
 577         if let Ok(mut lock) = GRAPH_NODE_OPACITY.write() { *lock = opacity as f32; }
 578     }
 579 
 580     if let Some(c) = get_color("/style/data/tree/background_color") {
 581         if let Ok(mut lock) = TREE_BACKGROUND_COLOR.write() { *lock = c; }
 582     }
 583     if let Some(c) = get_color("/style/data/tree/border_color") {
 584         if let Ok(mut lock) = TREE_BORDER_COLOR.write() { *lock = c; }
 585     }
 586     if let Some(c) = get_color("/style/data/tree/border_hover_color") {
 587         if let Ok(mut lock) = TREE_BORDER_HOVER_COLOR.write() { *lock = c; }
 588     }
 589     if let Some(c) = get_color("/style/data/tree/border_focus_color") {
 590         if let Ok(mut lock) = TREE_BORDER_FOCUS_COLOR.write() { *lock = c; }
 591     }
 592     if let Some(c) = get_color("/style/data/tree/section_bg_color") {
 593         if let Ok(mut lock) = TREE_SECTION_BG_COLOR.write() { *lock = c; }
 594     }
 595     if let Some(c) = get_color("/style/data/tree/section_bg_hover_color") {
 596         if let Ok(mut lock) = TREE_SECTION_BG_HOVER_COLOR.write() { *lock = c; }
 597     }
 598     if let Some(c) = get_color("/style/data/tree/leaf_bg_even_color") {
 599         if let Ok(mut lock) = TREE_LEAF_BG_EVEN_COLOR.write() { *lock = c; }
 600     }
 601     if let Some(c) = get_color("/style/data/tree/leaf_bg_odd_color") {
 602         if let Ok(mut lock) = TREE_LEAF_BG_ODD_COLOR.write() { *lock = c; }
 603     }
 604     if let Some(c) = get_color("/style/data/tree/leaf_bg_hover_color") {
 605         if let Ok(mut lock) = TREE_LEAF_BG_HOVER_COLOR.write() { *lock = c; }
 606     }
 607     if let Some(c) = get_color("/style/data/tree/leaf_bg_selected_color") {
 608         if let Ok(mut lock) = TREE_LEAF_BG_SELECTED_COLOR.write() { *lock = c; }
 609     }
 610     if let Some(c) = get_color("/style/data/tree/section_text_color") {
 611         if let Ok(mut lock) = TREE_SECTION_TEXT_COLOR.write() { *lock = c; }
 612     }
 613     if let Some(c) = get_color("/style/data/tree/leaf_text_color") {
 614         if let Ok(mut lock) = TREE_LEAF_TEXT_COLOR.write() { *lock = c; }
 615     }
 616     if let Some(c) = get_color("/style/data/tree/leaf_text_selected_color") {
 617         if let Ok(mut lock) = TREE_LEAF_TEXT_SELECTED_COLOR.write() { *lock = c; }
 618     }
 619     if let Some(c) = get_color("/style/data/tree/type_text_color") {
 620         if let Ok(mut lock) = TREE_TYPE_TEXT_COLOR.write() { *lock = c; }
 621     }
 622     if let Some(c) = get_color("/style/data/tree/value_text_color") {
 623         if let Ok(mut lock) = TREE_VALUE_TEXT_COLOR.write() { *lock = c; }
 624     }
 625     if let Some(c) = get_color("/style/data/tree/separator_color") {
 626         if let Ok(mut lock) = TREE_SEPARATOR_COLOR.write() { *lock = c; }
 627     }
 628     if let Some(k) = val.pointer("/style/data/tree/open_search").and_then(|v| v.as_str()) {
 629         if let Ok(mut lock) = TREE_OPEN_SEARCH_KEY.write() { *lock = k.to_string(); }
 630     }
 631     if let Some(k) = val.pointer("/style/data/list/open_search").and_then(|v| v.as_str()) {
 632         if let Ok(mut lock) = LIST_OPEN_SEARCH_KEY.write() { *lock = k.to_string(); }
 633     }
 634     if let Some(k) = val.pointer("/style/data/list/close_search").and_then(|v| v.as_str()) {
 635         if let Ok(mut lock) = LIST_CLOSE_SEARCH_KEY.write() { *lock = k.to_string(); }
 636     }
 637     if let Some(c) = get_color("/style/control/scrollbar/track_color") {
 638         if let Ok(mut lock) = SCROLLBAR_TRACK_COLOR.write() { *lock = c; }
 639     }
 640     if let Some(c) = get_color("/style/control/scrollbar/thumb_color") {
 641         if let Ok(mut lock) = SCROLLBAR_THUMB_COLOR.write() { *lock = c; }
 642     }
 643     if let Some(c) = get_color("/style/surface/plate/color") {
 644         if let Ok(mut lock) = PLATE_COLOR.write() { *lock = Some(c); }
 645     }
 646     if let Some(c) = get_color("/style/surface/plate/border_color") {
 647         if let Ok(mut lock) = PLATE_BORDER_COLOR.write() { *lock = Some(c); }
 648     }
 649     if let Some(t) = val.pointer("/style/surface/plate/border_thickness").and_then(|v| v.as_f64()) {
 650         if let Ok(mut lock) = PLATE_BORDER_THICKNESS.write() { *lock = t as f32; }
 651     }
 652     if let Some(t) = val.pointer("/style/surface/plate/bevel_width").and_then(|v| v.as_f64()) {
 653         if let Ok(mut lock) = PLATE_BEVEL_WIDTH.write() { *lock = t as f32; }
 654     }
 655     if let Some(c) = get_color("/style/surface/param/color") {
 656         if let Ok(mut lock) = PARAM_BG_COLOR.write() { *lock = c; }
 657     }
 658     if let Some(c) = val
 659         .pointer("/style/surface/plate/backdrop_compression")
 660         .and_then(|v| v.as_f64())
 661     {
 662         if let Ok(mut lock) = PLATE_BACKDROP_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
 663     }
 664     if let Some(r) = val.pointer("/style/surface/plate/refraction").and_then(|v| v.as_f64()) {
 665         if let Ok(mut lock) = PLATE_REFRACTION.write() { *lock = (r as f32).clamp(0.0, 1.0); }
 666     }
 667     if let Some(blur) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_bool()) {
 668         if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur; }
 669     } else if let Some(blur_val) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_f64()) {
 670         if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur_val > 0.001; }
 671     }
 672     if let Some(r) = val.pointer("/style/surface/plate/radius").and_then(|v| v.as_f64()) {
 673         if let Ok(mut lock) = PLATE_FROST_RADIUS.write() { *lock = (r as f32).max(0.0); }
 674     }
 675 
 676     // The DE finish beyond its strength (`relief.depth` / `light`, which
 677     // lives in the style registry): the three terms that were literals.
 678     let f = |k: &str| val.pointer(&format!("/style/surface/relief/{k}")).and_then(|v| v.as_f64()).map(|v| v as f32);
 679     if let Some(v) = f("spec") {
 680         if let Ok(mut lock) = FINISH_SPEC.write() { *lock = v.max(0.0); }
 681     }
 682     if let Some(v) = f("shininess") {
 683         if let Ok(mut lock) = FINISH_SHININESS.write() { *lock = v.max(1.0); }
 684     }
 685     if let Some(v) = f("curvature") {
 686         if let Ok(mut lock) = FINISH_CURVATURE.write() { *lock = v.max(0.0); }
 687     }
 688 
 689     // Named materials and the rung bindings (RFC material § 5), replaced
 690     // wholesale so a reload forgets what config no longer says.
 691     {
 692         use crate::scene::material::{FrostDef, MaterialDef};
 693         let num = |v: Option<&serde_json::Value>| v.and_then(|v| v.as_f64()).map(|v| v as f32);
 694         let mut map: Vec<(String, MaterialDef)> = Vec::new();
 695         if let Some(obj) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
 696             for (name, node) in obj {
 697                 let Some(node) = node.as_object() else { continue };
 698                 let frost = node.get("frost").map(|fr| match fr.as_object() {
 699                     Some(fo) => FrostDef {
 700                         compression: num(fo.get("backdrop_compression").or(fo.get("compression"))),
 701                         refraction: num(fo.get("refraction")),
 702                         radius: num(fo.get("radius")),
 703                     },
 704                     // A bare `frost` node (no knobs) is frosted at the defaults.
 705                     None => FrostDef::default(),
 706                 });
 707                 let fin = node.get("finish").and_then(|v| v.as_object());
 708                 let fk = |k: &str| fin.and_then(|fo| num(fo.get(k)));
 709                 map.push((
 710                     name.clone(),
 711                     MaterialDef {
 712                         tint: node.get("color").and_then(|v| v.as_str()).and_then(parse_hex),
 713                         frost,
 714                         light: fk("light").or(fk("depth")),
 715                         spec: fk("spec"),
 716                         shininess: fk("shininess"),
 717                         curvature: fk("curvature"),
 718                     },
 719                 ));
 720             }
 721         }
 722         if let Ok(mut lock) = NAMED_MATERIALS.write() { *lock = map; }
 723         let bind = |p: &str| val.pointer(p).and_then(|v| v.as_str()).map(|s| s.to_string()).filter(|s| !s.is_empty());
 724         let bindings = [
 725             bind("/style/surface/plate/root/material"),
 726             bind("/style/surface/plate/material"),
 727             bind("/style/control/material"),
 728         ];
 729         if let Ok(mut lock) = MATERIAL_BINDINGS.write() { *lock = bindings; }
 730     }
 731 }
 732 
 733 fn load_colors_once() {
 734     use std::sync::Once;
 735     static INIT: Once = Once::new();
 736     INIT.call_once(|| {
 737         if let Some(content) = read_config() {
 738             parse_and_set_colors(&content);
 739         }
 740     });
 741 }
 742 
 743 pub fn reload_colors(content: &str) {
 744     parse_and_set_colors(content);
 745 }
 746 
 747 /// Test-only: the color state is process-global, so any test that calls
 748 /// [`reload_colors`] and then asserts getter values races every other such
 749 /// test on the parallel harness. Each of those tests must hold this lock
 750 /// across its reload + asserts, and should fire the once-per-process config
 751 /// loads ([`load_colors_once`] via any getter, and
 752 /// `layout::lazy_init_style_registry`) inside the lock BEFORE its reload, so
 753 /// neither can rewrite the state from the live config file mid-assert.
 754 #[cfg(test)]
 755 pub(crate) fn test_color_state_lock() -> std::sync::MutexGuard<'static, ()> {
 756     static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
 757     LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
 758 }
 759 
 760 pub fn dropdown_background_color() -> [f32; 4] {
 761     load_colors_once();
 762     style_read(&DROPDOWN_BACKGROUND_COLOR)
 763 }
 764 
 765 pub fn set_dropdown_background_color(color: [f32; 4]) {
 766     style_write(&DROPDOWN_BACKGROUND_COLOR, color);
 767 }
 768 
 769 pub fn textbox_placeholder_text_color() -> [u8; 3] {
 770     load_colors_once();
 771     style_read(&TEXTBOX_PLACEHOLDER_TEXT_COLOR)
 772 }
 773 
 774 pub fn set_textbox_placeholder_text_color(color: [u8; 3]) {
 775     style_write(&TEXTBOX_PLACEHOLDER_TEXT_COLOR, color);
 776 }
 777 
 778 pub fn textbox_background_color() -> [f32; 4] {
 779     load_colors_once();
 780     style_read(&TEXTBOX_BACKGROUND_COLOR)
 781 }
 782 
 783 pub fn set_textbox_background_color(color: [f32; 4]) {
 784     style_write(&TEXTBOX_BACKGROUND_COLOR, color);
 785 }
 786 
 787 pub fn textbox_background_edit_color() -> [f32; 4] {
 788     load_colors_once();
 789     style_read(&TEXTBOX_BACKGROUND_EDIT_COLOR)
 790 }
 791 
 792 pub fn set_textbox_background_edit_color(color: [f32; 4]) {
 793     style_write(&TEXTBOX_BACKGROUND_EDIT_COLOR, color);
 794 }
 795 
 796 pub fn graph_wire_color() -> [f32; 4] {
 797     load_colors_once();
 798     style_read(&GRAPH_WIRE_COLOR)
 799 }
 800 
 801 pub fn set_graph_wire_color(color: [f32; 4]) {
 802     style_write(&GRAPH_WIRE_COLOR, color);
 803 }
 804 
 805 pub fn graph_wire_highlight_color() -> [f32; 4] {
 806     load_colors_once();
 807     style_read(&GRAPH_WIRE_HIGHLIGHT_COLOR)
 808 }
 809 
 810 pub fn set_graph_wire_highlight_color(color: [f32; 4]) {
 811     style_write(&GRAPH_WIRE_HIGHLIGHT_COLOR, color);
 812 }
 813 
 814 pub fn graph_connector_color() -> [f32; 4] {
 815     load_colors_once();
 816     style_read(&GRAPH_CONNECTOR_COLOR)
 817 }
 818 
 819 pub fn set_graph_connector_color(color: [f32; 4]) {
 820     style_write(&GRAPH_CONNECTOR_COLOR, color);
 821 }
 822 
 823 pub fn graph_connector_highlight_color() -> [f32; 4] {
 824     load_colors_once();
 825     style_read(&GRAPH_CONNECTOR_HIGHLIGHT_COLOR)
 826 }
 827 
 828 pub fn set_graph_connector_highlight_color(color: [f32; 4]) {
 829     style_write(&GRAPH_CONNECTOR_HIGHLIGHT_COLOR, color);
 830 }
 831 
 832 pub fn graph_node_color() -> [f32; 4] {
 833     load_colors_once();
 834     style_read(&GRAPH_NODE_COLOR)
 835 }
 836 
 837 pub fn set_graph_node_color(color: [f32; 4]) {
 838     style_write(&GRAPH_NODE_COLOR, color);
 839 }
 840 
 841 pub fn graph_node_selected_color() -> [f32; 4] {
 842     load_colors_once();
 843     style_read(&GRAPH_NODE_SELECTED_COLOR)
 844 }
 845 
 846 pub fn set_graph_node_selected_color(color: [f32; 4]) {
 847     style_write(&GRAPH_NODE_SELECTED_COLOR, color);
 848 }
 849 
 850 pub fn graph_node_drag_color() -> [f32; 4] {
 851     load_colors_once();
 852     style_read(&GRAPH_NODE_DRAG_COLOR)
 853 }
 854 
 855 pub fn set_graph_node_drag_color(color: [f32; 4]) {
 856     style_write(&GRAPH_NODE_DRAG_COLOR, color);
 857 }
 858 
 859 pub fn graph_cell_color() -> [f32; 3] {
 860     load_colors_once();
 861     style_read(&GRAPH_CELL_COLOR)
 862 }
 863 
 864 pub fn set_graph_cell_color(color: [f32; 3]) {
 865     style_write(&GRAPH_CELL_COLOR, color);
 866 }
 867 
 868 pub fn graph_gap_color() -> [f32; 3] {
 869     load_colors_once();
 870     style_read(&GRAPH_GAP_COLOR)
 871 }
 872 
 873 pub fn set_graph_gap_color(color: [f32; 3]) {
 874     style_write(&GRAPH_GAP_COLOR, color);
 875 }
 876 
 877 pub fn graph_opacity() -> f32 {
 878     load_colors_once();
 879     style_read(&GRAPH_OPACITY)
 880 }
 881 
 882 pub fn graph_node_opacity() -> f32 {
 883     load_colors_once();
 884     style_read(&GRAPH_NODE_OPACITY)
 885 }
 886 
 887 pub fn set_graph_node_opacity(opacity: f32) {
 888     style_write(&GRAPH_NODE_OPACITY, opacity);
 889 }
 890 
 891 pub fn set_graph_opacity(opacity: f32) {
 892     style_write(&GRAPH_OPACITY, opacity);
 893 }
 894 
 895 pub fn page_low_color() -> [f32; 4] {
 896     load_colors_once();
 897     let mut color = *PAGE_LOW_COLOR.read().unwrap();
 898     if let Some(opacity) = read_root_plate_opacity_if_configured() {
 899         color[3] = opacity;
 900     }
 901     color
 902 }
 903 
 904 pub fn set_page_low_color(color: [f32; 4]) {
 905     style_write(&PAGE_LOW_COLOR, color);
 906 }
 907 
 908 pub fn page_color() -> [f32; 4] {
 909     load_colors_once();
 910     style_read(&PAGE_COLOR)
 911 }
 912 
 913 pub fn set_page_color(color: [f32; 4]) {
 914     style_write(&PAGE_COLOR, color);
 915 }
 916 
 917 pub fn layer_color() -> [f32; 4] {
 918     load_colors_once();
 919     style_read(&LAYER_COLOR)
 920 }
 921 
 922 pub fn set_layer_color(color: [f32; 4]) {
 923     style_write(&LAYER_COLOR, color);
 924 }
 925 
 926 
 927 pub fn color_borders_color() -> [f32; 4] {
 928     load_colors_once();
 929     style_read(&COLOR_BORDERS_COLOR)
 930 }
 931 
 932 pub fn set_color_borders_color(color: [f32; 4]) {
 933     style_write(&COLOR_BORDERS_COLOR, color);
 934 }
 935 
 936 pub const SLIDER_THUMB: [f32; 4] = [0.60, 0.60, 0.65, 1.0];
 937 pub const SLIDER_THUMB_DRAG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
 938 pub const PROGRESS_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
 939 pub const PROGRESS_FILL: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
 940 
 941 pub const HIGHLIGHT_PRIMARY: [f32; 4] = [1.0, 1.0, 1.0, 0.12];
 942 pub const HIGHLIGHT_SECONDARY: [f32; 4] = [1.0, 1.0, 1.0, 0.06];
 943 
 944 pub const SPINBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
 945 pub const SPINBOX_BUTTON: [f32; 4] = [0.25, 0.25, 0.32, 1.0];
 946 pub const SPINBOX_BUTTON_HOVER: [f32; 4] = [0.35, 0.35, 0.42, 1.0];
 947 pub const SPINBOX_DISPLAY: [f32; 4] = [0.12, 0.12, 0.16, 1.0];
 948 
 949 pub const CANVAS_BG: [f32; 4] = [0.05, 0.05, 0.10, 1.0];
 950 pub const VIEWPORT_BG: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
 951 pub const PARAM_BG: [f32; 4] = [0.10, 0.10, 0.14, 0.25];
 952 
 953 /// The params pane's plate tint (linear rgba) — [`PARAM_BG`] made live:
 954 /// `style.surface.param.color` in config overrides it, and apps can retint at
 955 /// runtime (the designer's Style section "Plate Color"). Alpha doubles as the
 956 /// frost strength under plate blur.
 957 static PARAM_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PARAM_BG);
 958 
 959 pub fn param_bg_color() -> [f32; 4] {
 960     load_colors_once();
 961     style_read(&PARAM_BG_COLOR)
 962 }
 963 
 964 pub fn set_param_bg_color(color: [f32; 4]) {
 965     style_write(&PARAM_BG_COLOR, color);
 966 }
 967 
 968 /// The params plate's final fill as the renderer consumes it: the pane rung's
 969 /// material ([`crate::scene::Material::pane`] — the tint at the global plate
 970 /// opacity, frosted under plate blur) encoded for a nested plate. The single
 971 /// source both `ParametersBg`'s own plate and any surface that wants to match
 972 /// it (the designer's node bodies) draw from, so they track a live retint /
 973 /// opacity / blur toggle together.
 974 pub fn param_plate_fill() -> [f32; 4] {
 975     use crate::scene::material::{Material, PlateRole};
 976     Material::pane().fill(PlateRole::Nested)
 977 }
 978 
 979 pub const PANEL_MENU_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
 980 pub const PANEL_MENU_HOVER: [f32; 4] = [0.18, 0.18, 0.25, 1.0];
 981 pub const PANEL_MENU_FOCUSED: [f32; 4] = [0.08, 0.16, 0.28, 1.0];
 982 
 983 pub const SPLITTER_IDLE: [f32; 4] = [0.20, 0.20, 0.27, 1.0];
 984 pub const SPLITTER_HOVER: [f32; 4] = [0.40, 0.40, 0.50, 1.0];
 985 pub const SPLITTER_DRAG: [f32; 4] = [0.50, 0.50, 0.60, 1.0];
 986 
 987 pub const TEXT_FG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
 988 pub const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
 989 
 990 /// A canvas well's floor: the plate darkened, not a fill of its own, so every
 991 /// opening you look into — Trackpad, Slider2D, the bevel and ramp previews —
 992 /// is cut from the one material ([`crate::scene::paint::PaintCtx::well_floor`]).
 993 pub const WELL_FLOOR: [f32; 4] = [0.0, 0.0, 0.0, 0.18];
 994 /// [`WELL_FLOOR`] risen toward the plate: a clickable canvas's hover cue.
 995 pub const WELL_FLOOR_LIFTED: [f32; 4] = [0.0, 0.0, 0.0, 0.10];
 996 /// The hairline a well is framed with when relief is off, idle; see
 997 /// [`well_frame_color`].
 998 pub const WELL_FRAME: [f32; 4] = [0.18, 0.18, 0.24, 1.0];
 999 /// [`WELL_FRAME`] under the pointer.
1000 pub const WELL_FRAME_HOVER: [f32; 4] = [0.25, 0.25, 0.35, 1.0];
1001 
1002 /// The frame a flat well is drawn in — the one hairline every well (text,
1003 /// keybind, colour and font fields, the canvases) wears when relief is off:
1004 /// neutral greys idle and hovered, the highlight accent while the well is
1005 /// active (editing, recording, pressed) — the colour the relief wells light
1006 /// their rims with, so the two styles share one focus cue. A flat well has no
1007 /// floor of its own any more than a relief one: the plate is the floor.
1008 pub fn well_frame_color(hovered: bool, active: bool) -> [f32; 4] {
1009     if active {
1010         let c = highlight_primary_color();
1011         [c[0], c[1], c[2], 1.0]
1012     } else if hovered {
1013         WELL_FRAME_HOVER
1014     } else {
1015         WELL_FRAME
1016     }
1017 }
1018 pub const TEXT_HEADER: [f32; 4] = [0.90, 0.90, 0.95, 1.0];
1019 pub const TEXT_ACCENT: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
1020 
1021 pub fn srgb_to_linear(c: f32) -> f32 {
1022     if c <= 0.04045 {
1023         c / 12.92
1024     } else {
1025         ((c + 0.055) / 1.055).powf(2.4)
1026     }
1027 }
1028 
1029 /// Parse a hex color string into raw RGBA bytes.
1030 ///
1031 /// Accepts `#RRGGBB` or `#RRGGBBAA`, tolerating surrounding quotes/whitespace and
1032 /// an optional leading `#`. 6-digit input yields alpha `255`. Returns `None` for
1033 /// any shorter/invalid input. This is the primitive the `[f32;_]` parsers build on.
1034 pub fn parse_hex_bytes(s: &str) -> Option<[u8; 4]> {
1035     let hex = s
1036         .trim_matches(|c| c == '"' || c == '\'' || c == ' ')
1037         .trim_start_matches('#');
1038     let b = |i: usize| u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok();
1039     if hex.len() >= 8 {
1040         Some([b(0)?, b(1)?, b(2)?, b(3)?])
1041     } else if hex.len() >= 6 {
1042         Some([b(0)?, b(1)?, b(2)?, 255])
1043     } else {
1044         None
1045     }
1046 }
1047 
1048 /// Parse a hex color string into raw sRGB RGBA in `[0,1]` (no gamma conversion).
1049 /// See [`parse_hex_bytes`] for the accepted formats. Apply [`srgb_to_linear`] to
1050 /// the RGB channels yourself if your render target expects linear color.
1051 pub fn parse_hex_rgba(s: &str) -> Option<[f32; 4]> {
1052     parse_hex_bytes(s).map(|[r, g, b, a]| {
1053         [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]
1054     })
1055 }
1056 
1057 /// Like [`parse_hex_rgba`] but drops alpha, returning raw sRGB RGB in `[0,1]`.
1058 pub fn parse_hex_rgb(s: &str) -> Option<[f32; 3]> {
1059     parse_hex_rgba(s).map(|[r, g, b, _]| [r, g, b])
1060 }
1061 
1062 /// Like [`parse_hex_rgba`] but converts RGB from sRGB to linear (alpha kept as-is).
1063 /// Use when your render target samples colors in linear space.
1064 pub fn parse_hex_rgba_linear(s: &str) -> Option<[f32; 4]> {
1065     parse_hex_rgba(s).map(|[r, g, b, a]| {
1066         [srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b), a]
1067     })
1068 }
1069 
1070 pub fn linear_to_srgb(c: f32) -> f32 {
1071     if c <= 0.0031308 {
1072         c * 12.92
1073     } else {
1074         1.055 * c.powf(1.0 / 2.4) - 0.055
1075     }
1076 }
1077 
1078 pub fn to_linear(color: [f32; 4]) -> [f32; 4] {
1079     [
1080         srgb_to_linear(color[0]),
1081         srgb_to_linear(color[1]),
1082         srgb_to_linear(color[2]),
1083         color[3],
1084     ]
1085 }
1086 
1087 pub fn to_srgb(color: [f32; 4]) -> [f32; 4] {
1088     [
1089         linear_to_srgb(color[0]),
1090         linear_to_srgb(color[1]),
1091         linear_to_srgb(color[2]),
1092         color[3],
1093     ]
1094 }
1095 
1096 pub fn to_linear_rgb(color: [f32; 3]) -> [f32; 3] {
1097     [
1098         srgb_to_linear(color[0]),
1099         srgb_to_linear(color[1]),
1100         srgb_to_linear(color[2]),
1101     ]
1102 }
1103 
1104 pub fn to_srgb_rgb(color: [f32; 3]) -> [f32; 3] {
1105     [
1106         linear_to_srgb(color[0]),
1107         linear_to_srgb(color[1]),
1108         linear_to_srgb(color[2]),
1109     ]
1110 }
1111 
1112 pub fn linear_srgb_to_oklab(rgb: [f32; 3]) -> [f32; 3] {
1113     let l = 0.4122214708 * rgb[0] + 0.5363325363 * rgb[1] + 0.0514459929 * rgb[2];
1114     let m = 0.2119034982 * rgb[0] + 0.6806995451 * rgb[1] + 0.1073969566 * rgb[2];
1115     let s = 0.0883024619 * rgb[0] + 0.2817188376 * rgb[1] + 0.6299787005 * rgb[2];
1116 
1117     let l_ = l.max(0.0).powf(1.0 / 3.0);
1118     let m_ = m.max(0.0).powf(1.0 / 3.0);
1119     let s_ = s.max(0.0).powf(1.0 / 3.0);
1120 
1121     [
1122         0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1123         1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
1124         0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
1125     ]
1126 }
1127 
1128 pub fn oklab_to_linear_srgb(lab: [f32; 3]) -> [f32; 3] {
1129     let l_ = lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2];
1130     let m_ = lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2];
1131     let s_ = lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2];
1132 
1133     let l = l_ * l_ * l_;
1134     let m = m_ * m_ * m_;
1135     let s = s_ * s_ * s_;
1136 
1137     [
1138         4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
1139         -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
1140         -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
1141     ]
1142 }
1143 
1144 /// The alpha that makes an overlay's fade-out read as an EVEN fade to the eye.
1145 ///
1146 /// `t` runs 0 (overlay at `max_alpha`) to 1 (fully faded); `overlay` and `backdrop` are
1147 /// linear-light RGB, the backdrop being whatever the fade composites onto.
1148 ///
1149 /// A linear alpha ramp is not a linear fade: the surface is an sRGB attachment, so the GPU
1150 /// blends in LINEAR light, and perceived lightness goes as roughly the cube root of that. A
1151 /// straight alpha ramp therefore hangs bright through the middle and then dives near the
1152 /// end. This walks perceived lightness linearly instead and solves back for the alpha that
1153 /// lands on it — exactly, since the composite is linear in alpha:
1154 ///
1155 /// ```text
1156 /// Y(a) = a·Y_overlay + (1 - a)·Y_backdrop        (blending, in linear light)
1157 /// L    ≈ cbrt(Y)                                 (perception — OKLab's L, and CIE L* to within a hair)
1158 /// ```
1159 ///
1160 /// Solved on luminance rather than per channel because a single alpha can only satisfy one
1161 /// axis, and lightness is the one the eye reads a fade by. With no lightness contrast to
1162 /// shape (a fade between equally-bright colors) it falls back to the linear ramp.
1163 pub fn perceptual_fade_alpha(t: f32, overlay: [f32; 3], backdrop: [f32; 3], max_alpha: f32) -> f32 {
1164     let t = t.clamp(0.0, 1.0);
1165     let luminance = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
1166     let y_over = luminance(overlay);
1167     let y_back = luminance(backdrop);
1168     let contrast = y_over - y_back;
1169     if contrast.abs() < 1e-4 {
1170         return max_alpha * (1.0 - t);
1171     }
1172     // Walk L from the fully-applied composite down to the bare backdrop.
1173     let l = |y: f32| y.max(0.0).cbrt();
1174     let y_full = max_alpha * y_over + (1.0 - max_alpha) * y_back;
1175     let l_t = l(y_full) + (l(y_back) - l(y_full)) * t;
1176     let y_t = l_t * l_t * l_t;
1177     ((y_t - y_back) / contrast).clamp(0.0, max_alpha)
1178 }
1179 
1180 pub fn sidebar_bg_color() -> [f32; 4] {
1181     load_colors_once();
1182     let mut color = *SIDEBAR_BG_COLOR.read().unwrap();
1183     if let Some(opacity) = read_opacity_if_configured() {
1184         color[3] = opacity;
1185     }
1186     color
1187 }
1188 
1189 pub fn set_sidebar_bg_color(color: [f32; 4]) {
1190     style_write(&SIDEBAR_BG_COLOR, color);
1191 }
1192 
1193 pub fn highlight_primary_color() -> [f32; 4] {
1194     load_colors_once();
1195     style_read(&HIGHLIGHT_PRIMARY_COLOR)
1196 }
1197 
1198 pub fn set_highlight_primary_color(color: [f32; 4]) {
1199     if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() {
1200         *lock = [color[0], color[1], color[2], 0.12];
1201     }
1202 }
1203 
1204 pub fn menubar_tab_label_color() -> [f32; 4] {
1205     load_colors_once();
1206     style_read(&MENUBAR_TAB_LABEL_COLOR)
1207 }
1208 
1209 pub fn set_menubar_tab_label_color(color: [f32; 4]) {
1210     style_write(&MENUBAR_TAB_LABEL_COLOR, color);
1211 }
1212 
1213 pub fn read_opacity_if_configured() -> Option<f32> {
1214     load_colors_once();
1215     style_read(&OPACITY)
1216 }
1217 
1218 /// Tint strength for frosted menus/popovers (`/style/surface/menu/opacity`,
1219 /// default 0.8): the |alpha| of the blur-behind sentinel their plates carry.
1220 pub fn menu_opacity() -> f32 {
1221     load_colors_once();
1222     style_read(&MENU_OPACITY)
1223 }
1224 
1225 /// Backdrop compression of frosted menus/popovers
1226 /// (`/style/surface/menu/compression`, default 0.6) — see `MENU_COMPRESSION`.
1227 pub fn menu_compression() -> f32 {
1228     load_colors_once();
1229     style_read(&MENU_COMPRESSION)
1230 }
1231 
1232 pub fn set_menu_compression(c: f32) {
1233     style_write(&MENU_COMPRESSION, c.clamp(0.0, 1.0));
1234 }
1235 
1236 pub fn read_root_plate_opacity_if_configured() -> Option<f32> {
1237     load_colors_once();
1238     style_read(&ROOT_PLATE_OPACITY)
1239 }
1240 
1241 // The toggle's own palette (enabled/disabled/background) is RETIRED: a toggle
1242 // inherits the plate it sits on and marks state with light and relief, so
1243 // there is nothing left to tint. The `TOGGLE_*` consts above survive for the
1244 // graph's node geometry switch, which is a different control.
1245 
1246 pub fn list_bg_color() -> [f32; 4] {
1247     load_colors_once();
1248     style_read(&LIST_BG_COLOR)
1249 }
1250 
1251 pub fn set_list_bg_color(color: [f32; 4]) {
1252     if let Ok(mut lock) = LIST_BG_COLOR.write() {
1253         *lock = [color[0], color[1], color[2], 0.3];
1254     }
1255 }
1256 
1257 pub fn list_entry_bg_color() -> [f32; 4] {
1258     load_colors_once();
1259     style_read(&LIST_ENTRY_BG_COLOR)
1260 }
1261 
1262 pub fn set_list_entry_bg_color(color: [f32; 4]) {
1263     style_write(&LIST_ENTRY_BG_COLOR, color);
1264 }
1265 
1266 pub fn list_entry_highlight_color() -> [f32; 4] {
1267     load_colors_once();
1268     style_read(&LIST_ENTRY_HIGHLIGHT_COLOR)
1269 }
1270 
1271 pub fn set_list_entry_highlight_color(color: [f32; 4]) {
1272     style_write(&LIST_ENTRY_HIGHLIGHT_COLOR, color);
1273 }
1274 
1275 pub fn list_font_color() -> [f32; 4] {
1276     load_colors_once();
1277     style_read(&LIST_FONT_COLOR)
1278 }
1279 
1280 pub fn set_list_font_color(color: [f32; 4]) {
1281     style_write(&LIST_FONT_COLOR, color);
1282 }
1283 
1284 pub fn breadcrumb_bg_color() -> [f32; 4] {
1285     load_colors_once();
1286     style_read(&BREADCRUMB_BG_COLOR)
1287 }
1288 
1289 pub fn set_breadcrumb_bg_color(color: [f32; 4]) {
1290     if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
1291         *lock = [color[0], color[1], color[2], 1.0];
1292     }
1293 }
1294 
1295 pub fn popover_bg_color() -> [f32; 4] {
1296     load_colors_once();
1297     style_read(&POPOVER_BG_COLOR)
1298 }
1299 
1300 pub fn set_popover_bg_color(color: [f32; 4]) {
1301     if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
1302         *lock = [color[0], color[1], color[2], 1.0];
1303     }
1304 }
1305 
1306 /// Corner radius of the root plate (`style.surface.plate.root.corner_radius`).
1307 /// The window silhouette value — the compositor clips windows from the SHARED
1308 /// copy of this.
1309 pub fn root_plate_corner_radius() -> f32 {
1310     load_colors_once();
1311     style_read(&ROOT_PLATE_CORNER_RADIUS)
1312 }
1313 
1314 pub fn ramp_background_color() -> [f32; 4] {
1315     load_colors_once();
1316     style_read(&RAMP_BACKGROUND_COLOR)
1317 }
1318 
1319 pub fn ramp_border_color() -> [f32; 4] {
1320     load_colors_once();
1321     style_read(&RAMP_BORDER_COLOR)
1322 }
1323 
1324 pub fn control_panel_color() -> [f32; 4] {
1325     load_colors_once();
1326     style_read(&CONTROL_PANEL_COLOR)
1327 }
1328 
1329 pub fn set_control_panel_color(color: [f32; 4]) {
1330     style_write(&CONTROL_PANEL_COLOR, color);
1331 }
1332 
1333 pub fn control_panel_border_color() -> [f32; 4] {
1334     load_colors_once();
1335     style_read(&CONTROL_PANEL_BORDER_COLOR)
1336 }
1337 
1338 pub fn set_control_panel_border_color(color: [f32; 4]) {
1339     style_write(&CONTROL_PANEL_BORDER_COLOR, color);
1340 }
1341 
1342 pub fn progress_bg() -> [f32; 4] {
1343     load_colors_once();
1344     style_read(&PROGRESS_BG_COLOR)
1345 }
1346 
1347 pub fn set_progress_bg(color: [f32; 4]) {
1348     style_write(&PROGRESS_BG_COLOR, color);
1349 }
1350 
1351 pub fn progress_fill() -> [f32; 4] {
1352     load_colors_once();
1353     style_read(&PROGRESS_FILL_COLOR)
1354 }
1355 
1356 pub fn set_progress_fill(color: [f32; 4]) {
1357     style_write(&PROGRESS_FILL_COLOR, color);
1358 }
1359 
1360 pub fn button_border_color() -> Option<[f32; 4]> {
1361     load_colors_once();
1362     style_read(&BUTTON_BORDER_COLOR)
1363 }
1364 
1365 pub fn set_button_border_color(color: [f32; 4]) {
1366     if let Ok(mut lock) = BUTTON_BORDER_COLOR.write() {
1367         *lock = Some(color);
1368     }
1369 }
1370 
1371 pub fn dropdown_border_color() -> [f32; 4] {
1372     load_colors_once();
1373     style_read(&DROPDOWN_BORDER_COLOR)
1374 }
1375 
1376 pub fn set_dropdown_border_color(color: [f32; 4]) {
1377     style_write(&DROPDOWN_BORDER_COLOR, color);
1378 }
1379 
1380 pub fn dropdown_text_color() -> [f32; 4] {
1381     load_colors_once();
1382     style_read(&DROPDOWN_TEXT_COLOR)
1383 }
1384 
1385 pub fn set_dropdown_text_color(color: [f32; 4]) {
1386     style_write(&DROPDOWN_TEXT_COLOR, color);
1387 }
1388 
1389 
1390 pub fn slider_thumb() -> [f32; 4] {
1391     load_colors_once();
1392     style_read(&SLIDER_THUMB_COLOR)
1393 }
1394 
1395 pub fn set_slider_thumb(color: [f32; 4]) {
1396     style_write(&SLIDER_THUMB_COLOR, color);
1397 }
1398 
1399 pub fn slider_thumb_drag() -> [f32; 4] {
1400     load_colors_once();
1401     style_read(&SLIDER_THUMB_DRAG_COLOR)
1402 }
1403 
1404 pub fn set_slider_thumb_drag(color: [f32; 4]) {
1405     style_write(&SLIDER_THUMB_DRAG_COLOR, color);
1406 }
1407 
1408 pub fn rangeslider_thumb() -> [f32; 4] {
1409     load_colors_once();
1410     style_read(&RANGE_SLIDER_THUMB_COLOR)
1411 }
1412 
1413 pub fn set_rangeslider_thumb(color: [f32; 4]) {
1414     style_write(&RANGE_SLIDER_THUMB_COLOR, color);
1415 }
1416 
1417 pub fn rangeslider_thumb_drag() -> [f32; 4] {
1418     load_colors_once();
1419     style_read(&RANGE_SLIDER_THUMB_DRAG_COLOR)
1420 }
1421 
1422 pub fn set_rangeslider_thumb_drag(color: [f32; 4]) {
1423     style_write(&RANGE_SLIDER_THUMB_DRAG_COLOR, color);
1424 }
1425 
1426 pub fn spinbox_display() -> [f32; 4] {
1427     load_colors_once();
1428     style_read(&SPINBOX_DISPLAY_COLOR)
1429 }
1430 
1431 pub fn set_spinbox_display(color: [f32; 4]) {
1432     style_write(&SPINBOX_DISPLAY_COLOR, color);
1433 }
1434 
1435 pub fn spinbox_button() -> [f32; 4] {
1436     load_colors_once();
1437     style_read(&SPINBOX_BUTTON_COLOR)
1438 }
1439 
1440 pub fn set_spinbox_button(color: [f32; 4]) {
1441     style_write(&SPINBOX_BUTTON_COLOR, color);
1442 }
1443 
1444 pub fn spinbox_button_hover() -> [f32; 4] {
1445     load_colors_once();
1446     style_read(&SPINBOX_BUTTON_HOVER_COLOR)
1447 }
1448 
1449 pub fn set_spinbox_button_hover(color: [f32; 4]) {
1450     style_write(&SPINBOX_BUTTON_HOVER_COLOR, color);
1451 }
1452 
1453 pub fn spinbox_text_color() -> [f32; 4] {
1454     load_colors_once();
1455     style_read(&SPINBOX_TEXT_COLOR)
1456 }
1457 
1458 pub fn set_spinbox_text_color(color: [f32; 4]) {
1459     style_write(&SPINBOX_TEXT_COLOR, color);
1460 }
1461 
1462 pub fn set_root_plate_corner_radius(radius: f32) {
1463     style_write(&ROOT_PLATE_CORNER_RADIUS, radius);
1464 }
1465 
1466 #[derive(Debug, Clone, Copy)]
1467 pub struct Theme {
1468     pub surface_bg: [f32; 4],
1469     pub surface_border: [f32; 4],
1470     pub primary_accent: [f32; 4],
1471     pub press_overlay: [f32; 4],
1472     pub hover_overlay: [f32; 4],
1473 }
1474 
1475 pub fn active_theme() -> Theme {
1476     Theme {
1477         surface_bg: popover_bg_color(),
1478         surface_border: [0.25, 0.25, 0.35, 0.8],
1479         primary_accent: [0.20, 0.50, 0.75, 1.0],
1480         press_overlay: [1.0, 1.0, 1.0, 0.15],
1481         hover_overlay: [1.0, 1.0, 1.0, 0.08],
1482     }
1483 }
1484 
1485 pub fn active_window_mode() -> String {
1486     let app_id = crate::scale::app_id();
1487     if app_id.is_empty() {
1488         return "floating".to_string();
1489     }
1490 
1491     let config_path = crate::config::get_config_path();
1492     let content = match std::fs::read_to_string(&config_path) {
1493         Ok(c) => c,
1494         Err(_) => return "floating".to_string(),
1495     };
1496     let val = crate::config::parse_kdl_to_json(&content);
1497 
1498     if let Some(mode_rules) = val.get("mode_rule").and_then(|r| r.as_array()) {
1499         for rule in mode_rules {
1500             if rule.get("app_id").and_then(|id| id.as_str()) == Some(&app_id) {
1501                 if let Some(mode) = rule.get("mode").and_then(|m| m.as_str()) {
1502                     return mode.to_string();
1503                 }
1504             }
1505         }
1506     }
1507 
1508     let mut default_tile_mode = "cascade".to_string();
1509     if let Some(tag_layouts) = val.get("tag_layout").and_then(|l| l.as_array()) {
1510         for tl in tag_layouts {
1511             if tl.get("tag").and_then(|t| t.as_u64()) == Some(1) {
1512                 if let Some(m) = tl.get("mode").and_then(|m| m.as_str()) {
1513                     default_tile_mode = m.to_string();
1514                 }
1515             }
1516         }
1517     }
1518 
1519     if crate::scale::is_fullscreen() {
1520         return "fullscreen".to_string();
1521     }
1522     if crate::scale::is_maximized() {
1523         return default_tile_mode;
1524     }
1525 
1526     "floating".to_string()
1527 }
1528 
1529 /// Opacity of the root plate's fill (the alpha of the root-plate color).
1530 pub fn root_plate_opacity() -> f32 {
1531     page_low_color()[3]
1532 }
1533 
1534 pub fn tree_background_color() -> [f32; 4] {
1535     style_read(&TREE_BACKGROUND_COLOR)
1536 }
1537 pub fn tree_border_color() -> [f32; 4] {
1538     style_read(&TREE_BORDER_COLOR)
1539 }
1540 pub fn tree_border_hover_color() -> [f32; 4] {
1541     style_read(&TREE_BORDER_HOVER_COLOR)
1542 }
1543 pub fn tree_border_focus_color() -> [f32; 4] {
1544     style_read(&TREE_BORDER_FOCUS_COLOR)
1545 }
1546 
1547 pub fn tree_section_bg_color() -> [f32; 4] {
1548     style_read(&TREE_SECTION_BG_COLOR)
1549 }
1550 pub fn tree_section_bg_hover_color() -> [f32; 4] {
1551     style_read(&TREE_SECTION_BG_HOVER_COLOR)
1552 }
1553 
1554 pub fn tree_leaf_bg_even_color() -> [f32; 4] {
1555     style_read(&TREE_LEAF_BG_EVEN_COLOR)
1556 }
1557 pub fn tree_leaf_bg_odd_color() -> [f32; 4] {
1558     style_read(&TREE_LEAF_BG_ODD_COLOR)
1559 }
1560 pub fn tree_leaf_bg_hover_color() -> [f32; 4] {
1561     style_read(&TREE_LEAF_BG_HOVER_COLOR)
1562 }
1563 pub fn tree_leaf_bg_selected_color() -> [f32; 4] {
1564     style_read(&TREE_LEAF_BG_SELECTED_COLOR)
1565 }
1566 
1567 pub fn tree_section_text_color() -> [f32; 4] {
1568     style_read(&TREE_SECTION_TEXT_COLOR)
1569 }
1570 pub fn tree_leaf_text_color() -> [f32; 4] {
1571     style_read(&TREE_LEAF_TEXT_COLOR)
1572 }
1573 pub fn tree_leaf_text_selected_color() -> [f32; 4] {
1574     style_read(&TREE_LEAF_TEXT_SELECTED_COLOR)
1575 }
1576 
1577 pub fn tree_type_text_color() -> [f32; 4] {
1578     style_read(&TREE_TYPE_TEXT_COLOR)
1579 }
1580 pub fn tree_value_text_color() -> [f32; 4] {
1581     style_read(&TREE_VALUE_TEXT_COLOR)
1582 }
1583 pub fn tree_separator_color() -> [f32; 4] {
1584     style_read(&TREE_SEPARATOR_COLOR)
1585 }
1586 
1587 pub fn set_tree_background_color(c: [f32; 4]) {
1588     style_write(&TREE_BACKGROUND_COLOR, c);
1589 }
1590 pub fn set_tree_border_color(c: [f32; 4]) {
1591     style_write(&TREE_BORDER_COLOR, c);
1592 }
1593 pub fn set_tree_border_hover_color(c: [f32; 4]) {
1594     style_write(&TREE_BORDER_HOVER_COLOR, c);
1595 }
1596 pub fn set_tree_border_focus_color(c: [f32; 4]) {
1597     style_write(&TREE_BORDER_FOCUS_COLOR, c);
1598 }
1599 
1600 pub fn set_tree_section_bg_color(c: [f32; 4]) {
1601     style_write(&TREE_SECTION_BG_COLOR, c);
1602 }
1603 pub fn set_tree_section_bg_hover_color(c: [f32; 4]) {
1604     style_write(&TREE_SECTION_BG_HOVER_COLOR, c);
1605 }
1606 
1607 pub fn set_tree_leaf_bg_even_color(c: [f32; 4]) {
1608     style_write(&TREE_LEAF_BG_EVEN_COLOR, c);
1609 }
1610 pub fn set_tree_leaf_bg_odd_color(c: [f32; 4]) {
1611     style_write(&TREE_LEAF_BG_ODD_COLOR, c);
1612 }
1613 pub fn set_tree_leaf_bg_hover_color(c: [f32; 4]) {
1614     style_write(&TREE_LEAF_BG_HOVER_COLOR, c);
1615 }
1616 pub fn set_tree_leaf_bg_selected_color(c: [f32; 4]) {
1617     style_write(&TREE_LEAF_BG_SELECTED_COLOR, c);
1618 }
1619 
1620 pub fn set_tree_section_text_color(c: [f32; 4]) {
1621     style_write(&TREE_SECTION_TEXT_COLOR, c);
1622 }
1623 pub fn set_tree_leaf_text_color(c: [f32; 4]) {
1624     style_write(&TREE_LEAF_TEXT_COLOR, c);
1625 }
1626 pub fn set_tree_leaf_text_selected_color(c: [f32; 4]) {
1627     style_write(&TREE_LEAF_TEXT_SELECTED_COLOR, c);
1628 }
1629 
1630 pub fn set_tree_type_text_color(c: [f32; 4]) {
1631     style_write(&TREE_TYPE_TEXT_COLOR, c);
1632 }
1633 pub fn set_tree_value_text_color(c: [f32; 4]) {
1634     style_write(&TREE_VALUE_TEXT_COLOR, c);
1635 }
1636 pub fn set_tree_separator_color(c: [f32; 4]) {
1637     style_write(&TREE_SEPARATOR_COLOR, c);
1638 }
1639 
1640 pub fn scrollbar_track_color() -> [f32; 4] {
1641     style_read(&SCROLLBAR_TRACK_COLOR)
1642 }
1643 pub fn set_scrollbar_track_color(c: [f32; 4]) {
1644     style_write(&SCROLLBAR_TRACK_COLOR, c);
1645 }
1646 pub fn scrollbar_thumb_color() -> [f32; 4] {
1647     style_read(&SCROLLBAR_THUMB_COLOR)
1648 }
1649 pub fn set_scrollbar_thumb_color(c: [f32; 4]) {
1650     style_write(&SCROLLBAR_THUMB_COLOR, c);
1651 }
1652 
1653 pub fn tree_open_search_key() -> String {
1654     load_colors_once();
1655     let legacy = TREE_OPEN_SEARCH_KEY.read().unwrap().clone();
1656     crate::input::widget_chord("open_search", &legacy, "ctrl+f")
1657 }
1658 pub fn set_tree_open_search_key(k: String) {
1659     style_write(&TREE_OPEN_SEARCH_KEY, k);
1660 }
1661 
1662 pub fn list_open_search_key() -> String {
1663     load_colors_once();
1664     let legacy = LIST_OPEN_SEARCH_KEY.read().unwrap().clone();
1665     crate::input::widget_chord("open_search", &legacy, "ctrl+f")
1666 }
1667 pub fn set_list_open_search_key(k: String) {
1668     style_write(&LIST_OPEN_SEARCH_KEY, k);
1669 }
1670 
1671 pub fn list_close_search_key() -> String {
1672     load_colors_once();
1673     let legacy = LIST_CLOSE_SEARCH_KEY.read().unwrap().clone();
1674     crate::input::widget_chord("close_search", &legacy, "escape")
1675 }
1676 pub fn set_list_close_search_key(k: String) {
1677     style_write(&LIST_CLOSE_SEARCH_KEY, k);
1678 }
1679 
1680 pub fn root_plate_menubar_color() -> [f32; 4] {
1681     load_colors_once();
1682     style_read(&ROOT_PLATE_MENUBAR_COLOR)
1683 }
1684 
1685 pub fn set_root_plate_menubar_color(c: [f32; 4]) {
1686     style_write(&ROOT_PLATE_MENUBAR_COLOR, c);
1687 }
1688 
1689 pub fn root_plate_menubar_text_color() -> [f32; 4] {
1690     load_colors_once();
1691     style_read(&ROOT_PLATE_MENUBAR_TEXT_COLOR)
1692 }
1693 
1694 pub fn set_root_plate_menubar_text_color(c: [f32; 4]) {
1695     style_write(&ROOT_PLATE_MENUBAR_TEXT_COLOR, c);
1696 }
1697 
1698 pub fn root_plate_menubar_blur() -> bool {
1699     load_colors_once();
1700     style_read(&ROOT_PLATE_MENUBAR_BLUR)
1701 }
1702 
1703 pub fn set_root_plate_menubar_blur(b: bool) {
1704     style_write(&ROOT_PLATE_MENUBAR_BLUR, b);
1705 }
1706 
1707 pub fn root_plate_statusbar_color() -> [f32; 4] {
1708     load_colors_once();
1709     style_read(&ROOT_PLATE_STATUSBAR_COLOR)
1710 }
1711 
1712 pub fn set_root_plate_statusbar_color(c: [f32; 4]) {
1713     style_write(&ROOT_PLATE_STATUSBAR_COLOR, c);
1714 }
1715 
1716 pub fn root_plate_statusbar_text_color() -> [f32; 4] {
1717     load_colors_once();
1718     style_read(&ROOT_PLATE_STATUSBAR_TEXT_COLOR)
1719 }
1720 
1721 pub fn set_root_plate_statusbar_text_color(c: [f32; 4]) {
1722     style_write(&ROOT_PLATE_STATUSBAR_TEXT_COLOR, c);
1723 }
1724 
1725 pub fn root_plate_statusbar_blur() -> bool {
1726     load_colors_once();
1727     style_read(&ROOT_PLATE_STATUSBAR_BLUR)
1728 }
1729 
1730 pub fn set_root_plate_statusbar_blur(b: bool) {
1731     style_write(&ROOT_PLATE_STATUSBAR_BLUR, b);
1732 }
1733 
1734 static PLATE_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.15, 0.15, 0.2, 0.95]));
1735 static PLATE_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.3, 0.3, 0.4, 1.0]));
1736 static PLATE_BORDER_THICKNESS: RwLock<f32> = RwLock::new(1.0);
1737 /// Roll width of the beveled plate border (the control_relief replacement for
1738 /// the flat border line) — `style.surface.plate.bevel_width`.
1739 static PLATE_BEVEL_WIDTH: RwLock<f32> = RwLock::new(6.0);
1740 
1741 pub fn plate_color() -> Option<[f32; 4]> {
1742     load_colors_once();
1743     style_read(&PLATE_COLOR)
1744 }
1745 
1746 pub fn set_plate_color(c: Option<[f32; 4]>) {
1747     style_write(&PLATE_COLOR, c);
1748 }
1749 
1750 pub fn plate_border_color() -> Option<[f32; 4]> {
1751     load_colors_once();
1752     style_read(&PLATE_BORDER_COLOR)
1753 }
1754 
1755 pub fn set_plate_border_color(c: Option<[f32; 4]>) {
1756     style_write(&PLATE_BORDER_COLOR, c);
1757 }
1758 
1759 pub fn plate_border_thickness() -> f32 {
1760     load_colors_once();
1761     style_read(&PLATE_BORDER_THICKNESS)
1762 }
1763 
1764 pub fn set_plate_border_thickness(t: f32) {
1765     style_write(&PLATE_BORDER_THICKNESS, t);
1766 }
1767 
1768 pub fn plate_bevel_width() -> f32 {
1769     load_colors_once();
1770     style_read(&PLATE_BEVEL_WIDTH)
1771 }
1772 
1773 pub fn set_plate_bevel_width(t: f32) {
1774     style_write(&PLATE_BEVEL_WIDTH, t);
1775 }
1776 
1777 /// How hard a frosted plate pulls its backdrop's LUMINANCE toward its own key
1778 /// before tinting: 0 = the backdrop passes through untouched, 1 = flat.
1779 ///
1780 /// This is the plate's legibility control, and it is NOT opacity. Blur
1781 /// destroys a backdrop's spatial detail but preserves its mean luminance, and
1782 /// text contrast is a mean-luminance property — so a frosted plate over
1783 /// something bright washes out however hard it is blurred, which is the whole
1784 /// of the "liquid glass" legibility problem. Compression remaps the backdrop's
1785 /// luminance toward the plate's own, symmetrically: a bright backdrop comes
1786 /// down and a DARK one comes up, so the plate stops swinging through the ink's
1787 /// luminance while its hue, chroma and movement still read.
1788 ///
1789 /// Default 0.0 — the behavior every existing config already has.
1790 static PLATE_BACKDROP_COMPRESSION: RwLock<f32> = RwLock::new(0.0);
1791 
1792 pub fn plate_backdrop_compression() -> f32 {
1793     load_colors_once();
1794     style_read(&PLATE_BACKDROP_COMPRESSION)
1795 }
1796 
1797 pub fn set_plate_backdrop_compression(c: f32) {
1798     style_write(&PLATE_BACKDROP_COMPRESSION, c.clamp(0.0, 1.0));
1799 }
1800 
1801 /// How far a frosted plate's roll bends what it samples, and how much clearer
1802 /// its rim is than its frosted body: 0 = a flat window, 1 = full.
1803 ///
1804 /// This buys no legibility and is not meant to — see
1805 /// [`plate_backdrop_compression`] for that. What it buys is the plate reading
1806 /// as an OBJECT: a curved edge displaces the view through it, so the
1807 /// silhouette stops being where the haze ends and becomes where a slab with a
1808 /// thickness begins. The two are complementary, and on a dark desktop
1809 /// especially: compression flattens the body toward the tint, which leaves the
1810 /// rim as the only place the material can still say what it is.
1811 ///
1812 /// Default 0.0 — no existing config changes appearance.
1813 static PLATE_REFRACTION: RwLock<f32> = RwLock::new(0.0);
1814 
1815 pub fn plate_refraction() -> f32 {
1816     load_colors_once();
1817     style_read(&PLATE_REFRACTION)
1818 }
1819 
1820 pub fn set_plate_refraction(r: f32) {
1821     style_write(&PLATE_REFRACTION, r.clamp(0.0, 1.0));
1822 }
1823 
1824 static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
1825 
1826 /// The finish's three fixed terms — specular strength, shininess exponent,
1827 /// curvature/AO strength — as `scene::material::Finish::from_style` reads
1828 /// them. Until 2026-09-20 these were literals in the finish constructor
1829 /// (0.4 / 24 / 0.2); the getters exist so the DE's plastic can be edited and
1830 /// so a named material (RFC material, step 4) has somewhere to land. No config
1831 /// path yet: the defaults ARE the shipped look.
1832 static FINISH_SPEC: RwLock<f32> = RwLock::new(0.4);
1833 static FINISH_SHININESS: RwLock<f32> = RwLock::new(24.0);
1834 static FINISH_CURVATURE: RwLock<f32> = RwLock::new(0.2);
1835 
1836 /// The default material's blur radius (`style.surface.plate.radius`, the
1837 /// kernel sigma in logical px) — [`crate::scene::Frost::DEFAULT_RADIUS`]
1838 /// unless config says otherwise.
1839 static PLATE_FROST_RADIUS: RwLock<f32> = RwLock::new(crate::scene::material::Frost::DEFAULT_RADIUS);
1840 
1841 pub fn plate_frost_radius() -> f32 {
1842     load_colors_once();
1843     style_read(&PLATE_FROST_RADIUS)
1844 }
1845 pub fn set_plate_frost_radius(r: f32) {
1846     style_write(&PLATE_FROST_RADIUS, r.max(0.0));
1847 }
1848 
1849 /// The named materials config defines (`style.surface.material { <name> {…} }`)
1850 /// and the rung bindings (`plate material=`, `plate { root material= }`,
1851 /// `control material=`) — see `docs/rfc-material.md` § 5. Replaced wholesale
1852 /// on every config load, so a node or binding removed from config is gone
1853 /// after a reload.
1854 static NAMED_MATERIALS: RwLock<Vec<(String, crate::scene::material::MaterialDef)>> = RwLock::new(Vec::new());
1855 static MATERIAL_BINDINGS: RwLock<[Option<String>; 3]> = RwLock::new([None, None, None]);
1856 
1857 fn rung_index(rung: crate::scene::material::PlateRung) -> usize {
1858     match rung {
1859         crate::scene::material::PlateRung::Root => 0,
1860         crate::scene::material::PlateRung::Pane => 1,
1861         crate::scene::material::PlateRung::Control => 2,
1862     }
1863 }
1864 
1865 pub fn named_material(name: &str) -> Option<crate::scene::material::MaterialDef> {
1866     load_colors_once();
1867     style_read(&NAMED_MATERIALS).iter().find(|(n, _)| n == name).map(|(_, d)| d.clone())
1868 }
1869 
1870 /// Every defined material's name, sorted.
1871 pub fn material_names() -> Vec<String> {
1872     load_colors_once();
1873     let mut v: Vec<String> = style_read(&NAMED_MATERIALS).into_iter().map(|(n, _)| n).collect();
1874     v.sort();
1875     v
1876 }
1877 
1878 pub fn set_named_material(name: &str, def: Option<crate::scene::material::MaterialDef>) {
1879     let mut list = style_read(&NAMED_MATERIALS);
1880     list.retain(|(n, _)| n != name);
1881     if let Some(d) = def {
1882         list.push((name.to_string(), d));
1883     }
1884     style_write(&NAMED_MATERIALS, list);
1885 }
1886 
1887 pub fn material_binding(rung: crate::scene::material::PlateRung) -> Option<String> {
1888     load_colors_once();
1889     style_read(&MATERIAL_BINDINGS)[rung_index(rung)].clone()
1890 }
1891 
1892 pub fn set_material_binding(rung: crate::scene::material::PlateRung, name: Option<String>) {
1893     let mut b = style_read(&MATERIAL_BINDINGS);
1894     b[rung_index(rung)] = name;
1895     style_write(&MATERIAL_BINDINGS, b);
1896 }
1897 
1898 pub fn finish_spec() -> f32 {
1899     style_read(&FINISH_SPEC)
1900 }
1901 pub fn set_finish_spec(v: f32) {
1902     style_write(&FINISH_SPEC, v.max(0.0));
1903 }
1904 pub fn finish_shininess() -> f32 {
1905     style_read(&FINISH_SHININESS)
1906 }
1907 pub fn set_finish_shininess(v: f32) {
1908     style_write(&FINISH_SHININESS, v.max(1.0));
1909 }
1910 pub fn finish_curvature() -> f32 {
1911     style_read(&FINISH_CURVATURE)
1912 }
1913 pub fn set_finish_curvature(v: f32) {
1914     style_write(&FINISH_CURVATURE, v.max(0.0));
1915 }
1916 
1917 pub fn plate_blur() -> bool {
1918     load_colors_once();
1919     style_read(&PLATE_BLUR)
1920 }
1921 
1922 pub fn set_plate_blur(b: bool) {
1923     style_write(&PLATE_BLUR, b);
1924 }
1925 
1926 pub fn control_label_color() -> [f32; 4] {
1927     style_read(&CONTROL_LABEL_COLOR)
1928 }
1929 
1930 pub fn control_label_color_u8() -> [u8; 3] {
1931     let c = control_label_color();
1932     [
1933         (linear_to_srgb(c[0]) * 255.0).round() as u8,
1934         (linear_to_srgb(c[1]) * 255.0).round() as u8,
1935         (linear_to_srgb(c[2]) * 255.0).round() as u8,
1936     ]
1937 }
1938 
1939 pub fn set_control_label_color(color: [f32; 4]) {
1940     style_write(&CONTROL_LABEL_COLOR, color);
1941 }
1942 
1943 pub fn control_label_hover_color() -> Option<[f32; 4]> {
1944     style_read(&CONTROL_LABEL_HOVER_COLOR)
1945 }
1946 
1947 pub fn control_label_focus_color() -> Option<[f32; 4]> {
1948     style_read(&CONTROL_LABEL_FOCUS_COLOR)
1949 }
1950 
1951 pub fn control_label_color_for_state(hovered: bool, focused: bool) -> [u8; 3] {
1952     let c = if focused {
1953         control_label_focus_color().unwrap_or_else(|| control_label_color())
1954     } else if hovered {
1955         control_label_hover_color().unwrap_or_else(|| control_label_color())
1956     } else {
1957         control_label_color()
1958     };
1959     [
1960         (linear_to_srgb(c[0]) * 255.0).round() as u8,
1961         (linear_to_srgb(c[1]) * 255.0).round() as u8,
1962         (linear_to_srgb(c[2]) * 255.0).round() as u8,
1963     ]
1964 }
1965 
1966 pub fn control_label_color_detached() -> [f32; 4] {
1967     style_read(&CONTROL_LABEL_COLOR_DETACHED)
1968 }
1969 
1970 pub fn control_label_color_detached_u8() -> [u8; 3] {
1971     let c = control_label_color_detached();
1972     [
1973         (linear_to_srgb(c[0]) * 255.0).round() as u8,
1974         (linear_to_srgb(c[1]) * 255.0).round() as u8,
1975         (linear_to_srgb(c[2]) * 255.0).round() as u8,
1976     ]
1977 }
1978 
1979 pub fn set_control_label_color_detached(color: [f32; 4]) {
1980     style_write(&CONTROL_LABEL_COLOR_DETACHED, color);
1981 }
1982 
1983 pub fn control_label_color_detached_for_state(hovered: bool, focused: bool) -> [u8; 3] {
1984     let c = if focused {
1985         control_label_focus_color().unwrap_or_else(|| control_label_color_detached())
1986     } else if hovered {
1987         control_label_hover_color().unwrap_or_else(|| control_label_color_detached())
1988     } else {
1989         control_label_color_detached()
1990     };
1991     [
1992         (linear_to_srgb(c[0]) * 255.0).round() as u8,
1993         (linear_to_srgb(c[1]) * 255.0).round() as u8,
1994         (linear_to_srgb(c[2]) * 255.0).round() as u8,
1995     ]
1996 }
1997 
1998 #[cfg(test)]
1999 mod color_tests {
2000     use super::*;
2001 
2002     #[test]
2003     fn test_parse_hex_rgba() {
2004         // 6-digit → alpha 1.0, raw sRGB
2005         assert_eq!(parse_hex_rgba("#ff0000"), Some([1.0, 0.0, 0.0, 1.0]));
2006         // 8-digit → explicit alpha
2007         assert_eq!(parse_hex_rgba("#00ff0080"), Some([0.0, 1.0, 0.0, 128.0 / 255.0]));
2008         // leading '#' optional; quotes/whitespace tolerated
2009         assert_eq!(parse_hex_rgba("\" ffffff \""), Some([1.0, 1.0, 1.0, 1.0]));
2010         assert_eq!(parse_hex_rgba("000000"), Some([0.0, 0.0, 0.0, 1.0]));
2011         // invalid
2012         assert_eq!(parse_hex_rgba("#fff"), None);
2013         assert_eq!(parse_hex_rgba("nothex"), None);
2014         assert_eq!(parse_hex_rgba(""), None);
2015         // rgb drops alpha; linear applies gamma to rgb only
2016         assert_eq!(parse_hex_rgb("#ff0000"), Some([1.0, 0.0, 0.0]));
2017         assert_eq!(parse_hex_rgba_linear("#000000ff"), Some([0.0, 0.0, 0.0, 1.0]));
2018         // byte primitive
2019         assert_eq!(parse_hex_bytes("#010203"), Some([1, 2, 3, 255]));
2020         assert_eq!(parse_hex_bytes("#01020304"), Some([1, 2, 3, 4]));
2021         assert_eq!(parse_hex_bytes("#fff"), None);
2022     }
2023 
2024     #[test]
2025     fn perceptual_fade_alpha_walks_lightness_not_luminance() {
2026         let overlay = [0.62, 0.70, 0.95];
2027         let backdrop = [0.028, 0.028, 0.041];
2028         let a = |t: f32| perceptual_fade_alpha(t, overlay, backdrop, 1.0);
2029 
2030         // Endpoints are the plain ones, and the ramp only ever falls.
2031         assert!((a(0.0) - 1.0).abs() < 1e-4);
2032         assert!(a(1.0).abs() < 1e-4);
2033         for i in 1..=20 {
2034             assert!(a(i as f32 / 20.0) <= a((i - 1) as f32 / 20.0));
2035         }
2036 
2037         // The correction runs BELOW the straight ramp — that ramp's excess brightness
2038         // through the middle is the bow the eye reads as non-linear.
2039         assert!(a(0.5) < 0.5 - 0.1, "midpoint {} should sit well under 0.5", a(0.5));
2040 
2041         // What it buys: composited lightness lands on a straight line. cbrt(luminance) is
2042         // the same proxy the implementation uses, checked here end to end through blending.
2043         let lum = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
2044         let l_at = |t: f32| {
2045             let a = a(t);
2046             let y: f32 = a * lum(overlay) + (1.0 - a) * lum(backdrop);
2047             y.cbrt()
2048         };
2049         let (top, bottom) = (l_at(0.0), l_at(1.0));
2050         for i in 0..=10 {
2051             let t = i as f32 / 10.0;
2052             let ideal = top + (bottom - top) * t;
2053             assert!((l_at(t) - ideal).abs() < 1e-3, "t={t}: {} vs {ideal}", l_at(t));
2054         }
2055 
2056         // No lightness contrast to shape: falls back to the straight ramp.
2057         let flat = perceptual_fade_alpha(0.5, overlay, overlay, 1.0);
2058         assert!((flat - 0.5).abs() < 1e-4);
2059     }
2060 
2061     #[test]
2062     fn test_print_active_config() {
2063         let path = crate::config::get_config_path();
2064         println!("ACTIVE CONFIG PATH: {:?}", path);
2065         if let Ok(content) = std::fs::read_to_string(&path) {
2066             println!("FILE READ OK! Length: {}", content.len());
2067             let val = crate::config::parse_kdl_to_json(&content);
2068             println!("PARSED JSON POINTER: {:?}", val.pointer("/style/control/dropdown/color"));
2069         } else {
2070             println!("FILE READ FAILED!");
2071         }
2072         println!("DROPDOWN COLOR GETTER: {:?}", dropdown_background_color());
2073         println!("LIST FONT COLOR GETTER: {:?}", list_font_color());
2074         println!("PLATE COLOR: {:?}", plate_color());
2075         println!("PLATE BORDER COLOR: {:?}", plate_border_color());
2076         println!("PLATE BORDER THICKNESS: {:?}", plate_border_thickness());
2077         println!("PLATE BLUR: {:?}", plate_blur());
2078     }
2079 }
2080 
2081 
2082 #[cfg(test)]
2083 mod tests {
2084     use super::*;
2085 
2086     /// The hex that pins a built-in colour is NOT its floats times 255.
2087     ///
2088     /// The style loader gamma-decodes every config hex; the constants in this
2089     /// file are already linear. So `PARAM_BG = [0.10, 0.10, 0.14]` is spelled
2090     /// `#595969` in config, and the naive `#1a1a24` decodes to a plate ten
2091     /// times darker — silently, both being valid config. Pinned here because
2092     /// it cost a measurement round: a sweep meant to hold the tint constant
2093     /// was quietly sweeping it, and the two halves disagreed by 3x.
2094     #[test]
2095     fn the_hex_that_pins_a_default_round_trips_through_the_gamma_decode() {
2096         let decoded = parse_hex_rgba_linear("#595969").expect("valid hex");
2097         // RGB only — a 6-digit hex carries no alpha, which the asserts at the
2098         // bottom cover as its own hazard.
2099         for (got, want) in decoded.iter().take(3).zip(PARAM_BG.iter().take(3)) {
2100             assert!(
2101                 (got - want).abs() < 0.005,
2102                 "#595969 decodes to {decoded:?}, PARAM_BG is {PARAM_BG:?}"
2103             );
2104         }
2105 
2106         // The naive spelling, and how far off it lands.
2107         let naive = parse_hex_rgba_linear("#1a1a24").expect("valid hex");
2108         assert!(
2109             naive[0] < PARAM_BG[0] / 5.0,
2110             "#1a1a24 was supposed to be nowhere near PARAM_BG, got {naive:?}"
2111         );
2112 
2113         // Alpha is NOT decoded, and a 6-digit hex means OPAQUE — dropping the
2114         // last byte off a translucent plate colour does not leave it alone.
2115         assert!((parse_hex_rgba_linear("#05050840").unwrap()[3] - 0.251).abs() < 0.002);
2116         assert_eq!(parse_hex_rgba_linear("#050508").unwrap()[3], 1.0);
2117     }
2118 
2119     /// Rim refraction defaults OFF and clamps, like its neighbour.
2120     #[test]
2121     fn plate_refraction_defaults_off_and_clamps() {
2122         assert_eq!(plate_refraction(), 0.0, "off unless a config asks");
2123         set_plate_refraction(0.6);
2124         assert_eq!(plate_refraction(), 0.6);
2125         set_plate_refraction(9.0);
2126         assert_eq!(plate_refraction(), 1.0);
2127         set_plate_refraction(-0.5);
2128         assert_eq!(plate_refraction(), 0.0);
2129     }
2130 
2131     /// The plate's backdrop compression defaults OFF and clamps.
2132     ///
2133     /// Off is load-bearing: it is the behaviour every config already in the
2134     /// wild has, and a toolkit-wide default that changed how every frosted
2135     /// surface in the DE looks would arrive unannounced in eighteen apps.
2136     /// Opting in is a per-app `style.surface.plate.backdrop_compression`.
2137     #[test]
2138     fn backdrop_compression_defaults_off_and_clamps() {
2139         assert_eq!(plate_backdrop_compression(), 0.0, "off unless a config asks");
2140 
2141         set_plate_backdrop_compression(0.85);
2142         assert_eq!(plate_backdrop_compression(), 0.85);
2143 
2144         // The shader clamps too, but a nonsense config value should not be
2145         // able to reach it and make the plate flat or inverted.
2146         set_plate_backdrop_compression(4.0);
2147         assert_eq!(plate_backdrop_compression(), 1.0);
2148         set_plate_backdrop_compression(-1.0);
2149         assert_eq!(plate_backdrop_compression(), 0.0);
2150     }
2151 
2152     /// A colour pinned by one test is invisible to a test beside it.
2153     ///
2154     /// The invariant that ends the parallel-flake class here. These statics
2155     /// are process-wide, and `test_graph_style_configuration` pins a dozen of
2156     /// them without restoring any; before `style_write` became per-thread
2157     /// under `cfg(test)`, every test running alongside it saw those values.
2158     #[test]
2159     fn a_pinned_colour_is_private_to_its_thread() {
2160         let base = node_color();
2161         let pinned = [0.123, 0.456, 0.789, 1.0];
2162         assert_ne!(base, pinned, "pick a value the config cannot already hold");
2163 
2164         set_node_color(pinned);
2165         assert_eq!(node_color(), pinned, "the pinning thread sees its own value");
2166 
2167         let elsewhere = std::thread::spawn(node_color).join().unwrap();
2168         assert_eq!(elsewhere, base, "a thread beside it must still see the shared base");
2169     }
2170 }