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

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

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