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

commit17071874632dfff071cf9afe2ca69f298691de58
parenta0e43099d1
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 13:54
refactor(layout): retire legacy.rs; a page's sections are drawn once

The legacy layout engines are gone. What a settings page still needs moved to
layout/section.rs: PageFlow places sections in a masonry of columns, and
PageLayoutBuilder draws each section ONCE in the slot the flow gives it, then
hands its height back. Every section used to be drawn twice, once into a
throwaway target to measure it, through LayoutStrategy, whose allocate took the
height before the position. A section's position never depended on its own
height, so drawing once places everything where it was.

- LayoutStrategy, ColumnLayout, AdaptiveGrid, FlexLayout, RadialLayout and
  the unused Column / Row / Section / UiFrame / Radial / ManualLayout are
  deleted.
- ContainerLayout is a trait of its own (layout, measure), without the
  init / allocate cursor nobody called.
- SplitterLayout and CircularPaneLayout (cce-designer's) are layout/panes.rs;
  the system font helpers moved to registry.rs.

Checked in a scale-2 shadow against the previous binaries: all 14 settings
pages, cce-files (relief on and off, save mode) and cce-gallery draw
identically to the pixel, live readings aside.

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

 CLAUDE.md                                |   21 +-
 docs/rfc-core-rebuild.md                 |    9 +
 src/layout/legacy.rs                     | 1919 ------------------------------
 src/layout/mod.rs                        |   88 +-
 src/layout/panes.rs                      |   96 ++
 src/layout/registry.rs                   |   36 +
 src/layout/section.rs                    |  930 +++++++++++++++
 src/scene/layout.rs                      |    5 +-
 src/widget/container/container_layout.rs |  231 +---
 src/widget/container/mod.rs              |    2 +-
 src/widget/mod.rs                        |    2 +-
 11 files changed, 1130 insertions(+), 2209 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 3b10d4e..d2c5c7a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1771,14 +1771,27 @@ cce-system-interface) to confirm behavior, not just the test suite.
   - `src/layout/mod.rs` (~4.4k lines) — the sizing constants and the style getters/setters
     (heights, radii, fonts — many `*_font_parsed()` — gaps, the relief and bevel profile
     state), `reload_config`, and `read_preferred_fonts` / font-family resolution used by the
-    cosmic-text path. It names no widget: what does is in the two modules below.
+    cosmic-text path. It names no widget: what does is in the modules below.
   - `registry.rs` — the style registry (config flattened to one map of keys), its test overlay,
     the font-string helpers.
   - `bridge.rs` — the flat-host render bridge: `RenderTarget`, `PopoverCollector`,
     `render_widget`, `render_popovers`, the carve types that cross it.
