git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitc82016b44f126b980b11d1656024891d71964813
parent377e327bec
authorLucas Galante <[email protected]>
date2026-09-23 15:02
refactor: retire the root meta node; settings are settings, not nodes

The root `meta` node (nee Session) was a permanent, undeletable subnet
holding four utility subnets — `main`, `view`, `guides`, `render` — whose
params were every session-wide setting. It was the STORE OF RECORD:
`ensure_menubar_subnets` rebuilt it from live state and
`apply_settings_from_menubar_subnets` copied it back OVER live state after
every parameter edit anywhere.

Three things followed from that, all bad. A display preference was project
data — carried in the file, reset by opening someone else's scene. Half of
those settings were reachable only by finding the right node in the right
subnet. And a command that flipped a live flag was undone by the next
unrelated edit unless it ALSO wrote the node, which is what
`write_guides_toggle` / `write_render_toggle` were for, and what made "Show
Cube hides the cube until you touch any parameter" a real bug.

Display settings are live `State` fields now, persisted by `DesignSettings`
into `state.kdl` (`viewport`, plus a new `render` block), and edited in the
dialog's Settings half. `Owner::Subnet` becomes `Owner::Field`, carrying a
`Ctl` because a bare Rust field has no type or range to borrow. The rows
that a registry toggle already owns end to end became `Owner::Command`,
read through `command_toggle_state` — one reading, so a switch here and a
switch in the palette cannot disagree.

That table is now the app's whole display configuration, so a value left
out of it would be GONE rather than merely hidden. Everything the four
subnets held is in it, and `every_retired_subnet_setting_is_reachable`
fails if one goes missing. Three settings that were node toggles and
reachable nowhere else became commands: `toggle_ray_traced_preview`,
`toggle_wire_single_color`, `toggle_render_points`.

Two things that were NOT settings moved to where they belong instead:

- Pane visibility rode the `view` subnet's five toggles into the file. It
  is genuinely project state, so it is `ProjectViewState::visible_panes`
  beside the collapse list and the splitters, with `PANE_FLAGS` the one
  table the save and the load share.
- The recent-projects list was the Main node's "Open" dropdown, which
  would have left `recent_files` written on every save and read by
  nothing. It is rows at the head of the palette, under the open project's
  own path row; picking one opens it.

`migrate_meta_settings_node` runs on every load: it takes the node (and the
four subnets, which pre-Session saves parked flat at the root — hence no
early return on the container alone), reads the values onto live state, and
saves them. A node the USER left inside that subnet is re-homed at the
root, not eaten with it.

Gone with all this: `session_node`, `in_settings_dir` (no settings
directory, so Add Node offers every template everywhere), `write_meta_toggle`
and its wrappers, `refresh_main_node_live_toggles`,
`update_recent_files_layout`, the `utility`/`session`/`meta` node types, the
undeletable-node gate, and layout.rs's pinning — whose only pinned nodes
were these.

Also here: the dialog plate was 420px, sized for thirteen settings rows and
now showing eight of nearly thirty; the colour round-trip through KDL was a
hand-written pair of `if let`s covering two of what are now five fields (the
wire colour's ALPHA was among the misses); and `test_keyboard_shortcut_system`
had lost its `#[test]` to a stray duplicate on the test below it, so the one
test CLAUDE.md names as the run-a-single-test example had not been running.

Verified against the real default project, which carried both kinds of meta
node: it loads with neither, and its own settings (background, grid
thickness, wireframe) land in state.kdl instead of being lost.

Co-Authored-By: Claude Opus 5 <[email protected]>

 CLAUDE.md       |  180 ++++++----
 src/api.rs      |    2 +-
 src/app.rs      |  428 +++++++++++------------
 src/command.rs  |    3 +
 src/dialog.rs   |  535 ++++++++++++++++++++++-------
 src/layout.rs   |    8 +-
 src/main.rs     | 1010 ++++++++++++++++++++++++++++++++++---------------------
 src/project.rs  |  872 +++++++++--------------------------------------
 src/shortcut.rs |    7 +
 src/window.rs   |   28 +-
 10 files changed, 1547 insertions(+), 1526 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9633bfd..7eaab2f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -186,7 +186,7 @@ gone from cce-ui with the wgpu path).
   instead of orbiting the camera. The framework owns everything that turned out
   to be the same for any such tool: projection of world positions to handles
   through `State::last_scene_mvp` + `last_scene_view_rect` (both LOGICAL px,
-  the rect divided by scale where it is cached — the same path as the meta
+  the rect divided by scale where it is cached — the same path as the
   Point Numbers overlay), hit-testing against `cursor_x/y`, dragging by
   unprojecting the cursor at the grabbed handle's captured NDC depth, snapping,
   the HUD, per-gesture undo (`cce_ui::history::History` of handle snapshots on
@@ -236,53 +236,101 @@ The `zcce_inspector_v1` integration (window-position tracking + widget-state
 streaming to cce-test-interface) was dropped in the engine migration; the HTTP API
 is the introspection surface.
 
-### The root meta node (nee Session)
-
-Session-wide settings live under one permanent root node: `meta` (node type
-`meta` — retyped/renamed from the old `Session`/`session` on load, children
-intact) contains the main/view/guides/render utility subnets that used to
-sit flat in `/`. It is the root network's counterpart of every node's
-per-node `meta` child, but still a subnet. `ensure_menubar_subnets` creates
-it and MIGRATES older saves into it (root-level settings nodes moved, not
-recreated — params survive; a `session`-typed container retypes in place).
-It cannot be deleted: `delete_node` refuses the `meta` (and legacy
-`session`) type — the one gate every deletion route funnels through — the
-context menu omits Delete, and the graph draws it without a geometry
-toggle. `State::session_node()` / `in_settings_dir()` are the accessors —
-the latter walks the whole `current_path`, since a first-segment check
-stopped working the day the settings nodes gained a parent. Guides holds
-"Point Marker Size" (thousandths of a world unit), driving the per-node
-meta Point Markers overlay via `State::meta_marker_size`, and **"World Unit"**
-(a `choice`: mm / cm / m / in, `State::world_unit`) — what one world unit IS.
-Geometry never converts; the declaration feeds two things through the display
-metric (`cce_ui::units`): the viewport's bottom-left **scale readout**
-(`append_scale_readout`: `1:2.3`, `1 mm = 0.43 mm on screen`, marked when the
-metric is only assumed) and the viewport context menu's **View 1:1**
-(`view_one_to_one`), which moves the active camera along its eye ray so the
-pivot plane shows one world unit at its true length — the default camera by
-zoom, a camera node by rewriting its Position, as Frame All does. The
-projection is a perspective (vertical FOV 0.9 rad), so 1:1 holds on the
-pivot plane only; `view_scale_ratio` is the readout's number.
-
-### The meta node (per-node preferences)
-
-Every geometry-producing node carries a **`meta` child** (node type `meta`) —
-per-node preferences, edited by entering the node and selecting it. Current
-prefs: "Point Markers" and "Point Numbers" (viewport overlays on that node's
-output; numbers project through the cached raster mvp into the 2D text pass,
-`append_meta_point_numbers`). `ensure_meta_on` / `ensure_meta_children`
-(src/app.rs) create it at instantiation and at every project load — the same
-migration pattern as the Session node — and also restore missing pref params,
-so adding a pref is one entry in `ensure_meta_on`'s list plus its consumer.
-`meta_pref(node, name)` is the read. Meta is undeletable (the `delete_node`
-gate alongside `session`), shows no geometry toggle, and is invisible to
-evaluation. Overlay data rebuilds with the scene (`collect_meta_overlays` in
-src/render.rs, walked with the scene's visibility chain).
+### There are no meta nodes (retired 2026-09-23)
+
+Two different things were called `meta`, and both are gone. What replaced
+them is the one rule worth remembering: **a display setting belongs to the
+view, so it is a live field on `State`, persisted to `state.kdl`, and
+reached from the command palette.** Never a node.
+
+**The root `meta` node (nee Session)** was a permanent, undeletable root
+subnet holding four utility subnets — `main`, `view`, `guides`, `render` —
+whose params were every session-wide setting. It was the STORE OF RECORD:
+`ensure_menubar_subnets` rebuilt it from live state and
+`apply_settings_from_menubar_subnets` copied it back OVER live state after
+every parameter edit anywhere. Three things followed, all bad. A display
+preference was project data, carried in the file and reset by opening
+someone else's scene. Half of those settings were reachable only by finding
+the right node in the right subnet. And a command that flipped a live flag
+was undone by the next unrelated edit unless it also wrote the node — which
+is what `write_guides_toggle` / `write_render_toggle` existed for, and what
+made "Show Cube hides the cube until you touch any parameter" a real bug.
+
+**The per-node `meta` child** was a hidden child on every geometry node
+carrying four display switches (Point Markers, Point Numbers, Point Normals,
+Wireframe), so seeing the point numbering of what was on screen meant diving
+into each node and flipping its own switch, one node at a time. Wireframe
+was already duplicated by a global `toggle_wireframe`.
+
+Where it all went:
+
+- **Display settings** are live `State` fields, persisted by
+  `DesignSettings` into `state.kdl` (`viewport` and the new `render` block),
+  and edited in the dialog's **Settings** half — `SETTINGS` in
+  `src/dialog.rs`, whose rows are `Owner::Field` (a live field, with a `Ctl`
+  saying what control draws it), `Owner::Command` (a registry toggle, read
+  through `command_toggle_state`), or `Owner::ActiveCamera`. The table is now
+  the app's whole display configuration, so a value left out of it is GONE,
+  not merely hidden — `every_retired_subnet_setting_is_reachable` is the
+  backstop, and `dialog_settings_rows_name_owners_that_exist` round-trips
+  every `Field` row because a key no dispatch arm names draws, accepts an
+  edit and does nothing.
+- **The three point overlays** are `toggle_point_markers` / `_numbers` /
+  `_normals`, collected in `rebuild_scene_geometry` off the merged scene
+  `Detail` (`render::scene_point_overlays`) rather than by a second walk that
+  re-evaluated every flagged node. **Wireframe folded into the existing
+  `toggle_wireframe`**, and the survivor draws the TOPOLOGICAL edge list
+  (`render::scene_edge_verts`) the per-node flag used, not the triangle soup
+  the global one did — shared edges once, quads as quads.
+- **Main's buttons** (New/Open/Save/Save As/Set As Default/Exit, Undo/Redo,
+  the zoom family, Detach Circular Window) were already registry commands.
+  Three settings that were toggles on those nodes and reachable NOWHERE else
+  became commands: `toggle_ray_traced_preview`, `toggle_wire_single_color`,
+  `toggle_render_points`.
+- **The recent-projects list** was the Main node's "Open" dropdown, which
+  would have left `recent_files` written and read by nothing. It is rows at
+  the head of the palette's Commands list (`RECENT_ROW_PREFIX`), under the
+  open project's own path row; picking one opens it.
+- **Pane visibility** was the `view` subnet's five toggles riding `fs_root`
+  into the file. It is genuinely project state, so it moved to
+  `ProjectViewState::visible_panes` beside the collapse list and the
+  splitters. `State::PANE_FLAGS` is the one table the save and the load share.
+- **The active camera** keeps the viewport menubar's own menu, whose entries
+  are the camera NODES — not something a fixed table can hold.
+
+`Project::migrate_meta_settings_node` runs on every load: it takes the meta
+node (and the four subnets, which PRE-Session saves parked flat at the root —
+hence no early return on the container alone), reads its values onto the live
+state, and saves them to `state.kdl`. Per-node children go in
+`app::strip_meta_children`, called from `merge_template_defs` because that is
+the one function every deserialization runs. Their VALUES are dropped
+deliberately: four per-node booleans do not reduce to one global switch, and
+inferring one would turn a single node's preference into a setting over the
+whole scene.
+
+Gone with them: `session_node()`, `in_settings_dir()` (there is no settings
+directory, so Add Node offers every template everywhere), `write_meta_toggle`
+and its two wrappers, `refresh_main_node_live_toggles`,
+`update_recent_files_layout`, the `utility` / `session` / `meta` node types,
+the undeletable-node gate in `delete_node`, and `layout.rs`'s pinning (whose
+only pinned nodes were these).
+
+**"World Unit"** (mm / cm / m / in, `State::world_unit`) survives as a
+Settings row — what one world unit IS. Geometry never converts; the
+declaration feeds two things through the display metric (`cce_ui::units`):
+the viewport's bottom-left **scale readout** (`append_scale_readout`:
+`1:2.3`, `1 mm = 0.43 mm on screen`, marked when the metric is only assumed)
+and the viewport context menu's **View 1:1** (`view_one_to_one`), which moves
+the active camera along its eye ray so the pivot plane shows one world unit
+at its true length — the default camera by zoom, a camera node by rewriting
+its Position, as Frame All does. The projection is a perspective (vertical
+FOV 0.9 rad), so 1:1 holds on the pivot plane only; `view_scale_ratio` is the
+readout's number.
 
 ### App-written settings: `~/.config/cce/cce-designer/state.kdl`
 
 `default_project` in state.kdl points at the project the main window opens on
-startup (the Main node's File > "Set As Default" button; absent = the bundled
+startup (the `set_as_default` command; absent = the bundled
 `default_project.json`). It is a POINTER, never a rewrite of
 default_project.json — that file is versioned and is the detached-window sync
 channel. Detached windows ignore it: they must keep seeding from the sync
@@ -313,9 +361,10 @@ files migrate on load. Scroll behavior (`scroll_speed`, `inertial_scroll`,
 this one hardcoded `$HOME/.config` until 2026-09-23 and was the only holdout.
 
 **And under `cfg(test)` it is a temp directory**, which is the part worth
-knowing. `State::new` loads the bundled project, and
-`apply_settings_from_menubar_subnets` copies that project's meta subnets over
-the live viewport flags; so any test that then reached `save_settings` —
+knowing. `State::new` loads the bundled project, whose meta subnets used to
+be copied over the live viewport flags after every parameter change (the
+meta node is retired, but the hazard was real and this redirect is what
+caught it); so any test that then reached `save_settings` —
 `run_command("toggle_network_plate")`, the dialog's toggle rows — wrote the
 BUNDLED project's show_grid / show_cube / show_origin over the user's real
 state.kdl. `cargo test` reset three of the user's own toggles on every run,
@@ -566,8 +615,7 @@ Scatter SOP with Relax Points) beside its original Volume mode, and
 tangent plane unless In 3D Space; zero iterations is off) beside its
 original Springs mode. A native `embryo` in an older save is recomposed on
 load (`recompose_native_embryo` in `merge_template_defs`): id, name,
-position, flag, values and meta child carry over, the template's children
-arrive fresh.
+position, flag and values carry over, the template's children arrive fresh.
 
 Two deliberate differences from the HDA. **Subdivide does not smooth**: it
 is this app's `remesh::subdivide` (four triangles per triangle, points
@@ -1257,25 +1305,25 @@ sees one (`input.kdl`'s `cce-window-manager` domain has `super+d` on the app
 launcher), and Super held is the DE's window-adjust modifier besides. Alt is the
 app's own — the `move_*` family already lives there.
 
