git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit21c275aab4f1ab4769271c2c362e0720e9519677
parent3eb7c4b1aa
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 17:23
Replace the text scrim with compositor-side backdrop compression

The scrim darkened the whole bubble, even over a backdrop that needed no
help, and needed a per-frame backdrop measurement pushed from the
compositor to adapt. cce-fx now compresses the backdrop's luminance under
each segment (module { backdrop_compress }), per pixel, so the bar drops
the pool, the text_scrim/text_contrast keys, the backdrop subscription and
the run-width field of TextPrim that only the pool read.

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

 CLAUDE.md        | 116 ++++--------
 src/config.rs    |  36 ----
 src/listeners.rs |  35 +---
 src/main.rs      | 542 ++-----------------------------------------------------
 src/modules.rs   |   2 -
 5 files changed, 59 insertions(+), 672 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 55c6541..2d6e212 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,7 +19,7 @@ cce-icons glyph textures), `src/listeners.rs` (status/switcher socket tasks).
 
 ```sh
 cargo build --release                 # standalone build (or `-p cce-status-interface` from the workspace root)
-cargo test                            # 57 tests: main.rs (contrast, parsers), config.rs, tray.rs, the tray bridge's x11.rs and title.rs
+cargo test                            # 37 tests: main.rs (parsers, droplet geometry), config.rs, tray.rs, the tray bridge's x11.rs and title.rs
 make install                          # release build, then `ccebuild install --no-build cce-status-interface`
 ```
 
@@ -226,7 +226,7 @@ listen to tray D-Bus, etc.:
 - **Compositor status feed** (`spawn_status_listener`): connects to
   `/tmp/cce-status[-interface]-{WAYLAND_DISPLAY}.sock` and subscribes, one task
   per topic, line-oriented — `layout` and `title` only in the process that owns
-  the window module, `dismiss` and `backdrop` in every one. Reconnects back off
+  the window module, `dismiss` in every one. Reconnects back off
   1s doubling to 30s, reset the moment a connection delivers a line: a
   compositor that does not know a topic drops the subscription on sight, so a
   flat retry made a bar running ahead of its compositor reconnect once a second
@@ -264,11 +264,6 @@ listen to tray D-Bus, etc.:
 - **Tray** (`spawn_status_tray`): a full StatusNotifierItem/Watcher host over `zbus`,
   including DBusMenu fetching. Icons arrive as pixmaps or theme names (rendered via
   `resvg`/`png`).
-- **Backdrop** (`spawn_status_listener("backdrop <app_id>")`): what THIS segment
-  is composited over, measured compositor-side and pushed as `<luma> <spread>`
-  (0-100 each) or `unknown`. Every module process subscribes, naming itself with
-  `status_app_id()`. A Wayland client cannot see behind its own surface, so this
-  is the only source of the fact — see `module { text_contrast }` below.
 - **Switcher** (`spawn_switcher_listener`): binds
   `/tmp/cce-status-interface-switcher-{WAYLAND_DISPLAY}.sock`; a line on it fires
   `SwitcherTriggered`.
@@ -299,7 +294,7 @@ box — full strength drew a blocky lit picture-frame with the band pinned, and
 envelope folds across the body with the band grown; both were tried. The panel
 keeps the silhouette-hugging water terms (clarity, rim crest, core, contact
 shadow), and the hovered row's highlight is a rounded pill inset from the
-panel edge (`menu_hover_rect`, drawn post-scrim), not a full-width rect.
+panel edge (`menu_hover_rect`, drawn before the text), not a full-width rect.
 
 **No cce-cloud popups remain in this app**: the window picker (window-module
 click → `MenuReady` rows of `Ccectl(["focus-window", id])`) is an in-surface
@@ -360,18 +355,9 @@ readouts in the `stats` bubble, default `module { spacing }`) and
 `module { icon_weight }` (OpenType weight of the number, unset = regular)
 and `module { text_raise }` (lifts module text above vertical center, logical
 px, bar-side only — every module funnels through `centered_text_y`) and