-  - `src/layout/legacy.rs` (~2k lines) — the legacy layout engines (`Column`, `Section`, `Grid`,
-    the `LayoutStrategy` family, `PageLayoutBuilder`, `VStack`), still used by
-    cce-system-interface, cce-files and cce-gallery. New layout is `scene::layout`.
+  - `section.rs` — a settings page's sections: `PageFlow` places them (a masonry of
+    columns as wide as fit `grid_min_col_width`), `PageLayoutBuilder` draws each ONCE in
+    the slot the flow gives it and hands its height back, and `SectionContext` (with its
+    `VStack`) is what a section draws through — immediate-mode, a cursor down the content
+    box and a one- or two-column grid. cce-system-interface is its user. Since 2026-10-08:
+    until then it was `legacy.rs`, and every section was drawn TWICE, once into a
+    throwaway target to measure it, through the `LayoutStrategy` trait, whose `allocate`
+    took the height before the position. A section's position never depended on its own
+    height, so drawing once places everything where it was (all 14 settings pages
+    pixel-identical before and after in a scale-2 shadow, live readings aside).
+    `LayoutStrategy`, `ColumnLayout`, `AdaptiveGrid`, `FlexLayout`, `RadialLayout` and the
+    unused `Column` / `Row` / `Section` / `UiFrame` / `Radial` went with it; cce-files'
+    browse page is a `scene::layout` column now, and the container layouts
+    (`widget::ContainerLayout`, the gallery's Layout exhibit) are a trait of their own,
+    `layout` and `measure`, without the cursor. New layout is `scene::layout`.
+  - `panes.rs` — `SplitterLayout` and `CircularPaneLayout`, cce-designer's pane geometry.
 - `color/` — the colour model and named colours (`colors` re-export module in `lib.rs`),
   split the same way: `mod.rs` the constants, statics and getters; `load.rs` reading the
   config into them (and `retired_surface_keys`); `math.rs` sRGB/linear, OKLab and the
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 945822b..4b5d7d0 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -327,6 +327,15 @@ Constraint respected: **each crate still builds standalone** — the new core is
 - **Phase 2 — Layout pass.** Add `Style` + measure/arrange (taffy). Migrate containers to
   emit `Style` instead of `LayoutStrategy`; delete `allocate` and render-twice measurement as
   containers move over.
+  - **`LayoutStrategy`, `allocate` and render-twice: DELETED 2026-10-08.** The last users were
+    cce-system-interface's pages (`PageLayoutBuilder` over `AdaptiveGrid`), cce-files' browse
+    page (`ColumnLayout`) and the gallery's container-layout exhibit. A section's slot never
+    depended on its own height, so `layout::section::PageFlow` places it first and the
+    section draws once, in place (`each_section_is_drawn_once`); the browse page is a
+    `scene::layout` column; `ContainerLayout` keeps only `layout` / `measure`. Section
+    CONTENT is still immediate-mode `SectionContext` — porting each page's content to box
+    trees is not done. Every settings page, cce-files (relief on and off, save mode) and the
+    gallery drew identically to the pixel before and after, live readings aside.
 - **Phase 3 — Paint unification.** Introduce `DisplayList` + `PaintCtx` (clip/transform);
   route the backend `render()` through the arena walk; retire the top-level `widgets` Vec and
   `render_widget`/`SectionContext`.
diff --git a/src/layout/legacy.rs b/src/layout/legacy.rs
deleted file mode 100644
index 5528c60..0000000
--- a/src/layout/legacy.rs
+++ /dev/null
@@ -1,1919 +0,0 @@
-//! The legacy layout engines — `Column`, `Row`, `Section`, `Grid`, the `LayoutStrategy`
-//! family (`FlexLayout`, `ColumnLayout`, `AdaptiveGrid`, `RadialLayout`), `PageLayoutBuilder`
-//! and `VStack` — from before `scene::layout`'s box model. Still used by
-//! cce-system-interface, cce-files and cce-gallery; new layout goes through
-//! `scene::layout`.
-
-use super::*;
-use crate::widget::WidgetHost;
-use crate::context::UiContext;
-
-pub struct UiFrame;
-
-impl UiFrame {
-    pub fn start(scroll_offset: f32) -> Self {
-        crate::widget::hover_animation::reset_frame_registration();
-        crate::widget::hover_animation::set_scroll_offset(scroll_offset);
-        Self
-    }
-
-    pub fn finish(self, pc: &mut dyn RenderTarget) {
-        crate::widget::hover_animation::post_render_check();
-        if let Some((qx, qy, qw, qh, qc)) = crate::widget::hover_animation::get_quad() {
-            pc.rect(qc, qx, qy, qw, qh);
-        }
-        // render_popovers(pc);
-    }
-}
-
-pub struct Column {
-    ox: f32,
-    oy: f32,
-    cx: f32,
-    pub y: f32,
-    pub cw: f32,
-}
-
-impl Column {
-    pub fn new(ox: f32, oy: f32, cx: f32, cy: f32, cw: f32) -> Self {
-        Self { ox, oy, cx, y: cy, cw }
-    }
-
-    pub fn ax(&self, x_off: f32) -> f32 {
-        self.ox + self.cx + x_off
-    }
-
-    pub fn ay(&self) -> f32 {
-        self.oy + self.y
-    }
-
-    pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
-        pc.rect(color, self.ax(x_off), self.ay(), w, h);
-        self.y += h;
-    }
-
-    pub fn advance(&mut self, dy: f32) {
-        self.y += dy;
-    }
-
-    pub fn spacing(&mut self, dy: f32) {
-        self.y += dy;
-    }
-
-    pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
-        let x = self.ax(8.0);
-        let y = self.ay();
-        pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
-        self.y += 8.0;
-    }
-
-    pub fn header(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32) {
-        let x = self.ax(x_off);
-        let y = self.ay();
-        pc.text(text, x, y, 14.0, [0.83, 0.83, 0.83, 1.0]);
-        self.y += 22.0;
-    }
-
-    pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
-        let x = self.ax(x_off);
-        let y = self.ay() + y_off;
-        pc.text(text, x, y, font_size, color);
-    }
-
-    pub fn widget<T: WidgetHost + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
-        if let Some(pref) = w.preferred_height() {
-            wh = pref;
-        }
-        let top_room = w.label_strip();
-        let total_h = wh + top_room;
-        let x = self.ax(x_off);
-        let y = self.ay();
-        w.set_row_rect(self.ox + self.cx + 8.0, self.cw - 16.0);
-        let clamped_w = ww.min((self.cw - x_off).max(0.0));
-        render_widget(pc, w, x, y, clamped_w, total_h, ctx);
-        self.y += total_h;
-    }
-
-    pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
-        let row_y = self.ay();
-        let mut row = Row {
-            pc: &mut *pc,
-            base_x: self.ox + self.cx,
-            y: row_y,
-            cursor_x: 0.0,
-            spacing: CONTROL_GAP,
-        };
-        f(&mut row);
-        self.y += h;
-    }
-}
-
-pub struct Row<'a> {
-    pc: &'a mut dyn RenderTarget,
-    base_x: f32,
-    y: f32,
-    pub cursor_x: f32,
-    pub spacing: f32,
-}
-
-impl<'a> Row<'a> {
-    pub fn set_spacing(&mut self, spacing: f32) {
-        self.spacing = spacing;
-    }
-
-    pub fn gap(&mut self, width: f32) {
-        self.cursor_x += width;
-    }
-
-    pub fn text(&mut self, text: &str, y_off: f32, font_size: f32, color: [f32; 4], width: f32) {
-        self.pc
-            .text(text, self.base_x + self.cursor_x, self.y + y_off, font_size, color);
-        self.cursor_x += width + self.spacing;
-    }
-
-    pub fn widget<T: WidgetHost + 'static>(&mut self, w: &mut T, ww: f32, mut wh: f32, ctx: &mut UiContext) {
-        if let Some(pref) = w.preferred_height() {
-            wh = pref;
-        }
-        render_widget(self.pc, w, self.base_x + self.cursor_x, self.y, ww, wh, ctx);
-        self.cursor_x += ww + self.spacing;
-    }
-}
-
-fn estimate_label_width_helper(label: &str, font_size: f32, font_fam: &str) -> f32 {
-    let fam_lower = font_fam.to_lowercase();
-    let is_mono = fam_lower.contains("mono") || fam_lower.contains("courier") || fam_lower == "monospace";
-    if is_mono {
-        label.chars().count() as f32 * font_size * 0.60
-    } else {
-        let mut width = 0.0;
-        for c in label.chars() {
-            let factor = match c {
-                'i' | 'l' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.30,
-                'f' | 'j' | 't' => 0.35,
-                'r' | 's' | 'c' | 'z' => 0.50,
-                'a' | 'b' | 'd' | 'e' | 'g' | 'h' | 'k' | 'n' | 'o' | 'p' | 'q' | 'u' | 'v' | 'x' | 'y' => 0.60,
-                'm' | 'w' | 'M' | 'W' | '&' | '@' | 'O' | 'Q' | 'G' => 0.85,
-                'A' | 'B' | 'C' | 'D' | 'H' | 'N' | 'U' | 'V' | 'X' | 'Y' => 0.75,
-                'E' | 'F' | 'K' | 'L' | 'P' | 'R' | 'S' | 'T' | 'Z' | 'J' => 0.68,
-                '0'..='9' => 0.60,
-                _ => 0.60,
-            };
-            width += factor * font_size;
-        }
-        width
-    }
-}
-
-pub struct Section {
-    pub left: f32,
-    pub top: f32,
-    pub content_y: f32,
-    pub cw: f32,
-    pub label_width: f32,
-    pub is_child: bool,
-    pub grid: Grid,
-    pub last_col: usize,
-}
-
-impl Section {
-    pub const DEFAULT_MARGIN_X: f32 = 12.0;
-    pub const DEFAULT_ROW_GAP: f32 = 8.0;
-
-    fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
-        estimate_label_width_helper(label, font_size, font_fam)
-    }
-
-    pub fn padding(&self) -> f32 {
-        section_padding()
-    }
-
-    pub fn new(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
-        Self::new_opt(pc, left, top, cw, label, false)
-    }
-
-    pub fn new_opt(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str, is_child: bool) -> Self {
-        let font_setting = if is_child {
-            nested_section_label_font()
-        } else {
-            section_label_font()
-        };
-        let (font_fam, font_size_opt) = parse_font_string(&font_setting);
-        let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
-        let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
-        let label_width = Self::estimate_label_width(label, font_size, &font_fam);
-        let label_x = if is_child {
-            let base_x = match nested_section_label_alignment() {
-                0 => left + 12.0,
-                1 => left + (cw - label_width) / 2.0,
-                2 => left + cw - 12.0 - label_width,
-                _ => left + 12.0,
-            };
-            base_x + nested_section_label_offset()
-        } else {
-            left + (cw - label_width) / 2.0
-        };
-        pc.text_with_font(label, label_x, top, font_size, font_color, &font_fam);
-
-        let pad = section_padding();
-        let margin_x = 2.0 * pad + 12.0;
-        let usable_w = (cw - 2.0 * margin_x).max(1.0);
-        let min_col_width = 130.0;
-        let gap = 8.0;
-        let max_cols = if is_child {
-            1
-        } else {
-            ((usable_w + gap) / (min_col_width + gap)).floor().clamp(1.0, 2.0) as usize
-        };
-        let content_start_y = top + pad + 19.0;
-        let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
-
-        Self { left, top, content_y: content_start_y, cw, label_width, is_child, grid, last_col: usize::MAX }
-    }
-
-    /// Horizontal inset of content from this section's left edge — the same
-    /// one `row_layout`, the column `Grid` and `widget` use, so everything in
-    /// a section lines up. See `SectionContext::content_margin`.
-    pub fn content_margin(&self) -> f32 {
-        2.0 * self.padding() + 12.0
-    }
-
-    pub fn content_left(&self) -> f32 {
-        self.left + self.content_margin()
-    }
-
-    pub fn content_width(&self) -> f32 {
-        (self.cw - 2.0 * self.content_margin()).max(0.0)
-    }
-
-    /// See `SectionContext::ax` — same mapping, same reason it is no longer
-    /// stepped at `x_off == 12.0`.
-    pub fn ax(&self, x_off: f32) -> f32 {
-        self.left + 2.0 * self.padding() + x_off
-    }
-
-    pub fn ay(&self) -> f32 { self.content_y }
-
-    pub fn spacing(&mut self, dy: f32) {
-        if self.grid.col_heights.len() >= 2 {
-            if self.last_col == usize::MAX {
-                for h in &mut self.grid.col_heights {
-                    *h += dy;
-                }
-            } else if self.last_col < self.grid.col_heights.len() {
-                self.grid.col_heights[self.last_col] += dy;
-            }
-            self.content_y = self.grid.max_height();
-        } else {
-            self.content_y += dy;
-            for h in &mut self.grid.col_heights {
-                *h += dy;
-            }
-        }
-    }
-
-    pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
-        let x = self.ax(x_off);
-        let y = self.ay() + y_off;
-        // Bounded to the content box, like `SectionContext::text` — text is
-        // the only thing a section draws that is not already sized to fit it.
-        let right = self.content_left() + self.content_width();
-        let bounds = if right > x {
-            Some([x, y - font_size, right, y + 2.0 * font_size])
-        } else {
-            None
-        };
-        pc.text_with_bounds(text, x, y, font_size, color, bounds);
-    }
-
-    pub fn widget<T: WidgetHost + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
-        if let Some(pref) = w.preferred_height() {
-            wh = pref;
-        }
-        let pad = self.padding();
-        let top_room = w.label_strip();
-        let total_h = wh + top_room;
-
-        let name = w.type_name();
-        let span_full = name == "Trackpad"
-            || name == "Canvas"
-            || name == "UsageBar"
-            || name == "ProgressBar"
-            || name == "ButtonStrip"
-            || name == "Spreadsheet"
-            || name == "Graph";
-
-        if span_full {
-            let margin_x = 2.0 * pad + 12.0;
-            let x = self.left + margin_x;
-            let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
-            let max_h = self.grid.max_height().max(self.content_y);
-            let y = max_h;
-
-            w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
-            render_widget(pc, w, x, y, clamped_w, total_h, ctx);
-
-            let new_bottom = y + total_h;
-            self.content_y = new_bottom;
-            self.grid.col_heights.fill(new_bottom);
-        } else {
-            let max_h = self.grid.max_height();
-            if self.content_y > max_h {
-                self.grid.col_heights.fill(self.content_y);
-            }
-
-            let col = self.grid.next_column();
-            self.last_col = col;
-            let x = self.grid.col_lefts[col];
-            let y = self.grid.col_heights[col];
-
-            w.set_row_rect(x, self.grid.col_width);
-            render_widget(pc, w, x, y, self.grid.col_width, total_h, ctx);
-            self.grid.col_heights[col] += total_h;
-            self.content_y = self.grid.max_height();
-        }
-    }
-
-    pub fn widget_full<T: WidgetHost + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32, ctx: &mut UiContext) {
-        let x_off = 12.0;
-        let ww = self.cw - 2.0 * (self.padding() + x_off);
-        self.widget(pc, w, x_off, ww, wh, ctx);
-    }
-
-    pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
-        let pad = self.padding();
-        let x = self.ax(pad);
-        let max_h = self.grid.max_height().max(self.content_y);
-        let y = max_h;
-        pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
-        self.content_y = max_h + 8.0;
-        self.grid.col_heights.fill(self.content_y);
-    }
-
-    pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
-        let max_h = self.grid.max_height().max(self.content_y);
-        pc.rect(color, self.ax(x_off), max_h, w, h);
-        self.content_y = max_h + h;
-        self.grid.col_heights.fill(self.content_y);
-    }
-
-    pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
-        let margin_x = self.content_margin();
-        let usable_w = self.content_width();
-        if count == 0 {
-            return Vec::new();
-        }
-        let total_gap = gap * (count - 1) as f32;
-        let col_w = (usable_w - total_gap).max(0.0) / count as f32;
-
-        let mut cols = Vec::with_capacity(count);
-        for i in 0..count {
-            let x = self.left + margin_x + i as f32 * (col_w + gap);
-            cols.push((x, col_w));
-        }
-        cols
-    }
-
-    pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
-    where
-        F: FnMut(usize, f32, f32),
-    {
-        let max_h = self.grid.max_height().max(self.content_y);
-        self.grid.col_heights.fill(max_h);
-        self.content_y = max_h;
-
-        let cols = self.row_layout(count, gap);
-        for (i, &(x, w)) in cols.iter().enumerate() {
-            f(i, x, w);
-        }
-        self.content_y += h;
-
-        self.grid.col_heights.fill(self.content_y);
-    }
-
-    pub fn finish(&mut self, pc: &mut dyn RenderTarget) -> f32 {
-        self.finish_focused(pc, false)
-    }
-
-    pub fn finish_focused(&mut self, pc: &mut dyn RenderTarget, focused: bool) -> f32 {
-        let border: [f32; 4] = if self.is_child {
-            if focused {
-                [0.22, 0.38, 0.24, 1.0]
-            } else {
-                [0.18, 0.18, 0.25, 1.0]
-            }
-        } else {
-            if focused {
-                [0.30, 0.50, 0.32, 1.0]
-            } else {
-                [0.25, 0.25, 0.35, 1.0]
-            }
-        };
-        let pad = self.padding();
-        let x = self.left + pad;
-        let y = self.top + 7.0;
-        let w = self.cw - 2.0 * pad;
-        let h = self.content_y - y;
-        
-        let left_edge = x;
-        let right_edge = x + w;
-        if self.label_width > 0.0 {
-            let label_x = if self.is_child {
-                let base_x = match nested_section_label_alignment() {
-                    0 => self.left + 12.0,
-                    1 => self.left + (self.cw - self.label_width) / 2.0,
-                    2 => self.left + self.cw - 12.0 - self.label_width,
-                    _ => self.left + 12.0,
-                };
-                base_x + nested_section_label_offset()
-            } else {
-                self.left + (self.cw - self.label_width) / 2.0
-            };
-            let gap_margin = 6.0;
-            let gap_start = label_x - gap_margin;
-            let gap_end = label_x + self.label_width + gap_margin;
-            if gap_start > left_edge {
-                pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
-            }
-            if right_edge > gap_end {
-                pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
-            }
-        } else {
-            pc.rect(border, left_edge, y, w, 1.0);
-        }
-
-        let extra_bottom = pad + 12.0;
-        pc.rect(border, x, y + h + extra_bottom, w, 1.0);
-        pc.rect(border, x, y, 1.0, h + extra_bottom);
-        pc.rect(border, x + w - 1.0, y, 1.0, h + extra_bottom);
-        self.content_y + extra_bottom + 8.0
-    }
-
-    pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SectionVStack<'a> {
-        SectionVStack {
-            section: self,
-            pc,
-            spacing,
-        }
-    }
-}
-
-pub struct SectionVStack<'a> {
-    section: &'a mut Section,
-    pc: &'a mut dyn RenderTarget,
-    spacing: f32,
-}
-
-impl<'a> SectionVStack<'a> {
-    pub fn add_widget<T: WidgetHost + 'static>(&mut self, w: &mut T, ww: f32, wh: f32, ctx: &mut UiContext) {
-        self.section.widget(self.pc, w, Section::DEFAULT_MARGIN_X, ww, wh, ctx);
-        self.section.spacing(self.spacing);
-    }
-
-    pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, f: F)
-    where
-        F: FnMut(usize, f32, f32),
-    {
-        self.section.row(count, gap, h, f);
-        self.section.spacing(self.spacing);
-    }
-}
-
-
-pub struct SplitterLayout {
-    pub splitter1_x: f32,
-    pub splitter2_x: f32,
-    pub splitter_width: f32,
-    pub min_column_width: f32,
-}
-
-impl SplitterLayout {
-    pub fn new(width: f32, splitter_width: f32, min_column_width: f32) -> Self {
-        let s1 = (width - 2.0 * splitter_width) / 3.0;
-        let s2 = s1 + splitter_width + (width - 2.0 * splitter_width) / 3.0;
-        Self {
-            splitter1_x: s1,
-            splitter2_x: s2,
-            splitter_width,
-            min_column_width,
-        }
-    }
-
-    pub fn clamp(&mut self, total_width: f32, detached_circular_network: bool) {
-        if detached_circular_network {
-            let min_s2 = self.min_column_width;
-            let max_s2 = (total_width - self.min_column_width).max(min_s2);
-            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
-        } else {
-            let min_s1 = self.min_column_width;
-            let max_s1 = (self.splitter2_x - self.splitter_width - self.min_column_width).max(min_s1);
-            self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
-            let min_s2 = self.splitter1_x + self.splitter_width + self.min_column_width;
-            let max_s2 = (total_width - self.min_column_width).max(min_s2);
-            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
-        }
-    }
-
-    pub fn scale(&mut self, factor: f32) {
-        self.splitter1_x *= factor;
-        self.splitter2_x *= factor;
-    }
-
-    pub fn left_col(&self) -> (f32, f32) { // (x, width)
-        (0.0, self.splitter1_x)
-    }
-
-    pub fn center_col(&self) -> (f32, f32) { // (x, width)
-        let x = self.splitter1_x + self.splitter_width;
-        (x, self.splitter2_x - x)
-    }
-
-    pub fn right_col(&self, total_width: f32) -> (f32, f32) { // (x, width)
-        let x = self.splitter2_x + self.splitter_width;
-        (x, (total_width - x).max(0.0))
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct Grid {
-    pub left: f32,
-    pub top: f32,
-    pub width: f32,
-    pub col_width: f32,
-    pub gap: f32,
-    pub col_heights: Vec<f32>,
-    pub col_lefts: Vec<f32>,
-}
-
-impl Grid {
-    pub fn new(left: f32, top: f32, width: f32, min_col_width: f32, gap: f32, count: usize) -> Self {
-        let total_gap = gap * (count - 1) as f32;
-        let col_width = if count > 0 {
-            (width - total_gap).max(0.0) / count as f32
-        } else {
-            min_col_width
-        };
-        let left_offset = 0.0;
-
-        let mut col_lefts = Vec::with_capacity(count);
-        let col_heights = vec![top; count];
-        for i in 0..count {
-            col_lefts.push(left + left_offset + i as f32 * (col_width + gap));
-        }
-
-        Self {
-            left,
-            top,
-            width,
-            col_width,
-            gap,
-            col_heights,
-            col_lefts,
-        }
-    }
-
-    pub fn next_column(&self) -> usize {
-        let mut min_idx = 0;
-        let mut min_h = self.col_heights[0];
-        for i in 1..self.col_heights.len() {
-            if self.col_heights[i] < min_h {
-                min_h = self.col_heights[i];
-                min_idx = i;
-            }
-        }
-        min_idx
-    }
-
-    pub fn max_height(&self) -> f32 {
-        let mut max_h = self.col_heights[0];
-        for i in 1..self.col_heights.len() {
-            if self.col_heights[i] > max_h {
-                max_h = self.col_heights[i];
-            }
-        }
-        max_h
-    }
-}
-
-pub struct CircularPaneLayout {
-    pub x: f32,
-    pub y: f32,
-    pub r: f32,
-}
-
-impl CircularPaneLayout {
-    pub fn new(x: f32, y: f32, r: f32) -> Self {
-        Self { x, y, r }
-    }
-
-    pub fn hit_test_content(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist_sq = dx * dx + dy * dy;
-        dist_sq <= self.r * self.r && cy >= self.y - self.r + 45.0 + menubar_h + breadcrumb_h
-    }
-
-    pub fn hit_test_menubar(&self, cx: f32, cy: f32, menubar_h: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist = (dx * dx + dy * dy).sqrt();
-        dist >= self.r - menubar_h && dist <= self.r && cy < self.y
-    }
-
-    pub fn hit_test_breadcrumb(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist_sq = dx * dx + dy * dy;
-        dist_sq <= self.r * self.r && cy >= self.y - self.r + menubar_h && cy < self.y - self.r + 45.0 + menubar_h + breadcrumb_h
-    }
-
-    pub fn hit_test_border(&self, cx: f32, cy: f32, border_thickness: f32) -> bool {
-        let dx = cx - self.x;
-        let dy = cy - self.y;
-        let dist = (dx * dx + dy * dy).sqrt();
-        dist >= self.r - border_thickness && dist <= self.r
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct Radial {
-    pub center_x: f32,
-    pub center_y: f32,
-    pub aspect_ratio: f32,
-    pub base_spacing: f32,
-}
-
-impl Radial {
-    pub fn new(center_x: f32, center_y: f32, aspect_ratio: f32, base_spacing: f32) -> Self {
-        Self { center_x, center_y, aspect_ratio, base_spacing }
-    }
-
-    pub fn widget_rect(&self, idx: usize, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        if idx == 0 {
-            (self.center_x - ww / 2.0, self.center_y - wh / 2.0, ww, wh)
-        } else {
-            let mut ring = 1;
-            let mut ring_start = 1;
-            loop {
-                let ring_capacity = ring * 6;
-                if idx < ring_start + ring_capacity {
-                    let pos_in_ring = idx - ring_start;
-                    let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
-                    let radius = (ring as f32) * self.base_spacing;
-
-                    let x_offset = radius * angle.cos() * self.aspect_ratio;
-                    let y_offset = radius * angle.sin();
-
-                    return (
-                        self.center_x + x_offset - ww / 2.0,
-                        self.center_y + y_offset - wh / 2.0,
-                        ww,
-                        wh,
-                    );
-                }
-                ring_start += ring_capacity;
-                ring += 1;
-            }
-        }
-    }
-
-}
-
-
-
-pub trait LayoutStrategy: std::fmt::Debug {
-    fn init(&mut self, left: f32, top: f32, width: f32, height: f32);
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32);
-    fn set_section_count(&mut self, _count: usize) {}
-    fn get_column_width(&self) -> Option<f32> { None }
-    fn get_gap(&self) -> f32 { 20.0 }
-
-    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &mut crate::context::UiContext) -> f32;
-    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size;
-    fn box_clone(&self) -> Box<dyn LayoutStrategy>;
-}
-
-impl Clone for Box<dyn LayoutStrategy> {
-    fn clone(&self) -> Self {
-        self.box_clone()
-    }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FlexDirection {
-    Row,
-    Column,
-}
-
-#[derive(Debug, Clone)]
-pub struct FlexLayout {
-    left: f32,
-    top: f32,
-    width: f32,
-    height: f32,
-    direction: FlexDirection,
-    spacing: f32,
-    current_x: f32,
-    current_y: f32,
-}
-
-impl FlexLayout {
-    pub fn new(direction: FlexDirection, spacing: f32) -> Self {
-        Self {
-            left: 0.0,
-            top: 0.0,
-            width: 0.0,
-            height: 0.0,
-            direction,
-            spacing,
-            current_x: 0.0,
-            current_y: 0.0,
-        }
-    }
-}
-
-impl LayoutStrategy for FlexLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
-        self.left = left;
-        self.top = top;
-        self.width = width;
-        self.height = height;
-        self.current_x = left;
-        self.current_y = top;
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        match self.direction {
-            FlexDirection::Row => {
-                let rx = self.current_x;
-                let ry = self.current_y;
-                self.current_x += ww + self.spacing;
-                (rx, ry, ww, wh)
-            }
-            FlexDirection::Column => {
-                let rx = self.current_x;
-                let ry = self.current_y;
-                self.current_y += wh + self.spacing;
-                (rx, ry, ww, wh)
-            }
-        }
-    }
-
-    fn get_gap(&self) -> f32 {
-        self.spacing
-    }
-
-    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &mut crate::context::UiContext) -> f32 {
-        let (cur_x, cur_y) = (x, y);
-        // Blocks: the label row (`label_lead`) above every child's content, the
-        // gap between blocks, so a row's controls are level and a carve-out tab
-        // sits a full gap from its neighbour.
-        let lead = crate::widget::container::container_layout::label_lead(children);
-        match self.direction {
-            FlexDirection::Row => {
-                let mut cur_x = cur_x;
-                for &child_ptr in children {
-                    unsafe {
-                        let child = &mut *child_ptr;
-                        let child_w = child.rect().2;
-                        let child_h = crate::widget::container::container_layout::content_height(child);
-                        let use_h = if child_h > 0.0 { child_h } else { h };
-                        child.layout(
-                            crate::widget::Point { x: cur_x, y: cur_y + lead },
-                            crate::widget::LayoutConstraints::new(child_w, child_w, use_h, use_h),
-                            ctx,
-                        );
-                        cur_x += child_w + self.spacing;
-                    }
-                }
-                (cur_x - x).max(0.0)
-            }
-            FlexDirection::Column => {
-                let mut cur_y = cur_y;
-                for &child_ptr in children {
-                    unsafe {
-                        let child = &mut *child_ptr;
-                        let child_h = crate::widget::container::container_layout::content_height(child);
-                        let use_h = if child_h > 0.0 { child_h } else { 44.0 };
-                        child.layout(
-                            crate::widget::Point { x, y: cur_y + lead },
-                            crate::widget::LayoutConstraints::new(w, w, use_h, use_h),
-                            ctx,
-                        );
-                        cur_y += lead + use_h + self.spacing;
-                    }
-                }
-                (cur_y - y).max(0.0)
-            }
-        }
-    }
-
-    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
-        match self.direction {
-            FlexDirection::Row => {
-                let mut total_w = 0.0f32;
-                let mut max_h = 0.0f32;
-                for (i, &child_ptr) in children.iter().enumerate() {
-                    unsafe {
-                        let size = (*child_ptr).measure(constraints, ctx);
-                        total_w += size.width;
-                        max_h = max_h.max(size.height);
-                        if i > 0 {
-                            total_w += self.spacing;
-                        }
-                    }
-                }
-                crate::widget::Size {
-                    width: total_w.clamp(constraints.min_width, constraints.max_width),
-                    height: max_h.clamp(constraints.min_height, constraints.max_height),
-                }
-            }
-            FlexDirection::Column => {
-                let mut total_h = 0.0f32;
-                let mut max_w = 0.0f32;
-                for (i, &child_ptr) in children.iter().enumerate() {
-                    unsafe {
-                        let size = (*child_ptr).measure(constraints, ctx);
-                        total_h += size.height;
-                        max_w = max_w.max(size.width);
-                        if i > 0 {
-                            total_h += self.spacing;
-                        }
-                    }
-                }
-                crate::widget::Size {
-                    width: max_w.clamp(constraints.min_width, constraints.max_width),
-                    height: total_h.clamp(constraints.min_height, constraints.max_height),
-                }
-            }
-        }
-    }
-
-    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
-        Box::new(self.clone())
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct ColumnLayout {
-    left: f32,
-    top: f32,
-    width: f32,
-    current_y: f32,
-    gap: f32,
-}
-
-impl ColumnLayout {
-    pub fn new(gap: f32) -> Self {
-        Self {
-            left: 0.0,
-            top: 0.0,
-            width: 0.0,
-            current_y: 0.0,
-            gap,
-        }
-    }
-}
-
-impl LayoutStrategy for ColumnLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
-        self.left = left;
-        self.top = top;
-        self.width = width;
-        self.current_y = top;
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        let x = self.left;
-        let y = self.current_y;
-        self.current_y += wh + self.gap;
-        (x, y, ww, wh)
-    }
-
-    fn get_column_width(&self) -> Option<f32> {
-        Some(self.width)
-    }
-
-    fn get_gap(&self) -> f32 {
-        self.gap
-    }
-
-    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
-        let mut cur_y = y;
-        for &child_ptr in children {
-            unsafe {
-                let child = &mut *child_ptr;
-                let child_h = child.preferred_height().unwrap_or(child.rect().3);
-                let use_h = if child_h > 0.0 { child_h } else { 44.0 };
-                child.set_rect(x, cur_y, w, use_h);
-                cur_y += use_h + self.gap;
-            }
-        }
-        (cur_y - y).max(0.0)
-    }
-
-    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
-        let mut total_h = 0.0f32;
-        let mut max_w = 0.0f32;
-        for (i, &child_ptr) in children.iter().enumerate() {
-            unsafe {
-                let size = (*child_ptr).measure(constraints, ctx);
-                total_h += size.height;
-                max_w = max_w.max(size.width);
-                if i > 0 {
-                    total_h += self.gap;
-                }
-            }
-        }
-        crate::widget::Size {
-            width: max_w.clamp(constraints.min_width, constraints.max_width),
-            height: total_h.clamp(constraints.min_height, constraints.max_height),
-        }
-    }
-
-    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
-        Box::new(self.clone())
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct AdaptiveGrid {
-    grid: Option<Grid>,
-    #[allow(dead_code)]
-    min_col_width: f32,
-    #[allow(dead_code)]
-    gap: f32,
-    num_sections: Option<usize>,
-}
-
-impl AdaptiveGrid {
-    pub fn new(min_col_width: f32, gap: f32) -> Self {
-        Self {
-            grid: None,
-            min_col_width,
-            gap,
-            num_sections: None,
-        }
-    }
-}
-
-impl LayoutStrategy for AdaptiveGrid {
-    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
-        let min_col_width = crate::layout::grid_min_col_width();
-        let gap = crate::layout::grid_gap();
-        let max_cols = ((width + gap) / (min_col_width + gap)).floor().max(1.0) as usize;
-        let count = if let Some(n) = self.num_sections {
-            n.min(max_cols).max(1)
-        } else {
-            max_cols
-        };
-        self.grid = Some(Grid::new(left, top, width, min_col_width, gap, count));
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        if let Some(ref mut grid) = self.grid {
-            let num_cols = grid.col_heights.len();
-            let num_cols_spanned = (((ww + grid.gap) / (grid.col_width + grid.gap)).round() as usize)
-                .min(num_cols)
-                .max(1);
-
-            if num_cols_spanned >= num_cols {
-                let y = grid.max_height();
-                let x = grid.left;
-                let allocated_w = grid.width;
-                grid.col_heights.fill(y + wh + grid.gap);
-                (x, y, allocated_w, wh)
-            } else if num_cols_spanned == 1 {
-                let col = grid.next_column();
-                let x = grid.col_lefts[col];
-                let y = grid.col_heights[col];
-                grid.col_heights[col] += wh + grid.gap;
-                (x, y, grid.col_width, wh)
-            } else {
-                let n = num_cols_spanned;
-                let mut best_start_col = 0;
-                let mut min_max_h = f32::MAX;
-                for c in 0..=(num_cols - n) {
-                    let mut max_h = 0.0f32;
-                    for i in 0..n {
-                        if grid.col_heights[c + i] > max_h {
-                            max_h = grid.col_heights[c + i];
-                        }
-                    }
-                    if max_h < min_max_h {
-                        min_max_h = max_h;
-                        best_start_col = c;
-                    }
-                }
-                let x = grid.col_lefts[best_start_col];
-                let y = min_max_h;
-                let allocated_w = n as f32 * grid.col_width + (n - 1) as f32 * grid.gap;
-                for i in 0..n {
-                    grid.col_heights[best_start_col + i] = y + wh + grid.gap;
-                }
-                (x, y, allocated_w, wh)
-            }
-        } else {
-            (0.0, 0.0, 0.0, wh)
-        }
-    }
-
-    fn set_section_count(&mut self, count: usize) {
-        self.num_sections = Some(count);
-    }
-
-    fn get_column_width(&self) -> Option<f32> {
-        self.grid.as_ref().map(|g| g.col_width)
-    }
-
-    fn get_gap(&self) -> f32 {
-        crate::layout::grid_gap()
-    }
-
-    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
-        let usable_w = w.max(1.0);
-        let min_col_width = crate::layout::grid_min_col_width();
-        let gap = crate::layout::grid_gap();
-        let cols = (((usable_w + gap) / (min_col_width + gap)).floor().max(1.0)) as usize;
-        let count = if let Some(n) = self.num_sections {
-            n.min(cols).max(1)
-        } else {
-            cols
-        };
-
-        let total_gap = gap * (count - 1) as f32;
-        let available_w = (w - total_gap).max(1.0);
-        let col_w = available_w / count as f32;
-        
-        let mut col_heights = vec![y; count];
-
-        for &child_ptr in children {
-            unsafe {
-                let child = &mut *child_ptr;
-                let ch = child.preferred_height().unwrap_or(child.rect().3);
-                let use_h = if ch > 0.0 { ch } else { 44.0 };
-                
-                let mut min_col = 0;
-                let mut min_h = col_heights[0];
-                for i in 1..count {
-                    if col_heights[i] < min_h {
-                        min_h = col_heights[i];
-                        min_col = i;
-                    }
-                }
-                
-                let cx = x + min_col as f32 * (col_w + gap);
-                let cy = col_heights[min_col];
-                child.set_rect(cx, cy, col_w, use_h);
-                col_heights[min_col] += use_h + gap;
-            }
-        }
-        
-        let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
-        (max_h - y).max(0.0)
-    }
-
-    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
-        let usable_w = constraints.max_width.max(1.0);
-        let min_col_width = crate::layout::grid_min_col_width();
-        let gap = crate::layout::grid_gap();
-        let cols = (((usable_w + gap) / (min_col_width + gap)).floor().max(1.0)) as usize;
-        let count = if let Some(n) = self.num_sections {
-            n.min(cols).max(1)
-        } else {
-            cols
-        };
-
-        let mut col_heights = vec![0.0f32; count];
-        let total_gap = gap * (count - 1) as f32;
-        let available_w = (constraints.max_width - total_gap).max(1.0);
-        let col_w = available_w / count as f32;
-        
-        let child_constraints = crate::widget::LayoutConstraints::new(col_w, col_w, constraints.min_height, constraints.max_height);
-
-        for &child_ptr in children {
-            unsafe {
-                let size = (*child_ptr).measure(child_constraints, ctx);
-                let mut min_col = 0;
-                let mut min_h = col_heights[0];
-                for i in 1..count {
-                    if col_heights[i] < min_h {
-                        min_h = col_heights[i];
-                        min_col = i;
-                    }
-                }
-                col_heights[min_col] += size.height + gap;
-            }
-        }
-        
-        let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
-        crate::widget::Size {
-            width: constraints.max_width,
-            height: max_h.clamp(constraints.min_height, constraints.max_height),
-        }
-    }
-
-    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
-        Box::new(self.clone())
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct RadialLayout {
-    radial: Option<Radial>,
-    aspect_ratio: f32,
-    base_spacing: f32,
-    idx: usize,
-}
-
-impl RadialLayout {
-    pub fn new(aspect_ratio: f32, base_spacing: f32) -> Self {
-        Self {
-            radial: None,
-            aspect_ratio,
-            base_spacing,
-            idx: 0,
-        }
-    }
-}
-
-impl LayoutStrategy for RadialLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
-        let cx = left + width / 2.0;
-        let cy = top + height / 2.0;
-        let aspect = if self.aspect_ratio > 0.0 {
-            self.aspect_ratio
-        } else {
-            let screen_aspect = (width / height.max(1.0)).max(0.1);
-            1.0 + (screen_aspect - 1.0) * 0.4
-        };
-        self.radial = Some(Radial::new(cx, cy, aspect, self.base_spacing));
-        self.idx = 0;
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        if let Some(ref radial) = self.radial {
-            let rect = radial.widget_rect(self.idx, ww, wh);
-            self.idx += 1;
-            rect
-        } else {
-            (0.0, 0.0, ww, wh)
-        }
-    }
-
-    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
-        let cx = x + w / 2.0;
-        let cy = y + h / 2.0;
-        let aspect = if self.aspect_ratio > 0.0 {
-            self.aspect_ratio
-        } else {
-            let screen_aspect = (w / h.max(1.0)).max(0.1);
-            1.0 + (screen_aspect - 1.0) * 0.4
-        };
-        let radial = Radial::new(cx, cy, aspect, self.base_spacing);
-        for (idx, &child_ptr) in children.iter().enumerate() {
-            unsafe {
-                let child = &mut *child_ptr;
-                let cw = child.rect().2;
-                let ch = child.preferred_height().unwrap_or(child.rect().3);
-                let use_h = if ch > 0.0 { ch } else { 44.0 };
-                let (rx, ry, rw, rh) = radial.widget_rect(idx, cw, use_h);
-                child.set_rect(rx, ry, rw, rh);
-            }
-        }
-        h
-    }
-
-    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
-        let cx = constraints.max_width / 2.0;
-        let cy = constraints.max_height / 2.0;
-        let aspect = if self.aspect_ratio > 0.0 {
-            self.aspect_ratio
-        } else {
-            let screen_aspect = (constraints.max_width / constraints.max_height.max(1.0)).max(0.1);
-            1.0 + (screen_aspect - 1.0) * 0.4
-        };
-        let radial = Radial::new(cx, cy, aspect, self.base_spacing);
-        let mut max_w = 0.0f32;
-        let mut max_h = 0.0f32;
-        for (idx, &child_ptr) in children.iter().enumerate() {
-            unsafe {
-                let size = (*child_ptr).measure(constraints, ctx);
-                let (rx, ry, rw, rh) = radial.widget_rect(idx, size.width, size.height);
-                max_w = max_w.max(rx + rw);
-                max_h = max_h.max(ry + rh);
-            }
-        }
-        crate::widget::Size {
-            width: max_w.clamp(constraints.min_width, constraints.max_width),
-            height: max_h.clamp(constraints.min_height, constraints.max_height),
-        }
-    }
-
-    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
-        Box::new(self.clone())
-    }
-}
-
-/// Vertical slack for a section's content clip. The clip exists to stop
-/// content escaping its section SIDEWAYS, which is the axis a section's width
-/// actually fixes; a section's height is only known once its content has been
-/// placed, so bounding that axis too would risk cutting content off rather
-/// than keeping it in. Deliberately far larger than any section.
-const SECTION_CLIP_SLACK: f32 = 100_000.0;
-
-pub struct PageLayoutBuilder<'a, P> {
-    pub strategy: &'a mut dyn LayoutStrategy,
-    pub cx: f32,
-    pub cy: f32,
-    pub cw: f32,
-    pub ch: f32,
-    pub section_width: f32,
-    pub idx: usize,
-    _phantom: std::marker::PhantomData<P>,
-}
-
-impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
-    pub fn new(
-        strategy: &'a mut dyn LayoutStrategy,
-        cx: f32,
-        cy: f32,
-        cw: f32,
-        ch: f32,
-        section_width: f32,
-    ) -> Self {
-        strategy.init(cx, cy, cw, ch);
-        Self {
-            strategy,
-            cx,
-            cy,
-            cw,
-            ch,
-            section_width,
-            idx: 0,
-            _phantom: std::marker::PhantomData,
-        }
-    }
-
-    pub fn with_section_count(self, count: usize) -> Self {
-        self.strategy.set_section_count(count);
-        self.strategy.init(self.cx, self.cy, self.cw, self.ch);
-        self
-    }
-
-    pub fn add_section<F>(&mut self, final_pc: &mut P, label: &str, focused: bool, mut render_fn: F)
-    where
-        F: FnMut(&mut SectionContext<'_, P>),
-    {
-        let width = self.strategy.get_column_width().unwrap_or(self.section_width);
-        let mut dummy = P::default();
-        let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused, false);
-        render_fn(&mut dummy_ctx);
-        let wh = dummy_ctx.finish();
-        let (rx, ry, rw, _) = self.strategy.allocate(width, wh);
-        let mut real_ctx = SectionContext::new(final_pc, rx, ry, rw, label, focused, false);
-        let (clip_x, clip_w) = (real_ctx.content_left(), real_ctx.content_width());
-        real_ctx.pc.push_clip_rect(clip_x, ry - SECTION_CLIP_SLACK, clip_w, 2.0 * SECTION_CLIP_SLACK);
-        render_fn(&mut real_ctx);
-        real_ctx.pc.pop_clip_rect();
-        real_ctx.finish();
-        self.idx += 1;
-    }
-
-    pub fn add_section_with_width<F>(&mut self, final_pc: &mut P, width: f32, label: &str, focused: bool, mut render_fn: F)
-    where
-        F: FnMut(&mut SectionContext<'_, P>),
-    {
-        let mut dummy = P::default();
-        let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused, false);
-        render_fn(&mut dummy_ctx);
-        let wh = dummy_ctx.finish();
-        let (rx, ry, rw, _) = self.strategy.allocate(width, wh);
-        let mut real_ctx = SectionContext::new(final_pc, rx, ry, rw, label, focused, false);
-        let (clip_x, clip_w) = (real_ctx.content_left(), real_ctx.content_width());
-        real_ctx.pc.push_clip_rect(clip_x, ry - SECTION_CLIP_SLACK, clip_w, 2.0 * SECTION_CLIP_SLACK);
-        render_fn(&mut real_ctx);
-        real_ctx.pc.pop_clip_rect();
-        real_ctx.finish();
-        self.idx += 1;
-    }
-
-    pub fn add_section_spanned<F>(&mut self, final_pc: &mut P, label: &str, span: usize, focused: bool, render_fn: F)
-    where
-        F: FnMut(&mut SectionContext<'_, P>),
-    {
-        let col_width = self.strategy.get_column_width().unwrap_or(self.section_width);
-        let gap = self.strategy.get_gap();
-        let width = span as f32 * col_width + (span - 1) as f32 * gap;
-        self.add_section_with_width(final_pc, width, label, focused, render_fn);
-    }
-}
-
-pub struct SectionContext<'a, P> {
-    pub pc: &'a mut P,
-    pub left: f32,
-    pub top: f32,
-    pub content_y: f32,
-    pub cw: f32,
-    pub label_width: f32,
-    pub label_x: f32,
-    /// Whether the host renders sections as sunken wells (`section_relief_style`).
-    pub relief_style: bool,
-    /// The title tab box (x, y, w, h) when the host's relief styling laid the
-    /// label out left-aligned — `finish` offers it with the section carve.
-    /// None under relief styling means a label-less section: the well carves
-    /// tabless, flush with the allocation top.
-    pub relief_tab: Option<(f32, f32, f32, f32)>,
-    pub focused: bool,
-    pub is_child: bool,
-    pub grid: Grid,
-    pub last_col: usize,
-    pub content_start_y: f32,
-    pub row_gap: f32,
-}
-
-impl<'a, P: RenderTarget> SectionContext<'a, P> {
-    pub const DEFAULT_MARGIN_X: f32 = 12.0;
-    pub const DEFAULT_ROW_GAP: f32 = 8.0;
-
-    fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
-        estimate_label_width_helper(label, font_size, font_fam)
-    }
-
-    pub fn padding(&self) -> f32 {
-        section_padding()
-    }
-
-    pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool, is_child: bool) -> Self {
-        let font_setting = if is_child {
-            nested_section_label_font()
-        } else {
-            section_label_font()
-        };
-        let (font_fam, font_size_opt) = parse_font_string(&font_setting);
-        let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
-        let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
-        let label_width = Self::estimate_label_width(label, font_size, &font_fam);
-        let relief_style = pc.section_relief_style();
-        let label_x = if is_child {
-            let base_x = match nested_section_label_alignment() {
-                0 => left + 12.0,
-                1 => left + (cw - label_width) / 2.0,
-                2 => left + cw - 12.0 - label_width,
-                _ => left + 12.0,
-            };
-            base_x + nested_section_label_offset()
-        } else if relief_style {
-            // Sunken style: the title sits in a tab flush with the well's left
-            // edge (the designer look), not centered on the border.
-            left + 12.0
-        } else {
-            left + (cw - label_width) / 2.0
-        };
-        // Under relief styling the well fills the whole allocated rect (so the
-        // page's gaps and margins are the visual gaps) — the tab tops the
-        // allocation and the label centers inside it.
-        let label_y = if relief_style { top + 4.0 } else { top };
-        pc.text_with_font(label, label_x, label_y, font_size, font_color, &font_fam);
-
-        // The tab wraps the label, flush on the well's top-left corner; clamped
-        // so off-default child alignments can't push it outside the well.
-        let relief_tab = if relief_style && label_width > 0.0 {
-            let tab_x = (label_x - 12.0).max(left);
-            Some((tab_x, top, label_width + 24.0, font_size + 10.0))
-        } else {
-            None
-        };
-
-        let pad = section_padding();
-        let margin_x = 2.0 * pad + 12.0;
-        let usable_w = (cw - 2.0 * margin_x).max(1.0);
-        let min_col_width = 130.0;
-        let gap = 8.0;
-        let max_cols = if is_child {
-            1
-        } else {
-            ((usable_w + gap) / (min_col_width + gap)).floor().clamp(1.0, 2.0) as usize
-        };
-        // Under relief styling the content stands off the well's top wall by
-        // the same inset it keeps from the side walls (`margin_x`, which is
-        // also what `finish` leaves below it), so a well reads as one even
-        // frame. The outline style's `pad + 19` predates the title tab: under
-        // relief the tab alone is `font_size + 10` tall, which left the first
-        // line touching the well's top wall at the default padding while the
-        // sides kept 20px and more.
-        let content_start_y = if relief_style {
-            relief_tab.map(|t| t.1 + t.3).unwrap_or(top) + margin_x
-        } else {
-            top + pad + 19.0
-        };
-        let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
-
-        Self {
-            pc,
-            left,
-            top,
-            content_y: content_start_y,
-            cw,
-            label_width,
-            label_x,
-            relief_style,
-            relief_tab,
-            focused,
-            is_child,
-            grid,
-            last_col: usize::MAX,
-            content_start_y,
-            row_gap: Self::DEFAULT_ROW_GAP,
-        }
-    }
-
-    pub fn with_row_gap(mut self, gap: f32) -> Self {
-        self.row_gap = gap;
-        self
-    }
-
-    /// The section's content-box top edge: the body well's top (the tab's
-    /// bottom) under relief styling, the outline's border line otherwise. Lets
-    /// a page place content at an exact inset from the well's walls.
-    pub fn well_top(&self) -> f32 {
-        if self.relief_style {
-            self.relief_tab.map(|t| t.1 + t.3).unwrap_or(self.top)
-        } else {
-            self.top + 7.0
-        }
-    }
-
-    pub fn set_row_gap(&mut self, gap: f32) {
-        self.row_gap = gap;
-    }
-
-    /// Horizontal inset of section CONTENT from the section's left edge.
-    ///
-    /// One number, used by every content placer in here — `row_layout`, the
-    /// column `Grid`, `widget`, `VStack` and `ax` (so `text`) — because they
-    /// share a section and have to line up inside it. The section's border is
-    /// drawn at `left + padding()` (see `finish`), so content clears the
-    /// border by `padding() + 12`.
-    pub fn content_margin(&self) -> f32 {
-        2.0 * self.padding() + 12.0
-    }
-
-    /// Left edge of the content box: where a row, a widget or a `text(_, 12.0,
-    /// ..)` starts.
-    pub fn content_left(&self) -> f32 {
-        self.left + self.content_margin()
-    }
-
-    /// Width of the content box — the section's width less the inset on both
-    /// sides. Nothing a section draws should extend past `content_left() +
-    /// content_width()`.
-    pub fn content_width(&self) -> f32 {
-        (self.cw - 2.0 * self.content_margin()).max(0.0)
-    }
-
-    /// `x_off` px into the content box's coordinate space, where 12.0 is the
-    /// content's own left edge — the offset 46 of the ~55 call sites in the
-    /// tree already pass, and the one that lines text up with the buttons and
-    /// widgets beside it.
-    ///
-    /// This used to add `padding()` only when `x_off >= 12.0`, which made the
-    /// mapping DISCONTINUOUS: asking for 11 instead of 12 moved the text 5px
-    /// LEFT rather than 1px, and silently dropped it out of alignment with
-    /// every row in the same section. `cce-mail` and two others sit on the
-    /// wrong side of that cliff today.
-    pub fn ax(&self, x_off: f32) -> f32 {
-        self.left + 2.0 * self.padding() + x_off
-    }
-
-    pub fn ay(&self) -> f32 {
-        self.content_y
-    }
-
-    pub fn spacing(&mut self, dy: f32) {
-        if self.grid.col_heights.len() >= 2 {
-            if self.last_col == usize::MAX {
-                for h in &mut self.grid.col_heights {
-                    *h += dy;
-                }
-            } else if self.last_col < self.grid.col_heights.len() {
-                self.grid.col_heights[self.last_col] += dy;
-            }
-            self.content_y = self.grid.max_height();
-        } else {
-            self.content_y += dy;
-            for h in &mut self.grid.col_heights {
-                *h += dy;
-            }
-        }
-    }
-
-    pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
-        let mut y = self.content_y + y_off;
-        if y > self.content_start_y {
-            y += self.row_gap;
-        }
-        let x = self.ax(x_off);
-        // Bound it to the content box. A section's text was drawn unbounded,
-        // so a string wider than its section simply kept going — over the
-        // border, over whatever sat to the right, and off the window (the
-        // settings app's GPU names did all three). Rows and widgets have
-        // always been sized to the section; text was the one thing that could
-        // leave it. The vertical band is generous on purpose: it is the
-        // horizontal overrun that has to be cut, and a tight band would
-        // shave descenders.
-        let right = self.content_left() + self.content_width();
-        let bounds = if right > x {
-            Some([x, y - font_size, right, y + 2.0 * font_size])
-        } else {
-            None
-        };
-        self.pc.text_with_bounds(text, x, y, font_size, color, bounds);
-        let new_bottom = y + font_size + 4.0;
-        self.content_y = new_bottom;
-        self.grid.col_heights.fill(new_bottom);
-    }
-
-    pub fn widget<T: WidgetHost + 'static>(&mut self, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
-        if let Some(pref) = w.preferred_height() {
-            wh = pref;
-        }
-        let pad = self.padding();
-        let top_room = w.label_strip();
-        let total_h = wh + top_room;
-
-        let name = w.type_name();
-        let span_full = name == "Trackpad"
-            || name == "Canvas"
-            || name == "UsageBar"
-            || name == "ProgressBar"
-            || name == "ButtonStrip"
-            || name == "Spreadsheet"
-            || name == "Graph";
-
-        if span_full {
-            let margin_x = 2.0 * pad + 12.0;
-            let x = self.left + margin_x;
-            let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
-            let mut max_h = self.grid.max_height().max(self.content_y);
-            if max_h > self.content_start_y {
-                max_h += self.row_gap;
-            }
-            let y = max_h;
-
-            w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
-            render_widget(self.pc, w, x, y, clamped_w, total_h, ctx);
-
-            let new_bottom = y + total_h;
-            self.content_y = new_bottom;
-            self.grid.col_heights.fill(new_bottom);
-        } else {
-            let max_h = self.grid.max_height();
-            if self.content_y > max_h {
-                self.grid.col_heights.fill(self.content_y);
-            }
-
-            let col = self.grid.next_column();
-            self.last_col = col;
-            let x = self.grid.col_lefts[col];
-            let mut y = self.grid.col_heights[col];
-            if y > self.content_start_y {
-                y += self.row_gap;
-            }
-
-            let aligned_x = x;
-            let aligned_w = self.grid.col_width;
-
-            w.set_row_rect(aligned_x, aligned_w);
-            render_widget(self.pc, w, aligned_x, y, aligned_w, total_h, ctx);
-            self.grid.col_heights[col] = y + total_h;
-            self.content_y = self.grid.max_height();
-        }
-    }
-
-    pub fn widget_full<T: WidgetHost + 'static>(&mut self, w: &mut T, wh: f32, ctx: &mut UiContext) {
-        let x_off = 12.0;
-        let ww = self.cw - 2.0 * (self.padding() + x_off); // cw - 40.0
-        self.widget(w, x_off, ww, wh, ctx);
-    }
-
-    pub fn separator(&mut self) {
-        let pad = self.padding();
-        let x = self.ax(pad);
-        let max_h = self.grid.max_height().max(self.content_y);
-        let y = max_h;
-        self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
-        self.content_y = max_h + 8.0;
-        self.grid.col_heights.fill(self.content_y);
-    }
-
-    pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
-        let max_h = self.grid.max_height().max(self.content_y);
-        self.pc.rect(color, self.ax(x_off), max_h, w, h);
-        self.content_y = max_h + h;
-        self.grid.col_heights.fill(self.content_y);
-    }
-
-    pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
-        let margin_x = self.content_margin();
-        let usable_w = self.content_width();
-        if count == 0 {
-            return Vec::new();
-        }
-        let total_gap = gap * (count - 1) as f32;
-        let col_w = (usable_w - total_gap).max(0.0) / count as f32;
-
-        let mut cols = Vec::with_capacity(count);
-        for i in 0..count {
-            let x = self.left + margin_x + i as f32 * (col_w + gap);
-            cols.push((x, col_w));
-        }
-        cols
-    }
-
-    /// A row whose columns are sized to what goes IN them: each gets the width
-    /// it asked for in `needs`, and whatever is left over is shared equally.
-    ///
-    /// `row_layout` splits a row evenly and knows nothing about content, so it
-    /// hands "Reboot" and "Hibernate" the same width — one floats in slack
-    /// while the other is cut off, which is what a row of mismatched labels
-    /// looks like. Sharing the SLACK equally rather than sizing proportionally
-    /// is deliberate: proportional widths would make a two-character label a
-    /// sliver, where what is wanted is "everyone fits, then everyone gets the
-    /// same bonus".
-    ///
-    /// When the needs do not fit, every column is scaled by the same factor, so
-    /// the row still cannot overflow its section and the shortfall is shared
-    /// rather than landing entirely on the last column.
-    pub fn row_layout_for(&self, needs: &[f32], gap: f32) -> Vec<(f32, f32)> {
-        let count = needs.len();
-        if count == 0 {
-            return Vec::new();
-        }
-        let total_gap = gap * (count - 1) as f32;
-        let room = (self.content_width() - total_gap).max(0.0);
-        let total_need: f32 = needs.iter().map(|n| n.max(0.0)).sum();
-
-        let widths: Vec<f32> = if total_need <= room {
-            let extra = (room - total_need) / count as f32;
-            needs.iter().map(|n| n.max(0.0) + extra).collect()
-        } else if total_need > 0.0 {
-            let scale = room / total_need;
-            needs.iter().map(|n| n.max(0.0) * scale).collect()
-        } else {
-            vec![room / count as f32; count]
-        };
-
-        let mut cols = Vec::with_capacity(count);
-        let mut x = self.content_left();
-        for w in widths {
-            cols.push((x, w));
-            x += w + gap;
-        }
-        cols
-    }
-
-
-    pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
-    where
-        F: FnMut(usize, f32, f32),
-    {
-        let max_h = self.grid.max_height().max(self.content_y);
-        self.grid.col_heights.fill(max_h);
-        self.content_y = max_h;
-
-        let cols = self.row_layout(count, gap);
-        for (i, &(x, w)) in cols.iter().enumerate() {
-            f(i, x, w);
-        }
-        self.content_y += h;
-
-        self.grid.col_heights.fill(self.content_y);
-    }
-
-    pub fn vstack(&mut self, spacing: f32) -> VStack<'_, 'a, P> {
-        VStack {
-            context: self,
-            spacing,
-        }
-    }
-
-    pub fn add_section<F>(&mut self, label: &str, focused: bool, mut render_fn: F)
-    where
-        F: FnMut(&mut SectionContext<'_, P>),
-    {
-        let pad = self.padding();
-        let (left, top, cw, is_side_by_side) = if self.grid.col_heights.len() >= 2 {
-            let col = self.grid.next_column();
-            self.last_col = col;
-            let x = self.grid.col_lefts[col];
-            let mut y = self.grid.col_heights[col];
-            if y > self.content_start_y {
-                y += self.row_gap;
-            }
-            (x, y, self.grid.col_width, true)
-        } else {
-            let left = self.ax(0.0) + pad;
-            let mut max_h = self.grid.max_height().max(self.content_y);
-            if max_h > self.content_start_y {
-                max_h += self.row_gap;
-            }
-            let top = max_h;
-            let cw = self.cw - 2.0 * pad;
-            (left, top, cw, false)
-        };
-
-        let mut sub_ctx = SectionContext::new(self.pc, left, top, cw, label, focused, true);
-        render_fn(&mut sub_ctx);
-        let new_bottom = sub_ctx.finish();
-
-        if is_side_by_side {
-            let col = self.last_col;
-            self.grid.col_heights[col] = new_bottom;
-            self.content_y = self.grid.max_height();
-        } else {
-            self.content_y = new_bottom;
-            self.grid.col_heights.fill(new_bottom);
-        }
-    }
-
-    pub fn finish(self) -> f32 {
-        let border: [f32; 4] = if self.is_child {
-            if self.focused {
-                [0.22, 0.38, 0.24, 1.0]
-            } else {
-                [0.18, 0.18, 0.25, 1.0]
-            }
-        } else {
-            if self.focused {
-                [0.30, 0.50, 0.32, 1.0] // Focused green
-            } else {
-                [0.25, 0.25, 0.35, 1.0] // Default gray
-            }
-        };
-        let pad = self.padding();
-        let x = self.left + pad;
-        let y = self.top + 7.0;
-        let w = self.cw - 2.0 * pad;
-        let h = self.content_y - y;
-        // Under relief the well's walls are the allocation's edges, so the
-        // floor below the content matches the inset beside and above it (see
-        // `new`). The outline frame sits `pad` in from its allocation and
-        // keeps its own slack.
-        let extra_bottom = if self.relief_style { self.content_margin() } else { pad + 12.0 };
-        let bottom = y + h + extra_bottom;
-
-        if self.relief_style {
-            // Sunken style: the well spans the full allocated rect — body top
-            // edge at the tab's bottom (the tab is flush ON the body, the
-            // designer union shape; label-less sections carve tabless from the
-            // allocation top), walls on the allocation's edges, and no
-            // trailing slack so the layout gap IS the visual gap.
-            let body_y = self.relief_tab.map(|t| t.1 + t.3).unwrap_or(self.top);
-            let frame = SectionFrame {
-                x: self.left,
-                y: body_y,
-                w: self.cw,
-                h: bottom - body_y,
-                tab: self.relief_tab,
-                focused: self.focused,
-                is_child: self.is_child,
-            };
-            if self.pc.section_relief(&frame) {
-                return self.content_y + extra_bottom;
-            }
-        }
-
-        let left_edge = x;
-        let right_edge = x + w;
-        if self.label_width > 0.0 {
-            let label_x = self.label_x;
-            let gap_margin = 6.0;
-            let gap_start = label_x - gap_margin;
-            let gap_end = label_x + self.label_width + gap_margin;
-            if gap_start > left_edge {
-                self.pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
-            }
-            if right_edge > gap_end {
-                self.pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
-            }
-        } else {
-            self.pc.rect(border, left_edge, y, w, 1.0);
-        }
-
-        self.pc.rect(border, x, y + h + extra_bottom, w, 1.0);
-        self.pc.rect(border, x, y, 1.0, h + extra_bottom);
-        self.pc.rect(border, x + w - 1.0, y, 1.0, h + extra_bottom);
-        self.content_y + extra_bottom + 8.0
-    }
-}
-
-
-pub struct VStack<'b, 'a, P> {
-    pub context: &'b mut SectionContext<'a, P>,
-    pub spacing: f32,
-}
-
-impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
-    pub fn add_widget<T: WidgetHost + 'static>(&mut self, w: &mut T, _ww: f32, wh: f32, ctx: &mut UiContext) {
-        let pad = self.context.padding();
-        let margin_x = 2.0 * pad + 12.0;
-        let x = self.context.left + margin_x;
-        let clamped_w = (self.context.cw - 2.0 * margin_x).max(0.0);
-
-        let mut max_h = self.context.grid.max_height().max(self.context.content_y);
-        if max_h > self.context.content_start_y {
-            max_h += self.context.row_gap;
-        }
-        let y = max_h;
-
-        let pref_h = w.preferred_height().unwrap_or(wh);
-        let top_room = w.label_strip();
-        let total_h = pref_h + top_room;
-
-        w.set_row_rect(self.context.left + pad, self.context.cw - 2.0 * pad);
-        render_widget(self.context.pc, w, x, y, clamped_w, total_h, ctx);
-
-        let new_bottom = y + total_h;
-        self.context.content_y = new_bottom;
-        self.context.grid.col_heights.fill(new_bottom);
-        self.context.spacing(self.spacing);
-    }
-
-    /// [`add_row`](Self::add_row) with per-column widths from `needs` — see
-    /// [`SectionContext::row_layout_for`]. For a row of buttons, `needs` is
-    /// each label's measured width plus the plate's own inset.
-    pub fn add_row_for<F>(&mut self, needs: &[f32], gap: f32, h: f32, mut f: F)
-    where
-        F: FnMut(&mut SectionContext<'a, P>, usize, f32, f32),
-    {
-        let max_h = self.context.grid.max_height().max(self.context.content_y);
-        self.context.grid.col_heights.fill(max_h);
-        self.context.content_y = max_h;
-
-        let cols = self.context.row_layout_for(needs, gap);
-        for (i, &(x, w)) in cols.iter().enumerate() {
-            self.context.content_y = max_h;
-            f(self.context, i, x, w);
-        }
-
-        let new_bottom = max_h + h;
-        self.context.content_y = new_bottom;
-        self.context.grid.col_heights.fill(new_bottom);
-        self.context.spacing(self.spacing);
-    }
-
-    pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
-    where
-        F: FnMut(&mut SectionContext<'a, P>, usize, f32, f32),
-    {
-        let max_h = self.context.grid.max_height().max(self.context.content_y);
-        self.context.grid.col_heights.fill(max_h);
-        self.context.content_y = max_h;
-
-        let cols = self.context.row_layout(count, gap);
-        for (i, &(x, w)) in cols.iter().enumerate() {
-            self.context.content_y = max_h;
-            f(self.context, i, x, w);
-        }
-
-        let new_bottom = max_h + h;
-        self.context.content_y = new_bottom;
-        self.context.grid.col_heights.fill(new_bottom);
-        self.context.spacing(self.spacing);
-    }
-}
-
-pub fn get_system_monospace_font() -> &'static str {
-    static MONOSPACE_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
-    MONOSPACE_FONT.get_or_init(|| {
-        if let Ok(output) = std::process::Command::new("fc-match")
-            .args(["-f", "%{family}", "monospace"])
-            .output()
-        {
-            let name = String::from_utf8_lossy(&output.stdout);
-            let parsed = name.split(',').next().unwrap_or("monospace").trim();
-            if !parsed.is_empty() {
-                return parsed.to_string();
-            }
-        }
-        "monospace".to_string()
-    })
-}
-
-pub fn get_system_sans_serif_font() -> &'static str {
-    static SANS_SERIF_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
-    SANS_SERIF_FONT.get_or_init(|| {
-        if let Ok(output) = std::process::Command::new("fc-match")
-            .args(["-f", "%{family}", "sans-serif"])
-            .output()
-        {
-            let name = String::from_utf8_lossy(&output.stdout);
-            let parsed = name.split(',').next().unwrap_or("sans-serif").trim();
-            if !parsed.is_empty() {
-                return parsed.to_string();
-            }
-        }
-        "sans-serif".to_string()
-    })
-}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 19efb37..0d0e0b0 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -74,8 +74,10 @@ mod registry;
 pub use registry::*;
 mod bridge;
 pub use bridge::*;
