diff --git a/docs/plans/2026-03-06-geometry-push-design.md b/docs/plans/2026-03-06-geometry-push-design.md
deleted file mode 100644
index 49fa8d8..0000000
--- a/docs/plans/2026-03-06-geometry-push-design.md
+++ /dev/null
@@ -1,427 +0,0 @@
-# Geometry-Based Push — Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Replace bounding-box push with polygon-based directional distance so parts nestle together based on actual cut geometry.
-
-**Architecture:** Convert CNC programs to polygon line segments, offset the moving part's polygon by `PartSpacing`, then compute the exact minimum translation distance along the push axis before any edge contact. Plate edge checks remain bounding-box based.
-
-**Tech Stack:** .NET Framework 4.8, OpenNest.Core geometry primitives
-
----
-
-## Context
-
-- `PlateView.PushSelected` (in `OpenNest\Controls\PlateView.cs:753-839`) currently uses `Helper.ClosestDistance*` methods that operate on `Box` objects
-- `PushDirection` enum lives in `OpenNest\PushDirection.cs` (UI project) — must move to Core so `Helper` can reference it
-- Parts convert to geometry via: `ConvertProgram.ToGeometry()` → `Helper.GetShapes()` → `Shape.ToPolygon()` → `Polygon.ToLines()`
-- `Shape.OffsetEntity(distance, OffsetSide.Left)` offsets a shape outward (already implemented in `OpenNest.Core\Geometry\Shape.cs:355-423`)
-
----
-
-### Task 1: Move PushDirection enum to OpenNest.Core
-
-**Files:**
-- Move: `OpenNest\PushDirection.cs` → `OpenNest.Core\PushDirection.cs`
-
-`PushDirection` is currently in the UI project. `Helper.cs` (Core) needs to reference it. Since the enum has no UI dependencies, move it to Core. The UI project already references Core, so all existing usages continue to compile.
-
-**Step 1: Create PushDirection.cs in OpenNest.Core**
-
-```csharp
-namespace OpenNest
-{
- public enum PushDirection
- {
- Up,
- Down,
- Left,
- Right
- }
-}
-```
-
-**Step 2: Delete `OpenNest\PushDirection.cs`**
-
-**Step 3: Add the new file to `OpenNest.Core.csproj`**
-
-Add `` and remove from `OpenNest.csproj`.
-
-**Step 4: Build to verify**
-
-Run: `msbuild OpenNest.sln /p:Configuration=Release`
-Expected: Build succeeds — namespace is already `OpenNest`, no code changes needed in consumers.
-
-**Step 5: Commit**
-
-```
-feat: move PushDirection enum to OpenNest.Core
-```
-
----
-
-### Task 2: Add Helper.GetPartLines — convert a Part to positioned line segments
-
-**Files:**
-- Modify: `OpenNest.Core\Helper.cs`
-
-Add a helper that encapsulates the conversion pipeline: Program → geometry → shapes → polygons → lines, positioned at the part's world location. This is called once per part during push.
-
-**Step 1: Add GetPartLines to Helper.cs** (after the existing `GetShapes` methods, around line 337)
-
-```csharp
-public static List GetPartLines(Part part)
-{
- var entities = Converters.ConvertProgram.ToGeometry(part.Program);
- var shapes = GetShapes(entities.Where(e => e.Layer != Geometry.SpecialLayers.Rapid));
- var lines = new List();
-
- foreach (var shape in shapes)
- {
- var polygon = shape.ToPolygon();
- polygon.Offset(part.Location);
- lines.AddRange(polygon.ToLines());
- }
-
- return lines;
-}
-```
-
-**Step 2: Add GetOffsetPartLines to Helper.cs** (immediately after)
-
-Same pipeline but offsets shapes before converting to polygon:
-
-```csharp
-public static List GetOffsetPartLines(Part part, double spacing)
-{
- var entities = Converters.ConvertProgram.ToGeometry(part.Program);
- var shapes = GetShapes(entities.Where(e => e.Layer != Geometry.SpecialLayers.Rapid));
- var lines = new List();
-
- foreach (var shape in shapes)
- {
- var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
-
- if (offsetEntity == null)
- continue;
-
- var polygon = offsetEntity.ToPolygon();
- polygon.Offset(part.Location);
- lines.AddRange(polygon.ToLines());
- }
-
- return lines;
-}
-```
-
-**Step 3: Build to verify**
-
-Run: `msbuild OpenNest.sln /p:Configuration=Release`
-Expected: Build succeeds.
-
-**Step 4: Commit**
-
-```
-feat: add Helper.GetPartLines and GetOffsetPartLines
-```
-
----
-
-### Task 3: Add Helper.DirectionalDistance — core algorithm
-
-**Files:**
-- Modify: `OpenNest.Core\Helper.cs`
-
-This is the main algorithm. For two sets of line segments and a push direction, compute the minimum translation distance before any contact.
-
-**Step 1: Add RayEdgeDistance helper**
-
-For a vertex moving along an axis, find where it hits a line segment:
-
-```csharp
-///
-/// Finds the distance from a vertex to a line segment along a push axis.
-/// Returns double.MaxValue if the ray does not hit the segment.
-///
-private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
-{
- var p1 = edge.StartPoint;
- var p2 = edge.EndPoint;
-
- switch (direction)
- {
- case PushDirection.Left:
- {
- // Ray goes in -X direction. Edge must have a horizontal span.
- if (p1.Y.IsEqualTo(p2.Y))
- return double.MaxValue; // horizontal edge, parallel to ray
-
- // Find t where ray Y == edge Y at parametric t
- var t = (vertex.Y - p1.Y) / (p2.Y - p1.Y);
- if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
- return double.MaxValue;
-
- var ix = p1.X + t * (p2.X - p1.X);
- var dist = vertex.X - ix; // positive if edge is to the left
- return dist > Tolerance.Epsilon ? dist : double.MaxValue;
- }
-
- case PushDirection.Right:
- {
- if (p1.Y.IsEqualTo(p2.Y))
- return double.MaxValue;
-
- var t = (vertex.Y - p1.Y) / (p2.Y - p1.Y);
- if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
- return double.MaxValue;
-
- var ix = p1.X + t * (p2.X - p1.X);
- var dist = ix - vertex.X;
- return dist > Tolerance.Epsilon ? dist : double.MaxValue;
- }
-
- case PushDirection.Down:
- {
- if (p1.X.IsEqualTo(p2.X))
- return double.MaxValue;
-
- var t = (vertex.X - p1.X) / (p2.X - p1.X);
- if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
- return double.MaxValue;
-
- var iy = p1.Y + t * (p2.Y - p1.Y);
- var dist = vertex.Y - iy;
- return dist > Tolerance.Epsilon ? dist : double.MaxValue;
- }
-
- case PushDirection.Up:
- {
- if (p1.X.IsEqualTo(p2.X))
- return double.MaxValue;
-
- var t = (vertex.X - p1.X) / (p2.X - p1.X);
- if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
- return double.MaxValue;
-
- var iy = p1.Y + t * (p2.Y - p1.Y);
- var dist = iy - vertex.Y;
- return dist > Tolerance.Epsilon ? dist : double.MaxValue;
- }
-
- default:
- return double.MaxValue;
- }
-}
-```
-
-**Step 2: Add DirectionalDistance method**
-
-```csharp
-///
-/// Computes the minimum translation distance along a push direction before
-/// any edge of movingLines contacts any edge of stationaryLines.
-/// Returns double.MaxValue if no collision path exists.
-///
-public static double DirectionalDistance(List movingLines, List stationaryLines, PushDirection direction)
-{
- var minDist = double.MaxValue;
-
- // Case 1: Each moving vertex → each stationary edge
- for (int i = 0; i < movingLines.Count; i++)
- {
- var movingLine = movingLines[i];
-
- for (int j = 0; j < stationaryLines.Count; j++)
- {
- var d = RayEdgeDistance(movingLine.StartPoint, stationaryLines[j], direction);
- if (d < minDist) minDist = d;
- }
- }
-
- // Case 2: Each stationary vertex → each moving edge (opposite direction)
- var opposite = OppositeDirection(direction);
-
- for (int i = 0; i < stationaryLines.Count; i++)
- {
- var stationaryLine = stationaryLines[i];
-
- for (int j = 0; j < movingLines.Count; j++)
- {
- var d = RayEdgeDistance(stationaryLine.StartPoint, movingLines[j], opposite);
- if (d < minDist) minDist = d;
- }
- }
-
- return minDist;
-}
-
-private static PushDirection OppositeDirection(PushDirection direction)
-{
- switch (direction)
- {
- case PushDirection.Left: return PushDirection.Right;
- case PushDirection.Right: return PushDirection.Left;
- case PushDirection.Up: return PushDirection.Down;
- case PushDirection.Down: return PushDirection.Up;
- default: return direction;
- }
-}
-```
-
-**Step 3: Build to verify**
-
-Run: `msbuild OpenNest.sln /p:Configuration=Release`
-Expected: Build succeeds.
-
-**Step 4: Commit**
-
-```
-feat: add Helper.DirectionalDistance for polygon-based push
-```
-
----
-
-### Task 4: Rewrite PlateView.PushSelected to use geometry-based distance
-
-**Files:**
-- Modify: `OpenNest\Controls\PlateView.cs:753-839`
-
-**Step 1: Add `using OpenNest.Converters;`** at the top if not already present.
-
-**Step 2: Replace the PushSelected method body**
-
-Replace the entire `PushSelected` method (lines 753-839) with:
-
-```csharp
-public void PushSelected(PushDirection direction)
-{
- // Build line segments for all stationary parts.
- var stationaryParts = parts.Where(p => !p.IsSelected && !SelectedParts.Contains(p)).ToList();
- var stationaryLines = new List>(stationaryParts.Count);
- var stationaryBoxes = new List(stationaryParts.Count);
-
- foreach (var part in stationaryParts)
- {
- stationaryLines.Add(Helper.GetPartLines(part.BasePart));
- stationaryBoxes.Add(part.BoundingBox);
- }
-
- var workArea = Plate.WorkArea();
- var distance = double.MaxValue;
-
- foreach (var selected in SelectedParts)
- {
- // Get offset lines for the moving part.
- var movingLines = Plate.PartSpacing > 0
- ? Helper.GetOffsetPartLines(selected.BasePart, Plate.PartSpacing)
- : Helper.GetPartLines(selected.BasePart);
-
- var movingBox = selected.BoundingBox;
-
- // Check geometry distance against each stationary part.
- for (int i = 0; i < stationaryLines.Count; i++)
- {
- // Early-out: skip if bounding boxes don't overlap on the perpendicular axis.
- var stBox = stationaryBoxes[i];
- bool perpOverlap;
-
- switch (direction)
- {
- case PushDirection.Left:
- case PushDirection.Right:
- perpOverlap = !(movingBox.Bottom >= stBox.Top || movingBox.Top <= stBox.Bottom);
- break;
- default: // Up, Down
- perpOverlap = !(movingBox.Left >= stBox.Right || movingBox.Right <= stBox.Left);
- break;
- }
-
- if (!perpOverlap)
- continue;
-
- var d = Helper.DirectionalDistance(movingLines, stationaryLines[i], direction);
- if (d < distance)
- distance = d;
- }
-
- // Check distance to plate edge (actual geometry bbox, not offset).
- double edgeDist;
- switch (direction)
- {
- case PushDirection.Left:
- edgeDist = selected.Left - workArea.Left;
- break;
- case PushDirection.Right:
- edgeDist = workArea.Right - selected.Right;
- break;
- case PushDirection.Up:
- edgeDist = workArea.Top - selected.Top;
- break;
- default: // Down
- edgeDist = selected.Bottom - workArea.Bottom;
- break;
- }
-
- if (edgeDist > 0 && edgeDist < distance)
- distance = edgeDist;
- }
-
- if (distance < double.MaxValue && distance > 0)
- {
- var offset = new Vector();
-
- switch (direction)
- {
- case PushDirection.Left: offset.X = -distance; break;
- case PushDirection.Right: offset.X = distance; break;
- case PushDirection.Up: offset.Y = distance; break;
- case PushDirection.Down: offset.Y = -distance; break;
- }
-
- SelectedParts.ForEach(p => p.Offset(offset));
- Invalidate();
- }
-}
-```
-
-**Step 3: Build to verify**
-
-Run: `msbuild OpenNest.sln /p:Configuration=Release`
-Expected: Build succeeds.
-
-**Step 4: Manual test**
-
-1. Open a nest with at least two irregular parts on a plate
-2. Select one part, press X to push left — it should slide until its offset geometry touches the other part's cut geometry
-3. Press Shift+X to push right
-4. Press Y to push down, Shift+Y to push up
-5. Verify parts nestle closer than before (no bounding box gap)
-6. Verify parts stop at plate edges correctly
-
-**Step 5: Commit**
-
-```
-feat: rewrite PushSelected to use polygon directional-distance
-
-Parts now push based on actual cut geometry instead of bounding boxes,
-allowing irregular shapes to nestle together with PartSpacing gap.
-```
-
----
-
-### Task 5: Clean up — remove PushDirection from OpenNest.csproj if not already done
-
-**Files:**
-- Modify: `OpenNest\OpenNest.csproj` — remove ``
-- Delete: `OpenNest\PushDirection.cs` (if not already deleted in Task 1)
-
-**Step 1: Verify build is clean**
-
-Run: `msbuild OpenNest.sln /p:Configuration=Release`
-Expected: Build succeeds with zero warnings related to PushDirection.
-
-**Step 2: Commit**
-
-```
-chore: clean up PushDirection move
-```