-`module { text_scrim }` (0-1 resting opacity of a
-feathered pool filling each module box, the DE's one text-contrast treatment)
-and `module { text_scrim_feather }` (that pool's falloff in logical px,
-default a quarter of the box height) and `module { text_contrast }` (adaptive
-contrast 0-1, default 0 = off — the compositor's `backdrop` measurement
-deepens the pool through it, so the ground darkens only as far as a backdrop
-the configured text color cannot carry demands; on its own, with no
-`text_scrim`, it makes the pool appear ONLY when the backdrop earns it).
-(The glyph-decorating treatments this replaced — `text_relief`'s letterpress
-underlay and `text_halo`'s four-copy outline — were deleted 2026-08-28 once
-the scrim superseded both; don't reintroduce a per-letterform treatment
-without a reason the ground cannot serve.) Everything is read through
+`module { backdrop_compress }` (the minimum WCAG contrast ratio the text must
+hold against any backdrop pixel, e.g. 10; unset = off — read by the
+COMPOSITOR only, see "Text contrast" below). Everything is read through
 `cce_ui::config::cached_config()`; KDL is converted to JSON
 (`cce_ui::config::parse_kdl_to_json`) and looked up by **explicit JSON
 pointer only**: every key names its canonical nesting
@@ -401,69 +387,39 @@ is gone; `text_colors_stay_srgb_and_quad_colors_are_linearized`, in
 `config.rs`, is the spec. Config changes are picked up by polling the file
 mtime in `tick()`, so there is no reload event to wire up.
 
-## Adaptive text contrast
+## Text contrast: backdrop compression
 
 The bar draws into its own buffer and can never see what it is composited
 over, so a module box at `background_color` alpha `30` leaves its text at the
-mercy of whatever the desktop shows through it. `module { text_contrast }`
-closes that loop with the compositor, which CAN see:
-
-1. `cce-fx` measures each segment's backdrop per frame (`backdrop.rs`) and
-   pushes `<luma> <spread>` on the status socket's `backdrop` topic.
-2. `contrast_demand()` checks the configured text color's WCAG contrast
-   against that backdrop at three points — the mean AND both ends of the
-   spread — and takes the worst. Checking only the mean is the trap: a segment
-   half on a black grid cell and half on a light gap averages to a comfortable
-   mid-gray while the text is invisible over one half.
-3. `tick` eases `contrast_now` toward that demand over ~120ms. Stepping
-   straight to it makes the scrim pulse as the desktop pans under the segment.
-
-`module { text_scrim }` is the treatment itself: a feathered pool (`cce_ui`'s
-`Prim::Glow` — solid through a core rect, falling off to nothing across
-`text_scrim_feather` px, tessellated as per-vertex-alpha rings so there is no
-banding) filling each module box. It darkens the ground the glyphs sit on
-rather than decorating the letterforms. It rests at the configured opacity and
-`text_contrast` deepens it from there, so it is a constant when that knob is
-off — and either knob alone is meaningful.
-
-One pool per bubble, not per text run, so a segment reads as one darkened
-lozenge rather than a pill inside a pill. Its shape is the bubble's ACTUAL
-shape, which means two paths: a droplet module gets `Prim::DropletScrim`
-(cce-ui shader mode 12 — the droplet's own SDF under the same `DropletSpec`,
-filled flat and feathered inward, so the vignette's edge is the drop's edge by
-construction), while a plain rounded box gets `Prim::Glow` with its core inset
-by exactly the feather, which lands the gradient's outer edge on the box edge.
-The rounded-rect pool is clipped to its box; the droplet one needs no clip,
-since the shader cannot draw outside the silhouette it is evaluating.
-
-The pool's color comes from `dominant_run_color` — the widest run inside the
-box, using the width that rides `TextPrim`'s last field — with
-`dominant_icon_color` as the fallback for a box that holds tray icons and no
-run: the icons read as light glyphs (dark pixmaps are recolored toward white),
-so the tray is grounded as a white run would be, with a black pool. Before
-2026-09-05 a box with no measured run got no pool, which left the tray the
-one bare bubble in the strip — visibly lighter than its neighbors, and the
-one segment the backdrop feed could not deepen. A box holding neither text
-nor icons still gets no pool. Width is the tiebreak because a module mixing
-colors is led by its longest label.
-
-The pool takes its color from `treatment_rgb` — whichever of black/white the
-run reads against, by contrast ratio (the WCAG crossover is near 0.18, not
-0.5). Chosen from the run's OWN color, not the configured module color: a
-module may paint a run in something else entirely, and the volume module's
-muted state uses the shared `disabled_color`. A black pool behind black text
-is not a weaker treatment — it is an eraser. (That `disabled_color` is a light
-red as of 2026-08-28, chosen so the muted run keeps the same dark pool as
-every other bubble instead of inverting to a light one.)
-
-A window covering part of a segment is measured too — the compositor reads
-that window's own content over the overlapping strip and blends it with the
-desktop reading for the rest. `unknown` is left for content it genuinely
-cannot read (no committed buffer, an unsupported read format).
-
-Failures resolve toward legible in every direction: an unparseable or absent
-line reads as `(50, 100)` — mid luminance, full spread — which drives the
-scrim rather than switching it off.
+mercy of whatever the desktop shows through it. The fix lives in the
+compositor, which CAN see: `module { backdrop_compress }` makes `cce-fx`
+compress the luminance of each segment's backdrop — the blurred one, or the
+refracted one when the droplet lens is live — so the module text keeps that
+contrast ratio over every pixel of it. For light text the backdrop's bright
+parts are pulled down under a ceiling; for dark text its shadows are lifted
+instead. Below a knee (half the ceiling) nothing moves, so a backdrop that is
+already dark enough is left exactly as it is, and the curve approaches the
+ceiling asymptotically with no seam. The color is scaled, not desaturated, so
+a bright backdrop keeps its hue. The text color is `module { text_color }`
+(else the shared `/style/status/normal_color`), read compositor-side;
+`backdrop_compress_params` in cce-compositor's `config.rs` turns the pair into
+the shader's ceiling, and scenefx's `tex.frag`/`droplet.frag`
+(`compress_backdrop`) apply it. It rides the backdrop blur, so a bar with
+`/style/status/background_blur` at 0 has nothing to compress. The bubble's own
+translucent fill and the droplet's lighting composite on top of the compressed
+backdrop and lift it, so the ratio reached is well under the one asked for:
+measured in a shadow on a pure-white desktop with this repo's droplet style,
+`backdrop_compress 4.5` reached 2.7:1 and `10` reached 4.7:1 (`15`: 6.4:1).
+Over a backdrop already dark enough, on and off are pixel-identical.
+
+This replaced the bar-side scrim on 2026-10-01: a dark feathered pool
+(`text_scrim`, deepened by `text_contrast` from a per-segment `backdrop`
+luminance measurement the compositor pushed on the status socket) that
+darkened the whole bubble even over a backdrop that needed no help. Compression
+is per pixel, so it needs neither the pool nor the measurement loop. (Before
+the scrim, `text_relief`'s letterpress underlay and `text_halo`'s four-copy
+outline decorated the letterforms; they went 2026-08-28. Don't reintroduce a
+per-letterform treatment without a reason the backdrop cannot serve.)
 
 ## Interactions worth knowing before touching input code
 
diff --git a/src/config.rs b/src/config.rs
index 5d0dbae..aa267b3 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -238,42 +238,6 @@ pub(crate) fn read_icon_alpha_from_config() -> f32 {
     cfg_f32("/module/icon_alpha").unwrap_or(1.0).clamp(0.0, 1.0)
 }
 
-/// `module { text_contrast }` — adaptive contrast strength (0 = off, the
-/// default; 1 = full). The compositor's per-segment `backdrop` measurement
-/// drives the scrim through it: the ground darkens only as far as a backdrop
-/// the configured text color cannot carry demands, and its strength tracks
-/// how badly the text is losing. Bar-side only.
-///
-/// On its own it makes the scrim appear only when the backdrop earns it;
-/// alongside `module { text_scrim }` it deepens that resting ground.
-pub(crate) fn read_text_contrast_from_config() -> f32 {
-    cfg_f32("/module/text_contrast").unwrap_or(0.0).clamp(0.0, 1.0)
-}
-
-/// `module { text_scrim }` — opacity 0-1 of a dark feathered pool drawn
-/// inside each module box, beneath everything the module paints (0 = off,
-/// the default). It darkens the ground the glyphs sit on rather than
-/// decorating the letterforms, which is what survives a busy backdrop
-/// without putting a rim on every letterform.
-///
-/// The DE's one text-contrast treatment, and `module { text_contrast }`
-/// applies on top: the scrim rests at this opacity and deepens toward
-/// opaque as the measured backdrop demands more.
-pub(crate) fn read_text_scrim_from_config() -> f32 {
-    cfg_f32("/module/text_scrim").unwrap_or(0.0).clamp(0.0, 1.0)
-}
-
-/// `module { text_scrim_feather }` — how far, in logical px, the scrim fades
-/// out from its solid core (default: a quarter of the box's height, which
-/// keeps the whole gradient inside the box at any bar height).
-///
-/// The feather is drawn OUTSIDE the core rect, so the core is inset by this
-/// much: feather and inset are the same number, and the pool reaches the
-/// box's edge exactly.
-pub(crate) fn read_text_scrim_feather_from_config() -> Option<f32> {
-    cfg_f32("/module/text_scrim_feather").map(|v| v.max(0.0))
-}
-
 pub(crate) fn read_status_box_corner_radius_from_config() -> f32 {
     // App-native ONLY (~/.config/cce/cce-status-interface/config.kdl,
     // merged over the shared config by cce-ui): `module { corner_radius }`.
diff --git a/src/listeners.rs b/src/listeners.rs
index 03350f4..51ee090 100644
--- a/src/listeners.rs
+++ b/src/listeners.rs
@@ -6,9 +6,8 @@ use crate::CustomEvent;
 /// Subscribe to one compositor status topic and forward its pushes as
 /// [`CustomEvent`]s, reconnecting every second until the socket is there.
 ///
-/// `sub` is the whole subscription line, because one topic takes an argument:
-/// `backdrop <app_id>` asks what THAT segment is composited over (every other
-/// topic is the same for all subscribers, so it is a bare word).
+/// `sub` is the whole subscription line; the topic is its first word, so a
+/// topic that takes an argument can still be subscribed to.
 pub(crate) async fn spawn_status_listener(sub: String, sender: calloop::channel::Sender<CustomEvent>) {
     use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
     use tokio::net::UnixStream;
@@ -17,7 +16,7 @@ pub(crate) async fn spawn_status_listener(sub: String, sender: calloop::channel:
     // does not know this topic drops the subscription on sight, so the flat
     // 1s retry turned an unrecognized topic into a permanent once-a-second
     // reconnect from every module process — which is exactly what a bar
-    // running ahead of its compositor does with `backdrop` (the two halves
+    // running ahead of its compositor does with a new topic (the two halves
     // deploy separately, and cce-fx only restarts at login).
     let mut retry_s = 1u64;
     loop {
@@ -57,11 +56,6 @@ pub(crate) async fn spawn_status_listener(sub: String, sender: calloop::channel:
                             // Click-away-close: the payload is the app_id of
                             // the segment the press landed on ("-" for none).
                             "dismiss" => CustomEvent::MenuDismiss(val.clone()),
-                            // "<luma> <spread>", both 0-100, or "unknown"
-                            // when the compositor has no sample for this
-                            // segment — treated as the worst case rather
-                            // than as no news.
-                            "backdrop" => CustomEvent::BackdropUpdated(parse_backdrop(&val)),
                             _ => unreachable!(),
                         };
                         let _ = sender.send(ev);
@@ -75,29 +69,6 @@ pub(crate) async fn spawn_status_listener(sub: String, sender: calloop::channel:
     }
 }
 
-/// Parse a `backdrop` line into (luma, spread), both 0-100.
-///
-/// Anything unreadable — the literal "unknown", a truncated line, a future
-/// compositor's extra fields — reports the worst case: mid luminance and full
-/// spread, which drives the scrim. Guessing "uniform and bright" from a
-/// line we failed to understand would silently turn the treatment OFF, and
-/// unreadable text is a worse failure than an unnecessary scrim.
-pub(crate) fn parse_backdrop(line: &str) -> (u8, u8) {
-    const UNKNOWN: (u8, u8) = (50, 100);
-    let mut parts = line.split_whitespace();
-    let (Some(luma), Some(spread)) = (parts.next(), parts.next()) else {
-        return UNKNOWN;
-    };
-    // Parsed wide and range-checked rather than clamped, so that "101" and
-    // "300" fail the same way. Clamping would quietly turn an out-of-protocol
-    // luma into "bright and uniform" — the one answer that switches the
-    // treatment off.
-    match (luma.parse::<u16>(), spread.parse::<u16>()) {
-        (Ok(l), Ok(s)) if l <= 100 && s <= 100 => (l as u8, s as u8),
-        _ => UNKNOWN,
-    }
-}
-
 pub(crate) async fn spawn_switcher_listener(sender: calloop::channel::Sender<CustomEvent>) {
     use tokio::io::AsyncBufReadExt;
     use tokio::net::UnixListener;
diff --git a/src/main.rs b/src/main.rs
index 9ada056..5123310 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -76,8 +76,8 @@ pub struct SystemStats {
 /// A bundled cce-icons glyph placed in the bar: `image` is the tinted
 /// texture from `icons::tinted_icon`, the rect is logical px, `alpha` the
 /// ghosting under a superimposed readout. Retained like `text_prims` and
-/// replayed by `display_list`, drawn after the scrim and before the text so
-/// the number sits on the glyph.
+/// replayed by `display_list`, drawn before the text so the number sits on
+/// the glyph.
 #[derive(Debug, Clone, Copy)]
 pub struct IconPrim {
     pub image: u32,
@@ -151,63 +151,10 @@ pub(crate) enum CustomEvent {
     MenuDismiss(String),
     /// A tray icon's DBusMenu, fetched and flattened for the in-surface menu.
     TrayMenuFetched { destination: String, menu_path: String, pages: Vec<MenuPage> },
-    /// What this segment is composited over, measured by the compositor:
-    /// (luma, spread), both 0-100. The bar cannot see behind its own
-    /// translucent box, so this is the only source of that fact — see
-    /// `module { text_contrast }`.
-    BackdropUpdated((u8, u8)),
     ToggleHideModules,
     ToggleAdjustPositionMode,
 }
 
-/// One sRGB channel to linear light (the WCAG transfer function).
-fn to_linear(c: f32) -> f32 {
-    let c = c.clamp(0.0, 1.0);
-    if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
-}
-
-/// WCAG relative luminance of a raw-sRGB color, 0-1. Must agree with the
-/// compositor's `backdrop::relative_luminance` — the two are the halves of
-/// one contrast comparison, and weighting them differently would make the
-/// ratio meaningless.
-fn relative_luminance(rgba: [f32; 4]) -> f32 {
-    0.2126 * to_linear(rgba[0]) + 0.7152 * to_linear(rgba[1]) + 0.0722 * to_linear(rgba[2])
-}
-
-/// WCAG contrast ratio between two relative luminances, 1.0 (identical) to
-/// 21.0 (black on white).
-fn contrast_ratio(a: f32, b: f32) -> f32 {
-    let (hi, lo) = if a > b { (a, b) } else { (b, a) };
-    (hi + 0.05) / (lo + 0.05)
-}
-
-/// How badly `text` fails to read against a backdrop of luminance `bg`:
-/// 0 once the pair clears WCAG AA for normal text (4.5:1), rising to 1 as
-/// the two converge on invisible.
-fn contrast_deficit(text: f32, bg: f32) -> f32 {
-    const AA: f32 = 4.5;
-    ((AA - contrast_ratio(text, bg)) / (AA - 1.0)).clamp(0.0, 1.0)
-}
-
-/// How much help text of luminance `text_luma` needs over a backdrop
-/// measured as `(luma, spread)`, 0 (none) to 1 (as much as the knob allows).
-///
-/// Contrast is checked at BOTH ends of the spread as well as at the mean, and
-/// the worst answer wins: a segment lying half on a black cell and half on a
-/// light gap averages to a perfectly comfortable mid-gray while the text is
-/// unreadable over one of the two halves. Checking only the mean is the
-/// mistake that would make an adaptive scheme look broken exactly where the
-/// fixed one already worked.
-fn contrast_demand(text_luma: f32, (luma, spread): (u8, u8)) -> f32 {
-    let mid = luma as f32 / 100.0;
-    let half = (spread as f32 / 100.0) / 2.0;
-    let lo = (mid - half).clamp(0.0, 1.0);
-    let hi = (mid + half).clamp(0.0, 1.0);
-    contrast_deficit(text_luma, mid)
-        .max(contrast_deficit(text_luma, lo))
-        .max(contrast_deficit(text_luma, hi))
-}
-
 /// A droplet spec whose shape knobs resolve against `reference_h` instead of
 /// the box they are given, for a box that is actually `box_h` tall.
 ///
@@ -266,82 +213,6 @@ fn spec_at_reference_height(
     out
 }
 
-/// The color a treatment behind or around `rgb` text should be drawn in:
-/// whichever of black/white that text reads against.
-///
-/// Used by the scrim, and applied PER RUN rather than from the configured
-/// module color, because a module may paint a run in something else entirely
-/// — the volume module's muted state uses the shared `disabled_color`. A
-/// black pool behind black text is not a weaker treatment, it is an eraser.
-pub(crate) fn treatment_rgb(rgb: [u8; 3]) -> [f32; 3] {
-    let luma = relative_luminance([rgb[0] as f32 / 255.0, rgb[1] as f32 / 255.0, rgb[2] as f32 / 255.0, 1.0]);
-    if contrast_ratio(luma, 0.0) >= contrast_ratio(luma, 1.0) {
-        [0.0, 0.0, 0.0]
-    } else {
-        [1.0, 1.0, 1.0]
-    }
-}
-
-/// The scrim's opacity: it rests at the configured `base` and deepens toward
-/// opaque as the measured backdrop demands more. `demand` is the eased
-/// `contrast_now`, which is already zero when `module { text_contrast }` is
-/// off — so without that knob the scrim is a constant, which is the point of
-/// having it.
-fn scrim_alpha(base: f32, demand: f32) -> f32 {
-    (base + (1.0 - base) * demand.clamp(0.0, 1.0)).clamp(0.0, 1.0)
-}
-
-/// How far the pool fades out, logical px. Defaults to a quarter of the
-/// bubble's height so the gradient scales with the bar, and is capped at half
-/// of each axis: the feather is drawn OUTSIDE the solid core, so the core is
-/// inset by this much, and a larger one would invert it and the pool would
-/// vanish — exactly where a narrow module (a lone icon) lands.
-fn scrim_feather(w: f32, h: f32, configured: Option<f32>) -> f32 {
-    configured.unwrap_or(h * 0.25).max(0.0).min(w / 2.0).min(h / 2.0)
-}
-
-/// The color of the widest measured text run inside a box, which is the run a
-/// box-sized pool is really there to protect. None when the box holds no
-/// measured run at all.
-///
-/// Width is the tiebreak rather than, say, the first run, because a module
-/// that mixes colors (a value in an accent beside its label) is led by its
-/// longest label, and that is the one whose legibility carries the segment.
-fn dominant_run_color(runs: &[TextPrim], bx: f32, by: f32, bw: f32, bh: f32) -> Option<[u8; 3]> {
-    let mut best: Option<(f32, [u8; 3])> = None;
-    for (_, tsize, x, y, color, _, _, _, run_w, _) in runs {
-        let Some(rw) = *run_w else { continue };
-        // Runs belong to the box they sit in; a segment with an expanded menu
-        // has text in both.
-        let (cx, cy) = (x + rw * 0.5, y + tsize * 0.5);
-        if cx < bx || cx > bx + bw || cy < by || cy > by + bh {
-            continue;
-        }
-        if best.map_or(true, |(w, _)| rw > w) {
-            best = Some((rw, *color));
-        }
-    }
-    best.map(|(_, c)| c)
-}
-
-/// The pool color for a box that holds tray icons rather than text. The
-/// tray is the one module whose content is not a measured run, and the
-/// text-keyed lookup finding nothing used to leave its bubble bare — the
-/// only one in the strip painted without the pool, visibly lighter than its
-/// neighbors and the only one that never answered the backdrop. The icons
-/// read as light glyphs (a dark pixmap is recolored toward white in
-/// `TrayModule::render`), so the box gets what a white run would get: a
-/// black pool. None when no icon sits in the box.
-fn dominant_icon_color(icons: &[TrayIconBounds], bx: f32, by: f32, bw: f32, bh: f32) -> Option<[u8; 3]> {
-    icons
-        .iter()
-        .any(|b| {
-            let (cx, cy) = (b.x + b.w * 0.5, b.y + b.h * 0.5);
-            cx >= bx && cx <= bx + bw && cy >= by && cy <= by + bh
-        })
-        .then_some([255, 255, 255])
-}
-
 /// The in-surface right-click menu: instead of spawning a popup process, the
 /// module's own surface EXPANDS below the bar strip to contain the menu. The
 /// compositor treats a status segment thicker than the bar as expanded — it
@@ -509,35 +380,9 @@ struct StatusApp {
     ///
     /// The spec rides each box rather than being read from `self.droplet` at
     /// paint time because the expanded menu box needs a DIFFERENT one — see
-    /// `spec_at_reference_height` — and the scrim has to be handed the same
-    /// spec the drop was drawn with or the two silhouettes disagree.
+    /// `spec_at_reference_height`.
     droplet_boxes: Vec<(f32, f32, f32, f32, [f32; 4], cce_ui::scene::paint::DropletSpec)>,
     droplet: Option<cce_ui::scene::paint::DropletSpec>,
-    /// Adaptive-contrast strength (0 = off) — see
-    /// `read_text_contrast_from_config`. Deepens the scrim as the measured
-    /// backdrop demands more; on its own (no `text_scrim`) it makes the scrim
-    /// appear only when it is needed.
-    text_contrast: f32,
-    /// The compositor's last `backdrop` push for this segment: (luma,
-    /// spread), both 0-100. Starts at the worst case, so a segment that
-    /// never hears from the compositor errs toward legible rather than
-    /// toward bare.
-    backdrop: (u8, u8),
-    /// Relative luminance of the configured module text color, 0-1. Cached
-    /// at config-reload time because the contrast decision needs it every
-    /// frame and the color changes about never.
-    text_luma: f32,
-    /// Dark feathered pool behind each module's content (0 = off) — see
-    /// `read_text_scrim_from_config`. The DE's one text-contrast treatment.
-    text_scrim: f32,
-    /// Feather distance for that pool, logical px; None derives it from the
-    /// box height.
-    text_scrim_feather: Option<f32>,
-    /// The contrast demand actually in effect, eased toward the backdrop's
-    /// in `tick`. Stepping straight to the target makes the scrim pulse as
-    /// the desktop pans under a segment, which reads as a flicker rather than
-    /// as an adaptation.
-    contrast_now: f32,
     text_prims: Vec<TextPrim>,
     /// The glyphs the stat modules paint their readouts on — see `IconPrim`.
     icon_prims: Vec<IconPrim>,
@@ -572,8 +417,8 @@ struct StatusApp {
     /// what the in-surface menu expansion grows out of and contracts back to.
     collapsed_box: Option<(f32, f32)>,
     /// The hovered menu row's highlight pill (x, y, w, h), set by the menu
-    /// branch of `rebuild_layout` and drawn in `display_list` AFTER the scrim
-    /// (so the pool does not darken it) and before the text.
+    /// branch of `rebuild_layout` and drawn in `display_list` before the
+    /// text.
     menu_hover_rect: Option<(f32, f32, f32, f32)>,
     input_regions: Vec<(i32, i32, i32, i32)>,
     module_bounds: Vec<ModuleBounds>,
@@ -593,10 +438,8 @@ struct StatusApp {
 }
 
 /// The Wayland `app_id` a segment presents — the compositor places segments
-/// by it, and it is also how this process names itself to the `backdrop`
-/// subscription. A free function because `new()` must be able to spell it
-/// before there is a `StatusApp` to ask, and the two spellings below are
-/// exactly the kind of thing that drifts when copied.
+/// by it. The two spellings below are exactly the kind of thing that drifts
+/// when copied, so they live in one place.
 fn status_app_id(selected: Option<(&str, Side)>) -> String {
     let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
     let use_interface_prefix = std::path::Path::new(&format!("/tmp/cce-status-interface-{}.sock", display)).exists();
@@ -616,22 +459,6 @@ impl StatusApp {
         status_app_id(selected)
     }
 
-    /// The contrast help this segment's measured backdrop calls for, 0-1.
-    ///
-    /// The compositor reports a mean luminance and a spread. Contrast is
-    /// checked at BOTH ends of that spread as well as at the mean, and the
-    /// worst answer wins: a segment lying half on a black cell and half on a
-    /// light gap averages to a perfectly comfortable mid-gray, and the text
-    /// is still unreadable over one of the two halves. Checking only the mean
-    /// is the mistake that makes an adaptive scheme look broken exactly where
-    /// a fixed one already worked.
-    fn backdrop_contrast_demand(&self) -> f32 {
-        if self.text_contrast <= 0.0 {
-            return 0.0;
-        }
-        contrast_demand(self.text_luma, self.backdrop) * self.text_contrast
-    }
-
     fn is_vertical(&self) -> bool {
         // An open in-surface menu makes the surface taller than wide; that
         // must not read as a vertical bar (menus only open on horizontal
@@ -671,7 +498,6 @@ impl StatusApp {
         let padding = read_status_padding_from_config();
         let spacing = read_status_module_spacing_from_config();
         let normal_color = read_normal_color_from_config().unwrap_or(color::TEXT_FG);
-        self.text_luma = relative_luminance(normal_color);
         let sw_logical = if is_vertical { self.height as f32 } else { self.width as f32 };
         let bar_h = if is_vertical { self.width as f32 } else { read_status_height_from_config() };
 
@@ -695,9 +521,6 @@ impl StatusApp {
         self.box_bevel_depth = read_status_box_bevel_depth_from_config();
         self.droplet = read_droplet_from_config();
         self.droplet_boxes.clear();
-        self.text_contrast = read_text_contrast_from_config();
-        self.text_scrim = read_text_scrim_from_config();
-        self.text_scrim_feather = read_text_scrim_feather_from_config();
 
         self.status_bar.set_rect(0.0, 0.0, self.width as f32, self.height as f32);
         // The surface itself is transparent: every StatusApp is a single
@@ -987,8 +810,6 @@ impl StatusApp {
                     Some(font_family.clone()),
                     None,
                     None,
-                    // No scrim: the tooltip already sits on its own box.
-                    None,
                     None,
                 ));
             }
@@ -1112,8 +933,6 @@ impl StatusApp {
                         Some(font_family.clone()),
                         None,
                         None,
-                        // Menu text sits on the expanded box; no scrim.
-                        None,
                         None,
                     ));
 
@@ -1139,7 +958,7 @@ impl StatusApp {
                         } else {
                             if hovered == Some(i) && row.enabled {
                                 // A rounded pill inset from the panel edge,
-                                // drawn post-scrim in display_list — a
+                                // drawn before the text in display_list — a
                                 // square-cornered full-width rect butting
                                 // into the rounded silhouette was the last
                                 // blocky element of the expanded menu.
@@ -1156,7 +975,6 @@ impl StatusApp {
                                 None,
                                 None,
                                 None,
-                                None,
                             ));
                         }
                         bounds.push((off, h));
@@ -1463,14 +1281,9 @@ pub(crate) fn parse_ccectl_windows(output: &str) -> Vec<CcectlWindow> {
 }
 
 /// The frame's text as prim data: (text, size, x, y, color_u8, font, bounds, box-layout).
-/// The last field is the run's MEASURED width in logical px, when the emitter
-/// knew it — `draw_label` always does, since the label was built for its
-/// width. It is what lets the text scrim hug the run instead of the whole
-/// module box; `None` simply gets no scrim, which is right for the menu and
-/// tooltip text that sits on an opaque box already.
-/// The field after that is the run's OpenType weight (`Some(700)` = bold),
-/// `None` for the face's regular; only the icon readouts' numbers set it.
-pub(crate) type TextPrim = (String, f32, f32, f32, [u8; 3], Option<String>, Option<[f32; 4]>, Option<cce_ui::scene::paint::TextLayout>, Option<f32>, Option<u16>);
+/// The last field is the run's OpenType weight (`Some(700)` = bold), `None`
+/// for the face's regular; only the icon readouts' numbers set it.
+pub(crate) type TextPrim = (String, f32, f32, f32, [u8; 3], Option<String>, Option<[f32; 4]>, Option<cce_ui::scene::paint::TextLayout>, Option<u16>);
 
 /// Emit a measured `StyledLabel` as a text-prim tuple, returning its width (like the legacy
 /// `StyledLabel::draw`). The label was built for its width; `into_prim` carries the source
@@ -1478,7 +1291,7 @@ pub(crate) type TextPrim = (String, f32, f32, f32, [u8; 3], Option<String>, Opti
 pub(crate) fn draw_label(prims: &mut Vec<TextPrim>, label: cce_ui::widget::StyledLabel, x: f32, y: f32) -> f32 {
     let w = label.w;
     let p = label.into_prim(x, y);
-    prims.push((p.text, p.size, p.x, p.y, p.color, p.font, None, p.layout, Some(w), None));
+    prims.push((p.text, p.size, p.x, p.y, p.color, p.font, None, p.layout, None));
     w
 }
 
@@ -1530,17 +1343,6 @@ impl cce_ui::engine::Application for StatusApp {
         // Every module can host an in-surface menu, so every process listens
         // for the compositor's click-away dismiss pushes.
         tokio::spawn(spawn_status_listener("dismiss".to_string(), sender.clone()));
-        // ...and every module has text over a backdrop it cannot see, so
-        // every one asks the compositor what it is sitting on. Subscribed
-        // unconditionally rather than behind `module { text_contrast }`: the
-        // knob is re-read live from the config file, and a task spawned once
-        // in `new()` could not follow it being switched on.
-        {
-            let app_id = status_app_id(
-                selected_module.as_ref().map(|(name, side)| (name.as_str(), *side)),
-            );
-            tokio::spawn(spawn_status_listener(format!("backdrop {}", app_id), sender.clone()));
-        }
         let is_primary_for_switcher = selected_module.as_ref().map_or(true, |(name, _)| name == "window");
         if is_primary_for_switcher {
             tokio::spawn(spawn_switcher_listener(sender.clone()));
@@ -1582,12 +1384,6 @@ impl cce_ui::engine::Application for StatusApp {
             rounded_boxes: Vec::new(),
             droplet_boxes: Vec::new(),
             droplet: None,
-            text_contrast: 0.0,
-            backdrop: (50, 100),
-            text_luma: 0.0,
-            text_scrim: 0.0,
-            text_scrim_feather: None,
-            contrast_now: 0.0,
             text_prims: Vec::new(),
             icon_prims: Vec::new(),
             scale_factor: 1.0,
@@ -1733,15 +1529,6 @@ impl cce_ui::engine::Application for StatusApp {
                     changed = false;
                 }
             }
-            CustomEvent::BackdropUpdated(sample) => {
-                if self.backdrop != sample {
-                    self.backdrop = sample;
-                    // Only the paint changes, but something has to ask for a
-                    // frame: `tick` eases toward the new target and nothing
-                    // else on this segment is animating.
-                    self.needs_rebuild = true;
-                }
-            }
             CustomEvent::SwitcherTriggered => {
                 log::debug!("[switcher] SwitcherTriggered event received, calling trigger_switcher");
                 self.trigger_switcher(true);
@@ -1791,27 +1578,8 @@ impl cce_ui::engine::Application for StatusApp {
     }
 
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
-        // Ease toward what the backdrop currently demands. The measurement
-        // itself is quantized and only pushed on change, so this is the only
-        // thing standing between a camera pan and the scrim pulsing on the
-        // cell edges it crosses.
-        if self.text_contrast > 0.0 {
-            const CONTRAST_EASE_S: f32 = 0.12;
-            let target = self.backdrop_contrast_demand();
-            if (target - self.contrast_now).abs() > 0.002 {
-                let step = (dt / CONTRAST_EASE_S).clamp(0.0, 1.0);
-                self.contrast_now += (target - self.contrast_now) * step;
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            } else if self.contrast_now != target {
-                self.contrast_now = target;
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            }
-        }
-
-        // Ease the drawn bubble toward its live-content width — the same
-        // treatment the scrim gets: stepping straight there would snap the
+        // Ease the drawn bubble toward its live-content width: stepping
+        // straight there would snap the
         // bubble edges on every stat update or title change, and a flapping
         // browser title would make the box twitch instead of breathe.
         {
@@ -1944,84 +1712,13 @@ impl cce_ui::engine::Application for StatusApp {
             }
         }
 
-        // The pool, one per module box, filling the bubble rather than
-        // hugging the run inside it, so a segment reads as one darkened
-        // lozenge instead of a pill within a pill.
-        //
-        // Two shapes, because "the bubble" is two different things: a droplet
-        // module gets a pool of the droplet's own silhouette (below), while a
-        // plain rounded box gets `Prim::Glow` — a feathered aura, solid
-        // through its core rect and falling off across `reach`, so insetting
-        // the core by exactly the feather lands the gradient's outer edge on
-        // the box's own edge.
-        if self.text_scrim > 0.0 || self.text_contrast > 0.0 {
-            // Rests at the configured opacity and deepens with the measured
-            // demand. Either knob alone is meaningful: `text_scrim` with no
-            // `text_contrast` is a constant ground (the demand stays zero),
-            // and `text_contrast` with no `text_scrim` is a ground that
-            // appears only when the backdrop earns it.
-            let alpha = scrim_alpha(self.text_scrim, self.contrast_now);
-            let feather_cfg = self.text_scrim_feather;
-            let runs = &self.text_prims;
-            let icons = &self.tray_item_bounds;
-            let pool_in = |pc: &mut cce_ui::scene::paint::PaintCtx, bx: f32, by: f32, bw: f32, bh: f32, radius: f32| {
-                // Colored for the text it is protecting — the widest run
-                // inside this box, since a box with mixed colors is being
-                // led by its longest label. A box holding no measured run
-                // but tray icons is grounded for those (light glyphs, so a
-                // black pool); a box holding neither gets no pool at all.
-                let Some(color) = dominant_run_color(runs, bx, by, bw, bh)
-                    .or_else(|| dominant_icon_color(icons, bx, by, bw, bh))
-                else {
-                    return;
-                };
-                let feather = scrim_feather(bw, bh, feather_cfg);
-                let core = Rect {
-                    x: bx + feather,
-                    y: by + feather,
-                    width: (bw - feather * 2.0).max(0.0),
-                    height: (bh - feather * 2.0).max(0.0),
-                };
-                if core.width <= 0.0 || core.height <= 0.0 {
-                    return;
-                }
-                let c = treatment_rgb(color);
-                let box_rect = Rect { x: bx, y: by, width: bw, height: bh };
-                pc.clip_rounded(box_rect, radius, |pc| {
-                    pc.glow(core, (radius - feather).max(0.0), feather, [c[0], c[1], c[2], alpha]);
-                });
-            };
-            // A droplet bubble gets a pool of its OWN silhouette, not a
-            // rounded-rect stand-in: cce-ui's `Prim::DropletScrim` runs the
-            // droplet's shader path with the same spec, filled flat and
-            // feathered inward, so the vignette's edge is the drop's edge by
-            // construction rather than by approximation.
-            for &(x, y, w, h, _, spec) in &self.droplet_boxes {
-                let rect = Rect { x, y, width: w, height: h };
-                let Some(color) = dominant_run_color(runs, x, y, w, h)
-                    .or_else(|| dominant_icon_color(icons, x, y, w, h))
-                else {
-                    continue;
-                };
-                let c = treatment_rgb(color);
-                let feather = scrim_feather(w, h, feather_cfg);
-                pc.droplet_scrim(rect, &cce_ui::scene::Material::from_fill([c[0], c[1], c[2], alpha]), spec, feather);
-            }
-            // Everything else is genuinely a rounded rect, so a rounded-rect
-            // pool IS its exact shape.
-            for rb in &self.rounded_boxes {
-                pool_in(&mut pc, rb.x, rb.y, rb.w, rb.h, rb.radius);
-            }
-        }
-
         let (sb_x, sb_y, sb_w, sb_h) = self.status_bar.rect();
         pc.quad(Rect { x: sb_x, y: sb_y, width: sb_w, height: sb_h }, self.status_bar.color());
         for r in &self.rects {
             pc.quad(Rect { x: r.x, y: r.y, width: r.w, height: r.h }, r.color);
         }
 
-        // The hovered menu row's pill: post-scrim so the pool cannot darken
-        // it, pre-text so the label sits on it.
+        // The hovered menu row's pill, pre-text so the label sits on it.
         if let Some((hx, hy, hw, hh)) = self.menu_hover_rect {
             pc.rounded_rect(
                 Rect { x: hx, y: hy, width: hw, height: hh },
@@ -2031,19 +1728,18 @@ impl cce_ui::engine::Application for StatusApp {
             );
         }
 
-        // Readout glyphs: post-scrim so the pool grounds them like any run.
+        // Readout glyphs, under the numbers beside them.
         for ip in &self.icon_prims {
             pc.image(ip.image, Rect { x: ip.x, y: ip.y, width: ip.w, height: ip.h }, ip.alpha);
         }
 
-        for (text, tsize, x, y, color, font, bounds, layout, _run_w, weight) in &self.text_prims {
+        for (text, tsize, x, y, color, font, bounds, layout, weight) in &self.text_prims {
             let attrs = cce_ui::scene::paint::TextAttrs { italic: false, weight: *weight };
             match layout {
                 Some(l) => pc.text_boxed(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds, attrs, *l),
-                // Glyphs are drawn plain. Contrast is the scrim's job now —
-                // it darkens the ground rather than decorating the
-                // letterforms, and the two together were always one treatment
-                // too many.
+                // Glyphs are drawn plain: contrast is the compositor's job,
+                // which compresses the backdrop behind the segment
+                // (`module { backdrop_compress }`).
                 None => pc.text_attrs(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds, attrs),
             }
         }
@@ -2602,92 +2298,6 @@ mod tests {
         assert_eq!(tray_click_point(None, 40, 12), (40, 12));
     }
 
-    // ------------------------------------------------------------------
-    // Adaptive contrast: the bar cannot see its own backdrop, so these pin
-    // down what it does with the compositor's measurement of it.
-    // ------------------------------------------------------------------
-
-    /// Luminance of the black text the droplet style is configured with.
-    const BLACK_TEXT: f32 = 0.0;
-    const WHITE_TEXT: f32 = 1.0;
-
-    #[test]
-    fn dark_text_on_a_light_uniform_backdrop_wants_no_scrim() {
-        // The case that must stay untouched: the bar already reads fine, so
-        // an adaptive scheme that decorates it anyway is worse than nothing.
-        assert_eq!(contrast_demand(BLACK_TEXT, (100, 0)), 0.0);
-    }
-
-    #[test]
-    fn dark_text_on_a_dark_uniform_backdrop_wants_a_full_scrim() {
-        // Black text over a black grid cell — invisible, and the whole
-        // reason for the feature.
-        assert_eq!(contrast_demand(BLACK_TEXT, (0, 0)), 1.0);
-    }
-
-    #[test]
-    fn light_text_reverses_the_verdict() {
-        // The decision is about the CONFIGURED color, not a hardcoded
-        // assumption that module text is dark.
-        assert_eq!(contrast_demand(WHITE_TEXT, (0, 0)), 0.0);
-        assert_eq!(contrast_demand(WHITE_TEXT, (100, 0)), 1.0);
-    }
-
-    #[test]
-    fn a_comfortable_mean_over_a_split_backdrop_still_wants_a_scrim() {
-        // Half black cell, half light gap: the mean alone says "mid-gray,
-        // fine" while the text is invisible over one half. Checking the
-        // spread's ends is what catches it.
-        let mean_only = contrast_deficit(BLACK_TEXT, 0.5);
-        let with_spread = contrast_demand(BLACK_TEXT, (50, 100));
-        assert!(with_spread > mean_only, "{} !> {}", with_spread, mean_only);
-        assert_eq!(with_spread, 1.0);
-    }
-
-    #[test]
-    fn an_unknown_backdrop_is_treated_as_the_worst_case() {
-        // What a segment reports before it has heard from the compositor,
-        // and what an occluding window resolves to: assume unreadable.
-        assert_eq!(contrast_demand(BLACK_TEXT, (50, 100)), 1.0);
-    }
-
-    const BLACK: [f32; 3] = [0.0, 0.0, 0.0];
-    const WHITE: [f32; 3] = [1.0, 1.0, 1.0];
-
-    #[test]
-    fn a_treatment_contrasts_with_the_text_not_with_a_fixed_assumption() {
-        // A white outline around white text is not a weaker treatment, it is
-        // an eraser — which is exactly what a light backdrop got before this
-        // followed the text color. The same holds for a black pool behind
-        // black text.
-        assert_eq!(treatment_rgb([255, 255, 255]), BLACK);
-        assert_eq!(treatment_rgb([0, 0, 0]), WHITE);
-    }
-
-    #[test]
-    fn the_treatment_is_chosen_per_run_so_an_odd_colored_module_is_safe() {
-        // The volume module paints its muted state in the shared
-        // disabled_color while every other run is the configured white, and
-        // the two need not land on the same answer. They happen to today —
-        // disabled_color is a light red, picked so the muted run keeps the
-        // same dark pool as its neighbors — so the endpoints below stand in
-        // for a palette that could part them again.
-        assert_eq!(treatment_rgb([255, 255, 255]), BLACK);
-        assert_eq!(treatment_rgb([0, 0, 0]), WHITE);
-        // A mid accent color still resolves rather than landing in between.
-        assert!(matches!(treatment_rgb([125, 222, 143]), BLACK | WHITE));
-    }
-
-    #[test]
-    fn the_treatment_crossover_follows_the_wcag_curve_not_the_midpoint() {
-        // Mid-gray (sRGB 128) is luminance ~0.22, which reads better against
-        // black than white — so the crossover sits well below the halfway
-        // byte, and picking it by midpoint would give a swathe of grays the
-        // wrong treatment.
-        assert_eq!(treatment_rgb([128, 128, 128]), BLACK);
-        assert_eq!(treatment_rgb([80, 80, 80]), WHITE);
-    }
-
     // ------------------------------------------------------------------
     // The expanded menu's drop: the module box grows downward, and the
     // silhouette must not grow with it.
@@ -2775,118 +2385,6 @@ mod tests {
         assert_eq!(doubled.dome, 0.0);
     }
 
-    #[test]
-    fn the_scrim_is_constant_without_the_adaptive_knob() {
-        // text_contrast off leaves `demand` at zero, and the scrim is then
-        // exactly what was configured — a fixed dark ground, which is the
-        // whole reason it can stand alone as a treatment.
-        assert_eq!(scrim_alpha(0.55, 0.0), 0.55);
-        assert_eq!(scrim_alpha(0.0, 0.0), 0.0);
-    }
-
-    #[test]
-    fn the_scrim_deepens_with_demand_and_never_thins() {
-        // Adaptive contrast can only ever darken the ground further; a
-        // backdrop that needs help must not be able to lighten it.
-        assert!(scrim_alpha(0.55, 0.5) > 0.55);
-        assert_eq!(scrim_alpha(0.55, 1.0), 1.0);
-        assert!(scrim_alpha(0.55, 1.0) >= scrim_alpha(0.55, 0.0));
-    }
-
-    #[test]
-    fn the_feather_defaults_to_a_quarter_of_the_bubble_height() {
-        assert_eq!(scrim_feather(200.0, 28.0, None), 7.0);
-        assert_eq!(scrim_feather(200.0, 28.0, Some(9.0)), 9.0);
-        assert_eq!(scrim_feather(200.0, 28.0, Some(-3.0)), 0.0);
-    }
-
-    #[test]
-    fn the_feather_cannot_swallow_the_core_it_surrounds() {
-        // Drawn OUTSIDE the core, so the core is inset by it; a feather past
-        // half of either axis would invert the core and the pool would
-        // disappear — exactly where a narrow module (a lone icon) lands.
-        assert_eq!(scrim_feather(10.0, 28.0, Some(40.0)), 5.0);
-        assert_eq!(scrim_feather(200.0, 28.0, Some(40.0)), 14.0);
-    }
-
-    fn run(x: f32, y: f32, w: f32, color: [u8; 3]) -> TextPrim {
-        ("x".to_string(), 14.0, x, y, color, None, None, None, Some(w), None)
-    }
-
-    #[test]
-    fn the_pool_takes_its_color_from_the_widest_run_it_covers() {
-        let runs = vec![run(20.0, 7.0, 30.0, [255, 255, 255]), run(60.0, 7.0, 90.0, [0, 0, 0])];
-        assert_eq!(dominant_run_color(&runs, 10.0, 0.0, 200.0, 27.0), Some([0, 0, 0]));
-    }
-
-    #[test]
-    fn a_run_in_another_box_does_not_color_this_pool() {
-        // An expanded segment has text in the strip AND in the menu below it;
-        // the strip's pool must not be colored by a menu row.
-        let runs = vec![run(20.0, 7.0, 30.0, [255, 255, 255]), run(20.0, 60.0, 90.0, [0, 0, 0])];
-        assert_eq!(dominant_run_color(&runs, 10.0, 0.0, 200.0, 27.0), Some([255, 255, 255]));
-    }
-
-    #[test]
-    fn a_box_with_no_measured_text_gets_no_pool() {
-        // The tray is icons; there is no text to ground, and a pool there
-        // would just be a smudge behind the icons.
-        let runs: Vec<TextPrim> = vec![("i".to_string(), 14.0, 20.0, 7.0, [255, 255, 255], None, None, None, None, None)];
-        assert_eq!(dominant_run_color(&runs, 10.0, 0.0, 200.0, 27.0), None);
-        assert_eq!(dominant_run_color(&[], 10.0, 0.0, 200.0, 27.0), None);
-    }
-
-    #[test]
-    fn a_box_of_tray_icons_gets_the_pool_a_white_run_would() {
-        // The tray draws icons, not measured runs, so the text-keyed lookup
-        // finds nothing; the icon fallback grounds the box as light glyphs.
-        let icon = |x: f32| TrayIconBounds {
-            id: "i".into(), x, y: 5.5, w: 16.0, h: 16.0, title: None, dbus_id: None,
-        };
-        let icons = [icon(18.0), icon(42.0)];
-        assert_eq!(dominant_icon_color(&icons, 10.0, 0.0, 80.0, 27.0), Some([255, 255, 255]));
-        assert_eq!(treatment_rgb([255, 255, 255]), [0.0, 0.0, 0.0]);
-        // An icon whose center lies outside the box does not ground it —
-        // the expanded menu box below the strip holds no icons.
-        assert_eq!(dominant_icon_color(&icons, 10.0, 27.0, 80.0, 100.0), None);
-        assert_eq!(dominant_icon_color(&[], 10.0, 0.0, 80.0, 27.0), None);
-    }
-
-    #[test]
-    fn contrast_ratio_matches_the_wcag_endpoints() {
-        assert!((contrast_ratio(0.0, 1.0) - 21.0).abs() < 0.01);
-        assert!((contrast_ratio(0.5, 0.5) - 1.0).abs() < 0.001);
-    }
-
-    #[test]
-    fn parse_backdrop_reads_a_well_formed_line() {
-        assert_eq!(crate::listeners::parse_backdrop("42 17"), (42, 17));
-        assert_eq!(crate::listeners::parse_backdrop("  0 0  "), (0, 0));
-    }
-
-    #[test]
-    fn parse_backdrop_falls_back_to_the_worst_case_not_the_best() {
-        // Every unreadable form must fail toward "assume unreadable": a
-        // fallback of (bright, uniform) would silently switch the treatment
-        // off, and bare text over an unknown backdrop is the failure this
-        // whole path exists to prevent.
-        for line in ["unknown", "", "42", "nonsense here", "42 spread"] {
-            assert_eq!(crate::listeners::parse_backdrop(line), (50, 100), "line {:?}", line);
-        }
-        assert_eq!(contrast_demand(BLACK_TEXT, crate::listeners::parse_backdrop("unknown")), 1.0);
-    }
-
-    #[test]
-    fn parse_backdrop_rejects_out_of_range_but_tolerates_extra_fields() {
-        // Out of protocol is unknown, not clamped — clamping a bad luma to
-        // 100 would read as "bright and uniform" and switch the scrim off.
-        assert_eq!(crate::listeners::parse_backdrop("200 200"), (50, 100));
-        assert_eq!(crate::listeners::parse_backdrop("101 0"), (50, 100));
-        // Room for the compositor to grow the line without the bar
-        // misreading it as garbage.
-        assert_eq!(crate::listeners::parse_backdrop("30 40 future"), (30, 40));
-    }
-
     // ------------------------------------------------------------------
     // Characterization tests (phase 0): these pin down current behavior
     // before the refactors in PROPOSAL.md. Where the behavior is odd, the
diff --git a/src/modules.rs b/src/modules.rs
index b9995c6..0c0d6fd 100644
--- a/src/modules.rs
+++ b/src/modules.rs
@@ -393,7 +393,6 @@ impl IconReadout {
                         Some(font_family.to_string()),
                         None,
                         None,
-                        Some(nw),
                         crate::read_icon_weight_from_config(),
                     ));
                     w = nx + nw - x;
@@ -912,7 +911,6 @@ impl StatusModule for TrayModule {
                     Some(font_family.to_string()),
                     None,
                     None,
-                    Some(tw),
                     None,
                 ));
             }