-mod legacy;
-pub use legacy::*;
+mod section;
+pub use section::*;
+mod panes;
+pub use panes::*;
 
 static SECTION_PADDING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SECTION_PADDING, |s| &mut s.layout.SECTION_PADDING);
 
@@ -3501,30 +3503,6 @@ pub fn read_preferred_fonts() -> (String, String, String, String) {
     )
 }
 
-impl crate::widget::ContainerLayout for FlexLayout {
-    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
-        Box::new(self.clone())
-    }
-}
-
-impl crate::widget::ContainerLayout for ColumnLayout {
-    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
-        Box::new(self.clone())
-    }
-}
-
-impl crate::widget::ContainerLayout for AdaptiveGrid {
-    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
-        Box::new(self.clone())
-    }
-}
-
-impl crate::widget::ContainerLayout for RadialLayout {
-    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
-        Box::new(self.clone())
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use crate::widget::WidgetHost;
@@ -3827,27 +3805,26 @@ mod tests {
     /// derived from the getters instead, so the flow holds under any config.
     #[test]
     fn test_vstack_flow() {
-        // `Section` seats its first column one `margin_x` in from its left edge.
-        let first_col_x = 10.0 + 2.0 * section_padding() + Section::DEFAULT_MARGIN_X;
+        // A section's VStack seats every widget one content margin in from its left edge.
         let mut mock_pc = MockRenderTarget { rects: Vec::new() };
-        let mut sec = Section::new(&mut mock_pc, 10.0, 20.0, 200.0, "Test Section");
-        
+        let mut sec = SectionContext::new(&mut mock_pc, 10.0, 20.0, 200.0, "Test Section", false, false);
+        let first_col_x = sec.content_left();
+        let gap = sec.row_gap;
         let start_y = sec.ay();
-        let mut stack = sec.vstack(&mut mock_pc, 10.0);
+        let mut stack = sec.vstack(10.0);
 
         let mut dummy = crate::context::UiContext::new();
         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
         stack.add_widget(&mut w1, 50.0, 30.0, &mut dummy);
 
-        // Standard margin should be applied
         assert_eq!(w1.x, first_col_x);
         assert_eq!(w1.y, start_y);
 
         let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
         stack.add_widget(&mut w2, 60.0, 40.0, &mut dummy);
 
-        // Second widget should start after first widget height + vstack spacing
-        assert_eq!(w2.y, start_y + 30.0 + 10.0);
+        // The next starts below the first, the stack's spacing and the row gap on.
+        assert_eq!(w2.y, start_y + 30.0 + 10.0 + gap);
 
         let mut base = crate::widget::Widget::new();
         base.label = Some("Test Label".to_string());
@@ -3855,10 +3832,9 @@ mod tests {
         stack.add_widget(&mut w3, 70.0, 50.0, &mut dummy);
 
         let offset = w3.base.label_offset();
-        
-        // Third widget has label, so its y should be shifted by offset
+        // A labelled widget's control sits below its label strip.
         assert!(offset > 0.0, "a labelled widget has a strip");
-        assert_eq!(w3.base.y, start_y + 30.0 + 10.0 + 40.0 + 10.0 + offset);
+        assert_eq!(w3.base.y, start_y + 30.0 + 10.0 + gap + 40.0 + 10.0 + gap + offset);
     }
 
     #[test]
@@ -3893,44 +3869,20 @@ mod tests {
     #[test]
     fn test_subsection() {
         let mut pc = PopoverCollector::new();
-        let mut subsec = Section::new_opt(&mut pc, 10.0, 20.0, 300.0, "Test Subsec", true);
+        let mut subsec = SectionContext::new(&mut pc, 10.0, 20.0, 300.0, "Test Subsec", false, true);
         assert_eq!(subsec.left, 10.0);
         assert_eq!(subsec.top, 20.0);
         assert_eq!(subsec.cw, 300.0);
         assert!(subsec.is_child);
-        
+
         let mut dummy = crate::context::UiContext::new();
         let mut w = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
-        subsec.widget(&mut pc, &mut w, 12.0, 100.0, 40.0, &mut dummy);
-        
-        let bottom = subsec.finish(&mut pc);
+        subsec.widget(&mut w, 12.0, 100.0, 40.0, &mut dummy);
+
+        let bottom = subsec.finish();
         assert!(bottom > 20.0);
     }
 
-    #[test]
-    fn test_radial_layout() {
-        let radial = Radial::new(100.0, 100.0, 1.5, 50.0);
-        
-        // Check first widget is centered at (100.0, 100.0)
-        let rect0 = radial.widget_rect(0, 40.0, 30.0);
-        assert_eq!(rect0, (100.0 - 20.0, 100.0 - 15.0, 40.0, 30.0));
-        
-        // Check Ring 1 (idx = 1) vs Ring 2 (idx = 7)
-        let rect1 = radial.widget_rect(1, 40.0, 30.0);
-        let rect7 = radial.widget_rect(7, 40.0, 30.0);
-        
-        let c1_x = rect1.0 + rect1.2 / 2.0;
-        let c1_y = rect1.1 + rect1.3 / 2.0;
-        let c7_x = rect7.0 + rect7.2 / 2.0;
-        let c7_y = rect7.1 + rect7.3 / 2.0;
-        
-        let d1 = ((c1_x - 100.0).powi(2) + (c1_y - 100.0).powi(2)).sqrt();
-        let d7 = ((c7_x - 100.0).powi(2) + (c7_y - 100.0).powi(2)).sqrt();
-        
-        // Ring 2 should be further out than Ring 1
-        assert!(d7 > d1);
-        assert!(d1 > 0.0);
-    }
 
     // The `Once`-initialised style getters below are checked against the
     // statics their config pass fills, under whatever the live config says.
@@ -4251,7 +4203,7 @@ mod tests {
     #[test]
     fn test_mosaic_layout() {
         use crate::widget::MosaicLayout;
-        use crate::layout::LayoutStrategy;
+        use crate::widget::ContainerLayout;
         
         let layout = MosaicLayout {
             gap: 10.0,
@@ -4283,7 +4235,7 @@ mod tests {
     #[test]
     fn test_reverse_mosaic_layout() {
         use crate::widget::ReverseMosaicLayout;
-        use crate::layout::LayoutStrategy;
+        use crate::widget::ContainerLayout;
         
         let layout = ReverseMosaicLayout {
             gap: 10.0,
diff --git a/src/layout/panes.rs b/src/layout/panes.rs
new file mode 100644
index 0000000..963b9c2
--- /dev/null
+++ b/src/layout/panes.rs
@@ -0,0 +1,96 @@
+//! Pane geometry for a window of three resizable columns, and for a circular pane:
+//! cce-designer's. Plain arithmetic over rects and points, no layout pass.
+
+pub struct SplitterLayout {
+    pub splitter1_x: f32,
+    pub splitter2_x: f32,
+    pub splitter_width: f32,
+    pub min_column_width: f32,
+}
+
+impl SplitterLayout {
+    pub fn new(width: f32, splitter_width: f32, min_column_width: f32) -> Self {
+        let s1 = (width - 2.0 * splitter_width) / 3.0;
+        let s2 = s1 + splitter_width + (width - 2.0 * splitter_width) / 3.0;
+        Self {
+            splitter1_x: s1,
+            splitter2_x: s2,
+            splitter_width,
+            min_column_width,
+        }
+    }
+
+    pub fn clamp(&mut self, total_width: f32, detached_circular_network: bool) {
+        if detached_circular_network {
+            let min_s2 = self.min_column_width;
+            let max_s2 = (total_width - self.min_column_width).max(min_s2);
+            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
+        } else {
+            let min_s1 = self.min_column_width;
+            let max_s1 = (self.splitter2_x - self.splitter_width - self.min_column_width).max(min_s1);
+            self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
+            let min_s2 = self.splitter1_x + self.splitter_width + self.min_column_width;
+            let max_s2 = (total_width - self.min_column_width).max(min_s2);
+            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
+        }
+    }
+
+    pub fn scale(&mut self, factor: f32) {
+        self.splitter1_x *= factor;
+        self.splitter2_x *= factor;
+    }
+
+    pub fn left_col(&self) -> (f32, f32) { // (x, width)
+        (0.0, self.splitter1_x)
+    }
+
+    pub fn center_col(&self) -> (f32, f32) { // (x, width)
+        let x = self.splitter1_x + self.splitter_width;
+        (x, self.splitter2_x - x)
+    }
+
+    pub fn right_col(&self, total_width: f32) -> (f32, f32) { // (x, width)
+        let x = self.splitter2_x + self.splitter_width;
+        (x, (total_width - x).max(0.0))
+    }
+}
+
+pub struct CircularPaneLayout {
+    pub x: f32,
+    pub y: f32,
+    pub r: f32,
+}
+
+impl CircularPaneLayout {
+    pub fn new(x: f32, y: f32, r: f32) -> Self {
+        Self { x, y, r }
+    }
+
+    pub fn hit_test_content(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist_sq = dx * dx + dy * dy;
+        dist_sq <= self.r * self.r && cy >= self.y - self.r + 45.0 + menubar_h + breadcrumb_h
+    }
+
+    pub fn hit_test_menubar(&self, cx: f32, cy: f32, menubar_h: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist = (dx * dx + dy * dy).sqrt();
+        dist >= self.r - menubar_h && dist <= self.r && cy < self.y
+    }
+
+    pub fn hit_test_breadcrumb(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist_sq = dx * dx + dy * dy;
+        dist_sq <= self.r * self.r && cy >= self.y - self.r + menubar_h && cy < self.y - self.r + 45.0 + menubar_h + breadcrumb_h
+    }
+
+    pub fn hit_test_border(&self, cx: f32, cy: f32, border_thickness: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist = (dx * dx + dy * dy).sqrt();
+        dist >= self.r - border_thickness && dist <= self.r
+    }
+}
diff --git a/src/layout/registry.rs b/src/layout/registry.rs
index 8eb781a..95ac1b6 100644
--- a/src/layout/registry.rs
+++ b/src/layout/registry.rs
@@ -19,6 +19,42 @@ use std::collections::HashMap;
 ///
 /// Fixing it at the registry rather than per accessor covers every slot it
 /// holds in one place, including ones no test pins yet.
+/// The family fontconfig matches for `monospace` (`fc-match`), asked once.
+pub fn get_system_monospace_font() -> &'static str {
+    static MONOSPACE_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
+    MONOSPACE_FONT.get_or_init(|| {
+        if let Ok(output) = std::process::Command::new("fc-match")
+            .args(["-f", "%{family}", "monospace"])
+            .output()
+        {
+            let name = String::from_utf8_lossy(&output.stdout);
+            let parsed = name.split(',').next().unwrap_or("monospace").trim();
+            if !parsed.is_empty() {
+                return parsed.to_string();
+            }
+        }
+        "monospace".to_string()
+    })
+}
+
+/// The family fontconfig matches for `sans-serif`, asked once.
+pub fn get_system_sans_serif_font() -> &'static str {
+    static SANS_SERIF_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
+    SANS_SERIF_FONT.get_or_init(|| {
+        if let Ok(output) = std::process::Command::new("fc-match")
+            .args(["-f", "%{family}", "sans-serif"])
+            .output()
+        {
+            let name = String::from_utf8_lossy(&output.stdout);
+            let parsed = name.split(',').next().unwrap_or("sans-serif").trim();
+            if !parsed.is_empty() {
+                return parsed.to_string();
+            }
+        }
+        "sans-serif".to_string()
+    })
+}
+
 #[cfg(test)]
 mod test_overlay {
     use std::cell::RefCell;
diff --git a/src/layout/section.rs b/src/layout/section.rs
new file mode 100644
index 0000000..ea342e8
--- /dev/null
+++ b/src/layout/section.rs
@@ -0,0 +1,930 @@
+//! A settings page's sections: where each stands on the page ([`PageFlow`]), and what
+//! goes inside one ([`SectionContext`]).
+//!
+//! A page is a masonry of sections — the page split into as many columns as fit
+//! `grid_min_col_width()`, at most the page's section count, each section dropped into the
+//! shortest column. A section is drawn ONCE, in place: where it goes depends only on the
+//! sections before it, never on its own height, so [`PageLayoutBuilder`] asks the flow for
+//! the slot, lets the section draw its content there, and hands the height it came to back
+//! to the flow. Until 2026-10-08 every section was drawn twice — once into a throwaway
+//! target to measure it, then for real — through the legacy `LayoutStrategy`, whose
+//! `allocate` took the height first; the flow's choices are the same, so the pages are too.
+//!
+//! What a section draws is immediate-mode: [`SectionContext`] keeps a cursor down its content
+//! box and a one- or two-column grid for the widgets that do not span it. Its geometry is the
+//! settings app's, and documented on each placer.
+
+use super::*;
+use crate::widget::WidgetHost;
+use crate::context::UiContext;
+
+fn estimate_label_width_helper(label: &str, font_size: f32, font_fam: &str) -> f32 {
+    let fam_lower = font_fam.to_lowercase();
+    let is_mono = fam_lower.contains("mono") || fam_lower.contains("courier") || fam_lower == "monospace";
+    if is_mono {
+        label.chars().count() as f32 * font_size * 0.60
+    } else {
+        let mut width = 0.0;
+        for c in label.chars() {
+            let factor = match c {
+                'i' | 'l' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.30,
+                'f' | 'j' | 't' => 0.35,
+                'r' | 's' | 'c' | 'z' => 0.50,
+                'a' | 'b' | 'd' | 'e' | 'g' | 'h' | 'k' | 'n' | 'o' | 'p' | 'q' | 'u' | 'v' | 'x' | 'y' => 0.60,
+                'm' | 'w' | 'M' | 'W' | '&' | '@' | 'O' | 'Q' | 'G' => 0.85,
+                'A' | 'B' | 'C' | 'D' | 'H' | 'N' | 'U' | 'V' | 'X' | 'Y' => 0.75,
+                'E' | 'F' | 'K' | 'L' | 'P' | 'R' | 'S' | 'T' | 'Z' | 'J' => 0.68,
+                '0'..='9' => 0.60,
+                _ => 0.60,
+            };
+            width += factor * font_size;
+        }
+        width
+    }
+}
+
+/// A section's column grid, and the flow's: the column lefts, their width, and how far down
+/// each is filled.
+#[derive(Debug, Clone)]
+pub struct Grid {
+    pub left: f32,
+    pub top: f32,
+    pub width: f32,
+    pub col_width: f32,
+    pub gap: f32,
+    pub col_heights: Vec<f32>,
+    pub col_lefts: Vec<f32>,
+}
+
+impl Grid {
+    pub fn new(left: f32, top: f32, width: f32, min_col_width: f32, gap: f32, count: usize) -> Self {
+        let total_gap = gap * (count - 1) as f32;
+        let col_width = if count > 0 {
+            (width - total_gap).max(0.0) / count as f32
+        } else {
+            min_col_width
+        };
+        let left_offset = 0.0;
+
+        let mut col_lefts = Vec::with_capacity(count);
+        let col_heights = vec![top; count];
+        for i in 0..count {
+            col_lefts.push(left + left_offset + i as f32 * (col_width + gap));
+        }
+
+        Self {
+            left,
+            top,
+            width,
+            col_width,
+            gap,
+            col_heights,
+            col_lefts,
+        }
+    }
+
+    pub fn next_column(&self) -> usize {
+        let mut min_idx = 0;
+        let mut min_h = self.col_heights[0];
+        for i in 1..self.col_heights.len() {
+            if self.col_heights[i] < min_h {
+                min_h = self.col_heights[i];
+                min_idx = i;
+            }
+        }
+        min_idx
+    }
+
+    pub fn max_height(&self) -> f32 {
+        let mut max_h = self.col_heights[0];
+        for i in 1..self.col_heights.len() {
+            if self.col_heights[i] > max_h {
+                max_h = self.col_heights[i];
+            }
+        }
+        max_h
+    }
+}
+
+/// Where a page's sections stand (see the module docs).
+#[derive(Debug, Clone, Default)]
+pub struct PageFlow {
+    grid: Option<Grid>,
+    sections: Option<usize>,
+}
+
+/// Where [`PageFlow::place`] put a section, for [`PageFlow::commit`] to fill once its height
+/// is known.
+#[derive(Debug, Clone, Copy)]
+pub struct Slot {
+    pub x: f32,
+    pub y: f32,
+    pub w: f32,
+    span: SlotSpan,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum SlotSpan {
+    /// Every column: they all end below it.
+    All,
+    /// One column.
+    One(usize),
+    /// `n` columns from `first`.
+    Run { first: usize, n: usize },
+    /// The flow was never laid over a page.
+    Nowhere,
+}
+
+impl PageFlow {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Lay the flow over the page's content rect. The column count is what fits
+    /// `grid_min_col_width()` with `grid_gap()` between, capped by the section count.
+    pub fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+        let min_col_width = grid_min_col_width();
+        let gap = grid_gap();
+        let max_cols = ((width + gap) / (min_col_width + gap)).floor().max(1.0) as usize;
+        let count = if let Some(n) = self.sections {
+            n.min(max_cols).max(1)
+        } else {
+            max_cols
+        };
+        self.grid = Some(Grid::new(left, top, width, min_col_width, gap, count));
+    }
+
+    /// At most this many columns: a page with fewer sections than fit across does not leave
+    /// columns empty. Takes effect at the next [`init`](Self::init).
+    pub fn set_section_count(&mut self, count: usize) {
+        self.sections = Some(count);
+    }
+
+    /// One column's width, once laid over a page.
+    pub fn column_width(&self) -> Option<f32> {
+        self.grid.as_ref().map(|g| g.col_width)
+    }
+
+    /// The gap between sections, both ways.
+    pub fn gap(&self) -> f32 {
+        grid_gap()
+    }
+
+    /// Where a section `width` wide goes: as many columns as it is wide (rounded), at the
+    /// left of the run whose lowest fill is highest-up, just below that fill. A section as
+    /// wide as the page goes below everything.
+    pub fn place(&self, width: f32) -> Slot {
+        let Some(grid) = self.grid.as_ref() else {
+            return Slot { x: 0.0, y: 0.0, w: 0.0, span: SlotSpan::Nowhere };
+        };
+        let num_cols = grid.col_heights.len();
+        let n = (((width + grid.gap) / (grid.col_width + grid.gap)).round() as usize).clamp(1, num_cols);
+        if n >= num_cols {
+            Slot { x: grid.left, y: grid.max_height(), w: grid.width, span: SlotSpan::All }
+        } else if n == 1 {
+            let col = grid.next_column();
+            Slot { x: grid.col_lefts[col], y: grid.col_heights[col], w: grid.col_width, span: SlotSpan::One(col) }
+        } else {
+            let mut first = 0;
+            let mut lowest = f32::MAX;
+            for c in 0..=(num_cols - n) {
+                let fill = grid.col_heights[c..c + n].iter().fold(0.0f32, |a, &h| a.max(h));
+                if fill < lowest {
+                    lowest = fill;
+                    first = c;
+                }
+            }
+            let w = n as f32 * grid.col_width + (n - 1) as f32 * grid.gap;
+            Slot { x: grid.col_lefts[first], y: lowest, w, span: SlotSpan::Run { first, n } }
+        }
+    }
+
+    /// The section placed at `slot` came to `height`: its columns now end a gap below it.
+    pub fn commit(&mut self, slot: Slot, height: f32) {
+        let Some(grid) = self.grid.as_mut() else { return };
+        match slot.span {
+            SlotSpan::All => grid.col_heights.fill(slot.y + height + grid.gap),
+            SlotSpan::One(col) => grid.col_heights[col] += height + grid.gap,
+            SlotSpan::Run { first, n } => {
+                for h in &mut grid.col_heights[first..first + n] {
+                    *h = slot.y + height + grid.gap;
+                }
+            }
+            SlotSpan::Nowhere => {}
+        }
+    }
+}
+
+/// Vertical slack for a section's content clip. The clip exists to stop
+/// content escaping its section SIDEWAYS, which is the axis a section's width
+/// actually fixes; a section's height is only known once its content has been
+/// placed, so bounding that axis too would risk cutting content off rather
+/// than keeping it in. Deliberately far larger than any section.
+const SECTION_CLIP_SLACK: f32 = 100_000.0;
+
+/// A page's sections, placed by a [`PageFlow`] and drawn once each.
+pub struct PageLayoutBuilder<'a, P> {
+    pub flow: &'a mut PageFlow,
+    pub cx: f32,
+    pub cy: f32,
+    pub cw: f32,
+    pub ch: f32,
+    pub section_width: f32,
+    pub idx: usize,
+    _phantom: std::marker::PhantomData<P>,
+}
+
+impl<'a, P: RenderTarget> PageLayoutBuilder<'a, P> {
+    /// Lay `flow` over the page's content rect. `section_width` is a section's width when the
+    /// flow has none to offer (it always does once laid over a page).
+    pub fn new(flow: &'a mut PageFlow, cx: f32, cy: f32, cw: f32, ch: f32, section_width: f32) -> Self {
+        flow.init(cx, cy, cw, ch);
+        Self { flow, cx, cy, cw, ch, section_width, idx: 0, _phantom: std::marker::PhantomData }
+    }
+
+    /// At most `count` columns (see [`PageFlow::set_section_count`]).
+    pub fn with_section_count(self, count: usize) -> Self {
+        self.flow.set_section_count(count);
+        self.flow.init(self.cx, self.cy, self.cw, self.ch);
+        self
+    }
+
+    /// A section one column wide.
+    pub fn add_section<F>(&mut self, final_pc: &mut P, label: &str, focused: bool, render_fn: F)
+    where
+        F: FnOnce(&mut SectionContext<'_, P>),
+    {
+        let width = self.flow.column_width().unwrap_or(self.section_width);
+        self.add_section_with_width(final_pc, width, label, focused, render_fn);
+    }
+
+    /// A section `width` wide: as many columns as that rounds to.
+    pub fn add_section_with_width<F>(&mut self, final_pc: &mut P, width: f32, label: &str, focused: bool, render_fn: F)
+    where
+        F: FnOnce(&mut SectionContext<'_, P>),
+    {
+        let slot = self.flow.place(width);
+        let mut sec = SectionContext::new(final_pc, slot.x, slot.y, slot.w, label, focused, false);
+        let (clip_x, clip_w) = (sec.content_left(), sec.content_width());
+        sec.pc.push_clip_rect(clip_x, slot.y - SECTION_CLIP_SLACK, clip_w, 2.0 * SECTION_CLIP_SLACK);
+        render_fn(&mut sec);
+        sec.pc.pop_clip_rect();
+        let bottom = sec.finish();
+        self.flow.commit(slot, bottom - slot.y);
+        self.idx += 1;
+    }
+
+    /// A section `span` columns wide.
+    pub fn add_section_spanned<F>(&mut self, final_pc: &mut P, label: &str, span: usize, focused: bool, render_fn: F)
+    where
+        F: FnOnce(&mut SectionContext<'_, P>),
+    {
+        let col_width = self.flow.column_width().unwrap_or(self.section_width);
+        let gap = self.flow.gap();
+        let width = span as f32 * col_width + (span - 1) as f32 * gap;
+        self.add_section_with_width(final_pc, width, label, focused, render_fn);
+    }
+}
+
+pub struct SectionContext<'a, P> {
+    pub pc: &'a mut P,
+    pub left: f32,
+    pub top: f32,
+    pub content_y: f32,
+    pub cw: f32,
+    pub label_width: f32,
+    pub label_x: f32,
+    /// Whether the host renders sections as sunken wells (`section_relief_style`).
+    pub relief_style: bool,
+    /// The title tab box (x, y, w, h) when the host's relief styling laid the
+    /// label out left-aligned — `finish` offers it with the section carve.
+    /// None under relief styling means a label-less section: the well carves
+    /// tabless, flush with the allocation top.
+    pub relief_tab: Option<(f32, f32, f32, f32)>,
+    pub focused: bool,
+    pub is_child: bool,
+    pub grid: Grid,
+    pub last_col: usize,
+    pub content_start_y: f32,
+    pub row_gap: f32,
+}
+
+impl<'a, P: RenderTarget> SectionContext<'a, P> {
+    pub const DEFAULT_MARGIN_X: f32 = 12.0;
+    pub const DEFAULT_ROW_GAP: f32 = 8.0;
+
+    fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
+        estimate_label_width_helper(label, font_size, font_fam)
+    }
+
+    pub fn padding(&self) -> f32 {
+        section_padding()
+    }
+
+    pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool, is_child: bool) -> Self {
+        let font_setting = if is_child {
+            nested_section_label_font()
+        } else {
+            section_label_font()
+        };
+        let (font_fam, font_size_opt) = parse_font_string(&font_setting);
+        let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
+        let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
+        let label_width = Self::estimate_label_width(label, font_size, &font_fam);
+        let relief_style = pc.section_relief_style();
+        let label_x = if is_child {
+            let base_x = match nested_section_label_alignment() {
+                0 => left + 12.0,
+                1 => left + (cw - label_width) / 2.0,
+                2 => left + cw - 12.0 - label_width,
+                _ => left + 12.0,
+            };
+            base_x + nested_section_label_offset()
+        } else if relief_style {
+            // Sunken style: the title sits in a tab flush with the well's left
+            // edge (the designer look), not centered on the border.
+            left + 12.0
+        } else {
+            left + (cw - label_width) / 2.0
+        };
+        // Under relief styling the well fills the whole allocated rect (so the
+        // page's gaps and margins are the visual gaps) — the tab tops the
+        // allocation and the label centers inside it.
+        let label_y = if relief_style { top + 4.0 } else { top };
+        pc.text_with_font(label, label_x, label_y, font_size, font_color, &font_fam);
+
+        // The tab wraps the label, flush on the well's top-left corner; clamped
+        // so off-default child alignments can't push it outside the well.
+        let relief_tab = if relief_style && label_width > 0.0 {
+            let tab_x = (label_x - 12.0).max(left);
+            Some((tab_x, top, label_width + 24.0, font_size + 10.0))
+        } else {
+            None
+        };
+
+        let pad = section_padding();
+        let margin_x = 2.0 * pad + 12.0;
+        let usable_w = (cw - 2.0 * margin_x).max(1.0);
+        let min_col_width = 130.0;
+        let gap = 8.0;
+        let max_cols = if is_child {
+            1
+        } else {
+            ((usable_w + gap) / (min_col_width + gap)).floor().clamp(1.0, 2.0) as usize
+        };
+        // Under relief styling the content stands off the well's top wall by
+        // the same inset it keeps from the side walls (`margin_x`, which is
+        // also what `finish` leaves below it), so a well reads as one even
+        // frame. The outline style's `pad + 19` predates the title tab: under
+        // relief the tab alone is `font_size + 10` tall, which left the first
+        // line touching the well's top wall at the default padding while the
+        // sides kept 20px and more.
+        let content_start_y = if relief_style {
+            relief_tab.map(|t| t.1 + t.3).unwrap_or(top) + margin_x
+        } else {
+            top + pad + 19.0
+        };
+        let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
+
+        Self {
+            pc,
+            left,
+            top,
+            content_y: content_start_y,
+            cw,
+            label_width,
+            label_x,
+            relief_style,
+            relief_tab,
+            focused,
+            is_child,
+            grid,
+            last_col: usize::MAX,
+            content_start_y,
+            row_gap: Self::DEFAULT_ROW_GAP,
+        }
+    }
+
+    pub fn with_row_gap(mut self, gap: f32) -> Self {
+        self.row_gap = gap;
+        self
+    }
+
+    /// The section's content-box top edge: the body well's top (the tab's
+    /// bottom) under relief styling, the outline's border line otherwise. Lets
+    /// a page place content at an exact inset from the well's walls.
+    pub fn well_top(&self) -> f32 {
+        if self.relief_style {
+            self.relief_tab.map(|t| t.1 + t.3).unwrap_or(self.top)
+        } else {
+            self.top + 7.0
+        }
+    }
+
+    pub fn set_row_gap(&mut self, gap: f32) {
+        self.row_gap = gap;
+    }
+
+    /// Horizontal inset of section CONTENT from the section's left edge.
+    ///
+    /// One number, used by every content placer in here — `row_layout`, the
+    /// column `Grid`, `widget`, `VStack` and `ax` (so `text`) — because they
+    /// share a section and have to line up inside it. The section's border is
+    /// drawn at `left + padding()` (see `finish`), so content clears the
+    /// border by `padding() + 12`.
+    pub fn content_margin(&self) -> f32 {
+        2.0 * self.padding() + 12.0
+    }
+
+    /// Left edge of the content box: where a row, a widget or a `text(_, 12.0,
+    /// ..)` starts.
+    pub fn content_left(&self) -> f32 {
+        self.left + self.content_margin()
+    }
+
+    /// Width of the content box — the section's width less the inset on both
+    /// sides. Nothing a section draws should extend past `content_left() +
+    /// content_width()`.
+    pub fn content_width(&self) -> f32 {
+        (self.cw - 2.0 * self.content_margin()).max(0.0)
+    }
+
+    /// `x_off` px into the content box's coordinate space, where 12.0 is the
+    /// content's own left edge — the offset 46 of the ~55 call sites in the
+    /// tree already pass, and the one that lines text up with the buttons and
+    /// widgets beside it.
+    ///
+    /// This used to add `padding()` only when `x_off >= 12.0`, which made the
+    /// mapping DISCONTINUOUS: asking for 11 instead of 12 moved the text 5px
+    /// LEFT rather than 1px, and silently dropped it out of alignment with
+    /// every row in the same section. `cce-mail` and two others sit on the
+    /// wrong side of that cliff today.
+    pub fn ax(&self, x_off: f32) -> f32 {
+        self.left + 2.0 * self.padding() + x_off
+    }
+
+    pub fn ay(&self) -> f32 {
+        self.content_y
+    }
+
+    pub fn spacing(&mut self, dy: f32) {
+        if self.grid.col_heights.len() >= 2 {
+            if self.last_col == usize::MAX {
+                for h in &mut self.grid.col_heights {
+                    *h += dy;
+                }
+            } else if self.last_col < self.grid.col_heights.len() {
+                self.grid.col_heights[self.last_col] += dy;
+            }
+            self.content_y = self.grid.max_height();
+        } else {
+            self.content_y += dy;
+            for h in &mut self.grid.col_heights {
+                *h += dy;
+            }
+        }
+    }
+
+    pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+        let mut y = self.content_y + y_off;
+        if y > self.content_start_y {
+            y += self.row_gap;
+        }
+        let x = self.ax(x_off);
+        // Bound it to the content box. A section's text was drawn unbounded,
+        // so a string wider than its section simply kept going — over the
+        // border, over whatever sat to the right, and off the window (the
+        // settings app's GPU names did all three). Rows and widgets have
+        // always been sized to the section; text was the one thing that could
+        // leave it. The vertical band is generous on purpose: it is the
+        // horizontal overrun that has to be cut, and a tight band would
+        // shave descenders.
+        let right = self.content_left() + self.content_width();
+        let bounds = if right > x {
+            Some([x, y - font_size, right, y + 2.0 * font_size])
+        } else {
+            None
+        };
+        self.pc.text_with_bounds(text, x, y, font_size, color, bounds);
+        let new_bottom = y + font_size + 4.0;
+        self.content_y = new_bottom;
+        self.grid.col_heights.fill(new_bottom);
+    }
+
+    pub fn widget<T: WidgetHost + 'static>(&mut self, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
+        if let Some(pref) = w.preferred_height() {
+            wh = pref;
+        }
+        let pad = self.padding();
+        let top_room = w.label_strip();
+        let total_h = wh + top_room;
+
+        let name = w.type_name();
+        let span_full = name == "Trackpad"
+            || name == "Canvas"
+            || name == "UsageBar"
+            || name == "ProgressBar"
+            || name == "ButtonStrip"
+            || name == "Spreadsheet"
+            || name == "Graph";
+
+        if span_full {
+            let margin_x = 2.0 * pad + 12.0;
+            let x = self.left + margin_x;
+            let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
+            let mut max_h = self.grid.max_height().max(self.content_y);
+            if max_h > self.content_start_y {
+                max_h += self.row_gap;
+            }
+            let y = max_h;
+
+            w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
+            render_widget(self.pc, w, x, y, clamped_w, total_h, ctx);
+
+            let new_bottom = y + total_h;
+            self.content_y = new_bottom;
+            self.grid.col_heights.fill(new_bottom);
+        } else {
+            let max_h = self.grid.max_height();
+            if self.content_y > max_h {
+                self.grid.col_heights.fill(self.content_y);
+            }
+
+            let col = self.grid.next_column();
+            self.last_col = col;
+            let x = self.grid.col_lefts[col];
+            let mut y = self.grid.col_heights[col];
+            if y > self.content_start_y {
+                y += self.row_gap;
+            }
+
+            let aligned_x = x;
+            let aligned_w = self.grid.col_width;
+
+            w.set_row_rect(aligned_x, aligned_w);
+            render_widget(self.pc, w, aligned_x, y, aligned_w, total_h, ctx);
+            self.grid.col_heights[col] = y + total_h;
+            self.content_y = self.grid.max_height();
+        }
+    }
+
+    pub fn widget_full<T: WidgetHost + 'static>(&mut self, w: &mut T, wh: f32, ctx: &mut UiContext) {
+        let x_off = 12.0;
+        let ww = self.cw - 2.0 * (self.padding() + x_off); // cw - 40.0
+        self.widget(w, x_off, ww, wh, ctx);
+    }
+
+    pub fn separator(&mut self) {
+        let pad = self.padding();
+        let x = self.ax(pad);
+        let max_h = self.grid.max_height().max(self.content_y);
+        let y = max_h;
+        self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
+        self.content_y = max_h + 8.0;
+        self.grid.col_heights.fill(self.content_y);
+    }
+
+    pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+        let max_h = self.grid.max_height().max(self.content_y);
+        self.pc.rect(color, self.ax(x_off), max_h, w, h);
+        self.content_y = max_h + h;
+        self.grid.col_heights.fill(self.content_y);
+    }
+
+    pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
+        let margin_x = self.content_margin();
+        let usable_w = self.content_width();
+        if count == 0 {
+            return Vec::new();
+        }
+        let total_gap = gap * (count - 1) as f32;
+        let col_w = (usable_w - total_gap).max(0.0) / count as f32;
+
+        let mut cols = Vec::with_capacity(count);
+        for i in 0..count {
+            let x = self.left + margin_x + i as f32 * (col_w + gap);
+            cols.push((x, col_w));
+        }
+        cols
+    }
+
+    /// A row whose columns are sized to what goes IN them: each gets the width
+    /// it asked for in `needs`, and whatever is left over is shared equally.
+    ///
+    /// `row_layout` splits a row evenly and knows nothing about content, so it
+    /// hands "Reboot" and "Hibernate" the same width — one floats in slack
+    /// while the other is cut off, which is what a row of mismatched labels
+    /// looks like. Sharing the SLACK equally rather than sizing proportionally
+    /// is deliberate: proportional widths would make a two-character label a
+    /// sliver, where what is wanted is "everyone fits, then everyone gets the
+    /// same bonus".
+    ///
+    /// When the needs do not fit, every column is scaled by the same factor, so
+    /// the row still cannot overflow its section and the shortfall is shared
+    /// rather than landing entirely on the last column.
+    pub fn row_layout_for(&self, needs: &[f32], gap: f32) -> Vec<(f32, f32)> {
+        let count = needs.len();
+        if count == 0 {
+            return Vec::new();
+        }
+        let total_gap = gap * (count - 1) as f32;
+        let room = (self.content_width() - total_gap).max(0.0);
+        let total_need: f32 = needs.iter().map(|n| n.max(0.0)).sum();
+
+        let widths: Vec<f32> = if total_need <= room {
+            let extra = (room - total_need) / count as f32;
+            needs.iter().map(|n| n.max(0.0) + extra).collect()
+        } else if total_need > 0.0 {
+            let scale = room / total_need;
+            needs.iter().map(|n| n.max(0.0) * scale).collect()
+        } else {
+            vec![room / count as f32; count]
+        };
+
+        let mut cols = Vec::with_capacity(count);
+        let mut x = self.content_left();
+        for w in widths {
+            cols.push((x, w));
+            x += w + gap;
+        }
+        cols
+    }
+
+
+    pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
+    where
+        F: FnMut(usize, f32, f32),
+    {
+        let max_h = self.grid.max_height().max(self.content_y);
+        self.grid.col_heights.fill(max_h);
+        self.content_y = max_h;
+
+        let cols = self.row_layout(count, gap);
+        for (i, &(x, w)) in cols.iter().enumerate() {
+            f(i, x, w);
+        }
+        self.content_y += h;
+
+        self.grid.col_heights.fill(self.content_y);
+    }
+
+    pub fn vstack(&mut self, spacing: f32) -> VStack<'_, 'a, P> {
+        VStack {
+            context: self,
+            spacing,
+        }
+    }
+
+    pub fn add_section<F>(&mut self, label: &str, focused: bool, mut render_fn: F)
+    where
+        F: FnMut(&mut SectionContext<'_, P>),
+    {
+        let pad = self.padding();
+        let (left, top, cw, is_side_by_side) = if self.grid.col_heights.len() >= 2 {
+            let col = self.grid.next_column();
+            self.last_col = col;
+            let x = self.grid.col_lefts[col];
+            let mut y = self.grid.col_heights[col];
+            if y > self.content_start_y {
+                y += self.row_gap;
+            }
+            (x, y, self.grid.col_width, true)
+        } else {
+            let left = self.ax(0.0) + pad;
+            let mut max_h = self.grid.max_height().max(self.content_y);
+            if max_h > self.content_start_y {
+                max_h += self.row_gap;
+            }
+            let top = max_h;
+            let cw = self.cw - 2.0 * pad;
+            (left, top, cw, false)
+        };
+
+        let mut sub_ctx = SectionContext::new(self.pc, left, top, cw, label, focused, true);
+        render_fn(&mut sub_ctx);
+        let new_bottom = sub_ctx.finish();
+
+        if is_side_by_side {
+            let col = self.last_col;
+            self.grid.col_heights[col] = new_bottom;
+            self.content_y = self.grid.max_height();
+        } else {
+            self.content_y = new_bottom;
+            self.grid.col_heights.fill(new_bottom);
+        }
+    }
+
+    pub fn finish(self) -> f32 {
+        let border: [f32; 4] = if self.is_child {
+            if self.focused {
+                [0.22, 0.38, 0.24, 1.0]
+            } else {
+                [0.18, 0.18, 0.25, 1.0]
+            }
+        } else {
+            if self.focused {
+                [0.30, 0.50, 0.32, 1.0] // Focused green
+            } else {
+                [0.25, 0.25, 0.35, 1.0] // Default gray
+            }
+        };
+        let pad = self.padding();
+        let x = self.left + pad;
+        let y = self.top + 7.0;
+        let w = self.cw - 2.0 * pad;
+        let h = self.content_y - y;
+        // Under relief the well's walls are the allocation's edges, so the
+        // floor below the content matches the inset beside and above it (see
+        // `new`). The outline frame sits `pad` in from its allocation and
+        // keeps its own slack.
+        let extra_bottom = if self.relief_style { self.content_margin() } else { pad + 12.0 };
+        let bottom = y + h + extra_bottom;
+
+        if self.relief_style {
+            // Sunken style: the well spans the full allocated rect — body top
+            // edge at the tab's bottom (the tab is flush ON the body, the
+            // designer union shape; label-less sections carve tabless from the
+            // allocation top), walls on the allocation's edges, and no
+            // trailing slack so the layout gap IS the visual gap.
+            let body_y = self.relief_tab.map(|t| t.1 + t.3).unwrap_or(self.top);
+            let frame = SectionFrame {
+                x: self.left,
+                y: body_y,
+                w: self.cw,
+                h: bottom - body_y,
+                tab: self.relief_tab,
+                focused: self.focused,
+                is_child: self.is_child,
+            };
+            if self.pc.section_relief(&frame) {
+                return self.content_y + extra_bottom;
+            }
+        }
+
+        let left_edge = x;
+        let right_edge = x + w;
+        if self.label_width > 0.0 {
+            let label_x = self.label_x;
+            let gap_margin = 6.0;
+            let gap_start = label_x - gap_margin;
+            let gap_end = label_x + self.label_width + gap_margin;
+            if gap_start > left_edge {
+                self.pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+            }
+            if right_edge > gap_end {
+                self.pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+            }
+        } else {
+            self.pc.rect(border, left_edge, y, w, 1.0);
+        }
+
+        self.pc.rect(border, x, y + h + extra_bottom, w, 1.0);
+        self.pc.rect(border, x, y, 1.0, h + extra_bottom);
+        self.pc.rect(border, x + w - 1.0, y, 1.0, h + extra_bottom);
+        self.content_y + extra_bottom + 8.0
+    }
+}
+
+
+pub struct VStack<'b, 'a, P> {
+    pub context: &'b mut SectionContext<'a, P>,
+    pub spacing: f32,
+}
+
+impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
+    pub fn add_widget<T: WidgetHost + 'static>(&mut self, w: &mut T, _ww: f32, wh: f32, ctx: &mut UiContext) {
+        let pad = self.context.padding();
+        let margin_x = 2.0 * pad + 12.0;
+        let x = self.context.left + margin_x;
+        let clamped_w = (self.context.cw - 2.0 * margin_x).max(0.0);
+
+        let mut max_h = self.context.grid.max_height().max(self.context.content_y);
+        if max_h > self.context.content_start_y {
+            max_h += self.context.row_gap;
+        }
+        let y = max_h;
+
+        let pref_h = w.preferred_height().unwrap_or(wh);
+        let top_room = w.label_strip();
+        let total_h = pref_h + top_room;
+
+        w.set_row_rect(self.context.left + pad, self.context.cw - 2.0 * pad);
+        render_widget(self.context.pc, w, x, y, clamped_w, total_h, ctx);
+
+        let new_bottom = y + total_h;
+        self.context.content_y = new_bottom;
+        self.context.grid.col_heights.fill(new_bottom);
+        self.context.spacing(self.spacing);
+    }
+
+    /// [`add_row`](Self::add_row) with per-column widths from `needs` — see
+    /// [`SectionContext::row_layout_for`]. For a row of buttons, `needs` is
+    /// each label's measured width plus the plate's own inset.
+    pub fn add_row_for<F>(&mut self, needs: &[f32], gap: f32, h: f32, mut f: F)
+    where
+        F: FnMut(&mut SectionContext<'a, P>, usize, f32, f32),
+    {
+        let max_h = self.context.grid.max_height().max(self.context.content_y);
+        self.context.grid.col_heights.fill(max_h);
+        self.context.content_y = max_h;
+
+        let cols = self.context.row_layout_for(needs, gap);
+        for (i, &(x, w)) in cols.iter().enumerate() {
+            self.context.content_y = max_h;
+            f(self.context, i, x, w);
+        }
+
+        let new_bottom = max_h + h;
+        self.context.content_y = new_bottom;
+        self.context.grid.col_heights.fill(new_bottom);
+        self.context.spacing(self.spacing);
+    }
+
+    pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
+    where
+        F: FnMut(&mut SectionContext<'a, P>, usize, f32, f32),
+    {
+        let max_h = self.context.grid.max_height().max(self.context.content_y);
+        self.context.grid.col_heights.fill(max_h);
+        self.context.content_y = max_h;
+
+        let cols = self.context.row_layout(count, gap);
+        for (i, &(x, w)) in cols.iter().enumerate() {
+            self.context.content_y = max_h;
+            f(self.context, i, x, w);
+        }
+
+        let new_bottom = max_h + h;
+        self.context.content_y = new_bottom;
+        self.context.grid.col_heights.fill(new_bottom);
+        self.context.spacing(self.spacing);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// Where a section goes depends on the sections before it and never on its own height:
+    /// one column wide it takes the shortest column, as wide as the page it goes below
+    /// everything, and a run takes the columns whose lowest fill is highest up.
+    #[test]
+    fn a_section_is_placed_before_it_is_drawn() {
+        let (min, gap) = (grid_min_col_width(), grid_gap());
+        let width = 3.0 * min + 2.0 * gap + 1.0;
+        let mut flow = PageFlow::new();
+        flow.init(10.0, 20.0, width, 800.0);
+        let col = flow.column_width().unwrap();
+
+        let a = flow.place(col);
+        flow.commit(a, 100.0);
+        let b = flow.place(col);
+        flow.commit(b, 50.0);
+        let c = flow.place(col);
+        flow.commit(c, 70.0);
+        assert_eq!((a.x, a.y), (10.0, 20.0));
+        assert_eq!((b.y, c.y), (20.0, 20.0), "the first three stand side by side");
+        assert!(a.x < b.x && b.x < c.x);
+
+        let d = flow.place(col);
+        assert_eq!((d.x, d.y), (b.x, 20.0 + 50.0 + gap), "under the shortest column");
+        flow.commit(d, 10.0);
+
+        // Columns 1 and 2 reach 20+50+gap+10+gap and 20+70+gap; 0 and 1 reach further.
+        let pair = flow.place(2.0 * col + gap);
+        assert_eq!((pair.x, pair.y), (b.x, (20.0 + 50.0 + gap + 10.0 + gap).max(20.0 + 70.0 + gap)));
+        assert_eq!(pair.w, 2.0 * col + gap);
+        flow.commit(pair, 30.0);
+
+        let wide = flow.place(width);
+        assert_eq!((wide.x, wide.w), (10.0, width));
+        assert_eq!(wide.y, pair.y + 30.0 + gap, "below everything");
+    }
+
+    /// A page with fewer sections than fit across leaves no column empty.
+    #[test]
+    fn the_section_count_caps_the_columns() {
+        let width = 3.0 * grid_min_col_width() + 2.0 * grid_gap() + 1.0;
+        let mut flow = PageFlow::new();
+        flow.set_section_count(1);
+        flow.init(0.0, 0.0, width, 400.0);
+        assert_eq!(flow.column_width(), Some(width));
+    }
+
+    /// The builder draws each section once, in place, and the next one starts below it.
+    #[test]
+    fn each_section_is_drawn_once() {
+        let mut flow = PageFlow::new();
+        let mut pc = PopoverCollector::new();
+        let mut builder = PageLayoutBuilder::new(&mut flow, 0.0, 0.0, 600.0, 400.0, 300.0).with_section_count(1);
+        let mut tops = Vec::new();
+        for _ in 0..2 {
+            builder.add_section(&mut pc, "Section", false, |sec| {
+                tops.push(sec.top);
+                sec.spacing(40.0);
+            });
+        }
+        assert_eq!(tops.len(), 2, "one call per section");
+        assert!(tops[1] > tops[0] + 40.0, "{tops:?}");
+    }
+}
diff --git a/src/scene/layout.rs b/src/scene/layout.rs
index c56d197..5ab4082 100644
--- a/src/scene/layout.rs
+++ b/src/scene/layout.rs
@@ -1,8 +1,9 @@
 //! Hand-rolled two-phase layout engine — Phase 2 of the core rebuild.
 //!
-//! The legacy toolkit computes layout *inline during paint*, smeared across `LayoutStrategy`, a
+//! The legacy toolkit computed layout *inline during paint*, smeared across `LayoutStrategy`, a
 //! child-driven `allocate` bump-cursor, and hand-written `set_rect` calls with absolute
-//! coordinates — with `SectionContext` literally rendering twice to measure. There is no layout
+//! coordinates — with `SectionContext` literally rendering twice to measure (both gone since
+//! 2026-10-08). There is no layout
 //! pass that is independent of paint, which is why animated/relayout-able UI is hard and why a
 //! widget can end up sized by two different owners (the breadcrumb bug).
 //!
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
index 88ebe85..52421f5 100644
--- a/src/widget/container/container_layout.rs
+++ b/src/widget/container/container_layout.rs
@@ -1,7 +1,13 @@
 use crate::widget::*;
 use crate::context::UiContext;
 
-pub trait ContainerLayout: crate::layout::LayoutStrategy {
+/// How a container places its children: a rect in, every child's rect set, the height used
+/// out. The gallery's Layout dropdown shows each.
+pub trait ContainerLayout: std::fmt::Debug {
+    /// Place `children` in the rect at (`x`, `y`), `w` by `h`; returns the height they take.
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32;
+    /// What `children` need under `constraints`.
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn WidgetHost + 'static)], ctx: &UiContext) -> Size;
     fn box_clone_container(&self) -> Box<dyn ContainerLayout>;
 }
 
@@ -11,26 +17,11 @@ impl Clone for Box<dyn ContainerLayout> {
     }
 }
 
+/// Every child over the whole rect.
 #[derive(Debug, Clone, Copy, Default)]
-pub struct OverlayLayout {
-    left: f32,
-    top: f32,
-    width: f32,
-    height: f32,
-}
-
-impl crate::layout::LayoutStrategy for OverlayLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
-        self.left = left;
-        self.top = top;
-        self.width = width;
-        self.height = height;
-    }
-
-    fn allocate(&mut self, _ww: f32, _wh: f32) -> (f32, f32, f32, f32) {
-        (self.left, self.top, self.width, self.height)
-    }
+pub struct OverlayLayout;
 
+impl ContainerLayout for OverlayLayout {
     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn WidgetHost + 'static)], _ctx: &mut UiContext) -> f32 {
         for &child_ptr in children {
             unsafe {
@@ -56,60 +47,11 @@ impl crate::layout::LayoutStrategy for OverlayLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(*self)
-    }
-}
-
-impl ContainerLayout for OverlayLayout {
-    fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
-        Box::new(*self)
-    }
-}
-
-#[derive(Debug, Clone, Copy, Default, PartialEq)]
-pub struct ManualLayout {
-    left: f32,
-    top: f32,
-    width: f32,
-    height: f32,
-}
-
-impl crate::layout::LayoutStrategy for ManualLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
-        self.left = left;
-        self.top = top;
-        self.width = width;
-        self.height = height;
-    }
-
-    fn allocate(&mut self, _ww: f32, _wh: f32) -> (f32, f32, f32, f32) {
-        (self.left, self.top, self.width, self.height)
-    }
-
-    fn layout(&self, _x: f32, _y: f32, _w: f32, _h: f32, _children: &[*mut (dyn WidgetHost + 'static)], _ctx: &mut UiContext) -> f32 {
-        self.height
-    }
-
-    fn measure(&self, constraints: LayoutConstraints, _children: &[*mut (dyn WidgetHost + 'static)], _ctx: &UiContext) -> Size {
-        Size {
-            width: self.width.clamp(constraints.min_width, constraints.max_width),
-            height: self.height.clamp(constraints.min_height, constraints.max_height),
-        }
-    }
-
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(*self)
-    }
-}
-
-impl ContainerLayout for ManualLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
 }
 
-
 /// A child's content height for a strategy to allot: its preferred (intrinsic)
 /// height, else its landed rect less the detached-label strip.
 pub fn content_height(child: &dyn WidgetHost) -> f32 {
@@ -131,8 +73,6 @@ pub struct VerticalLayout {
     pub padding_x: f32,
     pub padding_y: f32,
     pub spacing: f32,
-    left: f32,
-    current_y: f32,
 }
 
 impl Default for VerticalLayout {
@@ -141,29 +81,11 @@ impl Default for VerticalLayout {
             padding_x: 0.0,
             padding_y: 0.0,
             spacing: crate::layout::control_gap(),
-            left: 0.0,
-            current_y: 0.0,
         }
     }
 }
 
-impl crate::layout::LayoutStrategy for VerticalLayout {
-    fn init(&mut self, left: f32, top: f32, _width: f32, _height: f32) {
-        self.left = left;
-        self.current_y = top + self.padding_y;
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        let x = self.left + self.padding_x;
-        let y = self.current_y;
-        self.current_y += wh + self.spacing;
-        (x, y, ww, wh)
-    }
-
-    fn get_gap(&self) -> f32 {
-        self.spacing
-    }
-
+impl ContainerLayout for VerticalLayout {
     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32 {
         let left_x = x + self.padding_x;
         let available_w = (w - 2.0 * self.padding_x).max(1.0);
@@ -211,12 +133,6 @@ impl crate::layout::LayoutStrategy for VerticalLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(*self)
-    }
-}
-
-impl ContainerLayout for VerticalLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
@@ -228,44 +144,9 @@ pub struct GridLayout {
     pub gap: f32,
     pub padding_x: f32,
     pub padding_y: f32,
-    pub grid: Option<crate::layout::Grid>,
 }
 
-impl crate::layout::LayoutStrategy for GridLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
-        let count = self.columns.max(1);
-        let usable_w = (width - 2.0 * self.padding_x).max(1.0);
-        let grid = crate::layout::Grid::new(
-            left + self.padding_x,
-            top + self.padding_y,
-            usable_w,
-            usable_w / count as f32,
-            self.gap,
-            count,
-        );
-        self.grid = Some(grid);
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        if let Some(ref mut grid) = self.grid {
-            let col = grid.next_column();
-            let x = grid.col_lefts[col];
-            let y = grid.col_heights[col];
-            grid.col_heights[col] += wh + grid.gap;
-            (x, y, grid.col_width, wh)
-        } else {
-            (0.0, 0.0, ww, wh)
-        }
-    }
-
-    fn get_column_width(&self) -> Option<f32> {
-        self.grid.as_ref().map(|g| g.col_width)
-    }
-
-    fn get_gap(&self) -> f32 {
-        self.gap
-    }
-
+impl ContainerLayout for GridLayout {
     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32 {
         let count = children.len();
         if count == 0 {
@@ -342,12 +223,6 @@ impl crate::layout::LayoutStrategy for GridLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(self.clone())
-    }
-}
-
-impl ContainerLayout for GridLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(self.clone())
     }
@@ -359,44 +234,9 @@ pub struct AdaptiveGridLayout {
     pub gap: f32,
     pub padding_x: f32,
     pub padding_y: f32,
-    pub grid: Option<crate::layout::Grid>,
 }
 
-impl crate::layout::LayoutStrategy for AdaptiveGridLayout {
-    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
-        let usable_w = (width - 2.0 * self.padding_x).max(1.0);
-        let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
-        let grid = crate::layout::Grid::new(
-            left + self.padding_x,
-            top + self.padding_y,
-            usable_w,
-            self.min_col_width,
-            self.gap,
-            cols,
-        );
-        self.grid = Some(grid);
-    }
-
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
-        if let Some(ref mut grid) = self.grid {
-            let col = grid.next_column();
-            let x = grid.col_lefts[col];
-            let y = grid.col_heights[col];
-            grid.col_heights[col] += wh + grid.gap;
-            (x, y, grid.col_width, wh)
-        } else {
-            (0.0, 0.0, ww, wh)
-        }
-    }
-
-    fn get_column_width(&self) -> Option<f32> {
-        self.grid.as_ref().map(|g| g.col_width)
-    }
-
-    fn get_gap(&self) -> f32 {
-        self.gap
-    }
-
+impl ContainerLayout for AdaptiveGridLayout {
     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32 {
         let usable_w = (w - 2.0 * self.padding_x).max(1.0);
         let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
@@ -405,7 +245,6 @@ impl crate::layout::LayoutStrategy for AdaptiveGridLayout {
             gap: self.gap,
             padding_x: self.padding_x,
             padding_y: self.padding_y,
-            grid: None,
         };
         grid.layout(x, y, w, h, children, ctx)
     }
@@ -418,17 +257,10 @@ impl crate::layout::LayoutStrategy for AdaptiveGridLayout {
             gap: self.gap,
             padding_x: self.padding_x,
             padding_y: self.padding_y,
-            grid: None,
         };
         grid.measure(constraints, children, ctx)
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(self.clone())
-    }
-}
-
-impl ContainerLayout for AdaptiveGridLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(self.clone())
     }
@@ -451,11 +283,7 @@ impl Default for ColumnsLayout {
     }
 }
 
