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

docs/rfc-core-rebuild.md (197.9K)

   1 # RFC: cce-ui core rebuild
   2 
   3 **Status:** Draft / proposal
   4 **Scope:** The core of `cce-ui` — tree ownership, layout, paint, clipping, and animation.
   5 **Appetite:** Breaking changes are acceptable. Migration is incremental, behind a stable
   6 `Application` trait, one widget/app at a time. Every crate must continue to build standalone.
   7 
   8 ---
   9 
  10 ## 1. Why
  11 
  12 `cce-ui` works, but it is not one system — it is three half-systems overlaid, with nothing
  13 arbitrating between them. Concretely, from an audit of the current code:
  14 
  15 - **Tree ownership is tripled.** A child lives in a container's own
  16   `Vec<*mut dyn Element>`, in `ctx.layout_tree.children`, *and* in `ctx.widget_registry`,
  17   kept in sync by hand in every `add_child`/`clear_children`. All three are raw
  18   `*mut dyn Element`. `Drop` (`core.rs` `clear_widget_references`) clears focus/context-menu
  19   refs but **not** the registry or parent/child maps, so stale pointers can linger.
  20 - **Rendering has three paths with no single owner:** the app's top-level `widgets: Vec<Box<dyn Element>>`
  21   iteration, parent→child `all_rounded_quads` recursion, and immediate-mode
  22   `render_widget`/`SectionContext`. Nothing enforces that a widget is drawn by exactly one.
  23   *(This is the direct cause of the breadcrumb "black rectangle": the breadcrumb was sized
  24   full-width by its container and drawn a second time under the dropdown.)*
  25 - **Layout is smeared across five mechanisms:** `LayoutStrategy::allocate` (a child-driven
  26   bump cursor), `Container::layout`, immediate-mode builders that literally *render twice to
  27   measure*, `Backplate` (which clips children but does not lay them out), and hand-written
  28   `set_rect` with absolute screen coordinates in page code. There is no measure→arrange pass
  29   and no owner of any given rect.
  30 - **No clip/transform abstraction.** Rect clipping is hand-copied rect-intersection in every
  31   container's `all_quads`/`all_rounded_quads`/`text_*`. The GPU has no `set_scissor_rect`;
  32   the only GPU clip is a per-fragment circular test in `shader.wgsl`.
  33 - **Animation barely exists.** One real helper (`hover_animation`) — copy-pasted into a
  34   second, *dead* implementation in `UiContext` (`tick_hover`/`get_hover_quad`, zero callers).
  35   Everything else (button hover/press, `network_opacity`) is an **instant boolean flip or a
  36   static multiplier, not interpolated**. No `Animated<T>`, tween, spring, or easing library.
  37   "Keep animating" is a bool hand-propagated up the `tick` chain — miss one link and the
  38   animation silently freezes.
  39 
  40 Every fragility we have hit is a symptom of this. The `Element` trait has grown to ~90
  41 methods spanning layout, paint, hit-testing, clipboard, tree expand/collapse, and ~12
  42 `as_*_controller` downcast escape hatches — a god-trait that makes each of the above worse.
  43 
  44 ### What is already good (keep it)
  45 
  46 - **The frame loop is sound.** Demand-driven redraw with a single `redraw` dirty bool, gated
  47   by a Wayland frame-callback vsync (`frame_callback_pending`). It idles correctly when
  48   nothing changes. `tick(dt)` plumbing (clamped `dt`, ~60 Hz dispatch) already exists.
  49 - **Immediate-per-frame tessellation from a retained tree** is a reasonable bones: widgets are
  50   long-lived, geometry is re-emitted each frame into one shared vertex buffer and one shader
  51   pipeline. We are not throwing this out.
  52 - **The primitive tessellators** (rounded rects with per-corner radii, vectors with caps,
  53   arcs, circles, relief primitives) are solid and reusable as-is.
  54 
  55 ---
  56 
  57 ## 2. Goals / non-goals
  58 
  59 **Goals**
  60 1. **Solid** — one source of truth for the widget tree; no raw pointers; no manual multi-store
  61    sync; no dangling-pointer class.
  62 2. **Flexible** — one real two-phase layout pass (measure → arrange) *separate from paint*,
  63    with genuine flex/grow, so layout can be recomputed without re-running paint.
  64 3. **Dynamic** — a first-class animation primitive; hover/press/opacity/slide/scale/collapse
  65    as interpolated values; the loop automatically keeps frames coming while anything is live.
  66 4. **Fast** — GPU scissor clipping, no per-frame debug I/O, and a path to per-subtree geometry
  67    caching later.
  68 
  69 **Non-goals (for this RFC)**
  70 - Changing the Wayland/wgpu/glyphon backend, the `calloop` loop, or the frame-callback vsync.
  71 - Changing the KDL config system or IPC.
  72 - A big-bang rewrite. This lands incrementally behind the existing `Application` trait.
  73 - Per-subtree tessellation caching — designed-for, but deferred (see §9).
  74 
  75 ---
  76 
  77 ## 3. Target architecture
  78 
  79 A retained scene graph with a clean separation of concerns, borrowing the proven
  80 Flutter/GPUI/Taffy split of *tree · layout · paint*:
  81 
  82 ```
  83             ┌─────────────────────────────────────────────┐
  84    update → │  Arena (owns all nodes, keyed by NodeId)     │
  85             │    Node { parent, children, widget, style,   │
  86             │           layout_out, anim_state, dirty }    │
  87             └───────────────┬─────────────────────────────┘
  88                             │
  89       ┌─────────────────────┼──────────────────────┬───────────────┐
  90       ▼                     ▼                      ▼               ▼
  91   Layout pass          Paint pass             Input pass       Anim tick
  92  (measure→arrange)   (emit DisplayList)     (hit-test by      (advance
  93   → LayoutOut rects   under clip/xform       layout rect+z)    Animated<T>)
  94       │                     │
  95       │                     ▼
  96       │            DisplayList → existing tessellators → one vertex buffer
  97       └── taffy (recommended) or hand-rolled solver
  98 ```
  99 
 100 ### 3.1 Node arena — one source of truth
 101 
 102 Replace `Vec<*mut dyn Element>` + `layout_tree` + `widget_registry` with a single arena
 103 (a `slotmap`/generational-index store). No raw pointers cross frames; code passes
 104 `&Arena` / `&mut Arena` + `NodeId`.
 105 
 106 ```rust
 107 pub struct NodeId(/* slotmap key: generational index */);
 108 
 109 pub struct Node {
 110     pub parent: Option<NodeId>,
 111     pub children: Vec<NodeId>,
 112     pub widget: Box<dyn Widget>,   // the payload (see §3.5)
 113     pub style: Style,              // layout inputs (flex/size/padding/…)
 114     pub layout_out: LayoutOut,     // computed rect+transform (written by layout pass)
 115     pub anim: AnimSet,             // this node's live Animated<T> values
 116     pub dirty: Dirty,              // LAYOUT | PAINT bitflags
 117 }
 118 
 119 pub struct Arena { nodes: SlotMap<NodeId, Node>, root: NodeId, /* free lists, dirty set */ }
 120 ```
 121 
 122 Why an arena and not `Rc<RefCell<>>` or keeping raw pointers:
 123 - Generational keys make use-after-free a `None` lookup, not UB. The entire dangling-pointer
 124   class disappears.
 125 - One store means no hand-sync of three collections; `add_child`/`remove` touch one place.
 126 - It sidesteps the borrow-checker tree problem: recursion passes `NodeId` and re-borrows the
 127   arena, and layout operates on `style`/`layout_out` (not the `widget` payload) so it never
 128   needs `&mut` to two nodes at once. `SlotMap::get_disjoint_mut` covers the rare cases that do.
 129 
 130 Widget **identity** becomes `NodeId` uniformly. Today identity is split: parent/child links
 131 key on `WidgetId` while `hit_test`/focus/`highlight_color` key on the raw `self` address cast
 132 to `usize`. Unify on `NodeId`.
 133 
 134 ### 3.2 Layout pass — measure then arrange, separate from paint
 135 
 136 Introduce a real two-phase pass that runs *before* paint and writes `LayoutOut` per node:
 137 
 138 - **measure(constraints) → Size** bubbles intrinsic sizes child→parent. Leaves (text, icons)
 139   measure their content — text via a glyphon measurement hook so wrapping is correct.
 140 - **arrange(final_rect)** flows final positions parent→child, writing absolute (or
 141   transform-local, see §3.4) rects into `layout_out`.
 142 
 143 **Decision: hand-roll the solver** (taffy was considered and declined). We own a compact
 144 measure/arrange engine inside `cce-ui`: `measure(constraints) -> Size` bubbles intrinsic sizes
 145 child→parent (leaves measure content — text via a glyphon hook), `arrange(final_rect)` flows
 146 final positions parent→child. Start with the layout primitives cce-ui actually needs —
 147 row/column with gap + padding, flex grow/shrink, main/cross alignment, and fixed/intrinsic
 148 sizing — rather than a full CSS flexbox/grid clone. This deletes `LayoutStrategy`, the
 149 `allocate` bump-cursor, and the render-twice-to-measure `SectionContext` pattern outright.
 150 
 151 Rationale for hand-rolling over a dependency: full control over the exact box model (no
 152 impedance-matching a general CSS engine to our primitives), no external version churn, and a
 153 solver scoped to what the DE uses. The cost is that we implement and test grow/shrink/wrap
 154 ourselves — acceptable given the constrained widget set.
 155 
 156 The key property either way: **layout is computed independently of paint.** That is what makes
 157 the breadcrumb bug structurally impossible (one owner writes each rect) and what makes animated
 158 layout cheap (re-arrange without re-emitting paint).
 159 
 160 ### 3.3 Paint pass — one path, a display list
 161 
 162 Collapse the three render paths into one. Paint walks the arena in z-order and each node emits
 163 primitives into a single `DisplayList`, given its computed `layout_out` and a `PaintCtx`:
 164 
 165 ```rust
 166 pub enum Prim {
 167     RoundedRect { rect: Rect, radii: CornerRadii, color: Rgba, corners: Corners },
 168     Vector { a: Vec2, b: Vec2, thickness: f32, color: Rgba, cap: LineCap },
 169     Circle { center: Vec2, r: f32, color: Rgba },
 170     Arc { .. }, Text { .. },
 171 }
 172 pub struct DisplayList { prims: Vec<(ZIndex, Prim)>, /* under active clip/xform */ }
 173 ```
 174 
 175 The `DisplayList` is then fed to the **existing tessellators** (`push_rounded_rect_vertices_corners`,
 176 `vector_vertices`, `circle_vertices`, glyphon text) → the existing single vertex buffer + shader.
 177 The backend `render()` stays; only its *input* changes from "call `view`/`view_rounded_quads`/
 178 `overlay_quads` on the app" to "walk the arena into one display list." The top-level `widgets`
 179 Vec iteration and the `render_widget`/`SectionContext` immediate path both go away.
 180 
 181 ### 3.4 Clip + transform stack
 182 
 183 `PaintCtx` carries a clip stack and a transform (translate+scale is enough for v1; a full 2×3
 184 affine is a small extension):
 185 
 186 ```rust
 187 impl PaintCtx {
 188     fn with_clip(&mut self, rect: Rect, f: impl FnOnce(&mut PaintCtx));
 189     fn with_transform(&mut self, xform: Affine2, f: impl FnOnce(&mut PaintCtx));
 190 }
 191 ```
 192 
 193 - Rect clips lower to GPU **`set_scissor_rect`** (a real render-pass feature we currently do
 194   not use), deleting the hand-copied rect-intersection in every container.
 195 - Keep the shader's circular clip for the cases that need it.
 196 - The transform gives **slide / scale / collapse transitions for free** and lets children use
 197   local coordinates instead of everyone storing absolute screen coords.
 198 
 199 ### 3.5 Trait split — kill the god-trait
 200 
 201 Replace the ~90-method `Element` with narrow traits, each a single concern:
 202 
 203 ```rust
 204 /// The payload. Most widgets implement only this + Paint.
 205 pub trait Widget: 'static {
 206     fn style(&self) -> Style { Style::default() }        // layout inputs
 207     fn measure(&self, c: Constraints, ctx: &MeasureCtx) -> Option<Size> { None } // leaves only
 208 }
 209 pub trait Paint  { fn paint(&self, layout: &LayoutOut, ctx: &mut PaintCtx); }
 210 pub trait Input  {
 211     fn hit(&self, layout: &LayoutOut, p: Vec2) -> bool { layout.rect.contains(p) } // default!
 212     fn on_event(&mut self, ev: &Event, ctx: &mut EventCtx) -> EventStatus { EventStatus::Ignored }
 213 }
 214 ```
 215 
 216 - Hit-testing gets a correct default from `layout_out.rect` + z-order, so the manual
 217   `hit_test`-by-`self`-address code across widgets disappears.
 218 - The 12 `as_*_controller` downcasts are replaced by typed messages / commands through
 219   `EventCtx` (an app-defined message channel), not runtime `Any` casts.
 220 - `Container` stops being special: a container is just a `Widget` with children and a flex
 221   `Style`; it no longer hand-clips or hand-lays-out.
 222 
 223 ### 3.6 Animation — first-class
 224 
 225 ```rust
 226 pub struct Animated<T> { current: T, target: T, motion: Motion } // Motion: Tween(easing,dur) | Spring(k,damp)
 227 impl<T: Lerp> Animated<T> {
 228     fn set_target(&mut self, t: T);
 229     fn tick(&mut self, dt: f32) -> bool;  // returns true while still moving
 230     fn value(&self) -> T;
 231 }
 232 ```
 233 
 234 - Hover/press/focus become `Animated<f32>` 0→1 factors, not bools. Buttons interpolate color
 235   by `lerp(idle, hover, factor)` instead of `if hovered { a } else { b }`.
 236 - `network_opacity` becomes an `Animated<f32>` that actually tweens.
 237 - **The loop keeps frames coming automatically:** the arena tracks "any node has a live
 238   animation." `tick` OR-reduces over the arena's animated nodes and sets `redraw`, so the
 239   hand-propagated `tick`-bool chain (and its silent-freeze failure mode) is gone.
 240 - Fold `hover_animation` (and delete its dead `UiContext` twin) into this; the global
 241   thread-local singleton — which today allows only one animated highlight at a time — becomes
 242   per-node state, so multiple highlights animate independently.
 243 
 244 ---
 245 
 246 ## 4. How this kills the classes of bug we have
 247 
 248 | Bug class | Fixed by |
 249 |---|---|
 250 | Dangling / stale `*mut dyn Element` | §3.1 arena, generational keys, single store |
 251 | Tree desync (Vec vs layout_tree vs registry) | §3.1 one store |
 252 | Same widget drawn twice at different rects (breadcrumb) | §3.2 single layout owner + §3.3 single paint path |
 253 | Overflow bleeding past clip regions | §3.4 GPU scissor + clip stack |
 254 | Animation silently freezes (missed tick-bool link) | §3.6 arena-driven auto frame requests |
 255 | One-highlight-at-a-time hover | §3.6 per-node animation state |
 256 | Per-frame debug I/O in hot path | §7 quick win |
 257 
 258 ---
 259 
 260 ## 5. Impact on the `Application` trait & clients
 261 
 262 The client-facing `Application` trait shape stays as stable as possible so apps migrate one at
 263 a time. Two viable migration shapes:
 264 
 265 - **Adapter (recommended):** the old `Element` widgets keep working via a compatibility shim
 266   that wraps each in a `Node` and forwards `all_rounded_quads`/`text_items` into a `DisplayList`.
 267   New/migrated widgets implement the narrow traits directly. Both coexist until the last old
 268   widget is gone.
 269 - The `view`/`view_rounded_quads`/`overlay_quads` methods become thin shims over the arena walk
 270   during the transition, then are removed.
 271 
 272 Constraint respected: **each crate still builds standalone** — the new core is entirely inside
 273 `cce-ui`; clients depend on it by path exactly as today. No `[workspace.dependencies]`.
 274 
 275 ---
 276 
 277 ## 6. Migration plan (staged; every stage leaves the tree building)
 278 
 279 - **Phase 0 — Quick wins (independent of the rebuild).** Fix breadcrumb double-render; remove
 280   the per-frame `eprintln!` text reconstruction in `render()`; add `set_scissor_rect` for the
 281   existing clip call sites; delete the dead `tick_hover`/`get_hover_quad` twin. *Ships value now.*
 282 - **Phase 1 — Arena + identity.** Introduce `Arena`, `NodeId`, `Node`; port `add_child`/tree
 283   ops onto it behind the adapter; unify identity on `NodeId`. No visual change.
 284   - **1a — Arena data structure: DONE.** `cce-ui/src/scene/arena.rs` — a generational forest
 285     arena, generic over payload, added additively (`pub mod scene;`) with nothing wired into the
 286     live path yet. `NodeId` carries a `NonZeroU32` generation so a handle to a removed node reads
 287     back as `None` (use-after-free → missed lookup, not UB). Tree ops (`insert`, `append_child`,
 288     `detach`, `remove_subtree`, `get_pair_mut`, `subtree`/`ancestors` iterators, cycle rejection)
 289     with 14 headless unit tests, including one that owns and trees real `dyn Element` payloads.
 290     Full `cce-ui` suite: 83 passing.
 291   - **1b — Adapter: DONE.** `cce-ui/src/scene/tree.rs` — `WidgetTree`, the arena-backed
 292     replacement for `UiContext`'s two stores (`widget_registry` + `layout_tree`), keyed by a
 293     `WidgetId → NodeId` index so the public `WidgetId` API is preserved. Consolidates both maps
 294     into one generational store; link-only-before-register is modeled as `Entry.ptr: None`. 10
 295     headless tests against real `dyn Element` payloads (register/overwrite, symmetric+deduped
 296     link, reparent, link-before-register, symmetric detach, clear_children, clear_all, removal
 297     staleness, registered-iteration). Not yet wired into `UiContext`.
 298   - **1b — Live swap: DONE (compile + tests; runtime verification pending).**
 299     `UiContext.{widget_registry, layout_tree}` are replaced by a single `tree: WidgetTree`. All
 300     access routed through it: the 5 public methods, the internal direct field reads in
 301     `context.rs` (event routing, `clear_dirty`, `rebuild_spatial_grid`, `is_widget_visible`,
 302     `tick`, `is_coordinate_covered`, `is_movable_backplate_at`, `is_widget_at`), the widget-layer
 303     defaults in `widget/mod.rs`, and the direct pokes in `keybinds_control.rs`/`multi_control.rs`
 304     (now `link_ids`) and `plate.rs`/`parameters_bg.rs` (now `tree.set_parent(id, None)`). `cce-ui`
 305     builds clean; **93 tests pass**; all 7 app crates that use the API build unchanged. Still to
 306     do: **`make run`** cce-files + cce-designer to confirm the symmetric-tree change (below) is
 307     behavior-safe in paint/event handling.
 308     - Original swap notes below retained for reference.
 309 
 310     Replace `UiContext.{widget_registry, layout_tree}`
 311     with a `WidgetTree`, and route through it: the 5 public methods apps depend on
 312     (`register_widget`, `link_ids`, `clear_hierarchy`, plus internally `clear_children_ids`,
 313     `unlink_child`), the internal direct field reads in `context.rs` (event routing, `mark_dirty`
 314     walk, `is_widget_visible`, `clear_dirty`, `rebuild_spatial_grid`, coverage/hit tests), and the
 315     widget-layer defaults in `widget/mod.rs` (`parent`/`set_parent`/`children`/`add_child`/
 316     `mark_dirty`) plus the direct pokes in `keybinds_control.rs`, `multi_control.rs`, `plate.rs`,
 317     `parameters_bg.rs`.
 318     - **One deliberate behavior change to verify at runtime:** the legacy maps are left
 319       *asymmetric* in a few spots (`set_parent(Some)` writes only `parents`; `plate`/`parameters_bg`
 320       detach via `parents.remove` only). `WidgetTree` keeps parent/child links symmetric, so
 321       `children()` — read by paint recursion and event propagation — becomes self-consistent. This
 322       is almost certainly a latent-bug fix, but it must be confirmed against the running apps
 323       (cce-files, cce-designer, cce-graph, cce-system-settings) before landing.
 324     - App compatibility: only `register_widget`/`link_ids`/`clear_hierarchy` have app callers;
 325       `get_widget`/`get_widget_mut`/`unlink_child` have **zero callers** workspace-wide and can be
 326       dropped or kept as thin shims.
 327 - **Phase 2 — Layout pass.** Add `Style` + measure/arrange (taffy). Migrate containers to
 328   emit `Style` instead of `LayoutStrategy`; delete `allocate` and render-twice measurement as
 329   containers move over.
 330 - **Phase 3 — Paint unification.** Introduce `DisplayList` + `PaintCtx` (clip/transform);
 331   route the backend `render()` through the arena walk; retire the top-level `widgets` Vec and
 332   `render_widget`/`SectionContext`.
 333 - **Phase 4 — Animation.** Land `Animated<T>` + arena-driven frame requests; convert
 334   hover/press/`network_opacity`; consolidate `hover_animation`.
 335 - **Phase 5 — Trait split & cleanup.** Split `Element` into `Widget`/`Paint`/`Input`; remove
 336   `as_*_controller` downcasts; migrate remaining widgets; delete the compatibility shim.
 337   - **Approach correction (resolved by experiment).** A *non-breaking supertrait carve-out* of
 338     `Element` (`trait Element: Paint + …`) turns out to be impossible in Rust here. The structural
 339     methods the passes need (`rect`, `children`, `set_rect`) are overridden in dozens of widgets
 340     across cce-ui **and** the app crates (`rect` 31+8, `children` 23+2, `set_rect` 42+4): moving
 341     them off `Element` breaks every override, and merely *declaring* them on a supertrait makes
 342     every `elem.children()`/`elem.rect()` call site ambiguous (a supertrait method is always in
 343     scope on the subtrait). Nor does a blanket "view" `impl<T: Element> Paint for T` let
 344     `&dyn Element` coerce to `&dyn Paint` — that coercion exists only for real supertraits. So the
 345     split follows the **adapter** path from §5, not a supertrait split: narrow traits independent
 346     of `Element`, with `Adapted<W>` bridging a narrow-trait widget into the `*mut dyn Element`
 347     tree. The compatibility-shim bullet is thus *this* adapter (there was never a discrete legacy
 348     shim to delete — the earlier migration hung hooks directly on `Element`).
 349   - **5a — Layout + Paint concerns + adapter: DONE (compile + tests; no runtime surface yet).**
 350     `cce-ui/src/widget/model.rs` — the independent `Layout` (`layout_style` / `intrinsic_size` /
 351     `layout_children`) and `Paint` (`color` / `paint` / `clips_children`, where `paint` takes the
 352     laid-out rect rather than reading a stored one) traits, plus `Adapted<W>`: a wrapper that
 353     carries the `Widget` base and forwards the `Element` layout/paint methods to `W`'s narrow
 354     traits. A headless test builds a pure narrow-trait tree (a `Col` container + two `Dot` leaves,
 355     none of which implement `Element`), wraps each in `Adapted`, and drives it through the
 356     *existing* `scene::bridge` layout pass and `scene::painter` paint pass — asserting both the
 357     computed rects and the painted quads. Purely additive: no existing widget or app changes, all
 358     137 cce-ui tests pass. Runtime verification is N/A until a real widget is migrated onto the
 359     adapter (nothing in a running app uses it yet).
 360   - **5b — Input concern: DONE (compile + tests; no runtime surface yet).** `widget/model.rs` —
 361     the `Input` trait (`hit` / `on_event`, both against the laid-out rect) plus adapter
 362     forwarding with the RFC's centralizations: `Adapted::handle_event` hit-gates pointer-
 363     positioned events (`MouseButton`/`MouseWheel`) once, so narrow widgets never carry the
 364     per-widget "check hit_test first" boilerplate every legacy `mouse_input` override does;
 365     unconsumed `PointerMove` falls back to the legacy hover bookkeeping, so `base.hovered` and
 366     the synthesized `MouseEnter`/`MouseLeave` (which re-enter `handle_event` and reach
 367     `on_event`) keep working; `hit_test` keeps the occlusion (`is_coordinate_covered`) check
 368     while delegating the geometric test to `Input::hit`. A headless test drives a narrow
 369     `Clicker` through the *real* `UiContext::propagate_event` router: in-rect click consumed +
 370     counted, out-of-rect click gated out, hover enter/leave transitions observed on both the
 371     narrow widget and the base flag. 138 cce-ui tests pass.
 372   - **5c — First real widget migrated: `ProgressBar`. DONE (runtime-verified, pixel-identical).**
 373     `widget/display/progress_bar.rs` now implements only `Layout` + `Paint` + `Input`;
 374     `ProgressBar::new` returns `Adapted<ProgressBar>`, so both construction sites
 375     (`cce-ui` demo, `cce-test-interface`, incl. `.with_label`) compile unchanged. The migration
 376     forced the adapter to absorb the legacy surface external render loops actually read, all
 377     added to `Adapted` in this step: an `all_rounded_quads` **reverse bridge** (the widget's
 378     `Paint::paint` output converted back to legacy tuples — cce-test-interface renders via this),
 379     `Paint::corner_style` → `corner_radius`/`rounded_corners` (for style-property painters like
 380     the demo's `widget_vertices`; transitional, dies with those paths), the detached-label
 381     convention (`set_rect` inflation + `with_label` builder + content-rect inset),
 382     `preferred_height` ← `intrinsic_size`, `highlight_quad → None` (narrow widgets own their
 383     pixels), and `type_name` reporting the *inner* type (layout.rs string-matches
 384     `"ProgressBar"` for span-full sizing). **Runtime verification:** ran cce-test-interface and
 385     the demo on the live compositor (via `ccectl center-window` + `grim`); an A/B pixel diff of
 386     the demo against the pre-migration build showed the two frames identical except a 19×20
 387     compositor corner artifact — zero differing pixels at any widget. 141 tests pass.
 388   - **5d — Leaf sweep: `Separator`, `StatusDot`, `UsageBar`. DONE (runtime-verified via the
 389     settings app's render stream).** New adapter machinery this round: `Deref`/`DerefMut` to the
 390     wrapped widget (call sites keep `dot.set_status(..)` / `bar.value`); per-prim reverse
 391     bridges (`Prim::Quad` → `extra_quads`, `RoundedRect` → `all_rounded_quads`, `Circle`/`Arc` →
 392     `extra_circles`/`extra_arcs`) so apps reading BOTH `all_quads` and `all_rounded_quads` draw
 393     each prim exactly once; `Input::blocks_backplate_drag`; and the mirrored-by-value-builder
 394     pattern (`Adapted<UsageBar>::with_colors`) since builders can't flow through `Deref`.
 395     `Separator` had no `Widget` base (public x/y/w/h fields) — its rect now lives on the adapter
 396     base, and cce-status-interface's rotation loop was updated to transpose via `rect`/`set_rect`.
 397     **One deliberate behavior fix:** legacy `StatusDot` emitted **zero** geometry on every render
 398     path (probe-confirmed — `render_widget` reads only `all_quads`/`all_rounded_quads`, both
 399     empty for it), so the Processes-page dots were invisible; the narrow `Paint` default emits
 400     the color quad, and the dots now render (verified in the live app's render dump: 10×10 rects
 401     in exact status colors). UsageBar verified byte-identical in the same dump (bg+fill rects at
 402     the exact `with_colors` colors). 147 tests pass; status-interface, system-settings, and
 403     test-interface all build.
 404   - **5e — First interactive widgets: `Checkbox` + `Toggle`. DONE (verified end-to-end with an
 405     injected live click).** New machinery: the legacy polling/value surface on `Input`
 406     (`take_click`/`take_change`/`value_string`/`set_value_string`/`value` — kept there to avoid
 407     a fourth bound; dies with RFC §3.5 typed messages); `Input::opens_context_menu` (the adapter
 408     routes a hit right-press to `UiContext::handle_right_click`, which ctx-less `on_event`
 409     can't); `Layout::inline_label` (Checkbox/Toggle draw the label inside their rect — no
 410     `set_rect` inflation/content inset, matching the legacy `label_offset` type-name special
 411     cases); `Paint::{solid_border, widget_font, sync_label}` (transitional forwards);
 412     prim-derived `text_labels` for inline-label widgets (one paint source feeds every text
 413     path) with a base-label fallback replica for detached ones; `as_any` now exposes the *inner*
 414     widget so legacy `downcast_mut::<Checkbox>()` sites keep working; `Drop` on `Adapted`
 415     clears the global focus/context-menu refs (bounds moved onto the struct for this);
 416     `Debug`/`Clone` derives; and an inherent `Adapted::set_label` that shadows
 417     `Control::set_label` (which writes only the base and left self-painted labels stale —
 418     caught by a test; `Control` impls override to route here). Both widgets track
 419     `hovered`/`focused` from the forwarded `MouseEnter`/`MouseLeave`/`FocusIn`/`FocusOut`
 420     events — the state that becomes `Animated<f32>` in §3.6. In-crate consumers updated
 421     (`json_layout` direct Element calls, `multi_control` enum variant, `parameters_bg` field);
 422     app repos updated (system-settings network+notifications, data-editor, layout-interface
 423     field types — construction sites unchanged). **Verification:** cce-test-interface pixel-
 424     diffed 0 against the pre-migration baseline, and a `wlrctl`-injected click on the live
 425     compositor flipped the Toggle's bordered half on-screen — the full input path through the
 426     adapter exercised for real. 152 tests pass.
 427   - **5f — `Button` (widest-radius widget: 19 app files + 8 in-crate). DONE.** The press/release
 428     contract forced an adapter refinement: **presses stay hit-gated, releases now flow ungated**
 429     — a press-tracking widget must see the release wherever the cursor ended up to commit
 430     (in-rect → `take_click` + `on_click_cb`) or cancel, exactly the legacy `mouse_input`
 431     contract (pinned by a router-level test incl. out-of-rect cancel). Also added:
 432     `Layout::layout_ignore` and `Input::set_selected` forwards. The model ports the per-kind
 433     color matrix (Primary/Reset/ListRow/CopyIcon × pressed/hovered/selected + bg overrides),
 434     SVG icon quads, per-kind label justification/fonts, and the Phase 2b `intrinsic_size`; the
 435     9 by-value builders are mirrored on `Adapted<Button>` (`with_label` comes from the generic
 436     + `sync_label`). In-crate consumers fixed (multi_control, keybinds_control, ramp, treelist,
 437     parameters_bg fields; json_layout + demo now call `take_click` on the box instead of
 438     concrete downcasts); ~11 app repos updated (field types + raw-cast→`as_ptr_mut` cleanups).
 439     **Verification:** full workspace (minus compositor, which doesn't use widgets) builds;
 440     154 cce-ui tests + cce-cloud's json_layout hover-simulation test pass; test-interface
 441     pixel-diffs cursor-only vs the 5e baseline; a live hover A/B against the stashed legacy
 442     build showed the identical fill pixel (the inert hover on that page is pre-existing app
 443     behavior, not a regression).
 444   - **5g — `Label`. DONE.** Text lives on the model, emitted as a `Text` prim; the adapter's
 445     prim bridge serves every legacy text path. Added the generic synced `Element::set_text`
 446     override on `Adapted` (same trap as `set_label`: the trait method wrote only the base and
 447     left the painted text stale — live-updating labels like system-info's CPU readouts hit it
 448     constantly). Builders mirrored; three app repos' field types updated. Workspace builds;
 449     155 tests pass; test-interface diff vs the post-Button baseline has a 0x0 bbox at 2%
 450     threshold (sub-perceptual blend noise only).
 451   - **5h — `Slider` + `RangeSlider`, and the event-capability layer. DONE.** Introduced the
 452     RFC §3.5 **`EventCtx`** (`on_event(&mut self, event, &mut EventCtx)`): content rect, widget
 453     id, `request_focus()` (readout edit mode), and a transitional `ui: Option<&mut UiContext>`
 454     for the legacy scroll-gesture gating. Added `Input` drag hooks
 455     (`draggable`/`is_dragging`/`drag_begin`/`drag_update`/`drag_end`, rect-carrying — hosts
 456     drive drags by direct call) and — critically — **direct-dispatch overrides**: hosts call
 457     `mouse_input`/`mouse_wheel`/`keyboard_input`/`focus`/`unfocus` directly on widgets, and
 458     without adapter overrides those hit the inert Element defaults (a latent 5e/5f regression:
 459     treelist's add-key button and parameters_bg checkboxes were deaf on that path — now routed
 460     into `handle_event`). `Layout::inflates_label_rect` distinguishes ProgressBar-style rect
 461     inflation from Slider-style label-eats-into-rect. `text_labels` is now prim-derived PLUS
 462     base fallback (sliders paint readout text AND have a detached label). **Found the hard
 463     way:** hosts under-size labeled sliders, so legacy content height went NEGATIVE and the
 464     flipped quads still rasterized — `content_rect` must not clamp at zero or tracks vanish
 465     (pixel-diffed to 0 vs baseline after the fix). Slider geometry consolidated into one
 466     `geom()` helper (legacy re-derived it in five places). 154 tests pass; workspace builds.
 467   - **5i — Display leaves + `Spinbox`: InfoBox, FontPreview, Sidebar, Splitter, Panel, Spinbox.
 468     DONE.** New adapter machinery: `Input::drag_reposition` (self-moving widgets — Panel,
 469     Splitter — return a new origin; the adapter applies it to the base rect the model can't
 470     reach), `Input::set_drag_bounds`, and `Layout::detached_label_inset` (the legacy
 471     `Control::control_label` +4px x-offset that the default `text_labels` path lacked — caught
 472     as a 4px label shift in the pixel diff, fixed to a 1×1-pixel residual). Spinbox ports the
 473     sub-zone hover (-/+ buttons) into `PointerMove` handling, display-click edit mode with
 474     cursor placement + `request_focus`, decimals/unit value formatting, and drops its vestigial
 475     raw-pointer parent/children fields. Skipped for later: `PreviewState` (needs per-label fonts
 476     on the `Text` prim), `StatusBar` (bigger custom surface), Float3/LayoutPreview (time).
 477     156 tests pass; workspace builds; test-interface pixel-diff vs the 5h baseline: 1 pixel.
 478   - **5j — `InteractiveListItem`. DONE (verified in the live settings render dump: service-row
 479     titles/subtitles + themed overlays through the prim bridge).** Button-pattern press/release
 480     with themed selected/hover/press overlays. `StatusBar` was surveyed and DEFERRED: it is
 481     parent-coupled (reads its parent's backplate state/rect/radius at paint time and owns
 482     `parent`/`set_parent`) — that belongs with the container/children design, not the leaf
 483     recipe. Also still pending from the leaf tier: Float3, LayoutPreview (time), PreviewState
 484     (needs per-label fonts on `Prim::Text`).
 485   - **5k — Controller capabilities + first controller widgets: `Breadcrumb`, `Node`. DONE.**
 486     The controller tier was blocked because `Element` is implemented exactly once (for
 487     `Adapted<W>`), so a migrated widget couldn't re-expose its `as_*_controller` downcasts.
 488     Resolution: transitional **capability hooks on `Input`** (`menu/graph/spreadsheet/path/
 489     param/geom _controller[_mut]`, default `None`) that the adapter forwards the `Element`
 490     downcast pairs to — a controller widget returns `Some(self)`. This is the pragmatic half of
 491     the §3.5 "typed messages" bullet: the controller *traits* are already the typed surface;
 492     what dies with `Element` is reaching them through the god-trait (end state: hold the
 493     concrete `Adapted<W>` or a `&dyn XController` directly). Also added:
 494     `EventCtx::open_context_menu` (Breadcrumb records the right-clicked segment *before* the
 495     shared menu opens — `opens_context_menu` can't express work-before-menu), an
 496     `Input::copy_path` forward (context menu "Copy Path"), and an `Adapted::on_cursor_moved`
 497     override — another direct-dispatch entry (cce-files drives breadcrumb hover through it)
 498     routing the raw move to `on_event` with the base hover bookkeeping as fallback.
 499     `ScrollController` was deleted outright: zero implementors and zero live callers
 500     workspace-wide (only a dead cce-designer helper, removed there). **Breadcrumb** (first
 501     controller widget, live in cce-files + cce-designer) verified on the live compositor:
 502     idle/hover captures pixel-identical to the stashed legacy build at the widget, and a
 503     breadcrumb-segment click navigates correctly through the direct-dispatch
 504     `mouse_input → on_event → path_click` chain. **Node** (ParamController + GeomController,
 505     self-moving drag with grid snap via `drag_reposition`) has no live constructors in any app
 506     repo — compile + router-level tests only. 160 tests pass; all 19 client crates build.
 507   - **5l — `Spreadsheet` + the tick/scroll adapter surface. DONE (live-verified in the
 508     designer: startup and pane-open captures diff 4px/32px vs the stashed legacy build, all in
 509     a one-pixel bottom-edge blend strip).** New `Input` surface: `tick(dt, rect)` +
 510     `wants_tick` (inertial scroll — hosts broadcast `Element::tick` per frame; §3.6
 511     `Animated<T>` eventually replaces this), `scrollable` (→ `is_scrollable`), and
 512     `draggable` now takes the laid-out rect (scroll widgets are draggable only while content
 513     overflows). **`Adapted` now owns real visibility**: the `Widget` base carries none and the
 514     legacy `Element` defaults are no-ops, so each hideable widget stored its own flag; the
 515     adapter stores it once, gating `hit_test` (hosts broadcast wheel/press dispatch and rely
 516     on hidden widgets rejecting the hit), direct-dispatch `keyboard_input` (hiding a pane
 517     doesn't unfocus it), and the `text_labels` bridge. Deliberate fix: `set_visible` on
 518     migrated widgets now works instead of being silently ignored. Spreadsheet's 7×-duplicated
 519     scroll/thumb math collapsed into one `geom()` helper; its `paint` deliberately does NOT
 520     emit the translucent `PARAM_BG` background (the designer draws widget backgrounds itself
 521     from `color()` + `corner_style` — emitting it again would double-blend).
 522   - **5m — `Graph` + the legacy dual-geometry escape hatch. DONE (designer startup pixel-diffs
 523     ZERO; cce-graph diffs to an empty 2%-threshold bbox; cce-files' Graph view verified
 524     visually + getter-contract unit test — its pixel A/B was blocked by the live session's
 525     terminal covering the capture region).** Graph's three hosts consume DIFFERENT getters
 526     (designer: plain `extra_quads` + `extra_circles`; cce-files `render_widget`:
 527     `all_rounded_quads` with highlight-only `all_quads`; cce-graph: the scene path's
 528     `paint_self`). The model keeps one geometry generator; `paint` emits the rounded view, and
 529     transitional `Paint` hooks serve the rest: `serves_legacy_plain_quads`/`legacy_plain_quads`
 530     (verbatim through `extra_quads`, with the adapter emptying `all_quads` to preserve the
 531     no-double-draw contract) and `text_bounds` (node names clip to the widget rect — the
 532     adapter now overrides `text_labels_with_[font_and_]bounds`, replicating the
 533     scroll-ancestor walk when the hook is `None`). Also: `Input::hit` override for the legacy
 534     edge-exclusive hit test; ctrl-wheel zoom reads `ctrl_pressed` through `EventCtx.ui`;
 535     the `Element::paint` register_hovered pre-pass is dropped (shared hover highlight is
 536     suppressed for all adapted widgets). Discovered en route: the designer's CONTENT pane IS a
 537     real `Graph` (index 1), and `ContentBg` is a standalone grid background whose
 538     GraphController impl is mostly stubs — it is NOT a Graph wrapper.
 539   - **5n — The container concern + first containers: `Switcher`, `ContentBg`. DONE
 540     (render-dump-verified: cce-system-settings — whose every page lives under the Switcher —
 541     A/B'd byte-identical across four pages against the stashed legacy build, modulo live
 542     system data).** The children/tree design, resolved transitional-first: tree links already
 543     live in `ctx.tree` (Phase 1b), so a container needs only (a) its own child-pointer Vec
 544     (ctx-less `set_rect` arrangement — the same reason legacy containers kept one) and (b) the
 545     subtree plumbing every legacy container hand-copied. (a) stays in the model behind new
 546     `Layout` hooks (`has_container_children`/`container_children`/`child_added`/
 547     `children_cleared`/`parent_changed`/`adjust_rect`/`arrange_children`/
 548     `layout_children_ctx`/`child_visible`); (b) moved into the ADAPTER once, filtered by the
 549     `child_visible` policy: plain-quad aggregation with the shared rounded-bg-skip rule,
 550     rounded recursion (the Element default this override had been shadowing — a latent
 551     container blocker), per-kind text aggregation, `get_text_items`/`prepare_text`/`tick`/
 552     popover recursion, container-style `add_child` (parents the child back), the ctx-carrying
 553     child layout pass, and `is_child_visible`. `Input` gains `hits_through_children` and
 554     `gates_presses` (event-proxying containers must see every press — Switcher unfocuses its
 555     child on an outside click). Event proxying itself stays in the model via `EventCtx::ui`,
 556     bug-for-bug (including Switcher's double `mouse_input` dispatch while a popover is open).
 557     `ContentBg` turned out to be a leaf and rode the ordinary recipe. NOT for this path:
 558     deep-composition containers (Page/Plate embed `Layer`; SectionContainer embeds
 559     `Container`; Paginator embeds ButtonStrip + `Vec<Page>`) — embedding means migrating the
 560     base struct inverts the dependency; those dissolve when their hosts move to the scene
 561     walk (Phase 6), not through `Adapted`.
 562   - **5o — `MenuBar` migrated; standalone `Menu` DELETED (zero constructors workspace-wide —
 563     dead code).** MenuBar keeps its legacy `ButtonStrip` EMBEDDED in the model (owned by
 564     value, driven through `Element` calls; events reach it via `EventCtx::ui`;
 565     `arrange_children` parents it back to the adapter). New adapter surface: `Paint::popover`
 566     / `draw_popover` (own dropdowns — the container recursion only covered child popovers),
 567     `Layout::z_order`, `Layout::tracked_parent` (serves `Element::parent` from the model's
 568     field — legacy parent-chain styling walks use a DUMMY ctx that tree lookups can't
 569     answer), `Input::set_modifiers` / `visibility_changed` / `is_focused` (conditional focus:
 570     the bar holds the global slot only while something is open) + `EventCtx::release_focus`,
 571     the `as_page_selector` capability pair, and `Paint::corner_style` now takes the laid-out
 572     rect (corners computed against the parent backplate's edges). Dropped, flagged: the
 573     never-read glyphon buffer caches and the vertical-mode dynamic `rect()` (`with_vertical`
 574     has no callers). Verified: 168 tests (full open→click→close roundtrip through real
 575     adapter dispatch), all four hosts run, live A/B on cce-test-interface pixel-equivalent
 576     (the strip has zero diffs above the 8% threshold; the File-click-opens-nothing behavior
 577     there is byte-identical pre-existing app behavior). Designer A/B (was pending on screen
 578     contention): DONE — startup, params-pane dropdown clicks, circular-pane mode, and a
 579     View→Circular-Pane `trigger_menu_click` roundtrip all pixel-equivalent vs the f1523ab^
 580     baseline (all residual diffs are composited-cursor + bottom-status-strip artifacts).
 581     Driven deterministically via the designer's embedded HTTP API on :3000
 582     (`{"action":"toggle_circular_pane"}`, `{"action":"menu_click","widget_idx":8,
 583     "menu_idx":2,"item_idx":2}`); the params-pane Circular-Pane dropdown not opening on
 584     click is pre-existing app behavior, identical in both builds.
 585   - **5p — `Dropdown` (first popover widget through the 5o `Paint::popover`/`draw_popover`
 586     surface). DONE (render-dump + live-A/B verified).** Slider label convention
 587     (`inflates_label_rect=false`), `Control::control_label`'s +4px via
 588     `detached_label_inset`, `gates_presses=false` (an open dropdown must see the outside
 589     press that closes it), `opens_context_menu`, dynamic `z_order` (100 while open), and
 590     focus parity bug-for-bug: `FocusIn` re-claims the global slot (direct `focus()` callers —
 591     test-interface), `FocusOut` closes without releasing it. New adapter surface:
 592     `Layout::intrinsic_measure_width` + an `Element::measure` override on `Adapted`
 593     (identical to the `Element` default unless a widget opts in — preserves `auto_width`
 594     measuring, which cce-system-settings sizes its page dropdown through). Parity decisions,
 595     flagged: the public `parent` field stays direct-write-only (legacy `set_parent` never
 596     wrote it — Ramp's dummy-ctx `set_parent` calls were silently discarded, so the Ramp
 597     popover clamp and fade-blend parent color were dormant in production and stay dormant);
 598     the backplate-concentric corner walk starts from a `parent_changed`-tracked pointer and
 599     hops field-based legacy `parent(&dummy)` impls (exact for cce-graph's
 600     Dropdown→Plate→Backplate chain; deep tree-only chains lose the adjustment); the row-rect
 601     hit expansion is dropped, consistent with every migrated control. Verified: 169 tests,
 602     full workspace builds, cce-system-settings fonts/notifications render dumps
 603     content-identical (only the detached label's emission order shifts — adapter appends it
 604     after the widget's prims), cce-graph startup/open/close live A/B **byte-identical**
 605     (AE=0 open state on a clean run; run-to-run compositor translucency noise ~15k AE dwarfs
 606     any residual), cce-fonts + cce-test-interface smoke-run. App sweep: 8 repos (graph,
 607     text-editor, fonts, layout-interface, system-settings, data-editor, files,
 608     test-interface) — field types to `Adapted<Dropdown>`, raw casts to `.as_ptr_mut()`,
 609     two `&mut Dropdown` fn params in cce-files pages.
 610   - **5q — `TextBox` (widest-surface leaf; the clipboard/selection tier). DONE (render-dump
 611     + live-A/B verified, data-editor byte-identical).** New adapter surface: the `Input`
 612     clipboard quintet (`cut_selection`/`copy_selection`/`paste_from_clipboard`/`select_all`/
 613     `clear_text` — defaults replicate the whole-value `Element` defaults so earlier
 614     migrations keep their shipped behavior), `Paint::prepare_text` (TextBox's glyph shaping
 615     is load-bearing: `map_x_to_idx` reads the measured advances), `Layout::hit_row_rect`
 616     (restores the legacy row-substituted, side-label-inset hit geometry — cce-files'
 617     save-name box relies on row hits; earlier migrations' drop of it stands, opt-in),
 618     `Layout::adjust_row_rect` + `Layout::rect_assigned` (the width/max-width clamp on both
 619     rect paths; ungated scroll re-clamp on every `set_rect`), `Input::tracks_base_focus`
 620     (legacy TextBox's `focus()` never set the base flag — its detached label must not color
 621     as focused), and **`Paint::legacy_focus_highlight`**: the shared focus-highlight overlay
 622     the adapter suppresses for every migrated widget is re-enabled per-widget — legacy
 623     TextBox kept the `Element` default, and the focused editor's primary-tint wash
 624     (data-editor's teal editing surface) is real legacy behavior. Found the honest way: the
 625     first A/B came back 1.4M pixels apart; after restoring the overlay (replicated
 626     byte-for-byte in `Adapted::highlight_quad` + the `all_quads`/`paint_self` inclusion
 627     points), data-editor startup AND focused-editor states are **byte-identical (AE=0)**.
 628     The asymmetric legacy render split is preserved (non-rounded `extra_quads`: full-width
 629     background + disabled special-case; rounded `all_rounded_quads`: side-label inset, no
 630     disabled branch). Flagged approximations: releases re-check plain-rect containment
 631     (legacy hit-gated them through the row-substituted test); wheel is now hit-gated by the
 632     adapter (legacy hosts dispatched it to the hovered widget themselves). Verified: 169
 633     tests, workspace builds, settings fonts/processes dumps content-identical (processes
 634     modulo live PID/CPU data), cce-data-editor live A/B byte-identical. Sweep: 9 app repos
 635     (authenticator, data-editor, display-manager, email, files, fonts, layout-interface,
 636     system-settings, text-editor) + 5 in-crate embedders (treelist, scrolling_list,
 637     keybinds_control, multi_control's `InstancedWidget` variant, parameters_bg).
 638   - **5r — `Paginator` (value-embedded container: ButtonStrip + Vec<Page>). DONE (live-A/B
 639     verified, cce-layout-interface byte-identical across four states).** The model owns both
 640     embedded legacy widgets by value and proxies events to them (strip first, draining its
 641     click into the selection; then the selected page while not hidden); the container
 642     concern serves them via `container_children`/`child_visible` (strip always, selected
 643     page only), and `arrange_children` is the legacy `set_rect` body. New adapter surface:
 644     **`Layout::register_embedded_children(host_id, ctx)`** — legacy `tick`/`layout`
 645     re-registered the strip + pages into the ctx registry every frame, and that registration
 646     is load-bearing (the spatial grid is rebuilt from registered widgets; the registered
 647     strip is what blocks backplate drags over the sidebar —
 648     `backplate::tests::test_paginator_blocks_backplate_drag`); the adapter calls it from
 649     `Element::tick` and `Element::layout`, the legacy cadence. **`Paint::
 650     aggregates_child_extra_quads`** — legacy container `extra_quads` served the CHILDREN's
 651     chrome only, while the widget's own background quad lived in `all_quads` alone;
 652     cce-mail and cce-layout-interface render the tab column through `extra_quads` over
 653     their own backgrounds (emitting the bg there would double-blend), and cce-test-interface
 654     renders through `all_quads` (dropping the bg there would blank it). The adapter's
 655     `all_quads` now draws own prims from a shared `own_plain_quads()` helper instead of
 656     `extra_quads()` (identical for every prior migration) so the two views never
 657     double-serve. **`Paint::forwarded_highlight(ctx)`** — legacy `highlight_quad` forwarded
 658     to the strip's (the hovered-tab tint cce-layout-interface draws by calling
 659     `highlight_quad` directly); served only through that getter, kept out of
 660     `all_quads`/`paint_self` (gated on `legacy_focus_highlight` now — where the strip's own
 661     aggregation already carries it, as legacy container `all_quads` overrides did).
 662     `PageSelector` + `MenuController` ride the existing `Input` capability hooks
 663     (cce-test-interface reaches `sidebar_w` through an `as_page_selector()` downcast on
 664     `dyn Element`). Ported bug-for-bug though unused workspace-wide: the `pages` container
 665     surface (`add_widget_to_page`/`set_pages`/…) — every app manages page content itself
 666     keyed on `selected_page()`. Verified: 171 tests, all four consumer apps build,
 667     cce-layout-interface live A/B **byte-identical (AE=0)** on idle, File-tab hover,
 668     Page-tab click, and Page-selected hover (exercises extra_quads aggregation, highlight
 669     forwarding, labels, and the click→selection→page-switch path);
 670     cce-test-interface A/B equals its launch-to-launch noise exactly (same 7.5k-px bbox —
 671     animated waveform phase; the `all_quads` gallery path contributes zero residual).
 672     Sweep: 3 app repos re-typed to `Adapted<Paginator>` (email, files, layout-interface);
 673     test-interface's `Box<dyn Element>` gallery needed no change.
 674   - **5s — `ParametersBg` (the designer's parameter panel; the last real container). DONE
 675     (live-A/B verified, cce-designer pixel-equivalent across four states).** The model caches
 676     its laid-out rect via `Layout::rect_assigned` (all row geometry derives from it — the
 677     TextBox pattern; `arrange_children` is the legacy child-stacking tail, visible-gated by
 678     the adapter exactly as legacy gated it), keeps the value-owned per-row widget vecs
 679     (mostly `Adapted<W>` already) plus the raw-pointer `children` list on the 5n container
 680     hooks, and ports the full bespoke event surface into `on_event` arms: the every-press
 681     dispatch chain (`gates_presses=false` — the panel consumes every left press, scrollbar
 682     thumb drag math, popover-first ordering, per-row-type dispatch with value commit-back,
 683     the inline emacs-flavored code editor) and the host-driven drag surface on the `Input`
 684     drag hooks. New adapter surface: **`Paint::serves_legacy_labels` /
 685     `legacy_labels_with_font_and_bounds(rect, ctx)`** — the text sibling of the 5m
 686     dual-geometry hatch: the standard bridge gives every own label ONE font and ONE clip
 687     rect, but this panel assigns them PER LABEL (viewport clip everywhere, code-box clip +
 688     monospace inside a code row); served as a full replacement, children included. Also
 689     **`EventCtx::widget_addr()`** — the wheel arm's occlusion check
 690     (`is_coordinate_covered`) keys on the adapter's address, which `on_event` couldn't
 691     reach. Reuses 5m's plain-quad hatch for the designer's raw `extra_quads` render path
 692     (row chrome, section borders, code cursor, scrollbar — with the panel's translucent
 693     PARAM_BG plate deliberately NOT emitted: the designer draws it from
 694     `color()`/`corner_style`, the 5l double-blend trap). `window_runner`'s
 695     `get_child_widget_for_quad` downcast keeps working unchanged (`as_any` exposes the
 696     inner type; the 9 pub sub-widget fields stay pub). Flagged approximation: the legacy
 697     scrollbar-press called `self.focus()` (base flag only — nothing reads it; the highlight
 698     keys on the ctx focus slot and the designer tracks panes by index); dropped.
 699     Verified: 175 tests (4 new: controller roundtrip + row layout, checkbox/code-editor
 700     commit flows, hatch split, overflow scrolling), cce-designer builds with ZERO app
 701     changes (`Box::new(ParametersBg::new())` coerces), live A/B across startup /
 702     wheel-scrolled pane / dropdown-row click / second click identical except the bottom
 703     status strip — calibrated as launch-to-launch live-data noise (same 1447×21 bbox,
 704     77 px between two launches of the SAME baseline vs 81 px old-vs-new). The wheel state
 705     changed 107k px within a build and matched across builds, so the event path is
 706     genuinely exercised. (The params-pane Circular-Pane dropdown not opening on click is
 707     the pre-existing app behavior recorded in 5o, identical in both builds.)
 708   - **5t — the deferred leaves: `Float3`, `LayoutPreview`, `PreviewState`, `StatusBar`.
 709     DONE (live-A/B verified: cce-files byte-identical ×3 states, cce-designer params pane
 710     byte-identical ×3 states, cce-system-settings byte-identical whole-window).**
 711     Consumer survey first (it reshaped the work): every external `Float3` grep hit is the
 712     MATH type (designer `GAttribute::Float3` / wgpu `Float32x3`) — the widget's only
 713     consumer is ParametersBg, whose pub-field reach (`values`/`mins`/`maxs`/`edit_buffer`/
 714     `editing_idx`) flows through `Deref` unchanged; LayoutPreview has ZERO consumers
 715     (definition + re-exports only); PreviewState is cce-files' preview pane; StatusBar has
 716     six construction sites across five apps plus the demo.
 717     **Float3**: rect cached via `rect_assigned` (`get_row_rects` is pub API with no rect
 718     param), readout-edit + track-drag into `on_event`/the drag hooks, the readout click's
 719     legacy `focus::set_focused(self)` rides `EventCtx::request_focus`, commit-on-unfocus via
 720     the FocusOut arm. **LayoutPreview**: mechanical (paint = quads + text; the duplicated
 721     SimNode match collapsed into one helper). **PreviewState**: the widget the 5s labels
 722     hatch was built for — canvas quads flow from `paint`, canvas labels (per-label
 723     monospace for content lines) through `serves_legacy_labels`; plain `text_labels` stays
 724     EMPTY like legacy (emitting the text as prims too would double-render under container
 725     aggregation — the legacy scene path showed no text either, preserved). The adapter
 726     gains a blanket `impl Default for Adapted<W: Default>` (cce-files constructs it via
 727     `Default`); the app's struct-literal update became field mutation (the private cached
 728     rect can't ride functional-update syntax — and now survives updates instead of zeroing
 729     until the next layout pass). **StatusBar**: the MenuBar parent-coupling pattern
 730     (tracked parent, backplate-aware color/text-color/blur, corners-against-parent at the
 731     parent's radius) plus ONE new hook — **`Paint::text_items()`**: pre-shaped glyphon
 732     buffers for the legacy `get_text_items` path, which prim-derived text cannot serve (it
 733     returns borrows of widget-owned buffers); cce-status-interface drives the bar by hand
 734     (`prepare_text` → `get_text_items` into its own paint) and data-editor/system-settings
 735     host it as a Backplate child. `inline_label` keeps `Element::set_text`'s base-label
 736     write from leaking a detached label. Deliberately preserved asymmetry: NO
 737     `widget_font`, so the container text path keeps rendering the bar's text in the
 738     default font while the buffer path uses the statusbar font, exactly as legacy.
 739     Verified: 177 tests (Float3 readout/drag flow; StatusBar manual-host pipeline —
 740     covering the path of the one app, cce-status-interface, not A/B'd live: it is the
 741     user's session status bar). Sweep: 4 app repos (files: field + literal→mutation;
 742     data-editor: field + one raw `*mut StatusBar` cast → `as_ptr_mut()`; system-settings +
 743     status-interface: field types).
 744   - **Phase 5 widget migration COMPLETE.** Everything remaining on `impl Element` is
 745     embedded-base machinery by design: the containers
 746     (Layer/Container/Page/Plate/Backplate/ScrollBox/List/…) dissolve via Phase 6 scene
 747     adoption instead. Then delete `Element` + `Adapted` once the last widget is across, at
 748     which point the `as_*_controller` pairs and the `Input` capability hooks die together
 749     (callers hold concrete types or `&dyn XController`).
 750 - **Phase 6 — Per-app migration.** Move each `cce-*` app onto the new core; delete legacy paths
 751   once the last app is across. **Definition of done per app:** the whole frame — geometry AND
 752   text — is one `Application::display_list()` (+ `display_list_text()`), layout runs through
 753   the scene solver where the app has a real tree, events reach widgets through routed dispatch
 754   rather than hand-rolled per-widget loops, and no embedded-base container
 755   (Layer/Page/Plate/Backplate/ScrollBox/List) is load-bearing. Seven apps already feed
 756   geometry through `display_list()` (colors, data-editor, files, fonts, graph, text-editor,
 757   system-settings) — their remaining gaps are text, layout, and events.
 758   - **6a — display-list text. DONE (live-A/B verified via cce-notifier).** `Prim::Text` now
 759     carries `font: Option<String>` + `bounds: Option<[f32;4]>` (`PaintCtx::text_with`; plain
 760     `text` emits None/None), and the backend renders a list's Text prims through the glyphon
 761     pass — shaped via the shared `get_text_buffer` cache, clipped to the item clip ∩ the prim
 762     bounds, held in `EngineState::dl_text_items` so the `TextArea`s can borrow the buffers.
 763     **Opt-in via `Application::display_list_text()` (default false)**: the seven Phase 3
 764     adopters' lists already carry Text prims that those apps ALSO push as `TextItem`s —
 765     rendering both would double-draw; each app flips the flag when it stops pushing its own.
 766     `Application::view`/`text_items` gained no-op defaults so a fully migrated app implements
 767     neither. Known limitation (scoped out, not a bug): the legacy `text_areas`
 768     popover-occlusion clip is not applied to display-list text yet — a popover plate does not
 769     hide list text beneath it (text draws after all geometry); apps with popovers keep their
 770     own text path until that lands. The 5s/5t labels hatches become expressible as prims once
 771     hosts consume lists directly.
 772   - **6b — `cce-notifier` (first app fully on one path). DONE (live-A/B on a private D-Bus
 773     session: text/accent pixel-identical; 60-px residual is compositor translucency noise in
 774     the alpha-0.9 background region).** The whole frame is one display list (accent quad +
 775     three `text_with` prims in the configured bundled family); deleted: the app-side
 776     `FontSystem`, the `TextItem` cache, `rebuild_layout`, and the scale/rebuild bookkeeping.
 777     Non-interactive, so no event surface. This is the reference shape for a minimal Phase 6
 778     app.
 779   - **6c — `cce-wallpaper` + `cce-screenaver` across; `display_list` gains `(size, scale)`.
 780     DONE (wallpaper live-A/B AE=0; screensaver background-fill verified live, sim quads are
 781     the same mechanical loop).** The Phase 6 frame entry point now receives the frame's
 782     logical size and HiDPI scale like `view` did (fullscreen apps size geometry from it);
 783     mechanical sweep across the eight implementors. Both apps' dead `TextItem` caches
 784     deleted.
 785   - **6d — the paint walk carries per-widget fonts + clip rects. DONE (179 tests; cce-graph
 786     live A/B shows zero structural diff — all residual below the 8% translucency-noise
 787     amplitude).** `Adapted::paint_self` no longer forwards `Paint::paint`'s plain Text prims:
 788     it re-emits the geometry verbatim (through the ctx so the walk's offset/clip apply
 789     once) and serves text as `text_with` prims from the SAME views the standard text bridges
 790     use — `own_labels_with_font_and_bounds` (prim text + detached base label, `widget_font`,
 791     `text_bounds` or the scroll-ancestor clip) or the 5s per-label hatch verbatim (caveat
 792     noted in-code: the hatch contract includes raw container children). This makes a
 793     `paint_tree` display list's text renderable-correct for migrated widgets, which is the
 794     precondition for the seven adopters flipping `display_list_text`. Found and recorded on
 795     the way: the LEGACY `Element::paint_self` default drains the child-aggregating
 796     `text_labels` for legacy containers, so scene-path text double-emits under the walk for
 797     trees that still contain Layer/Page/etc. — invisible today (text prims unrendered
 798     without the opt-in), but it means an app can only flip `display_list_text` once its
 799     tree is embedded-base-free. Consistent with the dissolution plan; revisit per app.
 800   - **6e — `cce-colors` flips `display_list_text` (first of the seven adopters). DONE
 801     (live-A/B'd; slider drag re-verified via wlrctl).** The whole frame is one
 802     `display_list()` — the rebuild check moved off the deleted `view`/`view_rounded_quads`
 803     overrides, `rebuild_layout` keeps the flat PageContent text tuples and the list emits
 804     them as `text_with` prims; deleted: the app-side `FontSystem`, the `TextItem`
 805     assembly, and the `CCE_LEGACY_PAINT` fallback. The A/B exposed a PRE-EXISTING runtime
 806     bug this fixes: the app shaped its `TextItem`s with its own
 807     `create_font_system_with_system_fonts()`, whose fontdb face IDs don't resolve in the
 808     engine's render `FontSystem` — every `font: None` label (slider names, channel
 809     values, hex readout) was INVISIBLE at runtime in the baseline (only the bundled-font
 810     button labels survived). Shaping through the engine's FontSystem (the dl-text path)
 811     is what makes the text render at all. Note for the remaining adopters: an app-side
 812     `FontSystem` is not just dead weight, it is a live font-resolution hazard — check
 813     each app's text for the same silent invisibility before trusting its baseline
 814     capture. cce-colors' safety: no popovers, no paint_tree (flat-list bridge), so
 815     neither the popover-occlusion gap nor the 6d embedded-base double-emit applies; its
 816     root `Backplate` remains for legacy layout/events (full definition-of-done still
 817     pending events + layout).
 818   - **6f — `cce-files` flips `display_list_text`. DONE (live-A/B pixel-identical, AE=0;
 819     dropdown popover + breadcrumb context menu re-verified live).** Same mechanical shape
 820     as 6e: rebuild check into `display_list()`, `rebuild_layout`'s text tuples emitted as
 821     `text_with` prims, `view*`/`CCE_LEGACY_PAINT`/TextItem assembly deleted. Two deltas
 822     from colors: the app-side `FontSystem` STAYS (TextBox/List `prepare_text` measurement
 823     still needs it — it's `create_font_system()`, bundled-only, so no 6e invisibility
 824     hazard), and popover occlusion needed nothing from the engine — cce-files folds
 825     popover/context-menu/dialog occlusion into each text's clip bounds app-side
 826     (`occlude_against`, both axes), and those bounds ride along as prim bounds. That's
 827     the general pattern for flat-list-bridge apps with popovers: the engine's missing
 828     dl-text occlusion pass only blocks apps that rely on the DEFAULT `text_areas`
 829     popover clamp (`ui_context().active_popovers`), e.g. widget-tree apps whose popovers
 830     register through `register_popover`.
 831   - **6g — popover occlusion for display-list text lands; `cce-system-settings` flips
 832     `display_list_text`. DONE (live-A/B pixel-identical, AE=0; page-dropdown popup, page
 833     switch, service-list scroll, live process refresh exercised).** Engine:
 834     `popover_occlusion_clamp` extracted from the default `text_areas` mapping and applied
 835     to `dl_text_items` in `render()`, driven by `ui_context().active_popovers` — the
 836     display_list_text known limitation is gone; apps whose popovers register through
 837     `register_popover` (which `render_widget` does for any open `popover_rect`) can flip.
 838     App: same mechanical shape as 6e/6f, with the app-side `FontSystem` kept for
 839     button-label width measurement (centering) and the search-match highlight rect, and
 840     the wheel fast-path mutating tuple y/bounds in place exactly as it did TextItems.
 841     Popovers/context menu were never main-surface here: they draw on the engine's
 842     xdg-popup surface via the `render_popovers` collector, which is orthogonal to the
 843     flip.
 844   - **6h — `cce-text-editor` flips `display_list_text`; `Paint::text_font` lands. DONE
 845     (live-verified: frame matches baseline modulo a uniform ~2px baseline shift from
 846     engine line-height shaping; File-menu popup + occlusion of editor text beneath it;
 847     click cursor placement identical to baseline).** The FIRST app rendering scene-walk
 848     text prims — its tree (Adapted Dropdown + Adapted TextBox, no embedded-base
 849     containers) was exactly the 6d-safe shape, unblocked by the 6g occlusion clamp.
 850     Engine: `own_labels_with_font_and_bounds` split into a parameterized helper — the
 851     tuple getters keep `widget_font` for every label (unmigrated apps byte-identical),
 852     while the walk view (`own_labels_for_walk`) attaches the new `Paint::text_font`
 853     (default = `widget_font`) to prim-derived labels; the detached base label keeps
 854     `widget_font`. TextBox overrides `text_font`: a customized `font_family`/`font_size`
 855     serves the bare family name so the value text draws in the widget's own font at the
 856     label's size (the control-font string's size suffix would otherwise override it) —
 857     this is what keeps the editor monospace. App: view()'s side effects (registration,
 858     initial focus, relayout, popover registration) moved into `display_list`; chrome
 859     text emitted as prims in the system mono family; `rebuild_text_items` + hand-rolled
 860     Buffer shaping deleted; the app `FontSystem` stays for `editor.prepare_text` (glyph
 861     advances — cursor↔pixel mapping).
 862   - **6i — the 6d trap is FIXED in the walk; `cce-data-editor` flips `display_list_text`.
 863     DONE (live-verified: full-frame parity modulo the uniform ~2px engine line-height
 864     shift; row selection → inline value editor + statusbar update; choice-dropdown
 865     popover renders and occludes rows beneath).** Engine, two changes that make a
 866     `paint_tree` list's text emit exactly once — the embedded-base-dissolution
 867     precondition is GONE for the flip step: (1) the legacy `Element::paint_self` default
 868     emits text only for LEAVES — the legacy container `text_labels` overrides
 869     (Backplate/Layer/Page/SplitBox/Plate) aggregate their children's labels, which the
 870     walk reaches itself; a legacy container with OWN text overrides `paint_self` (Plate
 871     now serves its label this way; SectionContainer still aggregates from internal
 872     non-child widgets and needs the same treatment if it is ever walked). (2) the walk's
 873     `renders_own_subtree` branch (TreeList) emits the subtree's text from the recursive
 874     `text_labels_with_font_and_bounds` — the walk never descends there, so the aggregate
 875     is that subtree's text once, with the tuple pipeline's fonts/bounds. App: the old
 876     `view()` body (registration, focus, relayout, inline-editor placement, popover
 877     registration) moved into `display_list`; `rebuild_text_items` shrank to a
 878     widget-state refresh (rebuild_tree, prepare_text, statusbar text); the toolbar file
 879     label is a prim; `add_element_labels` and the TextItem cache deleted (−240 lines).
 880   - **6j — `cce-graph` flips `display_list_text`. DONE (live-verified: static A/B
 881     residual is only the uniform ~2px engine line-height text shift; node selection,
 882     View-menu popover occluding the node beneath, control-panel toggle with its
 883     multi-line info text via the 6i container fix).** Same recipe as 6i; the app
 884     `FontSystem` deleted outright (no prepare_text dependency). Also fixed a latent
 885     Phase 3 loss found on the way: the loaded-image pixel quads and selection borders
 886     were pushed into `view()`'s plain quads, which the backend DISCARDS when
 887     `display_list` returns Some — images had only rendered under `CCE_LEGACY_PAINT`
 888     since the Phase 3 adoption; they now emit into the display list itself.
 889   - **6k — `Application::load_system_fonts` lands; cce-fonts opts in. DONE (live-verified:
 890     baseline previews Berkeley Mono (bundled) but drew Adwaita Mono (system-only) BLANK —
 891     the app's core purpose was broken for installed fonts; with the opt-in Adwaita renders
 892     as its own face).** The render-FontSystem design settled as a bool `Application` hook,
 893     consulted once at GPU init: the engine's `WgpuAdapter` FontSystem loads system fonts
 894     additively (bundled first, so fontdb face IDs stay aligned with every
 895     `create_font_system*` database — the alignment that makes app-side-shaped buffers
 896     rasterizable engine-side). This was the same face-ID-mismatch class as the 6e
 897     cce-colors bug, and it predates Phase 6 entirely.
 898   - **6l — `TextAttrs` lands; `cce-fonts` flips `display_list_text`. ALL TEN display_list
 899     adopters are now fully on the single paint path. DONE (live-verified: style popover
 900     renders with labels on top and occludes the text beneath via the ui_context-only
 901     registration; selecting Italic re-renders the alphabet in the italic face).** Engine:
 902     `Prim::Text` gains `attrs: TextAttrs { italic, weight }` (toolkit-plain — no glyphon
 903     types in the scene layer), emitted by `PaintCtx::text_attrs`, shaped by
 904     `get_text_buffer_attrs` (cache key includes them). App: same recipe, plus the popover
 905     drawn INTO the list (replacing `overlay_quads`) with labels bounded to the popover
 906     rect, and the open popover registered in `ui_context` ONLY — a global registration
 907     would spawn an empty xdg popup (no `render_popovers` here). Restored two more Phase 3
 908     view()-quad losses (panel borders, alphabet box) and fixed the alphabet's premature
 909     wrapping (legacy passed a LOGICAL width to `set_size` on a physical-unit buffer).
 910   - **6m — first container dissolution: `cce-data-editor`'s root Backplate. DONE
 911     (live-verified: A/B residual 43px over the 8% threshold — translucency noise;
 912     selection/editor/statusbar interactions exercised; held window drag not headlessly
 913     drivable, covered by the new unit test).** The root-Backplate dissolution recipe,
 914     now established: (1) the plate becomes prims replicating `Backplate::color()`/
 915     `corner_radius()` (page-low bg at active backplate opacity, config radius); (2)
 916     top-level widgets register directly in `ui_context`, parentless, and the paint walk
 917     runs per top-level widget in the old child order (composite widgets keep their own
 918     children — the splitter still owns its panes); (3) window dragging answers via the
 919     new `UiContext::drag_allowed_at` — the `is_movable_backplate_at` candidate walk
 920     minus the registered-movable-Backplate requirement, because the surface itself is
 921     the movable plate once the Backplate is gone.
 922   - **6n — `cce-graph`'s root Backplate dissolved; two engine input holes fixed. DONE
 923     (live-verified: A/B residual 14px over the 8% threshold; View menu → Control Panel
 924     toggle → panel renders through the walk).** The 6m recipe applied (plate prims,
 925     parentless top-level registration, walk in old child order, `drag_allowed_at`);
 926     popovers moved to the 6l ui_context-only registration (the global registration
 927     spawned a render-only xdg popup double-drawing the menu; `render_popovers` override
 928     deleted). Found en route, both pre-existing: (1) a press inside an OPEN popover's
 929     plate could start a window move and swallow the click when the widget beneath does
 930     not block dragging (Graph's edge-exclusive canvas hit) — both drag questions now
 931     veto via `point_in_active_popover`; (2) the engine's render-only popups took input
 932     with their default full input region — they now carry an EMPTY input region.
 933     Verification lesson recorded: the compositor drops pointer focus after each click
 934     (Leave with no re-Enter on in-window motion), so headless click sequences MUST
 935     re-park the pointer (`wlrctl pointer move -10000 -10000`) before every click — a
 936     skipped re-park looks exactly like an input regression.
 937   - **6o — graph's two Plates dissolved: `cce-graph` is EMBEDDED-BASE-FREE (the first app
 938     to get there via dissolution; text-editor was born free). DONE (dropdown row A/B'd to
 939     the solver's exact rects after switching to the bridge's own sizing entry,
 940     `Element::intrinsic_size` — a `measure` call returns display-label widths instead;
 941     control-panel area A/B: AE=0).** The transparent dropdown-row Plate (pure layout
 942     shim) became direct placement; the draggable control panel became app state + prims
 943     replicating Plate's exact visual (config plate color / drag tint, plate opacity,
 944     negative-alpha blur flag, border, radius) with the label walked standalone via
 945     Plate's centered-first-child rule. Panel drag reimplemented properly app-side —
 946     NB the legacy `Plate::on_cursor_moved` forwarded drags only to CHILDREN, so the old
 947     panel's own drag likely never moved it (manual drag check pending).
 948   - **6p — cce-fonts' root Backplate + all three Plates dissolved. DONE (live-verified:
 949     family select, style popover + occlusion, Oblique re-render, select-mode bar).** The
 950     6m/6o recipes at full width, plus the first APP-OWNED EVENT DISPATCH: a
 951     `dispatch_widgets` list replicating the Plates' forwarding — popover-first press
 952     pass, unfocus-on-missed-press, `drag_update` forwarding for dragging children
 953     (scrollbar thumbs), panel-grouped order. The A/B surfaced another legacy
 954     double-draw: `Plate::paint_self` aggregated child plain-quads while the walk painted
 955     the child again, double-compositing the ScrollBox background (~22 units darker) —
 956     the dissolved single-draw is the correct rendering. Remaining in fonts: `ScrollBox`
 957     (mid-panel scroll state/scrollbar) and `List` (browse-list scroll/frame) — the last
 958     two embedded-base types in the app.
 959   - **6q — fonts' ScrollBox + List dissolved: cce-fonts is FULLY EMBEDDED-BASE-FREE.
 960     DONE (live-verified: wheel scroll, scrollbar track-jump lands proportionally, family
 961     click after deep scroll + scroll_to_index, alphabet re-render in the new family).**
 962     Both were pure scroll frames in this app (List with columns=None; the rows are
 963     standalone Buttons), so they reduce to one app-owned `ScrollRegion` (~150 lines):
 964     scroll state, wheel, thumb-grab/track-jump/drag, hover-scoped keyboard scrolling,
 965     item-y math with List's silently adjusted item height (max(24, list font + 14)), and
 966     bg/track/thumb prims. The bg is a single list_bg layer — the legacy leaf-walk
 967     stacked rounded + plain copies (the translucent double-compositing class again);
 968     residual A/B delta is a 2px bottom-edge strip.
 969   - **6r — colors' and files' root Backplates dissolved, BOTH A/B'd to AE=0.** These are
 970     the flat-pipeline (render_widget) apps, and their dissolution surfaced the legacy
 971     aggregate's GLOBAL tuple-order contract: `render_widget(root)` emitted every
 972     descendant's PLAIN quads first (via `all_quads` aggregation), then the root's
 973     rounded bg, then every descendant's ROUNDED quads — so the root's translucent plate
 974     WASHES over the plain content (colors' muted slider gradients depend on it; a
 975     naive plate-first order renders saturated). Replication: per-child `render_widget`,
 976     partition the tuples by radius, and interleave [plain…, plate, rounded…]. Wheel in
 977     colors propagates per-slider; both apps answer dragging via `drag_allowed_at`.
 978     Files' view-dropdown popover + breadcrumb context menu re-verified live.
 979   - **6s — settings' root Backplate + StatusBar dissolved; `Dropdown::set_corner_frame`
 980     lands. DONE (WINDOW_PC tuple stream byte-identical; pixels AE=0; live: dropdown
 981     popover → page switch to Processes with statusbar text following, wheel scroll,
 982     service-list render).** Settings needed what colors/files didn't: its plate radius
 983     is a hardcoded 12, so the legacy aggregate's corner RESOLUTION mattered — a child
 984     plain quad flush with a window corner picks up the plate radius there (the
 985     `render_widget` extra-corners logic against the ROOT rect). The hand assembly
 986     replicates the full aggregate: child plain quads (window-clipped, root-corner-
 987     resolved), plate, child rounded quads, root-clamped text, in the old child order.
 988     Two parent couplings surfaced (the widgets read their Backplate ancestor):
 989     (1) StatusBar — bg falls back from the backplate-statusbar theme color to
 990     STATUS_BG, bottom corners round at the PARENT's radius, text color/font are
 991     backplate-specific; it dissolves outright (pure chrome in this app) into tuples +
 992     a `status_text` String. (2) Dropdown — the backplate-concentric corner cut walks
 993     for a Backplate ancestor and silently degrades to a plain rounded box when the
 994     walk finds nothing; new `Dropdown::set_corner_frame((rect, radius, corners))`
 995     hands it the frame explicitly and takes precedence. Also found (pre-existing,
 996     reproduced on the pre-6s baseline): the engine xdg-popup positioner anchors at
 997     the widget's BOTTOM edge regardless of the app's open-upward popover rect, so
 998     settings' page popover displays below the window while clicks land on the
 999     app-side (invisible, in-window) popover rect — the engine popup path's last
1000     consumer; fix when settings' popovers move to the 6l ui_context-only pattern.
1001     The root's `with_border` was never rendered (a rounded Backplate emits no plain
1002     bg quad; the border branch fires only on plain bg quads) — dropped, not ported.
1003   - **6t — settings' popovers + context menu draw INTO the frame;
1004     `Application::draws_own_popovers` lands. DONE (live-verified: dropdown popover
1005     in-window with page geometry occluded beneath, item click switches pages both
1006     ways, spinbox right-click context menu at cursor with dl-text occluded beneath,
1007     dismissal).** This fixes the 6s finding at the source: the engine's render-only
1008     xdg popup anchored at the widget's bottom edge regardless of the app's open-upward
1009     popover rect, so settings' page popover displayed BELOW the window while clicks
1010     landed on the app-side in-window rect. The app now runs the same
1011     `render_popovers` collector into its own tuple stream (appended above window/
1012     page/search content; kept out of the scrollable vecs so the wheel fast-path can't
1013     shift popover content) and deletes the override. Page-widget popovers
1014     (notifications/fonts menus) shift by −scroll_y — the subtraction the popup
1015     positioner used to apply — keeping display aligned with hit-testing under scroll.
1016     Engine: `draws_own_popovers` (default false) gates BOTH popup spawn triggers (the
1017     global popover registry and global context-menu visibility), and under the flag
1018     `render()` adds the visible context menu's rect to the dl-text occlusion overlays
1019     (the menu is engine-global state, not a `ui_context` popover; its own labels are
1020     exempt via bounds == rect). Remaining popup-path consumers: cce-data-editor and
1021     cce-text-editor (`render_popovers` overrides) — the popup surface, `ActivePopup`,
1022     `PopoverCollector`, and this flag all go away once they draw their own.
1023   - **6u — settings' Switcher + Page dissolved; the System page comes back from the
1024     dead. DONE (audio A/B: window tuple stream byte-identical, pixels AE=0;
1025     live-verified across six pages — spinboxes, context menu, page switching, wheel
1026     + fallback, System governor dropdown + scroll, notifications dropdown, fonts
1027     textbox focus).** The top two tree layers reduce to app state: the active page
1028     was always `app.current_page`, page scroll was already `scroll_y`, so what was
1029     load-bearing was Page's scrollbar child, its out-of-bounds event gate, keyboard
1030     scrolling, and being the propagate root. `dispatch_page_event` replicates the
1031     routing (OOB gate with scrollbar-drag bypass; scrollbar first with the y-unshift,
1032     then sections in reverse child order; PointerMove visits all, others stop at the
1033     first handler) against the app-held SectionContainer clones; the scrollbar is an
1034     app field whose quads collect into the window assembly's plain slot with the
1035     legacy one-frame-stale content height. Found on the way: the System page's
1036     widget-tree render path — the only page not on immediate-mode — SEGFAULTED at
1037     launch on the pre-6u baseline (raw-pointer one-time section/label tree; the
1038     use-after-free class this rebuild exists to kill). A complete immediate-mode
1039     view for it existed in the file, never wired to the `AppPage` impl; 6u flips it
1040     (labels → `sec.text`, InfoBoxes advance the section cursor, menus linked into
1041     the clone sections like every other page). Pre-existing, deferred to the
1042     List/ScrollBox dissolution: the processes lists' inner wheel is dead; the page
1043     scrollbar's right half sits in the compositor's 8px edge-resize zone.
1044   - **6v — settings' List/ScrollBox dissolved (all five lists). DONE (A/B render
1045     dumps on processes/packages/radios: rect streams byte-identical minus one
1046     duplicated pair per list, see below; live-verified — per-list wheel, page-scroll
1047     fallback, scrollbar track-jump that sticks, focus tint, package row click →
1048     selection + info fetch, scroll state surviving watcher rebuilds).** Every
1049     settings list was a pure scroll frame (`List` with `columns: None`; the pages
1050     draw the rows), so the recipe is fonts' 6q `ScrollRegion` ported app-side
1051     (`cce-settings/src/scroll_region.rs`) with the List-flavored visuals (1px
1052     focus/hover-tinted rounded border + inset bg) and the List-mirroring API the
1053     pages already used. Routing: `AppPage` grows `extra_dispatch_roots` — the
1054     `InteractiveListItem` rows dispatch directly as propagate roots (`Adapted`'s
1055     press/wheel hit-gate makes misses fall through, so root order is immaterial) —
1056     plus `handle_mouse_wheel` (after widget dispatch, before the manual page-scroll
1057     fallback: the legacy "inner ScrollBoxes take the wheel first" slot) and
1058     `handle_key_input` (hover/focus-scoped, before the page's scroll-key fallback);
1059     regions ride the existing pointer down/move/up hooks (audio's slider-drag slots).
1060     This FIXES the 6u-deferred dead inner wheel, and two latent visuals of the
1061     columns=None List path: `List::extra_quads`' early return never removed the
1062     ScrollBox bg quad, so `render_widget` emitted the border+bg pair TWICE (plain
1063     bg through the solid-border branch + `all_rounded_quads`) — the 6p double-
1064     composite class — with the scrollbar track/thumb sandwiched UNDER the second
1065     translucent bg wash. Single-drawn now; the inner scrollbars are visible for the
1066     first time. Only other A/B delta: item-label clip bounds relax by ScrollBox's
1067     4px text inset (rows are fully-visible-culled, nothing renders in that band).
1068     Replication trap for other apps: the region's `focused` is a local bool
1069     (press-inside sets, press-miss clears) standing in for the global
1070     `focus::set_focused(scroll_box)` — ctrl-nav can no longer land on a list, and
1071     the focused border tint shows through the translucent bg as a green wash
1072     (legacy did this too, darker under its doubled bg). Spot-check PASSED
1073     (2026-07-13, via ccectl held-drag injection, cce 881c2b1): held thumb drag
1074     scrolls and tracks mid-drag; arrow scrolls one row; PageUp/PageDown page both
1075     directions over a hovered list. The green focused wash appeared as documented.
1076   - **6w — settings' SectionContainer dissolved; cce-system-settings is
1077     embedded-base-FREE. DONE (A/B render dumps: all nine pages byte-identical
1078     modulo live data — the sections never painted; live-verified — notifications
1079     spinbox + menu open/select with in-frame occlusion, audio spinbox round trip
1080     through pactl and the watcher, processes filter-box click-to-focus, services
1081     list wheel).** The per-rebuild section clones were pure event/focus plumbing:
1082     propagate roots whose `container` children were the pages' widgets, plus the
1083     ctrl-nav focus targets. `AppPage::section_widgets()` (one widget-pointer group
1084     per section, old count/order) replaces `get_section_containers` +
1085     `link_children` + `clear_children`; the widgets dispatch directly as propagate
1086     roots flattened in the legacy order, and section-level keyboard focus is an
1087     app-side index, single-slot with the global widget focus exactly as when both
1088     lived in `FOCUSED_WIDGET` (entry → section 0; ctrl+j/k cycle; ctrl+i descends
1089     to the section's first widget — the legacy walk went through the
1090     header/container intermediates; ctrl+u ascends from a widget to its section;
1091     a focus-taking click and page switches drop the highlight). Also killed a
1092     latent use-after-free of exactly the class this rebuild targets: the focused
1093     section clone was dropped and reallocated EVERY rebuild while the global
1094     focus pointer kept aiming at it — it survived only because same-size Vec
1095     reallocation tends to reuse the freed block. Ctrl-nav spot-check PASSED
1096     (2026-07-13, nested rig) — and it took two fixes to get there. (1) cce 114ef89:
1097     injected key-down/key-up now updates an injected xkb mask and pushes a
1098     modifiers event (OR'd over the device state), so ctrl/shift/alt/super combos
1099     land like hardware. (2) The check then caught a REAL cce-ui bug (ec511f4):
1100     handle_key preferred event.utf8, which xkb control-transforms while Ctrl is
1101     held (ctrl+j arrived as Character("\n")) — settings' ctrl-nav could never
1102     have fired from real hardware either. With the keysym preferred under Ctrl,
1103     ctrl+j/k cycle the section highlight and ctrl+i/u descend/ascend
1104     (audio-page border diffs). ~~NB accounts/storage take `_sec_focused` and
1105     render no highlight — ctrl-nav is invisible on those two pages.~~ FIXED
1106     2026-08-13 (cce-system-interface@`573bf43`): both pages now thread
1107     `sec_focused` into their `view()` and pass it per section instead of a
1108     hardcoded `false`. Live-verified — storage cycles the highlight
1109     Local Storage → Memory on successive ctrl+j, accounts highlights its single
1110     section (42,513 px changed in a rectangle-outline distribution: dense at the
1111     well's top and bottom edges, constant down the sides). All 14 pages now
1112     consume `sec_focused`; none takes it as `_sec_focused`.
1113     Re-verified on the LIVE session (2026-07-13, after the compositor restarted
1114     onto the 114ef89 binary — the restart was the only reason this had been
1115     nested-rig-only): ctrl+j → j → k and ctrl+i → u on the audio page, both
1116     round-trips returning pixel-identical frames (0 AE diff), highlight
1117     visually confirmed cycling Output→Input. Nothing pending on ctrl-nav.
1118   - **6x — data-editor + text-editor off the engine popup path; the render-only xdg
1119     popup machinery is DELETED. DONE (live-verified: text-editor File menu open +
1120     item click; data-editor recent-files menu → config.kdl load, tree context menu
1121     with occlusion + Copy Key through the clipboard; settings page dropdown +
1122     page switch unaffected after losing its gate).** Both apps now collect their
1123     ui_context-registered popovers via `PopoverCollector` and emit them last in
1124     the display list (data-editor appends the global context menu too), labels
1125     bounded to the overlay rect — the 6l/6t recipe; registration is
1126     ui_context-only. With the last consumers across, the engine sheds the whole
1127     popup path: `ActivePopup` (wgpu surface + viewport + vertex buffer per
1128     popover), the xdg positioner/spawn/despawn block in the event loop, the popup
1129     render pass, the popup-surface pointer-coordinate translation, the
1130     `PopupHandler` + `delegate_xdg_popup` plumbing, and the
1131     `Application::render_popovers` + `draws_own_popovers` hooks (settings'
1132     override removed; the context-menu dl-text occlusion rect is now
1133     unconditional). Every popover in the workspace is app-drawn, in-frame, where
1134     it hit-tests — the 6s below-window-popover class of positioner bugs is
1135     unrepresentable. NOTE: the compositor-side dismissal in `PopupHandler::done`
1136     (unfocus popovers + hide context menu when the popup was dismissed) went with
1137     it — in-frame apps already own dismissal (press-outside), same as
1138     fonts/settings. The global `widget::popovers` registry is now write-only
1139     (apps still clear/register into it) — delete it with the legacy paths.
1140     Drive-by: cce-designer had not compiled since 6k (direct `WgpuAdapter::new`
1141     call missing the new `load_system_fonts` bool) — fixed.
1142   - **6y — files' SplitBoxes + BrowseContainer/NetworkContainer dissolved. DONE
1143     (browse page A/B: zero >8%-amplitude pixel diffs; network page's only diff is
1144     a removed paint bug, see below; live-verified — row select, double-click
1145     navigation, view-dropdown page switch, divider hover tint via hover-on/off
1146     crop diff, preview populate).** The split reduces to an app-owned `SplitPane`
1147     (frac + divider drag/hover + divider quad — the SplitBox two-child horizontal
1148     math verbatim); the pane containers were pure layout shims whose child copies
1149     the pages have always re-rendered on top (the Phase 0 double-paint), so the
1150     window assembly now emits only the divider quad and the preview pane
1151     (`render_widget` at the right pane rect, text clamped to the pane like the
1152     legacy SplitBox bounds clamp). Killed on the way: the left pane's under-copy
1153     double-compositing every translucent quad, including the NetworkContainer's
1154     full-width breadcrumb-copy strip that visibly leaked behind the graph page's
1155     top bar — the exact class the Phase 0 stopgap patched for Browse only.
1156     Verification trap for the log: `wlrctl` pointer warps land as Enter WITHOUT
1157     Motion — nudge (`move 2 2`) after warping or app hover state never updates
1158     (cost an hour chasing a "broken" divider tint that was fine). Held divider
1159     drag spot-check PASSED (2026-07-13, ccectl pointer-press/release): frac
1160     tracks the held drag, panes re-lay out, window stays put. The Enter-without-
1161     Motion trap turned out to be a REAL RUNNER GAP, not just an injection quirk:
1162     the backend's Enter arm set the cursor icon but never fed the enter position
1163     to `handle_pointer_move`, so a press straight after crossing into the window
1164     hit the movable-backplate check with stale hover and moved the WINDOW instead
1165     of grabbing the divider. Fixed in cce-ui 9e23229 (Enter now routes like
1166     Motion); re-verified no-nudge cross+press drags the divider.
1167   - **6z — files' List dissolved; cce-files is embedded-base-FREE. DONE
1168     (live-verified: row click select with preview/details update, double-click
1169     navigation, breadcrumb navigation, wheel scroll with selection retained,
1170     hover tint, item count; 3 new RowList unit tests).** The column-mode List
1171     flavor ports verbatim to the app-owned `RowList`
1172     (`cce-files/src/row_list.rs`): column_bounds (Flex/Absolute/RightOffset),
1173     row virtualization + hit math, 400ms double-click, the scrollbar, and the
1174     cell layout (icon column, primary/secondary tints, char-estimate truncation,
1175     viewport-inset clip bounds). The in-List search box became a standalone
1176     BrowseState TextBox; the open/close shortcuts and SearchChanged plumbing
1177     move app-side (close returns the empty SearchChanged the legacy
1178     just_changed flag produced). Two fixes: the 6v sandwich again
1179     (render_widget emitted scrollbar + row overlays UNDER the rounded bg), and
1180     a NEW DISSOLUTION TRAP for the checklist — a dissolved widget no longer
1181     blocks window drags via its registered `blocks_backplate_drag`, so
1182     `is_movable_backplate_at` must veto its rect app-side; without it every row
1183     press became a compositor window-move grab and the app saw only the release
1184     (looked exactly like a dead click). Kept legacy: the view's
1185     scroll-into-view snaps the wheel back while the selected row would leave
1186     the viewport. Search typing not headlessly drivable — user spot-check
1187     pending.
1188   - **6aa — data-editor's SplitBox dissolved. DONE (live-verified: empty-state
1189     pixels identical modulo the cursor sprite; config load via the File menu,
1190     tree wheel, tree row select with the inline value editor, divider hover tint
1191     via crop diff).** The 6y `SplitPane` recipe on the scene-walk app: panes
1192     positioned directly from the pane rects and walked as separate roots, the
1193     divider quad emitted in the splitter's old walk slot. Removes the app's last
1194     raw-pointer child container and retires the Phase 2b
1195     `scene::bridge::layout_subtree` showcase that drove the split (the layout
1196     engine's app-facing debut now waits for the routed-events/scene-layout
1197     phase). TreeList intentionally NOT dissolved: at ~1.8k lines of tree
1198     expansion/inline-edit/annotation logic it is a self-contained walked widget
1199     (renders_own_subtree) whose internal ScrollBox never leaks — porting it
1200     app-side buys no hazard reduction; it converts to narrow traits with the
1201     `Element` deletion instead. Held divider drag spot-check PASSED (2026-07-13,
1202     ccectl injection): press at the divider grabbed it (app log: press 482 →
1203     release 332), split tracked the full 150px, and the grab stole keyboard
1204     focus from the raw editor exactly as designed.
1205   - **6ab — cce-text-editor on routed events + scene-solver layout: the FIRST app
1206     fully on the target architecture, end to end. DONE (live-verified: menu-open
1207     pixels match the pre-change capture at 0.13% = cursor sprite; menu item
1208     click through the routed release; editor click focus; the
1209     focused-border-after-outside-click oddity reproduced byte-identically on
1210     the stashed pre-change binary — pre-existing).** Layout: the frame is a
1211     plain `Arena<LayoutBox>` tree solved by `scene::layout::compute_layout` —
1212     no Element in the loop, the solver used directly by the app (stretched
1213     column [top bar fixed 42 / content grow padded 10 / status fixed 30], menu
1214     a fixed leaf, editor growing) — and it reproduces the legacy hand-math
1215     rects exactly, clamps included. Events: each handler builds one `Event` and
1216     routes it through `UiContext::propagate_event` per root; the router owns
1217     press hit-gating, Enter/Leave synthesis, drag-target recording, and
1218     KeyInput-to-focused delivery, leaving the app take_change plumbing and
1219     app-level shortcuts only. This is the shape the remaining widget-tree apps
1220     (data-editor foremost) migrate toward, and the pattern the demo
1221     (`cce-ui/src/main.rs`) should teach.
1222   - **6ac — data-editor on routed events + scene-solver layout; the
1223     routed-events/scene-layout item is COMPLETE. DONE (live-verified:
1224     empty-state pixels match 6aa at 0.06% (cursor + caret); config load through
1225     the routed menu; tree row select → inline choice editor + raw-span sync;
1226     choice popover open/select; tree wheel).** Layout: chrome + panes are one
1227     solver tree (stretched column [menubar fixed 42 + File-menu leaf / content
1228     row grow with pad 10, gap = divider width, panes growing by the SplitPane
1229     fractions / statusbar fixed 30]) reproducing the 6aa hand rects exactly;
1230     the SplitPane keeps divider input state, its frame derived from the solved
1231     panes; the inline value editors stay hand-positioned (they float over tree
1232     rows). Events: all 30 direct dispatch call sites route one `Event` through
1233     `propagate_event` per root with the plumbing intact. THE ROUTING TRAP worth
1234     remembering: the router delivers KeyInput to the ctx-focused widget FIRST
1235     on every propagate call, so a legacy non-short-circuited keyboard chain
1236     would deliver a typed key to the focused widget once per call site
1237     (N-time character insertion) — short-circuit the chain on first handled,
1238     and re-gate any plumbing that keyed off WHICH call returned true onto
1239     widget state instead (Enter→ApplyValue now checks the value editor was
1240     editing when the key arrived). Keyboard flows not headlessly drivable —
1241     user spot-check (typing, Enter-apply, tree search, keybind recording).
1242   - **6ad — the demo rewritten as `DemoApp`, the reference `Application`. DONE
1243     (live-verified: button click, toggle with app-state re-assert, slider wheel
1244     nudge, dropdown popover open/select with the occlusion clamp visibly
1245     working, all through routed dispatch).** `src/main.rs` had never been the
1246     "reference Application" the docs claimed — it was a 1925-line fossil
1247     predating the engine entirely: a raw Wayland client with its own
1248     CompositorHandler/SeatHandler impls, its own wgpu state, and hand-copied
1249     tessellators. Replaced by ~450 teaching-commented lines on the full target
1250     architecture: display-list frame + display_list_text, solver-driven layout
1251     (with `shrink` demonstrated for min-width rows), routed events with the
1252     KeyInput short-circuit rule and state-gated `drain_widget_changes`
1253     plumbing, ui_context-only popover registration with the in-frame draw, the
1254     dissolved-root window plate, and `drag_allowed_at` window dragging. API
1255     footgun surfaced for the log: `Slider::set_value` takes the NORMALIZED
1256     0..1 value (`with_range` only scales `get_scaled_value`) — passing a
1257     ranged value silently clamps to 1.0.
1258   - **6ae — legacy deletion, part 1: the global `widget::popovers` registry is
1259     DELETED. DONE (write-only since 6x; the mod, its `render_widget` write, and
1260     the four apps' `clear()` calls are gone; settings' popover renders
1261     byte-identically after).** Part 1 also produced a CORRECTED precondition
1262     map for the rest of the deletion — the endgame list had been assuming "the
1263     last app is across," and it is not:
1264     - The `view*`/`text_items` paths CANNOT be deleted yet: SIX apps still
1265       implement them — cce-test-interface (2.1k), cce-authenticator (0.9k),
1266       cce-display-manager (1.5k), cce-mail (2.5k), cce-layout-interface
1267       (3.8k), cce-status-interface (4.2k). Each needs its own Phase-6-style
1268       migration (display-list flip at minimum; dissolutions as found).
1269       Suggested order: smallest/least-critical first (test-interface,
1270       authenticator — NOTE it may be the lock screen, verify carefully),
1271       status-interface last (layer-shell, always-running).
1272     - The per-widget text getters are additionally load-bearing for the walk's
1273       legacy branches (`renders_own_subtree`, container `text_labels`
1274       aggregation) and the migrated apps' hand-rolled window aggregates
1275       (settings' `collect_window_child`, files' assembly) — they go when those
1276       consumers move to `paint_self`-only trees.
1277     - `Element` + `Adapted` go last, after both of the above; TreeList
1278       converts to narrow traits then.
1279   - **6af — cce-test-interface across (1 of 6). DONE (A/B: zero >8%-amplitude
1280     pixel diffs; live-verified — full gallery render, page-dropdown popover
1281     in-frame, page switch updating the MenuBar title and the status prim).**
1282     The recipe for the remaining five: move the `view()` +
1283     `view_rounded_quads()` bodies into `display_list()` in the engine wrapper's
1284     order (ROUNDED first, then plain, then popover rects — the wrapper reversed
1285     the intuitive order and apps' visuals bake it in), and re-emit the
1286     `rebuild_text_items` assembly as `Prim::Text` built fresh per frame,
1287     deleting the cache + its invalidation call sites + the app FontSystem +
1288     any `text_areas` override (its extra areas become prims).
1289     `custom_vertices` stays. Remaining queue: authenticator (verify carefully —
1290     lock screen), display-manager, email, layout-interface, status-interface.
1291   - **6ag — cce-authenticator across (2 of 6); the flip FIXED runtime-invisible
1292     text. DONE (live-verified --standalone + CCE_AUTH_SIMULATE: full dialog text
1293     renders, zero font-ID warnings — was hundreds per frame — fingerprint-scan
1294     click drives the animated glow + hint).** It is an xdg-toplevel polkit auth
1295     dialog (NOT a session lock — safe to run; needs `--standalone` +
1296     `CCE_AUTH_SIMULATE=1` to show a window without a live polkit request, and it
1297     auto-exits ~3s in simulate mode so capture fast). The single `view()` (both
1298     geometry and text) → `display_list()`; the `text_items` assembly → prims;
1299     app FontSystem / make_text_buffer / text_items field+getter deleted. The
1300     6e face-ID class again, and worse here — the app used
1301     `create_font_system_with_system_fonts()`, so EVERY label was invisible at
1302     runtime; the migration is the fix. General lesson reinforced: any
1303     legacy-path app with its own FontSystem is a latent-invisible-text
1304     candidate — don't trust its baseline capture.
1305   - **6ah — cce-display-manager across (3 of 6). DONE (A/B: 131px AE = 0.004%
1306     caret blink, zero >8%-amplitude diffs; live-verified --greeter renders
1307     identically).** The greetd login greeter — run the GUI standalone with
1308     `--greeter` (daemon mode needs root/greetd). Bundled fonts, byte-match flip.
1309     view() + view_rounded_quads() → display_list() (rounded then plain, both
1310     skipping the card); rebuild_text_items → prims; FontSystem / make_text_buffer
1311     / info_buffer / text_items machinery deleted. The card — a soft radial-glow
1312     blob drawn with the circular clip disabled — STAYS in custom_vertices
1313     (escape-hatch layer, on top, untouched). REPO HAZARD hit here: these crates
1314     live under ~/Dropbox, and a Dropbox sync reverted the edited main.rs to disk
1315     AFTER build+test but BEFORE the commit landed (a harness-interrupted commit,
1316     exit 144, left the tree clean at the old file) — had to re-apply and commit
1317     immediately. Verify `git log`/`grep display_list` actually stuck before
1318     moving on.
1319   - **6ai — cce-mail across (4 of 6); the flip FIXED invisible list/detail text.
1320     DONE (live-verified: inbox list of 3 emails, detail placeholder, and on click
1321     the full detail view — subject/From/To/Date/body + Reply/Delete/Mark-Unread
1322     toolbar).** view() body (all plain quads) → display_list() via a small
1323     `__EmailQuadSink` shim mapping the ported quads.push/extend to PaintCtx::quad;
1324     rebuild_text_items → emit_text_prims(&mut pc). The 6e face-ID class AGAIN
1325     (create_font_system_with_system_fonts): the list rows, detail metadata/body,
1326     and placeholder were all invisible — only the paginator tabs showed. Fix:
1327     switch the app FontSystem to bundled create_font_system() (KEPT for the
1328     TextBoxes' prepare_text measurement — now matching the engine render FS) and
1329     render via prims. Note the AE-vs-baseline metric is misleading for these
1330     invisible-text fixes (tiny % of dark-on-dark pixels change) — judge by
1331     whether text APPEARS, not by AE.
1332   - **6aj — cce-layout-interface across (5 of 6); needed + consumes the new
1333     boxed-text prim; the flip FIXED invisible text. DONE (live-verified: the
1334     whole properties/geometry/alignment/add-elements UI renders where the
1335     baseline showed nothing; 104→0 font-ID warnings).** This app forced the
1336     boxed-text feature (previous commit): its canvas Element::Text boxes need
1337     word-wrap + h/v alignment, unrepresentable as a plain Text prim. view() +
1338     view_vectors() → display_list() (a __LayoutQuadSink shim for the
1339     quads.push/extend body; vectors → PaintCtx::vector); rebuild_text_items →
1340     text-prim tuples carrying an optional TextLayout, emitted via text_with /
1341     text_boxed. Same 6e face-ID class (create_font_system_with_system_fonts);
1342     fixed by bundled FS + prims. VERIFY GAP: the canvas boxed prim itself
1343     (wrap/align on the page) could not be pinned headlessly — a placed text box
1344     defaults to page x=40, behind the ~540px properties panel, and there is no
1345     virtual keyboard to type a clear coordinate; it renders via the identical
1346     dl_text_items path as the confirmed-visible text. User spot-check: place +
1347     drag a text box onto open page, confirm wrap + alignment.
1348   - **6ak — cce-status-interface across (6 of 6, LAST legacy-path app). DONE
1349     (live-verified: an isolated `--module clock` instance renders "Friday, July
1350     10, 2026 … PM" with its rounded background pill through the display list).**
1351     The persistent layer-shell bar. view() + view_rounded_quads() bodies move
1352     into display_list() (rounded boxes, then status-bar bg / module rects /
1353     separators as prims, in the wrapper's ROUNDED-then-plain order); module text
1354     becomes fresh Prim::Text each frame. overlay_quads() stays a separate on-top
1355     pass (tray hover highlights over text). The status bar's OWN text is never
1356     set in this app (get_text_items was a no-op), so its text_items()/custom
1357     text_areas() overrides are deleted; self.font_system is kept only for the
1358     modules' measure-then-position shaping. Mechanism: modules build a
1359     StyledLabel to measure width, then emit via the new StyledLabel::into_prim
1360     (cce-ui 005a53f) through a draw_label helper — the vertical bar's centered
1361     per-char text rides the boxed-text TextLayout. Bundled create_font_system(),
1362     so NO 6e invisible-text hazard — a byte-match flip. Full multi-module A/B was
1363     avoided (the no-arg binary is a launcher daemon that would spawn a bar
1364     conflicting with the user's live one); a single isolated module segment was
1365     the test surface.
1366   - **All six legacy-path apps are now across.** The `view*`/`text_items`
1367     Application-trait deletion is unblocked.
1368   - **6al — legacy `view*`/`text_items`/`text_areas` DELETED. DONE (16 client
1369     apps compile; 180 tests pass; settings live-verified).** With every app on
1370     `display_list()`, the legacy geometry/text trait surface was dead code:
1371     removed `view` / `view_rounded_quads` / `view_vectors` / `text_items` and the
1372     default `text_areas` mapping from the `Application` trait; in `render()`,
1373     dropped the `quads`/`rounded_quads`/`vectors` collection + the tuple-wrapping
1374     `None =>` branch (so `dl = display_list().unwrap_or_else(empty)`) and the
1375     `text_areas()` call (an empty `areas` vec the dl-text loop fills). KEPT:
1376     `overlay_quads` (status-bar tray hover), `custom_vertices` (display-manager
1377     card, test-interface gallery), `display_list` / `display_list_text`. Pure
1378     dead-path removal — every implementor already took the `Some(dl)`/empty-text
1379     arms. (cce-designer + cce-cloud drive `WgpuAdapter` directly, never implement
1380     `Application`, so they're untouched.)
1381   - **6am — dead widgets SplitBox / MultiControl / KeybindsControl DELETED.
1382     DONE (cce-ui + all 18 apps compile; 175 tests pass; settings A/B AE=0).**
1383     Phase-6 dissolutions orphaned all three — no client app (nor live cce-ui
1384     path) constructs them. Removed the files, re-exports, the two
1385     `get_*_sub_widget_info` helpers + their `render_widget`/`window_runner`
1386     downcast blocks (fire only when the widget IS that type — none is, so
1387     behavior-preserving), the dead `name == "MultiControl"/"KeybindsControl"`
1388     span-full terms, and `render_widget`'s now-redundant `let mut corners = …`
1389     shadows. Each carried the full legacy text-getter aggregation, so this trims
1390     a big slice of the getter consumer graph.
1391   - **Per-widget text getters — GATED, not yet deletable.** Investigation
1392     (6am) established that every getter still has a LIVE consumer, so none can
1393     be removed until those move to `paint_self`/prims first:
1394     - `get_text_items` → **cce-designer** (custom `WgpuAdapter` render loop in
1395       `render.rs`: text-buffer cache + curved-menu-text special cases). Designer
1396       + cce-cloud never implement `Application` — they were skipped by all of
1397       Phase 6 and still drive `WgpuAdapter` directly.
1398     - ~~`text_labels_with_font_and_bounds` + `widget_font` → `layout::render_widget`~~
1399       **DONE (6an).** render_widget now sources its text from the scene walk
1400       (`paint_root_into` → keep only `Prim::Text` → emit onto the RenderTarget),
1401       keeping its own geometry path; the prim carries the per-widget font+clip so
1402       the getters are gone from here. Settings A/B AE=0 on Accounts + (stash-based)
1403       the spinbox-heavy Audio page. The last difference vs the getter is
1404       widget_font→text_font, which coincides except for a custom-font TextBox.
1405     - ~~`.text_labels()` / `.text_labels_with_bounds()` → **four hand-aggregate
1406       apps** (email, authenticator, display-manager, layout-interface) whose
1407       `display_list()` emits widget text by calling the getter per widget.~~
1408       **DONE (6ao)** — see below.
1409     - Note: the "orphaned" containers Layer / Page are NOT deletable — Layer is
1410       the embedded base of the live Plate/Page; Page is embedded by the live
1411       Paginator (transitive liveness through inheritance, not direct app use).
1412   - **6an — render_widget off the getters (see above). DONE (all 18 apps
1413     compile; 176 tests; settings A/B AE=0 on Accounts + Audio).** One of the
1414     three getter-consumer classes cleared.
1415   - **6ao — the four hand-aggregate apps off the getters. DONE (176 tests;
1416     all four A/B-verified live).** New `scene::painter::append_widget_text(ui,
1417     &dyn Element, &mut PaintCtx)`: walks the subtree and appends only its
1418     `Prim::Text` items — per-widget content font, walk clip composed into prim
1419     bounds (the 6an recipe as a reusable helper). Every per-widget
1420     `.text_labels()`/`.text_labels_with_bounds()` call in email, authenticator,
1421     display-manager and layout-interface replaced with it. A/B results:
1422     email + layout-interface byte-identical; authenticator + display-manager
1423     identical except widget-owned labels now render in the widgets' configured
1424     control font (legacy aggregates dropped the font to `None` — the same
1425     widget_font→text_font delta as 6an, here visible because these apps
1426     configure a monospace control font).
1427     - **Display-manager UAF found + fixed:** its `new()` linked the ui_context
1428       tree and captured `focused_widget` while the State was a stack local, so
1429       every registered pointer dangled after the move — the walk's child
1430       descent was the first render-path consumer to dereference them (abort on
1431       a garbage-length alloc); `propagate_event` and the `all_*` child
1432       aggregation read the same stale pointers all along. Fix: per-frame
1433       idempotent `relink_tree()` (register/link are id-keyed) + initial focus
1434       re-derived from the boxes' own focus flags. Also: its flat `widgets_iter`
1435       lists the card AND the card's children, so text moved to walking the TRUE
1436       roots (bg + root_container) — flat would double-emit — and `LoginCard`
1437       (a container with own, non-aggregating labels) got the Plate-style
1438       `paint_self` override for its two header labels.
1439     - Lesson for the getter deletion: an app whose tree is linked from `new()`
1440       by value is a dangling-registry candidate — audit any remaining
1441       `link_parent_child` calls made before the owning struct reaches its
1442       final address.
1443   - **6ap — cce-designer's render loop off the getters. DONE (default +
1444     circular-pane A/B pixel-identical; add_node label renders via the new
1445     path; full workspace builds).** The custom `WgpuAdapter` loop's two text
1446     sources (`get_text_items` widget-buffer fast path +
1447     `text_labels_with_font_and_bounds` fallback) became one walk: per
1448     non-menubar widget, `append_widget_text` → text prims, shaped app-side in
1449     `text_buffer_cache` with the same size*1.4 metrics the fallback always
1450     used. Per-widget special cases (plate-ancestor bounds, circular cull,
1451     network opacity, curved-ring feed) operate on prim fields unchanged. The
1452     curved-ring branch is unreachable today (menubars are skipped before its
1453     condition) — preserved verbatim, flagged for a future dead-code decision.
1454     **ALL app-side getter consumers are now gone.**
1455   - **6aq — walk getter-use consolidated to ONE fonted default; the three
1456     missed app consumers cleared. DONE (176 tests; settings audio AE=0;
1457     test-interface AE=5 cursor-level; data-editor loaded-tree AE=0; cloud
1458     fuzzel+json standalone AE=0; designer unchanged).** CORRECTION to 6ap's
1459     "all app-side consumers gone": three call sites had escaped the audit —
1460     settings' `renderer.rs` `collect_window_child` (outside render_widget),
1461     test-interface's gallery loop, and cce-cloud's `jl`/`fuzzel` labels (cloud
1462     drives WgpuAdapter directly, like pre-6ap designer, and json_layout was
1463     live only through it). All three now use walk-derived text. Engine side:
1464     the `renders_own_subtree` walk branch is just `paint_self` (TreeList +
1465     newly-flagged JsonLayout carry subtree-emitting overrides; descending
1466     JsonLayout would draw inactive pages and miss its checkbox side-labels);
1467     the default `paint_self` leaf drain moved from `text_labels()` to the
1468     FONTED getter — same labels every legacy tuple consumer served. Ramp got
1469     the own-labels `paint_self` (the Plate/LoginCard class: container own
1470     text vs the walk's aggregate rule). Traps recorded: a widget with a
1471     ui-tree parent must NOT also be walked as a top-level root (test-
1472     interface's page selector under the status bar double-drew ~10%
1473     brighter); cce-cloud launches reach the user's DAEMON via
1474     /run/user/UID/cce-cloud.socket — hold the socket aside to A/B a local
1475     standalone build.
1476   - **6ar — the per-widget text getters are DELETED from `Element`. DONE
1477     (176 tests; full workspace builds; nine apps A/B-verified — AE=0 or
1478     cursor/translucency/status-race noise only).** `text_labels` /
1479     `text_labels_with_bounds` / `text_labels_with_font_and_bounds` /
1480     `get_text_items` are gone from the trait, with Adapted's impls, the
1481     `Paint::text_items` hook, and every container aggregate (Backplate,
1482     Layer, Page, Plate, SectionContainer, ColorRamp, ControlPanel,
1483     JsonLayout, ButtonStrip). Every widget reaches the frame through
1484     `paint_self`. The deleted defaults survive as painter helpers with the
1485     labels passed in (`paint_legacy_leaf`, `fonted_leaf_labels`,
1486     `scroll_ancestor_text_bounds`, `base_control_label`); legacy leaves keep
1487     their label logic as inherent `own_labels()`; TreeList reads its concrete
1488     Adapted children via the now-pub(crate) `own_labels_with_font_and_bounds`;
1489     ControlPanel/ParametersBg/JsonLayout source dyn-children labels off the
1490     paint walk (ControlPanel re-applies its scroll shift + viewport clamp;
1491     List and ControlPanel are `renders_own_subtree` — walking into a legacy
1492     scroll frame desyncs text from geometry at scroll ≠ 0). The concrete
1493     `context_menu::text_labels()` global stays (inherent method, not the
1494     trait). ~125-method god-trait is now 4 methods lighter and text has ONE
1495     path: prims.
1496   - **6as (teardown, in progress).** Landed: (1) census of all 117 `Element`
1497     methods vs workspace-wide call sites — five were call-less and are
1498     DELETED (`Element::paint` — the hover-registration default nothing
1499     invoked — `as_geom_controller`/`as_spreadsheet_controller` & variants,
1500     `color_u8`, `is_layer`); (2) the seven tree context-menu actions
1501     (`copy_key`/`copy_value`/`delete_key`/`expand_node`/`collapse_node`/
1502     `expand_all_nodes`/`collapse_all_nodes`) are now transitional `Input`
1503     capability hooks with `Adapted` forwards (the 5k pattern), so the global
1504     context menu's `dyn Element` dispatch survives the TreeList conversion.
1505   - **TreeList → `Adapted<TreeList>` — DONE (the staged plan below executed
1506     verbatim; 176 tests; data-editor loaded-tree A/B AE=0 byte-identical incl.
1507     the focus wash; row select / context-menu Copy-Key-to-clipboard / search
1508     click-to-focus verified live).** Two new transitional hooks landed with
1509     it: `Input::tick_ctx` (EventCtx-carrying tick — the rename-commit focus
1510     re-target needs the routing ctx) and `Paint::paints_own_subtree` →
1511     `Element::renders_own_subtree` (the field widgets stay ctx-linked for
1512     event propagation, but the walk must not also descend — descending
1513     double-painted them and drew the CLOSED add-key popover box). The 5q
1514     `legacy_focus_highlight` trap struck again (the focused tree's teal wash).
1515     KNOWN-LATENT (pre-existing since 6ac, verified identical in the
1516     pre-conversion baseline): wheel-over-tree doesn't scroll and the Add-Key
1517     button doesn't open its popover — children-first propagation
1518     short-circuits on the hit child before the tree's own toggle/scroll
1519     logic runs. Fix belongs to the event-routing follow-up, not the widget.
1520     Original staged plan (executed):
1521     - `Layout`: `rect_assigned` caches the rect; the `set_rect` body
1522       (search box / add-key button / popover box / scroll box arrangement +
1523       `update_bounds`) moves to the assignment hook. No container children —
1524       the walk treats the adapter as a leaf, so `renders_own_subtree`
1525       becomes unnecessary.
1526     - `Paint`: `color`/`rounded_corners`/`corner_radius`/`solid_border`/
1527       `widget_font` port straight; geometry aggregates become ctx-less
1528       (TreeList's children are FIELDS — its `children(_ctx)` ignores the
1529       ctx already); subtree text rides the `serves_legacy_labels` hatch
1530       (`legacy_labels_with_font_and_bounds(rect, ctx)` = today's
1531       `subtree_fonted_labels`); `prepare_text`; popover via
1532       `Paint::popover`/`draw_popover`.
1533     - `Input`: the mouse/cursor/wheel/keyboard/tick bodies move into
1534       `on_event` arms with `ectx.ui` (the 5s ParametersBg pattern);
1535       drag via the Input drag hooks; `blocks_backplate_drag`; the seven
1536       tree hooks return their inherent bodies; focus semantics —
1537       `ctx.set_focused(self)` sites become `ectx.request_focus()` (the
1538       ADAPTER's pointer, not the inner). CAUTION: `mouse_input` registers
1539       the inline `edit_box` into the ctx TREE (`register_widget` +
1540       `link_ids(self_id, …)` + `set_parent`) — under the adapter, `self_id`
1541       must be the adapter's base id (`EventCtx::widget_addr` precedent).
1542     - Sweep: data-editor field → `Adapted<TreeList>` (Deref covers the
1543       concrete calls: `scroll_box.scroll_y`, `set_flat_keys`, `take_*`
1544       drains, `get_row_rect`, `select_and_show_key`, `focus_search`), one
1545       raw `*mut TreeList` cast → `as_ptr_mut()`. A/B: loaded tree, row
1546       click + inline rename (double-click), context menu Copy Key via
1547       wl-paste, search focus, add-key popover, wheel.
1548   - **Leaf sweep COMPLETE — every widget is on the narrow traits (6as).**
1549     Trackpad, KeybindRecorder, FontSelector, ColorSelector, Ramp, ColorRamp
1550     all converted (each A/B'd: gallery/DE/LI diffs = cursors, launch-phase
1551     animation, or AE=0). Notables: ParametersBg's typed color rows re-typed
1552     to `Adapted<ColorSelector>`; ColorSelector's in-file keyboard tests pass
1553     THROUGH the adapter; Ramp/ColorRamp take the TreeList shape
1554     (paints_own_subtree + tick_ctx + per-tick re-parenting of field widgets
1555     so their label fade blends against the adapter's color); and
1556     `Adapted::paint_self` gained a subtree TEXT PASS-THROUGH — a
1557     paints_own_subtree widget's Text prims forward verbatim with per-child
1558     fonts/bounds instead of being flattened to widget_font by the
1559     own-labels re-derivation (composites with mixed child fonts rendered in
1560     the default serif without it). `impl Element` now remains ONLY on: the
1561     containers (Layer/Container/Page/Plate/Backplate/ScrollBox/ScrollBar/
1562     List/ControlPanel/SectionContainer/JsonLayout/Menu/MenuBar-internals),
1563     app-local widgets, ContextMenu, and `Adapted` itself.
1564   - **Then: retire the `as_*_controller` pairs — SCOPE CORRECTED.** The
1565     earlier four-site estimate came from an over-filtered grep; the real
1566     surface is ~45 sites: cce-designer's HTTP-action/menu plumbing holds
1567     ~16 (including generic roster queries like "does ANY widget have an
1568     open menu" via `as_menu_controller()` over `Box<dyn Element>`), and
1569     `Switcher` implements MenuController by forwarding to its ACTIVE PANE
1570     through `as_menu_controller_mut()` on `dyn Element` — a live
1571     capability-dispatch system, not vestigial casts. Retirement needs a
1572     design decision first: either a standalone capability registry
1573     (`&dyn XController` handles registered beside the tree) or deferral to
1574     the Element deletion itself, where the designer's roster becomes
1575     concretely typed. Do NOT sweep it mechanically.
1576   - **Controller-capability decision RESOLVED (6aw): option 2 — defer to
1577     concrete typing; no registry.** A registry would be permanent
1578     infrastructure preserving the anonymous-widget pattern the rebuild
1579     exists to kill. Instead the queries die when their callers get
1580     retyped. First payoff immediately: `Switcher` — the largest holder
1581     (17 sites) — turned out to have ZERO constructors workspace-wide
1582     (settings dissolved its switcher in 6u) and is DELETED. Remaining
1583     map: cce-designer's roster retype (~16 sites, the bulk), the
1584     test-interface roster (3), one production site in
1585     `display/serialize.rs` (`serialize_widgets`' menu-state dump), and
1586     in-file tests that assert capabilities ride the adapter (die with
1587     `Element`). The designer retype is therefore the finale's next
1588     structural step, folded into the tree-machinery retype.
1589   - **Capability system DELETED (6aw, same session).** The designer's
1590     accessor block reaches each controller trait by `as_any` downcast to
1591     the roster index's known concrete type (note: `Adapted::as_any`
1592     exposes the INNER widget, so the downcast targets `MenuBar`/`Graph`/
1593     `ParametersBg`/`Spreadsheet`/`Breadcrumb` directly — the first build
1594     against `Adapted<W>` panicked at launch, caught by the live A/B);
1595     test-interface downcasts its Paginator; `serialize_widgets` tries
1596     the two MenuController implementors a roster can hold. With no
1597     callers left, Element's 12 `as_*` hooks, Adapted's forwards, and the
1598     14 `Input` capability hooks + their per-widget impls are all
1599     deleted. Element: 92 methods. A/B: designer/TI static diffs are the
1600     known noise shapes; designer HTTP `menu_click` (the dynamic-index
1601     path) verified live.
1602   - **Gallery containers went app-local (6as).** cce-test-interface's
1603     widget gallery was the last constructor of ControlPanel / Plate /
1604     SectionContainer / Backplate: it now owns `ti_widgets.rs` — a verbatim
1605     ControlPanel copy plus passive Plate/SectionContainer/Backplate
1606     lookalikes (each reproducing EVERY Element getter the render/event
1607     paths read: plate color/opacity/blur alpha-negation/corner radius/
1608     border/label_offset bg shift, the childless section-header row, the
1609     backplate bevel). A/B: launch-phase progress-bar animation only.
1610   - **CONTAINER TYPES DELETED (6as).** `Backplate`, `Plate`, `List`,
1611     `ControlPanel`, `SectionContainer` (+`SectionHeader`) removed from
1612     cce-ui outright — five files, the re-exports, the `List` branches in
1613     the scroll-ancestor text-bounds walks, and the then-dead
1614     `own_labels_with_font_and_bounds`. `ColumnWidth`/`ListColumn` moved to
1615     their only consumer (cce-files `row_list.rs`). TreeList's drag tests
1616     re-anchored on `drag_allowed_at`. 171 tests (5 died with their files);
1617     settings/DE A/Bs cursor-only / AE=0.
1618   - **Finale opened: dead-flag constant-fold sweep (6at).** With the
1619     plates gone, census round 2 found no zero-CALL methods but seven
1620     zero-OVERRIDE ones (only the trait default exists ⇒ they are
1621     constants). Folded and deleted: `capture_event` (the capture-phase
1622     branch in `propagate_event_impl` was unreachable), `is_active` (folded
1623     into the `highlight_color` default), `is_plate` (designer render.rs's
1624     whole `parent_plate_rect` text-clamp machinery was dead),
1625     `is_backplate` + `is_movable_backplate` (folded the drag walks —
1626     `UiContext::is_movable_backplate_at` had become constant-false and is
1627     DELETED; the `Application` default now just returns false — plus the
1628     backplate-parent theming/corner branches in MenuBar, StatusBar, and
1629     Dropdown's `backplate_ancestor` walk; MenuBar/StatusBar `corner_style`
1630     still reports the parent radius for children that read it through the
1631     parent pointer, but corners never round). `corner_radii`/`mark_dirty`
1632     also have zero overrides but carry real derived logic — they die with
1633     the retype, not by folding. Element: 112 → 107 methods. A/B: files
1634     AE=0, settings audio AE=0, designer diff = terminal behind the
1635     translucent window.
1636   - **Layer + Page DELETED (6au).** Per the 5r survey, no app ever put
1637     content in Paginator's pages — every consumer keys its own content
1638     on `selected_page()` — so the `Vec<Page>` was empty containers being
1639     arranged/registered/toggled/event-proxied for nothing. Their one
1640     visual (the page-area bg quad, page_color × page_opacity) moved into
1641     `Paint::paint`. With the stack gone Page had no constructor and
1642     Layer's only constructor was Page's base: both files deleted, plus
1643     `Element::is_page`, ScrollBar's Page-downcast write-back, and the
1644     dead 8/11 of `PageSelector` (now just selected_page /
1645     set_selected_page / sidebar_w; MenuBar's impls + `page_hidden` field
1646     went with it). A/B: email/LI/files AE=0; live LI tab click switches
1647     pages correctly. Census round 3 then found the deletion stranded two
1648     more zero-override methods — `check_out_of_bounds` and
1649     `transform_event_for_child` (Page was the only override of each) —
1650     folded and deleted in the follow-up. Element: 104 methods.
1651   - **ScrollBar DELETED, ScrollBox demoted off `Element` (6av).**
1652     ScrollBar's only consumer was cce-system-settings' page scrollbar
1653     (the dissolved Page subtree's survivor, evented through
1654     `propagate_event` and painted through `collect_window_child`) — the
1655     file moved there verbatim and the cce-ui type is gone. ScrollBox is
1656     never ctx-registered by either consumer (TreeList + the
1657     test-interface panel copy call it concretely), so its `Element`
1658     impl was dyn-dispatch ballast: now a plain struct whose former
1659     Element entry points survive as inherent methods with
1660     default-derived parity (the scrollbar-click focus claim became
1661     `focus::clear_focus()` — unfocusing the previous holder was its
1662     only observable effect). The painter/model scroll-ancestor text
1663     clamps folded to `None` (no tree parent can be a ScrollBox; none
1664     ever was at runtime). A/B: data-editor AE=0 plus live wheel +
1665     track-jump-scroll on a 100-key tree; settings diff = process-row
1666     churn; TI sub-threshold. Census round 4: only `corner_radii` +
1667     `mark_dirty` remain zero-override (real derived logic — they die
1668     with the retype, not by folding).
1669   - **Dead raw-widget sweep (6ax).** The raw-`Element` census after the
1670     capability deletion found four more zero-constructor widgets:
1671     Header, VBox, HBox (export-only) and Svg — which also rode Button
1672     as an `Option<Svg>` payload no caller ever set, so Button's icon
1673     branches were statically dead and went too. All deleted.
1674   - **Container DELETED (6ax part 2).** Settings' system-info
1675     `actions_row` was a dead field; dm's `root_container` was a
1676     transparent origin-anchored fan-out — dissolved into direct
1677     dispatch/walk roots (session list first for events, matching the
1678     reversed child order; card first for text, matching `children()`
1679     order). dm A/B: background-animation phase only (B-vs-B control
1680     differs full-frame), click behavior identical to baseline, 3/3
1681     interaction rounds alive on both builds. One UNREPRODUCED
1682     bogus-alloc seen once on the new build — dm's known latent
1683     stale-pointer signature, 0/3 repro on either binary; watch it.
1684   - **JsonLayoutWidget moved into cce-cloud (6ay).** The KDL/JSON
1685     launcher-layout host had one consumer; it cannot be demoted off
1686     `Element` (cloud feeds it to the paint walk as `&dyn Element`), so
1687     the file moved app-side verbatim — the impl dies with the machinery
1688     retype. `Justification` stayed in cce-ui (Button/files/settings
1689     share it), same path. A/B: content-identical renders (raw diff =
1690     the overlay's run-to-run spawn position + wallpaper bleed through
1691     the translucent plate); live checkbox click toggles.
1692   - **Canvas + Viewport3D moved into cce-designer (6ay part 2).** Both
1693     designer-only (preview.rs's "canvas" is its own internal type).
1694     A/B: AE=0. cce-ui's raw `impl Element` surface is now exactly ONE
1695     production type: ButtonStrip, the ctx-registered embed of
1696     MenuBar/Paginator — load-bearing in the tree, pinned to the
1697     machinery retype.
1698   - **App-local impls onto the narrow traits (6az) — COMPLETE:
1699     `Element` has exactly ONE production implementor (`Adapted<W>`).**
1700     Each remaining raw `impl Element` converted to
1701     `Layout`/`Paint`/`Input` + `Adapted<W>` ahead of the machinery
1702     retype; only test mocks still implement the trait directly.
1703     Done: cce-colors' ColorSlider (constructor returns the wrapper, so
1704     construction and direct-dispatch sites are untouched; A/B AE=0,
1705     click/wheel live-verified); settings' ScrollBar (now plain data —
1706     its raw-pointer parent/children fields, Drop, and unsafe Send/Sync
1707     had zero consumers; strip A/B AE=0 and cross-build byte-identical
1708     after an identical wheel + track-click sequence); cloud's Fuzzel
1709     (set_rect side effects → rect_assigned; overlay A/B identical, row
1710     click moves the selection live) and JsonLayoutWidget (the TreeList
1711     shape — paints_own_subtree + whole-subtree routing in on_event with
1712     gates_presses off + tick_ctx; the old overrides survive verbatim as
1713     inherent methods paint composes with a dummy ctx; render sites'
1714     all_quads/all_rounded_quads calls now resolve to the adapter's
1715     reverse bridges, same tuples; checkbox toggle live-verified) —
1716     cce-cloud is raw-impl-free. Designer's four (PassivePlate, Canvas,
1717     NodePalette, Viewport3D) followed — constructors return the wrapper
1718     so the roster's Box pushes and as_any downcasts stand; PassivePlate's
1719     full getter surface rides Paint, Viewport3D's wheel inertia moves to
1720     on_event/tick; static A/B = the status sliver only, cross-build
1721     captures after an identical HTTP circular-toggle + viewport wheel
1722     structurally identical — cce-designer is raw-impl-free. TI's four
1723     followed (part 6): ControlPanel keeps its aggregate overrides as
1724     inherent methods composed by a paints_own_subtree paint, its child
1725     arrangement in Layout::arrange_children (adapter as parent), and its
1726     routing in on_event; the raw `as *mut ControlPanel` casts became
1727     as_any downcasts; inline_label keeps the adapter's label machinery
1728     out of all four. Gallery + child-window A/Bs pixel-parity, cross-
1729     build sequences identical — cce-test-interface is raw-impl-free.
1730     The greeter's three followed (part 7: labels fold into paint off the
1731     laid-out rect; the display list's card-skip moves from base-pointer
1732     equality to id comparison; cursor-only diffs, 3/3 click rounds alive
1733     on both builds, after-state byte-identical). ButtonStrip closed the
1734     sweep (part 8): embedders hold `Adapted<ButtonStrip>`; two wrapper-
1735     shadowing collisions (`set_selected`, `take_click` — Element's bool
1736     signatures vs the model's `Option<usize>` ones) route through
1737     `inner_mut()`; presses stay ungated so a tab press lands under an
1738     open dropdown popover; LI's Paginator strip and email's MenuBar
1739     strip both cross-build byte-identical after identical clicks.
1740   - **Compiler census of the dyn surface (6ba, experiment reverted —
1741     the finding is the deliverable).** Hypothesis: with one implementor,
1742     many `Element` methods are only ever called on concrete
1743     `Adapted<W>` receivers and could move off the trait before the
1744     retype. Method: delete a method from the trait, keep it inherent on
1745     the wrapper (same signature — concrete sites resolve unchanged),
1746     `cargo check --workspace`; every error is a true dynamic-dispatch
1747     site. Verdict: **~91 of 92 methods fail — the trait IS the
1748     machinery surface; there is nothing to slim first.** Three consumer
1749     classes pin it: (1) cce-ui machinery (context.rs routing/focus/
1750     drag, the paint walk, layout.rs render paths — `T: Element`
1751     generics count: measure/preferred_height/set_row_rect live there —
1752     and core.rs context-menu actions on `dyn` targets); (2) container
1753     child aggregation over raw child pointers (menu/paginator/treelist
1754     in-tree, ControlPanel/JsonLayout app-side) touching the full
1755     paint+input getter surface; (3) the designer/test-interface roster
1756     broadcast loops (`Vec<Box<dyn Element>>`) calling nearly everything.
1757     NOTE: a `--workspace` check that fails in cce-ui never reaches the
1758     app crates — the first pass under-reported; the app rosters were
1759     where 4 of 5 "clean" candidates actually failed.
1760   - **The designer roster is concretely typed (6bb).** First retype
1761     slice, in census order: `Vec<Box<dyn Element>>` → one
1762     `Box<WidgetSlots>` of 17 named concrete fields (boxed whole so
1763     registered pointers stay stable while `State` moves). Const-indexed
1764     sites reach fields directly; the genuinely index-driven paths
1765     (draw order, focus cycling, broadcast dispatch, the `*_IDX`-keyed
1766     HTTP API) go through `get_dyn`/`get_dyn_mut`; serialize takes a
1767     per-slot `dyn_refs` view (`serialize_widgets` now takes
1768     `&[&dyn Element]`). Static A/B byte-identical; cross-build final
1769     after an identical menu/toggle/wheel/click sequence within 6 px.
1770     TI's two-mode gallery followed (part 2): 53 named concrete slots
1771     for the gallery, typed enums for the child window's runtime-variant
1772     slots, a `Roster` enum carrying whichever mode runs; the numeric
1773     indexes keep addressing slots through `get_dyn`. **No app stores
1774     widgets behind `Box<dyn Element>` anywhere — the roster phase is
1775     complete.** Cross-build A/Bs: gallery empty masks static and after
1776     an identical interaction sequence; Ramp child at noise level;
1777     non-Ramp child arms hit the pre-existing unconditional
1778     Ramp-downcast panic identically on both builds.
1779     Cloud's `JsonControl` closed the owned-storage class (part 3): the
1780     JSON-config controls become a typed enum over `Adapted<T>` replacing
1781     `JsonWidget`'s `Box<dyn Element>`; the label walk dropped its
1782     `as_ptr` round-trip unsafe for a plain reborrow. **No owned
1783     type-erased widget storage remains anywhere in the workspace.**
1784   - **The borrowed-pointer retype design (6bc, decided 2026-07-12).**
1785     The replacement handle is **`WidgetId`, resolved through the
1786     generational `WidgetTree` at every use**. Rationale: apps own
1787     widgets concretely (the 6bb rosters) and re-register pointers
1788     idempotently per frame from boxed storage, so the registry is the
1789     one place a raw pointer is refreshed before use; every *other*
1790     stored `*mut dyn Element` bypasses that guard and is exactly where
1791     the real UAFs happened (display-manager 6ao — dangling registry
1792     from a by-value `new()`; settings 6w — `FOCUSED_WIDGET` surviving a
1793     rebuild via same-size alloc reuse). Under the retype, raw pointers
1794     remain only (a) as the `WidgetTree` registry payload and (b) as
1795     transient same-frame values inside resolution helpers; every stored
1796     reference becomes a `WidgetId`, and a stale id resolves to `None`
1797     and is skipped — the UAF class becomes unrepresentable outside the
1798     registry itself. Public signatures taking `&dyn Element` stay
1799     (deriving the id from `base()` internally) so most call sites
1800     survive verbatim; direct field readers convert compiler-driven.
1801     Slices, each independently shippable and A/B-verifiable:
1802     1. **Focus (DONE 2026-07-12)** — both stores
1803        (`UiContext.focused_widget` AND the `core.rs` thread-local
1804        `FOCUSED_WIDGET`) → `Option<WidgetId>`, kept as two stores with
1805        their existing reader sets (merging them changes observable
1806        focus behavior — not this phase's job). Thread-local fns that
1807        must dispatch `unfocus`/`FocusOut` gained a ctx param (every
1808        dispatching call site had one in reach). `set_focused` /
1809        `set_focused_ptr` refresh the registry with the pointer they
1810        are handed, so focus on a not-yet-registered widget keeps
1811        working; `is_focused_addr` → `is_focused_id` (base-id
1812        comparison). Verified: 168 tests; identical click sequences on
1813        text-editor (AE≤6, empty 8% masks) and data-editor (focus
1814        click AE=0); settings spinbox click-to-focus live.
1815     2. **Popovers + context-menu target (DONE 2026-07-12)** —
1816        `active_popovers: Vec<WidgetId>`, `ContextMenuState.target:
1817        Option<WidgetId>`; every occlusion/render walk resolves through
1818        the tree; `register_popover` takes `&mut` and self-registers;
1819        `show_context_menu` derives + registers the target id;
1820        `context_menu::mouse_input` takes the resolving ctx;
1821        `is_coordinate_covered` id-keyed with `WidgetId(0)` as the
1822        no-base sentinel; `EventCtx::widget_addr` deleted (use
1823        `ectx.id`). Verified: A/B vs slice-1 captures byte-equivalent
1824        (incl. the TE File-menu popover); DE leaf context menu Copy
1825        Key → wl-paste; settings page-dropdown popover switches pages.
1826     3. **Container child storage (DONE 2026-07-12 — by deletion, not
1827        retype).** The stored-field census found the slated vecs were
1828        ballast: `ParametersBg.children` was never populated anywhere
1829        (deleted with its parent-tracking twin, the container hooks,
1830        eleven dead iteration blocks, `collect_child_quads`, and
1831        window_runner's quad-attribution loop); `ScrollBox.children`
1832        was write-only (deleted with TI's one push). What still stores
1833        raw pointers after this slice: the `WidgetTree` registry (by
1834        design) and TI's app-side `ControlPanel.children` — live,
1835        pointing into boxed slots, consumed in ctx-less paint/arrange
1836        paths, so it retypes when those paths gain ctx (the endgame).
1837        Paginator's `container_children` is a transient field ref, not
1838        storage. The `Element::children`/`parent`/`add_child`/
1839        `set_parent` *signatures* still traffic in pointers, but every
1840        value is transient and tree-resolved at call time — they die
1841        with the `Element` endgame rather than warranting a standalone
1842        signature sweep.
1843     4. **`propagate_event(event, root: WidgetId)` — DONE (2026-07-13,
1844        the plumbing retype).** The handle rule it establishes: raw
1845        `*mut dyn WidgetHost` may appear ONLY as (a) the `WidgetTree`
1846        registry payload — the one ownership bridge, written at
1847        registration; (b) a registration argument derived from a live
1848        `&mut` (`register_widget`, `set_focused_ptr`-class
1849        self-registration — never stored); (c) machinery-internal
1850        transients resolved from the registry inside one call.
1851        Everything else crossing an API boundary carries `WidgetId` and
1852        resolves through the generational tree at use — a stale id is a
1853        loud no-op (`eprintln` canary), never a deref. Executed: the
1854        router resolves the root at the top of `propagate_event`
1855        (`propagate_event_impl` keeps its private resolved-ptr param);
1856        ~470 app dispatch sites across 14 apps went `.as_ptr_mut()` →
1857        `.id()` (field paths regex-converted; `let ptr = …` pairs,
1858        ptr-Vec collections, and dyn-roster receivers hand-converted);
1859        settings' `section_widgets`/`extra_dispatch_roots`/
1860        `page_dispatch_roots` retyped to `Vec<WidgetId>` with the
1861        keyboard section-focus block on `focus::is_focused_id`/
1862        `set_focused_id`; dead `WidgetPtr` + caller-less
1863        `register_popover_ptr` deleted. THE CONTRACT THE RETYPE
1864        SURFACES: a dispatch root must be REGISTERED. Most apps get
1865        registration as a `render_widget`/`paint_root_into` side
1866        effect; the canary caught every gap live: TI (roster never
1867        registered — per-frame `register_roster()`), settings chrome
1868        (wiped by `rebuild_layout`'s `clear_hierarchy` — re-registered
1869        after the view pass) + per-page rows rebuilt on data refresh
1870        (`AppPage::register_extra_dispatch_roots(ctx)` runs before
1871        each dispatch — the same liveness cadence the ptr router had)
1872        + custom-drawn menus (fonts/system pages), email (hand
1873        aggregate, per-frame block), authenticator (same), dm's bg
1874        root, LI's word-processor box, cloud (registers at its five
1875        dispatch sites), designer (frame re-registration skipped
1876        INVISIBLE slots while the wheel loop dispatches the whole
1877        roster). BUG FOUND: cce-graph registered its graph under a
1878        hand-minted `NEXT_WIDGET_ID` instead of the widget's own base
1879        id — `graph.id()` was unresolvable all along (focus/drag
1880        lookups on it silently failed); registration now uses the
1881        real id and the synthetic field is gone. Verified: 165 cce-ui
1882        tests + full workspace suite; canary-silent pointer/click/
1883        wheel probes over 13 apps and all 10 settings pages; TI
1884        dropdown→Grid relayout lands end-to-end through the id
1885        router; designer /state serves; demo's four event loops shed
1886        their unsafe self-alias entirely.
1887        FOLLOW-UP (same day): **`children`/`parent` left the trait
1888        (~52→50)** — tree structure is read off `ctx.tree`
1889        (`parent_id`/`parent_ptr`/`child_ids`/`children_ptrs`); no
1890        trait method returns a raw pointer anymore. `Adapted`'s
1891        container branch was redundant: Paginator (the one
1892        `Layout::container_children` implementor) tree-links its strip
1893        every tick via `register_embedded_children`, so the tree
1894        serves the walks identically (worst case a first-frame gap
1895        before the first tick). Machinery consumers (propagate
1896        descent, `find_hovered_scrollable`, the paint walk,
1897        `all_quads`/`all_rounded_quads` defaults, designer's render
1898        walks, TI's flat-walk parent skip) now read the tree directly
1899        — sanctioned transient-pointer class. DEAD CODE FOUND: both
1900        `navigate_focus` twins deleted — `UiContext::navigate_focus`
1901        had zero callers, and `focus::navigate_focus` (settings'
1902        ctrl-nav preamble) resolved parent/children through a
1903        freshly-made EMPTY UiContext, so it always returned false
1904        (parent has no field-derived form; ctrl+i needed a focused
1905        Paginator, which is never focusable). Settings' real ctrl-nav
1906        is its own section machinery, unchanged. serialize.rs's
1907        dummy-ctx child lookup could only ever surface Paginator's
1908        strip — kept via the 6aw concrete downcast. Verified: 165
1909        tests + workspace suite; email tab strip paints and a Sent
1910        click lands through the tree link; TI page-selector crop
1911        byte-identical (no double-draw); designer /state + full panel
1912        text intact; settings/files/demo canary-silent.
1913        FOLLOW-UP: **`as_ptr`/`as_ptr_mut` left the trait (50→48).**
1914        They live on as inherent `Adapted<W>` methods (the
1915        registration-bridge class) — every concrete call site resolved
1916        unchanged; dyn/generic receivers became the plain casts the
1917        impl always was (`w as *mut (dyn WidgetHost + 'static)`;
1918        `impl_widget_base!` no longer generates them). Designer's
1919        `find_widget_index` now honestly takes the thin `*const ()`
1920        address its body always compared. Verified: 165 tests +
1921        workspace suite; designer capture pixel-identical to the
1922        prior slice's (empty 8% mask) + /state live; TI click probe
1923        canary-silent.
1924        **THE DESIGNER EVENT REDESIGN — DONE (2026-07-13), closing the
1925        6bd deferral.** The resolution is a design decision, not a
1926        router conversion: the designer's event layer IS its own
1927        z-ordered windowing system (custom hit shapes with
1928        circular-pane overrides, hardcoded pane z, pane-focus
1929        derivation, unfocus rituals) and deliberately delivers through
1930        `handle_event` directly — the UiContext router's
1931        hit-gating/descent/drag tracking cannot own that policy, and
1932        mixing the two would double-run drag state machines. What the
1933        redesign fixes is the CONFLATION the deferral named:
1934        `drag_widget` no longer doubles as app-mode-drag marker. A new
1935        `AppDrag` enum (NetworkResize/ParamResize/SpreadsheetResize,
1936        each variant carrying its whole gesture state — dir, start
1937        rect/width/height, start mouse) owns the floating-pane edge
1938        resizes; the three `is_resizing_*` flags and four
1939        `drag_start_*` scratch fields are deleted; `drag_widget` only
1940        ever names a widget drag driven through the slot's Input drag
1941        hooks; exactly one of the two is armed per press. Release
1942        teardown, cursor hiding, path-change resets, and the
1943        hover-loop gate all read the split state; the hot-path
1944        MouseInput debug `println!` died as a rider. Verified: A/B vs
1945        stashed baseline — canvas-click and menu-open frames
1946        byte-identical (AE=0), id-stripped /state identical, launch
1947        frame differs by cursor sprites only (same-binary control
1948        AE=0). Held-gesture spot-check (2026-07-13, ccectl held-drag
1949        injection): node drag PASSED (Camera [1,1]→[3,3] via /state),
1950        NetworkResize right-edge PASSED, ParamResize left-edge PASSED,
1951        and SpreadsheetResize top-edge PASSED (the earlier "pane opens
1952        collapsed" report was a misdiagnosis: the HTTP menu_click driving
1953        the toggle was a SILENT NO-OP — see below — so the pane was never
1954        open; a real click opens it at its correct 250px default and the
1955        held top-edge drag grows it). Found instead: the HTTP `menu_click`
1956        action only reaches the MenuBar widgets' index-matched dispatch in
1957        window.rs (LEFT_MENUBAR's View menu has items 0-4), while the
1958        pane-toggle items ("Show Spreadsheet Pane" etc.) live in the
1959        OTHER menu system — the button-param menu pane drained by
1960        `sync_parameters_to_project`'s label match; out-of-range item
1961        indices replied "success" while doing nothing, and the handler's
1962        synthesized cursor park (-9999) sheds stray hover-diff pixels
1963        that can masquerade as the click's effect. The menu dispatch
1964        existing TWICE (index-matched in window.rs vs label-matched in
1965        app.rs, with diverging item sets) is the trap that produced the
1966        misdiagnosis. Both API holes are since FIXED (cce-designer
1967        b4ac763 + afe0f78): `menu_click` validates its indices against
1968        the target menubar's real dropdowns (a non-menubar widget_idx
1969        used to PANIC the app) and echoes the clicked label; the label
1970        match is extracted to `State::execute_menu_action`, and the new
1971        `{"action":"menu_action","label":"Show Spreadsheet Pane"}` drives
1972        the menu-pane items directly (verified: pane toggles on at its
1973        exact rect via curl, toggles off byte-identical). The network pane's breadcrumb-strip "panel
1974        move" is CONFIRMED INERT: the press arms
1975        `drag_widget = NETWORK_PANEL_IDX` on `Adapted<PassivePlate>`
1976        ("no children and no events"), so DragUpdates land on a widget
1977        with no drag hooks — dead scaffolding, DELETED (cce-designer
1978        00e148a: both press arms, the always-false hit_menubar block,
1979        the NETWORK_PANEL_IDX drag-driver arms, and the two is_dragging
1980        layout read-backs; border/strip presses keep their real focus+
1981        consume behavior — regression-verified live). And the
1982        spot-check caught a real press-routing bug the A/B frames could
1983        not: VIEWPORT_IDX and PARAM_IDX shared the -4 z tier, and the
1984        stable sort's index tiebreak (3 < 6) sent EVERY press over the
1985        floating params pane to the viewport — slider/Float3 held drags
1986        were dead end-to-end (only the scrollbar, armed via its own
1987        press path, worked). Fixed in cce-designer 4b78aa5 (PARAM_IDX
1988        gets its own -3 tier above the viewport it floats over);
1989        live-verified Position X 2.50→10.00 via /state. The
1990        `draggable`/`is_dragging` trait methods still have this
1991        cascade + TI's ControlPanel as dyn consumers — they leave the
1992        trait with the CP endgame.
1993        **THE CONTROLPANEL ENDGAME — DONE (2026-07-13). The last
1994        stored child-pointer surface is gone, and `draggable`/
1995        `is_dragging` left the trait (49→47).** TI's ControlPanel is
1996        DISSOLVED to scroll chrome (~120 lines: ScrollBox + bg/border
1997        paint + drag hooks): its `Vec<*mut dyn>` children, label-
1998        matched arrangement, aggregate views, dummy-ctx event/tick/
1999        drag forwarding, and scroll-translated coordinates are all
2000        deleted. The app owns the panel now: `arrange_control_panel`
2001        lays the child slots at SCREEN coordinates (scroll offset
2002        applied at layout time, re-run every frame — the wheel moves
2003        content on the frame it repaints); `display_list` emits child
2004        geometry/text clamped to the panel viewport with the legacy
2005        partial-clip radius-zeroing and border-inset rules; children
2006        dispatch as ordinary routed roots. THE GATE THE DISSOLUTION
2007        REQUIRES: children at real rects are hit-testable even when
2008        clipped below the fold — `cp_gate` (panel rect ∪ open child
2009        popovers, the legacy `ControlPanel::hit`) gates the press
2010        pre-scan, release broadcast, and wheel; keys stay
2011        focused-path-only; the panel takes the wheel before its
2012        children (legacy scroll-frame order). BUG THE GATE FIXED
2013        LIVE: a fold-hidden StatusBar toggle stole the page-selector
2014        press. FOUND: the legacy panel double-drew its scrollbar
2015        (rounded AND plain aggregate views — the 6p/6v class);
2016        single-drawn now, thumb correctly dimmer. `draggable`/
2017        `is_dragging` became inherent `Adapted` reads; the two
2018        index-driven rosters (TI 53+5 slots incl. child-mode enum
2019        variants, designer 17) route them through generated per-slot
2020        matches; 14 UFCS test forms became dot calls. Verified live:
2021        Windows page A/B vs pre-dissolution baseline (static +
2022        scrolled + popover states — masks empty except the scrollbar
2023        single-draw strip), dropdown item select updates the Surface
2024        Info description end-to-end, spinbox +/- increments, wheel
2025        scrolls with content following, page switching intact both
2026        ways, Controls page unchanged, child mode alive; 28-target
2027        suite. Scrollbar thumb drag: spot-check PASSED (2026-07-13,
2028        ccectl pointer-press/release): the held thumb drag scrolls the
2029        Windows-page ControlPanel with content tracking the drag —
2030        Create Window/Width/Height/Window Type scroll off as the
2031        Window Elements/Border/Bevel tier comes in, thumb following.
2032     5. **window_runner render plumbing + remaining `as_ptr` sites —
2033        DONE (2026-07-13, the last slice).** The slice-3 census was
2034        right: window_runner held no pointer state (its one mention
2035        was a doc comment). The real residue was the paint walk's
2036        entry signatures — `paint_tree`/`paint_root_into` now take
2037        `&dyn WidgetHost` (the walk only reads; descent resolves
2038        children through the registry and derefs those transients
2039        internally), and `append_widget_text`'s lifetime-erasing
2040        transmute died with the ptr param it bridged to. Every caller
2041        simplified: the unsafe self-alias blocks that existed ONLY to
2042        mint `*mut` arguments (text-editor, graph, data-editor, the
2043        demo, fonts' walk calls) are plain shared borrows now;
2044        `render_widget`'s internal cast is gone. What still carries
2045        `*mut dyn WidgetHost`, all deliberate and documented: the
2046        WidgetTree registry payload + registration arguments (the
2047        ownership bridge), machinery-internal walk transients, the
2048        `Layout::arrange_children`/`container_children` hook
2049        signatures (narrow-trait, ctx-less by design), and
2050        EventCtx's transient host ptr. Verified: 28-target suite;
2051        text-editor/graph/data-editor/fonts live captures render
2052        fully, canary-silent. THE POINTER-RETYPE PROGRAM IS
2053        COMPLETE — no further slices are recorded.
2054        ~~then the `Element` + `Adapted` endgame (own design pass)~~
2055        — landed long since as the 6bd flip (`Element` deleted,
2056        `Adapted` survives as the one host wrapper).
2057     Stored-pointer state remaining after slices 1–3, all deliberate:
2058     the `WidgetTree` registry; TI's `ControlPanel.children`; and the
2059     tick-refreshed parent copies in MenuBar/StatusBar/Dropdown models
2060     (`parent_changed`/`tracked_parent` — written each tick by the
2061     re-parenting pattern with the live host pointer, read in ctx-less
2062     popover-direction/corner-radius math). Endgame option for the
2063     parent copies: snapshot the *data* read through them (parent rect,
2064     radius, is-Ramp flag) at re-parent time instead of the pointer —
2065     same refresh cadence, no deref of potentially-dead memory; watch
2066     the one-frame rect lag on resize if reads move to snapshots.
2067   - **The `Element` endgame design (6bd, decided 2026-07-12).** The
2068     endgame is a **trait replacement, not an app rewrite**. Grounding
2069     facts (consumer survey): direct per-method dispatch
2070     (`mouse_input`/`cursor_moved`/`keyboard_input`/wheel on concrete
2071     `Adapted` fields) exists in nearly every app — making routed
2072     events a prerequisite would gate the endgame on ~12 app
2073     migrations, so the direct-dispatch surface *stays on the new
2074     trait* and shrinks later as apps move to routed events at their
2075     own pace. Designer's index-driven roster broadcast needs only a
2076     small dyn set (unfocus, drag hooks, set_modifiers, z_index,
2077     hit_test, visibility, tick, prepare_text, focused, as_any). The
2078     legacy tuple getters' consumers are the `T: Element` generics in
2079     layout.rs (render_widget/Column/Section — settings, LI, files,
2080     colors render paths) and window_runner's tessellators
2081     (widget_vertices — designer/TI): generic, not dyn, so they can
2082     re-bound onto a narrower bound without touching the dyn surface.
2083     Target shape: a **`WidgetHost` trait of ~40 methods** implemented
2084     once by `Adapted<W>` (blanket over the narrow traits). `Adapted`
2085     does NOT die — it survives as the single host wrapper owning base
2086     state; what dies is the 92-method god-trait and its Option-base
2087     escape hatches (`base()` becomes a guaranteed `id()`/state
2088     access, killing the `WidgetId(0)` sentinel class). Phases, each
2089     shippable:
2090     1. **Capability actions → one enum method (DONE 2026-07-12)**:
2091        the 13 context-menu action methods (`cut_selection`…
2092        `copy_path`) became `context_action(ContextAction) -> bool`
2093        across Element, the Input hooks, and the Adapted forwards; the
2094        Input default keeps whole-value Cut/Copy/Paste through the
2095        value-string pair; TreeList's seven action bodies moved to
2096        inherent methods via an impl split (zero code movement);
2097        core.rs maps the menu strings (incl. the load-bearing "Cear")
2098        to enum values. Element is down to 79 methods. Live-verified:
2099        tree Copy Key/Copy Value/Collapse and TextBox Paste through
2100        the enum dispatch.
2101     2. **Tuple getters — DISSOLVED into the flip (measured
2102        2026-07-12).** The premise was wrong on two counts. (a) The
2103        dyn consumers are real: designer's render loop reads
2104        `extra_quads`/`extra_circles`/`extra_arcs`/`color` directly
2105        off its `&dyn` handle AND feeds it to the tessellators, and
2106        the paint walk's legacy-leaf branch reads
2107        `all_quads`/`all_rounded_quads`/`extra_*`/`widget_font` via
2108        dyn — the visual surface must ride the host trait object.
2109        (b) A separate `LegacyVisual` trait can't be reached from the
2110        existing trait object (no cross-trait-object casts; a
2111        supertrait split trips over `rect`/`color` defaults needing
2112        `base()`). The genuinely generic consumers (layout.rs
2113        render_widget/Column/Section, settings'
2114        `collect_window_child`, dm/cloud's local tessellator copies
2115        with concrete receivers) simply re-bind at the flip.
2116     3. **Tree-link methods — landed pre-flip after all (2026-07-12).**
2117        `children`/`parent` are core machinery walk methods (propagate,
2118        painter, navigate) — host-trait material, they stay.
2119        `clear_children` left in batch 2; **`set_parent`/`add_child`
2120        left in batch 4**: both are inherent `Adapted<W>` methods now
2121        (files' concrete sites resolve unchanged); the dyn callers
2122        were only three — `focus::link_parent_child`'s body (rewritten
2123        as the register + `tree.link` + `tree.set_parent` ops the pair
2124        always was), TI's page-selector/StatusBar roster pair (now one
2125        `link_parent_child` call), and TI ControlPanel's per-arrange
2126        dummy-ctx child re-parent (deleted — every effect was
2127        discarded with the dummy ctx, the same inert-ritual class as
2128        Ramp's tick re-parents). A/B: TI gallery (flat-walk `parent()`
2129        skip intact — no page-selector double-draw) + dm greeter, both
2130        empty 8% masks. Note Paginator both serves
2131        `container_children` AND `link_ids`-registers its strip — the
2132        ctx-less walks (popover_rect/prepare_text/render_popover) are
2133        why the field-derived form must stay.
2134     4. **The flip — DONE (2026-07-12): `Element` is deleted; the
2135        trait is `WidgetHost`.** Landed in two shippable halves:
2136        **(a) the base() guarantee** — `base`/`base_mut` return
2137        `&Widget`/`&mut Widget` (no Option), killing the escape hatch
2138        and the `WidgetId(0)` no-base sentinel class;
2139        `as_any`/`as_any_mut`/`as_ptr`/`as_ptr_mut` became required
2140        (their defaults manufactured DummyAny/null-DummyElement
2141        stand-ins nothing could use); every Option-handling call
2142        site collapsed 1:1 to direct reads (12 repos); the
2143        layout/arena/tree test mocks grew a base field via
2144        `impl_widget_base!`. **(b) the rename** — 634 word-boundary
2145        occurrences across 18 crates; the workspace compiled on the
2146        first pass. `ElementState` (input enum) keeps its name;
2147        cce-layout-interface's local `Element` document enum was
2148        already alias-insulated (`Element as UiElement`). Since the
2149        shrink batches had removed every non-blueprint method first,
2150        the rename IS the retype — the registry/context/painter/
2151        window_runner signatures all read `dyn WidgetHost` now.
2152        Verified: 163 tests; settings audio render stream
2153        byte-identical across BOTH halves; files + data-editor A/B
2154        AE=0; live settings page-dropdown popover → Fonts page
2155        switch. The trait sits at ~65 methods; the remaining
2156        shrink-later blocks (direct-dispatch, value, as_ptr
2157        transitional, `preferred_height`/`value` dyn consumers)
2158        thin out per-app as routed events / concrete slots spread.
2159     5. **The routed-events tail — DONE (2026-07-13).** All 12 apps
2160        (+ TE/DE/demo from 6ab–6ad) dispatch through
2161        `propagate_event`; self-routing composites (Paginator, whose
2162        tree-registered strip would consume its presses under the
2163        router's children-first descent) go through `handle_event`;
2164        only designer's press/move cascade stays direct, by recorded
2165        design (its `drag_widget` doubles as app-mode drag with
2166        circular-pane hit shapes). En route, the ROUTED-DRAG GAP was
2167        found and fixed: the router's DragStart/DragUpdate/DragEnd
2168        fell into `Input::on_event`'s default and every routed drag
2169        was silently dead — `Adapted::handle_event` now maps them
2170        onto the Input drag hooks (regression test drives a full drag
2171        through `propagate_event`).
2172     6. **Direct-dispatch block census (2026-07-13) — the collapse
2173        design.** With every app-side dispatch caller gone, the
2174        block's remaining consumers are: (a) cce-ui widget-INTERNAL
2175        forwards — composites driving embedded children (ramp,
2176        parameters_bg, treelist, dropdown, menu, paginator,
2177        breadcrumb, color_selector, scroll_box, TI's ControlPanel,
2178        cloud's json_layout); (b) designer's deferred cascade (dyn
2179        roster calls); (c) ~40 tests (UFCS `WidgetHost::` forms);
2180        (d) `Adapted`'s own entry-point impls (die with the methods).
2181        THE COLLAPSE: every remaining caller rewrites as
2182        `handle_event(&Event::…)` — behavior-identical by
2183        construction (the entry points literally forward there, and
2184        the drag fix routes `Event::Drag*` to the hooks) — then
2185        `mouse_input`, `cursor_moved`, `on_cursor_moved`,
2186        `mouse_wheel`, `keyboard_input`, `drag_begin`, `drag_update`,
2187        `drag_end` leave WidgetHost (8 methods, ~67→59). TWO
2188        CAVEATS: (1) `Adapted::keyboard_input`'s `!visible()` gate
2189        must MOVE INTO `handle_event`'s KeyInput arm (designer's
2190        hidden-widget broadcast relies on it; the routed path
2191        currently lacks it — moving it also fixes that latent
2192        inconsistency); (2) `Adapted`'s PointerMove arm calls the
2193        trait `cursor_moved` internally — inline the
2194        coverage-gate + hover-recompute body as inherent before
2195        deleting. The QUERY/POLLING surface (`draggable`,
2196        `is_dragging`, `take_click`, `take_change`, value getters)
2197        stays — no Event form; dies with typed messages (§3.5) or
2198        container dissolutions. ~100 call sites, one session.
2199        **DONE (2026-07-13): WidgetHost 67→59.** The eight left the
2200        trait; in-crate composite forwards to concrete embedded
2201        children resolve unchanged through the inherent `Adapted<W>`
2202        entry points (the batch-1 recipe — far cheaper than the
2203        feared 100 rewrites); dyn callers (designer's cascade, TI's
2204        ControlPanel child forwards, cloud's json_layout slider
2205        drag) build the equivalent `Event` and call `handle_event`.
2206        Both caveats landed: the keyboard `!visible()` gate lives in
2207        `handle_event`'s KeyInput arm (closing the routed path's
2208        missing-gate hole), and the coverage-gated `cursor_moved`
2209        default became the inherent `Adapted::cursor_moved`. The
2210        trait's default `handle_event` serves test shims only.
2211        Verified: 164 tests; settings render stream byte-identical;
2212        TI interactive four-state A/B empty masks; designer /state
2213        identical across a canvas click.
2214        ~~The MenuBar/StatusBar/Dropdown parent-pointer
2215        snapshot change rides this phase.~~ **Landed early
2216        (2026-07-12): the census showed all five stored widget-side
2217        parent pointers production-DEAD** (nothing ever set_parent's
2218        a MenuBar/StatusBar; ramp's per-tick re-parents fed a
2219        write-only field through a dummy ctx — legacy behaved the
2220        same). TextBox.parent deleted outright; Dropdown.parent
2221        became `parent_snapshot` read-DATA (rect/is_ramp/color, same
2222        direct-write activation, Ramp-clamp test adapted) and its
2223        write-only `tracked_parent` died; MenuBar/StatusBar lost the
2224        fields, their `parent_changed`/`tracked_parent` overrides,
2225        MenuBar's never-firing `adjust_rect` clamp, and now report
2226        the 0.0 corner radius production always read; the
2227        `Layout::parent_changed`/`tracked_parent` hooks are deleted
2228        (implementor-less), `Adapted::parent` is tree-only, and the
2229        Ramp/ColorRamp tick_ctx re-parent rituals are gone. Stored
2230        `*mut dyn Element` survives ONLY in the WidgetTree registry
2231        payload, EventCtx's transient host ptr, and TI's
2232        ControlPanel. A/B: text-editor + TI Ramp-child static AND
2233        preset-popover-open frames all empty 8% masks; 163 tests. **Measured blueprint (~55
2234        methods, from the machinery's actual call sites):**
2235        identity/tree — id (guaranteed, no more `Option<&Widget>`),
2236        type_name, label, as_any/as_any_mut, as_ptr/as_ptr_mut
2237        (transitional), visible/set_visible, z_index,
2238        is_child_visible, children(ctx), parent(ctx), add_child
2239        (transitional, TI);
2240        layout — rect, set_rect, measure, layout, label_x_offset,
2241        set_row_rect;
2242        events — handle_event, hit_test, mark_dirty, tick/wants_tick,
2243        set_modifiers, focus/unfocus/focused, context_action,
2244        blocks_backplate_drag, is_scrollable, plus the
2245        direct-dispatch block (mouse_input, cursor_moved, mouse_wheel,
2246        keyboard_input, drag_begin/drag_update/drag_end/is_dragging/
2247        draggable, take_click, take_change) — shrinks per app as they
2248        move to routed events;
2249        value — get_value_string/set_value_string, set_text,
2250        set_selected — shrinks as app loops go concrete-slot;
2251        paint — paint_self, clips_children, renders_own_subtree,
2252        prepare_text, popover_rect, render_popover, dirty-flag access;
2253        visual tuples (walk legacy branch + designer loop) — color,
2254        all_quads, all_rounded_quads, extra_quads, extra_circles,
2255        extra_arcs, corner_radii, plate_bevel, solid_border,
2256        widget_font, highlight_quad.
2257        Everything else on today's Element (79 methods plus the
2258        generic-only surface) either moves to inherent `Adapted<W>`
2259        methods for the generic render machinery or dies.
2260        **Execution mode: shrink Element IN PLACE toward the blueprint,
2261        then rename it to `WidgetHost` when it matches** — a parallel
2262        trait can't be reached from the existing trait object, but
2263        removing non-blueprint methods one census-driven commit at a
2264        time keeps every state shippable. **First shrink batch (DONE
2265        2026-07-12, 79→73):** `highlight_color` folded into the
2266        `highlight_quad` default (zero overrides); `set_drag_bounds` +
2267        `intrinsic_size` moved to inherent `Adapted<W>` methods (their
2268        concrete callers — designer's network panel, fonts'/graph's
2269        hand-laid sizing — resolve unchanged); `layout_style`/
2270        `layout_children` deleted with **scene/bridge.rs itself** (its
2271        last production user was retired in 6aa; the narrow
2272        `Layout::intrinsic_size` hook stays — `Adapted::measure` reads
2273        it); `layout_ignore` deleted with its only consumers, the
2274        uncalled `layout_widgets`/`layout_widget_ptors`. Census
2275        lesson: grep BOTH `.method(` and UFCS `::method(` forms — the
2276        fonts/graph `Element::intrinsic_size(&x)` callers only
2277        surfaced at compile. **Second shrink batch (DONE 2026-07-12,
2278        73→69):** `hovered`/`set_hovered` deleted — the state is the
2279        base `Widget::hovered` flag, read/written directly by the
2280        `cursor_moved`/`on_cursor_moved`/`highlight_quad` defaults and
2281        `serialize.rs`; Button/Checkbox keep inherent accessors for
2282        immediate-mode hosts (cloud's json_layout downcasts to
2283        concrete `Checkbox`, so it already resolved to those).
2284        `corner_radius` + `rounded_corners` replaced by ONE
2285        `corner_style() -> (f32, (bool,bool,bool,bool))` mirroring the
2286        narrow `Paint::corner_style` — NOT folded into `corner_radii`,
2287        which is lossy: the radius is meaningful with every corner off
2288        (Menu/StatusBar report their parent's radius to children
2289        through the flags-off channel; breadcrumb can be
2290        flags-true/radius-0, whose legacy radius-0 rounded bg quad
2291        would vanish). `clear_children` moved to an inherent
2292        `Adapted<W>` method (every caller is a concrete Adapted field
2293        in cce-files). Consumer commits: settings renderer,
2294        TI ControlPanel aggregates, cloud json_layout. Verified:
2295        settings audio render stream byte-identical, files A/B AE=0,
2296        TI gallery empty 8% amplitude mask, 163 tests.
2297        Census facts for the leftovers: `preferred_height` has DYN
2298        consumers (container_layout.rs child-ptr walks + layout.rs
2299        machinery on `&dyn` children) — blueprint-adjacent, rides the
2300        flip, not inherent-movable; `value` has a live dyn consumer
2301        (`serialize.rs` over designer's `dyn_refs()`) — rides the
2302        flip. Remaining non-blueprint candidates:
2303        `on_cursor_moved` (belongs in the direct-dispatch block —
2304        blueprint addition, not a deletion), `set_parent` (flip
2305        material, with the parent-ptr snapshot change), `value`,
2306        `preferred_height`.
2307     7. **The value/polling block — DONE (2026-07-13): WidgetHost
2308        59→52.** This is the §3.5 "typed messages" resolution, and it
2309        lands the way 5k's controller half did: no app-defined message
2310        channel is needed — the polling drains stay concrete (inherent
2311        `Adapted<W>` forwards to the narrow `Input` hooks), and what
2312        dies is reaching them through the host trait. Seven methods
2313        left: `take_click`, `take_change`, `get_value_string`,
2314        `set_value_string`, `value`, `set_text`, `set_selected`.
2315        Census: five had ZERO non-test dyn consumers (the old
2316        designer-side serialize consumer of `value` is gone; the
2317        in-crate `widget/display/serialize.rs` inspector feed was the
2318        one live reader — now a concrete downcast chain over the five
2319        `Input::value` implementors Checkbox/Dropdown/Slider/
2320        RangeSlider/Spinbox, pinned by a unit test that fails if a new
2321        implementor is missed). The dyn readers of the rest went
2322        concrete-slot: TI's index-driven roster reads route through
2323        app-local `Roster::take_click/value/get_value_string/set_text
2324        (idx)` matches onto the concrete gallery slots (arms exist per
2325        drained slot; an unwired slot panics loudly); cloud's
2326        `JsonControl` grew an inherent variant-matched `take_click`
2327        (both call sites already gate on the button type); designer's
2328        pane-focus menubar loop writes `set_selected` on its five
2329        concrete `Adapted<MenuBar>` fields. The UFCS test forms became
2330        dot calls resolving to the inherent methods. STILL on the
2331        trait, each with live dyn consumers: `draggable`/`is_dragging`
2332        (designer's deferred press/move cascade + TI's ControlPanel
2333        child pointers), `preferred_height` (layout.rs container
2334        machinery) — these ride the designer event redesign / CP
2335        dissolution. Verified: 165 tests (new serialize pin);
2336        settings audio render stream byte-identical vs the stashed
2337        baseline; TI live probe — Button/Toggle clicks, Layout
2338        dropdown popover open, and a "Grid" selection re-laying out
2339        the gallery through the new roster drains end-to-end.
2340        RIDER: the `Control` subtrait (set_label + control_label) is
2341        DELETED — zero dyn consumers, zero `control_label()` callers;
2342        every impl just routed `set_label` to the inherent shadow, so
2343        the deletion is call-site-invisible (compile-verified across
2344        the workspace).
2345     Former slices 4/5 fold in: the app `as_ptr_mut` dispatch sites
2346     are rewritten by whichever of routed-events (per app) or the
2347     phase-4 flip reaches them first; no standalone pointer-to-id
2348     signature sweep.
2349 
2350 - **Phase 7 — Plate unification: backplate becomes a ROLE of Plate (7a/7b/7c stages DONE 2026-08-25; open questions below).**
2351   Finish what 6as/6at began. The `Backplate` and `Plate` container widgets are deleted and the
2352   `is_backplate`/`is_movable_backplate`/`is_plate` flags are folded, but "backplate" survives as
2353   a second vocabulary for what is now one concept — a lit base surface (`Prim::Plate`). What
2354   remains under the old name: the `style.surface.backplate.*` config namespace and its getters
2355   (`backplate_{padding,gap,color,blur,corner_radius}`, the menubar/statusbar sub-styles); a
2356   partial merge already in the tree (`layout::plate_corner_radius()` falls back to
2357   `backplate_corner_radius`); and — the real content — a ROLE: "the plate that meets the window
2358   edge" (window-background drag via `blocks_backplate_drag`/`drag_allowed_at`, MenuBar/StatusBar
2359   carving into it, the compositor clipping every window at the span-widened backplate radius).
2360   Each app also hand-rolls its root-surface painting from the backplate getters (`DemoApp`
2361   in `src/main.rs` is the reference copy; every client repeats a variant).
2362 
2363   **Motivation.** One surface concept instead of two makes plates fully compositional: a plate
2364   can be the base surface of a window OR a child of another surface, with nothing but role data
2365   distinguishing them. The concrete driver is detachable plates — cce-designer's detached panes
2366   already behave exactly like this (a pane plate becomes a new window's root plate; its interior
2367   corners become window corners), but the geometry lives app-side in
2368   `cce-designer/src/render.rs::pane_plate_radii` and the detach machinery is designer-only.
2369 
2370   **Design.** No widget returns (6as stays won). The unification lives in the paint/geometry
2371   layer: a `PlateSpec` — rect, per-corner radii, color, blur, plus role flags:
2372   `window_corners: (bool, bool, bool, bool)` (which corners lie on the window silhouette) and
2373   `drag_background: bool` (whether uncovered area is a window-drag region). The toolkit computes
2374   per-corner radii from the flags (a window corner wears
2375   `window_corner_radius() * corner_span_factor()`, an interior corner wears
2376   `plate_corner_radius()` — the `pane_plate_radii` math, moved in from the designer), and the
2377   engine paints any plate root-or-nested through the one path, absorbing the per-app hand-rolled
2378   root painting. A window's base surface is just a plate whose four corners are all window
2379   corners.
2380 
2381   **Invariants.**
2382   - *The window silhouette stays a shared cross-process contract.* The compositor clips windows
2383     from the SHARED corner value; per-plate radius freedom must never leak into a
2384     `window_corners=true` corner (the designer's config.kdl radius override already documents
2385     this trap at its `pane_plate_radii` call site). The role flags are where the constraint
2386     lives: flagged corners read the shared value, period.
2387   - *Blur regime follows the role.* A root plate frosts against the compositor's blur-behind
2388     (the negative-alpha marker convention); a nested plate blurs against app content. Detaching
2389     moves a plate between regimes; the marker choice keys off the role flags, and this is the
2390     subtlest part of the phase — it gets its own design note before code.
2391   - *No root container widget.* `PlateSpec` is data consumed by the paint path, not a node that
2392     owns the window.
2393   - *The droplet family rides the plate PUSH-CONSTANT block, not the plate concept.*
2394     `Prim::Droplet` (shader2d `MODE_DROPLET`) reuses the plate block's fields by mode-10
2395     reinterpretation only — it deliberately does not consume `PlateSpec` or the backplate
2396     getters. 7b may reshape how plates are DESCRIBED, but the shader-side field packing is
2397     shared: changing the plate block means re-checking the droplet arm. If droplets are ever
2398     folded into a generalized plate role, two external contracts must survive: the
2399     `module { droplet "k=v" }` spec-string idiom, and the compositor's scenefx droplet node,
2400     which parses the same `DropletSpec`.
2401 
2402   **Stages.**
2403   - **7a — Vocabulary. REVISED at implementation, 7a-1 DONE (2026-08-25).** The original
2404     text said "`style.surface.plate.*` becomes canonical" — implementation surveying found
2405     `style.surface.plate.*` ALREADY EXISTS as the NESTED-plate style namespace (padding,
2406     color, border_color, border_thickness, blur) carrying deliberately different values
2407     from `backplate.*`; a flat alias would have merged root styling into pane styling.
2408     The canonical namespace is therefore role-scoped: **`style.surface.plate.root.*`**
2409     (with `plate.root.menubar.*` for the bar sub-style) — truer to the phase's thesis
2410     anyway: backplate = plate in the root role. Getter names follow as `root_plate_*`.
2411     - **7a-1 DONE.** Both config paths aliased: the layout style-registry table maps
2412       `plate.root.*` rows onto the same slots as `backplate.*` (slot names keep the
2413       historical prefix — invisible), and color.rs's JSON-pointer loads are canonical-first
2414       chains (`/style/surface/plate/root/…` `.or_else(` `/backplate/…)`), so the new
2415       spelling WINS when both are present; in the registry table both spellings write one
2416       slot and document order decides (single-spelling configs — all real ones — are exact).
2417       Canonical getters (`root_plate_{padding,gap,opacity,corner_radius}`,
2418       `root_plate_{menubar,statusbar}_{color,text_color,blur}`) with the old `backplate_*`
2419       names as plain delegating wrappers — NOT `#[deprecated]` yet: 15 crates + the
2420       compositor still call them (16-crate caller census in the 7a-1 commit). "root" joined
2421       `PROP_NODES` for the config-editor path helpers. cce-ui's own callers (config.rs
2422       tests, DemoApp, cce-relief, cce-ramp) migrated. Tests: the legacy styling test now
2423       reads through canonical getters (legacy-config → canonical-getter equivalence), plus
2424       a canonical-spelling test proving parse, precedence over legacy, and legacy-only
2425       fallback. 245 lib tests green; designer A/B AE=0.
2426     - **7a-2 — DONE (2026-08-25).** All caller crates migrated per-repo and the
2427       `backplate_*` wrappers flipped to `#[deprecated]`. Census correction: the compositor's
2428       9 census hits were all its OWN vocabulary (serde fields + a local default fn) — zero
2429       cce-ui getter calls; what it actually needed was the CANONICAL KDL alias, since it
2430       parses the silhouette block from the shared config.kdl itself (`plate { root ... }`
2431       accepted canonical-first, legacy `backplate` unchanged, tested both ways —
2432       cce-compositor@0ef2901). Implementation trap for the record: exact-match renaming of
2433       `backplate_corner_radius()` also matched the compositor's
2434       `default_backplate_corner_radius()` calls while its serde `default = "..."` string
2435       attribute did not — audit renames for substring collisions against local wrappers.
2436       The original census, by call sites:
2437       cce-compositor `server/config.rs` (9 — reads the SHARED silhouette values; the
2438       migration must not change which slot it reads), cce-files (10 across main/
2439       preview_pane/pages), cce-test-interface (11), cce-data-editor (5), cce-terminal (3),
2440       cce-graph (2), cce-fonts (2), cce-color-editor (2), cce-cloud (2), cce-text-editor
2441       (1), cce-system-interface `main.rs` (1), cce-authenticator (1). DONE: cce-designer
2442       (vk-smoke, the exemplar). CONFIRMED CLEAN, nothing to migrate: cce-status-interface
2443       (its bar styling reads `module { }` keys + `/style/status/*`, not the root-plate
2444       getters — verified by its owning session 2026-08-25). The
2445       `Application::is_movable_backplate_at` trait-method NAME is 7b vocabulary
2446       (behavioral role naming), not 7a's. With 7a-2 done, 7a is COMPLETE: new code uses
2447       `root_plate_*` / `plate.root.*`; the deprecated wrappers and the legacy config
2448       spelling were kept for out-of-tree configs.
2449     - **7a-3 — DONE (2026-09-06).** Every live config (shared, cce-graph, cce-designer,
2450       cce-notifier's per-app `plate { }`) was rewritten to the canonical spelling, then the
2451       aliases came out: the `backplate.*` pointer/registry read-aliases in cce-ui, the
2452       compositor's `backplate` node fallback, cce-grid's and cce-notifier's fallbacks, the
2453       `#[deprecated] backplate_*` getters, and `backplate` in `PROP_NODES`. A `backplate`
2454       block in a config is now silently ignored.
2455     - **7a-4 — DONE (2026-09-06).** The internal vocabulary followed: registry slots
2456       (`root_plate_*`), the `ROOT_PLATE_*` statics, `set_root_plate_*` setters,
2457       `read_root_plate_opacity_if_configured`, the trait methods
2458       `Application::is_movable_root_plate_at` and `Widget::blocks_root_plate_drag`, the
2459       compositor's `root_plate_{color,blur,corner_radius}` config fields (and the flat-form
2460       keys of the same name), cce-gallery's `RootPlate` exhibit and `--root-plate` flags,
2461       and the comment prose across every crate. "backplate" survives only in this RFC's
2462       history and in test fixtures that prove the legacy spelling is ignored.
2463   - **7b — `PlateSpec` + window-corner math toolkit-side.** Introduce the spec, port
2464     `pane_plate_radii` in, and give the engine a root-plate paint path fed by a spec instead of
2465     each app's hand-rolled quads (DemoApp first, then the clients). The designer's per-pane
2466     plates convert to specs with computed role flags. A/B: AE=0 per app.
2467 
2468     **Blur-regime design note (required before code; written 2026-08-25).** Two frost
2469     regimes; the role selects between them:
2470     - *Root plate* (all four corners on the window silhouette): the fill stays
2471       POSITIVE-alpha translucent; the COMPOSITOR frosts what lies behind the window
2472       (`plate.root.blur` in the shared config drives cce-fx's blur-behind). The app
2473       draws no frost of its own.
2474     - *Nested plate* (any interior corner): frost is the NEGATIVE-ALPHA sentinel on
2475       the fill — the in-app vk frost pass blurs app content drawn BEFORE the plate
2476       (draw-order-dependent by design; see `param_plate_fill`). A pane touching some
2477       window edges is still nested for blur purposes: it frosts app content.
2478     `PlateSpec` therefore stores `color` with positive alpha plus `blur: bool`, and
2479     `fill()` applies the role-correct encoding: root → alpha forced non-negative,
2480     nested+blur → alpha negated. DETACH is exactly a role flip: interior corners become
2481     window corners, `fill()` flips regimes, and the formerly-frosted app content beneath
2482     simply does not exist in the new window. No other app-side blur change is needed.
2483 
2484     **Radii rule.** A window-flagged corner wears
2485     `window_corner_radius() * corner_span_factor()` (the SHARED silhouette curve — the
2486     invariant); an interior corner wears `plate_corner_radius()` (the app-overridable
2487     pane value). This is `pane_plate_radii` verbatim, moved in.
2488 
2489     - **7b-1 DONE (2026-08-25).** `PlateSpec` in `scene/paint.rs` (rect, positive-alpha
2490       color, `blur`, per-corner `window_corners`, perimeter `depth`) with
2491       `window_corner_flags(rect, win_w, win_h)`, `radii()`/`radii_for()`, role-aware
2492       `fill()`, and `PaintCtx::plate_spec`. DemoApp's hand-rolled root plate migrated —
2493       DELIBERATE visual correction: its radius was the un-spanned
2494       `root_plate_corner_radius`, so under squircle `corner_shape` its perimeter shading
2495       detached from the compositor's span-widened clip; the spec snaps it to the
2496       silhouette (demo AE≠0 expected and intended). The designer's `pane_plate_radii`
2497       delegates to the toolkit (A/B AE=0). Two 7a stragglers the getter census could not
2498       see (raw JSON-pointer reads, not getter calls) gained the canonical-first chain:
2499       `layout::window_corner_radius`'s shared-config read, and cce-grid's silhouette
2500       read.
2501     - **7b-2 — DONE (2026-08-25).** Nine clients migrated: files, terminal,
2502       system-interface, authenticator, data-editor (Prim::Plate hand-rolls →
2503       `plate_spec`, plus data-editor's concentric `corner_frame` now follows the
2504       silhouette), and graph, fonts, color-editor, text-editor (non-Plate root emissions —
2505       rounded_rect/border/legacy tuples — take their values from the spec via the new
2506       `layout::window_silhouette_radius()` scalar, which `radii_for` also uses). A/B
2507       revision: the original "AE=0 each" predates 7b-1's discovery that migrating IS a
2508       correction — every app moved off the un-spanned radius, so corners change by
2509       design. Verified: seven apps diff ONLY within 120px corner squares; the eighth
2510       (system-interface, hardcoded r=12 → silhouette, the largest jump) also shifts the
2511       perimeter roll's edge gradient, eyeball-confirmed as the arc correction.
2512 
2513       **CORRECTION (2026-08-25, post-7c): the Plate-group half of that story was
2514       inverted — a double-span, since fixed.** `plate_spec` fed the spec's FINAL
2515       radii into `Prim::Plate`, whose contract is NOMINAL radii spanned downstream
2516       by `plate_push_raised(scale_corners = true)` — so the five Plate-group
2517       clients drew window corners at span² (12 → ~100 logical at n=4.5). Those
2518       apps had been CORRECT all along (nominal in, spanned once by the push); the
2519       "arc correction" the A/B eyeball accepted was the regression itself, caught
2520       when the user reported corners rounder than the desktop grid. Measured on a
2521       live corner diagonal: clip/grid arc at the expected 0.202·span depth, plate
2522       arc ~2× deeper. The four scalar-group clients (graph, fonts, color-editor,
2523       text-editor) tessellate without a downstream span, so for them the spanned
2524       scalar was and remains the genuine correction. Fix: `plate_spec` pre-divides
2525       by `corner_span_factor()` so the push's multiply reconstructs the spec's
2526       exact values; unit test `plate_spec_emission_round_trips_the_span` guards
2527       it. The same double-span reached DemoApp via 7b-1 — its "AE≠0 expected"
2528       diff bundled the genuine correction WITH the overshoot.
2529       Authenticator is values-only verification (never launch it in a shadow — it claims
2530       the PolicyKit D-Bus name). SKIPPED deliberately: cce-cloud (overlay popup windows —
2531       whether they share the decorated-window silhouette was an open question, since
2532       CLOSED: **yes** — resolved 2026-08-25 post-7c, cce-cloud@4829451. The compositor
2533       never clips layer surfaces, so the app's drawing IS the overlay's silhouette, and a
2534       launcher-sized panel at the nominal radius read nearly square beside real windows;
2535       its root emission is now a PlateSpec with all four corners window-flagged, verified
2536       pixel-identical corner depth to a real window's plate) and
2537       cce-test-interface's `Backplate` gallery shim (a legacy-lookalike test fixture;
2538       migrating it would defeat its purpose — still skipped, still deliberate). Designer pane EMISSION: radii and the
2539       nested-blur sentinel already flow from spec-derived values; full spec-OBJECT
2540       emission is deferred into 7c, because pane plates carry focus tint and
2541       widget-driven bevel styling `PlateSpec` does not yet model — detach will dictate
2542       whether the spec grows those fields or the widget hooks stay authoritative.
2543       7c builds on the role flip.
2544   - **7c — Detach/dock generalization.** Lift the designer's plate-corner control, collapse,
2545     and dock-drag onto `PlateSpec` so any app can offer them. The detached-window PROCESS model
2546     and sync channel (`default_project.json` polling) remain app policy — the toolkit provides
2547     the plate-role flip (interior→window corners, blur regime swap, CSD hookup via the existing
2548     `standard_csd`/`take_window_action` hooks), not the process management.
2549     - **7c-1 DONE (2026-08-25).** `widget::plate_dock`: the app-agnostic PROTOCOL —
2550       constants (control radius/inset, min plate span, stub height, drag threshold),
2551       `PlateDockState { collapsed, detached }` with `stubbed()`, `corner_center(rect,
2552       stubbed)` (rect placement incl. the stub exemption; the designer's circular-pane
2553       arc placement stays app policy), `corner_hit`, `press_becomes_drag`, and
2554       `standard_menu(state, can_detach)` → (label, `PlateDockAction`) rows the host
2555       appends its own items after (the designer's spreadsheet span modes). Deliberately
2556       LEAN: no container type, no widget — a second consumer decides those.
2557       `PlateSpec::detached()` is the role flip (all corners→window, radii snap to the
2558       silhouette, `fill()` swaps frost regimes), unit-tested. The designer delegates:
2559       constants re-exported, rect placement/hit/threshold/standard-menu all
2560       toolkit-calls; PLATE_SLOTS membership, visibility, dock regions, layout
2561       application, and the detach process spawn stay designer policy. Verified live:
2562       corner menus (Collapse/Detach; Reattach-only stubs), the collapse→stub→expand
2563       cycle, and dock-drag arming+drop all behave identically through the delegation.
2564     - **7c-2 — DONE (2026-08-25): cce-files is the second consumer.** Its preview pane
2565       carries the corner control; Collapse narrows every page's pane column to a Preview
2566       title stub (the list keeps the freed width — the feature's actual value), the
2567       stub's control restores the prior split fracs, and the divider/wheel are inert
2568       while collapsed. The adoption answered the open API questions:
2569       - `draw_corner_dot(pc, center, emphasized)` EARNED — both hosts drew the identical
2570         dot; the designer now uses it too.
2571       - A `PlateDock` CONTAINER did NOT earn its place: one `PlateDockState` field
2572         sufficed. Revisit only if a host manages many dockable plates outside its own
2573         state arrays.
2574       - Press arming was designer-specific after all: it exists to disambiguate click
2575         from dock-DRAG, and a single pane has nowhere to dock — files opens the menu
2576         directly. Moreover, files keys the whole interaction on mouse RELEASES: its
2577         routed-widget path consumes left PRESSES before the `Application` hook (only
2578         releases reliably arrive there), a per-app dispatch reality any adopter must
2579         check first. The designer's release-opened menu means the two feel identical.
2580       - The detached-window CSD packaging question is ANSWERED (2026-08-25,
2581         cce-files@805be06 — a working, end-to-end-verified preview-pane detach;
2582         RETIRED the same day at the user's direction, cce-files@f2a89d5:
2583         detach is designer-only for now. The answers below were derived from
2584         that implementation and stand; the code is one revert away at 805be06
2585         for whichever app adopts detach next): **no new toolkit packaging was
2586         needed.** A detached window is an ordinary
2587         `Application` whose root plate carries the detached role (all window
2588         corners); the compositor's decoration IS the CSD in this DE (border =
2589         grab surface, no titlebars), and plate_dock's existing pieces —
2590         `corner_center`/`corner_hit`/`draw_corner_dot`/`standard_menu` — cover
2591         the control. What the second implementation DID establish as the
2592         convention worth naming: **the child's Reattach is process exit** — the
2593         parent `try_wait`s and reclaims the pane, so every way a detached
2594         window can die reattaches it; and the child exits itself when its sync
2595         file or parent pid disappears, so orphans (crash, stale session
2596         restore) self-collect. The sync-file idiom generalizes: parent pid
2597         then payload, written before the spawn, unlinked on reattach.
2598         One packaging gap surfaced and stands as app policy for now: the
2599         toolkit context menu dispatches through a widget tree a minimal
2600         detached window does not have, so the child draws its one-row menu
2601         itself — a third consumer hitting this earns a widget-tree-free menu
2602         helper.
2603       - The designer pane spec-OBJECT emission question got its answer from the
2604         same implementation: detach dictated NO new spec fields — the files
2605         pane detached with `PlateSpec` as it stands, so focus tint and
2606         widget-driven bevel styling stay widget-hook territory and the spec
2607         stays lean. The designer's emission migration remains optional and
2608         unblocked.
2609 
2610   All numbered stages DONE (2026-08-25), and all three open questions CLOSED the same
2611   day: cce-cloud overlays share the silhouette (7b-2); the first non-designer detach
2612   (cce-files' preview pane — built, verified, then retired by product choice the same
2613   day, see 7c-2) answered CSD packaging — nothing new was needed, the conventions are
2614   recorded there — and established that detach dictates no new `PlateSpec` fields,
2615   leaving the designer's spec-OBJECT emission optional and unblocked. The designer
2616   remains the only detach host. Phase 7 is COMPLETE.
2617 
2618   **Design note (recorded 2026-08-30) — why carve GROUPING exists, and why its
2619   rarity is correct.** The mechanism predates this phase: it landed with the SDF-lit
2620   plate system (cce-ui@3877dd6), and its rationale lived only in that commit message
2621   and the `tessellate_display_list` doc comment until now. The tessellator promotes a
2622   `Recess` emitted while a `Plate`/`Bevel` is still "open" into a **CSG feature of
2623   that plate's single draw** (the per-frame feature UBO); every other carve renders
2624   through the standalone overlay branch. Two things justify the dual path:
2625 
2626   - *Junction correctness at the perimeter roll.* A grouped plate is ONE composite
2627     height field — the rolled edge minus its carves — lit once per pixel from summed
2628     analytic slope vectors, so a carve wall meeting the plate's perimeter roll is an
2629     arithmetic junction. The overlay branch approximates that meeting with the
2630     host-box fade.
2631   - *Features never blend in color space.* An overlay is shading drawn over
2632     already-lit pixels, so stacked shading double-counts — the same reason
2633     `Prim::Ridge` exists rather than a boss+recess pair (double-counted specular at
2634     the crest).
2635 
2636   The corollary that makes the design coherent: **the two paths differ visibly only
2637   near the host's rolled perimeter.** An interior carve (a TreeList or TextBox well
2638   in the middle of a window plate) never touches the roll, so the fallback is
2639   effectively exact there. Two scope corrections recorded 2026-08-30 (the first
2640   version of this note got them wrong): what groups is **full-ring untinted carves**
2641   (button grooves, slider wells) — the flush menubar/status bands never group, by
2642   design since cce-ui@80d50de: an edge-suppressed carve's wall rect extends past the
2643   boundary, relying on the overlay cover quad to clip it, a clip the grouped
2644   whole-plate draw does not have (grouped, the extended walls smeared across the
2645   plate). So the junction where a flush band meets the plate's roll is ALWAYS the
2646   host-box fade; grouping's value is the single-evaluation lighting of full-ring
2647   carves, exact wherever one sits near the roll. And per the audit that shipped
2648   `CCE_PLATE_DEBUG` (cce-ui@949e35e, three apps): **no misgrouping — every fallback
2649   is a documented rule firing correctly**; grouping is rare (demo 2 of 10, cce-files
2650   0 of 7) because apps constantly interleave flat fills with reliefs, and each one
2651   correctly closes the grouping window (the carve's shading is baked into the
2652   plate's earlier draw). Neither path is retirable: grouping-always is impossible
2653   for exactly that reason, and fallback-only would forfeit the exact junctions
2654   full-ring carves get when they do group.
2655 
2656   The standing hazard is the *silent flip*: three of the six grouping conditions are
2657   dynamic (draw order, sibling plates, whether another plate claimed the host's
2658   feature run), so the same widget can render through either path depending on its
2659   surroundings. That shipped as a bug once — a hovered button's opaque fill severed
2660   every later button from the backplate they carve into — fixed by making the
2661   carve-host tracker a stack (`plate_stack`, cce-ui@9cfadad). `CCE_PLATE_DEBUG=1`
2662   reports each carve's verdict, the fallback reason, and which prim closed a
2663   grouping window; it is the first tool for any "same widget, different look"
2664   report. Hardening (landed 2026-08-30): debug builds warn loudly — once per
2665   geometry, no env var — when a groupable full-ring carve is enclosed by a
2666   still-open plate, its shaded region reaches that plate's roll band, and a dynamic
2667   rule (occlusion, feature-run contiguity, budget) rejected it: the one class where
2668   the flip is visually significant (`near_roll_fallback_reason` in
2669   `backend/window_runner.rs`, unit-tested). Deliberately a warning, NOT an assert:
2670   the audit established every rejection is conservative-correct — the render is
2671   right, it is the frame-to-frame look that flips — so a panic would crash debug
2672   builds on correct behavior. The ubiquitous accepted case (ordinary geometry
2673   already closed every grouping window) stays quiet by construction: no open
2674   enclosing plate remains for the check to run against.
2675 
2676 - **Design note (2026-09-01) — the scroll-virtualization contract, and the ScrollRegion
2677   de-duplication sweep.** A class fix, recorded because the class outlived every
2678   individual sighting of it. `ScrollBox::get_item_draw_y` returned a row's position
2679   only when the row was FULLY inside the viewport, so callers drew nothing for a row
2680   straddling the edge — cards/rows visibly vanished mid-scroll. The helper predates
2681   the §3.4 clip stack (when there was no way to draw a row "cut", culling whole was
2682   the only option), and the contract then traveled: the struct around it was copied
2683   into cce-system-interface, and from there into cce-fonts, cce-mail, cce-cloud, and
2684   cce-layout-interface as each dissolved its List/ScrollBox embedded base (Phase 6q),
2685   plus reimplemented in cce-files' RowList — the multi-repo copy-drift failure mode,
2686   in widget form. The bug was then rediscovered and fixed **per app**
2687   (cce-system-interface first; cce-cloud@1063422 fixed only the CLICK half), which is
2688   exactly the cost the sweep exists to stop paying.
2689 
2690   The sweep (one commit per repo, 2026-09-01): the fixed system-interface copy was
2691   lifted verbatim into **`widget::ScrollRegion`** (cce-ui@764d9e7) — the union of all
2692   the copies' APIs — so the contract lives in one place: **`get_item_draw_y`/
2693   `get_draw_y` return every row that INTERSECTS the viewport; callers draw those rows
2694   under a clip (the §3.4 stack, or exact per-quad clamping for flat pipelines), and
2695   hit-test the SAME partial rows the draw shows** — visible ⇒ clickable, culled ⇒
2696   not; a fix to only one half just mirrors the bug (1063422's blank-band click gate
2697   became the sliver-selects-correctly gate with no shape change once the draw side
2698   caught up). `ScrollBox::get_item_draw_y` itself moved to the intersection contract.
2699   Deliberately NOT migrated: TreeList's `get_row_rect` keeps full containment — it
2700   places a floating overlay that draws over the well unclipped, and an editor hanging
2701   half off the list edge is worse than one that waits — and TreeList's row-bottom
2702   separator gate, which is spatial (the separator would land outside the well), not a
2703   cull.
2704 
2705   Two pipeline lessons from the app migrations, for anyone adding a scrolled list:
2706   the clip must survive to EVERY stage that renders row content, or partial rows
2707   bleed instead of cut. cce-mail's hand-emitted row labels ride a boundless labels
2708   drain — they drew whole into the menubar until given viewport bounds; cce-cloud's
2709   span assembly dropped the paint walk's merged clip on the floor (`bounds: None`)
2710   until it was threaded through to the glyph pass. And where widgets keep their full
2711   rect while drawing cut (cce-layout-interface's row buttons above other controls),
2712   the stored hit rect is re-clamped to the visible sliver after the visuals are
2713   recorded, so the hidden part cannot shadow what's beneath it. New scrolled lists
2714   build on `widget::ScrollRegion`; hand-copying it back into an app is how this
2715   class got six lives.
2716 
2717 Order rationale: each phase is independently valuable and reversible, and no phase requires the
2718 next to compile. Phase 0 can land immediately regardless of the rest.
2719 
2720 ---
2721 
2722 ## 7. Quick wins to land first (Phase 0 detail)
2723 
2724 1. **Breadcrumb black rectangle — DONE.** `cce-files` `BrowseContainer::set_rect` now positions
2725    the breadcrumb to exactly match `browse::view`'s layout (inset by the page margin, reserving
2726    the dropdown width) so the container's duplicate paint sits fully behind the page copy
2727    instead of leaking a dark strip. This is a stop-gap; the real fix is the single paint path in
2728    Phase 3 (the breadcrumb is still painted twice — the copies now just coincide).
2729 2. **Hot-path debug I/O — DONE.** Removed the per-frame `eprintln!` in `render()` that
2730    reconstructed every text area's string via `layout_runs()`.
2731 3. **Dead code — DONE.** Removed `UiContext::tick_hover`/`get_hover_quad` (zero callers
2732    workspace-wide), the dead duplicate of `hover_animation`.
2733 4. **GPU scissor — DEFERRED to Phase 3.** Threading clip rects to `render_pass.set_scissor_rect`
2734    is not actually a "quick win": clip rects are computed CPU-side and folded into geometry
2735    today, with nothing carried to the render pass. Doing it properly needs the clip stack from
2736    §3.4, so it lands with the paint-pass rework rather than as a risky standalone change.
2737 
2738 ---
2739 
2740 ## 8. Risks & mitigations
2741 
2742 - **Migration surface across ~19 apps.** Mitigation: adapter shim + per-app Phase 6; the core
2743   lands and is validated before any app is forced across.
2744 - **Borrow-checker friction with an arena tree.** Mitigation: layout operates on
2745   `style`/`layout_out`, not the `widget` payload; `get_disjoint_mut` for the rare dual-borrow.
2746 - **Hand-rolled layout correctness.** Grow/shrink/wrap/alignment are subtle. Mitigation: keep
2747   the box model small (row/column + flex + align + gap/padding only), and unit-test the solver
2748   in isolation — it operates on `Style`/`Size`, independent of paint, so it is directly testable.
2749 - **Effort.** This is multi-week. Phasing keeps every intermediate state shippable so it can be
2750   paused/resumed without a broken tree.
2751 
2752 ---
2753 
2754 ## 9. Deferred (designed-for, not built now)
2755 
2756 - **Per-subtree geometry caching:** cache tessellated vertices per node, re-tessellate only
2757   dirty subtrees instead of the whole scene each frame. The arena `Dirty` flags are the hook;
2758   worth it only once scenes are large.
2759 - **Full affine transforms / rotation** beyond translate+scale.
2760 - **Damage-rect partial redraw** at the GPU level (currently full-surface clear each dirty frame).
2761 
2762 ---
2763 
2764 ## 10. Decisions (resolved 2026-07-07)
2765 
2766 1. **Layout solver: hand-rolled** (not taffy). Compact measure/arrange engine owned in
2767    `cce-ui`, scoped to the DE's box model. See §3.2.
2768 2. **A new shared dependency in `cce-ui` is acceptable** when needed (one shared path dep does
2769    not break standalone builds). Note the layout decision means no layout dep is required.
2770 3. **Phase 0 quick wins land now**, as separate commits ahead of the rebuild. See §7.