-**A Settings row edits the meta node, never the live field.** The values behind
-those rows have exactly one owner, and it is not `State` and not
-`DesignSettings`: `apply_settings_from_menubar_subnets` copies the utility
-subnets (`Main`, `View`, `Guides`) onto the live state on EVERY param change, so
-a write straight to `State::grid_thickness` survives until the next one and no
-longer. `SETTINGS` is the table of which row belongs to which owner, and `Owner`
-has three arms for the three kinds there turn out to be: a subnet param, a
-registry command (Square Aspect and Show Camera Pivot are per-CAMERA, with no
-node at all behind the Default Camera — their commands are the only code that
+**A Settings row edits the live field** — see "There are no meta nodes"
+above, which is where these values used to live and why a direct write did
+not stick. `SETTINGS` is the table of which row belongs to which owner, and
+`Owner` has three arms for the three kinds there turn out to be: a live
+field (`Field`, with a `Ctl` saying what control draws it, since a bare Rust
+field carries no type or range the way a param did), a registry command
+(`Command` — Square Aspect and Show Camera Pivot are per-CAMERA, with no node
+at all behind the Default Camera, and their commands are the only code that
 gets both cases right), and an active-camera param with the live field as its
-fallback. Writeback is `sync_parameters_to_project`'s shape, polled rather than
-pushed for the same reason: a `ParametersBg` reports its values, it does not
-emit events. `dialog_settings_rows_name_owners_that_exist` is the backstop,
-because the failure is silent — `dialog_settings_params` SKIPS a row whose param
-it cannot find, so a rename quietly shortens the Settings half.
+fallback. Writeback is `sync_parameters_to_project`'s shape, polled rather
+than pushed for the same reason: a `ParametersBg` reports its values, it does
+not emit events. `dialog_settings_rows_name_owners_that_exist` is the
+backstop, because the failure is silent — a `Field` key no dispatch arm names
+reads a default and writes nowhere, so the row draws, takes an edit and does
+nothing, which is why that test round-trips every one of them.
 
 **The dialog is painted after the overlay passes, not in the widget walk.** A
 high `z_order` is not enough: `append_frame_text`, `append_scale_readout` and the
-meta-point overlays all run AFTER the whole walk, so the graph's node labels drew
+point-number overlay all run AFTER the whole walk, so the graph's node labels drew
 straight over a dialog that had already covered them. `append_dialog` runs
 between the plate corners and the context menu instead.
 
@@ -1351,9 +1399,7 @@ lowercase, as Houdini's are, and carry no spaces (since 2026-09-21).
 between a template name and its index goes ("Sphere 1" → "sphere1", which
 is also what minting now produces), any other whitespace becomes an
 underscore ("My Region" → "my_region"), the whole thing is lowercased, and
-empty comes back as `node`. The app's OWN nodes follow it — the root meta
-node's utility subnets are `main`, `view`, `guides` and `render`, and every
-lookup names them so — because a path convention with exceptions is two
+empty comes back as `node`, because a path convention with exceptions is two
 conventions. The template merge matches an instance to its template
 case-insensitively ("sphere3" → "Sphere"). It runs at every entry point — minting, the
 `add_node` name override, `rename_node` — and as a LOAD-TIME MIGRATION on
diff --git a/src/api.rs b/src/api.rs
index 67b2a77..a33c374 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -92,7 +92,7 @@ pub(crate) fn mcp_tools() -> Vec<McpTool> {
         ),
         tool(
             "toggle_geometry",
-            "Toggle geometry visibility for the node at the given slot (not valid on utility nodes).",
+            "Toggle geometry visibility for the node at the given slot.",
             json!({
                 "type": "object",
                 "properties": { "slot": slot("Child index in the current network level") },
diff --git a/src/app.rs b/src/app.rs
index 7bffd1d..8fa9922 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -198,7 +198,7 @@ impl FsNode {
     /// of them learned about new container types: subnet-like types by name,
     /// otherwise anything that actually has children.
     pub fn is_enterable(&self) -> bool {
-        matches!(self.node_type.as_str(), "node" | "utility" | "simnet" | "session")
+        matches!(self.node_type.as_str(), "node" | "simnet")
             || !self.children.is_empty()
     }
 
@@ -247,6 +247,17 @@ pub struct ProjectViewState {
     pub current_path: Vec<usize>,
     #[serde(default)]
     pub selected_node: Option<usize>,
+    /// Which panes are OPEN, by name ("network", "viewport", "parameters",
+    /// "spreadsheet", "playbar").
+    ///
+    /// Pane visibility rode the root meta node's `view` subnet params into
+    /// the file until 2026-09-23 — five toggles on a node that existed to
+    /// hold them. It is pane state like the collapse list and the splitter
+    /// proportions below, so it sits with them. `None` (older saves, and
+    /// saves written before the move) keeps the live layout, which is what
+    /// the absent-value case always did.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub visible_panes: Option<Vec<String>>,
     /// Collapsed plate panes by name ("network", "parameters", "spreadsheet",
     /// "playbar"). Absent from older saves — an empty list expands everything,
     /// so loading is deterministic either way.
@@ -289,7 +300,7 @@ pub struct ProjectViewState {
     /// camera-pivot marker and its size, and the view itself (orbit, zoom,
     /// pivot). Absent in older saves keeps the live values. A named camera's
     /// own params still win over these when it is active and in the
-    /// directory (`apply_settings_from_menubar_subnets`).
+    /// directory.
     #[serde(default)]
     pub default_view: Option<DefaultCameraView>,
 }
@@ -340,7 +351,7 @@ pub struct Project {
 pub enum NodeMenuAction {
     /// Dive into the node's subnet (the double-click behavior).
     Enter,
-    /// Flip the node's geometry visibility (utility nodes excluded).
+    /// Flip the node's geometry visibility.
     ToggleGeometry,
     /// Enter/exit the curve viewer state (curve nodes only).
     EditCurve,
@@ -586,9 +597,8 @@ pub fn strip_meta_children(root: &mut FsNode) {
 /// between a template's name and its index ("Sphere 1"), simply goes, so a
 /// migrated save reads like a fresh one; any other whitespace becomes an
 /// underscore, so "My Region" keeps its two words. And the whole thing is
-/// lowercased, as Houdini names its nodes (`sphere1`, `camera1`) — the
-/// app's own utility nodes included (`/meta/guides`), since a path
-/// convention with exceptions is two conventions. Empty comes back as
+/// lowercased, as Houdini names its nodes (`sphere1`, `camera1`), since a
+/// path convention with exceptions is two conventions. Empty comes back as
 /// `node`, since a node with no name has no path at all.
 pub fn sanitize_node_name(name: &str) -> String {
     let lowered = name.to_lowercase();
@@ -725,10 +735,6 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
                             fp.default = p.default.clone();
                         }
                     }
-                    if let Some(meta) = c.children.iter().find(|m| m.node_type == "meta") {
-                        fresh.children.retain(|m| m.node_type != "meta");
-                        fresh.children.push(meta.clone());
-                    }
                     *c = fresh;
                 }
             }
@@ -947,6 +953,97 @@ pub struct ViewportSettings {
     pub show_point_numbers: bool,
     #[serde(default)]
     pub show_point_normals: bool,
+    /// World-unit radius and colour of the Show Point Markers overlay.
+    #[serde(default = "default_point_marker_size")]
+    pub point_marker_size: f32,
+    #[serde(default = "default_point_marker_color")]
+    pub point_marker_color: [f32; 3],
+    /// What one world unit IS (mm / cm / m / in). A DECLARATION — geometry
+    /// never converts; it feeds the scale readout and `View 1:1`.
+    #[serde(default = "default_world_unit")]
+    pub world_unit: String,
+    /// The path-traced preview.
+    #[serde(default)]
+    pub rt_mode: bool,
+    /// The network pane's circular shape.
+    #[serde(default)]
+    pub circular_pane: bool,
+}
+
+fn default_point_marker_size() -> f32 {
+    0.02
+}
+
+fn default_point_marker_color() -> [f32; 3] {
+    [0.85, 0.85, 1.0]
+}
+
+fn default_world_unit() -> String {
+    "mm".to_string()
+}
+
+/// How the geometry itself is drawn — the wire pass and the point display.
+///
+/// These lived on the root meta node's `render` utility subnet until
+/// 2026-09-23, which made them per-PROJECT: opening someone else's scene
+/// reset how you looked at geometry. They are display preferences like the
+/// guides, so they persist here, and the dialog's Settings half is where
+/// they are edited.
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct RenderSettings {
+    #[serde(default)]
+    pub wireframe: bool,
+    #[serde(default)]
+    pub wire_single_color: bool,
+    /// RGBA: the alpha is the wireframe's OWN opacity, in both colour modes
+    /// (the geometry Opacity below is polygons-only).
+    #[serde(default = "default_wire_color")]
+    pub wire_color: [f32; 4],
+    #[serde(default = "default_wire_width")]
+    pub wire_width: f32,
+    #[serde(default = "default_geo_opacity")]
+    pub geo_opacity: f32,
+    #[serde(default)]
+    pub render_points: bool,
+    #[serde(default = "default_point_size")]
+    pub point_size: f32,
+    #[serde(default = "default_point_color")]
+    pub point_color: [f32; 3],
+}
+
+fn default_wire_color() -> [f32; 4] {
+    [0.0, 0.0, 0.0, 1.0]
+}
+
+fn default_wire_width() -> f32 {
+    1.0
+}
+
+fn default_geo_opacity() -> f32 {
+    1.0
+}
+
+fn default_point_size() -> f32 {
+    0.02
+}
+
+fn default_point_color() -> [f32; 3] {
+    [1.0, 1.0, 1.0]
+}
+
+impl Default for RenderSettings {
+    fn default() -> Self {
+        Self {
+            wireframe: false,
+            wire_single_color: false,
+            wire_color: default_wire_color(),
+            wire_width: default_wire_width(),
+            geo_opacity: default_geo_opacity(),
+            render_points: false,
+            point_size: default_point_size(),
+            point_color: default_point_color(),
+        }
+    }
 }
 
 fn default_network_plate() -> bool {
@@ -967,6 +1064,11 @@ impl Default for ViewportSettings {
             show_point_markers: false,
             show_point_numbers: false,
             show_point_normals: false,
+            point_marker_size: default_point_marker_size(),
+            point_marker_color: default_point_marker_color(),
+            world_unit: default_world_unit(),
+            rt_mode: false,
+            circular_pane: false,
             origin_size: 1.0,
             grid_thickness: default_grid_thickness(),
             grid_color: default_grid_color(),
@@ -1043,6 +1145,8 @@ pub const MAX_PITCH_Y: f32 = 375.0;
 pub struct DesignSettings {
     #[serde(default)]
     pub viewport: ViewportSettings,
+    #[serde(default)]
+    pub render: RenderSettings,
     /// Project to open at startup instead of the bundled default — the Main
     /// node's "Set As Default" button. A path string (what
     /// `loaded_project_path` held when it was set); absent = the bundled
@@ -1064,6 +1168,22 @@ fn hex_to_float_array(hex: &str) -> Option<[f32; 3]> {
     cce_ui::color::parse_hex_rgb(hex)
 }
 
+fn float_array_to_hex4(rgba: &[f32; 4]) -> String {
+    let c = |v: f32| (v * 255.0).clamp(0.0, 255.0).round() as u8;
+    format!("#{:02x}{:02x}{:02x}{:02x}", c(rgba[0]), c(rgba[1]), c(rgba[2]), c(rgba[3]))
+}
+
+fn hex_to_float_array4(hex: &str) -> Option<[f32; 4]> {
+    let h = hex.trim_start_matches('#');
+    if h.len() == 8 {
+        let v = |i: usize| u8::from_str_radix(&h[i..i + 2], 16).ok().map(|b| b as f32 / 255.0);
+        Some([v(0)?, v(2)?, v(4)?, v(6)?])
+    } else {
+        let rgb = cce_ui::color::parse_hex_rgb(hex)?;
+        Some([rgb[0], rgb[1], rgb[2], 1.0])
+    }
+}
+
 /// Where `cfg(test)` builds keep the files the installed app keeps under
 /// `<config home>/cce/cce-designer/` — a directory of this TEST's own,
 /// created on first use.
@@ -1112,9 +1232,10 @@ impl DesignSettings {
     /// The same file under test, in a temp directory — and that redirect is
     /// not a convenience.
     ///
-    /// `State::new` loads the BUNDLED project, whose meta subnets overwrite
-    /// the live viewport flags through `apply_settings_from_menubar_subnets`.
-    /// So any test that then reached `save_settings` — `toggle_network_plate`,
+    /// `State::new` loads the BUNDLED project, whose meta subnets overwrote
+    /// the live viewport flags on every parameter change (the meta node is
+    /// retired, but the hazard was real and the redirect is what caught it).
+    /// Any test that then reached `save_settings` — `toggle_network_plate`,
     /// the dialog's toggle rows — wrote the bundled project's
     /// show_grid / show_cube / show_origin over the user's own state.kdl.
     /// `cargo test` reset three of the user's toggles on every run, and
@@ -1135,24 +1256,32 @@ impl DesignSettings {
         Some(Self::from_kdl_str(&content))
     }
 
+    /// Every colour field, as `(block, field, components)`. KDL carries them
+    /// as hex strings — `#rrggbb`, or `#rrggbbaa` for the four-component wire
+    /// colour — so both directions walk this one table. It was a hand-written
+    /// pair of `if let`s per colour, which is why only two of the five were
+    /// ever converted once the render block arrived.
+    const COLOR_FIELDS: &'static [(&'static str, &'static str, usize)] = &[
+        ("viewport", "bg_color", 3),
+        ("viewport", "grid_color", 3),
+        ("viewport", "point_marker_color", 3),
+        ("render", "wire_color", 4),
+        ("render", "point_color", 3),
+    ];
+
     pub(crate) fn from_kdl_str(content: &str) -> Self {
         let mut json_val = cce_ui::config::parse_kdl_to_json(content);
-        // Convert hex strings back to color arrays
         if let Some(obj) = json_val.as_object_mut() {
-            if let Some(viewport) = obj.get_mut("viewport").and_then(|v| v.as_object_mut()) {
-                if let Some(serde_json::Value::String(hex_str)) = viewport.get("bg_color") {
-                    if let Some(arr) = hex_to_float_array(hex_str) {
-                        if let Ok(arr_val) = serde_json::to_value(arr) {
-                            viewport.insert("bg_color".to_string(), arr_val);
-                        }
-                    }
-                }
-                if let Some(serde_json::Value::String(hex_str)) = viewport.get("grid_color") {
-                    if let Some(arr) = hex_to_float_array(hex_str) {
-                        if let Ok(arr_val) = serde_json::to_value(arr) {
-                            viewport.insert("grid_color".to_string(), arr_val);
-                        }
-                    }
+            for &(block, field, n) in Self::COLOR_FIELDS {
+                let Some(b) = obj.get_mut(block).and_then(|v| v.as_object_mut()) else { continue };
+                let Some(serde_json::Value::String(hex)) = b.get(field) else { continue };
+                let parsed = if n == 4 {
+                    hex_to_float_array4(hex).and_then(|a| serde_json::to_value(a).ok())
+                } else {
+                    hex_to_float_array(hex).and_then(|a| serde_json::to_value(a).ok())
+                };
+                if let Some(v) = parsed {
+                    b.insert(field.to_string(), v);
                 }
             }
         }
@@ -1196,20 +1325,17 @@ impl DesignSettings {
 
     pub(crate) fn to_kdl_str(&self) -> Option<String> {
         if let Ok(mut json_val) = serde_json::to_value(self) {
-            // Convert color arrays to hex strings
             if let Some(obj) = json_val.as_object_mut() {
-                if let Some(viewport) = obj.get_mut("viewport").and_then(|v| v.as_object_mut()) {
-                    if let Some(val) = viewport.get("bg_color") {
-                        if let Ok(arr) = serde_json::from_value::<[f32; 3]>(val.clone()) {
-                            let hex_str = float_array_to_hex(&arr);
-                            viewport.insert("bg_color".to_string(), serde_json::Value::String(hex_str));
-                        }
-                    }
-                    if let Some(val) = viewport.get("grid_color") {
-                        if let Ok(arr) = serde_json::from_value::<[f32; 3]>(val.clone()) {
-                            let hex_str = float_array_to_hex(&arr);
-                            viewport.insert("grid_color".to_string(), serde_json::Value::String(hex_str));
-                        }
+                for &(block, field, n) in Self::COLOR_FIELDS {
+                    let Some(b) = obj.get_mut(block).and_then(|v| v.as_object_mut()) else { continue };
+                    let Some(val) = b.get(field).cloned() else { continue };
+                    let hex = if n == 4 {
+                        serde_json::from_value::<[f32; 4]>(val).ok().map(|a| float_array_to_hex4(&a))
+                    } else {
+                        serde_json::from_value::<[f32; 3]>(val).ok().map(|a| float_array_to_hex(&a))
+                    };
+                    if let Some(hex) = hex {
+                        b.insert(field.to_string(), serde_json::Value::String(hex));
                     }
                 }
             }
@@ -1887,26 +2013,6 @@ impl State {
 
 
 
-    pub fn update_recent_files_layout(&mut self) {
-        let mut opts = vec!["- Select -".to_string()];
-        for path in &self.recent_files {
-            opts.push(path.to_string_lossy().to_string());
-        }
-        opts.push("Other".to_string());
-
-        let main_node = self
-            .session_node_mut()
-            .and_then(|s| s.children.iter_mut().find(|c| c.name == "main"));
-        if let Some(main_node) = main_node {
-            if let Some(p) = main_node.params.iter_mut().find(|p| p.name == "Open") {
-                p.options = opts;
-                if !p.options.contains(&p.default) {
-                    p.default = "- Select -".to_string();
-                }
-            }
-        }
-        self.sync_parameters_pane();
-    }
 
     pub fn save_settings(&mut self) {
         let settings = DesignSettings {
@@ -1925,6 +2031,21 @@ impl State {
                 show_point_markers: self.show_point_markers,
                 show_point_numbers: self.show_point_numbers,
                 show_point_normals: self.show_point_normals,
+                point_marker_size: self.point_marker_size,
+                point_marker_color: self.point_marker_color,
+                world_unit: self.world_unit.suffix().to_string(),
+                rt_mode: self.viewport().rt_mode,
+                circular_pane: self.circular_network_pane,
+            },
+            render: RenderSettings {
+                wireframe: self.wireframe,
+                wire_single_color: self.wire_single_color,
+                wire_color: self.wire_color,
+                wire_width: self.wire_width,
+                geo_opacity: self.geo_opacity,
+                render_points: self.render_points,
+                point_size: self.point_size,
+                point_color: self.point_color,
             },
             default_project: self.default_project_setting.clone(),
         };
@@ -2478,37 +2599,6 @@ impl State {
         self.splitter_layout.clamp(self.width, self.detached_circular_network);
     }
 
-    /// The Session node: the permanent root container for the session-wide
-    /// settings nodes (Main/View/Guides/Render). `ensure_menubar_subnets`
-    /// guarantees it exists, so `None` only before the first ensure.
-    pub fn session_node(&self) -> Option<&FsNode> {
-        self.fs_root.children.iter().find(|c| c.node_type == "meta")
-    }
-
-    pub fn session_node_mut(&mut self) -> Option<&mut FsNode> {
-        self.fs_root.children.iter_mut().find(|c| c.node_type == "meta")
-    }
-
-    /// Is the network currently inside a settings directory (the Session node
-    /// or any utility node)? Geometry templates are refused there. Checks the
-    /// whole path, not `current_path[0]` — the settings nodes live NESTED
-    /// under Session now, so the old first-segment check would miss them.
-    pub fn in_settings_dir(&self) -> bool {
-        let mut node = &self.fs_root;
-        for &idx in &self.current_path {
-            match node.children.get(idx) {
-                Some(child) => {
-                    if matches!(child.node_type.as_str(), "utility" | "session" | "meta") {
-                        return true;
-                    }
-                    node = child;
-                }
-                None => return false,
-            }
-        }
-        false
-    }
-
     pub fn current_dir(&self) -> &FsNode {
         let mut node = &self.fs_root;
         for &i in &self.current_path {
@@ -2730,7 +2820,6 @@ impl State {
                     }
 
                     if param_changed {
-                        self.apply_settings_from_menubar_subnets();
                         self.sync_grid_settings();
                         self.rebuild_scene_geometry();
                         self.sync_nodes();
@@ -3153,91 +3242,14 @@ impl State {
         }
     }
 
-    /// Write a toggle on the session's Render utility node — the source
-    /// `apply_settings_from_menubar_subnets` reads render settings from —
-    /// so a command that changed the live state leaves the node agreeing
-    /// with it. Nothing when the project has no Render node yet.
-    pub(crate) fn write_render_toggle(&mut self, name: &str, val: bool) {
-        self.write_meta_toggle("render", name, val);
-    }
 
-    /// The Guides node's counterpart: what Show Grid, Show Cube and Show
-    /// Origin write, for the same reason Show Wireframe writes the Render
-    /// node — see [`State::write_meta_toggle`].
-    pub(crate) fn write_guides_toggle(&mut self, name: &str, val: bool) {
-        self.write_meta_toggle("guides", name, val);
-    }
 
-    /// Write a toggle's value onto the utility subnet that OWNS it.
-    ///
-    /// A command that flips only the live flag has flipped it until the next
-    /// parameter edit anywhere: `apply_settings_from_menubar_subnets` copies
-    /// the meta node's subnets onto the live state on every change, so the
-    /// stored value wins and the toggle silently reverts. Show Cube did
-    /// exactly that on 2026-09-21 — hidden by its command, back the moment a
-    /// node's parameter was edited. The subnet param is the value's one
-    /// owner (the dialog's Settings half writes there too), so a command
-    /// that changes the value writes it there.
-    pub(crate) fn write_meta_toggle(&mut self, subnet: &str, name: &str, val: bool) {
-        if let Some(p) = self
-            .fs_root
-            .children
-            .iter_mut()
-            .find(|c| c.node_type == "meta")
-            .and_then(|s| s.children.iter_mut().find(|c| c.name == subnet))
-            .and_then(|n| n.params.iter_mut().find(|p| p.name == name))
-        {
-            p.default = if val { "true" } else { "false" }.to_string();
-        }
-    }
 
-    pub(crate) fn refresh_main_node_live_toggles(&mut self, slot_idx: usize) {
-        let live_main: [(&str, bool); 2] = [
-            ("Circular Pane", self.circular_network_pane),
-            ("Ray Traced Preview", self.viewport().rt_mode),
-        ];
-        let live_view: [(&str, bool); 6] = [
-            ("Show Network Plate", self.network_plate),
-            ("Show Network Pane", self.show_network),
-            ("Show Viewport Pane", self.show_viewport),
-            ("Show Parameters Pane", self.show_parameters),
-            ("Show Spreadsheet Pane", self.show_spreadsheet),
-            ("Show Playbar Pane", self.show_playbar),
-        ];
-        let live_guides: [(&str, bool); 3] = [
-            ("Show Grid Guide", self.viewport().show_grid),
-            ("Show Reference Cube", self.viewport().show_cube),
-            ("Show Origin Axes", self.viewport().show_origin),
-        ];
-        let live_render: [(&str, bool); 2] = [
-            ("Show Wireframe", self.wireframe),
-            ("Wire Single Color", self.wire_single_color),
-        ];
-        let dir = self.param_editor_dir_mut();
-        let Some(child) = dir.children.get_mut(slot_idx) else { return };
-        let live: &[(&str, bool)] = match child.name.as_str() {
-            "main" => &live_main,
-            "view" => &live_view,
-            "guides" => &live_guides,
-            "render" => &live_render,
-            _ => return,
-        };
-        for &(name, on) in live {
-            if let Some(p) = child.params.iter_mut().find(|p| p.name == name && p.param_type == "toggle") {
-                p.default = if on { "true" } else { "false" }.to_string();
-            }
-        }
-    }
 
     pub fn sync_parameters_pane(&mut self) {
         // Selection reads through the param-editor accessors: whichever
         // network editor took the last node click feeds the pane, at ITS
         // level — no matter the tab.
-        if !self.is_detached_network {
-            if let Some(slot_idx) = self.param_editor_selected() {
-                self.refresh_main_node_live_toggles(slot_idx);
-            }
-        }
         let params = if !self.is_detached_network {
             if let Some(slot_idx) = self.param_editor_selected() {
                 let dir = self.param_editor_dir();
@@ -3517,7 +3529,7 @@ impl State {
 
     /// Open the node right-click context menu at the cursor for `slot`. The
     /// items are contextual: Enter (dive into the subnet) for enterable nodes,
-    /// Show/Hide Geometry for non-utility nodes, and Delete always.
+    /// Show/Hide Geometry, and Delete.
     fn open_node_context_menu(&mut self, slot: usize) {
         let (is_utility, geom_visible, enterable, curve_editing) = {
             let dir = self.current_dir();
@@ -3531,7 +3543,7 @@ impl State {
                 self.viewer_tool.as_ref().map(|t| t.node_id == node.id).unwrap_or(false)
             });
             (
-                matches!(node.node_type.as_str(), "utility" | "session" | "meta"),
+                false,
                 node.geometry_visible,
                 enterable,
                 curve_editing,
@@ -3541,7 +3553,7 @@ impl State {
             let dir = self.current_dir();
             dir.children
                 .get(slot)
-                .map(|n| !matches!(n.node_type.as_str(), "session" | "meta"))
+                .map(|_| true)
                 .unwrap_or(false)
         };
         let mut options: Vec<String> = Vec::new();
@@ -4241,14 +4253,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
 
     pub fn delete_node(&mut self, slot: usize) -> bool {
         let len = self.current_dir().children.len();
-        // The root meta node (nee Session) is permanent: every deletion
-        // route (context menu, Delete key, MCP) funnels through here, so
-        // this is the one gate.
-        if slot < len
-            && matches!(self.current_dir().children[slot].node_type.as_str(), "session" | "meta")
-        {
-            return false;
-        }
         if slot < len {
             self.current_dir_mut().children.remove(slot);
             if let Some(sel_idx) = self.graph().selected_node() {
@@ -4580,6 +4584,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             viewport.show_camera_pivot = settings.viewport.show_camera_pivot_enabled;
             viewport.bg_color = settings.viewport.bg_color;
             viewport.grid_color = settings.viewport.grid_color;
+            viewport.rt_mode = settings.viewport.rt_mode;
             viewport.active_camera = active_camera.clone();
         }
 
@@ -4742,7 +4747,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             graph_inertial_scroll: true,
             graph_scroll_friction: 0.90,
             last_config_read: Instant::now(),
-            circular_network_pane: is_detached_network,
+            circular_network_pane: is_detached_network || settings.viewport.circular_pane,
             circular_network_layout: cce_ui::layout::CircularPaneLayout::new(250.0, 300.0, 180.0),
             is_detached_network,
             detached_circular_network: false,
@@ -4814,20 +4819,20 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             last_viewport_height: 0,
             last_viewport_active_camera: String::new(),
             last_viewport_show_viewport: false,
-            wireframe: false,
+            wireframe: settings.render.wireframe,
             last_viewport_wireframe: false,
-            wire_single_color: false,
-            wire_color: [1.0, 1.0, 1.0, 1.0],
-            wire_width: 1.0,
+            wire_single_color: settings.render.wire_single_color,
+            wire_color: settings.render.wire_color,
+            wire_width: settings.render.wire_width,
             last_viewport_wire_single_color: false,
             last_viewport_wire_color: [1.0, 1.0, 1.0, 1.0],
             last_applied_wire_color: None,
             last_viewport_wire_width: 1.0,
-            geo_opacity: 1.0,
+            geo_opacity: settings.render.geo_opacity,
             last_viewport_geo_opacity: 1.0,
-            render_points: false,
-            point_size: 0.02,
-            point_color: [1.0, 1.0, 1.0],
+            render_points: settings.render.render_points,
+            point_size: settings.render.point_size,
+            point_color: settings.render.point_color,
             last_points_key: None,
             point_vertex_count: 0,
             last_viewport_render_points: false,
@@ -4847,9 +4852,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             show_point_numbers: settings.viewport.show_point_numbers,
             show_point_normals: settings.viewport.show_point_normals,
             scene_edge_verts: Vec::new(),
-            point_marker_size: 0.02,
-            point_marker_color: [0.85, 0.85, 1.0],
-            world_unit: cce_ui::units::Unit::Mm,
+            point_marker_size: settings.viewport.point_marker_size,
+            point_marker_color: settings.viewport.point_marker_color,
+            world_unit: cce_ui::units::Unit::parse(&settings.viewport.world_unit)
+                .unwrap_or(cce_ui::units::Unit::Mm),
             pick_cache: None,
             last_scene_mvp: None,
             last_scene_view_rect: (0.0, 0.0, 0.0, 0.0),
@@ -4865,8 +4871,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
         state.update_graph_settings_from_config();
         state.update_window_title();
         colors::set_node_color(state.node_color);
-        state.ensure_menubar_subnets();
-        state.apply_settings_from_menubar_subnets();
+        state.migrate_meta_settings_node();
         state.sync_nodes();
         state.rebuild_scene_geometry();
         state.sync_grid_settings();
@@ -5786,7 +5791,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 widget.set_rect(x, y, w, h);
             }
         }
-        self.update_recent_files_layout();
     }
 
     pub fn update_panel_bounds(&mut self) {
@@ -5895,7 +5899,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                     .map(|p| p.default.clone()),
                 position: c.position,
                 // Utility trees stay where they were put; see the module doc.
-                pinned: matches!(c.node_type.as_str(), "utility" | "session" | "meta"),
+                pinned: false,
             })
             .collect();
         let moved = crate::layout::arrange(&nodes);
@@ -6414,21 +6418,18 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             Action::ToggleGrid => {
                 let val = !self.viewport().show_grid;
                 self.viewport_mut().show_grid = val;
-                self.write_guides_toggle("Show Grid Guide", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 0, val);
                 settings_changed = true;
             }
             Action::ToggleCube => {
                 let val = !self.viewport().show_cube;
                 self.viewport_mut().show_cube = val;
-                self.write_guides_toggle("Show Reference Cube", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 1, val);
                 settings_changed = true;
             }
             Action::ToggleOrigin => {
                 let val = !self.viewport().show_origin;
                 self.viewport_mut().show_origin = val;
-                self.write_guides_toggle("Show Origin Axes", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 2, val);
                 settings_changed = true;
             }
@@ -6440,14 +6441,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 settings_changed = true;
             }
             Action::ToggleWireframe => {
-                // The Render utility node's "Show Wireframe" toggle is the
-                // value `apply_settings_from_menubar_subnets` reads back on
-                // EVERY parameter edit, so flipping the flag alone would
-                // revert on the next unrelated edit: the node's toggle is
-                // written too, and the pane shows the switch moved.
                 let val = !self.wireframe;
                 self.wireframe = val;
-                self.write_render_toggle("Show Wireframe", val);
                 // The edge list is collected with the scene and dropped
                 // while the wireframe is off, so switching it on has to
                 // rebuild — there is nothing staged to draw otherwise.
@@ -6472,6 +6467,22 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.rebuild_scene_geometry();
                 settings_changed = true;
             }
+            Action::ToggleRenderPoints => {
+                self.render_points = !self.render_points;
+                self.viewport_dirty = true;
+                settings_changed = true;
+            }
+            Action::ToggleWireSingleColor => {
+                self.wire_single_color = !self.wire_single_color;
+                self.viewport_dirty = true;
+                settings_changed = true;
+            }
+            Action::ToggleRayTracedPreview => {
+                let val = !self.viewport().rt_mode;
+                self.viewport_mut().rt_mode = val;
+                self.viewport_dirty = true;
+                settings_changed = true;
+            }
             Action::WireframeColor => self.open_dialog_on_settings(),
             Action::ToggleSquareViewport => {
                 self.square_viewport = !self.square_viewport;
@@ -6520,7 +6531,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 // The Main node owns this one, as Guides owns the guide
                 // toggles above: without the write, the next parameter edit
                 // put the pane back the way the node said.
-                self.write_meta_toggle("main", "Circular Pane", val);
                 self.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
                 self.rebuild_positions();
                 self.apply_layout();
@@ -8160,7 +8170,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                                                 for slot_idx in selected {
                                                     let visible = target.unwrap_or(false);
                                                     let dir = self.current_dir();
-                                                    if slot_idx < dir.children.len() && dir.children[slot_idx].node_type != "utility" {
+                                                    if slot_idx < dir.children.len() {
                                                         self.current_dir_mut().set_child_geometry_visible(slot_idx, visible);
                                                         self.sync_nodes();
                                                         self.rebuild_scene_geometry();
@@ -8478,8 +8488,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             self.viewport_mut().pending_pitch = 0.0;
         }
 
-        self.update_recent_files_layout();
-
         // Drag-release fling: the velocity a middle / space+left drag had
         // when the button went up keeps the canvas sliding under friction.
         // (Wheel and trackpad motion coast inside the Graph widget instead.)
diff --git a/src/command.rs b/src/command.rs
index 4d76fa5..0ca75c9 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -196,6 +196,9 @@ pub const COMMANDS: &[Command] = &[
     Command { id: "toggle_point_markers", label: "Show Point Markers", context: Context::Viewport, run: Run::Key(Action::TogglePointMarkers), default_chord: None },
     Command { id: "toggle_point_numbers", label: "Show Point Numbers", context: Context::Viewport, run: Run::Key(Action::TogglePointNumbers), default_chord: None },
     Command { id: "toggle_point_normals", label: "Show Point Normals", context: Context::Viewport, run: Run::Key(Action::TogglePointNormals), default_chord: None },
+    Command { id: "toggle_render_points", label: "Show Points", context: Context::Viewport, run: Run::Key(Action::ToggleRenderPoints), default_chord: None },
+    Command { id: "toggle_wire_single_color", label: "Wireframe Single Color", context: Context::Viewport, run: Run::Key(Action::ToggleWireSingleColor), default_chord: None },
+    Command { id: "toggle_ray_traced_preview", label: "Ray Traced Preview", context: Context::Viewport, run: Run::Key(Action::ToggleRayTracedPreview), default_chord: None },
     Command { id: "toggle_square_viewport", label: "Square Aspect", context: Context::Viewport, run: Run::Key(Action::ToggleSquareViewport), default_chord: Some("Ctrl+a") },
 
     // --- Parameters ---
diff --git a/src/dialog.rs b/src/dialog.rs
index 8d107a5..20aec18 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -11,9 +11,8 @@
 //! (`DIALOG_PARAMS_IDX`, a `ParametersBg`) laid out inside this one's body, so
 //! a slider in the dialog is the same slider as a slider in the params pane
 //! rather than a second implementation that drifts from it. The values behind
-//! those rows stay owned by the meta node's utility subnets, which is where
-//! `apply_settings_from_menubar_subnets` reads them from — see
-//! `State::dialog_settings_rows`.
+//! those rows are the live `State` fields, persisted by `DesignSettings` —
+//! see [`SETTINGS`] and `Owner`.
 //!
 //! App-owned on the narrow traits wrapped in `Adapted<Dialog>`, like
 //! [`crate::playbar::Playbar`], and a subtree painter for the same reason:
@@ -104,7 +103,13 @@ pub struct Row {
 /// list, and a list that grows with the window turns into a wall of rows with
 /// the one you want somewhere in it.
 const DIALOG_W: f32 = 520.0;
-const DIALOG_H: f32 = 420.0;
+/// The plate's height, clamped to the window by [`layout_in`].
+///
+/// 420 until 2026-09-23, which was sized for a Settings half of thirteen
+/// rows. Retiring the root meta node moved everything its four utility
+/// subnets held into that table — it is nearer thirty now — and a list that
+/// shows eight of them is a list you scroll rather than read.
+const DIALOG_H: f32 = 640.0;
 
 const PAD: f32 = 12.0;
 /// Tab strip height, and the query line's.
@@ -1091,94 +1096,176 @@ pub const ZOOM_ROW_ID: &str = "zoom_level";
 /// commands act on.
 pub const PATH_ROW_ID: &str = "project_path";
 
-/// Where a Settings row's value actually lives.
+/// Prefix of a recent-project row's id; the rest is the path.
+///
+/// The recent list was the Main utility node's "Open" dropdown, and it went
+/// with that node — leaving `State::recent_files` written on every save and
+/// read by nothing. It is a list of documents, so it belongs where the open
+/// document's own path already is: rows under the path row, each opening its
+/// project. Ranked against the path text like everything else.
+pub const RECENT_ROW_PREFIX: &str = "recent:";
+
+/// How many recent projects the list offers. `recent_files` keeps ten; five
+/// is what fits above the commands without the palette reading as a file
+/// manager, and a query narrows the rest.
+pub const RECENT_ROW_LIMIT: usize = 5;
+
+/// Where a Settings row's value lives.
 ///
-/// Not the live `State` fields, and not `DesignSettings`: both are DOWNSTREAM
-/// of the meta node. `apply_settings_from_menubar_subnets` copies the utility
-/// subnets onto the live state on every param change, so a write straight to
-/// `State::grid_thickness` would survive exactly until the next one. The
-/// subnet param is the value's owner; this enum says which owner each row has,
-/// so the dialog edits values in the one place that keeps them.
+/// It used to be neither the live `State` fields nor `DesignSettings`: both
+/// were DOWNSTREAM of the root meta node, whose utility subnets were copied
+/// over live state on every param change, so a write straight to
+/// `State::grid_thickness` survived exactly until the next one. With that
+/// node retired the live field IS the value; this enum says which of the
+/// three remaining kinds of owner each row has.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Owner {
-    /// A param on a utility subnet under the root meta node (`Main`, `View`,
-    /// `Guides`), named here by subnet and param name.
-    Subnet(&'static str, &'static str),
+    /// A display setting the app owns outright: a live field on `State`,
+    /// persisted by `DesignSettings` into `state.kdl`. Named by the key
+    /// `settings_field_read` / `settings_field_write` dispatch on.
+    ///
+    /// These were `Subnet(node, param)` — a param on a utility node under the
+    /// root meta node — until 2026-09-23. That node tree WAS the store of
+    /// record: `apply_settings_from_menubar_subnets` copied it onto the live
+    /// state after every edit, so writing a live field directly survived
+    /// until the next unrelated change and no longer. With the meta node
+    /// retired the live field is simply the value, and the row writes it.
+    Field(&'static str),
     /// A toggle the command registry already owns end to end: the command does
-    /// the live flip, the menu checkmark AND the per-camera writeback in one
-    /// place. Square Aspect and Show Camera Pivot are per-CAMERA settings with
-    /// no node at all behind the Default Camera, and their commands are the
-    /// only code that gets both cases right — so the row dispatches instead of
-    /// writing, and reads its displayed value off the live state.
+    /// the live flip, the menu checkmark AND any writeback in one place.
+    /// Square Aspect and Show Camera Pivot are per-CAMERA settings with no
+    /// node at all behind the Default Camera, and their commands are the only
+    /// code that gets both cases right — so the row dispatches instead of
+    /// writing, and reads its displayed value off `command_toggle_state`, the
+    /// same table the palette's own switches read.
     Command(&'static str),
-    /// A param on the ACTIVE camera node, with the live field as the fallback:
-    /// the Default Camera has no node, so there is nothing to write but the
-    /// field, and `apply_settings_from_menubar_subnets` leaves it alone.
+    /// A param on the ACTIVE camera node, with the live field as the
+    /// fallback: the Default Camera has no node, so there is nothing to write
+    /// but the field.
     ActiveCamera(&'static str),
 }
 
+/// The control a [`Setting`] row draws, for the rows that have no param
+/// elsewhere to borrow a shape from.
+///
+/// `Owner::Field` rows need this because their value is a bare Rust field —
+/// there is no `ParamDef` behind them carrying a type and a range the way a
+/// subnet param did. Spelling it here keeps the table the single description
+/// of the Settings half.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Ctl {
+    Toggle,
+    /// `#rrggbb`.
+    Color,
+    /// `#rrggbbaa` — the wire colour, whose alpha is its own opacity.
+    Rgba,
+    /// An integer spinbox over `min..=max`. The stored float is scaled by
+    /// `unit` (thousandths for Grid Thickness, tenths for Origin Size), which
+    /// is the convention those params already used.
+    Spin { min: f32, max: f32, unit: f32 },
+    /// A float slider, `min..=max`, shown to `dec` decimals.
+    Slider { min: f32, max: f32, dec: usize },
+    /// A fixed set of strings.
+    Choice(&'static [&'static str]),
+}
+
 /// One row of the Settings half.
 pub struct Setting {
     /// What the dialog calls it — and, because `param_display` keys a row by
-    /// its label, the identity the writeback resolves back to this row. Unique
-    /// across the table, section titles included.
-    ///
-    /// Spelled out rather than borrowed from the owning param's own label: the
-    /// subnets' labels are tuned for the section they sit in ("Plate", under
-    /// the View node's Network section) and stop making sense anywhere else.
+    /// its label, the identity the writeback resolves back to this row.
+    /// Unique across the table, section titles included.
     pub label: &'static str,
     pub owner: Option<Owner>,
+    /// The control, for `Owner::Field` rows. `Command` rows are always
+    /// switches and `ActiveCamera` rows borrow the camera param's shape.
+    pub ctl: Option<Ctl>,
 }
 
 impl Setting {
     const fn section(label: &'static str) -> Self {
-        Setting { label, owner: None }
+        Setting { label, owner: None, ctl: None }
+    }
+
+    /// A row whose value the command registry owns.
+    const fn cmd(label: &'static str, id: &'static str) -> Self {
+        Setting { label, owner: Some(Owner::Command(id)), ctl: Some(Ctl::Toggle) }
     }
 
-    const fn row(label: &'static str, owner: Owner) -> Self {
-        Setting { label, owner: Some(owner) }
+    /// A row over a live field.
+    const fn field(label: &'static str, key: &'static str, ctl: Ctl) -> Self {
+        Setting { label, owner: Some(Owner::Field(key)), ctl: Some(ctl) }
+    }
+
+    const fn camera(label: &'static str, name: &'static str) -> Self {
+        Setting { label, owner: Some(Owner::ActiveCamera(name)), ctl: None }
     }
 }
 
 /// The Settings half, in order.
 ///
-/// Scope is exactly what `DesignSettings` persists — the viewport and graph
-/// DISPLAY state, which is the part of the app's configuration that is a
-/// preference rather than part of a project. What is deliberately NOT here:
-/// the render subnet (per-project look), the pane-visibility toggles (the
-/// View menu and the plate corners already own those, and a settings dialog
-/// is a strange place to hide a pane from), and keybindings, which this DE
-/// edits as `input.kdl` on purpose. The one exception is the wireframe
-/// pair: the palette's Wireframe Color command lands on the colour, so it
-/// is a row here with the single-colour switch that makes it apply, their
-/// owner still the Render node — per-project, unlike the rest of this
-/// table.
+/// Scope is exactly what `DesignSettings` persists: the DISPLAY state, which
+/// is the part of the app's configuration that is a preference rather than
+/// part of a project. That is now the whole of it — the four utility subnets
+/// under the root meta node (`main`, `view`, `guides`, `render`) held these
+/// values until 2026-09-23, and every one of them that was reachable only by
+/// selecting one of those nodes is a row here. Anything left out would not
+/// be "hidden in the node tree", it would be gone.
+///
+/// Still deliberately NOT here: the pane-visibility toggles (the View menu
+/// and the plate corners own those, and a settings dialog is a strange place
+/// to hide a pane from), the active camera (the viewport menubar's own menu,
+/// whose entries are the camera NODES and so cannot be a fixed table), and
+/// keybindings, which this DE edits as `input.kdl` on purpose.
 pub const SETTINGS: &[Setting] = &[
     Setting::section("Viewport"),
-    Setting::row("Background Color", Owner::Subnet("main", "Background Color")),
-    Setting::row("Square Aspect", Owner::Command("toggle_square_viewport")),
+    Setting::field("Background Color", "bg_color", Ctl::Color),
+    Setting::cmd("Square Aspect", "toggle_square_viewport"),
+    Setting::cmd("Ray Traced Preview", "toggle_ray_traced_preview"),
+    Setting::field("World Unit", "world_unit", Ctl::Choice(&["mm", "cm", "m", "in"])),
+    Setting::section("Geometry"),
+    Setting::field("Opacity", "geo_opacity", Ctl::Slider { min: 0.0, max: 1.0, dec: 2 }),
     Setting::section("Wireframe"),
-    Setting::row("Wireframe Color", Owner::Subnet("render", "Wire Color")),
+    Setting::cmd("Show Wireframe", "toggle_wireframe"),
+    Setting::field("Wireframe Color", "wire_color", Ctl::Rgba),
     // The colour applies only in single-colour mode (off, the wires carry
     // the geometry's vertex colours and the colour row sets their alpha
     // alone) — so the switch sits beside the colour, or a colour set here
     // looks ignored.
-    Setting::row("Wireframe Single Color", Owner::Subnet("render", "Wire Single Color")),
+    Setting::cmd("Wireframe Single Color", "toggle_wire_single_color"),
+    Setting::field("Wire Thickness", "wire_width", Ctl::Slider { min: 1.0, max: 8.0, dec: 1 }),
+    Setting::section("Points"),
+    Setting::cmd("Show Points", "toggle_render_points"),
+    Setting::field("Point Size", "point_size", Ctl::Slider { min: 0.0, max: 0.1, dec: 3 }),
+    Setting::field("Point Color", "point_color", Ctl::Color),
+    Setting::cmd("Show Point Markers", "toggle_point_markers"),
+    Setting::field("Point Marker Size", "point_marker_size", Ctl::Spin { min: 5.0, max: 100.0, unit: 1000.0 }),
+    Setting::field("Point Marker Color", "point_marker_color", Ctl::Color),
+    Setting::cmd("Show Point Numbers", "toggle_point_numbers"),
+    Setting::cmd("Show Point Normals", "toggle_point_normals"),
     Setting::section("Grid"),
-    Setting::row("Show Grid", Owner::Subnet("guides", "Show Grid Guide")),
-    Setting::row("Grid Color", Owner::Subnet("guides", "Grid Color")),
-    Setting::row("Grid Thickness", Owner::Subnet("guides", "Grid Thickness")),
+    Setting::cmd("Show Grid", "toggle_grid"),
+    Setting::field("Grid Color", "grid_color", Ctl::Color),
+    Setting::field("Grid Thickness", "grid_thickness", Ctl::Spin { min: 2.0, max: 200.0, unit: 1000.0 }),
     Setting::section("Guides"),
-    Setting::row("Show Origin Axes", Owner::Subnet("guides", "Show Origin Axes")),
-    Setting::row("Origin Size", Owner::Subnet("guides", "Origin Guide Size")),
-    Setting::row("Show Reference Cube", Owner::Subnet("guides", "Show Reference Cube")),
+    Setting::cmd("Show Origin Axes", "toggle_origin"),
+    Setting::field("Origin Size", "origin_size", Ctl::Spin { min: 1.0, max: 50.0, unit: 10.0 }),
+    Setting::cmd("Show Reference Cube", "toggle_cube"),
     Setting::section("Camera"),
-    Setting::row("Show Camera Pivot", Owner::Command("toggle_camera_pivot")),
-    Setting::row("Camera Pivot Size", Owner::ActiveCamera("Camera Pivot Size")),
+    Setting::cmd("Show Camera Pivot", "toggle_camera_pivot"),
+    Setting::camera("Camera Pivot Size", "Camera Pivot Size"),
     Setting::section("Network"),
-    Setting::row("Show Network Plate", Owner::Subnet("view", "Show Network Plate")),
+    Setting::cmd("Show Network Plate", "toggle_network_plate"),
+    Setting::cmd("Circular Pane", "toggle_circular_pane"),
 ];
 
+#[cfg(test)]
+fn field_key(s: &Setting) -> &'static str {
+    match s.owner {
+        Some(Owner::Field(key)) => key,
+        _ => panic!("'{}' is not a Field row", s.label),
+    }
+}
+
 fn setting_by_label(label: &str) -> Option<&'static Setting> {
     SETTINGS.iter().find(|s| s.label == label)
 }
@@ -1190,15 +1277,30 @@ fn relabel(src: &ParamDef, label: &'static str) -> ParamDef {
 }
 
 fn bool_param(label: &'static str, on: bool) -> ParamDef {
+    shaped(label, "toggle", if on { "true" } else { "false" }.to_string(), None, None, None, &[])
+}
+
+/// A synthetic `ParamDef` for a row with no param of its own behind it: the
+/// label is both its name and its display key, which is how the writeback
+/// resolves a control back to its `Setting`.
+fn shaped(
+    label: &str,
+    param_type: &str,
+    default: String,
+    min: Option<f32>,
+    max: Option<f32>,
+    step: Option<f32>,
+    options: &[&str],
+) -> ParamDef {
     ParamDef {
         name: label.to_string(),
         label: label.to_string(),
-        param_type: "toggle".to_string(),
-        default: if on { "true" } else { "false" }.to_string(),
-        options: Vec::new(),
-        min: None,
-        max: None,
-        step: None,
+        param_type: param_type.to_string(),
+        default,
+        options: options.iter().map(|s| s.to_string()).collect(),
+        min,
+        max,
+        step,
         show_when: String::new(),
     }
 }
@@ -1361,6 +1463,41 @@ impl State {
                         );
                     }
                 }
+                // The recent projects, under the path row — the open
+                // document, then the ones before it. The project already
+                // open is not offered again.
+                let open_now = self.loaded_project_path.clone();
+                let recent: Vec<std::path::PathBuf> = self
+                    .recent_files
+                    .iter()
+                    .filter(|p| Some(*p) != open_now.as_ref())
+                    .take(RECENT_ROW_LIMIT)
+                    .cloned()
+                    .collect();
+                for path in recent.iter().rev() {
+                    let text = path.to_string_lossy().to_string();
+                    if crate::command::fuzzy_rank(&query, &[text.as_str()]).is_empty() {
+                        continue;
+                    }
+                    let name = path
+                        .file_name()
+                        .map(|n| n.to_string_lossy().to_string())
+                        .unwrap_or_default();
+                    rows.insert(
+                        0,
+                        Row {
+                            id: format!("{RECENT_ROW_PREFIX}{text}"),
+                            label: text,
+                            chord: name,
+                            swatch: None,
+                            toggle: None,
+                            slider: None,
+                            // Same reason as the path row: the tail of a
+                            // path is what identifies it.
+                            truncate_head: true,
+                        },
+                    );
+                }
                 // The zoom slider heads the network pane's list, ranked like
                 // a row labelled "Zoom" so a query still finds (or drops) it.
                 if self.focused_context() == crate::command::Context::Network
@@ -1385,18 +1522,11 @@ impl State {
                 rows
             }
             Mode::AddNode => {
-                // In a utility dir geometry templates are rejected at
-                // placement — don't offer them.
-                let in_utility = self.in_settings_dir();
-                let offered: Vec<&str> = self
-                    .node_templates
-                    .iter()
-                    .filter(|t| {
-                        !in_utility
-                            || !crate::geometry::is_geometry_node_type(&t.node.node_type)
-                    })
-                    .map(|t| t.label.as_str())
-                    .collect();
+                // Every template, everywhere. The settings directories that
+                // refused geometry were the root meta node's utility subnets,
+                // and they are gone.
+                let offered: Vec<&str> =
+                    self.node_templates.iter().map(|t| t.label.as_str()).collect();
                 crate::command::fuzzy_rank(&query, &offered)
                     .into_iter()
                     .map(|i| Row {
@@ -1497,6 +1627,9 @@ impl State {
             "toggle_point_markers" => self.show_point_markers,
             "toggle_point_numbers" => self.show_point_numbers,
             "toggle_point_normals" => self.show_point_normals,
+            "toggle_render_points" => self.render_points,
+            "toggle_wire_single_color" => self.wire_single_color,
+            "toggle_ray_traced_preview" => self.viewport().rt_mode,
             "toggle_square_viewport" => self.square_viewport,
             "toggle_network_plate" => self.network_plate,
             "toggle_circular_pane" => self.circular_network_pane,
@@ -1535,15 +1668,6 @@ impl State {
     /// place, shared with the params pane, instead of a second copy here that
     /// could disagree about what a spinbox is.
     fn dialog_settings_params(&self) -> Vec<ParamDef> {
-        let subnet_param = |subnet: &str, name: &str| -> Option<&ParamDef> {
-            self.session_node()?
-                .children
-                .iter()
-                .find(|c| c.name == subnet)?
-                .params
-                .iter()
-                .find(|p| p.name == name)
-        };
         let camera_param = |name: &str| -> Option<&ParamDef> {
             if self.active_camera == "Default Camera" {
                 return None;
@@ -1560,49 +1684,31 @@ impl State {
         let mut out = Vec::with_capacity(SETTINGS.len());
         for s in SETTINGS {
             match s.owner {
-                None => out.push(ParamDef {
-                    name: s.label.to_string(),
-                    label: s.label.to_string(),
-                    param_type: "section".to_string(),
-                    default: String::new(),
-                    options: Vec::new(),
-                    min: None,
-                    max: None,
-                    step: None,
-                    show_when: String::new(),
-                }),
-                Some(Owner::Subnet(subnet, name)) => {
-                    // A subnet param that does not exist is a row that cannot
-                    // work, so it is not offered — the subnets are recreated
-                    // on every load, but a detached window has no meta node at
-                    // all.
-                    if let Some(p) = subnet_param(subnet, name) {
-                        out.push(relabel(p, s.label));
-                    }
+                None => out.push(shaped(s.label, "section", String::new(), None, None, None, &[])),
+                Some(Owner::Field(key)) => {
+                    let ctl = s.ctl.expect("a Field row declares its control");
+                    out.push(self.settings_field_param(s.label, key, ctl));
                 }
                 Some(Owner::Command(id)) => {
-                    let on = match id {
-                        "toggle_square_viewport" => self.square_viewport,
-                        "toggle_camera_pivot" => self.viewport().show_camera_pivot,
-                        _ => false,
-                    };
-                    out.push(bool_param(s.label, on));
+                    // The same table the palette's own switches read, so a
+                    // row here and a row there cannot disagree about which
+                    // way a toggle is set.
+                    out.push(bool_param(s.label, self.command_toggle_state(id).unwrap_or(false)));
                 }
                 Some(Owner::ActiveCamera(name)) => match camera_param(name) {
                     Some(p) => out.push(relabel(p, s.label)),
-                    // No camera node behind the Default Camera: the live field
-                    // is the value, in the same tenths the camera param uses.
-                    None => out.push(ParamDef {
-                        name: s.label.to_string(),
-                        label: s.label.to_string(),
-                        param_type: "spinbox".to_string(),
-                        default: ((self.camera_pivot_size * 10.0).round() as i32).to_string(),
-                        options: Vec::new(),
-                        min: Some(1.0),
-                        max: Some(50.0),
-                        step: Some(1.0),
-                        show_when: String::new(),
-                    }),
+                    // No camera node behind the Default Camera: the live
+                    // field is the value, in the same tenths the camera param
+                    // uses.
+                    None => out.push(shaped(
+                        s.label,
+                        "spinbox",
+                        ((self.camera_pivot_size * 10.0).round() as i32).to_string(),
+                        Some(1.0),
+                        Some(50.0),
+                        Some(1.0),
+                        &[],
+                    )),
                 },
             }
         }
@@ -1618,6 +1724,171 @@ impl State {
         trimmed
     }
 
+    /// One `Owner::Field` row: the live value, in the shape its `Ctl` names.
+    fn settings_field_param(&self, label: &'static str, key: &str, ctl: Ctl) -> ParamDef {
+        match ctl {
+            Ctl::Toggle => bool_param(label, self.settings_field_bool(key)),
+            Ctl::Color => {
+                let c = self.settings_field_color(key);
+                shaped(label, "color", crate::project::color_to_hex(c), None, None, None, &[])
+            }
+            Ctl::Rgba => {
+                let c = match key {
+                    "wire_color" => self.wire_color,
+                    _ => [0.0, 0.0, 0.0, 1.0],
+                };
+                shaped(label, "rgba", crate::project::color_to_hex8(c), None, None, None, &[])
+            }
+            Ctl::Spin { min, max, unit } => shaped(
+                label,
+                "spinbox",
+                ((self.settings_field_f32(key) * unit).round() as i32).to_string(),
+                Some(min),
+                Some(max),
+                Some(1.0),
+                &[],
+            ),
+            Ctl::Slider { min, max, dec } => shaped(
+                label,
+                &format!("slider:{min:.*}:{max:.*}:{dec}", dec, dec),
+                format!("{:.*}", dec, self.settings_field_f32(key)),
+                Some(min),
+                Some(max),
+                None,
+                &[],
+            ),
+            Ctl::Choice(options) => {
+                shaped(label, "choice", self.settings_field_text(key), None, None, None, options)
+            }
+        }
+    }
+
+    fn settings_field_bool(&self, key: &str) -> bool {
+        match key {
+            "wire_single_color" => self.wire_single_color,
+            "render_points" => self.render_points,
+            _ => false,
+        }
+    }
+
+    fn settings_field_color(&self, key: &str) -> [f32; 3] {
+        match key {
+            "bg_color" => self.viewport().bg_color,
+            "grid_color" => self.viewport().grid_color,
+            "point_color" => self.point_color,
+            "point_marker_color" => self.point_marker_color,
+            _ => [0.0; 3],
+        }
+    }
+
+    fn settings_field_f32(&self, key: &str) -> f32 {
+        match key {
+            "grid_thickness" => self.grid_thickness,
+            "origin_size" => self.origin_size,
+            "point_marker_size" => self.point_marker_size,
+            "wire_width" => self.wire_width,
+            "geo_opacity" => self.geo_opacity,
+            "point_size" => self.point_size,
+            _ => 0.0,
+        }
+    }
+
+    fn settings_field_text(&self, key: &str) -> String {
+        match key {
+            "world_unit" => self.world_unit.suffix().to_string(),
+            _ => String::new(),
+        }
+    }
+
+    /// Write one `Owner::Field` row's new value onto the live state.
+    ///
+    /// `settings_field_keys_are_all_handled` walks the table against these
+    /// four readers and this writer, because a key that no arm names reads
+    /// as a default and writes nowhere — a row that looks live and is inert.
+    fn settings_field_write(&mut self, key: &str, ctl: Ctl, value: &str) {
+        match ctl {
+            Ctl::Toggle => {
+                let on = value == "true";
+                match key {
+                    "wire_single_color" => self.wire_single_color = on,
+                    "render_points" => self.render_points = on,
+                    _ => {}
+                }
+            }
+            Ctl::Color => {
+                let Some(c) = crate::project::hex_to_color(value) else { return };
+                match key {
+                    "bg_color" => self.viewport_mut().bg_color = c,
+                    "grid_color" => self.viewport_mut().grid_color = c,
+                    "point_color" => self.point_color = c,
+                    "point_marker_color" => self.point_marker_color = c,
+                    _ => {}
+                }
+            }
+            Ctl::Rgba => {
+                let Some(c) = crate::project::hex_to_rgba(value) else { return };
+                if key == "wire_color" {
+                    // Setting a wire colour means wanting to see it: the
+                    // colour applies in single-colour mode only, so a colour
+                    // edit turns that mode on if it was off. Twice read as
+                    // "the colour did not take" (2026-09-21).
+                    let changed = c != self.wire_color;
+                    self.wire_color = c;
+                    if changed && !self.wire_single_color {
+                        self.wire_single_color = true;
+                    }
+                }
+            }
+            Ctl::Spin { unit, .. } => {
+                let Ok(v) = value.parse::<f32>() else { return };
+                let v = v / unit;
+                match key {
+                    "grid_thickness" => self.grid_thickness = v,
+                    "origin_size" => self.origin_size = v,
+                    "point_marker_size" => self.point_marker_size = v,
+                    _ => {}
+                }
+            }
+            Ctl::Slider { min, max, .. } => {
+                let Ok(v) = value.parse::<f32>() else { return };
+                let v = v.clamp(min, max);
+                match key {
+                    "wire_width" => self.wire_width = v,
+                    "geo_opacity" => self.geo_opacity = v,
+                    "point_size" => self.point_size = v,
+                    _ => {}
+                }
+            }
+            Ctl::Choice(_) => {
+                if key == "world_unit" {
+                    if let Some(u) = cce_ui::units::Unit::parse(value) {
+                        self.world_unit = u;
+                        self.viewport_dirty = true;
+                    }
+                }
+            }
+        }
+    }
+
+    /// One Settings row's value as the dialog would show it. Test-facing:
+    /// `dialog_settings_rows_name_owners_that_exist` round-trips every
+    /// `Owner::Field` row through this and [`State::settings_write_row`],
+    /// which is the only way to catch a key that no dispatch arm names.
+    #[cfg(test)]
+    pub(crate) fn settings_row_value(&self, label: &str) -> String {
+        let s = SETTINGS.iter().find(|s| s.label == label).expect("no such Settings row");
+        let ctl = s.ctl.expect("that row declares no control");
+        self.settings_field_param(s.label, field_key(s), ctl).default
+    }
+
+    /// Write one Settings row, as the writeback does.
+    #[cfg(test)]
+    pub(crate) fn settings_write_row(&mut self, label: &str, value: &str) {
+        let s = SETTINGS.iter().find(|s| s.label == label).expect("no such Settings row");
+        let ctl = s.ctl.expect("that row declares no control");
+        self.settings_field_write(field_key(s), ctl, value);
+    }
+
     /// Push the Settings rows into the dialog's params body, and remember them
     /// as the baseline the writeback diffs against.
     pub fn refresh_dialog_settings(&mut self) {
@@ -1650,14 +1921,9 @@ impl State {
             let Some(owner) = setting.owner else { continue };
             changed = true;
             match owner {
-                Owner::Subnet(subnet, name) => {
-                    if let Some(session) = self.session_node_mut() {
-                        if let Some(node) = session.children.iter_mut().find(|c| c.name == subnet) {
-                            if let Some(p) = node.params.iter_mut().find(|p| p.name == name) {
-                                p.default = value.clone();
-                            }
-                        }
-                    }
+                Owner::Field(key) => {
+                    let ctl = setting.ctl.expect("a Field row declares its control");
+                    self.settings_field_write(key, ctl, value);
                 }
                 Owner::Command(id) => commands.push(id),
                 Owner::ActiveCamera(name) => {
@@ -1697,7 +1963,6 @@ impl State {
             self.run_command(id);
         }
 
-        self.apply_settings_from_menubar_subnets();
         // The viewport meshes bake their sizes and colors in, so a changed
         // thickness/size/tint is a re-generate, not a re-draw. This is the
         // same set the settings-file reload in `tick_frame` regenerates.
@@ -1889,6 +2154,18 @@ impl State {
             self.copy_project_path();
             return;
         }
+        // A recent project: open it, and say so if it will not open — a
+        // path in this list can have been moved or deleted since.
+        if mode == Mode::Tabbed {
+            if let Some(path) = id.strip_prefix(RECENT_ROW_PREFIX) {
+                let path = std::path::PathBuf::from(path);
+                self.close_dialog();
+                if let Err(e) = self.load_from_file(&path) {
+                    self.update_status_text(&format!("Could not open {}: {e}", path.display()));
+                }
+                return;
+            }
+        }
         if mode == Mode::Tabbed && self.command_toggle_state(&id).is_some() {
             self.run_command(&id);
             self.refresh_dialog_toggles();
diff --git a/src/layout.rs b/src/layout.rs
index b705fbe..33cc26a 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -19,10 +19,10 @@
 //! the shortest, so a node always sits below every one of its inputs rather
 //! than beside one of them.
 //!
-//! Utility nodes are pinned. The settings tree lives at a place the user put
-//! it, and an "arrange everything" that relocated the meta node would be a
-//! surprise every time. Their cells are treated as occupied so nothing lands
-//! on top of them.
+//! Nothing is pinned any more. The settings tree — the root meta node and
+//! its four utility subnets — was, because it lived where the user put it and
+//! relocating it would have been a surprise every time; it is retired, and
+//! `Node::pinned` outlives it for whatever wants it next.
 
 /// One node's layout input: what it is called, what it reads, where it is now,
 /// and whether it may be moved.
diff --git a/src/main.rs b/src/main.rs
index 23d4d2a..4f4846c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -321,19 +321,20 @@ mod tests {
         assert_eq!(shape.default, "None");
     }
 
-    /// The button must exist on Main, inside the File section, before Exit.
+    /// Set As Default is reachable. It was a button on the Main utility
+    /// node's File section; with that node retired it is a registry command
+    /// like the rest of that section, findable in the palette.
     #[test]
     fn test_main_node_offers_set_as_default() {
-        let mut state = State::new(false);
-        state.ensure_menubar_subnets();
-        let (s_idx, m_idx) = session_and_main(&state);
-        let main = &state.fs_root.children[s_idx].children[m_idx];
-        let names: Vec<&str> = main.params.iter().map(|p| p.name.as_str()).collect();
-        let idx = names.iter().position(|n| *n == "Set As Default").expect("Set As Default param");
-        let save_as = names.iter().position(|n| *n == "Save As").unwrap();
-        let exit = names.iter().position(|n| *n == "Exit").unwrap();
-        assert!(save_as < idx && idx < exit, "Set As Default out of place: {names:?}");
-        assert_eq!(main.params[idx].param_type, "button");
+        use crate::command::{by_id, Run};
+        let cmd = by_id("set_as_default").expect("no set_as_default command");
+        assert_eq!(cmd.label, "Set As Default");
+        assert_eq!(cmd.run, Run::Menu("Set As Default"));
+        // Its File-section neighbours are commands too, or the retirement
+        // of the Main node took them with it.
+        for id in ["new_project", "open_project", "save_document", "save_document_as", "exit"] {
+            assert!(by_id(id).is_some(), "the File section lost '{id}'");
+        }
     }
 
     /// A scratch project has no path — the click must not invent a default.
@@ -748,41 +749,59 @@ mod tests {
     }
 
     /// DE chrome is config-owned (`style.surface.relief.profile` /
-    /// `.edge_profile` / `style.surface.param.color`), so Main's retired Style
-    /// section must not come back from an older project file — while it did,
-    /// loading a project silently outranked the user's config.kdl.
+    /// `.edge_profile` / `style.surface.param.color`), and a project file
+    /// must not outrank the user's config.kdl. It did while the Main utility
+    /// node carried a Style section; the retirement of that whole node tree
+    /// is what closes it for good, so what is asserted now is that a save
+    /// carrying those params brings nothing back.
     #[test]
     fn test_legacy_style_params_are_dropped_from_main() {
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
-        let (s_idx, main_idx) = session_and_main(&state);
-
-        // Re-seed the params exactly as a pre-removal save carries them.
-        for (name, ty, val) in [
-            ("Style", "section", ""),
-            ("Bevel Profile", "ramp", "smooth;0.000:0.000,0.500:0.900,1.000:1.000"),
-            ("Edge Profile", "ramp", "smooth;0.000:0.000,1.000:1.000"),
-            ("Plate Color", "rgba", "#11223344"),
-        ] {
-            state.fs_root.children[s_idx].children[main_idx].params.push(crate::app::ParamDef {
-                name: name.to_string(),
-                label: String::new(),
-                param_type: ty.to_string(),
-                default: val.to_string(),
-                options: Vec::new(),
-                min: None,
-                max: None,
-                step: None,
-                show_when: String::new(),
-            });
-        }
+        // A pre-removal save: the four utility subnets flat at the root,
+        // Main carrying its retired Style section.
+        let style = |name: &str, ty: &str, val: &str| crate::app::ParamDef {
+            name: name.to_string(),
+            label: String::new(),
+            param_type: ty.to_string(),
+            default: val.to_string(),
+            options: Vec::new(),
+            min: None,
+            max: None,
+            step: None,
+            show_when: String::new(),
+        };
+        state.fs_root.children.push(crate::app::FsNode {
+            id: "legacy-main".to_string(),
+            name: "main".to_string(),
+            node_type: "utility".to_string(),
+            children: vec![],
+            params: vec![
+                style("Style", "section", ""),
+                style("Bevel Profile", "ramp", "smooth;0.000:0.000,1.000:1.000"),
+                style("Edge Profile", "ramp", "smooth;0.000:0.000,1.000:1.000"),
+                style("Plate Color", "rgba", "#11223344"),
+            ],
+            geometry_visible: true,
+            position: (0.0, 0.0),
+            inputs: 1,
+            outputs: 1,
+        });
+
+        state.migrate_meta_settings_node();
 
-        state.ensure_menubar_subnets();
-        let (s_idx, main_idx) = session_and_main(&state);
-        let names: Vec<&str> = state.fs_root.children[s_idx].children[main_idx]
-            .params.iter().map(|p| p.name.as_str()).collect();
+        assert!(
+            !state.fs_root.children.iter().any(|c| c.node_type == "utility"),
+            "a utility subnet survived the migration"
+        );
+        let names: Vec<&str> = state
+            .fs_root
+            .children
+            .iter()
+            .flat_map(|c| c.params.iter())
+            .map(|p| p.name.as_str())
+            .collect();
         for retired in ["Style", "Bevel Profile", "Edge Profile", "Plate Color"] {
-            assert!(!names.contains(&retired), "retired style param survived load: {retired} in {names:?}");
+            assert!(!names.contains(&retired), "retired style param survived load: {retired}");
         }
     }
 
@@ -807,14 +826,6 @@ mod tests {
         }
     }
 
-    /// The settings nodes live inside the permanent root meta node (nee
-    /// Session) now; tests that need Main resolve it through there.
-    fn session_and_main(state: &State) -> (usize, usize) {
-        let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").expect("root meta node");
-        let m_idx = state.fs_root.children[s_idx].children.iter().position(|c| c.name == "main").expect("main inside Session");
-        (s_idx, m_idx)
-    }
-
     /// `View 1:1` puts the pivot plane at true size: afterwards one world
     /// unit spans its real length on the display, so the readout's ratio
     /// is 1. Exercised on the default camera (zoom) at a centimetre world
@@ -823,7 +834,6 @@ mod tests {
     #[test]
     fn view_one_to_one_reaches_true_scale() {
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
         state.world_unit = cce_ui::units::Unit::Cm;
         // The default camera's eye ray is fixed, so 1:1 is a zoom — the one
         // number the readout's cached state can follow here without a
@@ -849,114 +859,202 @@ mod tests {
         assert!((mm - 1.0).abs() < 1e-3, "mm ratio {mm}");
     }
 
-    /// The root meta node (nee Session): exists at root, typed "meta" but
-    /// still a subnet, holds exactly the four settings nodes, and refuses
-    /// deletion through the one gate every deletion route funnels into.
+    /// The root meta node is gone, and a save that still carries one is
+    /// migrated rather than opened with it.
+    ///
+    /// It was the permanent root container for four utility subnets holding
+    /// every session-wide display setting — and it was the STORE OF RECORD
+    /// for them, copied back over live state after each parameter edit. That
+    /// made a display preference a piece of project data, editable only by
+    /// finding the right node. The settings live on `State` now and persist
+    /// to `state.kdl`; `migrate_meta_settings_node` reads an old file's
+    /// values across once and takes the node out.
     #[test]
     fn test_session_node_exists_and_cannot_be_deleted() {
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
-
-        let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").expect("root meta node");
-        let session = &state.fs_root.children[s_idx];
-        assert_eq!(session.name, "meta");
-        assert!(session.is_enterable(), "the root meta stays a subnet");
-        let names: Vec<&str> = session.children.iter().map(|c| c.name.as_str()).collect();
-        for expected in ["main", "view", "guides", "render"] {
-            assert!(names.contains(&expected), "Session is missing {expected}: {names:?}");
-        }
-        // None of the four remain at root.
-        for c in &state.fs_root.children {
-            assert!(
-                !(c.node_type == "utility" && matches!(c.name.as_str(), "main" | "view" | "guides" | "render")),
-                "settings node '{}' still at root", c.name
-            );
-        }
+        assert!(
+            !state.fs_root.children.iter().any(|c| c.node_type == "meta"),
+            "a fresh tree grew a meta node"
+        );
 
-        let before = state.fs_root.children.len();
-        assert!(!state.delete_node(s_idx), "delete_node deleted the root meta node");
-        assert_eq!(state.fs_root.children.len(), before, "root meta vanished anyway");
-        assert!(state.fs_root.children[s_idx].node_type == "meta");
-
-        // An old save's "session"-typed container retypes to meta in place,
-        // children intact.
-        state.fs_root.children[s_idx].node_type = "session".to_string();
-        state.fs_root.children[s_idx].name = "Session".to_string();
-        state.ensure_menubar_subnets();
-        let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta")
-            .expect("session retyped to meta");
-        assert_eq!(state.fs_root.children[s_idx].name, "meta");
-        let names: Vec<&str> =
-            state.fs_root.children[s_idx].children.iter().map(|c| c.name.as_str()).collect();
-        for expected in ["main", "view", "guides", "render"] {
-            assert!(names.contains(&expected), "retype lost {expected}: {names:?}");
-        }
-
-        // Guides carries the Point Marker Size control (thousandths), and
-        // applying the settings drives the overlay size.
-        {
-            let guides = state.fs_root.children[s_idx].children.iter_mut()
-                .find(|c| c.name == "guides").unwrap();
-            let p = guides.params.iter_mut().find(|p| p.name == "Point Marker Size")
-                .expect("Guides has Point Marker Size");
-            assert_eq!(p.default, "20", "default = 0.02 world units");
-            p.default = "50".to_string();
-            let c = guides.params.iter_mut().find(|p| p.name == "Point Marker Color")
-                .expect("Guides has Point Marker Color");
-            assert_eq!(c.param_type, "color");
-            c.default = "#ff8000".to_string();
-            let u = guides.params.iter_mut().find(|p| p.name == "World Unit")
-                .expect("Guides has World Unit");
-            assert_eq!(u.param_type, "choice");
-            assert_eq!(u.default, "mm", "a world unit is a millimetre until declared otherwise");
-            u.default = "cm".to_string();
-        }
-        state.apply_settings_from_menubar_subnets();
+        // An old save: a "session"-typed container with the four subnets,
+        // carrying values that are not the defaults.
+        let p = |name: &str, ty: &str, val: &str| crate::app::ParamDef {
+            name: name.to_string(),
+            label: String::new(),
+            param_type: ty.to_string(),
+            default: val.to_string(),
+            options: Vec::new(),
+            min: None,
+            max: None,
+            step: None,
+            show_when: String::new(),
+        };
+        let subnet = |name: &str, params: Vec<crate::app::ParamDef>| crate::app::FsNode {
+            id: format!("legacy-{name}"),
+            name: name.to_string(),
+            node_type: "utility".to_string(),
+            children: vec![],
+            params,
+            geometry_visible: true,
+            position: (0.0, 0.0),
+            inputs: 1,
+            outputs: 1,
+        };
+        state.fs_root.children.push(crate::app::FsNode {
+            id: "legacy-session".to_string(),
+            name: "Session".to_string(),
+            node_type: "session".to_string(),
+            children: vec![
+                subnet("guides", vec![
+                    p("Point Marker Size", "spinbox", "50"),
+                    p("Point Marker Color", "color", "#ff8000"),
+                    p("World Unit", "choice", "cm"),
+                    p("Grid Thickness", "spinbox", "40"),
+                    p("Show Reference Cube", "toggle", "true"),
+                ]),
+                subnet("render", vec![
+                    p("Show Wireframe", "toggle", "true"),
+                    p("Wire Thickness", "slider", "4.0"),
+                    p("Point Color", "color", "#00ff00"),
+                ]),
+                subnet("main", vec![p("Circular Pane", "toggle", "true")]),
+            ],
+            params: vec![],
+            geometry_visible: true,
+            position: (0.0, 0.0),
+            inputs: 0,
+            outputs: 0,
+        });
+
+        state.migrate_meta_settings_node();
+
+        // The node is gone, along with every utility subnet it held.
+        assert!(!state.fs_root.children.iter().any(|c| {
+            matches!(c.node_type.as_str(), "meta" | "session" | "utility")
+        }), "the meta node survived the migration");
+
+        // And its values are the live settings — the point of migrating at
+        // all rather than simply dropping the node.
         assert_eq!(state.world_unit, cce_ui::units::Unit::Cm);
         assert!((state.world_unit_mm() - 10.0).abs() < 1e-4);
         assert!((state.point_marker_size - 0.05).abs() < 1e-6);
         assert!((state.point_marker_color[0] - 1.0).abs() < 0.01);
         assert!((state.point_marker_color[1] - 0.5).abs() < 0.01);
         assert!((state.point_marker_color[2] - 0.0).abs() < 0.01);
+        assert!((state.grid_thickness - 0.04).abs() < 1e-6);
+        assert!(state.viewport().show_cube);
+        assert!(state.wireframe);
+        assert!((state.wire_width - 4.0).abs() < 1e-6);
+        assert_eq!(state.point_color[1], 1.0);
+        assert!(state.circular_network_pane);
+
+        // Idempotent: a second pass has nothing to find and changes nothing.
+        let before = serde_json::to_string(&state.fs_root).unwrap();
+        state.migrate_meta_settings_node();
+        assert_eq!(before, serde_json::to_string(&state.fs_root).unwrap());
     }
 
-    /// An old save carries Main/View/Guides/Render at the root with the user's
-    /// values in their params — migration must MOVE them (values intact), not
-    /// recreate them fresh.
+    /// A node the user put INSIDE the meta subnet is re-homed, not eaten.
+    ///
+    /// Adding a non-geometry node in there was allowed, so the migration
+    /// cannot treat everything under that container as the app's own —
+    /// dropping the node with it would silently delete the user's work, and
+    /// the only trace would be its absence.
+    #[test]
+    fn the_migration_rehomes_a_node_the_user_left_in_the_meta_subnet() {
+        let mut state = State::new(false);
+        let mine = crate::app::FsNode {
+            id: "mine".to_string(),
+            name: "my_notes".to_string(),
+            node_type: "node".to_string(),
+            children: vec![],
+            params: vec![],
+            geometry_visible: false,
+            position: (2.0, 3.0),
+            inputs: 1,
+            outputs: 1,
+        };
+        state.fs_root.children.push(crate::app::FsNode {
+            id: "legacy-meta".to_string(),
+            name: "meta".to_string(),
+            node_type: "meta".to_string(),
+            children: vec![
+                crate::app::FsNode {
+                    id: "legacy-guides".to_string(),
+                    name: "guides".to_string(),
+                    node_type: "utility".to_string(),
+                    children: vec![],
+                    params: vec![],
+                    geometry_visible: true,
+                    position: (0.0, 4.0),
+                    inputs: 1,
+                    outputs: 1,
+                },
+                mine,
+            ],
+            params: vec![],
+            geometry_visible: true,
+            position: (0.0, 0.0),
+            inputs: 0,
+            outputs: 0,
+        });
+
+        state.migrate_meta_settings_node();
+
+        assert!(!state.fs_root.children.iter().any(|c| c.node_type == "meta"));
+        let kept = state
+            .fs_root
+            .children
+            .iter()
+            .find(|c| c.id == "mine")
+            .expect("the user's node was eaten with the meta subnet");
+        assert_eq!(kept.name, "my_notes");
+        // Re-homed onto a free cell — the root may already have something
+        // standing where it was.
+        assert!(
+            state.fs_root.children.iter().filter(|c| c.position == kept.position).count() == 1,
+            "it landed on top of another node"
+        );
+    }
+
+    /// An OLDER save still carries Main/View/Guides/Render flat at the root,
+    /// with no meta node above them at all — the shape before the Session
+    /// node existed. The migration has to reach that generation too, or the
+    /// four nodes stay in the network forever doing nothing.
     #[test]
     fn test_old_saves_migrate_settings_nodes_into_session() {
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
-
-        // Simulate the old shape: pull the four back out to root, drop the
-        // Session node, and plant a probe param ensure doesn't own — the live-
-        // synced toggles are rewritten from app state by design, so only a
-        // foreign param can distinguish MOVED (probe survives) from RECREATED
-        // (probe gone).
-        let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").unwrap();
-        let mut session = state.fs_root.children.remove(s_idx);
-        for mut child in session.children.drain(..) {
-            if child.name == "guides" {
-                child.params.push(crate::app::ParamDef {
-                    name: "migration probe".to_string(),
-                    label: String::new(),
-                    param_type: "text".to_string(),
-                    default: "survived".to_string(),
-                    options: vec![],
-                    min: None,
-                    max: None,
-                    step: None,
-                    show_when: String::new(),
-                });
-            }
-            state.fs_root.children.push(child);
-        }
+        state.viewport_mut().show_grid = true;
+        state.fs_root.children.push(crate::app::FsNode {
+            id: "flat-guides".to_string(),
+            name: "guides".to_string(),
+            node_type: "utility".to_string(),
+            children: vec![],
+            params: vec![crate::app::ParamDef {
+                name: "Show Grid Guide".to_string(),
+                label: String::new(),
+                param_type: "toggle".to_string(),
+                default: "false".to_string(),
+                options: vec![],
+                min: None,
+                max: None,
+                step: None,
+                show_when: String::new(),
+            }],
+            geometry_visible: true,
+            position: (0.0, 4.0),
+            inputs: 1,
+            outputs: 1,
+        });
+
+        state.migrate_meta_settings_node();
 
-        state.ensure_menubar_subnets();
-        let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").expect("root meta recreated");
-        let guides = state.fs_root.children[s_idx].children.iter().find(|c| c.name == "guides").expect("Guides migrated in");
-        let v = guides.params.iter().find(|p| p.name == "migration probe").map(|p| p.default.as_str());
-        assert_eq!(v, Some("survived"), "migration recreated Guides instead of moving it");
+        assert!(
+            !state.fs_root.children.iter().any(|c| c.node_type == "utility"),
+            "a root-level settings node survived"
+        );
+        assert!(!state.viewport().show_grid, "its value did not reach the live state");
     }
 
     /// Ctrl+S saves in place, Ctrl+Shift+S is Save As — and the Shift must
@@ -1122,7 +1220,6 @@ mod tests {
 
         let mut a = State::new(false);
         a.width = 1600.0;
-        a.ensure_menubar_subnets();
         assert!(a.show_viewport && !a.show_spreadsheet, "test assumes the default pane set");
         a.execute_menu_action("Show Viewport Pane");
         a.execute_menu_action("Show Spreadsheet Pane");
@@ -1143,7 +1240,6 @@ mod tests {
 
         let mut b = State::new(false);
         b.width = 800.0;
-        b.ensure_menubar_subnets();
         b.load_from_file(&dir).expect("load");
         assert!(!b.show_viewport, "viewport hidden in the save must load hidden");
         assert!(b.show_spreadsheet, "spreadsheet shown in the save must load shown");
@@ -1167,7 +1263,6 @@ mod tests {
 
         // A detached pane window must ignore the same file's pane state.
         let mut d = State::new(true);
-        d.ensure_menubar_subnets();
         let vp_before = d.show_viewport;
         d.load_from_file(&dir).expect("load detached");
         assert_eq!(d.show_viewport, vp_before, "detached windows keep their own pane layout");
@@ -1186,7 +1281,6 @@ mod tests {
 
         let mut a = State::new(false);
         a.resize(1600.0, 900.0, 1.0);
-        a.ensure_menubar_subnets();
         a.execute_menu_action("Show Spreadsheet Pane");
         assert!(a.show_spreadsheet);
         a.floating_network_layout.2 = 520.0;
@@ -1204,7 +1298,6 @@ mod tests {
 
         let mut b = State::new(false);
         b.resize(1600.0, 900.0, 1.0);
-        b.ensure_menubar_subnets();
         b.load_from_file(&dir).expect("load");
         assert!((b.floating_network_layout.2 - 520.0).abs() < 0.5, "network width: {}", b.floating_network_layout.2);
         assert!((b.floating_param_width - 360.0).abs() < 0.5, "param width: {}", b.floating_param_width);
@@ -1216,14 +1309,12 @@ mod tests {
         // Half the window: the same fractions land at half the pixels.
         let mut c = State::new(false);
         c.resize(800.0, 450.0, 1.0);
-        c.ensure_menubar_subnets();
         c.load_from_file(&dir).expect("load half-size");
         assert!((c.floating_network_layout.2 - 260.0).abs() < 0.5, "scaled network width: {}", c.floating_network_layout.2);
         assert!((c.floating_param_width - 180.0).abs() < 0.5, "scaled param width: {}", c.floating_param_width);
 
         // A detached pane window keeps its own plates.
         let mut d = State::new(true);
-        d.ensure_menubar_subnets();
         let before = d.floating_param_width;
         d.load_from_file(&dir).expect("load detached");
         assert_eq!(d.floating_param_width, before);
@@ -1240,7 +1331,6 @@ mod tests {
         let _ = fs::remove_dir_all(&dir);
 
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
         // The tree baseline is taken before the meta-node migrations run
         // (startup's project load re-baselines); this test is about the
         // layout half, so baseline here.
@@ -3502,7 +3592,6 @@ mod tests {
         assert_eq!(vw_e, 764.0);
     }
 
-    #[test]
     /// The list's scrollbar is cce-mail's: sunk until a scroll raises it,
     /// draggable while raised, sunk again after the hold — and while sunk it
     /// takes no input, so a press on its lane reaches the row beneath.
@@ -3587,6 +3676,7 @@ mod tests {
         assert!(d.scroll_px > before, "a wheel notch glides the list: {} -> {}", before, d.scroll_px);
     }
 
+    #[test]
     fn test_keyboard_shortcut_system() {
         // Test parsing simple shortcut
         let ctrl_g = Shortcut::parse("Ctrl+g").unwrap();
@@ -3651,11 +3741,19 @@ mod tests {
         // geometry pass sat under every label, and the labels of buttons
         // beneath the params pane's "Open" dropdown bled through it.
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
-        let (s_idx, m_idx) = session_and_main(&state);
-        state.current_path.push(s_idx);
-        state.on_path_changed();
-        state.graph_mut().set_selected_node(Some(m_idx));
+        // Any node with a choice param will do; the Main utility node's
+        // "Open" dropdown was this test's subject until that node was
+        // retired. Scatter's Mode is a choice.
+        let scatter = state
+            .node_templates
+            .iter()
+            .find(|t| t.label == "Scatter")
+            .expect("a Scatter template")
+            .node
+            .clone();
+        state.fs_root.children.push(scatter);
+        let idx = state.fs_root.children.len() - 1;
+        state.graph_mut().set_selected_node(Some(idx));
         state.sync_parameters_pane();
 
         {
@@ -3667,7 +3765,7 @@ mod tests {
                 .iter_mut()
                 .flatten()
                 .next()
-                .expect("Main's params include a dropdown (Open)");
+                .expect("Scatter's params include a dropdown (Mode)");
             dropdown.open = true;
             // The popover expands on a wall-clock animation, and every geometry
             // reader uses `anim_snap`, a snapshot refreshed only on tick/event —
@@ -3685,10 +3783,11 @@ mod tests {
                 matches!(&item.prim, cce_ui::scene::paint::Prim::Text { text, .. } if text == needle)
             })
         };
-        // "New" is a button label sitting under the open dropdown;
-        // "Other" only exists inside the popover's option list.
-        let label_idx = text_pos("New").expect("button label in display list");
-        let option_idx = text_pos("Other").expect("popover option text in display list");
+        // "Points" is a param label sitting under the open dropdown;
+        // "Surface" is not Mode's current value, so it exists only inside
+        // the popover's option list.
+        let label_idx = text_pos("Points").expect("param label in display list");
+        let option_idx = text_pos("Surface").expect("popover option text in display list");
         assert!(option_idx > label_idx, "popover text must draw after widget labels");
         let has_bg_between = list.items[label_idx..option_idx]
             .iter()
@@ -4272,9 +4371,10 @@ mod tests {
 
     /// The Wireframe Color command is a palette row that PREVIEWS the colour
     /// — the row carries the live wire colour as its swatch — and, picked,
-    /// lands on the Settings half's Wireframe Color row, whose owner is the
-    /// Render node's "Wire Color" (the value's one home). The palette row
-    /// shows the value; the settings row edits it.
+    /// lands on the Settings half's Wireframe Color row, which edits the
+    /// live `wire_color` (its owner was the Render node's "Wire Color" until
+    /// that node was retired). The palette row shows the value; the settings
+    /// row edits it.
     #[test]
     fn test_wireframe_color_row_previews_and_lands_on_settings() {
         use crate::command::{by_id, Run};
@@ -4302,7 +4402,7 @@ mod tests {
 
         // Picked from the list: the dialog closes, the command reopens it on
         // Settings, and the Wireframe Color row is there as a colour control
-        // owned by the Render node.
+        // over the live wire colour.
         state.take_dialog_pick("wireframe_color".to_string());
         assert!(state.dialog_visible());
         assert_eq!(state.dialog_tab(), Tab::Settings);
@@ -4310,19 +4410,31 @@ mod tests {
         assert!(shown.iter().any(|(k, _, t)| k == "Wireframe Color" && t == "rgba"), "{shown:?}");
         assert!(shown.iter().any(|(k, _, t)| k == "Wireframe Single Color" && t == "toggle"), "{shown:?}");
         let s = crate::dialog::SETTINGS.iter().find(|s| s.label == "Wireframe Color").unwrap();
-        assert_eq!(s.owner, Some(crate::dialog::Owner::Subnet("render", "Wire Color")));
+        assert_eq!(s.owner, Some(crate::dialog::Owner::Field("wire_color")));
 
         // Editing both rows reaches the live state: the colour AND the switch
         // that makes the wire pass use it (off, the wires carry the
         // geometry's colours and the colour row is their alpha alone).
-        assert!(!state.wire_single_color, "single-colour mode is off by default");
+        state.wire_single_color = false;
+        state.wire_color = [1.0, 1.0, 1.0, 1.0];
+        state.refresh_dialog_settings();
         let mut rows = state.dialog_settings_shown.clone();
         rows.iter_mut().find(|(k, _, _)| k == "Wireframe Color").unwrap().1 = "#000000ff".to_string();
-        rows.iter_mut().find(|(k, _, _)| k == "Wireframe Single Color").unwrap().1 = "true".to_string();
         state.slots.dialog_params_mut().set_display_params(&rows);
         state.sync_dialog_settings_to_project();
         assert_eq!(state.wire_color, [0.0, 0.0, 0.0, 1.0], "the colour row writes the live wire colour");
-        assert!(state.wire_single_color, "the switch row turns single-colour mode on");
+        // Setting a colour means wanting to see it, so single-colour mode
+        // comes on with it — off, the wires carry the geometry's own colours
+        // and this row is only their alpha.
+        assert!(state.wire_single_color, "a colour edit turns single-colour mode on");
+
+        // And the switch is a row of its own, dispatching its command.
+        state.refresh_dialog_settings();
+        let mut rows = state.dialog_settings_shown.clone();
+        rows.iter_mut().find(|(k, _, _)| k == "Wireframe Single Color").unwrap().1 = "false".to_string();
+        state.slots.dialog_params_mut().set_display_params(&rows);
+        state.sync_dialog_settings_to_project();
+        assert!(!state.wire_single_color, "the switch row did not reach the flag");
     }
 
     /// Frame All frames the displayed geometry from wherever the view is:
@@ -4364,108 +4476,159 @@ mod tests {
         assert!(state.viewport().zoom < 1.0, "a 0.25 sphere frames closer than the stock view: zoom {}", state.viewport().zoom);
     }
 
-    /// The viewport settings live in the scene file: the Render node's
+    /// What the scene file carries, and what it no longer does.
+    ///
+    /// It used to carry the viewport DISPLAY settings — the Render node's
     /// wireframe state and colour, the Guides node's grid and origin, Main's
-    /// background, and the Default Camera view (square aspect, pivot marker,
-    /// orbit/zoom/pivot). A fresh State whose live values differ takes the
-    /// file's on load. Before this, `ensure_menubar_subnets` re-seeded the
-    /// nodes from live state on load and the file's values were lost.
+    /// background — because the nodes holding them rode `fs_root` into the
+    /// file. That made a preference part of the project: opening someone
+    /// else's scene reset how you looked at geometry. Those settings persist
+    /// to `state.kdl` now, and the scene file keeps what is genuinely the
+    /// project's: the Default Camera VIEW (square aspect, pivot marker,
+    /// orbit/zoom/pivot), which is where you were standing in this scene.
     #[test]
     fn viewport_settings_round_trip_through_the_scene_file() {
         let dir = std::env::temp_dir().join(format!("cce-designer-vp-settings-{}", std::process::id()));
         let _ = std::fs::remove_dir_all(&dir);
 
         let mut a = State::new(false);
-        a.ensure_menubar_subnets();
         // The Default Camera is active: a camera NODE's own Square Aspect and
         // pivot params would override the saved view's, by design.
         a.active_camera = "Default Camera".to_string();
-        a.wireframe = true;
-        a.wire_single_color = true;
-        a.wire_color = [0.0, 0.0, 0.0, 1.0];
-        a.wire_width = 3.0;
-        a.viewport_mut().show_grid = false;
-        a.viewport_mut().show_origin = true;
-        a.viewport_mut().bg_color = [0.1, 0.2, 0.3];
         a.square_viewport = true;
         a.viewport_mut().show_camera_pivot = true;
         a.viewport_mut().rotation_y = 0.7;
         a.viewport_mut().zoom = 0.4;
         a.viewport_mut().pivot = Vec3::new(3.0, 0.5, -2.0);
+        // A display setting, deliberately NOT expected to travel.
+        a.wireframe = true;
         a.save_to_file(&dir).expect("save");
 
         let mut b = State::new(false);
-        b.ensure_menubar_subnets();
-        assert!(!b.wireframe && !b.wire_single_color, "a fresh state starts without wires");
+        b.wireframe = false;
         b.load_from_file(&dir).expect("load");
-        assert!(b.wireframe, "Show Wireframe loads from the file");
-        assert!(b.wire_single_color, "Wire Single Color loads from the file");
-        assert_eq!(b.wire_color, [0.0, 0.0, 0.0, 1.0]);
-        assert!((b.wire_width - 3.0).abs() < 1e-4);
-        assert!(!b.viewport().show_grid, "Show Grid loads from the file");
-        assert!(b.viewport().show_origin, "Show Origin loads from the file");
-        let bg = b.viewport().bg_color;
-        assert!((bg[0] - 0.1).abs() < 0.01 && (bg[1] - 0.2).abs() < 0.01 && (bg[2] - 0.3).abs() < 0.01, "background {bg:?}");
         assert!(b.square_viewport, "Square Aspect loads from the file");
         assert!(b.viewport().show_camera_pivot, "the pivot marker loads from the file");
         assert!((b.viewport().rotation_y - 0.7).abs() < 1e-4);
         assert!((b.viewport().zoom - 0.4).abs() < 1e-4);
         assert_eq!(b.viewport().pivot, Vec3::new(3.0, 0.5, -2.0));
-        // And the nodes agree with the live state after the load.
-        let render = b.fs_root.children.iter().find(|c| c.node_type == "meta").unwrap()
-            .children.iter().find(|c| c.name == "render").unwrap();
-        assert_eq!(render.params.iter().find(|p| p.name == "Show Wireframe").unwrap().default, "true");
+        assert!(!b.wireframe, "a display preference rode the project file");
         let _ = std::fs::remove_dir_all(&dir);
     }
 
-    /// Changing the wire colour turns single-colour mode on — on the node
-    /// and live — so the colour shows; a load does not (a file that says
-    /// off stays off, whatever colour it carries), and turning the switch
-    /// off afterwards sticks until the colour changes again.
+    /// …and the display settings round-trip through `state.kdl` instead,
+    /// every one of them, including the colours that pass through hex on the
+    /// way. The colour table was a hand-written pair of `if let`s per field
+    /// and covered two of the five, so a new colour setting serialized as a
+    /// JSON array and came back as the default.
+    #[test]
+    fn display_settings_round_trip_through_state_kdl() {
+        use crate::app::DesignSettings;
+        let mut a = State::new(false);
+        a.viewport_mut().show_grid = false;
+        a.viewport_mut().show_cube = true;
+        a.viewport_mut().bg_color = [0.1, 0.2, 0.3];
+        a.viewport_mut().grid_color = [0.4, 0.5, 0.6];
+        a.viewport_mut().rt_mode = true;
+        a.grid_thickness = 0.04;
+        a.origin_size = 2.5;
+        a.show_point_markers = true;
+        a.show_point_numbers = true;
+        a.point_marker_size = 0.05;
+        a.point_marker_color = [1.0, 0.5, 0.0];
+        a.world_unit = cce_ui::units::Unit::Cm;
+        a.wireframe = true;
+        a.wire_single_color = true;
+        a.wire_color = [0.2, 0.4, 0.6, 0.5];
+        a.wire_width = 3.0;
+        a.geo_opacity = 0.75;
+        a.render_points = true;
+        a.point_size = 0.05;
+        a.point_color = [0.0, 1.0, 0.0];
+        a.save_settings();
+
+        let kdl = std::fs::read_to_string(DesignSettings::file_path()).expect("state.kdl was written");
+        let back = DesignSettings::from_kdl_str(&kdl);
+        let close = |x: f32, y: f32| (x - y).abs() < 0.01;
+
+        assert!(!back.viewport.show_grid_enabled);
+        assert!(back.viewport.show_cube_enabled);
+        assert!(back.viewport.rt_mode);
+        assert!(close(back.viewport.grid_thickness, 0.04));
+        assert!(close(back.viewport.origin_size, 2.5));
+        assert!(back.viewport.show_point_markers && back.viewport.show_point_numbers);
+        assert!(!back.viewport.show_point_normals);
+        assert!(close(back.viewport.point_marker_size, 0.05));
+        assert_eq!(back.viewport.world_unit, "cm");
+        for (got, want) in [
+            (back.viewport.bg_color, [0.1, 0.2, 0.3]),
+            (back.viewport.grid_color, [0.4, 0.5, 0.6]),
+            (back.viewport.point_marker_color, [1.0, 0.5, 0.0]),
+            (back.render.point_color, [0.0, 1.0, 0.0]),
+        ] {
+            for k in 0..3 {
+                assert!(close(got[k], want[k]), "colour {got:?} came back as {want:?}");
+            }
+        }
+        assert!(back.render.wireframe && back.render.wire_single_color);
+        // The wire colour is the four-component one: its ALPHA is the wire's
+        // own opacity, and dropping it would silently make every wireframe
+        // fully opaque.
+        for k in 0..4 {
+            assert!(close(back.render.wire_color[k], [0.2, 0.4, 0.6, 0.5][k]), "{:?}", back.render.wire_color);
+        }
+        assert!(close(back.render.wire_width, 3.0));
+        assert!(close(back.render.geo_opacity, 0.75));
+        assert!(back.render.render_points);
+        assert!(close(back.render.point_size, 0.05));
+    }
+
+    /// Changing the wire colour turns single-colour mode on, so the colour
+    /// shows; turning the switch off afterwards sticks, and a LOAD never
+    /// flips it — a project that says off stays off whatever colour it
+    /// carries.
     #[test]
     fn changing_the_wire_colour_turns_single_colour_mode_on() {
         let mut state = State::new(false);
-        state.ensure_menubar_subnets();
-        state.apply_settings_from_menubar_subnets();
-        assert!(!state.wire_single_color);
-        let render_param = |state: &State, name: &str| -> String {
-            state.fs_root.children.iter().find(|c| c.node_type == "meta").unwrap()
-                .children.iter().find(|c| c.name == "render").unwrap()
-                .params.iter().find(|p| p.name == name).unwrap().default.clone()
-        };
-        // An edit through the node, as the params pane and the dialog make it.
-        {
-            let meta = state.fs_root.children.iter_mut().find(|c| c.node_type == "meta").unwrap();
-            let render = meta.children.iter_mut().find(|c| c.name == "render").unwrap();
-            render.params.iter_mut().find(|p| p.name == "Wire Color").unwrap().default = "#000000ff".to_string();
-        }
-        state.apply_settings_from_menubar_subnets();
+        state.wire_single_color = false;
+        state.wire_color = [1.0, 1.0, 1.0, 1.0];
+
+        // An edit through the dialog's Settings half, which is the only way
+        // in now that the Render node is gone.
+        state.open_dialog();
+        state.slots.dialog.tab = crate::dialog::Tab::Settings;
+        state.refresh_dialog_settings();
+        let mut rows = state.dialog_settings_shown.clone();
+        rows.iter_mut().find(|(k, _, _)| k == "Wireframe Color").unwrap().1 = "#000000ff".to_string();
+        state.slots.dialog_params_mut().set_display_params(&rows);
+        state.sync_dialog_settings_to_project();
+        assert_eq!(state.wire_color, [0.0, 0.0, 0.0, 1.0]);
         assert!(state.wire_single_color, "a colour change switches single-colour mode on");
-        assert_eq!(render_param(&state, "Wire Single Color"), "true", "and the node's switch shows it");
-        // Off again by hand stays off while the colour is unchanged.
-        state.write_render_toggle("Wire Single Color", false);
-        state.apply_settings_from_menubar_subnets();
-        assert!(!state.wire_single_color);
-
-        // A load: the file's colour differs from the fresh state's, its
-        // switch is off, and it stays off.
+
+        // Off again by hand stays off while the colour is unchanged: the
+        // auto-enable fires on a CHANGE, not on every settings pass.
+        state.wire_single_color = false;
+        state.refresh_dialog_settings();
+        state.sync_dialog_settings_to_project();
+        assert!(!state.wire_single_color, "an unrelated poll flipped it back on");
+
+        // A load carries the project's geometry and leaves the wire
+        // settings — preferences now — exactly where they are.
         let dir = std::env::temp_dir().join(format!("cce-designer-wire-colour-{}", std::process::id()));
         let _ = std::fs::remove_dir_all(&dir);
         state.save_to_file(&dir).expect("save");
-        let mut fresh = State::new(false);
-        fresh.ensure_menubar_subnets();
-        fresh.load_from_file(&dir).expect("load");
-        assert_eq!(fresh.wire_color, [0.0, 0.0, 0.0, 1.0]);
-        assert!(!fresh.wire_single_color, "a load never flips the switch");
+        state.load_from_file(&dir).expect("load");
+        assert_eq!(state.wire_color, [0.0, 0.0, 0.0, 1.0]);
+        assert!(!state.wire_single_color, "a load never flips the switch");
         let _ = std::fs::remove_dir_all(&dir);
     }
 
-    /// The wireframe toggle is a palette row that flips the live flag AND
-    /// the Render node's "Show Wireframe" switch. The node matters: it is
-    /// what `apply_settings_from_menubar_subnets` reads back on every
-    /// parameter edit, so a flag flipped alone would revert on the next
-    /// unrelated edit. No settings write is involved, so running it here
-    /// touches nothing outside the test.
+    /// The wireframe toggle is a palette row that flips the live flag, and
+    /// the flag is the whole of it. The value used to live on the Render
+    /// utility node, which `apply_settings_from_menubar_subnets` read back
+    /// over live state after every parameter edit anywhere — so a flag
+    /// flipped alone reverted on the next unrelated change, and the command
+    /// had to write the node too. There is no node and no read-back now.
     #[test]
     fn test_toggle_wireframe_flips_the_flag_and_the_render_node() {
         use crate::command::{by_id, Run};
@@ -4473,28 +4636,19 @@ mod tests {
         assert_eq!(cmd.label, "Show Wireframe");
         assert_eq!(cmd.run, Run::Key(crate::shortcut::Action::ToggleWireframe));
 
-        let render_toggle = |state: &State| -> String {
-            state
-                .fs_root
-                .children
-                .iter()
-                .find(|c| c.node_type == "meta")
-                .and_then(|s| s.children.iter().find(|c| c.name == "render"))
-                .and_then(|n| n.params.iter().find(|p| p.name == "Show Wireframe"))
-                .map(|p| p.default.clone())
-                .expect("a Render node with a Show Wireframe toggle")
-        };
         let mut state = State::new(false);
-        assert!(!state.wireframe, "wireframe is off unless the project turned it on");
+        state.wireframe = false;
         assert!(state.run_command("toggle_wireframe"));
         assert!(state.wireframe);
-        assert_eq!(render_toggle(&state), "true");
-        // The read-back path agrees with the flag instead of reverting it.
-        state.apply_settings_from_menubar_subnets();
-        assert!(state.wireframe);
+        assert_eq!(state.command_toggle_state("toggle_wireframe"), Some(true));
+        // An unrelated settings poll no longer reverts it.
+        state.open_dialog();
+        state.slots.dialog.tab = crate::dialog::Tab::Settings;
+        state.refresh_dialog_settings();
+        state.sync_dialog_settings_to_project();
+        assert!(state.wireframe, "a settings pass read the flag back over itself");
         assert!(state.run_command("toggle_wireframe"));
         assert!(!state.wireframe);
-        assert_eq!(render_toggle(&state), "false");
     }
 
     /// The registry's own invariants. Ids are what `input.kdl` binds and
@@ -5243,84 +5397,49 @@ mod tests {
         assert_eq!(cmd.run, Run::Key(crate::shortcut::Action::ToggleNetworkPlate));
         assert!(cmd.default_chord.is_some(), "the plate toggle has no chord");
 
-        // The View settings node mirrors the live flag, so the row in the
-        // params pane shows what is actually on screen.
+        // The live flag and the dialog's switch are one reading: the row
+        // in the Settings half is `Owner::Command`, so it goes through
+        // `command_toggle_state` rather than mirroring the flag onto a node
+        // that could fall out of step with it.
         let mut state = State::new(false);
-        let session = state
-            .fs_root
-            .children
-            .iter()
-            .position(|c| c.node_type == "meta")
-            .expect("root meta node");
-        let view = state.fs_root.children[session]
-            .children
-            .iter()
-            .position(|c| c.name == "view")
-            .expect("View node");
-        let plate_row = |state: &State| {
-            state.fs_root.children[session].children[view]
-                .params
-                .iter()
-                .find(|p| p.name == "Show Network Plate")
-                .map(|p| (p.default.clone(), p.label.clone()))
-        };
-        assert_eq!(
-            plate_row(&state),
-            Some(("true".to_string(), "Plate".to_string())),
-            "the View node has no Plate row, or it does not read as on"
-        );
+        assert_eq!(state.command_toggle_state("toggle_network_plate"), Some(true));
+        assert!(state.run_command("toggle_network_plate"));
+        assert!(!state.network_plate);
+        assert_eq!(state.command_toggle_state("toggle_network_plate"), Some(false));
 
-        state.network_plate = false;
-        state.current_path = vec![session];
-        state.refresh_main_node_live_toggles(view);
-        assert_eq!(
-            plate_row(&state).map(|(v, _)| v),
-            Some("false".to_string()),
-            "the View node's row did not follow the live flag"
-        );
+        let row = crate::dialog::SETTINGS
+            .iter()
+            .find(|s| s.label == "Show Network Plate")
+            .expect("a Show Network Plate row");
+        assert_eq!(row.owner, Some(crate::dialog::Owner::Command("toggle_network_plate")));
     }
 
     /// The viewport guide toggles survive the next parameter edit.
     ///
-    /// `apply_settings_from_menubar_subnets` copies the Guides node onto the
-    /// live flags on EVERY parameter change, so a command that flipped only
-    /// the flag was undone by the next edit anywhere — Show Cube hid the
-    /// cube, and editing any node's parameter brought it back. The command
-    /// has to write the Guides node, the value's owner, as Show Wireframe
-    /// writes the Render node.
+    /// `apply_settings_from_menubar_subnets` used to copy the Guides utility
+    /// node onto the live flags on EVERY parameter change, so a command that
+    /// flipped only the flag was undone by the next edit anywhere — Show
+    /// Cube hid the cube, and editing any node's parameter brought it back.
+    /// The fix was to write the node as well; the node is gone now and the
+    /// flag is simply the value, which is the same guarantee with nothing
+    /// left to fall out of step. Still asserted, because the failure it
+    /// catches (an edit reverting a display toggle) is invisible in a test
+    /// that only flips the toggle.
     #[test]
     fn guide_toggles_survive_the_settings_apply_pass() {
         let mut state = State::new(false);
-        let guides_value = |state: &State, name: &str| -> String {
-            state
-                .session_node()
-                .and_then(|s| s.children.iter().find(|c| c.name == "guides"))
-                .and_then(|g| g.params.iter().find(|p| p.name == name))
-                .map(|p| p.default.clone())
-                .expect("the Guides param")
-        };
-        for (command, param) in [
-            ("toggle_cube", "Show Reference Cube"),
-            ("toggle_grid", "Show Grid Guide"),
-            ("toggle_origin", "Show Origin Axes"),
-        ] {
+        for command in ["toggle_cube", "toggle_grid", "toggle_origin", "toggle_point_markers"] {
             let flag = |state: &State| match command {
                 "toggle_cube" => state.viewport().show_cube,
                 "toggle_grid" => state.viewport().show_grid,
-                _ => state.viewport().show_origin,
+                "toggle_origin" => state.viewport().show_origin,
+                _ => state.show_point_markers,
             };
             let before = flag(&state);
-            assert_eq!(guides_value(&state, param), before.to_string(), "{param} starts in step with the flag");
-
             assert!(state.run_command(command));
             assert_eq!(flag(&state), !before, "{command} flipped the flag");
-            assert_eq!(guides_value(&state, param), (!before).to_string(), "{command} wrote the Guides node");
-
-            // What every parameter edit runs.
-            state.apply_settings_from_menubar_subnets();
-            assert_eq!(flag(&state), !before, "{command} was undone by the apply pass");
 
-            // And a real edit through the action path, on an unrelated node.
+            // A real edit through the action path, on an unrelated node.
             let mut redraw = false;
             let sphere = state.current_dir().children.iter().position(|c| c.name.starts_with("sphere")).expect("a sphere");
             state
@@ -5329,12 +5448,10 @@ mod tests {
             assert_eq!(flag(&state), !before, "{command} was undone by a parameter edit");
         }
 
-        // Circular Pane lives on the Main node and had the same hole.
+        // Circular Pane had the same hole.
         let before = state.circular_network_pane;
         assert!(state.run_command("toggle_circular_pane"));
         assert_eq!(state.circular_network_pane, !before);
-        state.apply_settings_from_menubar_subnets();
-        assert_eq!(state.circular_network_pane, !before, "Circular Pane was undone by the apply pass");
     }
 
     /// A replacement renderer invalidates the page pane's image id, and the
@@ -8350,38 +8467,82 @@ mod tests {
         key_press(Key::Character(c.to_string()))
     }
 
-    /// Every Settings row still names something that exists.
+    /// Every Settings row still names something that exists, and every
+    /// `Owner::Field` key is one the readers and the writer both handle.
     ///
-    /// The failure this catches is silent and the reason the table is a table:
-    /// `dialog_settings_params` SKIPS a row whose owning param it cannot find,
-    /// so renaming a subnet param quietly shortens the Settings half and
-    /// nothing says why. Same argument as
-    /// `test_every_menu_command_names_a_label_that_is_dispatched`.
+    /// The failure this catches is silent, and it is the reason the table is
+    /// a table: a `Field` key that no arm names reads as a zero and writes
+    /// nowhere, so the row draws, accepts an edit and does nothing. (Before
+    /// the meta node was retired the same failure was a renamed subnet param
+    /// SKIPPING its row, which quietly shortened the Settings half.) Same
+    /// argument as `test_every_menu_command_names_a_label_that_is_dispatched`.
     #[test]
     fn dialog_settings_rows_name_owners_that_exist() {
-        use crate::dialog::Owner;
-        let state = State::new(false);
-        let session = state.session_node().expect("the root meta node");
+        use crate::dialog::{Ctl, Owner};
+        let mut state = State::new(false);
         for s in crate::dialog::SETTINGS {
             match s.owner {
-                None => {}
-                Some(Owner::Subnet(subnet, name)) => {
-                    let node = session
-                        .children
-                        .iter()
-                        .find(|c| c.name == subnet)
-                        .unwrap_or_else(|| panic!("no '{subnet}' subnet for row '{}'", s.label));
+                None => assert!(s.ctl.is_none(), "section '{}' declares a control", s.label),
+                Some(Owner::Field(key)) => {
+                    let ctl = s.ctl.unwrap_or_else(|| panic!("row '{}' declares no control", s.label));
+                    // The round trip IS the check: read the row, write the
+                    // value straight back, and read again. A key no arm
+                    // names reads a default and writes nothing, so the two
+                    // reads differ the moment the default is not the live
+                    // value — which is why each row is nudged first.
+                    match ctl {
+                        Ctl::Toggle => {
+                            let before = state.settings_row_value(s.label);
+                            let flipped = if before == "true" { "false" } else { "true" };
+                            state.settings_write_row(s.label, flipped);
+                            assert_eq!(state.settings_row_value(s.label), flipped,
+                                "row '{}' (key '{key}') did not take a write", s.label);
+                        }
+                        Ctl::Color => {
+                            state.settings_write_row(s.label, "#123456");
+                            assert_eq!(state.settings_row_value(s.label), "#123456",
+                                "row '{}' (key '{key}') did not take a write", s.label);
+                        }
+                        Ctl::Rgba => {
+                            state.settings_write_row(s.label, "#12345678");
+                            assert_eq!(state.settings_row_value(s.label), "#12345678",
+                                "row '{}' (key '{key}') did not take a write", s.label);
+                        }
+                        Ctl::Spin { min, max, .. } => {
+                            let v = ((min + max) / 2.0).round() as i32;
+                            state.settings_write_row(s.label, &v.to_string());
+                            assert_eq!(state.settings_row_value(s.label), v.to_string(),
+                                "row '{}' (key '{key}') did not take a write", s.label);
+                        }
+                        Ctl::Slider { min, max, dec } => {
+                            let v = format!("{:.*}", dec, (min + max) / 2.0);
+                            state.settings_write_row(s.label, &v);
+                            assert_eq!(state.settings_row_value(s.label), v,
+                                "row '{}' (key '{key}') did not take a write", s.label);
+                        }
+                        Ctl::Choice(options) => {
+                            let last = options.last().expect("a choice with no options");
+                            state.settings_write_row(s.label, last);
+                            assert_eq!(state.settings_row_value(s.label), *last,
+                                "row '{}' (key '{key}') did not take a write", s.label);
+                        }
+                    }
+                }
+                Some(Owner::Command(id)) => {
                     assert!(
-                        node.params.iter().any(|p| p.name == name),
-                        "'{subnet}' has no param '{name}' — row '{}' would vanish",
+                        crate::command::by_id(id).is_some(),
+                        "row '{}' names no command '{id}'",
+                        s.label
+                    );
+                    // A Command row paints as a switch, so the command has
+                    // to answer the toggle table or the row shows as off
+                    // whatever the setting is.
+                    assert!(
+                        state.command_toggle_state(id).is_some(),
+                        "row '{}' names command '{id}', which has no toggle state",
                         s.label
                     );
                 }
-                Some(Owner::Command(id)) => assert!(
-                    crate::command::by_id(id).is_some(),
-                    "row '{}' names no command '{id}'",
-                    s.label
-                ),
                 // The active camera's params exist only once a camera node
                 // does; the Default Camera branch is exercised below.
                 Some(Owner::ActiveCamera(_)) => {}
@@ -8789,10 +8950,14 @@ mod tests {
         assert!(!state.slots.dialog_params.visible());
     }
 
-    /// A Settings row writes to whatever OWNS its value, not to the live field
-    /// — which is the only write that survives, since
-    /// `apply_settings_from_menubar_subnets` copies the subnets over the live
-    /// state on every param change.
+    /// A Settings row writes to whatever OWNS its value.
+    ///
+    /// That used to mean a param on a utility subnet, never the live field:
+    /// `apply_settings_from_menubar_subnets` copied those subnets back over
+    /// live state on every param change, so a direct write survived until
+    /// the next edit and no longer. The live field IS the value now, and a
+    /// `Command` row goes through the command so the menus and the persist
+    /// come with it.
     #[test]
     fn dialog_settings_write_reaches_the_owning_subnet() {
         use crate::dialog::Tab;
@@ -8800,25 +8965,112 @@ mod tests {
         state.run_command("toggle_dialog");
         state.set_dialog_tab(Tab::Settings);
 
+        // A Command row: Show Grid dispatches `toggle_grid`.
         let was = state.viewport().show_grid;
-        // What a click on the toggle leaves behind: the control reports the
-        // flipped value, and the poll picks it up.
         let mut rows = state.dialog_settings_shown.clone();
         let row = rows.iter_mut().find(|(k, _, _)| k == "Show Grid").expect("the Show Grid row");
         row.1 = if was { "false" } else { "true" }.to_string();
         state.slots.dialog_params_mut().set_display_params(&rows);
         state.sync_dialog_settings_to_project();
-
         assert_eq!(state.viewport().show_grid, !was, "the live state followed");
-        let guides = state
-            .session_node()
-            .expect("meta")
-            .children
-            .iter()
-            .find(|c| c.name == "guides")
-            .expect("Guides");
-        let p = guides.params.iter().find(|p| p.name == "Show Grid Guide").expect("the param");
-        assert_eq!(p.default == "true", !was, "and so did its owner");
+        assert_eq!(state.command_toggle_state("toggle_grid"), Some(!was), "and the switch shows it");
+
+        // A Field row: Grid Thickness is a spinbox in thousandths.
+        let mut rows = state.dialog_settings_shown.clone();
+        rows.iter_mut().find(|(k, _, _)| k == "Grid Thickness").expect("the row").1 = "40".to_string();
+        state.slots.dialog_params_mut().set_display_params(&rows);
+        state.sync_dialog_settings_to_project();
+        assert!((state.grid_thickness - 0.04).abs() < 1e-6, "{}", state.grid_thickness);
+
+        // And it survives an unrelated parameter edit, which is the whole
+        // reason the subnets had to be the owner before.
+        let mut redraw = false;
+        let sphere = state.current_dir().children.iter().position(|c| c.name.starts_with("sphere")).expect("a sphere");
+        state
+            .apply_action(crate::app::McpAction::SetParam { slot: sphere, name: "Radius".into(), value: "0.8".into() }, &mut redraw)
+            .expect("set a sphere param");
+        assert_eq!(state.viewport().show_grid, !was, "a param edit reverted the toggle");
+        assert!((state.grid_thickness - 0.04).abs() < 1e-6, "a param edit reverted the thickness");
+    }
+
+    /// The recent projects are rows of the Commands list.
+    ///
+    /// The list was the Main utility node's "Open" dropdown and went with
+    /// that node, which left `recent_files` written on every save and read
+    /// by nothing — a feature with no way in. It is a list of documents, so
+    /// it sits under the open document's own path row.
+    #[test]
+    fn the_palette_offers_the_recent_projects() {
+        use crate::dialog::RECENT_ROW_PREFIX;
+        let mut state = State::new(false);
+        let a = std::path::PathBuf::from("/tmp/cce-recent-alpha");
+        let b = std::path::PathBuf::from("/tmp/cce-recent-beta");
+        state.recent_files = vec![a.clone(), b.clone()];
+
+        state.open_dialog();
+        let rows: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        let id_a = format!("{RECENT_ROW_PREFIX}{}", a.display());
+        let id_b = format!("{RECENT_ROW_PREFIX}{}", b.display());
+        let ia = rows.iter().position(|r| *r == id_a).expect("no row for the newest recent project");
+        let ib = rows.iter().position(|r| *r == id_b).expect("no row for the older recent project");
+        assert!(ia < ib, "the recent list is not in most-recent-first order");
+        // The row shows the path, truncated from the LEFT — the tail is what
+        // identifies a project — with the file name in the chord column.
+        let row = &state.slots.dialog.rows[ia];
+        assert_eq!(row.label, a.display().to_string());
+        assert_eq!(row.chord, "cce-recent-alpha");
+        assert!(row.truncate_head);
+        // And it ranks against the path text like any other row.
+        state.slots.dialog.query = "beta".to_string();
+        state.refresh_dialog_rows();
+        let rows: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        assert!(rows.contains(&id_b) && !rows.contains(&id_a), "{rows:?}");
+        state.close_dialog();
+
+        // The project already open is not offered a second time.
+        state.loaded_project_path = Some(a.clone());
+        state.open_dialog();
+        let rows: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        assert!(!rows.contains(&id_a), "the open project is listed as a recent one");
+        assert!(rows.contains(&id_b));
+    }
+
+    /// Every display setting the retired utility subnets held is reachable —
+    /// as a Settings row, a command, or both.
+    ///
+    /// This is the check the removal turns on. Those four nodes were the only
+    /// way to reach a good half of these values, so a setting left out of the
+    /// table when they went is not "hidden in the node tree", it is GONE, and
+    /// nothing else in the suite would notice.
+    #[test]
+    fn every_retired_subnet_setting_is_reachable() {
+        let labels: Vec<&str> = crate::dialog::SETTINGS.iter().map(|s| s.label).collect();
+        for label in [
+            // guides
+            "Show Grid", "Grid Color", "Grid Thickness", "Show Origin Axes",
+            "Origin Size", "Show Reference Cube", "Point Marker Size",
+            "Point Marker Color", "World Unit",
+            // render
+            "Show Wireframe", "Wireframe Single Color", "Wireframe Color",
+            "Wire Thickness", "Opacity", "Show Points", "Point Size", "Point Color",
+            // main
+            "Background Color", "Ray Traced Preview", "Circular Pane",
+            // camera
+            "Show Camera Pivot", "Camera Pivot Size", "Square Aspect",
+            // network
+            "Show Network Plate",
+        ] {
+            assert!(labels.contains(&label), "'{label}' has no Settings row and no other way in");
+        }
+        // The Main node's buttons are commands, and the active camera keeps
+        // the viewport menubar's own menu — neither is a row here.
+        for id in [
+            "new_project", "open_project", "save_document", "save_document_as",
+            "set_as_default", "exit", "undo", "redo",
+            "zoom_in", "zoom_out", "reset_zoom", "detach_circular_window",
+        ] {
+            assert!(crate::command::by_id(id).is_some(), "the Main node's '{id}' has no command");
+        }
     }
 
     /// Reopening starts clean: on Commands, with an empty query.
@@ -9560,44 +9812,34 @@ mod tests {
         assert_eq!(added.position, (3.0, 2.0), "placed at the grid cursor");
     }
 
-    /// Geometry templates are refused inside a utility dir, so the list does
-    /// not offer them there — the same filter the popup was fed.
+    /// The Add Node list offers every template, everywhere.
+    ///
+    /// It used to hide the geometry ones inside a "utility dir" — the root
+    /// meta node and its `main`/`view`/`guides`/`render` subnets, where
+    /// placing geometry was refused. Those nodes are gone with the settings
+    /// they held, so there is no such directory left to be in and no filter
+    /// to apply.
     #[test]
     fn dialog_add_node_hides_geometry_templates_in_a_utility_dir() {
         let mut state = State::new(false);
-        let offered_at_root = {
-            state.open_node_palette();
-            let n = state.slots.dialog.rows.len();
-            state.close_dialog();
-            n
-        };
+        state.open_node_palette();
+        let at_root = state.slots.dialog.rows.len();
+        assert_eq!(at_root, state.node_templates.len(), "the palette dropped templates");
+        assert!(state.slots.dialog.rows.iter().any(|r| r.label == "Grid"));
+        assert!(state.slots.dialog.rows.iter().any(|r| r.label == "Box"));
+        state.close_dialog();
 
-        // Into the root meta node, which `in_settings_dir` reports as utility.
-        let meta = state
+        // Inside a subnet, the same list.
+        let sphere = state
             .fs_root
             .children
             .iter()
-            .position(|c| c.node_type == "meta")
-            .expect("the root meta node");
-        state.current_path.push(meta);
-        assert!(state.in_settings_dir());
-
+            .position(|c| c.name.starts_with("sphere"))
+            .expect("a sphere at the root");
+        state.current_path.push(sphere);
+        state.on_path_changed();
         state.open_node_palette();
-        let offered_in_utility = state.slots.dialog.rows.len();
-        assert!(
-            offered_in_utility < offered_at_root,
-            "{offered_in_utility} offered in a utility dir vs {offered_at_root} at the root"
-        );
-        assert!(
-            !state.slots.dialog.rows.iter().any(|r| r.label == "Grid"),
-            "a geometry template would be refused at placement"
-        );
-        // Box/Sphere/Plane/Extrude are `"type": "node"` SUBNET templates, not
-        // native geometry types, so `is_geometry_node_type` does not claim
-        // them and the filter leaves them offered. Pre-existing, and exactly
-        // what the popup was fed — asserted so the next reader does not take
-        // it for a hole in this filter.
-        assert!(state.slots.dialog.rows.iter().any(|r| r.label == "Box"));
+        assert_eq!(state.slots.dialog.rows.len(), at_root);
     }
 
     /// Ctrl+P lands on Commands rather than toggling, which is the one thing
diff --git a/src/project.rs b/src/project.rs
index 693a3ae..261c060 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -4,7 +4,7 @@ use std::path::Path;
 use crate::app::{State, Project, FsNode, ProjectViewState, PlateGeometry, ParamDef};
 use crate::slots::CONTENT_IDX;
 
-fn color_to_hex(rgb: [f32; 3]) -> String {
+pub(crate) fn color_to_hex(rgb: [f32; 3]) -> String {
     format!("#{:02x}{:02x}{:02x}",
         (rgb[0] * 255.0).round().clamp(0.0, 255.0) as u8,
         (rgb[1] * 255.0).round().clamp(0.0, 255.0) as u8,
@@ -12,11 +12,11 @@ fn color_to_hex(rgb: [f32; 3]) -> String {
     )
 }
 
-fn hex_to_color(hex: &str) -> Option<[f32; 3]> {
+pub(crate) fn hex_to_color(hex: &str) -> Option<[f32; 3]> {
     cce_ui::color::parse_hex_rgb(hex)
 }
 
-fn color_to_hex8(rgba: [f32; 4]) -> String {
+pub(crate) fn color_to_hex8(rgba: [f32; 4]) -> String {
     format!("#{:02x}{:02x}{:02x}{:02x}",
         (rgba[0] * 255.0).round().clamp(0.0, 255.0) as u8,
         (rgba[1] * 255.0).round().clamp(0.0, 255.0) as u8,
@@ -26,10 +26,19 @@ fn color_to_hex8(rgba: [f32; 4]) -> String {
 }
 
 /// 6- or 8-digit hex → RGBA (alpha 1.0 when absent).
-fn hex_to_rgba(hex: &str) -> Option<[f32; 4]> {
+pub(crate) fn hex_to_rgba(hex: &str) -> Option<[f32; 4]> {
     cce_ui::color::parse_hex_rgba(hex)
 }
 
+/// "network" -> "Network", for rebuilding an old save's param names.
+fn capitalize(s: &str) -> String {
+    let mut c = s.chars();
+    match c.next() {
+        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
+        None => String::new(),
+    }
+}
+
 impl State {
 
 
@@ -90,15 +99,15 @@ impl State {
         self.recent_files.insert(0, abs_path);
         self.recent_files.truncate(10);
         Self::save_recent_files(&self.recent_files);
-        self.ensure_menubar_subnets();
+        self.migrate_meta_settings_node();
     }
 
 
 
-    /// The view-state block every save and snapshot shares. Pane visibility
-    /// is NOT here — it lives in the root meta node's View subnet params,
-    /// which ride `fs_root` into the file; this carries the rest of the pane
-    /// state (collapse + splitter proportions) beside the camera/pan fields.
+    /// The view-state block every save and snapshot shares — the pane state
+    /// (visibility, collapse, splitter proportions, docks, pins) beside the
+    /// camera/pan fields. Visibility rode the root meta node's View subnet
+    /// params into the file until that node was retired.
     pub(crate) fn project_view_state(&self) -> ProjectViewState {
         let collapsed_panes = crate::plate_corner::PLATE_SLOTS
             .iter()
@@ -149,6 +158,13 @@ impl State {
             pan: (self.pan_x, self.pan_y),
             current_path: self.current_path.clone(),
             selected_node: self.graph().selected_node(),
+            visible_panes: Some(
+                Self::PANE_FLAGS
+                    .iter()
+                    .filter(|(_, get, _)| get(self))
+                    .map(|(name, _, _)| name.to_string())
+                    .collect(),
+            ),
             collapsed_panes,
             splitters,
             dock_tabs,
@@ -172,8 +188,7 @@ impl State {
     /// active camera and the path are known. The orbit, zoom and pivot are
     /// the view and always restore; the square aspect, pivot marker and its
     /// size are a camera NODE's own params when one is active in the
-    /// current directory (`apply_settings_from_menubar_subnets` reads them
-    /// off it), so those restore only for a view with no node.
+    /// current directory, so those restore only for a view with no node.
     fn apply_default_view_from_project(&mut self, view: Option<crate::app::DefaultCameraView>) {
         let Some(v) = view else { return };
         let active = self.active_camera.clone();
@@ -222,7 +237,7 @@ impl State {
         // its single-pane layout into the file, and the main window's next
         // reload would apply it (the load side is gated the same way).
         if !self.is_detached_network && self.detached_pane.is_none() {
-            self.ensure_menubar_subnets();
+            self.migrate_meta_settings_node();
         }
         if path.file_name().map_or(false, |n| n == "default_project.json") {
             let proj = Project {
@@ -257,45 +272,35 @@ impl State {
         Ok(())
     }
 
-    /// The pane-visibility toggles as saved in a project tree's meta→View
-    /// subnet. Read them off the LOADED tree before `ensure_menubar_subnets`
-    /// runs — it refreshes those params from live state, clobbering what the
-    /// file said.
-    fn project_pane_visibility(root: &FsNode) -> Vec<(String, bool)> {
-        root.children
-            .iter()
-            .find(|c| c.node_type == "meta")
-            .and_then(|m| m.children.iter().find(|c| c.name == "view"))
-            .map(|v| {
-                v.params
-                    .iter()
-                    .filter(|p| p.param_type == "toggle" && p.name.starts_with("Show ") && p.name.ends_with(" Pane"))
-                    .filter_map(|p| p.default.parse::<bool>().ok().map(|b| (p.name.clone(), b)))
-                    .collect()
-            })
-            .unwrap_or_default()
-    }
+    /// The five pane-visibility flags by their saved name, with the menu
+    /// action that flips each — the one table the save and the load share,
+    /// so a pane cannot be written under a name the loader does not know.
+    const PANE_FLAGS: [(&'static str, fn(&State) -> bool, &'static str); 5] = [
+        ("network", |s| s.show_network, "Show Network Pane"),
+        ("viewport", |s| s.show_viewport, "Show Viewport Pane"),
+        ("parameters", |s| s.show_parameters, "Show Parameters Pane"),
+        ("spreadsheet", |s| s.show_spreadsheet, "Show Spreadsheet Pane"),
+        ("playbar", |s| s.show_playbar, "Show Playbar Pane"),
+    ];
 
     /// Apply a loaded project's pane state: visibility diffs fire the same
     /// menu actions the View toggles use (slots, checkmarks, focus fixup all
     /// included), then collapse and splitter proportions. Main window only —
     /// detached windows own their single-pane layout, and the sync channel
     /// must not re-shape them.
-    fn apply_pane_state_from_project(&mut self, visibility: &[(String, bool)], vs: &ProjectViewState) {
+    fn apply_pane_state_from_project(&mut self, vs: &ProjectViewState) {
         if self.is_detached_network || self.detached_pane.is_some() {
             return;
         }
-        for (name, desired) in visibility {
-            let cur = match name.as_str() {
-                "Show Network Pane" => Some(self.show_network),
-                "Show Viewport Pane" => Some(self.show_viewport),
-                "Show Parameters Pane" => Some(self.show_parameters),
-                "Show Spreadsheet Pane" => Some(self.show_spreadsheet),
-                "Show Playbar Pane" => Some(self.show_playbar),
-                _ => None,
-            };
-            if cur == Some(!*desired) {
-                self.execute_menu_action(name);
+        // Absent (an older save, or one written before pane state moved off
+        // the meta node) keeps the live layout — the same rule the collapse
+        // list and the splitters follow.
+        if let Some(open) = &vs.visible_panes {
+            for (name, get, action) in Self::PANE_FLAGS {
+                let desired = open.iter().any(|n| n == name);
+                if get(self) != desired {
+                    self.execute_menu_action(action);
+                }
             }
         }
         // Absent names expand: an older save (no collapse list) loads with
@@ -386,21 +391,13 @@ impl State {
             let mut proj: Project = serde_json::from_str(&content)?;
             proj.sanitize_node_names();
             crate::app::merge_template_defs(&mut proj.root, &self.node_templates);
-            let saved_pane_vis = Self::project_pane_visibility(&proj.root);
             self.fs_root = proj.root;
-            // The project's viewport settings — the Guides and Render
-            // nodes' values, Main's background — onto the live state FIRST:
-            // `ensure_menubar_subnets` re-seeds those params from live state
-            // (so a chord-flipped toggle shows on the node), which on a load
-            // stamped the preferences file's values over the file's and lost
-            // them before the apply below could read them (2026-09-21).
-            // A load is not a colour change: the loaded tree's value is the
-            // baseline, so the auto-enable of single-colour mode stays quiet.
+            // A load is not a colour change: an older save's wire colour is
+            // the baseline, so the auto-enable of single-colour mode stays
+            // quiet while the migration reads it.
             self.last_applied_wire_color = None;
-            self.apply_settings_from_menubar_subnets();
-            self.ensure_menubar_subnets();
-            self.apply_settings_from_menubar_subnets();
-            self.apply_pane_state_from_project(&saved_pane_vis, &proj.view_state);
+            self.migrate_meta_settings_node();
+            self.apply_pane_state_from_project(&proj.view_state);
             self.active_camera = proj.view_state.active_camera;
             self.pan_x = proj.view_state.pan.0;
             self.pan_y = proj.view_state.pan.1;
@@ -452,17 +449,11 @@ impl State {
         let mut proj: Project = serde_json::from_str(&content)?;
         proj.sanitize_node_names();
         crate::app::merge_template_defs(&mut proj.root, &self.node_templates);
-        let saved_pane_vis = Self::project_pane_visibility(&proj.root);
         self.fs_root = proj.root;
-        // As in the default-project branch: the file's viewport settings
-        // land on the live state before ensure re-seeds the nodes from it.
-        // A load is not a colour change: the loaded tree's value is the
-        // baseline, so the auto-enable of single-colour mode stays quiet.
+        // As in the default-project branch.
         self.last_applied_wire_color = None;
-        self.apply_settings_from_menubar_subnets();
-        self.ensure_menubar_subnets();
-        self.apply_settings_from_menubar_subnets();
-        self.apply_pane_state_from_project(&saved_pane_vis, &proj.view_state);
+        self.migrate_meta_settings_node();
+        self.apply_pane_state_from_project(&proj.view_state);
         self.active_camera = proj.view_state.active_camera;
         self.pan_x = proj.view_state.pan.0;
         self.pan_y = proj.view_state.pan.1;
@@ -559,8 +550,7 @@ impl State {
             inputs: 0,
             outputs: 0,
         };
-        self.ensure_menubar_subnets();
-        self.apply_settings_from_menubar_subnets();
+        self.migrate_meta_settings_node();
         self.active_camera = "Default Camera".to_string();
         self.pan_x = 0.0;
         self.pan_y = 0.0;
@@ -590,668 +580,138 @@ impl State {
         self.update_window_title();
     }
 
-    pub(crate) fn ensure_menubar_subnets(&mut self) {
-        let vp_show_grid = self.viewport().show_grid;
-        let vp_show_cube = self.viewport().show_cube;
-        let vp_show_origin = self.viewport().show_origin;
-        let vp_show_camera_pivot = self.viewport().show_camera_pivot;
-        let vp_bg_color = self.viewport().bg_color;
-        let vp_grid_color = self.viewport().grid_color;
-        let vp_rt_mode = self.viewport().rt_mode;
-        let show_network = self.show_network;
-        let show_viewport = self.show_viewport;
-        let show_parameters = self.show_parameters;
-        let show_spreadsheet = self.show_spreadsheet;
-        let show_playbar = self.show_playbar;
-        let network_plate = self.network_plate;
-        let wireframe = self.wireframe;
-        let wire_single_color = self.wire_single_color;
-        let wire_color = self.wire_color;
-        let wire_width = self.wire_width;
-        let geo_opacity = self.geo_opacity;
-        let render_points = self.render_points;
-        let point_size = self.point_size;
-        let point_color = self.point_color;
-        let square_viewport = self.square_viewport;
-        let bool_str = |b: bool| if b { "true" } else { "false" };
-
-        let camera_nodes: Vec<String> = self.current_dir().children.iter()
-            .filter(|c| c.node_type == "camera")
-            .map(|c| c.name.clone())
-            .collect();
-        let mut camera_options = vec!["Default Camera".to_string()];
-        camera_options.extend(camera_nodes);
-        let camera_options_refs: Vec<&str> = camera_options.iter().map(|s| s.as_str()).collect();
-
-        let mut recent_options = vec!["- Select -".to_string()];
-        for path in &self.recent_files {
-            recent_options.push(path.to_string_lossy().to_string());
-        }
-        recent_options.push("Other".to_string());
-        let recent_options_refs: Vec<&str> = recent_options.iter().map(|s| s.as_str()).collect();
-
-
-        fn find_or_create_subnet<'a>(parent: &'a mut FsNode, name: &str, node_type: &str, pos: (f32, f32)) -> &'a mut FsNode {
-            if let Some(idx) = parent.children.iter().position(|c| c.name == name) {
-                let node = &mut parent.children[idx];
-                node.node_type = node_type.to_string();
-                if node.position == (0.0, 0.0) {
-                    node.position = pos;
-                }
-                node
+    /// Pull an older project's settings off its root `meta` node, then take
+    /// the node out.
+    ///
+    /// Until 2026-09-23 session-wide display settings lived as params on four
+    /// utility subnets (`main`, `view`, `guides`, `render`) under a permanent
+    /// root `meta` node, and that node tree was the STORE OF RECORD:
+    /// `ensure_menubar_subnets` rebuilt it from live state and
+    /// `apply_settings_from_menubar_subnets` copied it back over live state
+    /// after every parameter edit anywhere. A display preference was
+    /// therefore a piece of project data, carried in the file, reset by
+    /// opening someone else's scene — and editable only by selecting the
+    /// right node in the right utility subnet.
+    ///
+    /// They are settings, and they are set from the command palette now: the
+    /// live fields are the values, `DesignSettings` persists them to
+    /// `state.kdl`, and the dialog's Settings half edits them (see
+    /// `SETTINGS` in `src/dialog.rs`). This runs once per load to carry a
+    /// saved project's values across rather than dropping them on the floor —
+    /// a user who set a grid colour two years ago keeps it.
+    ///
+    /// Pane visibility is NOT read here: it is genuinely project state and
+    /// has moved to `ProjectViewState::visible_panes`, which `load_from_file`
+    /// applies. An old save's `view` subnet is read for it, though, or every
+    /// project saved before the move would open with the default layout.
+    pub(crate) fn migrate_meta_settings_node(&mut self) {
+        let meta_idx = self.fs_root.children.iter().position(|c| {
+            matches!(c.node_type.as_str(), "session" | "meta") || c.name == "Session"
+        });
+        // Pre-Session saves parked the four subnets FLAT at the root, with no
+        // container above them, so the sweep below runs whether or not a meta
+        // node was found — an early return on the container alone left that
+        // whole generation of file carrying four dead nodes forever.
+        let mut subnets: Vec<FsNode> = match meta_idx {
+            Some(i) => self.fs_root.children.remove(i).children,
+            None => Vec::new(),
+        };
+        let mut i = 0;
+        while i < self.fs_root.children.len() {
+            let c = &self.fs_root.children[i];
+            if c.node_type == "utility"
+                && matches!(c.name.as_str(), "main" | "view" | "guides" | "render")
+            {
+                subnets.push(self.fs_root.children.remove(i));
             } else {
-                let new_node = FsNode {
-                    id: crate::app::generate_node_id(),
-                    name: name.to_string(),
-                    node_type: node_type.to_string(),
-                    children: vec![],
-                    params: vec![],
-                    geometry_visible: true,
-                    position: pos,
-                    inputs: 1,
-                    outputs: 1,
-                };
-                parent.children.push(new_node);
-                parent.children.last_mut().unwrap()
+                i += 1;
             }
         }
-
-        fn ensure_param(node: &mut FsNode, name: &str, param_type: &str, default_val: &str, options: &[&str], min: Option<f32>, max: Option<f32>, step: Option<f32>) {
-            if !node.params.iter().any(|p| p.name == name) {
-                node.params.push(ParamDef {
-                    name: name.to_string(),
-                    label: name.to_string(),
-                    param_type: param_type.to_string(),
-                    default: default_val.to_string(),
-                    options: options.iter().map(|s| s.to_string()).collect(),
-                    min,
-                    max,
-                    step,
-                    show_when: String::new(),
-                });
+        // Anything else that was living under the meta node is the user's,
+        // not ours: adding a non-geometry node in there was allowed, so a
+        // migration that quietly ate one would be eating their work. Re-home
+        // it at the root, where the level it was in used to be.
+        let mut i = 0;
+        while i < subnets.len() {
+            if matches!(subnets[i].name.as_str(), "main" | "view" | "guides" | "render") {
+                i += 1;
+            } else {
+                let mut node = subnets.remove(i);
+                let (nx, ny) = self.find_empty_cell(node.position.0, node.position.1, None);
+                node.position = (nx, ny);
+                self.fs_root.children.push(node);
             }
         }
-
-        fn set_toggle(p: &mut ParamDef, on: bool) {
-            p.param_type = "toggle".to_string();
-            p.options.clear();
-            p.default = if on { "true" } else { "false" }.to_string();
-        }
-
-        // Retain only the Main utility subnet, removing the rest
-        self.fs_root.children.retain(|c| c.name != "Network" && c.name != "Viewport" && c.name != "Parameters" && c.name != "Spreadsheet");
-
-        // The root meta node: the permanent root container for the
-        // session-wide settings nodes (Main/View/Guides/Render) — the root
-        // network's counterpart of every node's per-node `meta` child, and
-        // still a subnet. It began life as the "Session" node; older saves
-        // carry it typed "session" (or the four settings nodes flat at the
-        // root) and are migrated — retyped/renamed, params intact. The node
-        // itself is undeletable (delete_node refuses the "meta" type).
-        let mut migrated: Vec<FsNode> = Vec::new();
-        {
-            let mut idx = 0;
-            while idx < self.fs_root.children.len() {
-                let c = &self.fs_root.children[idx];
-                if c.node_type == "utility"
-                    && matches!(c.name.as_str(), "main" | "view" | "guides" | "render")
-                {
-                    migrated.push(self.fs_root.children.remove(idx));
-                } else {
-                    idx += 1;
-                }
-            }
+        if subnets.is_empty() {
+            return;
         }
-        let session_idx = match self
-            .fs_root
-            .children
-            .iter()
-            .position(|c| matches!(c.node_type.as_str(), "session" | "meta") || c.name == "Session")
-        {
-            Some(i) => {
-                self.fs_root.children[i].node_type = "meta".to_string();
-                self.fs_root.children[i].name = "meta".to_string();
-                i
-            }
-            None => {
-                self.fs_root.children.push(FsNode {
-                    id: crate::app::generate_node_id(),
-                    name: "meta".to_string(),
-                    node_type: "meta".to_string(),
-                    children: vec![],
-                    params: vec![],
-                    geometry_visible: true,
-                    position: (0.0, 0.0),
-                    inputs: 0,
-                    outputs: 0,
-                });
-                self.fs_root.children.len() - 1
-            }
+        let params = |name: &str| -> Vec<ParamDef> {
+            subnets
+                .iter()
+                .find(|c| c.name.eq_ignore_ascii_case(name))
+                .map(|n| n.params.clone())
+                .unwrap_or_default()
         };
-        for node in migrated {
-            let session = &mut self.fs_root.children[session_idx];
-            if !session.children.iter().any(|c| c.name == node.name) {
-                session.children.push(node);
-            }
-        }
-        let session = &mut self.fs_root.children[session_idx];
-
-        // 1. Main subnet
-        let main_node = find_or_create_subnet(session, "main", "utility", (0.0, 0.0));
-        main_node.children.clear();
+        let as_bool = |p: &ParamDef| p.default.parse::<bool>().ok();
+        let as_f32 = |p: &ParamDef| p.default.parse::<f32>().ok();
 
-        ensure_param(main_node, "File", "section", "", &[], None, None, None);
-        ensure_param(main_node, "New Project", "button", "", &[], None, None, None);
-
-        if let Some(p) = main_node.params.iter_mut().find(|p| p.name == "Open") {
-            p.options = recent_options.clone();
-            if !p.options.contains(&p.default) {
-                p.default = "- Select -".to_string();
-            }
-        } else {
-            ensure_param(main_node, "Open", "choice", "- Select -", &recent_options_refs, None, None, None);
-        }
-
-        ensure_param(main_node, "Save", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Save As", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Set As Default", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Exit", "button", "", &[], None, None, None);
-
-        ensure_param(main_node, "Edit", "section", "", &[], None, None, None);
-        ensure_param(main_node, "Undo", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Redo", "button", "", &[], None, None, None);
-
-        // The pane-visibility toggles moved to the View utility node (below):
-        // retire Main's copies and its now-empty View section from older saves.
-        // No value migration — pane state is session-owned, never applied from
-        // the project, so the View node seeds from live state.
-        main_node.params.retain(|p| {
-            !matches!(
-                p.name.as_str(),
-                "View" | "Show Network Pane" | "Show Viewport Pane" | "Show Parameters Pane"
-                    | "Show Spreadsheet Pane" | "Show Playbar Pane"
-            )
-        });
-
-        // Network — renamed from the retired "Network Settings" (migrate older
-        // saves' section param in place so its position survives the reorder).
-        if let Some(sec) = main_node.params.iter_mut().find(|p| p.name == "Network Settings") {
-            sec.name = "Network".to_string();
-            sec.label = "Network".to_string();
-        }
-        ensure_param(main_node, "Network", "section", "", &[], None, None, None);
-        ensure_param(main_node, "Zoom In", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Zoom Out", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Reset Zoom", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Detach Circular Window", "button", "", &[], None, None, None);
-        ensure_param(main_node, "Circular Pane", "toggle", bool_str(self.circular_network_pane), &[], None, None, None);
-        // Node color is config-owned (style.surface.graph.node.color in
-        // config.kdl) — drop the retired per-project params from older saves.
-        main_node.params.retain(|p| !matches!(p.name.as_str(), "Node Color R" | "Node Color G" | "Node Color B"));
-
-        // Viewport — renamed from the retired "Viewport Settings" (migrate
-        // older saves' section param in place).
-        if let Some(sec) = main_node.params.iter_mut().find(|p| p.name == "Viewport Settings") {
-            sec.name = "Viewport".to_string();
-            sec.label = "Viewport".to_string();
-        }
-        ensure_param(main_node, "Viewport", "section", "", &[], None, None, None);
-        if let Some(p) = main_node.params.iter_mut().find(|p| p.name == "Active Camera") {
-            p.options = camera_options.clone();
-            if !p.options.contains(&p.default) {
-                p.default = "Default Camera".to_string();
-            }
-        } else {
-            ensure_param(main_node, "Active Camera", "choice", &self.active_camera, &camera_options_refs, None, None, None);
-        }
-
-        ensure_param(main_node, "Ray Traced Preview", "toggle", bool_str(vp_rt_mode), &[], None, None, None);
-        ensure_param(main_node, "Background Color", "color", &color_to_hex(vp_bg_color), &[], None, None, None);
-        // Mirrors the live value, as the toggles do: a background set on the
-        // viewport rather than through the node still reaches the saved tree.
-        if let Some(p) = main_node.params.iter_mut().find(|p| p.name == "Background Color") {
-            p.default = color_to_hex(vp_bg_color);
-        }
-
-        // The Style section is retired — DE chrome is config-owned, not
-        // per-project: the wall and edge relief curves are
-        // `style.surface.relief.profile` / `.edge_profile` and the params
-        // plate tint is `style.surface.param.color` in config.kdl, which
-        // cce-ui already applies for every client. Main's copies shadowed
-        // those on load, so a project file silently outranked the user's
-        // config. Drop them from older saves.
-        main_node.params.retain(|p| {
-            !matches!(
-                p.name.as_str(),
-                "Style" | "Bevel Profile" | "Edge Profile" | "Plate Color"
-            )
-        });
-
-        // The Help section is retired — its only row was an About button nothing
-        // dispatched. Drop it from older saves too.
-        main_node.params.retain(|p| !matches!(p.name.as_str(), "Help" | "About"));
-
-        // The viewport guide params moved to the Guides utility node: retire
-        // Main's copies, keeping an older save's values as the seeds.
-        let migrated_grid = main_node.params.iter()
-            .find(|p| p.name == "Show Grid Guide")
-            .and_then(|p| p.default.parse::<bool>().ok());
-        let migrated_cube = main_node.params.iter()
-            .find(|p| p.name == "Show Reference Cube")
-            .and_then(|p| p.default.parse::<bool>().ok());
-        let migrated_origin = main_node.params.iter()
-            .find(|p| p.name == "Show Origin Axes")
-            .and_then(|p| p.default.parse::<bool>().ok());
-        let migrated_thickness = main_node.params.iter()
-            .find(|p| p.name == "Grid Thickness")
-            .map(|p| p.default.clone());
-        let migrated_origin_size = main_node.params.iter()
-            .find(|p| p.name == "Origin Guide Size")
-            .map(|p| p.default.clone());
-        let migrated_grid_color = main_node.params.iter()
-            .find(|p| p.name == "Grid Color")
-            .map(|p| p.default.clone());
-        main_node.params.retain(|p| {
-            !matches!(
-                p.name.as_str(),
-                "Show Grid Guide" | "Show Reference Cube" | "Show Origin Axes"
-                    | "Grid Thickness" | "Origin Guide Size" | "Grid Color"
-            )
-        });
-
-        // Square Aspect / Show Camera Pivot moved to the camera nodes
-        // (per-camera display params): retire Main's copies, keeping an older
-        // save's values as the seed for the cameras below.
-        let migrated_square = main_node.params.iter()
-            .find(|p| p.name == "Square Aspect")
-            .and_then(|p| p.default.parse::<bool>().ok());
-        let migrated_pivot = main_node.params.iter()
-            .find(|p| p.name == "Show Camera Pivot")
-            .and_then(|p| p.default.parse::<bool>().ok());
-        let migrated_pivot_size = main_node.params.iter()
-            .find(|p| p.name == "Camera Pivot Size")
-            .map(|p| p.default.clone());
-        main_node.params.retain(|p| {
-            !matches!(p.name.as_str(), "Square Aspect" | "Show Camera Pivot" | "Camera Pivot Size")
-        });
-
-        // Boolean settings render as toggles. Older saves stored these as
-        // choice dropdowns / buttons; retype them so a reopened project shows
-        // real switches.
-        for p in main_node.params.iter_mut() {
+        for p in params("guides").iter().chain(params("main").iter()) {
             match p.name.as_str() {
-                "Circular Pane" | "Ray Traced Preview" => {
-                    p.param_type = "toggle".to_string();
-                    p.options.clear();
-                    if p.default != "true" { p.default = "false".to_string(); }
+                "Show Grid Guide" | "Show Grid" => {
+                    if let Some(v) = as_bool(p) { self.viewport_mut().show_grid = v; }
                 }
-                _ => {}
-            }
-        }
-
-        const MAIN_PARAM_ORDER: [&str; 20] = [
-            "File", "New Project", "Open", "Save", "Save As", "Set As Default", "Exit",
-            "Edit", "Undo", "Redo",
-            "Network", "Zoom In", "Zoom Out",
-            "Reset Zoom", "Detach Circular Window", "Circular Pane",
-            "Viewport", "Active Camera",
-            "Ray Traced Preview",
-            "Background Color",
-        ];
-        main_node.params.sort_by_key(|p| {
-            MAIN_PARAM_ORDER
-                .iter()
-                .position(|n| *n == p.name)
-                .unwrap_or(MAIN_PARAM_ORDER.len())
-        });
-
-        // Display labels only — the `name` stays the dispatch identity used by
-        // execute_menu_action and the live-toggle refresh. The params pane keys
-        // off `label` when set (param_display), and sync_parameters_to_project
-        // resolves a click back to its param by that same display key.
-        for p in main_node.params.iter_mut() {
-            if p.name == "New Project" {
-                p.label = "New".to_string();
-            } else if p.param_type == "toggle" {
-                if let Some(rest) = p.name.strip_prefix("Show ") {
-                    p.label = rest.to_string();
+                "Show Reference Cube" | "Cube" => {
+                    if let Some(v) = as_bool(p) { self.viewport_mut().show_cube = v; }
                 }
-            }
-        }
-
-        // 2. View subnet — the pane-visibility switches, migrated off Main's
-        // View section (the Guides pattern: a setting's home is a utility
-        // node; the header menu items stay as command access). The toggles
-        // refresh from live state — mid-session, the live flags are the
-        // authority — but they are ALSO the persisted pane state: save_to_file
-        // syncs this mirror before cloning the tree, and load_from_file reads
-        // the loaded values (before this refresh clobbers them) and applies
-        // the diffs via apply_pane_state_from_project.
-        let view_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "view", "utility", (0.0, 2.0));
-        view_node.children.clear();
-        ensure_param(view_node, "Panes", "section", "", &[], None, None, None);
-        ensure_param(view_node, "Show Network Pane", "toggle", bool_str(show_network), &[], None, None, None);
-        ensure_param(view_node, "Show Viewport Pane", "toggle", bool_str(show_viewport), &[], None, None, None);
-        ensure_param(view_node, "Show Parameters Pane", "toggle", bool_str(show_parameters), &[], None, None, None);
-        ensure_param(view_node, "Show Spreadsheet Pane", "toggle", bool_str(show_spreadsheet), &[], None, None, None);
-        ensure_param(view_node, "Show Playbar Pane", "toggle", bool_str(show_playbar), &[], None, None, None);
-        // Appearance, not visibility — hence its own section. The pane
-        // toggles above say which panes EXIST; this one says whether the
-        // network draws a surface under its graph or lets the scene through.
-        ensure_param(view_node, "Network", "section", "", &[], None, None, None);
-        ensure_param(
-            view_node,
-            "Show Network Plate",
-            "toggle",
-            bool_str(network_plate),
-            &[],
-            None,
-            None,
-            None,
-        );
-        for p in view_node.params.iter_mut() {
-            match p.name.as_str() {
-                "Show Network Pane" => set_toggle(p, show_network),
-                "Show Viewport Pane" => set_toggle(p, show_viewport),
-                "Show Parameters Pane" => set_toggle(p, show_parameters),
-                "Show Spreadsheet Pane" => set_toggle(p, show_spreadsheet),
-                "Show Playbar Pane" => set_toggle(p, show_playbar),
-                "Show Network Plate" => set_toggle(p, network_plate),
-                _ => {}
-            }
-            // "Show Network Pane" -> "Network": inside the Panes section the
-            // toggles read by pane name alone. The `name` stays the dispatch
-            // identity execute_menu_action fires on.
-            if p.param_type == "toggle" {
-                if let Some(rest) = p.name.strip_prefix("Show ").and_then(|r| r.strip_suffix(" Pane")) {
-                    p.label = rest.to_string();
-                } else if p.name == "Show Network Plate" {
-                    // Under its own "Network" section the row reads as
-                    // "Plate", the same way the pane rows read as their pane.
-                    p.label = "Plate".to_string();
+                "Show Origin Axes" | "Origin" => {
+                    if let Some(v) = as_bool(p) { self.viewport_mut().show_origin = v; }
                 }
-            }
-        }
-
-        // 3. Guides subnet — viewport guide toggles (home of the grid toggle,
-        // migrated off Main). The utility column keeps one empty cell between
-        // nodes: Main (0,0), View (0,2), Guides (0,4), Render (0,6); older
-        // saves parked at prior defaults slide to the spaced slots.
-        let guides_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "guides", "utility", (0.0, 4.0));
-        if guides_node.position == (0.0, 1.0) || guides_node.position == (0.0, 2.0) {
-            guides_node.position = (0.0, 4.0);
-        }
-        guides_node.children.clear();
-        ensure_param(guides_node, "Guides", "section", "", &[], None, None, None);
-        ensure_param(guides_node, "Show Grid Guide", "toggle", bool_str(migrated_grid.unwrap_or(vp_show_grid)), &[], None, None, None);
-        ensure_param(guides_node, "Show Reference Cube", "toggle", bool_str(migrated_cube.unwrap_or(vp_show_cube)), &[], None, None, None);
-        ensure_param(guides_node, "Show Origin Axes", "toggle", bool_str(migrated_origin.unwrap_or(vp_show_origin)), &[], None, None, None);
-        let thickness_seed = migrated_thickness
-            .unwrap_or_else(|| ((self.grid_thickness * 1000.0) as i32).to_string());
-        ensure_param(guides_node, "Grid Thickness", "spinbox", &thickness_seed, &[], Some(2.0), Some(200.0), Some(1.0));
-        let origin_size_seed = migrated_origin_size
-            .unwrap_or_else(|| ((self.origin_size * 10.0) as i32).to_string());
-        ensure_param(guides_node, "Origin Guide Size", "spinbox", &origin_size_seed, &[], Some(1.0), Some(50.0), Some(1.0));
-        let grid_color_seed = migrated_grid_color.unwrap_or_else(|| color_to_hex(vp_grid_color));
-        ensure_param(guides_node, "Grid Color", "color", &grid_color_seed, &[], None, None, None);
-        // Size of the Show Point Markers overlay, in thousandths
-        // (the Grid Thickness convention): 20 = 0.02 world units.
-        let marker_size_seed = ((self.point_marker_size * 1000.0).round() as i32).to_string();
-        ensure_param(guides_node, "Point Marker Size", "spinbox", &marker_size_seed, &[], Some(5.0), Some(100.0), Some(1.0));
-        let marker_color_seed = color_to_hex(self.point_marker_color);
-        ensure_param(guides_node, "Point Marker Color", "color", &marker_color_seed, &[], None, None, None);
-        // What a world unit is in the real world. The geometry never
-        // converts; the viewport's scale readout and `View 1:1` do.
-        ensure_param(guides_node, "World Unit", "choice", self.world_unit.suffix(), &["mm", "cm", "m", "in"], None, None, None);
-        for p in guides_node.params.iter_mut() {
-            match p.name.as_str() {
-                "Show Grid Guide" => set_toggle(p, vp_show_grid),
-                "Show Reference Cube" => set_toggle(p, vp_show_cube),
-                "Show Origin Axes" => set_toggle(p, vp_show_origin),
+                "Grid Thickness" => if let Some(v) = as_f32(p) { self.grid_thickness = v / 1000.0; },
+                "Origin Guide Size" => if let Some(v) = as_f32(p) { self.origin_size = v / 10.0; },
+                "Camera Pivot Size" => if let Some(v) = as_f32(p) { self.camera_pivot_size = v / 10.0; },
+                "Grid Color" => if let Some(c) = hex_to_color(&p.default) { self.viewport_mut().grid_color = c; },
+                "Background Color" => if let Some(c) = hex_to_color(&p.default) { self.viewport_mut().bg_color = c; },
+                "Point Marker Size" => if let Some(v) = as_f32(p) { self.point_marker_size = v / 1000.0; },
+                "Point Marker Color" => if let Some(c) = hex_to_color(&p.default) { self.point_marker_color = c; },
+                "World Unit" => if let Some(u) = cce_ui::units::Unit::parse(&p.default) { self.world_unit = u; },
+                "Circular Pane" => if let Some(v) = as_bool(p) { self.circular_network_pane = v; },
+                "Ray Traced Preview" => if let Some(v) = as_bool(p) { self.viewport_mut().rt_mode = v; },
                 _ => {}
             }
-            if p.param_type == "toggle" {
-                if let Some(rest) = p.name.strip_prefix("Show ") {
-                    p.label = rest.to_string();
-                }
-            }
         }
-
-        // 4. Render subnet — render/display controls, present by default like
-        // Main. Toggles reflect live state so a reopened project shows real
-        // switches. Two rows below Guides (the spaced column); older saves
-        // parked at the prior defaults slide down.
-        let render_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "render", "utility", (0.0, 6.0));
-        if render_node.position == (0.0, 1.0) || render_node.position == (0.0, 2.0) || render_node.position == (0.0, 4.0) {
-            render_node.position = (0.0, 6.0);
-        }
-        render_node.children.clear();
-
-        // The overlay toggle is retired — drop it from older saves so the
-        // pane doesn't resurrect it.
-        render_node.params.retain(|p| p.name != "Wireframe Overlay");
-
-        ensure_param(render_node, "Render Settings", "section", "", &[], None, None, None);
-        ensure_param(render_node, "Show Wireframe", "toggle", bool_str(wireframe), &[], None, None, None);
-        ensure_param(render_node, "Wire Single Color", "toggle", bool_str(wire_single_color), &[], None, None, None);
-        // rgba: the alpha channel is the wireframe's own opacity (the
-        // geometry Opacity slider deliberately leaves wires alone).
-        ensure_param(render_node, "Wire Color", "rgba", &color_to_hex8(wire_color), &[], None, None, None);
-        ensure_param(render_node, "Wire Thickness", "slider:1.0:8.0:1", &format!("{:.1}", wire_width), &[], Some(1.0), Some(8.0), None);
-        ensure_param(render_node, "Opacity", "slider:0.00:1.00", &format!("{:.2}", geo_opacity), &[], Some(0.0), Some(1.0), None);
-        ensure_param(render_node, "Render Points", "toggle", bool_str(render_points), &[], None, None, None);
-        ensure_param(render_node, "Point Size", "slider:0.000:0.100:3", &format!("{:.3}", point_size), &[], Some(0.0), Some(0.10), None);
-        ensure_param(render_node, "Point Color", "color", &color_to_hex(point_color), &[], None, None, None);
-
-        for p in render_node.params.iter_mut() {
+        for p in params("render") {
             match p.name.as_str() {
-                "Show Wireframe" => set_toggle(p, wireframe),
-                "Wire Single Color" => set_toggle(p, wire_single_color),
-                "Wire Color" => {
-                    // Type migration: early saves carried a plain rgb color.
-                    p.param_type = "rgba".to_string();
-                    p.default = color_to_hex8(wire_color);
-                }
-                "Wire Thickness" => p.default = format!("{:.1}", wire_width),
-                "Opacity" => p.default = format!("{:.2}", geo_opacity),
-                "Render Points" => set_toggle(p, render_points),
-                "Point Size" => {
-                    // Range/precision migration: older saves carried
-                    // slider:0.01:0.30 (2 decimals).
-                    p.param_type = "slider:0.000:0.100:3".to_string();
-                    p.min = Some(0.0);
-                    p.max = Some(0.10);
-                    p.default = format!("{:.3}", point_size);
-                }
-                "Point Color" => p.default = color_to_hex(point_color),
+                "Show Wireframe" => if let Some(v) = as_bool(&p) { self.wireframe = v; },
+                "Wire Single Color" => if let Some(v) = as_bool(&p) { self.wire_single_color = v; },
+                "Wire Color" => if let Some(c) = hex_to_rgba(&p.default) { self.wire_color = c; },
+                "Wire Thickness" => if let Some(v) = as_f32(&p) { self.wire_width = v.clamp(1.0, 8.0); },
+                "Opacity" => if let Some(v) = as_f32(&p) { self.geo_opacity = v.clamp(0.0, 1.0); },
+                "Render Points" => if let Some(v) = as_bool(&p) { self.render_points = v; },
+                "Point Size" => if let Some(v) = as_f32(&p) { self.point_size = v.clamp(0.0, 0.1); },
+                "Point Color" => if let Some(c) = hex_to_color(&p.default) { self.point_color = c; },
                 _ => {}
             }
-            if p.param_type == "toggle" {
-                if let Some(rest) = p.name.strip_prefix("Show ") {
-                    p.label = rest.to_string();
-                }
-            }
         }
-
-        // 3. Camera display params — Square Aspect / Show Camera Pivot /
-        // Camera Pivot Size live on the camera nodes (applied from the
-        // ACTIVE camera). Ensured on every camera in the tree, seeded from
-        // the retired Main copies (older saves) or the live values.
-        fn ensure_camera_display_params(node: &mut FsNode, square: bool, pivot: bool, pivot_size: &str) {
-            if node.node_type == "camera" {
-                let bool_str = |b: bool| if b { "true" } else { "false" };
-                if !node.params.iter().any(|p| p.name == "Square Aspect") {
-                    node.params.push(ParamDef {
-                        name: "Square Aspect".to_string(),
-                        label: "Square Aspect".to_string(),
-                        param_type: "toggle".to_string(),
-                        default: bool_str(square).to_string(),
-                        options: Vec::new(),
-                        min: None,
-                        max: None,
-                        step: None,
-                        show_when: String::new(),
-                    });
-                }
-                if !node.params.iter().any(|p| p.name == "Show Camera Pivot") {
-                    node.params.push(ParamDef {
-                        name: "Show Camera Pivot".to_string(),
-                        label: "Camera Pivot".to_string(),
-                        param_type: "toggle".to_string(),
-                        default: bool_str(pivot).to_string(),
-                        options: Vec::new(),
-                        min: None,
-                        max: None,
-                        step: None,
-                        show_when: String::new(),
-                    });
-                }
-                if !node.params.iter().any(|p| p.name == "Camera Pivot Size") {
-                    node.params.push(ParamDef {
-                        name: "Camera Pivot Size".to_string(),
-                        label: "Camera Pivot Size".to_string(),
-                        param_type: "spinbox".to_string(),
-                        default: pivot_size.to_string(),
-                        options: Vec::new(),
-                        min: Some(1.0),
-                        max: Some(50.0),
-                        step: Some(1.0),
-                        show_when: String::new(),
-                    });
-                }
-            }
-            for child in &mut node.children {
-                ensure_camera_display_params(child, square, pivot, pivot_size);
-            }
-        }
-        let square_seed = migrated_square.unwrap_or(square_viewport);
-        let pivot_seed = migrated_pivot.unwrap_or(vp_show_camera_pivot);
-        let pivot_size_seed = migrated_pivot_size
-            .unwrap_or_else(|| ((self.camera_pivot_size * 10.0) as i32).to_string());
-        ensure_camera_display_params(&mut self.fs_root, square_seed, pivot_seed, &pivot_size_seed);
-    }
-
-    pub(crate) fn apply_settings_from_menubar_subnets(&mut self) {
-        let session_params = |root: &FsNode, name: &str| -> Option<Vec<ParamDef>> {
-            root.children
-                .iter()
-                .find(|c| c.node_type == "meta")
-                .and_then(|s| s.children.iter().find(|c| c.name == name))
-                .map(|n| n.params.clone())
-        };
-        if let Some(params) = session_params(&self.fs_root, "guides") {
-            for p in &params {
-                match p.name.as_str() {
-                    "Show Grid Guide" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_grid = val; }
-                    "Show Reference Cube" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_cube = val; }
-                    "Show Origin Axes" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_origin = val; }
-                    "Grid Thickness" => if let Ok(val) = p.default.parse::<f32>() { self.grid_thickness = val / 1000.0; }
-                    "Origin Guide Size" => if let Ok(val) = p.default.parse::<f32>() { self.origin_size = val / 10.0; }
-                    "Grid Color" => if let Some(col) = hex_to_color(&p.default) { self.viewport_mut().grid_color = col; }
-                    "Point Marker Size" => if let Ok(val) = p.default.parse::<f32>() {
-                        let size = val / 1000.0;
-                        if (size - self.point_marker_size).abs() > 1e-6 {
-                            self.point_marker_size = size;
-                            // The marker geometry bakes the radius in, so a
-                            // size change re-collects the overlays.
-                            self.rebuild_scene_geometry();
-                        }
-                    }
-                    "Point Marker Color" => if let Some(col) = hex_to_color(&p.default) {
-                        if col != self.point_marker_color {
-                            self.point_marker_color = col;
-                            // Baked into the marker verts, like the radius.
-                            self.rebuild_scene_geometry();
-                        }
-                    }
-                    "World Unit" => if let Some(u) = cce_ui::units::Unit::parse(&p.default) {
-                        if u != self.world_unit {
-                            self.world_unit = u;
-                            self.viewport_dirty = true;
-                        }
-                    }
-                    _ => {}
-                }
-            }
-        }
-        if let Some(params) = session_params(&self.fs_root, "main") {
-            for p in &params {
-                match p.name.as_str() {
-                    // Network Settings
-                    "Circular Pane" => if let Ok(val) = p.default.parse::<bool>() { self.circular_network_pane = val; }
-
-                    // Viewport Settings
-                    "Show Grid Guide" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_grid = val; }
-                    "Show Reference Cube" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_cube = val; }
-                    "Show Origin Axes" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_origin = val; }
-                    "Ray Traced Preview" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().rt_mode = val; }
-                    "Grid Thickness" => if let Ok(val) = p.default.parse::<f32>() { self.grid_thickness = val / 1000.0; }
-                    "Origin Guide Size" => if let Ok(val) = p.default.parse::<f32>() { self.origin_size = val / 10.0; }
-                    "Camera Pivot Size" => if let Ok(val) = p.default.parse::<f32>() { self.camera_pivot_size = val / 10.0; }
-                    "Background Color" => if let Some(col) = hex_to_color(&p.default) { self.viewport_mut().bg_color = col; }
-                    "Grid Color" => if let Some(col) = hex_to_color(&p.default) { self.viewport_mut().grid_color = col; }
-                    "Show Grid" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_grid = val; }
-                    "Cube" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_cube = val; }
-                    "Origin" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_origin = val; }
-                    "Active Camera" => {
-                        let cam = p.default.clone();
-                        self.active_camera = cam.clone();
-                        self.viewport_mut().active_camera = cam;
-                    }
-
-                    _ => {}
-                }
-            }
-        }
-
-        // The ACTIVE camera's display params (per-camera). Default Camera has
-        // no node — the live values stand.
-        if self.active_camera != "Default Camera" {
-            let active = self.active_camera.clone();
-            let cam_params = self.current_dir().children.iter()
-                .find(|c| c.node_type == "camera" && c.name == active)
-                .map(|c| c.params.clone());
-            if let Some(params) = cam_params {
-                for p in &params {
-                    match p.name.as_str() {
-                        "Square Aspect" => if let Ok(val) = p.default.parse::<bool>() { self.square_viewport = val; }
-                        "Show Camera Pivot" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_camera_pivot = val; }
-                        "Camera Pivot Size" => if let Ok(val) = p.default.parse::<f32>() { self.camera_pivot_size = val / 10.0; }
-                        _ => {}
+        // The pane layout an old save carried on its `view` subnet, in the
+        // shape `apply_pane_state_from_project` now reads.
+        let view = params("view");
+        if !view.is_empty() && !(self.is_detached_network || self.detached_pane.is_some()) {
+            for (name, get, action) in Self::PANE_FLAGS {
+                let Some(p) = view
+                    .iter()
+                    .find(|p| p.param_type == "toggle" && p.name == format!("Show {} Pane", capitalize(name)))
+                else {
+                    continue;
+                };
+                if let Some(desired) = as_bool(p) {
+                    if get(self) != desired {
+                        self.execute_menu_action(action);
                     }
                 }
             }
         }
-
-        if let Some(params) = session_params(&self.fs_root, "render") {
-            let before = self.wire_color;
-            for p in &params {
-                match p.name.as_str() {
-                    "Show Wireframe" => if let Ok(val) = p.default.parse::<bool>() { self.wireframe = val; }
-                    "Wire Single Color" => if let Ok(val) = p.default.parse::<bool>() { self.wire_single_color = val; }
-                    "Wire Color" => if let Some(col) = hex_to_rgba(&p.default) { self.wire_color = col; }
-                    "Wire Thickness" => if let Ok(val) = p.default.parse::<f32>() { self.wire_width = val.clamp(1.0, 8.0); }
-                    "Opacity" => if let Ok(val) = p.default.parse::<f32>() { self.geo_opacity = val.clamp(0.0, 1.0); }
-                    "Render Points" => if let Ok(val) = p.default.parse::<bool>() { self.render_points = val; }
-                    "Point Size" => if let Ok(val) = p.default.parse::<f32>() { self.point_size = val.clamp(0.0, 0.1); }
-                    "Point Color" => if let Some(col) = hex_to_color(&p.default) { self.point_color = col; }
-                    _ => {}
-                }
-            }
-            // Setting a wire colour means wanting to see it: a CHANGE to the
-            // colour (not a load — the first read of a tree sets the
-            // baseline, and a load's value is what it is) turns single-colour
-            // mode on if it was off, on the node as well as live, so the
-            // switch shows moved. Off, the wires carry the geometry's own
-            // colours and the colour row is their alpha alone — which twice
-            // read as "the colour did not take" (2026-09-21).
-            let changed = self.last_applied_wire_color.is_some_and(|last| last != self.wire_color)
-                && before != self.wire_color;
-            self.last_applied_wire_color = Some(self.wire_color);
-            if changed && !self.wire_single_color {
-                self.wire_single_color = true;
-                self.write_render_toggle("Wire Single Color", true);
-            }
-        }
+        // The values are live state now, so they belong in state.kdl — this
+        // is the one write that makes the migration stick.
+        self.save_settings();
     }
 }
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 9827df0..9489f88 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -20,6 +20,13 @@ pub enum Action {
     TogglePointMarkers,
     TogglePointNumbers,
     TogglePointNormals,
+    /// The path-traced preview, the wireframe's single-colour mode, and the
+    /// point display. All three were toggles on the root meta node's utility
+    /// subnets and reachable ONLY there; with those nodes retired they are
+    /// commands, which is what makes them reachable at all.
+    ToggleRayTracedPreview,
+    ToggleWireSingleColor,
+    ToggleRenderPoints,
     ToggleCircularPane,
     DetachCircularWindow,
     Save,
diff --git a/src/window.rs b/src/window.rs
index 6815e18..c60a13c 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -339,7 +339,6 @@ impl State {
                         2 => {
                             state.circular_network_pane = !state.circular_network_pane;
                             let val = state.circular_network_pane;
-                            state.write_meta_toggle("main", "Circular Pane", val);
                             state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
                             state.rebuild_positions();
                             state.apply_layout();
@@ -691,7 +690,7 @@ impl State {
             }
             McpAction::Enter { slot } => {
                 let dir = state.current_dir();
-                if slot < dir.children.len() && (dir.children[slot].node_type == "node" || dir.children[slot].node_type == "utility" || !dir.children[slot].children.is_empty()) {
+                if slot < dir.children.len() && (dir.children[slot].node_type == "node" || !dir.children[slot].children.is_empty()) {
                     state.current_path.push(slot);
                     state.on_path_changed();
                     state.sync_parameters_pane();
@@ -724,7 +723,6 @@ impl State {
                         // Same sequence as the interactive param-pane
                         // path, so settings params (viewport flags,
                         // grid) actually take effect via automation.
-                        state.apply_settings_from_menubar_subnets();
                         state.sync_grid_settings();
                         state.sync_nodes();
                         state.rebuild_scene_geometry();
@@ -775,9 +773,7 @@ impl State {
             McpAction::ToggleGeometry { slot } => {
                 let active_nodes = state.current_dir().children.len();
                 if slot < active_nodes {
-                    if state.current_dir().children[slot].node_type == "utility" {
-                        Err("Cannot toggle geometry visibility on utility nodes".to_string())
-                    } else {
+                    {
                         let visible = !state.current_dir().children[slot].geometry_visible;
                         state.current_dir_mut().set_child_geometry_visible(slot, visible);
                         state.sync_nodes();
@@ -799,16 +795,7 @@ impl State {
                     // Fresh ids, like paste: a verbatim clone shares the
                     // template's ids across every instance.
                     crate::app::regenerate_node_ids(&mut node);
-                    let mut allowed = true;
-                    let is_in_utility = state.in_settings_dir();
-                    if is_in_utility {
-                        if crate::geometry::is_geometry_node_type(&node.node_type) {
-                            allowed = false;
-                        }
-                    }
-                    if !allowed {
-                        Err("Utility nodes cannot contain geometry.".to_string())
-                    } else {
+                    {
                         let (nx, ny) = state.find_empty_cell(x, y, None);
                         node.position = (nx, ny);
                         if let Some(n) = name {
@@ -836,14 +823,6 @@ impl State {
                 }
             }
             McpAction::DeleteNode { slot } => {
-                if slot < state.current_dir().children.len()
-                    && matches!(
-                        state.current_dir().children[slot].node_type.as_str(),
-                        "session" | "meta"
-                    )
-                {
-                    return Err("Meta nodes are permanent and cannot be deleted".to_string());
-                }
                 if state.delete_node(slot) {
                     // delete_node clears/shifts the selection; the param pane
                     // resync normally comes from process_window_event's tail.
@@ -980,7 +959,6 @@ impl State {
             McpAction::ToggleCircularPane => {
                 state.circular_network_pane = !state.circular_network_pane;
                 let val = state.circular_network_pane;
-                state.write_meta_toggle("main", "Circular Pane", val);
                 state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
                 state.rebuild_positions();
                 state.apply_layout();