-impl crate::layout::LayoutStrategy for ColumnsLayout {
-    fn init(&mut self, _left: f32, _top: f32, _width: f32, _height: f32) {}
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) { (0.0, 0.0, ww, wh) }
-    fn get_gap(&self) -> f32 { self.spacing }
-
+impl ContainerLayout for ColumnsLayout {
     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32 {
         let count = children.len();
         if count == 0 {
@@ -503,12 +331,6 @@ impl crate::layout::LayoutStrategy for ColumnsLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(*self)
-    }
-}
-
-impl ContainerLayout for ColumnsLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
@@ -596,11 +418,7 @@ impl Packer {
     }
 }
 
-impl crate::layout::LayoutStrategy for MosaicLayout {
-    fn init(&mut self, _left: f32, _top: f32, _width: f32, _height: f32) {}
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) { (0.0, 0.0, ww, wh) }
-    fn get_gap(&self) -> f32 { self.gap }
-
+impl ContainerLayout for MosaicLayout {
     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32 {
         let count = children.len();
         if count == 0 {
@@ -657,12 +475,6 @@ impl crate::layout::LayoutStrategy for MosaicLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(*self)
-    }
-}
-
-impl ContainerLayout for MosaicLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
@@ -685,11 +497,7 @@ impl Default for ReverseMosaicLayout {
     }
 }
 
-impl crate::layout::LayoutStrategy for ReverseMosaicLayout {
-    fn init(&mut self, _left: f32, _top: f32, _width: f32, _height: f32) {}
-    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) { (0.0, 0.0, ww, wh) }
-    fn get_gap(&self) -> f32 { self.gap }
-
+impl ContainerLayout for ReverseMosaicLayout {
     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn WidgetHost + 'static)], ctx: &mut UiContext) -> f32 {
         let count = children.len();
         if count == 0 {
@@ -766,13 +574,8 @@ impl crate::layout::LayoutStrategy for ReverseMosaicLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
-        Box::new(*self)
-    }
-}
-
-impl ContainerLayout for ReverseMosaicLayout {
     fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
 }
+
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index caaa728..e2f64d1 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -10,7 +10,7 @@ pub mod treelist;
 pub mod dialog;
 pub mod group;
 
-pub use container_layout::{ContainerLayout, OverlayLayout, ManualLayout, VerticalLayout, GridLayout, AdaptiveGridLayout, ColumnsLayout, MosaicLayout, ReverseMosaicLayout};
+pub use container_layout::{ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout, ColumnsLayout, MosaicLayout, ReverseMosaicLayout};
 pub use content_bg::ContentBg;
 pub use parameters_bg::ParametersBg;
 pub use menu::MenuBar;
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 0f78eca..48004f0 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -695,7 +695,7 @@ pub use self::input::{
 };
 pub use self::container::{
     Dialog, Group, GroupFrame,
-    ContainerLayout, OverlayLayout, ManualLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
+    ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
     ColumnsLayout, MosaicLayout, ReverseMosaicLayout,
     ContentBg, ParametersBg,
     ScrollBox, MenuBar, SheetColumn, Spreadsheet, Breadcrumb,