diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,21 @@
 # Changelog
 
+This is release chronology, not current interface authority. The package
+[README](./README.md), Cabal manifest, and linked focused guides own the live
+surface.
+
 `moonlight-triangulation` follows the
 [Haskell Package Versioning Policy](https://pvp.haskell.org).
 
 The serialization format carries its own version tag, independent of the package
 version; any change to it is recorded here explicitly.
+
+## 1.4.0.2 - 2026-08-28
+
+* Consolidate usage guidance behind one operation index and exact set-operation
+  type contract. Focused guides now uniquely own publication schedules,
+  examples, comparisons, exhibits, and foreign-consumer details instead of
+  repeating them across the package overview.
 
 ## 1.4.0.1 - 2026-08-24
 
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2026 agent, Blue Rose
+Copyright (c) 2026 Blue Rose
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -14,323 +14,62 @@
 regions and labelled overlay, intrinsic valuations, polygonal Minkowski
 morphology, the Voronoi dual, natural-neighbour interpolation, Ruppert
 refinement, walk point location, convex hull, exact Shewchuk predicates,
-incremental insertion and removal, and versioned binary serialization.
+and versioned binary serialization.
 
 ## Persistence rose
 
-![Animated Moonlight persistence rose: exact Delaunay alpha filtration, persistent homology, Voronoi dual, and natural-neighbour field](./docs/social/moonlight-triangulation-persistence-rose.gif)
-
-[Open the animation directly.](./docs/social/moonlight-triangulation-persistence-rose.gif)
-· [Open the static vector poster.](./docs/moonlight-triangulation-persistence-rose.svg)
-· [Browse the exhibit files.](./docs/README.md)
-
-This is computed, not ornament. A deterministic 169-site cover becomes one
-Delaunay mesh, one exact 626-value alpha filtration, and one Homology reduction.
-The poster state has `β₁ = 7`; seven bounded components recovered
-independently by face descent and boundary gluing agree with that persistent
-Betti number. The colour field is 447 fallback-free Sibson natural-neighbour
-queries, the fine cyan geometry is the finite Voronoi dual, and the flower
-silhouette is seven exact Minkowski-offset petals glued by labelled planar
-overlay.
-
-On GHC 9.14, the package-owned generator reproduces the poster, the exact alpha
-eclipse, or the canonical 65-frame seamless SVG sweep:
-
-```console
-cabal run moonlight-triangulation-persistence-rose -- poster persistence-rose.svg
-cabal run moonlight-triangulation-persistence-rose -- eclipse alpha-eclipse.svg
-cabal run moonlight-triangulation-persistence-rose -- frames 65 persistence-rose-frames
-```
-
-The retained GIF is the 960×540, 8 fps projection of those 65 source frames.
-Its exact encoding boundary is deliberately outside geometry and Homology:
-rasterize the SVG section, encode it, then let the exhibit owner stamp the GIF
-with the deterministic source-frame fingerprint it computed itself.
-
-```console
-mkdir -p persistence-rose-png
-sips -s format png persistence-rose-frames/frame-*.svg --out persistence-rose-png
-ffmpeg -framerate 8 -start_number 0 -i persistence-rose-png/frame-%04d.png \
-  -vf "scale=960:-2:flags=lanczos,palettegen=max_colors=96:stats_mode=diff" \
-  -y persistence-rose-palette.png
-ffmpeg -framerate 8 -start_number 0 -i persistence-rose-png/frame-%04d.png \
-  -i persistence-rose-palette.png \
-  -lavfi "[0:v]scale=960:-2:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \
-  -loop 0 -y persistence-rose-raw.gif
-cabal run moonlight-triangulation-persistence-rose -- \
-  stamp-gif persistence-rose-raw.gif docs/social/moonlight-triangulation-persistence-rose.gif
-cabal run moonlight-triangulation-persistence-rose -- verify-artifacts .
-```
+![Animated Moonlight persistence rose: exact Delaunay alpha filtration, persistent homology, Voronoi dual, and natural-neighbour field](./docs/persistence-rose/moonlight-triangulation-persistence-rose.gif)
 
-`verify-artifacts` compares both retained SVGs byte-for-byte with their pure
-renderers, checks the GIF dimensions, timing, and image-frame inventory, and requires
-the GIF's embedded source fingerprint to equal the freshly rendered canonical
-frame section. A plausible binary can no longer wander in wearing a nice hat.
+[Open the animation directly.](./docs/persistence-rose/moonlight-triangulation-persistence-rose.gif)
+· [Open the static vector poster.](./docs/persistence-rose/moonlight-triangulation-persistence-rose.svg)
+· [Read the exhibit guide.](./docs/persistence-rose/README.md)
 
 ## Operations
 
-| Operation | Role | What it returns |
-| --- | --- | --- |
-| `union` | join | A valid Delaunay representative of both site sets; overlapping annotations glue through `JoinSemilattice`. |
-| `unions` | balanced fold | The same join over a list, associated for the tournament rather than the left. |
-| `siteRelation` | support order | Exact equality, proper-subset, disjointness or partial-overlap classification. |
-| `intersection` | geometry-only meet | The sites both meshes hold. |
-| `intersectionWith` | annotated meet | Shared sites with a left-then-right annotation combiner. |
-| `difference` | relative complement | The left's sites and annotations, less the right's support. |
-| `symmetricDifference` | exclusive or | The sites and annotations carried by exactly one mesh. |
-| `canonicalize` | physical normal form | Construction-independent dense numbering, derived explicitly when required. |
-| `refine` | quality | Steiner insertion, composed *after* any of the above — never a second kind of mesh. |
-| `constrainedDelaunay` | boundary | Sites plus segments, after which interiority is computable. |
-| `overlayLayers` | exact common refinement | One provenance-bearing arrangement whose bounded cells carry both source labels. |
-| `overlayClosedUnion` / `overlayClosedIntersection` / `overlayRegularizedDifference` | planar Boolean selection | A closed exact cell set retaining point- and edge-only results; difference is regularized from selected faces. |
-| `overlaySelectedRegion` | polygon publication | Selected bounded faces with internal arrangement edges dissolved through the existing boundary walker. |
-| `cellValuations` / `regionValuations` | intrinsic measurement | Exact Euler characteristic and rational area plus symbolic radical length with certified bounds. |
-| `minkowskiSum` | polygonal convolution | Exact convex or general polygonal Minkowski addition, with work recorded in a receipt. |
-| `erodeBy` / `openWith` / `closeWith` | regularized morphology | Full-dimensional polygonal erosion and its opening/closing compositions for an origin-anchored convex kernel. |
-| `fromExactCellSet` | topology interpretation | An opaque `DCELComplex` view of the already-admitted cell inventory; no mesh copy or second validation authority. |
-| `alphaFiltration` | exact scale geometry | One handle-indexed birth section for every resident vertex, edge, and bounded face of a Delaunay triangulation. |
-| `filteredAlphaComplex` | persistent topology | The same resident cells and exact births lowered into Homology's checked filtered chain complex. |
-
-### Choose the operation
-
-| What you need | Use | Do not substitute |
-| --- | --- | --- |
-| Build a geometry-only mesh from coordinates | `delaunayGeometry` | Do not manufacture unit annotations and erase them afterward. |
-| Build from separate coordinates and annotations | `delaunayFromCoordinates` | Do not fabricate a `HasPosition` instance for an annotation that does not own geometry. |
-| Combine two unconstrained meshes | `union` | Do not concatenate vertices and rebuild manually; `union` owns overlap annotations and the measured schedule. |
-| Combine many unconstrained meshes | `unions` | Do not left-fold `union`; `unions` owns balanced association. |
-| Classify coordinate support without constructing a mesh | `siteRelation` | Do not compare vertex counts or resident `Eq`; neither answers support order. |
-| Keep coordinates present in both geometry-only meshes | `intersection` | Use `intersectionWith` instead when annotations must survive or be recomputed. |
-| Keep shared coordinates and choose their annotation | `intersectionWith combine` | For a left annotation-preserving mask, use `intersectionWith const left mask`. |
-| Remove a coordinate mask from a mesh | `difference source mask` | Do not remove by stale `VertexId`; removal compacts arenas while coordinates remain stable. |
-| Keep coordinates present in exactly one mesh | `symmetricDifference` | Do not spell it as two differences plus `union`; the direct operation owns the persistent toggle schedule. |
-| Build the first constrained mesh | `constrainedDelaunay` | Do not build unconstrained and treat a rendered outline as topology. |
-| Canonically combine arbitrary constrained meshes | `unionConstrainedWith` or geometry-only `unionConstrained` | Do not use unconstrained `union`; constrained union owns complete conflict witnesses and constraint recovery. |
-| Join two strictly x-separated constrained meshes | `joinSeparatedConstrained` | Separation means annotations are copied, never combined; use the general constrained union when separation is not proved. |
-| Append one constrained section to an authoritative constrained base | `extendConstrainedWith` | Do not use symmetric constrained union when base identity and already-solved constraints must remain resident. |
-| Insert one site persistently | `BulkLoad.insert` or `BulkLoad.insertAt` | Do not open a manual transaction; the singleton entry owns the dense/copy-on-write crossover. |
-| Insert a vector of sites | `BulkLoad.insertMany` | Do not fold singleton insertion; one batch thaws and publishes once. |
-| Compose insertions and removals | one `Session.withSession` | Do not publish every intermediate mesh. Use coordinate-keyed removal after the first compaction. |
-| Refine the whole mesh | `refine` | Do not manufacture a domain witness merely to reach the local API. |
-| Refine a proved face section without changing protected faces | `refineWithinDomain` | Supply its exact permitted faces; the immutable interface is derived once by the domain owner and returned as receipt evidence. |
-| Partition bounded faces by any caller-owned label | `faceComponents mesh labelFace` | Do not rebuild adjacency outside the DCEL or collapse disconnected regions sharing a label. |
-| Recover one component's polygonal boundary | `componentBoundary mesh component` | Do not emit one polygon per triangle; outer and hole loops have canonical winding and exact collinear simplification. |
-| Combine two labelled polygon layers | `overlayLayers left right` | Do not run separate clipping engines for union, intersection, and difference; select cells from the one common refinement. |
-| Preserve a point- or edge-only Boolean result | `overlayClosedUnion`, `overlayClosedIntersection`, or `overlayRegularizedDifference` | Do not publish immediately as `PlanarRegion`; that carrier is deliberately full-dimensional. |
-| Publish a selected polygonal region | `overlaySelectedRegion predicate overlay` | Do not collect arrangement cells independently; selection must precede boundary descent so internal seams disappear. |
-| Measure a closed exact selection or region | `cellValuations` or `regionValuations` | Do not coerce irrational perimeter to an allegedly exact `Double`; inspect its radical expression and certified interval. |
-| Expand a polygon by another polygon | `minkowskiSum` or `polygonOffset` | Do not add resident mesh vertices pairwise and rebuild; morphology acts on the represented continuum. |
-| Erode, open, or close a polygonal region | `erodeBy`, `openWith`, or `closeWith` | Do not infer a hidden universe or invent lower-dimensional polygons; the result is regularized to representable 2D area. |
-| Render only Delaunay faces below one alpha threshold | `alphaShapeContainsFace threshold mesh` | This constant-work-per-face projection deliberately does not build the full filtration. |
-| Ask topology questions across many radii | `alphaFiltration mesh`, then `filteredAlphaComplex` and `mod2PersistentPairs` | Do not rebuild disc unions, triangulations, or homology independently at every radius. |
-| Require construction-independent numbering | `canonicalize` at the observation boundary | Do not canonicalize every intermediate value; it is intentionally global work. |
-
-### Cell-complex and category interpretation
-
-The `cell-complex` component interprets an `ExactCellSet` as
-Homology's generic `CellComplex2D`. The carrier is opaque and retains the
-authoritative selected handles and resident DCEL; `fromExactCellSet` is total
-because `ExactCellSet` construction has already discharged downward closure and
-handle validity.
-
-The interpretation is available on GHC 9.14 and newer. Existing Triangulation
-components retain the package's GHC 9.8–9.14 support range: below 9.14 Cabal
-marks only `cell-complex`, its focused test, and the category-observatory
-exporter unbuildable. Hackage aggregates dependencies from every component on
-the package page, but an ordinary main-library consumer does not inherit the
-Homology or Category closure.
-
-```cabal
--- the consumer package
-build-depends:
-  moonlight-triangulation:cell-complex >= 1.4 && < 1.5,
-  moonlight-homology:cell-category >= 0.1 && < 0.2
-```
-
-```haskell
-import Moonlight.Homology.Pure.Topology.CellCategory (complexCategory)
-import Moonlight.Triangulation.CellComplex (fromExactCellSet)
-
-incidenceCategory = complexCategory . fromExactCellSet
-```
-
-`moonlight-triangulation-category-observatory-export` is the executable witness:
-it constructs a real Delaunay mesh, selects its bounded cells, derives the finite
-incidence category and normalized nerve, validates their overlap counts, and emits
-only the checked visualization projection.
-
-### Exact alpha filtration and persistent topology
-
-![Alpha eclipse: a fixed Delaunay site set closes one hole and later fills it across exact critical radii](./docs/moonlight-triangulation-alpha-eclipse.svg)
-
-[Open the full-size alpha eclipse.](./docs/moonlight-triangulation-alpha-eclipse.svg)
-
-The eclipse uses the package's canonical four-site square witness and isolates
-its one finite `H₁` interval. The four sites, five resident Delaunay edges, and
-two resident faces remain fixed: the four hull edges close one hole at exact
-`α² = 1`; the resident diagonal and both resident faces enter at exact
-`α² = 2`, making that cycle a boundary. Only the alpha sublevel changes. Every
-visible cell is projected from the resident DCEL, and the `β₁ = 0 → 1 → 0`
-profile comes from Moonlight Homology rather than a hand-authored diagram.
-
-The full alpha path is a different observation of the same resident Delaunay
-DCEL. `alphaFiltration` computes exact squared-radius births once: vertices at
-zero, Gabriel edges at one quarter of their squared length, other edges at their
-least incident-face witness, and faces at their circumradius squared.
-`filteredAlphaComplex` lowers that section to the one Homology persistence
-owner. `AlphaBirth` is an opaque reduced rational; equality and ordering never
-round through `Double`.
-
-This is the planar alpha-complex shortcut to the Cech filtration: the Delaunay
-subcomplex has the same homotopy type as the corresponding union of discs, but
-has only the resident planar cells. One reduction therefore yields the barcode
-for every critical radius. `persistentBettiAtCriticalValues` sweeps all of
-those sublevels through dense complex-relative ranks without rebuilding
-geometry, reducing another boundary matrix, or rescanning the barcode per
-radius.
-
-The minimal component path names only Delaunay construction, the DCEL alpha
-owner, the opt-in lowering component, and Homology. A consumer already using
-the broad `moonlight-triangulation` facade may substitute it for `build` and
-`dcel` below.
-
-```cabal
-build-depends:
-  moonlight-triangulation:build >= 1.4 && < 1.5,
-  moonlight-triangulation:dcel >= 1.4 && < 1.5,
-  moonlight-triangulation:cell-complex >= 1.4 && < 1.5,
-  moonlight-homology >= 0.1 && < 0.2
-```
-
-```haskell
-import Data.Foldable (traverse_)
-import qualified Data.Vector as Vector
-import Moonlight.Homology.Persistence
-  ( filteredCriticalValues
-  , mod2PersistentPairs
-  , persistentBettiAtCriticalValues
-  )
-import Moonlight.Triangulation.Alpha (alphaFiltration)
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.CellComplex (filteredAlphaComplex)
-import Moonlight.Triangulation.Types (Point (..))
-
-main :: IO ()
-main = do
-  mesh <- requireRight (delaunayGeometry (Vector.fromList [Point (-1) (-1), Point 1 (-1), Point 1 1, Point (-1) 1]))
-  filtration <- requireRight (alphaFiltration mesh)
-  filtered <- requireRight (filteredAlphaComplex filtration)
-  pairs <- requireRight (mod2PersistentPairs filtered)
-  profiles <- requireRight (persistentBettiAtCriticalValues filtered pairs)
-  traverse_ print (zip (filteredCriticalValues filtered) profiles)
-
-requireRight :: Show failure => Either failure value -> IO value
-requireRight = either (fail . show) pure
-```
+| Operation | Use when | Inputs | Result |
+| --- | --- | --- | --- |
+| `delaunayGeometry` | Coordinates own the input | `Vector Point` | Geometry-only mesh / `BuildError` |
+| `delaunayFromCoordinates` | Payloads have a separate plane | Defaults, points, payloads, duplicate policy | `BuildResult` / `BuildError` |
+| `siteRelation` | Supports need exact classification | Two meshes | `SiteRelation` |
+| `union` / `unions` | Unconstrained supports must join | Two meshes / mesh list | Union mesh / `BuildError` |
+| `intersection` / `intersectionWith` | Shared support is required | Two meshes; optional payload combiner | Common-site mesh / `BuildError` |
+| `difference` / `symmetricDifference` | Left-only / exclusive support is required | Source and mask / two meshes | Result mesh / `BuildError` |
+| `constrainedDelaunay` | Segments author topology | Defaults, positioned vertices, index pairs | `BuildResult` / `CdtError` |
+| `unionConstrained` / `unionConstrainedWith` | Arbitrary constrained meshes must join | Two meshes; optional payload combiner | Constrained mesh / `ConstrainedUnionError` |
+| `extendConstrainedWith` | One constrained mesh owns identity | Combiner, base, extension | `ConstrainedExtensionResult` / `ConstrainedUnionError` |
+| `joinSeparatedConstrained` | Inputs are strictly x-separated | Face predicate, refinement parameters, two meshes | `ConstrainedSeamResult` / `ConstrainedUnionError` |
+| `refine` | Quality applies globally | Vertex constructor, parameters, mesh | `RefinementResult` / `BuildError` |
+| `refineWithinDomain` | Quality applies to admitted faces | Vertex constructor, parameters, face set, mesh | `RefinementDomainResult` / `BuildError` |
+| `faceComponents` | Bounded faces must descend by label | Mesh, `FaceId -> label` | `[(label, FaceComponent)]` |
+| `componentBoundary` | One component must publish as loops | Mesh, `FaceComponent` | `RegionBoundary` / `BoundaryObstruction` |
+| `overlayLayers` | Layers need one exact arrangement | Two `PlanarLayer`s | `OverlayResult` / `OverlayError` |
+| `overlayClosedUnion` / `overlayClosedIntersection` / `overlayRegularizedDifference` | Boolean output may retain lower cells | Two label predicates, `OverlayResult` | `ExactCellSet` / `OverlaySelectionError` |
+| `overlaySelectedRegion` | Selected faces must publish as polygons | Label-pair predicate, `OverlayResult` | `PlanarRegion` / `RegionPublicationError` |
+| `cellValuations` / `regionValuations` | Intrinsic measures are required | `ExactCellSet` / `PlanarRegion` | `PlanarValuations` / `ValuationError` |
+| `minkowskiSum` / `polygonOffset` | Regions must add / expand | Two regions / element and region | Region plus receipt / `MinkowskiError` |
+| `erodeBy` / `openWith` / `closeWith` | Regularized morphology is required | Structuring element, region | Region plus receipt / `MinkowskiError` |
+| `alphaShapeContainsFace` | One face at one radius is enough | `RadiusSquared`, mesh, `FaceId` | `Bool` |
+| `alphaFiltration` | Every critical radius matters | Delaunay mesh | `AlphaFiltration` / `AlphaFiltrationError` |
+| `fromExactCellSet` | Exact cells need a generic complex view | `ExactCellSet` | `DCELComplex` |
+| `filteredAlphaComplex` | Alpha births must enter persistence | `AlphaFiltration` | Filtered chain complex / `DCELError` |
+| `canonicalize` | Numbering must ignore construction history | Mesh with unit edge/face payloads | Canonical mesh / `BuildError` |
 
-The focused `moonlight-triangulation-alpha-bench` measures exact preparation,
-one persistence reduction plus all critical-radius queries, and repeated
-threshold homology as separate lanes over the same prebuilt Delaunay mesh.
+[Interpret cell sets as incidence categories.](docs/category-observatory/README.md#cell-complex-and-category-interpretation)
+· [Compute persistent alpha topology.](docs/persistence-rose/README.md#exact-alpha-filtration-and-persistent-topology)
 
 ## Foreign bindings
 
-The `moonlight-triangulation-c` foreign-library component exposes opaque
-immutable geometry meshes, admitted exact planar regions, and reusable
-polygonal structuring elements through ABI version 2. Mesh `site_*` operations
-combine Delaunay sites; region operations perform actual polygonal Boolean
-selection through the exact overlay owner. Regions cross in one bulk call as
-coordinates plus loop/component counts and project back as components, loops,
-and binary64 rendering points. Exact area remains lossless as a reduced ratio;
-Euler is integral and perimeter carries certified bounds. Python, TypeScript,
-and Rust bindings live beneath [`bindings/`](bindings/README.md) and descend
-through that same ABI; none restates overlay, publication, valuation, or
-morphology.
-
-For the Haskell mesh algebra, `union a a` is `a`; commutativity and associativity hold after explicit
-`canonicalize`; and a join adds no sites: the result carries
-`|A| + |B| − |A ∩ B|` of them. Structural `Eq` remains exact resident equality
-for caches and serialization rather than secretly rebuilding the mesh.
-
-### Persistent publication schedules
-
-The algebraic result is independent of the execution schedule. These are the
-currently measured publication choices; `canonicalize` remains the explicit
-global observation when construction-independent numbering is required.
-
-| Operation context | Publication schedule |
-| --- | --- |
-| `siteRelation left right` | Index the smaller support in one transient exact open-addressed section, scan the other operand, then discard the index; no support maps or published cache state. |
-| `difference mesh empty`, `symmetricDifference mesh empty`, `symmetricDifference empty mesh` | Return the surviving representative verbatim. |
-| `difference left right` with `size right <= (size left - size right) / 128` | Remove the right support through one local copy-on-write session; return `left` verbatim when the supports are disjoint. Larger masks rebuild. |
-| Geometry-only `intersection left right` | Return an existing operand for equality or subset, return the empty mesh for disjoint supports, and locally remove the smaller complement when it is at most `overlap / 128`. Other partial overlaps rebuild. |
-| `intersectionWith` | Rebuild, because the annotation combiner may rewrite every surviving payload even when topology changes locally. |
-| `symmetricDifference left right` with `small <= (large - small) / 128` | Toggle the smaller operand through one local session. Comparable operands and small-output, large-input cases rebuild. |
-| Comparable `symmetricDifference left right` | Partition through one transient exact index and a matched bitset, then rebuild only the exclusive output section. |
-| `BulkLoad.insert` / `BulkLoad.insertAt` | Dense publication below 10,000 resident sites; copy-on-write publication at 10,000 and above. A sequence still belongs in one `Session`. |
-| `extendConstrainedWith` | Copy-on-write only for a base of at least 200,000 sites, at most 128 incoming sites, exactly one incoming segment, and a pre-thaw corridor with no resident intersection. Every unmeasured or resident-corridor case stays dense. |
-| `refineWithinDomain` | Dense publication. The local transaction candidate preserved semantics but did not improve wall time, so it was removed. |
-
-### Overall Moonlight–Spade comparison
-
-The overall referent is broader than the construction board below. Its live
-inventory contains 17 parity lanes and eight diagnostic cliff lanes. Every
-timing descends through its declared artifact gate first: cross-language
-canonical equality where the result is unique, and pinned per-side artifacts
-where valid refinement or floating-point accumulation may differ.
-
-| Surface | Parity workloads |
-| --- | --- |
-| Construction | bulk load at 1k and 10k sites; incremental insertion at 1k, 10k, and 500k sites |
-| Search and dual | 5k nearest-neighbour queries over 20k sites; Voronoi dual sweep over 1k sites; DCEL traversal over 2k sites |
-| Constraints | recover 800 constraints over 8k sites; refine with 625 and 2,500 Steiner-site budgets |
-| Persistent update | remove 250 / 1k, 2.5k / 10k, and 25k / 100k sites |
-| Natural-neighbour interpolation | 1k queries over 10k sites; 2k queries over 100k sites |
-| Segment traversal | 500 line intersections over 10k sites |
-
-The eight diagnostic lanes cover one-at-a-time constraint recovery, constraint
-splitting, hierarchy insertion, duplicate insertion, hierarchy removal,
-single-angle sweep collapse, exactly collinear construction with face-less
-location, and line intersection from outside the hull. They expose known
-complexity cliffs; they are not omitted merely because the numbers are ugly.
-
-The package-owned [operation suite](bench/spade-compare/README.md) documents the
-complete inventory, semantic descent, timing protocol, and evidence rules. Its
-`scorecard.sh` entry point consumes the closed Haskell lane registry directly;
-`list` exposes the operation surface, while only a complete, attributable
-`refresh` may create a retained performance board.
-
-```console
-compiler/foundation/moonlight-triangulation/bench/spade-compare/scorecard.sh list
-compiler/foundation/moonlight-triangulation/bench/spade-compare/scorecard.sh check
-```
-
-### Package-owned Delaunay construction board
-
-The Haskell-owned port of Spade's construction matrix lives with the
-triangulation library at [`bench/delaunay-compare`](bench/delaunay-compare/README.md).
-It appends Moonlight to Spade, Spade hierarchy, `cdt`, and `delaunator`, checks
-all 24 fixture summaries across all five implementations before timing, and
-derives both pictures from the retained 120-case CSV.
-
-![Moonlight Delaunay construction over the small Spade fixtures](https://raw.githubusercontent.com/PaleRoses/moonlight/moonlight-triangulation-1.4.0.1/moonlight-triangulation/bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg)
-
-The [benchmark README](bench/delaunay-compare/README.md) owns the single
-retained board, its result, receipt, big-point picture, and reproduction
-procedure. This package overview does not maintain a second benchmark history.
+The C ABI and its Python, TypeScript, and Rust consumers are documented in the
+[foreign-bindings guide](ffi/bindings/README.md). It owns construction, ABI
+versioning, lifecycle, obstruction semantics, and consumer examples.
 
-Spade exposes no public triangulation set algebra corresponding to these
-operations. Their independently witnessed Moonlight publication costs are:
+## Algebraic contract
 
-| Operation | Workload | Moonlight |
-| --- | ---: | ---: |
-| `siteRelation` | 1m sites vs 995k retained | **96.759 ms** |
-| `intersection` | retain 995k / 1m sites | **109.513 ms** |
-| `symmetricDifference` | publish the 5k-site exclusive result | **119.298 ms** |
-| `difference` | remove 5k / 1m sites | **129.820 ms** |
-| `union` | add 5k / 5m sites | **130.295 ms** |
+`union` is idempotent; commutativity and associativity hold after
+`canonicalize`. Structural `Eq` is resident equality; use `siteRelation` for
+support comparison.
 
-These are raw publication measurements. `canonicalize` remains a separate,
-explicit observation when construction-independent numbering is required.
+### Set-operation types
 
 ```haskell
 union :: JoinSemilattice annotation
@@ -369,401 +108,46 @@
       -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
 ```
 
-## Constraints
-
-* No `Semigroup`, no `Monoid`, no `<>`. A join the finite arena cannot
-  represent is a `Left`; a partial class instance would lie about totality.
-* `union` and `unions` glue overlapping annotations through
-  `JoinSemilattice`. `intersectionWith` supplies the corresponding explicit
-  overlap combiner. `difference` preserves the left annotation and
-  `symmetricDifference` preserves the annotation of whichever exclusive site
-  survives. Plain `intersection` remains the geometry-only specialization.
-  Annotation-preserving restriction is derived rather than stored as a second
-  operator: `intersectionWith const left mask`.
-* `refine` composes after an operation. It is not a mode, a flag, or a second
-  triangulation type.
-* Mutation lives in `ST` behind `Moonlight.Triangulation.Internal.Mutable` and
-  never escapes.
-* `serialize` is absent from the facade by design; import
-  `Moonlight.Triangulation.Serialization` to reach it. Its canonical decoder
-  requires both a `DecodingBudget` and explicit `TrustedPayloadDecoders`; wire
-  format 6 validates one structural count prefix before decoding defaults,
-  payloads, or allocating a section, and preserves the vertex payload plane's
-  optional fill. The budget covers the input envelope and every module-owned
-  container. `trustedBinaryPayloadDecoders` records the separate caller
-  decision that the selected executable `Binary` decoders have an acceptable
-  internal resource policy. Version 5 is intentionally unsupported.
-* Resident triangulation coordinates are binary64. Exact planar-region APIs use
-  normalized rational `ExactPoint`s; an admitted `OverlayResult` seals those
-  authoritative coordinates beside one certified binary64 DCEL projection.
-  `vertexPoints` and `innerFaceVertexTriples` still project the resident DCEL
-  directly; callers do not reconstruct a sibling mesh DTO.
-* One half-edge mesh underneath. `Cdt` is a mode index on it, not a second
-  structure.
-
-The Haskell facade is the authority for exact region, overlay, valuation, and
-morphology. `ExactCellSet` preserves closed point, edge, and face selections;
-`PlanarRegion` is its full-dimensional polygonal publication. ABI version 2
-retains that admitted region behind an opaque handle; foreign binary64 points
-are authoring/rendering boundaries, not a competing exact representation.
+[Publication schedules and scale behavior.](docs/mesh-publication.md)
 
 ## Use
 
-A triangulation is a value of its site set: construction returns `Either` with
-typed obstructions, `union` returns the same typed refusal when the finite arena
-cannot represent its result, and mesh quality is a
-composition over the result rather than a second operation. This program
-compiles against the facade alone; `fromList` is `GHC.Exts`, so nothing beyond
-`base` and `moonlight-triangulation` is in scope.
-
-```haskell
-module Main where
-
-import GHC.Exts (fromList)
-import Moonlight.Triangulation
-
-main :: IO ()
-main = do
-  let build :: [(Double, Double)] -> IO (DelaunayTriangulation ())
-      build coords =
-        either (fail . show) pure (delaunayGeometry (fromList [Point x y | (x, y) <- coords]))
-  a <- build [(x, y) | x <- [0 .. 9], y <- [0 .. 9]]
-  b <- build [(x + 6, y) | x <- [0 .. 9], y <- [0 .. 9]]
-
-  -- valid Delaunay representative of the union of sites
-  joined <- either (fail . show) pure (union a b)
-
-  -- meet and differences return typed obstructions, never partial results
-  met <- either (fail . show) pure (intersection a b)
-
-  -- mesh quality is a composition, not a second operation
-  parameters <- either (fail . show) pure (withMinimumAngle 14 defaultRefinementParameters)
-  result <- either (fail . show) pure (refine (const ()) parameters joined)
-  let healed = refinedTriangulation result
-
-  print (numVertices joined, numVertices met, numVertices healed, refinementComplete result)
-```
-
-Output: `(160,40,160,True)` — the union carries 160 sites, the meet 40, and
-refinement inserts nothing because every triangle already clears the 14° bar;
-`refinementComplete` states that sufficiency. Where operands leave concavities,
-the same composition places Steiner sites exactly at the slivers it dissolves.
-
-`delaunayGeometry` constructs the unit-annotated carrier directly. Payloads can
-instead enter with their coordinates through `delaunay`, or independently
-through `delaunayFromCoordinates`; annotation-preserving restriction is
-available through `intersectionWith`, `difference`, and `symmetricDifference`. Constraints enter through
-`constrainedDelaunay`; bulk incremental work names the machine-room module
-directly (`Moonlight.Triangulation.BulkLoad` for `insertMany`,
-`Moonlight.Triangulation.Session` for the owned editing transaction —
-`withSession`: thaw once, insert and remove freely, publish once).
-
-### Interior without hull-fill
-
-A point set has no boundary, so `delaunay` necessarily meshes the convex hull:
-concavities and holes are spanned by faces that belong to the hull, not to any
-intended region. The region becomes real the moment its boundary is authored:
-`constrainedDelaunay` takes the sites plus constraint segments as input-index
-pairs, and interiority is then computable — `facesAtEvenBarrierDepth` runs a
-0–1 BFS from the outer face and returns every face at even barrier depth,
-which is exactly the outside (depth 0) plus anything nested behind a second
-loop. The interior is the complement. `refine` consumes the same parity
-through `refineExcludeOuterFaces`, so Steiner sites respect the boundary too.
-
-```haskell
-module Main where
-
-import GHC.Exts (fromList)
-import Moonlight.Triangulation
-
-ring :: Double -> Int -> [(Double, Double)]
-ring radius n =
-  [ (radius * cos t, radius * sin t)
-  | k <- [0 .. n - 1]
-  , let t = 2 * pi * fromIntegral k / fromIntegral n
-  ]
-
-main :: IO ()
-main = do
-  let outer = ring 4 32
-      inner = ring 2 16
-      middle = ring 3 24
-      pts = fromList [Point x y | (x, y) <- outer <> inner <> middle]
-      loop base count = [(base + k, base + (k + 1) `mod` count) | k <- [0 .. count - 1]]
-      constraints = fromList (loop 0 32 <> loop 32 16)
-  annulus <- case constrainedDelaunay unitElementDefaults pts constraints of
-    Left err -> fail (show err)
-    Right result -> pure (buildTriangulation result)
-  let outside =
-        [ fromIntegral raw :: Int
-        | FaceId raw <- facesAtEvenBarrierDepth annulus (isConstraintEdge annulus)
-        ]
-      interior =
-        [ FaceId (fromIntegral k)
-        | k <- [0 .. numFaces annulus - 1]
-        , not (k `elem` outside)
-        ]
-  print (numFaces annulus, length interior)
-```
-
-Output: `(111,97)` — the annulus band is the 97 interior faces; the 14
-excluded faces are the hole's hull-fill and the outer region. Rendering the
-interior list draws the ring with a genuine void: no crop, no edge-length
-heuristic, the engine's own verdict. `faceVertices` walks each interior face
-for display, and a nested loop flips parity again, so islands inside holes
-come back automatically.
-
-### Deriving the boundary
-
-When no boundary is known, classify the Delaunay faces and let the DCEL descend
-them. `faceComponents` evaluates the caller's label once per bounded face and
-keeps disconnected regions distinct even when their labels compare equal.
-`componentBoundary` then returns one counter-clockwise outer loop and clockwise
-hole loops, dropping a boundary vertex only when the library's exact
-orientation predicate proves it collinear and the point lies on the closed
-neighbour segment. A point pinch is a typed `BoundaryPinch`, never an invented
-polygon.
-
-For alpha-shape work the supplied label is simply
-`alphaShapeContainsFace threshold mesh`. `mkRadiusSquared` admits the
-finite non-negative threshold and exact dyadic comparison makes membership
-closed at equality. This is the face, or 2-simplex, filtration; it does not manufacture a second mesh or
-pretend to own palette, colour-space, or rendering policy.
-
-```haskell
-module Main where
-
-import GHC.Exts (fromList)
-import Moonlight.Triangulation
-
-ring :: Double -> Int -> [(Double, Double)]
-ring radius n =
-  [ (radius * cos t, radius * sin t)
-  | k <- [0 .. n - 1]
-  , let t = 2 * pi * fromIntegral k / fromIntegral n
-  ]
-
-main :: IO ()
-main = do
-  let pts = ring 4 32 <> ring 2 16 <> ring 3 24
-  mesh <-
-    either
-      (fail . show)
-      pure
-      (delaunayGeometry (fromList [Point x y | (x, y) <- pts]))
-  threshold <- either (fail . show) pure (mkRadiusSquared 0.4)
-  let components = faceComponents mesh (alphaShapeContainsFace threshold mesh)
-      kept = [(label, component) | (label, component) <- components, label]
-  boundaries <-
-    traverse
-      (either (fail . show) pure . componentBoundary mesh . snd)
-      kept
-  print
-    [ ( length (boundaryLoopVertices (regionBoundaryOuterLoop boundary))
-      , length (regionBoundaryHoleLoops boundary)
-      )
-    | boundary <- boundaries
-    ]
-```
-
-The numeric threshold remains application policy; the circumradius calculation,
-face adjacency, loop extraction, winding, holes, and exact simplification do
-not. The derived boundary walks through data sites, so it is as jagged as the
-sampling; an authored boundary stays smooth at any pitch and wins where the
-generator is known.
-
-### Exact overlay, valuations, and morphology
-
-Author polygonal regions with rational coordinates, refine labelled layers
-once, and select every Boolean from that common arrangement. Closed selection
-retains zero- and one-dimensional intersections; polygon publication dissolves
-all arrangement edges internal to the selected faces.
-
-Valuations read either carrier. Euler characteristic and area remain exact,
-while Euclidean length is a normalized radical expression with certified
-binary64 bounds. Minkowski addition, offset, erosion, opening, and closing then
-compose over the same admitted `PlanarRegion`.
-
-```haskell
-module Main where
-
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Map.Strict as Map
-import Moonlight.Triangulation
-
-admit :: Show obstruction => Either obstruction value -> IO value
-admit = either (fail . show) pure
-
-rectangleLoop :: Integer -> Integer -> Integer -> Integer -> IO ExactLoop
-rectangleLoop minimumX minimumY maximumX maximumY =
-  admit
-    ( exactLoop
-        ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
-            :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
-               , exactPoint (fromInteger maximumX) (fromInteger maximumY)
-               , exactPoint (fromInteger minimumX) (fromInteger maximumY)
-               ]
-        )
-    )
-
-rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
-rectangleRegion minimumX minimumY maximumX maximumY = do
-  outer <- rectangleLoop minimumX minimumY maximumX maximumY
-  component <- admit (polygonComponent outer [])
-  admit (planarRegion [component])
-
-insideLayer :: PlanarRegion -> IO (PlanarLayer Bool)
-insideLayer region = admit (planarLayer False (Map.singleton True region))
-
-cellSummary :: ExactCellSet -> IO (Int, Integer, Integer, CertifiedInterval)
-cellSummary cells = do
-  values <- admit (cellValuations cells)
-  perimeter <- admit (cellSetPerimeter cells)
-  let area = exactAreaValue (valuationArea values)
-  pure
-    ( eulerCharacteristicValue (valuationEuler values)
-    , exactRationalNumerator area
-    , exactRationalDenominator area
-    , exactLengthBounds perimeter
-    )
-
-regionArea :: PlanarRegion -> IO (Integer, Integer)
-regionArea region = do
-  values <- admit (regionValuations region)
-  let area = exactAreaValue (valuationArea values)
-  pure (exactRationalNumerator area, exactRationalDenominator area)
-
-main :: IO ()
-main = do
-  left <- rectangleRegion 0 0 4 4
-  right <- rectangleRegion 2 1 6 3
-  leftLayer <- insideLayer left
-  rightLayer <- insideLayer right
-  overlay <- admit (overlayLayers leftLayer rightLayer)
-
-  unionCells <- admit (overlayClosedUnion id id overlay)
-  intersectionCells <- admit (overlayClosedIntersection id id overlay)
-  differenceCells <- admit (overlayRegularizedDifference id id overlay)
-  traverse cellSummary [unionCells, intersectionCells, differenceCells]
-    >>= print
-
-  kernelLoop <- rectangleLoop (-1) (-1) 1 1
-  kernel <- admit (convexPolygon (exactLoopPoints kernelLoop)) >>= admit . structuringElement
-  (sumRegion, _) <- admit (minkowskiSum left right)
-  (offsetRegion, _) <- admit (polygonOffset kernel left)
-  (insetRegion, _) <- admit (polygonInset kernel left)
-  (openedRegion, _) <- admit (openWith kernel left)
-  (closedRegion, _) <- admit (closeWith kernel left)
-  traverse regionArea [sumRegion, offsetRegion, insetRegion, openedRegion, closedRegion]
-    >>= print
-```
-
-The Boolean summaries are union `(χ = 1, area = 20, perimeter = 20)`,
-intersection `(1, 4, 8)`, and regularized difference `(1, 12, 20)`. The
-morphology areas are `(48, 36, 4, 16, 16)`: Minkowski sum, one-unit square
-offset, inset, opening, and closing respectively.
-
-## The growing city
-
-A triangulation is a value so that a large one can be extended without being
-rebuilt or locked: a city mesh a district is added to, and then another.
-
-`union` rebuilds last. `planPair` classifies the pair first — an empty or
-repeated operand returns the other verbatim, separable operands merge along
-their seam, a subset inserts into its superset, skewed sizes insert the smaller
-into the larger.
+Use the main facade with explicit `Vector` inputs. Construction and set algebra
+retain their typed obstruction in `Either`:
 
-```haskell
-insertionIsCheaper addition base = addition <= 64 || addition <= base `quot` 8
+```cabal
+build-depends:
+  base >= 4.19 && < 5,
+  moonlight-triangulation >= 1.4 && < 1.5,
+  vector >= 0.13 && < 0.14
 ```
 
-A district is small against a city, so it inserts through a local copy-on-write
-transaction. Publication scales with the insertion work and pages dirtied
-rather than copying and renumbering the whole city.
-
-The mesh being read is never the mesh being written. A render or pathfinding
-thread queries the published value while the next is built: no lock, no
-defensive copy, no interval in which the world and the mesh disagree. Mutation
-offers only the choice between stalling in place and answering from a clone
-that does not yet know what was built, and both are visible from the frame.
-
-The same fact makes a failed union a `Left` over an untouched operand,
-reverting an edit a selection, and a preview a second value rather than a copy.
-
-`Moonlight.Triangulation.Parallel` reads the operation the other way: regions
-triangulated apart, folded up a balanced tournament, each node's sides
-concurrent.
-
-## Representation
-
-Structure-of-arrays over paged, copy-on-write storage. Half-edge twins are index
-complements (`e xor 1`), so traversal is arithmetic rather than indirection.
-Local insertion transactions copy only the pages they touch; mutation is
-confined to `ST` behind `Moonlight.Triangulation.Internal.Mutable` and never
-escapes.
-
-## Predicates
-
-`Moonlight.Triangulation.Internal.Dyadic` carries the exact layer: mantissas are
-decoded and aligned to a common exponent, with bounded predicates evaluated in
-machine words and larger cases over `Integer`. Floating approximations are
-trusted only inside proved error bounds and fall through to exact evaluation
-otherwise.
-
-## Public sublibraries
-
-Depend on the sublibrary you actually use, not on the facade. The footprint is
-the interface: each row states what the token costs you, and a build that pulls
-more than the row says is a bug in this table or in the cabal.
-
-| Sublibrary | Surface |
-| --- | --- |
-| `core` | Exact-arithmetic scalars (`Scalar`, `LineSideInfo`) over paged, copy-on-write storage (`Internal.Dyadic`, `.Paged`, `.BoxedPaged`, `.PageDirectory`, `.Growable`, `.PackedIndex`, `.FaceQueue`). Depends only on `base`/`containers`/`deepseq`/`vector` — no other sublibrary. |
-| `dcel` | The finite half-edge mesh and its whole read surface: `Types`, `Math`, exact geometry, closed `CellSet`, `Region`, `Valuation`, `Interop`, `Dcel`, `Payload`, `JoinSemilattice`, `Handles` with its iterator family, `PointLocation`, `Validation`, `FloodFillIterator`, `IntersectionIterator`. Adds `primitive` and `vector-algorithms` over `core`. |
-| `build` | The construction kernel: `BulkLoad`, `Session`, `Removal`, typed `SetAlgebra`, `Cdt` constraint recovery, and `Refinement`. Adds no external dependency over `core` and `dcel`. |
-| `parallel` | Concurrent evaluation of the pure union plan. Adds `async` at this effect boundary rather than below it. |
-| `serialize` | `Serialization` — the versioned binary envelope, and the only sublibrary that costs you `binary`, `bytestring`, and `transformers`. Deliberately absent from the facade. |
-| `dual` | The dcel-only Voronoi dual and its readers: `Voronoi`, `Voronoi.Handles`, and `Interpolation` (natural-neighbour). It can compile concurrently with `build`. |
-| facade (`moonlight-triangulation`) | The equational surface plus the two real overlaps: exact `Overlay`/`Minkowski` operations that descend through `build`, and `HintGenerator`, which glues construction to the dual. It re-exports the theory and deliberately excludes `serialize`; there is no duplicate facade implementation beneath it. |
-
-### Fast local feedback
-
-Run exact components through the repository build guard so a local edit never
-silently expands into the complete package. From the repository's `compiler/`
-directory, the optimized construction kernel is:
+[`SetAlgebra.hs`](docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs)
+is the smallest compile-checked workflow. The
+[example index](docs/examples/README.md) adds constrained interiors,
+alpha-boundary descent, exact overlay and valuations, and polygonal morphology.
 
-    ../scripts/safe-cabal.sh build moonlight-triangulation:lib:build \
-      --project-file=cabal.project.triangulation-dev \
-      --builddir=dist-newstyle-triangulation-dev
+## Architecture
 
-For type-and-interface feedback where no benchmarkable artifact is required,
-use the same component and cache at `-O0`:
+The resident DCEL is immutable structure-of-arrays over paged copy-on-write
+storage; local mutation is sealed in `ST`. Half-edge twins are index
+complements. Exact predicates use bounded machine-word evaluation and fall
+through to aligned `Integer` arithmetic when required.
 
-    ../scripts/safe-cabal.sh build moonlight-triangulation:lib:build \
-      --project-file=cabal.project.triangulation-dev \
-      --builddir=dist-newstyle-triangulation-fast \
-      --disable-optimization
+## Consumer libraries
 
-The full facade is still an admitted five-component closure. Release and
-runtime claims use the optimized profile; the `-O0` command is deliberately a
-different feedback workload, not counterfeit evidence of a faster production
-build.
+The Cabal manifest owns component visibility and dependencies; this is its
+consumer-facing projection. External packages normally use the main library
+and add only the public sublibraries whose modules they import. The finite DCEL
+and construction kernel remain private behind the main facade.
 
-## Surface
+| Dependency | Imports | Purpose |
+| --- | --- | --- |
+| `moonlight-triangulation` | `Moonlight.Triangulation`, `.HintGenerator`, `.Overlay`, `.Minkowski` | Construction, observations, set algebra, CDT, refinement, alpha geometry, and exact planar operations. |
+| `moonlight-triangulation:core` | `.Scalar`, `.LineSideInfo` | Low-level exact scalars and paged storage primitives. |
+| `moonlight-triangulation:dual` | `.Voronoi`, `.Voronoi.Handles`, `.Interpolation` | Voronoi observations and natural-neighbour interpolation. |
+| `moonlight-triangulation:parallel` | `.Parallel` | Bounded concurrent union; adds `async`. |
+| `moonlight-triangulation:serialize` | `.Serialization` | Versioned binary envelopes; adds `binary`, `bytestring`, and `transformers`. |
+| `moonlight-triangulation:cell-complex` (GHC 9.14+) | `.CellComplex` | Cell, chain, and filtered-alpha interpretations; adds Homology. |
 
-* `Moonlight.Triangulation` — the apex facade.
-* `.BulkLoad` — circle-sweep construction and incremental insertion.
-* `.Cdt` — constraint recovery by conflict strip, with requeue on re-intersection.
-* `.PointLocation`, `.HintGenerator` — walk location and Delaunay hierarchy hints.
-* `.IntersectionIterator`, `.FloodFillIterator` — ordered line traversal,
-  barrier fill, labelled face components, and boundary loops.
-* `.Alpha` — exact vertex/edge/face births, closed alpha subcomplexes, and the
-  constant-work bounded-face membership projection.
-* `.Exact`, `.CellSet`, `.Region`, `.Valuation` — exact rational geometry,
-  closed cell selections, admitted polygonal regions, and intrinsic measures.
-* `.Overlay`, `.Minkowski` — labelled common refinement, planar Booleans,
-  polygonal convolution, offset, erosion, opening, and closing.
-* `.Voronoi`, `.Interpolation` — dual cells and natural-neighbour interpolation.
-* `.Refinement` — Ruppert-style angle and area refinement.
-* `.Validation` — structural and Delaunay-property audits.
-* `.Serialization` (sublibrary `serialize`) — versioned binary envelope.
+[Build and validate package components locally.](docs/development.md)
diff --git a/app/category-observatory/Main.hs b/app/category-observatory/Main.hs
deleted file mode 100644
--- a/app/category-observatory/Main.hs
+++ /dev/null
@@ -1,562 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-
-module Main (main) where
-
-import Data.Aeson (ToJSON)
-import Data.Aeson qualified as Aeson
-import Data.Bifunctor (first)
-import Data.ByteString.Lazy.Char8 qualified as LazyByteString
-import Data.Containers.ListUtils (nubOrd)
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.Vector qualified as Vector
-import GHC.Generics (Generic)
-import Moonlight.Homology.Pure.Topology.CellCategory
-  ( ComplexCat,
-    ComplexMor (..),
-    ComplexOb (..),
-    complexCategory,
-  )
-import Moonlight.Triangulation.CellComplex
-  ( DCELComplex,
-    fromExactCellSet,
-  )
-import Moonlight.Category
-  ( Category (..),
-    FiniteComposableCategory (..),
-    chainVertices,
-  )
-import Moonlight.Category.Simplicial
-  ( GeneratedSSet,
-    NerveSimplex,
-    TruncatedNormalizedSSet,
-    generatedSimplicesAtDimension,
-    nerveSimplexChain,
-    normalizedNerve,
-    simplicesAtDimension,
-    unnormalizedNerve,
-  )
-import Moonlight.Homology.Pure.Topology.CellComplex
-  ( CellComplex2D (..),
-    CellTypes (..),
-    OrientedEdge (..),
-  )
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.CellSet
-  ( CellSelectionError,
-    ExactCellSet,
-    closeFaceCellSet,
-    exactCellSetEdgeCount,
-    exactCellSetFaceCount,
-    exactCellSetVertexCount,
-  )
-import Moonlight.Triangulation.Dcel
-  ( faceVertices,
-    undirectedEndpoints,
-    vertexPoint,
-  )
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( FaceId,
-    UndirectedEdgeId,
-    VertexId,
-    unFaceId,
-    unUndirectedEdgeId,
-    unVertexId,
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
-import Moonlight.Triangulation.Types
-  ( BuildError,
-    DelaunayTriangulation,
-    Point (..),
-  )
-import Numeric.Natural (Natural)
-import System.Exit (die)
-
-main :: IO ()
-main =
-  either
-    (die . renderExhibitError)
-    (LazyByteString.putStrLn . Aeson.encode)
-    buildExhibit
-
-data ExhibitError
-  = ExhibitTriangulationBuildFailed BuildError
-  | ExhibitCellSelectionFailed CellSelectionError
-  | ExhibitMorphismEndpointUnavailable MorphismEndpoint Int
-  | ExhibitFaceWithoutVertices FaceId
-  | ExhibitMalformedTwoSimplex Int
-  | ExhibitInventoryMismatch (Int, Int, Int) (Int, Int, Int)
-  | ExhibitCategoryEnumerationMismatch (Int, Int, Int, Int) (Int, Int, Int, Int)
-  | ExhibitNerveCountMismatch Int Int
-  deriving stock (Show)
-
-data MorphismEndpoint
-  = MorphismSource
-  | MorphismTarget
-  deriving stock (Show)
-
-renderExhibitError :: ExhibitError -> String
-renderExhibitError = show
-
-data Exhibit = Exhibit
-  { schemaVersion :: Int,
-    exhibitTitle :: String,
-    mesh :: MeshProjection,
-    category :: CategoryProjection,
-    nerve :: NerveProjection
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshProjection = MeshProjection
-  { meshVertices :: [MeshVertex],
-    meshEdges :: [MeshEdge],
-    meshFaces :: [MeshFace]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshVertex = MeshVertex
-  { meshVertexIdentifier :: String,
-    meshVertexObjectIdentifier :: String,
-    meshVertexPosition :: Position
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshEdge = MeshEdge
-  { meshEdgeIdentifier :: String,
-    meshEdgeObjectIdentifier :: String,
-    meshEdgeVertexIdentifiers :: [String]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshFace = MeshFace
-  { meshFaceIdentifier :: String,
-    meshFaceObjectIdentifier :: String,
-    meshFaceVertexIdentifiers :: [String]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data Position = Position
-  { positionX :: Double,
-    positionY :: Double
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryProjection = CategoryProjection
-  { categoryObjects :: [CategoryObject],
-    categoryMorphisms :: [CategoryMorphism]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryObject = CategoryObject
-  { categoryObjectIdentifier :: String,
-    categoryObjectKind :: CategoryObjectKind,
-    categoryObjectLabel :: String,
-    categoryObjectPosition :: Position
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryObjectKind
-  = VertexObject
-  | EdgeObject
-  | FaceObject
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryMorphism = CategoryMorphism
-  { categoryMorphismIdentifier :: String,
-    categoryMorphismKind :: CategoryMorphismKind,
-    categoryMorphismSourceObject :: String,
-    categoryMorphismTargetObject :: String
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryMorphismKind
-  = IdentityMorphism
-  | FaceEdgeMorphism
-  | EdgeVertexMorphism
-  | FaceVertexComposite
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data NerveProjection = NerveProjection
-  { nerveTwoSimplices :: [NerveTwoSimplex],
-    nerveSimplexCounts :: [NerveSimplexCount]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data NerveSimplexCount = NerveSimplexCount
-  { nerveDimension :: Natural,
-    normalizedSimplexCount :: Int,
-    unnormalizedSimplexCount :: Int
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data NerveTwoSimplex = NerveTwoSimplex
-  { nerveSimplexIdentifier :: String,
-    nerveSimplexObjectIdentifiers :: [String]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-buildExhibit :: Either ExhibitError Exhibit
-buildExhibit = do
-  triangulation <- first ExhibitTriangulationBuildFailed (delaunayGeometry exhibitPoints)
-  selectedCells <-
-    first ExhibitCellSelectionFailed
-      (closeFaceCellSet triangulation (innerFaces triangulation))
-  let complexValue = fromExactCellSet selectedCells
-      categoryValue = complexCategory complexValue
-  validateInventory selectedCells complexValue
-  validateCategoryEnumeration complexValue categoryValue
-  categoryProjection <- projectCategory triangulation categoryValue
-  nerveProjection <- projectNerve categoryValue
-  validateNerveCount complexValue nerveProjection
-  pure
-    Exhibit
-      { schemaVersion = 2,
-        exhibitTitle = "Triangulation, incidence category, and flag nerve",
-        mesh = projectMesh triangulation complexValue,
-        category = categoryProjection,
-        nerve = nerveProjection
-      }
-
-validateInventory :: ExactCellSet -> DCELComplex -> Either ExhibitError ()
-validateInventory cellSet complexValue =
-  let expectedCounts =
-        ( exactCellSetVertexCount cellSet,
-          exactCellSetEdgeCount cellSet,
-          exactCellSetFaceCount cellSet
-        )
-      observedCounts =
-        ( length (vertices complexValue),
-          length (edges complexValue),
-          length (faces complexValue)
-        )
-   in if observedCounts == expectedCounts
-        then Right ()
-        else Left (ExhibitInventoryMismatch expectedCounts observedCounts)
-
-validateCategoryEnumeration ::
-  DCELComplex ->
-  ComplexCat DCELComplex ->
-  Either ExhibitError ()
-validateCategoryEnumeration complexValue categoryValue =
-  let morphisms = enumerateMorphisms categoryValue
-      expectedCounts =
-        ( length (vertices complexValue) + length (edges complexValue) + length (faces complexValue),
-          sum (fmap (length . faceBoundary complexValue) (faces complexValue)),
-          sum (fmap (edgeEndpointCount complexValue) (edges complexValue)),
-          sum (fmap (faceFlagCount complexValue) (faces complexValue))
-        )
-      observedCounts =
-        ( length (filter isIdentityMorphism morphisms),
-          length (filter isFaceEdgeMorphism morphisms),
-          length (filter isEdgeVertexMorphism morphisms),
-          length (filter isFaceVertexMorphism morphisms)
-        )
-   in if observedCounts == expectedCounts
-        then Right ()
-        else Left (ExhibitCategoryEnumerationMismatch expectedCounts observedCounts)
-
-validateNerveCount :: DCELComplex -> NerveProjection -> Either ExhibitError ()
-validateNerveCount complexValue nerveProjection =
-  let expectedCount = sum (fmap (faceFlagCount complexValue) (faces complexValue))
-      observedCount = length (nerveTwoSimplices nerveProjection)
-   in if observedCount == expectedCount
-        then Right ()
-        else Left (ExhibitNerveCountMismatch expectedCount observedCount)
-
-edgeEndpointCount :: DCELComplex -> Edge DCELComplex -> Int
-edgeEndpointCount complexValue edgeValue =
-  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
-   in length (nubOrd [sourceVertex, targetVertex])
-
-faceFlagCount :: DCELComplex -> Face DCELComplex -> Int
-faceFlagCount complexValue faceValue =
-  sum
-    ( fmap
-        (edgeEndpointCount complexValue . orientedEdge)
-        (faceBoundary complexValue faceValue)
-    )
-
-isIdentityMorphism :: ComplexMor DCELComplex -> Bool
-isIdentityMorphism morphismValue =
-  case morphismValue of
-    IdentityMor _ -> True
-    _ -> False
-
-isFaceEdgeMorphism :: ComplexMor DCELComplex -> Bool
-isFaceEdgeMorphism morphismValue =
-  case morphismValue of
-    FaceToEdge _ _ _ -> True
-    _ -> False
-
-isEdgeVertexMorphism :: ComplexMor DCELComplex -> Bool
-isEdgeVertexMorphism morphismValue =
-  case morphismValue of
-    EdgeToVertex _ _ -> True
-    _ -> False
-
-isFaceVertexMorphism :: ComplexMor DCELComplex -> Bool
-isFaceVertexMorphism morphismValue =
-  case morphismValue of
-    FaceToVertex _ _ _ -> True
-    _ -> False
-
-projectMesh :: DelaunayTriangulation () -> DCELComplex -> MeshProjection
-projectMesh triangulation complexValue =
-  MeshProjection
-    { meshVertices = fmap (projectMeshVertex triangulation) (vertices complexValue),
-      meshEdges = fmap (projectMeshEdge triangulation) (edges complexValue),
-      meshFaces = fmap (projectMeshFace triangulation) (faces complexValue)
-    }
-
-projectMeshVertex :: DelaunayTriangulation () -> VertexId -> MeshVertex
-projectMeshVertex triangulation vertexValue =
-  MeshVertex
-    { meshVertexIdentifier = vertexIdentifier vertexValue,
-      meshVertexObjectIdentifier = vertexObjectIdentifier vertexValue,
-      meshVertexPosition = pointPosition (vertexPoint triangulation vertexValue)
-    }
-
-projectMeshEdge :: DelaunayTriangulation () -> UndirectedEdgeId -> MeshEdge
-projectMeshEdge triangulation edgeValue =
-  let (sourceVertex, targetVertex) = undirectedEndpoints triangulation edgeValue
-   in MeshEdge
-        { meshEdgeIdentifier = edgeIdentifier edgeValue,
-          meshEdgeObjectIdentifier = edgeObjectIdentifier edgeValue,
-          meshEdgeVertexIdentifiers = fmap vertexIdentifier [sourceVertex, targetVertex]
-        }
-
-projectMeshFace :: DelaunayTriangulation () -> FaceId -> MeshFace
-projectMeshFace triangulation faceValue =
-  MeshFace
-    { meshFaceIdentifier = faceIdentifier faceValue,
-      meshFaceObjectIdentifier = faceObjectIdentifier faceValue,
-      meshFaceVertexIdentifiers = fmap vertexIdentifier (faceVertices triangulation faceValue)
-    }
-
-projectCategory ::
-  DelaunayTriangulation () ->
-  ComplexCat DCELComplex ->
-  Either ExhibitError CategoryProjection
-projectCategory triangulation categoryValue = do
-  objectProjections <-
-    traverse
-      (projectCategoryObject triangulation)
-      (enumerateObjects categoryValue)
-  morphismProjections <-
-    traverse
-      (projectCategoryMorphism categoryValue)
-      (zip [0 ..] (enumerateMorphisms categoryValue))
-  pure
-    CategoryProjection
-      { categoryObjects = objectProjections,
-        categoryMorphisms = morphismProjections
-      }
-
-projectCategoryObject ::
-  DelaunayTriangulation () ->
-  ComplexOb DCELComplex ->
-  Either ExhibitError CategoryObject
-projectCategoryObject triangulation objectValue = do
-  objectPosition <- positionForObject triangulation objectValue
-  pure
-    CategoryObject
-      { categoryObjectIdentifier = objectIdentifier objectValue,
-        categoryObjectKind = objectKind objectValue,
-        categoryObjectLabel = objectLabel objectValue,
-        categoryObjectPosition = objectPosition
-      }
-
-projectCategoryMorphism ::
-  ComplexCat DCELComplex ->
-  (Int, ComplexMor DCELComplex) ->
-  Either ExhibitError CategoryMorphism
-projectCategoryMorphism categoryValue (morphismIndex, morphismValue) = do
-  sourceObject <-
-    first
-      (const (ExhibitMorphismEndpointUnavailable MorphismSource morphismIndex))
-      (source categoryValue morphismValue)
-  targetObject <-
-    first
-      (const (ExhibitMorphismEndpointUnavailable MorphismTarget morphismIndex))
-      (target categoryValue morphismValue)
-  pure
-    CategoryMorphism
-      { categoryMorphismIdentifier = "morphism-" <> show morphismIndex,
-        categoryMorphismKind = morphismKind morphismValue,
-        categoryMorphismSourceObject = objectIdentifier sourceObject,
-        categoryMorphismTargetObject = objectIdentifier targetObject
-      }
-
-projectNerve :: ComplexCat DCELComplex -> Either ExhibitError NerveProjection
-projectNerve categoryValue = do
-  let normalizedNerveValue = normalizedNerve categoryValue nerveObservationBound
-      unnormalizedNerveValue = unnormalizedNerve categoryValue nerveObservationBound
-  twoSimplices <-
-    traverse
-      projectTwoSimplex
-      (zip [0 ..] (simplicesAtDimension normalizedNerveValue nerveObservationBound))
-  pure
-    NerveProjection
-      { nerveTwoSimplices = twoSimplices,
-        nerveSimplexCounts =
-          fmap
-            (projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue)
-            nerveObservationDimensions
-      }
-
-nerveObservationBound :: Natural
-nerveObservationBound = 2
-
-nerveObservationDimensions :: [Natural]
-nerveObservationDimensions = [0, 1, 2]
-
-projectNerveSimplexCount ::
-  TruncatedNormalizedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
-  GeneratedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
-  Natural ->
-  NerveSimplexCount
-projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue dimensionValue =
-  NerveSimplexCount
-    { nerveDimension = dimensionValue,
-      normalizedSimplexCount = length (simplicesAtDimension normalizedNerveValue dimensionValue),
-      unnormalizedSimplexCount = length (generatedSimplicesAtDimension unnormalizedNerveValue dimensionValue)
-    }
-
-projectTwoSimplex ::
-  (Int, NerveSimplex (ComplexCat DCELComplex)) ->
-  Either ExhibitError NerveTwoSimplex
-projectTwoSimplex (simplexIndex, simplexValue) =
-  case chainVertices (nerveSimplexChain simplexValue) of
-    faceObject :| [edgeObject, vertexObject] ->
-      pure
-        NerveTwoSimplex
-          { nerveSimplexIdentifier = "simplex-" <> show simplexIndex,
-            nerveSimplexObjectIdentifiers =
-              fmap objectIdentifier [faceObject, edgeObject, vertexObject]
-          }
-    _ -> Left (ExhibitMalformedTwoSimplex simplexIndex)
-
-positionForObject ::
-  DelaunayTriangulation () ->
-  ComplexOb DCELComplex ->
-  Either ExhibitError Position
-positionForObject triangulation objectValue =
-  case objectValue of
-    VertexOb vertexValue ->
-      Right (pointPosition (vertexPoint triangulation vertexValue))
-    EdgeOb edgeValue ->
-      let (sourceVertex, targetVertex) = undirectedEndpoints triangulation edgeValue
-       in Right
-            ( midpoint
-                (pointPosition (vertexPoint triangulation sourceVertex))
-                (pointPosition (vertexPoint triangulation targetVertex))
-            )
-    FaceOb faceValue ->
-      centroid faceValue
-        (fmap (pointPosition . vertexPoint triangulation) (faceVertices triangulation faceValue))
-
-centroid :: FaceId -> [Position] -> Either ExhibitError Position
-centroid faceValue positions =
-  case positions of
-    [] -> Left (ExhibitFaceWithoutVertices faceValue)
-    _ ->
-      let (totalX, totalY) =
-            foldl'
-              (\(accumulatedX, accumulatedY) positionValue ->
-                 ( accumulatedX + positionX positionValue,
-                   accumulatedY + positionY positionValue
-                 )
-              )
-              (0, 0)
-              positions
-          positionCount = fromIntegral (length positions)
-       in Right (Position (totalX / positionCount) (totalY / positionCount))
-
-midpoint :: Position -> Position -> Position
-midpoint firstPosition secondPosition =
-  Position
-    { positionX = (positionX firstPosition + positionX secondPosition) / 2,
-      positionY = (positionY firstPosition + positionY secondPosition) / 2
-    }
-
-pointPosition :: Point -> Position
-pointPosition pointValue =
-  Position
-    { positionX = pointX pointValue,
-      positionY = pointY pointValue
-    }
-
-objectIdentifier :: ComplexOb DCELComplex -> String
-objectIdentifier objectValue =
-  case objectValue of
-    VertexOb vertexValue -> vertexObjectIdentifier vertexValue
-    EdgeOb edgeValue -> edgeObjectIdentifier edgeValue
-    FaceOb faceValue -> faceObjectIdentifier faceValue
-
-objectKind :: ComplexOb DCELComplex -> CategoryObjectKind
-objectKind objectValue =
-  case objectValue of
-    VertexOb _ -> VertexObject
-    EdgeOb _ -> EdgeObject
-    FaceOb _ -> FaceObject
-
-objectLabel :: ComplexOb DCELComplex -> String
-objectLabel objectValue =
-  case objectValue of
-    VertexOb vertexValue -> "v" <> show (unVertexId vertexValue)
-    EdgeOb edgeValue -> "e" <> show (unUndirectedEdgeId edgeValue)
-    FaceOb faceValue -> "f" <> show (unFaceId faceValue)
-
-morphismKind :: ComplexMor DCELComplex -> CategoryMorphismKind
-morphismKind morphismValue =
-  case morphismValue of
-    IdentityMor _ -> IdentityMorphism
-    FaceToEdge _ _ _ -> FaceEdgeMorphism
-    EdgeToVertex _ _ -> EdgeVertexMorphism
-    FaceToVertex _ _ _ -> FaceVertexComposite
-
-vertexIdentifier :: VertexId -> String
-vertexIdentifier vertexValue = "vertex-" <> show (unVertexId vertexValue)
-
-edgeIdentifier :: UndirectedEdgeId -> String
-edgeIdentifier edgeValue = "edge-" <> show (unUndirectedEdgeId edgeValue)
-
-faceIdentifier :: FaceId -> String
-faceIdentifier faceValue = "face-" <> show (unFaceId faceValue)
-
-vertexObjectIdentifier :: VertexId -> String
-vertexObjectIdentifier vertexValue = "object-" <> vertexIdentifier vertexValue
-
-edgeObjectIdentifier :: UndirectedEdgeId -> String
-edgeObjectIdentifier edgeValue = "object-" <> edgeIdentifier edgeValue
-
-faceObjectIdentifier :: FaceId -> String
-faceObjectIdentifier faceValue = "object-" <> faceIdentifier faceValue
-
-exhibitPoints :: Vector.Vector Point
-exhibitPoints =
-  Vector.fromList
-    [ Point 80 72,
-      Point 222 48,
-      Point 362 92,
-      Point 52 208,
-      Point 176 166,
-      Point 310 190,
-      Point 414 232,
-      Point 112 330,
-      Point 258 314,
-      Point 380 354
-    ]
diff --git a/app/persistence-rose/Main.hs b/app/persistence-rose/Main.hs
deleted file mode 100644
--- a/app/persistence-rose/Main.hs
+++ /dev/null
@@ -1,211 +0,0 @@
-module Main (main) where
-
-import Control.Exception (IOException, try)
-import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Except
-  ( ExceptT (..),
-    except,
-    runExceptT,
-  )
-import Data.Bifunctor (first)
-import Data.ByteString (ByteString)
-import Data.ByteString qualified as ByteString
-import Data.Foldable (traverse_)
-import Moonlight.Triangulation.Exhibit.PersistenceRose
-  ( PersistenceRoseError,
-    PersistenceRoseScene,
-    alphaEclipseReceipt,
-    buildAlphaEclipseScene,
-    buildPersistenceRoseScene,
-    persistenceRoseFrameSpecs,
-    persistenceRoseReceipt,
-    renderAlphaEclipse,
-    renderAlphaEclipseReceipt,
-    renderPersistenceRoseAnimationReceipt,
-    renderPersistenceRoseFrame,
-    renderPersistenceRosePoster,
-    renderPersistenceRoseReceipt,
-    stampPersistenceRoseGif,
-    verifyPersistenceRoseGif,
-  )
-import System.Directory (createDirectoryIfMissing)
-import System.Environment (getArgs)
-import System.Exit (die)
-import System.FilePath ((</>), takeDirectory)
-import Text.Printf (printf)
-import Text.Read (readMaybe)
-
-data PersistenceRoseCommand
-  = WritePoster !FilePath
-  | WriteEclipse !FilePath
-  | WriteFrames !Int !FilePath
-  | StampGif !FilePath !FilePath
-  | VerifyArtifacts !FilePath
-  | PrintReceipt
-
-data PersistenceRoseCommandError
-  = PersistenceRoseUsage ![String]
-  | PersistenceRoseInvalidFrameCount !String
-  | PersistenceRoseConstructionFailed !PersistenceRoseError
-  | PersistenceRoseDirectoryFailed !FilePath !IOException
-  | PersistenceRoseReadFailed !FilePath !IOException
-  | PersistenceRoseWriteFailed !FilePath !IOException
-  | PersistenceRoseArtifactMismatch !FilePath
-
-instance Show PersistenceRoseCommandError where
-  show commandError =
-    case commandError of
-      PersistenceRoseUsage arguments ->
-        "invalid persistence-rose command "
-          <> show arguments
-          <> "; expected: poster OUTPUT.svg | eclipse OUTPUT.svg | frames FRAME_COUNT OUTPUT_DIRECTORY | stamp-gif INPUT.gif OUTPUT.gif | verify-artifacts PACKAGE_ROOT | receipt"
-      PersistenceRoseInvalidFrameCount submitted ->
-        "frame count must be an integer greater than one, received " <> show submitted
-      PersistenceRoseConstructionFailed sceneError -> show sceneError
-      PersistenceRoseDirectoryFailed path caughtError ->
-        "could not create output directory " <> show path <> ": " <> show caughtError
-      PersistenceRoseReadFailed path caughtError ->
-        "could not read " <> show path <> ": " <> show caughtError
-      PersistenceRoseWriteFailed path caughtError ->
-        "could not write " <> show path <> ": " <> show caughtError
-      PersistenceRoseArtifactMismatch path ->
-        "tracked exhibit differs from its canonical renderer: " <> show path
-
-main :: IO ()
-main = do
-  arguments <- getArgs
-  result <- runExceptT (runPersistenceRoseCommand arguments)
-  either (die . show) pure result
-
-runPersistenceRoseCommand :: [String] -> ExceptT PersistenceRoseCommandError IO ()
-runPersistenceRoseCommand arguments = do
-  command <- except (parseCommand arguments)
-  case command of
-    WriteEclipse outputPath -> do
-      scene <- except (first PersistenceRoseConstructionFailed buildAlphaEclipseScene)
-      createOutputDirectory (takeDirectory outputPath)
-      writeOutput outputPath (renderAlphaEclipse scene)
-      lift
-        ( putStrLn
-            (renderAlphaEclipseReceipt (alphaEclipseReceipt scene) <> "\nwrote alpha eclipse " <> outputPath)
-        )
-    StampGif inputPath outputPath -> do
-      scene <- requirePersistenceRoseScene
-      inputBytes <- readBytes inputPath
-      (receipt, stamped) <- except (first PersistenceRoseConstructionFailed (stampPersistenceRoseGif scene inputBytes))
-      createOutputDirectory (takeDirectory outputPath)
-      writeBytes outputPath stamped
-      lift
-        ( putStrLn
-            (renderPersistenceRoseAnimationReceipt receipt <> "\nwrote stamped GIF " <> outputPath)
-        )
-    VerifyArtifacts packageRoot -> verifyTrackedArtifacts packageRoot
-    PrintReceipt -> do
-      scene <- requirePersistenceRoseScene
-      lift (putStrLn (renderPersistenceRoseReceipt (persistenceRoseReceipt scene)))
-    WritePoster outputPath -> do
-      scene <- requirePersistenceRoseScene
-      let receiptText = renderPersistenceRoseReceipt (persistenceRoseReceipt scene)
-      createOutputDirectory (takeDirectory outputPath)
-      writeOutput outputPath (renderPersistenceRosePoster scene)
-      lift (putStrLn (receiptText <> "\nwrote poster " <> outputPath))
-    WriteFrames frameCount outputDirectory -> do
-      scene <- requirePersistenceRoseScene
-      let receiptText = renderPersistenceRoseReceipt (persistenceRoseReceipt scene)
-      frames <- except (first PersistenceRoseConstructionFailed (persistenceRoseFrameSpecs frameCount))
-      createOutputDirectory outputDirectory
-      traverse_
-        (\(frameIndex, frame) -> writeOutput (framePath outputDirectory frameIndex) (renderPersistenceRoseFrame scene frame))
-        (zip [0 :: Int ..] frames)
-      lift
-        ( putStrLn
-            ( receiptText
-                <> "\nwrote "
-                <> show (length frames)
-                <> " frames to "
-                <> outputDirectory
-            )
-        )
-
-requirePersistenceRoseScene :: ExceptT PersistenceRoseCommandError IO PersistenceRoseScene
-requirePersistenceRoseScene =
-  except (first PersistenceRoseConstructionFailed buildPersistenceRoseScene)
-
-parseCommand :: [String] -> Either PersistenceRoseCommandError PersistenceRoseCommand
-parseCommand arguments =
-  case arguments of
-    ["poster", outputPath] -> Right (WritePoster outputPath)
-    ["eclipse", outputPath] -> Right (WriteEclipse outputPath)
-    ["stamp-gif", inputPath, outputPath] -> Right (StampGif inputPath outputPath)
-    ["verify-artifacts", packageRoot] -> Right (VerifyArtifacts packageRoot)
-    ["frames", submittedCount, outputDirectory] ->
-      case readMaybe submittedCount of
-        Just frameCount
-          | frameCount > 1 -> Right (WriteFrames frameCount outputDirectory)
-        _ -> Left (PersistenceRoseInvalidFrameCount submittedCount)
-    ["receipt"] -> Right PrintReceipt
-    _ -> Left (PersistenceRoseUsage arguments)
-
-createOutputDirectory :: FilePath -> ExceptT PersistenceRoseCommandError IO ()
-createOutputDirectory outputDirectory =
-  liftIOException
-    (PersistenceRoseDirectoryFailed outputDirectory)
-    (createDirectoryIfMissing True outputDirectory)
-
-writeOutput :: FilePath -> String -> ExceptT PersistenceRoseCommandError IO ()
-writeOutput outputPath contents =
-  liftIOException (PersistenceRoseWriteFailed outputPath) (writeFile outputPath contents)
-
-readOutput :: FilePath -> ExceptT PersistenceRoseCommandError IO String
-readOutput inputPath =
-  liftIOException (PersistenceRoseReadFailed inputPath) (readFile inputPath)
-
-readBytes :: FilePath -> ExceptT PersistenceRoseCommandError IO ByteString
-readBytes inputPath =
-  liftIOException (PersistenceRoseReadFailed inputPath) (ByteString.readFile inputPath)
-
-writeBytes :: FilePath -> ByteString -> ExceptT PersistenceRoseCommandError IO ()
-writeBytes outputPath contents =
-  liftIOException (PersistenceRoseWriteFailed outputPath) (ByteString.writeFile outputPath contents)
-
-verifyTrackedArtifacts :: FilePath -> ExceptT PersistenceRoseCommandError IO ()
-verifyTrackedArtifacts packageRoot = do
-  roseScene <- requirePersistenceRoseScene
-  eclipseScene <- except (first PersistenceRoseConstructionFailed buildAlphaEclipseScene)
-  let posterPath = packageRoot </> "docs" </> "moonlight-triangulation-persistence-rose.svg"
-      eclipsePath = packageRoot </> "docs" </> "moonlight-triangulation-alpha-eclipse.svg"
-      gifPath = packageRoot </> "docs" </> "social" </> "moonlight-triangulation-persistence-rose.gif"
-  verifyTextArtifact posterPath (renderPersistenceRosePoster roseScene)
-  verifyTextArtifact eclipsePath (renderAlphaEclipse eclipseScene)
-  gifBytes <- readBytes gifPath
-  animationReceipt <-
-    except
-      (first PersistenceRoseConstructionFailed (verifyPersistenceRoseGif roseScene gifBytes))
-  lift
-    ( putStrLn
-        ( "verified poster, alpha eclipse, and stamped GIF\n"
-            <> renderPersistenceRoseAnimationReceipt animationReceipt
-        )
-    )
-
-verifyTextArtifact
-  :: FilePath
-  -> String
-  -> ExceptT PersistenceRoseCommandError IO ()
-verifyTextArtifact artifactPath expectedContents = do
-  actualContents <- readOutput artifactPath
-  except
-    ( if actualContents == expectedContents
-        then Right ()
-        else Left (PersistenceRoseArtifactMismatch artifactPath)
-    )
-
-liftIOException
-  :: (IOException -> PersistenceRoseCommandError)
-  -> IO value
-  -> ExceptT PersistenceRoseCommandError IO value
-liftIOException wrapFailure = ExceptT . fmap (first wrapFailure) . try
-
-framePath :: FilePath -> Int -> FilePath
-framePath outputDirectory frameIndex =
-  outputDirectory </> printf "frame-%04d.svg" frameIndex
diff --git a/app/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs b/app/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs
deleted file mode 100644
--- a/app/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs
+++ /dev/null
@@ -1,1625 +0,0 @@
-{-# LANGUAGE NamedFieldPuns #-}
-
--- | A deterministic, pure observatory assembled from Moonlight's existing
--- geometry, exact filtration, interpolation, and persistence owners.
-module Moonlight.Triangulation.Exhibit.PersistenceRose
-  ( PersistenceRoseError (..),
-    PersistenceRoseReceipt (..),
-    PersistenceRoseScene,
-    PersistenceRoseFrame,
-    PersistenceRoseAnimationReceipt (..),
-    AlphaEclipseReceipt (..),
-    AlphaEclipseScene,
-    buildPersistenceRoseScene,
-    buildAlphaEclipseScene,
-    persistenceRoseReceipt,
-    alphaEclipseReceipt,
-    renderPersistenceRoseReceipt,
-    renderAlphaEclipseReceipt,
-    renderPersistenceRosePoster,
-    renderAlphaEclipse,
-    persistenceRoseFrameSpecs,
-    renderPersistenceRoseFrame,
-    persistenceRoseAnimationReceipt,
-    renderPersistenceRoseAnimationReceipt,
-    stampPersistenceRoseGif,
-    verifyPersistenceRoseGif,
-  )
-where
-
-import Control.Monad (foldM)
-import Control.Monad.ST (runST)
-import Data.Bifunctor (first)
-import Data.Bits ((.&.), xor)
-import Data.ByteString (ByteString)
-import Data.ByteString qualified as ByteString
-import Data.ByteString.Char8 qualified as ByteString.Char8
-import Data.List (find, intercalate, sortOn)
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.List.NonEmpty qualified as NonEmpty
-import Data.Map.Strict (Map)
-import Data.Map.Strict qualified as Map
-import Data.Maybe (mapMaybe)
-import Data.Ord (Down (..))
-import Data.Set qualified as Set
-import Data.Text qualified as Text
-import Data.Text.Encoding qualified as Text.Encoding
-import Data.Vector qualified as Vector
-import Data.Word (Word64, Word8)
-import Moonlight.Homology.Chain
-  ( HomologicalDegree (..),
-    HomologyFailure,
-    PersistencePair (..),
-  )
-import Moonlight.Homology.Persistence
-  ( criticalBettiVectors,
-    mod2PersistentPairsWithCriticalBettiTable,
-  )
-import Moonlight.Triangulation
-  ( AlphaBirth,
-    AlphaFiltration,
-    AlphaFiltrationError,
-    BuildError,
-    DelaunayTriangulation,
-    ExactLoop,
-    MinkowskiError,
-    OverlayError,
-    PlanarRegion,
-    Point (..),
-    PointValidationError,
-    PolygonComponent,
-    QueryPoint,
-    RegionPublicationError,
-    RegionValidationError,
-    alphaBirthDenominator,
-    alphaBirthNumerator,
-    alphaBirthToDouble,
-    alphaEdgeBirth,
-    alphaFaceBirth,
-    alphaFiltration,
-    alphaFiltrationCriticalBirths,
-    convexPolygon,
-    delaunayGeometry,
-    exactLoop,
-    exactLoopPoints,
-    exactPointFromPoint,
-    exactPointToEmbeddingCandidate,
-    innerFaces,
-    innerFaceVertexTriples,
-    mkQueryPoint,
-    overlayLayers,
-    overlaySelectedRegion,
-    planarLayer,
-    planarRegion,
-    planarRegionComponents,
-    polygonComponent,
-    polygonHoleLoops,
-    polygonOffset,
-    polygonOuterLoop,
-    queryPointValue,
-    structuringElement,
-    undirectedEdges,
-    undirectedEndpoints,
-    vertexPoint,
-    vertices,
-  )
-import Moonlight.Triangulation.CellComplex
-  ( DCELError,
-    filteredAlphaComplex,
-  )
-import Moonlight.Triangulation.FloodFillIterator
-  ( BoundaryObstruction,
-    FaceComponent,
-    boundaryLoopVertices,
-    componentBoundaryLoops,
-    faceComponentFaces,
-    faceComponentsBy,
-    outerFaceFloodFill,
-  )
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( FaceId (..),
-    UndirectedEdgeId,
-    VertexId,
-    normalizedDirected,
-  )
-import Moonlight.Triangulation.Interpolation
-  ( InterpolationStats (..),
-    NaturalNeighborResult (..),
-    naturalNeighborWeights,
-    newNaturalNeighborWorkspace,
-  )
-import Moonlight.Triangulation.Voronoi
-  ( VoronoiEdgeGeometry (..),
-    asDirectedVoronoiEdge,
-    voronoiEdgeGeometry,
-  )
-import Numeric (showFFloat, showHex)
-
--- | Every boundary at which the exhibit may be refused. The renderer never
--- invents a fallback geometry or topology state.
-data PersistenceRoseError
-  = RoseDelaunayBuildFailed !BuildError
-  | RoseAlphaFiltrationFailed !AlphaFiltrationError
-  | RoseCellComplexLoweringFailed !DCELError
-  | RosePersistenceFailed !HomologyFailure
-  | RoseCriticalProfileCountMismatch !Int !Int
-  | RoseCriticalProfileDegreeCountMismatch !Int !Int
-  | RoseBirthRankMissing !AlphaBirth
-  | RoseEdgeBirthMissing !UndirectedEdgeId
-  | RoseFaceBirthMissing !FaceId
-  | RoseBoundaryDescentFailed !Int !BoundaryObstruction
-  | RoseNoCriticalStates
-  | RoseNoMaskRegions
-  | RoseNoPersistentHole
-  | RoseNoCoherentHero !Int ![(Int, Int)]
-  | RoseVoronoiGeometryMissing !UndirectedEdgeId
-  | RoseNaturalNeighborFallbacks !Int
-  | RosePointInvalid !PointValidationError
-  | RoseRegionInvalid !RegionValidationError
-  | RoseOverlayFailed !(OverlayError Bool Bool)
-  | RoseOverlayPublicationFailed !RegionPublicationError
-  | RoseMinkowskiFailed !MinkowskiError
-  | RoseFrameCountTooSmall !Int
-  | RoseEclipseFiniteHoleCountMismatch !Int
-  | RoseEclipseFiniteHoleMissingDeath
-  | RoseEclipseBettiProfileMismatch !Int !Int !Int
-  | RoseGifInvalidSignature
-  | RoseGifTruncated !Int
-  | RoseGifUnexpectedBlock !Int !Word8
-  | RoseGifDimensionMismatch !Int !Int !Int !Int
-  | RoseGifFrameCountMismatch !Int !Int
-  | RoseGifFrameDelayCountMismatch !Int !Int
-  | RoseGifDurationMismatch !Int !Int
-  | RoseGifProvenanceMismatch
-  | RoseGifMarkerTooLong !Int
-  deriving stock (Show)
-
--- | A compact, test-readable projection of everything the scene actually
--- computed. It is intentionally not another geometry representation.
-data PersistenceRoseReceipt = PersistenceRoseReceipt
-  { roseSiteCount :: !Int,
-    roseEdgeCount :: !Int,
-    roseFaceCount :: !Int,
-    roseCriticalBirthCount :: !Int,
-    roseFinitePersistentHoleCount :: !Int,
-    roseEssentialPersistentHoleCount :: !Int,
-    roseHeroCriticalRank :: !Int,
-    roseHeroBettiOne :: !Int,
-    roseHeroGeometricHoleCount :: !Int,
-    roseVoronoiSegmentCount :: !Int,
-    roseNaturalNeighborSampleCount :: !Int,
-    roseNaturalNeighborFallbackCount :: !Int,
-    roseMaskComponentCount :: !Int
-  }
-  deriving stock (Eq, Show)
-
-data PersistenceRoseScene = PersistenceRoseScene
-  { sceneSites :: ![Point],
-    sceneEdges :: ![RankedSegment],
-    sceneFaces :: ![RankedTriangle],
-    sceneCriticalStates :: !(NonEmpty CriticalState),
-    sceneHeroState :: !CriticalState,
-    sceneVoronoiSegments :: ![Segment],
-    sceneFieldSamples :: ![FieldSample],
-    sceneMaskComponents :: ![MaskComponent],
-    scenePersistenceArcs :: ![PersistenceArc],
-    sceneReceipt :: !PersistenceRoseReceipt
-  }
-
--- | The canonical source projection carried inside the retained GIF. The GIF
--- codec remains an effect-boundary view; this receipt binds that binary view
--- to the exact deterministic SVG frame section used to produce it.
-data PersistenceRoseAnimationReceipt = PersistenceRoseAnimationReceipt
-  { roseAnimationFrameCount :: !Int,
-    roseAnimationFramesPerSecond :: !Int,
-    roseAnimationWidth :: !Int,
-    roseAnimationHeight :: !Int,
-    roseAnimationSourceFingerprint :: !Word64
-  }
-  deriving stock (Eq, Show)
-
--- | The exact, test-readable facts shown by the alpha eclipse. The numerator
--- and denominator fields are projections of opaque 'AlphaBirth' values; the
--- renderer never chooses thresholds independently of the filtration.
-data AlphaEclipseReceipt = AlphaEclipseReceipt
-  { eclipseSiteCount :: !Int,
-    eclipseEdgeCount :: !Int,
-    eclipseFaceCount :: !Int,
-    eclipseHoleBirthNumerator :: !Integer,
-    eclipseHoleBirthDenominator :: !Integer,
-    eclipseHoleDeathNumerator :: !Integer,
-    eclipseHoleDeathDenominator :: !Integer,
-    eclipseBettiBeforeBirth :: !Int,
-    eclipseBettiAtBirth :: !Int,
-    eclipseBettiAtDeath :: !Int
-  }
-  deriving stock (Eq, Show)
-
--- | A three-section observation of one fixed resident Delaunay DCEL. Every
--- displayed edge and face is projected from that DCEL with its exact birth
--- rank; only the selected sublevel differs between sections.
-data AlphaEclipseScene = AlphaEclipseScene
-  { eclipseSites :: ![Point],
-    eclipseEdges :: ![RankedSegment],
-    eclipseFaces :: ![RankedTriangle],
-    eclipseBeforeState :: !CriticalState,
-    eclipseBirthState :: !CriticalState,
-    eclipseDeathState :: !CriticalState,
-    eclipseReceipt :: !AlphaEclipseReceipt
-  }
-
--- | The shared local descent used by both exhibits. This is not a second
--- filtration carrier: it retains the canonical mesh and filtration beside
--- their checked Homology projection and only the ranked rendering views.
-data AlphaProjection = AlphaProjection
-  { projectionTriangulation :: !(DelaunayTriangulation ()),
-    projectionPersistencePairs :: ![PersistencePair AlphaBirth],
-    projectionCriticalStates :: !(NonEmpty CriticalState),
-    projectionBirthRanks :: !(Map AlphaBirth Int),
-    projectionEdges :: ![RankedSegment],
-    projectionFaces :: ![RankedTriangle]
-  }
-
-newtype PersistenceRoseFrame = PersistenceRoseFrame
-  { frameProgress :: Double
-  }
-  deriving stock (Eq, Show)
-
-data CriticalState = CriticalState
-  { criticalRank :: !Int,
-    criticalBirth :: !AlphaBirth,
-    criticalBettiOne :: !Int,
-    criticalHoles :: ![HoleComponent]
-  }
-
-data RankedSegment = RankedSegment
-  { rankedSegmentGeometry :: !Segment,
-    rankedSegmentBirthRank :: !Int
-  }
-
-data RankedTriangle = RankedTriangle
-  { rankedTrianglePoints :: !(Point, Point, Point),
-    rankedTriangleBirthRank :: !Int
-  }
-
-data Segment = Segment !Point !Point
-
-newtype HoleComponent = HoleComponent [[Point]]
-
-data FieldSample = FieldSample
-  { fieldSamplePoint :: !Point,
-    fieldSampleValue :: !Double,
-    fieldSampleNeighborCount :: !Int
-  }
-
-data MaskComponent = MaskComponent
-  { maskOuterLoop :: ![Point],
-    maskHoleLoops :: ![[Point]]
-  }
-
-data PersistenceArc = PersistenceArc
-  { persistenceArcBirthRank :: !Int,
-    persistenceArcDeathRank :: !Int
-  }
-
-buildPersistenceRoseScene :: Either PersistenceRoseError PersistenceRoseScene
-buildPersistenceRoseScene = do
-  projection <- buildAlphaProjection roseSites
-  let triangulation = projectionTriangulation projection
-      criticalStates = projectionCriticalStates projection
-      persistencePairs = projectionPersistencePairs projection
-      criticalBirthCount = NonEmpty.length criticalStates
-  heroState <- chooseHeroState criticalStates
-  finiteVoronoiSegments <- projectFiniteVoronoiSegments triangulation
-  (fieldSamples, fallbackCount) <- naturalNeighborField triangulation
-  if fallbackCount == 0
-    then pure ()
-    else Left (RoseNaturalNeighborFallbacks fallbackCount)
-  maskComponents <- buildRoseMask
-  persistenceArcs <-
-    projectPersistenceArcs
-      (projectionBirthRanks projection)
-      criticalBirthCount
-      persistencePairs
-  let finiteHoles = filter isFinitePositiveHole persistencePairs
-      essentialHoles = filter isEssentialHole persistencePairs
-      heroHoleCount = length (criticalHoles heroState)
-      receipt =
-        PersistenceRoseReceipt
-          { roseSiteCount = length (vertices triangulation),
-            roseEdgeCount = length (projectionEdges projection),
-            roseFaceCount = length (projectionFaces projection),
-            roseCriticalBirthCount = criticalBirthCount,
-            roseFinitePersistentHoleCount = length finiteHoles,
-            roseEssentialPersistentHoleCount = length essentialHoles,
-            roseHeroCriticalRank = criticalRank heroState,
-            roseHeroBettiOne = criticalBettiOne heroState,
-            roseHeroGeometricHoleCount = heroHoleCount,
-            roseVoronoiSegmentCount = length finiteVoronoiSegments,
-            roseNaturalNeighborSampleCount = length fieldSamples,
-            roseNaturalNeighborFallbackCount = fallbackCount,
-            roseMaskComponentCount = length maskComponents
-          }
-  pure
-    PersistenceRoseScene
-      { sceneSites = fmap (vertexPoint triangulation) (vertices triangulation),
-        sceneEdges = projectionEdges projection,
-        sceneFaces = projectionFaces projection,
-        sceneCriticalStates = criticalStates,
-        sceneHeroState = heroState,
-        sceneVoronoiSegments = finiteVoronoiSegments,
-        sceneFieldSamples = fieldSamples,
-        sceneMaskComponents = maskComponents,
-        scenePersistenceArcs = persistenceArcs,
-        sceneReceipt = receipt
-      }
-
-buildAlphaProjection :: [Point] -> Either PersistenceRoseError AlphaProjection
-buildAlphaProjection sites = do
-  triangulation <- first RoseDelaunayBuildFailed (delaunayGeometry (Vector.fromList sites))
-  filtration <- first RoseAlphaFiltrationFailed (alphaFiltration triangulation)
-  filteredComplex <- first RoseCellComplexLoweringFailed (filteredAlphaComplex filtration)
-  (persistencePairs, criticalBettiTable) <-
-    first RosePersistenceFailed
-      (mod2PersistentPairsWithCriticalBettiTable filteredComplex)
-  let criticalBirths = alphaFiltrationCriticalBirths filtration
-      criticalProfiles = criticalBettiVectors criticalBettiTable
-      birthRanks = Map.fromDistinctAscList (zip criticalBirths [0 ..])
-  if length criticalBirths == length criticalProfiles
-    then pure ()
-    else Left (RoseCriticalProfileCountMismatch (length criticalBirths) (length criticalProfiles))
-  criticalStates <-
-    traverse
-      (buildCriticalState triangulation filtration)
-      (zip3 [0 ..] criticalBirths criticalProfiles)
-      >>= maybe (Left RoseNoCriticalStates) Right . NonEmpty.nonEmpty
-  rankedEdges <-
-    traverse
-      (projectRankedEdge triangulation filtration birthRanks)
-      (undirectedEdges triangulation)
-  rankedFaces <-
-    traverse
-      (projectRankedFace triangulation filtration birthRanks)
-      (zip [1 ..] (Vector.toList (innerFaceVertexTriples triangulation)))
-  pure
-    AlphaProjection
-      { projectionTriangulation = triangulation,
-        projectionPersistencePairs = persistencePairs,
-        projectionCriticalStates = criticalStates,
-        projectionBirthRanks = birthRanks,
-        projectionEdges = rankedEdges,
-        projectionFaces = rankedFaces
-      }
-
-buildAlphaEclipseScene :: Either PersistenceRoseError AlphaEclipseScene
-buildAlphaEclipseScene = do
-  projection <- buildAlphaProjection alphaEclipseSites
-  finiteHoles <-
-    case filter isFinitePositiveHole (projectionPersistencePairs projection) of
-      [finiteHole] -> Right finiteHole
-      holes -> Left (RoseEclipseFiniteHoleCountMismatch (length holes))
-  deathBirth <-
-    maybe
-      (Left RoseEclipseFiniteHoleMissingDeath)
-      Right
-      (persistenceDeath finiteHoles)
-  let birth = persistenceBirth finiteHoles
-      birthRanks = projectionBirthRanks projection
-      states = projectionCriticalStates projection
-  birthRank <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-  deathRank <- maybe (Left (RoseBirthRankMissing deathBirth)) Right (Map.lookup deathBirth birthRanks)
-  let beforeState = criticalStateAt (max 0 (birthRank - 1)) states
-      birthState = criticalStateAt birthRank states
-      deathState = criticalStateAt deathRank states
-      bettiProfile =
-        ( criticalBettiOne beforeState,
-          criticalBettiOne birthState,
-          criticalBettiOne deathState
-        )
-  case bettiProfile of
-    (0, 1, 0) -> pure ()
-    (beforeBetti, birthBetti, deathBetti) ->
-      Left (RoseEclipseBettiProfileMismatch beforeBetti birthBetti deathBetti)
-  let triangulation = projectionTriangulation projection
-      receipt =
-        AlphaEclipseReceipt
-          { eclipseSiteCount = length (vertices triangulation),
-            eclipseEdgeCount = length (projectionEdges projection),
-            eclipseFaceCount = length (projectionFaces projection),
-            eclipseHoleBirthNumerator = alphaBirthNumerator birth,
-            eclipseHoleBirthDenominator = alphaBirthDenominator birth,
-            eclipseHoleDeathNumerator = alphaBirthNumerator deathBirth,
-            eclipseHoleDeathDenominator = alphaBirthDenominator deathBirth,
-            eclipseBettiBeforeBirth = criticalBettiOne beforeState,
-            eclipseBettiAtBirth = criticalBettiOne birthState,
-            eclipseBettiAtDeath = criticalBettiOne deathState
-          }
-  pure
-    AlphaEclipseScene
-      { eclipseSites = fmap (vertexPoint triangulation) (vertices triangulation),
-        eclipseEdges = projectionEdges projection,
-        eclipseFaces = projectionFaces projection,
-        eclipseBeforeState = beforeState,
-        eclipseBirthState = birthState,
-        eclipseDeathState = deathState,
-        eclipseReceipt = receipt
-      }
-
-persistenceRoseReceipt :: PersistenceRoseScene -> PersistenceRoseReceipt
-persistenceRoseReceipt = sceneReceipt
-
-alphaEclipseReceipt :: AlphaEclipseScene -> AlphaEclipseReceipt
-alphaEclipseReceipt = eclipseReceipt
-
-renderPersistenceRoseReceipt :: PersistenceRoseReceipt -> String
-renderPersistenceRoseReceipt receipt =
-  intercalate
-    " · "
-    [ "sites " <> show (roseSiteCount receipt),
-      "edges " <> show (roseEdgeCount receipt),
-      "faces " <> show (roseFaceCount receipt),
-      "critical births " <> show (roseCriticalBirthCount receipt),
-      "finite H1 intervals " <> show (roseFinitePersistentHoleCount receipt),
-      "essential H1 intervals " <> show (roseEssentialPersistentHoleCount receipt),
-      "hero rank " <> show (roseHeroCriticalRank receipt),
-      "beta1 " <> show (roseHeroBettiOne receipt),
-      "geometric holes " <> show (roseHeroGeometricHoleCount receipt),
-      "Voronoi segments " <> show (roseVoronoiSegmentCount receipt),
-      "Sibson samples " <> show (roseNaturalNeighborSampleCount receipt),
-      "fallbacks " <> show (roseNaturalNeighborFallbackCount receipt),
-      "mask components " <> show (roseMaskComponentCount receipt)
-    ]
-
-renderAlphaEclipseReceipt :: AlphaEclipseReceipt -> String
-renderAlphaEclipseReceipt receipt =
-  intercalate
-    " · "
-    [ "sites " <> show (eclipseSiteCount receipt),
-      "edges " <> show (eclipseEdgeCount receipt),
-      "faces " <> show (eclipseFaceCount receipt),
-      "H1 ["
-        <> renderExactRatio (eclipseHoleBirthNumerator receipt) (eclipseHoleBirthDenominator receipt)
-        <> ", "
-        <> renderExactRatio (eclipseHoleDeathNumerator receipt) (eclipseHoleDeathDenominator receipt)
-        <> ")",
-      "beta1 "
-        <> show
-          ( eclipseBettiBeforeBirth receipt,
-            eclipseBettiAtBirth receipt,
-            eclipseBettiAtDeath receipt
-          )
-    ]
-
-buildCriticalState
-  :: DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> (Int, AlphaBirth, [Int])
-  -> Either PersistenceRoseError CriticalState
-buildCriticalState triangulation filtration (rankValue, birthValue, profile) = do
-  bettiOne <-
-    case profile of
-      _ : secondDegree : _ : _ -> Right secondDegree
-      _ -> Left (RoseCriticalProfileDegreeCountMismatch rankValue (length profile))
-  holes <- holeComponentsAt rankValue triangulation filtration birthValue
-  pure
-    CriticalState
-      { criticalRank = rankValue,
-        criticalBirth = birthValue,
-        criticalBettiOne = bettiOne,
-        criticalHoles = holes
-      }
-
-chooseHeroState :: NonEmpty CriticalState -> Either PersistenceRoseError CriticalState
-chooseHeroState states =
-  let stateList = NonEmpty.toList states
-      maximumBettiOne = foldl' (\best state -> max best (criticalBettiOne state)) 0 stateList
-      maximalStates = filter ((== maximumBettiOne) . criticalBettiOne) stateList
-      agreements = fmap (\state -> (criticalBettiOne state, length (criticalHoles state))) maximalStates
-      coherentStates =
-        filter
-          (\state -> criticalBettiOne state > 0 && criticalBettiOne state == length (criticalHoles state))
-          stateList
-      sevenPetalStates = filter ((== rosePetalCount) . criticalBettiOne) coherentStates
-   in if maximumBettiOne <= 0
-        then Left RoseNoPersistentHole
-        else
-          case NonEmpty.nonEmpty sevenPetalStates of
-            Just preferredStates -> Right (latestCriticalState preferredStates)
-            Nothing ->
-              maybe
-                (Left (RoseNoCoherentHero maximumBettiOne agreements))
-                Right
-                (find (\state -> criticalBettiOne state == length (criticalHoles state)) maximalStates)
-
-latestCriticalState :: NonEmpty CriticalState -> CriticalState
-latestCriticalState (firstState :| remainingStates) =
-  foldl'
-    (\latest candidate -> if criticalRank candidate > criticalRank latest then candidate else latest)
-    firstState
-    remainingStates
-
-holeComponentsAt
-  :: Int
-  -> DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> AlphaBirth
-  -> Either PersistenceRoseError [HoleComponent]
-holeComponentsAt rankValue triangulation filtration threshold = do
-  inactiveEdges <-
-    inactiveHandlesAt
-      RoseEdgeBirthMissing
-      (alphaEdgeBirth filtration)
-      (undirectedEdges triangulation)
-      threshold
-  inactiveFaces <-
-    inactiveHandlesAt
-      RoseFaceBirthMissing
-      (alphaFaceBirth filtration)
-      (innerFaces triangulation)
-      threshold
-  let edgeInactive edge = Set.member edge inactiveEdges
-      faceInactive face = Set.member face inactiveFaces
-      exteriorFaces = Set.fromList (outerFaceFloodFill triangulation edgeInactive)
-      inactiveComponents =
-        fmap snd
-          ( faceComponentsBy
-              triangulation
-              (\face -> if faceInactive face then Just () else Nothing)
-              edgeInactive
-          )
-      boundedComponents =
-        filter
-          (all (`Set.notMember` exteriorFaces) . faceComponentFaces)
-          inactiveComponents
-  traverse
-    (projectHoleComponent rankValue triangulation)
-    boundedComponents
-
-inactiveHandlesAt
-  :: Ord handle
-  => (handle -> PersistenceRoseError)
-  -> (handle -> Maybe AlphaBirth)
-  -> [handle]
-  -> AlphaBirth
-  -> Either PersistenceRoseError (Set.Set handle)
-inactiveHandlesAt missingBirth birthAt handles threshold =
-  Set.fromList . mapMaybe id
-    <$> traverse
-      ( \handle ->
-          maybe
-            (Left (missingBirth handle))
-            (\birth -> Right (if birth > threshold then Just handle else Nothing))
-            (birthAt handle)
-      )
-      handles
-
-projectHoleComponent
-  :: Int
-  -> DelaunayTriangulation ()
-  -> FaceComponent
-  -> Either PersistenceRoseError HoleComponent
-projectHoleComponent rankValue triangulation component = do
-  loops <-
-    first (RoseBoundaryDescentFailed rankValue)
-      (componentBoundaryLoops triangulation component)
-  pure
-    ( HoleComponent
-        ( fmap
-            (fmap (vertexPoint triangulation) . NonEmpty.toList . boundaryLoopVertices)
-            (NonEmpty.toList loops)
-        )
-    )
-
-projectRankedEdge
-  :: DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> Map AlphaBirth Int
-  -> UndirectedEdgeId
-  -> Either PersistenceRoseError RankedSegment
-projectRankedEdge triangulation filtration birthRanks edge = do
-  birth <- maybe (Left (RoseEdgeBirthMissing edge)) Right (alphaEdgeBirth filtration edge)
-  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-  let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-  pure
-    RankedSegment
-      { rankedSegmentGeometry = Segment (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex),
-        rankedSegmentBirthRank = rankValue
-      }
-
-projectRankedFace
-  :: DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> Map AlphaBirth Int
-  -> (Int, (VertexId, VertexId, VertexId))
-  -> Either PersistenceRoseError RankedTriangle
-projectRankedFace triangulation filtration birthRanks (rawFace, (firstVertex, secondVertex, thirdVertex)) = do
-  let face = toFaceId rawFace
-  birth <- maybe (Left (RoseFaceBirthMissing face)) Right (alphaFaceBirth filtration face)
-  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-  pure
-    RankedTriangle
-      { rankedTrianglePoints =
-          ( vertexPoint triangulation firstVertex,
-            vertexPoint triangulation secondVertex,
-            vertexPoint triangulation thirdVertex
-          ),
-        rankedTriangleBirthRank = rankValue
-      }
-
-toFaceId :: Int -> FaceId
-toFaceId = FaceId . fromIntegral
-
-projectFiniteVoronoiSegments
-  :: DelaunayTriangulation ()
-  -> Either PersistenceRoseError [Segment]
-projectFiniteVoronoiSegments triangulation =
-  fmap (mapMaybe id)
-    . traverse projectSegment
-    $ undirectedEdges triangulation
- where
-  projectSegment :: UndirectedEdgeId -> Either PersistenceRoseError (Maybe Segment)
-  projectSegment edge =
-    case voronoiEdgeGeometry triangulation (asDirectedVoronoiEdge (normalizedDirected edge)) of
-      Nothing -> Left (RoseVoronoiGeometryMissing edge)
-      Just (VoronoiSegment fromPoint toPoint) -> Right (Just (Segment fromPoint toPoint))
-      Just (VoronoiRay _ _) -> Right Nothing
-      Just (VoronoiLine _ _) -> Right Nothing
-
-naturalNeighborField
-  :: DelaunayTriangulation ()
-  -> Either PersistenceRoseError ([FieldSample], Int)
-naturalNeighborField triangulation = do
-  admittedQueries <- traverse (first RosePointInvalid . mkQueryPoint) fieldQueryPoints
-  let results = runST $ do
-        workspace <- newNaturalNeighborWorkspace triangulation
-        traverse (naturalNeighborWeights workspace Nothing) admittedQueries
-      pairedResults = zip admittedQueries results
-      fallbackCount = length (filter (interpolationUsedFallback . naturalNeighborStats . snd) pairedResults)
-      samples = mapMaybe (fieldSampleFromResult triangulation) pairedResults
-  pure (samples, fallbackCount)
-
-fieldSampleFromResult
-  :: DelaunayTriangulation ()
-  -> (QueryPoint, NaturalNeighborResult)
-  -> Maybe FieldSample
-fieldSampleFromResult triangulation (query, result)
-  | Vector.null weights = Nothing
-  | interpolationUsedFallback (naturalNeighborStats result) = Nothing
-  | otherwise =
-      let weightedValue =
-            Vector.foldl'
-              (\total (vertex, weight) -> total + weight * siteSignal (vertexPoint triangulation vertex))
-              0
-              weights
-       in Just
-            FieldSample
-              { fieldSamplePoint = queryPointValue query,
-                fieldSampleValue = clamp 0 1 weightedValue,
-                fieldSampleNeighborCount = Vector.length weights
-              }
- where
-  weights = naturalNeighborValues result
-
-siteSignal :: Point -> Double
-siteSignal (Point x y) =
-  clamp 0 1 (0.5 + 0.24 * sin (1.31 * x - 0.73 * y) + 0.18 * cos (0.61 * x + 1.17 * y))
-
-fieldQueryPoints :: [Point]
-fieldQueryPoints =
-  [ Point (fromIntegral column * 0.34 - 5.1) (fromIntegral row * 0.34 - 4.42)
-  | row <- [0 .. 26 :: Int],
-    column <- [0 .. 30 :: Int]
-  ]
-
-projectPersistenceArcs
-  :: Map AlphaBirth Int
-  -> Int
-  -> [PersistencePair AlphaBirth]
-  -> Either PersistenceRoseError [PersistenceArc]
-projectPersistenceArcs birthRanks criticalCount pairs = do
-  arcs <- traverse pairArc (filter isPositiveHole pairs)
-  pure (take 36 (sortOn (Down . persistenceArcLifetime) arcs))
- where
-  finalRank = max 0 (criticalCount - 1)
-  pairArc pair = do
-    birthRank <- rankOf (persistenceBirth pair)
-    deathRank <- maybe (Right finalRank) rankOf (persistenceDeath pair)
-    pure
-      PersistenceArc
-        { persistenceArcBirthRank = birthRank,
-          persistenceArcDeathRank = deathRank
-        }
-  rankOf birth = maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-
-persistenceArcLifetime :: PersistenceArc -> Int
-persistenceArcLifetime PersistenceArc{persistenceArcBirthRank, persistenceArcDeathRank} =
-  max 0 (persistenceArcDeathRank - persistenceArcBirthRank)
-
-isPositiveHole :: PersistencePair AlphaBirth -> Bool
-isPositiveHole pair =
-  persistenceDegree pair == HomologicalDegree 1
-    && maybe True (> persistenceBirth pair) (persistenceDeath pair)
-
-isFinitePositiveHole :: PersistencePair AlphaBirth -> Bool
-isFinitePositiveHole pair = isPositiveHole pair && maybe False (> persistenceBirth pair) (persistenceDeath pair)
-
-isEssentialHole :: PersistencePair AlphaBirth -> Bool
-isEssentialHole pair = persistenceDegree pair == HomologicalDegree 1 && persistenceDeath pair == Nothing
-
-buildRoseMask :: Either PersistenceRoseError [MaskComponent]
-buildRoseMask = do
-  kernelPoints <- traverse (first RosePointInvalid . exactPointFromPoint) minkowskiKernel
-  kernel <- first RoseMinkowskiFailed (convexPolygon kernelPoints >>= structuringElement)
-  authoredRegions <- traverse authoredRegion roseMaskLoops
-  offsetRegions <-
-    traverse
-      (fmap fst . first RoseMinkowskiFailed . polygonOffset kernel)
-      authoredRegions
-  unioned <-
-    case NonEmpty.nonEmpty offsetRegions of
-      Nothing -> Left RoseNoMaskRegions
-      Just (firstRegion :| remainingRegions) -> foldM exactRegionUnion firstRegion remainingRegions
-  traverse projectMaskComponent (planarRegionComponents unioned)
-
-authoredRegion :: NonEmpty Point -> Either PersistenceRoseError PlanarRegion
-authoredRegion points = do
-  exactPoints <- traverse (first RosePointInvalid . exactPointFromPoint) points
-  loop <- first RoseRegionInvalid (exactLoop exactPoints)
-  component <- first RoseRegionInvalid (polygonComponent loop [])
-  first RoseRegionInvalid (planarRegion [component])
-
-exactRegionUnion :: PlanarRegion -> PlanarRegion -> Either PersistenceRoseError PlanarRegion
-exactRegionUnion leftRegion rightRegion = do
-  leftLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True leftRegion))
-  rightLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True rightRegion))
-  overlaid <- first RoseOverlayFailed (overlayLayers leftLayer rightLayer)
-  first RoseOverlayPublicationFailed (overlaySelectedRegion (uncurry (||)) overlaid)
-
-projectMaskComponent :: PolygonComponent -> Either PersistenceRoseError MaskComponent
-projectMaskComponent component =
-  MaskComponent
-    <$> projectExactLoop (polygonOuterLoop component)
-    <*> traverse projectExactLoop (polygonHoleLoops component)
-
-projectExactLoop :: ExactLoop -> Either PersistenceRoseError [Point]
-projectExactLoop =
-  traverse
-    (first RosePointInvalid . exactPointToEmbeddingCandidate)
-    . NonEmpty.toList
-    . exactLoopPoints
-
-roseSites :: [Point]
-roseSites =
-  concatMap petalSites [0 .. rosePetalCount - 1]
-    <> centerSites
-
-petalSites :: Int -> [Point]
-petalSites petalIndex =
-  fmap
-    (petalPoint petalIndex 2.52 1.82 0.69)
-    (sampleAngles 22 (0.021 * fromIntegral petalIndex))
-
-centerSites :: [Point]
-centerSites =
-  Point 0 0
-    : fmap
-      (\angle -> Point (0.56 * cos angle) (0.56 * sin angle))
-      (sampleAngles 14 0.037)
-
-roseMaskLoops :: [NonEmpty Point]
-roseMaskLoops =
-  fmap
-    (\petalIndex -> fmap (petalPoint petalIndex 2.42 2.12 0.9) (sampleAnglesNonEmpty 34 0))
-    [0 .. rosePetalCount - 1]
-    <> [fmap (\angle -> Point (1.08 * cos angle) (1.08 * sin angle)) (sampleAnglesNonEmpty 30 0)]
-
-petalPoint :: Int -> Double -> Double -> Double -> Double -> Point
-petalPoint petalIndex centerRadius radialRadius tangentialRadius localAngle =
-  let orientation = twoPi * fromIntegral petalIndex / fromIntegral rosePetalCount - pi / 2
-      radial = centerRadius + radialRadius * cos localAngle
-      tangential = tangentialRadius * sin localAngle * (0.91 + 0.09 * cos (2 * localAngle))
-      radialX = cos orientation
-      radialY = sin orientation
-      tangentX = -radialY
-      tangentY = radialX
-   in Point
-        (radial * radialX + tangential * tangentX)
-        (radial * radialY + tangential * tangentY)
-
-sampleAngles :: Int -> Double -> [Double]
-sampleAngles sampleCount phase =
-  fmap
-    (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
-    [0 .. sampleCount - 1]
-
-sampleAnglesNonEmpty :: Int -> Double -> NonEmpty Double
-sampleAnglesNonEmpty sampleCount phase =
-  phase
-    :| fmap
-      (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
-      [1 .. max 1 sampleCount - 1]
-
-minkowskiKernel :: NonEmpty Point
-minkowskiKernel =
-  Point 0.085 0
-    :| [ Point 0.043 0.074,
-         Point (-0.043) 0.074,
-         Point (-0.085) 0,
-         Point (-0.043) (-0.074),
-         Point 0.043 (-0.074)
-       ]
-
-rosePetalCount :: Int
-rosePetalCount = 7
-
--- | The package's canonical exact persistence witness. Its four hull edges
--- enter at squared radius one; the resident diagonal and two resident faces
--- enter at squared radius two. Homology therefore observes exactly one finite
--- degree-one interval @[1,2)@ over this fixed DCEL.
-alphaEclipseSites :: [Point]
-alphaEclipseSites =
-  [ Point (-1) (-1),
-    Point 1 (-1),
-    Point 1 1,
-    Point (-1) 1
-  ]
-
-renderPersistenceRosePoster :: PersistenceRoseScene -> String
-renderPersistenceRosePoster scene =
-  renderScene scene (criticalRank (sceneHeroState scene)) 1 True
-
-renderAlphaEclipse :: AlphaEclipseScene -> String
-renderAlphaEclipse scene =
-  let receipt = eclipseReceipt scene
-      birthLabel =
-        renderExactRatio
-          (eclipseHoleBirthNumerator receipt)
-          (eclipseHoleBirthDenominator receipt)
-      deathLabel =
-        renderExactRatio
-          (eclipseHoleDeathNumerator receipt)
-          (eclipseHoleDeathDenominator receipt)
-   in concat
-        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 620\" width=\"1440\" height=\"620\" role=\"img\" aria-labelledby=\"title description\">\n",
-          "<title id=\"title\">Moonlight Triangulation alpha eclipse</title>\n",
-          "<desc id=\"description\">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius ",
-          birthLabel,
-          ", and its resident diagonal and faces kill that hole at squared radius ",
-          deathLabel,
-          ".</desc>\n",
-          renderAlphaEclipseDefinitions,
-          "<rect width=\"1440\" height=\"620\" fill=\"url(#eclipse-background)\"/>\n",
-          "<circle cx=\"720\" cy=\"300\" r=\"246\" fill=\"#ff68bb\" opacity=\"0.035\" filter=\"url(#eclipse-soft-glow)\"/>\n",
-          "<text x=\"58\" y=\"58\" fill=\"#84f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"17\" font-weight=\"600\" letter-spacing=\"6\">MOONLIGHT TRIANGULATION</text>\n",
-          "<text x=\"58\" y=\"101\" fill=\"#f7f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"38\" font-weight=\"420\">ALPHA ECLIPSE</text>\n",
-          "<text x=\"1382\" y=\"61\" text-anchor=\"end\" fill=\"#9baedb\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.5\">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>\n",
-          renderAlphaEclipseStage
-            scene
-            245
-            "BEFORE BIRTH"
-            "four isolated sites"
-            (eclipseBeforeState scene),
-          renderAlphaEclipseStage
-            scene
-            720
-            "LOOP BORN"
-            "resident boundary closes"
-            (eclipseBirthState scene),
-          renderAlphaEclipseStage
-            scene
-            1195
-            "CYCLE KILLED"
-            "resident faces fill it"
-            (eclipseDeathState scene),
-          "<g font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\">\n",
-          "<text x=\"58\" y=\"516\" fill=\"#9badd8\" font-size=\"13\" letter-spacing=\"2.5\">ONE EXACT H₁ INTERVAL</text>\n",
-          "<line x1=\"58\" y1=\"559\" x2=\"1382\" y2=\"559\" stroke=\"#344070\" stroke-width=\"3\"/>\n",
-          "<line x1=\"535\" y1=\"559\" x2=\"1042\" y2=\"559\" stroke=\"#ff72c6\" stroke-width=\"9\" stroke-linecap=\"round\"/>\n",
-          "<circle cx=\"535\" cy=\"559\" r=\"8\" fill=\"#fff6d6\" stroke=\"#ff72c6\" stroke-width=\"3\"/>\n",
-          "<rect x=\"1034\" y=\"551\" width=\"16\" height=\"16\" fill=\"#fff6d6\" stroke=\"#66efff\" stroke-width=\"3\"/>\n",
-          "<text x=\"58\" y=\"589\" fill=\"#7588ba\" font-size=\"12\">α² = 0</text>\n",
-          "<text x=\"535\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">birth α² = ",
-          birthLabel,
-          "</text>\n",
-          "<text x=\"788\" y=\"548\" text-anchor=\"middle\" fill=\"#fff6d6\" font-size=\"14\">the eclipse hole persists</text>\n",
-          "<text x=\"1042\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">death α² = ",
-          deathLabel,
-          "</text>\n",
-          "<text x=\"1382\" y=\"589\" text-anchor=\"end\" fill=\"#7588ba\" font-size=\"12\">exact α² increases →</text>\n",
-          "</g>\n",
-          "</svg>\n"
-        ]
-
-renderAlphaEclipseDefinitions :: String
-renderAlphaEclipseDefinitions =
-  concat
-    [ "<defs>\n",
-      "<radialGradient id=\"eclipse-background\" cx=\"50%\" cy=\"38%\" r=\"78%\"><stop offset=\"0\" stop-color=\"#111744\"/><stop offset=\"0.55\" stop-color=\"#070b25\"/><stop offset=\"1\" stop-color=\"#02040f\"/></radialGradient>\n",
-      "<linearGradient id=\"eclipse-corona\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#66efff\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#ffd86b\"/></linearGradient>\n",
-      "<linearGradient id=\"eclipse-face\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#7c4dff\"/><stop offset=\"0.5\" stop-color=\"#ff4fa3\"/><stop offset=\"1\" stop-color=\"#ffbf5b\"/></linearGradient>\n",
-      "<filter id=\"eclipse-soft-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "<filter id=\"eclipse-dot-glow\" x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "</defs>\n"
-    ]
-
-renderAlphaEclipseStage
-  :: AlphaEclipseScene
-  -> Double
-  -> String
-  -> String
-  -> CriticalState
-  -> String
-renderAlphaEclipseStage scene centerX stageLabel explanation state =
-  let rankValue = criticalRank state
-      activeFaces = filter ((<= rankValue) . rankedTriangleBirthRank) (eclipseFaces scene)
-      activeEdges = filter ((<= rankValue) . rankedSegmentBirthRank) (eclipseEdges scene)
-      highlighted = criticalBettiOne state > 0
-      frameStroke = if highlighted then "#ff6fbd" else "#344070"
-      frameOpacity = if highlighted then "0.72" else "0.55"
-   in concat
-        [ "<g transform=\"translate(",
-          formatDouble centerX,
-          " 292)\">\n",
-          "<rect x=\"-194\" y=\"-150\" width=\"388\" height=\"292\" rx=\"28\" fill=\"#090d26\" fill-opacity=\"0.72\" stroke=\"",
-          frameStroke,
-          "\" stroke-opacity=\"",
-          frameOpacity,
-          "\"/>\n",
-          "<text x=\"-162\" y=\"-114\" fill=\"",
-          if highlighted then "#ff89cb" else "#8fa4d6",
-          "\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"13\" letter-spacing=\"2.4\">",
-          stageLabel,
-          "</text>\n",
-          "<circle r=\"104\" fill=\"#01020a\" stroke=\"#7fefff\" stroke-opacity=\"0.18\" stroke-width=\"2\"/>\n",
-          "<g fill=\"url(#eclipse-face)\" fill-opacity=\"0.58\" stroke=\"#ffd56d\" stroke-opacity=\"0.48\" stroke-width=\"1.2\">",
-          concatMap renderAlphaEclipseTriangle activeFaces,
-          "</g>\n",
-          "<g fill=\"none\" stroke=\"url(#eclipse-corona)\" stroke-width=\"5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" filter=\"url(#eclipse-soft-glow)\">",
-          concatMap renderAlphaEclipseSegment activeEdges,
-          "</g>\n",
-          "<g fill=\"none\" stroke=\"#fff7da\" stroke-width=\"3.2\" stroke-linejoin=\"round\" opacity=\"0.86\">",
-          concatMap renderAlphaEclipseHole (criticalHoles state),
-          "</g>\n",
-          "<g fill=\"#fff8df\" stroke=\"#ff75c5\" stroke-width=\"2\" filter=\"url(#eclipse-dot-glow)\">",
-          concatMap renderAlphaEclipseSite (eclipseSites scene),
-          "</g>\n",
-          "<rect x=\"-126\" y=\"-18\" width=\"252\" height=\"36\" rx=\"18\" fill=\"#05071a\" fill-opacity=\"0.82\"/>\n",
-          "<text y=\"5\" text-anchor=\"middle\" fill=\"#fff7da\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.2\">",
-          explanation,
-          "</text>\n",
-          "<text x=\"-162\" y=\"121\" fill=\"#fff4cc\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"25\">β₁ = ",
-          show (criticalBettiOne state),
-          "</text>\n",
-          "<text x=\"162\" y=\"121\" text-anchor=\"end\" fill=\"#8495c4\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"12\">α² = ",
-          renderAlphaBirth (criticalBirth state),
-          "</text>\n",
-          "</g>\n"
-        ]
-
-renderAlphaEclipseTriangle :: RankedTriangle -> String
-renderAlphaEclipseTriangle RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint)} =
-  "<polygon points=\""
-    <> renderScreenPoints (fmap toAlphaEclipseLocal [firstPoint, secondPoint, thirdPoint])
-    <> "\"/>\n"
-
-renderAlphaEclipseSegment :: RankedSegment -> String
-renderAlphaEclipseSegment RankedSegment{rankedSegmentGeometry = Segment firstPoint secondPoint} =
-  let Point firstX firstY = toAlphaEclipseLocal firstPoint
-      Point secondX secondY = toAlphaEclipseLocal secondPoint
-   in "<line x1=\""
-        <> formatDouble firstX
-        <> "\" y1=\""
-        <> formatDouble firstY
-        <> "\" x2=\""
-        <> formatDouble secondX
-        <> "\" y2=\""
-        <> formatDouble secondY
-        <> "\"/>\n"
-
-renderAlphaEclipseHole :: HoleComponent -> String
-renderAlphaEclipseHole (HoleComponent loops) =
-  concatMap
-    (\points -> "<path d=\"" <> renderClosedPath (fmap toAlphaEclipseLocal points) <> "\"/>\n")
-    loops
-
-renderAlphaEclipseSite :: Point -> String
-renderAlphaEclipseSite point =
-  let Point x y = toAlphaEclipseLocal point
-   in "<circle cx=\""
-        <> formatDouble x
-        <> "\" cy=\""
-        <> formatDouble y
-        <> "\" r=\"6\"/>\n"
-
-toAlphaEclipseLocal :: Point -> Point
-toAlphaEclipseLocal (Point x y) =
-  let scale = 72
-      inverseSquareRootTwo = sqrt 0.5
-   in Point
-        (scale * inverseSquareRootTwo * (x - y))
-        (scale * inverseSquareRootTwo * (x + y))
-
-renderAlphaBirth :: AlphaBirth -> String
-renderAlphaBirth birth =
-  renderExactRatio (alphaBirthNumerator birth) (alphaBirthDenominator birth)
-
-renderExactRatio :: Integer -> Integer -> String
-renderExactRatio numerator denominator
-  | denominator == 1 = show numerator
-  | otherwise = show numerator <> "/" <> show denominator
-
-persistenceRoseFrameSpecs :: Int -> Either PersistenceRoseError [PersistenceRoseFrame]
-persistenceRoseFrameSpecs frameCount
-  | frameCount < 2 = Left (RoseFrameCountTooSmall frameCount)
-  | otherwise =
-      Right
-        ( fmap
-            (\frameIndex ->
-               PersistenceRoseFrame
-                 { frameProgress = frameProgressAt frameCount frameIndex
-                 })
-            [0 .. frameCount - 1]
-        )
-
-renderPersistenceRoseFrame :: PersistenceRoseScene -> PersistenceRoseFrame -> String
-renderPersistenceRoseFrame scene frame =
-  let finalRank = criticalRank (NonEmpty.last (sceneCriticalStates scene))
-      selectedRank = clampInt 0 finalRank (floor (frameProgress frame * fromIntegral finalRank))
-   in renderScene scene selectedRank (frameProgress frame) False
-
-persistenceRoseAnimationReceipt
-  :: PersistenceRoseScene
-  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
-persistenceRoseAnimationReceipt scene = do
-  frames <- persistenceRoseFrameSpecs canonicalAnimationFrameCount
-  let renderedFrames = fmap (renderPersistenceRoseFrame scene) frames
-  pure
-    PersistenceRoseAnimationReceipt
-      { roseAnimationFrameCount = canonicalAnimationFrameCount,
-        roseAnimationFramesPerSecond = canonicalAnimationFramesPerSecond,
-        roseAnimationWidth = canonicalAnimationWidth,
-        roseAnimationHeight = canonicalAnimationHeight,
-        roseAnimationSourceFingerprint = fingerprintSections renderedFrames
-      }
-
-renderPersistenceRoseAnimationReceipt :: PersistenceRoseAnimationReceipt -> String
-renderPersistenceRoseAnimationReceipt receipt =
-  intercalate
-    " "
-    [ "MOONLIGHT_PERSISTENCE_ROSE",
-      "frame-count=" <> show (roseAnimationFrameCount receipt),
-      "fps=" <> show (roseAnimationFramesPerSecond receipt),
-      "size=" <> show (roseAnimationWidth receipt) <> "x" <> show (roseAnimationHeight receipt),
-      "source-fnv1a64=" <> renderWord64Hex (roseAnimationSourceFingerprint receipt)
-    ]
-
-stampPersistenceRoseGif
-  :: PersistenceRoseScene
-  -> ByteString
-  -> Either PersistenceRoseError (PersistenceRoseAnimationReceipt, ByteString)
-stampPersistenceRoseGif scene gifBytes = do
-  receipt <- persistenceRoseAnimationReceipt scene
-  summary <- inspectGif gifBytes
-  validateGifSummary receipt summary
-  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
-      markerLength = ByteString.length marker
-  if markerLength > 255
-    then Left (RoseGifMarkerTooLong markerLength)
-    else
-      if marker `ByteString.isInfixOf` gifBytes
-        then Right (receipt, gifBytes)
-        else
-          if gifMarkerPrefix `ByteString.isInfixOf` gifBytes
-            then Left RoseGifProvenanceMismatch
-            else do
-              insertionOffset <- gifDataStart gifBytes
-              let (prefix, suffix) = ByteString.splitAt insertionOffset gifBytes
-                  commentExtension =
-                    ByteString.pack [0x21, 0xfe, fromIntegral markerLength]
-                      <> marker
-                      <> ByteString.singleton 0
-              pure (receipt, prefix <> commentExtension <> suffix)
-
-verifyPersistenceRoseGif
-  :: PersistenceRoseScene
-  -> ByteString
-  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
-verifyPersistenceRoseGif scene gifBytes = do
-  receipt <- persistenceRoseAnimationReceipt scene
-  summary <- inspectGif gifBytes
-  validateGifSummary receipt summary
-  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
-  if marker `ByteString.isInfixOf` gifBytes
-    then Right receipt
-    else Left RoseGifProvenanceMismatch
-
-data GifSummary = GifSummary
-  { gifSummaryWidth :: !Int,
-    gifSummaryHeight :: !Int,
-    gifSummaryFrameCount :: !Int,
-    gifSummaryFrameDelayCount :: !Int,
-    gifSummaryDurationCentiseconds :: !Int
-  }
-
-inspectGif :: ByteString -> Either PersistenceRoseError GifSummary
-inspectGif gifBytes = do
-  dataStart <- gifDataStart gifBytes
-  width <- gifLittleEndianWord16 gifBytes 6
-  height <- gifLittleEndianWord16 gifBytes 8
-  (frameCount, frameDelayCount, durationCentiseconds) <-
-    countGifFrames gifBytes dataStart 0 0 0
-  pure
-    GifSummary
-      { gifSummaryWidth = width,
-        gifSummaryHeight = height,
-        gifSummaryFrameCount = frameCount,
-        gifSummaryFrameDelayCount = frameDelayCount,
-        gifSummaryDurationCentiseconds = durationCentiseconds
-      }
-
-gifDataStart :: ByteString -> Either PersistenceRoseError Int
-gifDataStart gifBytes = do
-  signature <- maybe (Left (RoseGifTruncated 0)) Right (gifSlice gifBytes 0 6)
-  if signature == ByteString.Char8.pack "GIF87a" || signature == ByteString.Char8.pack "GIF89a"
-    then pure ()
-    else Left RoseGifInvalidSignature
-  packed <- maybe (Left (RoseGifTruncated 10)) Right (gifByteAt gifBytes 10)
-  let globalColourTableBytes =
-        if packed .&. 0x80 == 0
-          then 0
-          else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
-      dataStart = 13 + globalColourTableBytes
-  maybe (Left (RoseGifTruncated dataStart)) (const (Right dataStart)) (gifSlice gifBytes 0 dataStart)
-
-countGifFrames
-  :: ByteString
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Either PersistenceRoseError (Int, Int, Int)
-countGifFrames gifBytes offset frameCount frameDelayCount durationCentiseconds = do
-  block <- maybe (Left (RoseGifTruncated offset)) Right (gifByteAt gifBytes offset)
-  case block of
-    0x3b -> Right (frameCount, frameDelayCount, durationCentiseconds)
-    0x21 -> do
-      extensionLabel <- maybe (Left (RoseGifTruncated (offset + 1))) Right (gifByteAt gifBytes (offset + 1))
-      nextOffset <- skipGifSubBlocks gifBytes (offset + 2)
-      if extensionLabel == 0xf9
-        then do
-          delay <- gifLittleEndianWord16 gifBytes (offset + 4)
-          countGifFrames
-            gifBytes
-            nextOffset
-            frameCount
-            (frameDelayCount + 1)
-            (durationCentiseconds + delay)
-        else
-          countGifFrames
-            gifBytes
-            nextOffset
-            frameCount
-            frameDelayCount
-            durationCentiseconds
-    0x2c -> do
-      packed <- maybe (Left (RoseGifTruncated (offset + 9))) Right (gifByteAt gifBytes (offset + 9))
-      let localColourTableBytes =
-            if packed .&. 0x80 == 0
-              then 0
-              else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
-          imageDataStart = offset + 11 + localColourTableBytes
-      maybe (Left (RoseGifTruncated imageDataStart)) (const (Right ())) (gifByteAt gifBytes (imageDataStart - 1))
-      skipGifSubBlocks gifBytes imageDataStart >>= \next ->
-        countGifFrames
-          gifBytes
-          next
-          (frameCount + 1)
-          frameDelayCount
-          durationCentiseconds
-    0x00 ->
-      countGifFrames
-        gifBytes
-        (offset + 1)
-        frameCount
-        frameDelayCount
-        durationCentiseconds
-    unexpected -> Left (RoseGifUnexpectedBlock offset unexpected)
-
-skipGifSubBlocks :: ByteString -> Int -> Either PersistenceRoseError Int
-skipGifSubBlocks gifBytes offset = do
-  blockLength <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
-  if blockLength == 0
-    then Right (offset + 1)
-    else do
-      let nextOffset = offset + 1 + blockLength
-      maybe (Left (RoseGifTruncated nextOffset)) (const (Right ())) (gifSlice gifBytes offset (1 + blockLength))
-      skipGifSubBlocks gifBytes nextOffset
-
-validateGifSummary
-  :: PersistenceRoseAnimationReceipt
-  -> GifSummary
-  -> Either PersistenceRoseError ()
-validateGifSummary receipt summary
-  | actualWidth /= expectedWidth || actualHeight /= expectedHeight =
-      Left (RoseGifDimensionMismatch expectedWidth expectedHeight actualWidth actualHeight)
-  | actualFrameCount /= expectedFrameCount =
-      Left (RoseGifFrameCountMismatch expectedFrameCount actualFrameCount)
-  | actualFrameDelayCount /= expectedFrameCount =
-      Left (RoseGifFrameDelayCountMismatch expectedFrameCount actualFrameDelayCount)
-  | actualDurationCentiseconds /= canonicalAnimationDurationCentiseconds =
-      Left
-        ( RoseGifDurationMismatch
-            canonicalAnimationDurationCentiseconds
-            actualDurationCentiseconds
-        )
-  | otherwise = Right ()
- where
-  expectedWidth = roseAnimationWidth receipt
-  expectedHeight = roseAnimationHeight receipt
-  actualWidth = gifSummaryWidth summary
-  actualHeight = gifSummaryHeight summary
-  expectedFrameCount = roseAnimationFrameCount receipt
-  actualFrameCount = gifSummaryFrameCount summary
-  actualFrameDelayCount = gifSummaryFrameDelayCount summary
-  actualDurationCentiseconds = gifSummaryDurationCentiseconds summary
-
-gifLittleEndianWord16 :: ByteString -> Int -> Either PersistenceRoseError Int
-gifLittleEndianWord16 gifBytes offset = do
-  low <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
-  high <- maybe (Left (RoseGifTruncated (offset + 1))) (Right . fromIntegral) (gifByteAt gifBytes (offset + 1))
-  pure (low + 256 * high)
-
-gifByteAt :: ByteString -> Int -> Maybe Word8
-gifByteAt gifBytes offset = fst <$> ByteString.uncons (ByteString.drop offset gifBytes)
-
-gifSlice :: ByteString -> Int -> Int -> Maybe ByteString
-gifSlice gifBytes offset width =
-  let selected = ByteString.take width (ByteString.drop offset gifBytes)
-   in if ByteString.length selected == width then Just selected else Nothing
-
-fingerprintSections :: [String] -> Word64
-fingerprintSections =
-  foldl'
-    (\fingerprint section -> fingerprintByte (fingerprintText fingerprint section) 0)
-    fnvOffsetBasis
-
-fingerprintText :: Word64 -> String -> Word64
-fingerprintText fingerprint =
-  ByteString.foldl' fingerprintByte fingerprint
-    . Text.Encoding.encodeUtf8
-    . Text.pack
-
-fingerprintByte :: Word64 -> Word8 -> Word64
-fingerprintByte fingerprint byte =
-  (fingerprint `xor` fromIntegral byte) * fnvPrime
-
-renderWord64Hex :: Word64 -> String
-renderWord64Hex value =
-  let rendered = showHex value ""
-   in replicate (16 - length rendered) '0' <> rendered
-
-gifMarkerPrefix :: ByteString
-gifMarkerPrefix = ByteString.Char8.pack "MOONLIGHT_PERSISTENCE_ROSE "
-
-canonicalAnimationFrameCount :: Int
-canonicalAnimationFrameCount = 65
-
-canonicalAnimationFramesPerSecond :: Int
-canonicalAnimationFramesPerSecond = 8
-
-canonicalAnimationWidth :: Int
-canonicalAnimationWidth = 960
-
-canonicalAnimationHeight :: Int
-canonicalAnimationHeight = 540
-
-canonicalAnimationDurationCentiseconds :: Int
-canonicalAnimationDurationCentiseconds = 813
-
-fnvOffsetBasis :: Word64
-fnvOffsetBasis = 14695981039346656037
-
-fnvPrime :: Word64
-fnvPrime = 1099511628211
-
-frameProgressAt :: Int -> Int -> Double
-frameProgressAt frameCount frameIndex
-  | frameIndex <= 0 || frameIndex >= frameCount - 1 = 0
-  | otherwise =
-      let phase = fromIntegral frameIndex / fromIntegral (frameCount - 1)
-       in 0.5 - 0.5 * cos (twoPi * phase)
-
-renderScene :: PersistenceRoseScene -> Int -> Double -> Bool -> String
-renderScene scene selectedRank progress poster =
-  let selectedState = criticalStateAt selectedRank (sceneCriticalStates scene)
-      finalRank = max 1 (criticalRank (NonEmpty.last (sceneCriticalStates scene)))
-      activation = if poster then fromIntegral selectedRank / fromIntegral finalRank else progress
-   in concat
-        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 1080\" width=\"1920\" height=\"1080\" role=\"img\" aria-labelledby=\"title description\">\n",
-          "<title id=\"title\">Moonlight Triangulation Persistence Rose</title>\n",
-          "<desc id=\"description\">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>\n",
-          renderDefinitions (sceneMaskComponents scene),
-          "<rect width=\"1920\" height=\"1080\" fill=\"url(#background)\"/>\n",
-          renderAmbientOrbits activation,
-          renderPersistenceHalo finalRank selectedRank (scenePersistenceArcs scene),
-          "<g clip-path=\"url(#rose-clip)\">\n",
-          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"url(#rose-wash)\" opacity=\"0.28\"/>\n",
-          "<g filter=\"url(#field-soften)\">", concatMap renderFieldSample (sceneFieldSamples scene), "</g>\n",
-          "<g opacity=\"0.58\">", concatMap renderVoronoiSegment (sceneVoronoiSegments scene), "</g>\n",
-          concatMap (renderRankedTriangle finalRank activation) (sceneFaces scene),
-          concatMap (renderRankedSegment finalRank activation) (sceneEdges scene),
-          "<g filter=\"url(#hole-glow)\">", concatMap renderHoleComponent (criticalHoles selectedState), "</g>\n",
-          concatMap (renderSite activation) (sceneSites scene),
-          "</g>\n",
-          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"none\" stroke=\"url(#mask-stroke)\" stroke-width=\"2.2\" opacity=\"0.92\" filter=\"url(#edge-glow)\"/>\n",
-          renderTitle selectedState,
-          "</svg>\n"
-        ]
-
-criticalStateAt :: Int -> NonEmpty CriticalState -> CriticalState
-criticalStateAt target (firstState :| remainingStates) =
-  foldl'
-    (\chosen candidate -> if criticalRank candidate <= target then candidate else chosen)
-    firstState
-    remainingStates
-
-renderDefinitions :: [MaskComponent] -> String
-renderDefinitions maskComponents =
-  concat
-    [ "<defs>\n",
-      "<radialGradient id=\"background\" cx=\"52%\" cy=\"48%\" r=\"72%\"><stop offset=\"0\" stop-color=\"#10143d\"/><stop offset=\"0.48\" stop-color=\"#070b24\"/><stop offset=\"1\" stop-color=\"#02040e\"/></radialGradient>\n",
-      "<radialGradient id=\"rose-wash\" cx=\"48%\" cy=\"42%\" r=\"63%\"><stop offset=\"0\" stop-color=\"#ffe7a3\"/><stop offset=\"0.38\" stop-color=\"#ff5fb7\"/><stop offset=\"0.72\" stop-color=\"#7c4dff\"/><stop offset=\"1\" stop-color=\"#00d9ff\"/></radialGradient>\n",
-      "<linearGradient id=\"mask-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#ffd86b\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#66efff\"/></linearGradient>\n",
-      "<linearGradient id=\"hole-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#fff9db\"/><stop offset=\"0.52\" stop-color=\"#ff9ad7\"/><stop offset=\"1\" stop-color=\"#9ff7ff\"/></linearGradient>\n",
-      "<filter id=\"field-soften\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"24\"/></filter>\n",
-      "<filter id=\"hole-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "<filter id=\"edge-glow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "<clipPath id=\"rose-clip\"><path d=\"",
-      renderMaskPath maskComponents,
-      "\" fill-rule=\"evenodd\"/></clipPath>\n",
-      "</defs>\n"
-    ]
-
-renderAmbientOrbits :: Double -> String
-renderAmbientOrbits activation =
-  concatMap
-    ( \orbit ->
-        let radius = 322 + fromIntegral orbit * 34
-            opacity = 0.055 + activation * 0.025
-         in "<circle cx=\"960\" cy=\"552\" r=\""
-              <> formatDouble radius
-              <> "\" fill=\"none\" stroke=\"#8ba5ff\" stroke-width=\"0.8\" stroke-dasharray=\"2 12\" opacity=\""
-              <> formatDouble opacity
-              <> "\"/>\n"
-    )
-    [0 .. 5 :: Int]
-
-renderPersistenceHalo :: Int -> Int -> [PersistenceArc] -> String
-renderPersistenceHalo finalRank selectedRank arcs =
-  "<g fill=\"none\" stroke-linecap=\"round\">\n"
-    <> concatMap renderArc (zip ([0 :: Int ..]) arcs)
-    <> "</g>\n"
- where
-  renderArc :: (Int, PersistenceArc) -> String
-  renderArc (index, arc) =
-    let active = persistenceArcBirthRank arc <= selectedRank && selectedRank < persistenceArcDeathRank arc
-        radius = 414 + fromIntegral (index `mod` 7) * 7.5
-        start = rankAngle finalRank (persistenceArcBirthRank arc)
-        finish = rankAngle finalRank (persistenceArcDeathRank arc)
-        points = arcPolyline radius start finish 42
-        hue = 188 + (index * 17 `mod` 122)
-        opacity = if active then 0.72 else 0.16
-        width = if active then 2.6 else 1.1
-     in "<polyline points=\""
-          <> renderScreenPoints points
-          <> "\" stroke=\"hsl("
-          <> show hue
-          <> " 92% 72%)\" stroke-width=\""
-          <> formatDouble width
-          <> "\" opacity=\""
-          <> formatDouble opacity
-          <> "\"/>\n"
-
-rankAngle :: Int -> Int -> Double
-rankAngle finalRank rankValue =
-  -pi / 2 + twoPi * fromIntegral rankValue / fromIntegral (max 1 finalRank)
-
-arcPolyline :: Double -> Double -> Double -> Int -> [Point]
-arcPolyline radius start finish sampleCount =
-  fmap
-    ( \sampleIndex ->
-        let fraction = fromIntegral sampleIndex / fromIntegral sampleCount
-            angle = start + fraction * (finish - start)
-         in screenPointFromPolar radius angle
-    )
-    [0 .. sampleCount]
-
-screenPointFromPolar :: Double -> Double -> Point
-screenPointFromPolar radius angle =
-  Point (960 + radius * cos angle) (552 + radius * sin angle)
-
-renderFieldSample :: FieldSample -> String
-renderFieldSample FieldSample{fieldSamplePoint, fieldSampleValue, fieldSampleNeighborCount} =
-  let Point x y = toScreen fieldSamplePoint
-      hue = round (186 + 126 * fieldSampleValue) :: Int
-      radius = 30 + min 5 (fromIntegral fieldSampleNeighborCount)
-      opacity = 0.16 + 0.16 * fieldSampleValue
-   in "<circle cx=\""
-        <> formatDouble x
-        <> "\" cy=\""
-        <> formatDouble y
-        <> "\" r=\""
-        <> formatDouble radius
-        <> "\" fill=\"hsl("
-        <> show hue
-        <> " 94% 61%)\" opacity=\""
-        <> formatDouble opacity
-        <> "\"/>\n"
-
-renderVoronoiSegment :: Segment -> String
-renderVoronoiSegment segment =
-  renderLine segment "#61efff" 1.15 0.5 ""
-
-renderRankedTriangle :: Int -> Double -> RankedTriangle -> String
-renderRankedTriangle finalRank activation RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint), rankedTriangleBirthRank} =
-  let opacity = activationOpacity finalRank activation rankedTriangleBirthRank
-      hue :: Int
-      hue = 282 - round (94 * fromIntegral rankedTriangleBirthRank / fromIntegral (max 1 finalRank) :: Double)
-   in if opacity <= 0.001
-        then ""
-        else
-          "<polygon points=\""
-            <> renderWorldPoints [firstPoint, secondPoint, thirdPoint]
-            <> "\" fill=\"hsl("
-            <> show hue
-            <> " 88% 60%)\" opacity=\""
-            <> formatDouble (0.06 + 0.17 * opacity)
-            <> "\"/>\n"
-
-renderRankedSegment :: Int -> Double -> RankedSegment -> String
-renderRankedSegment finalRank activation RankedSegment{rankedSegmentGeometry, rankedSegmentBirthRank} =
-  let opacity = activationOpacity finalRank activation rankedSegmentBirthRank
-      width = 0.75 + 1.25 * opacity
-      color = if opacity > 0.72 then "#ffd56a" else "#dd78ff"
-   in renderLine rankedSegmentGeometry color width (0.06 + 0.82 * opacity) "filter=\"url(#edge-glow)\""
-
-activationOpacity :: Int -> Double -> Int -> Double
-activationOpacity finalRank activation birthRank =
-  let level = activation * fromIntegral finalRank
-      distance = level - fromIntegral birthRank
-   in smoothstep (distance + 0.6)
-
-smoothstep :: Double -> Double
-smoothstep value =
-  let bounded = clamp 0 1 value
-   in bounded * bounded * (3 - 2 * bounded)
-
-renderHoleComponent :: HoleComponent -> String
-renderHoleComponent (HoleComponent loops) =
-  concatMap
-    ( \points ->
-        "<path d=\""
-          <> renderClosedWorldPath points
-          <> "\" fill=\"none\" stroke=\"url(#hole-stroke)\" stroke-width=\"5.4\" stroke-linejoin=\"round\" opacity=\"0.98\"/>\n"
-    )
-    loops
-
-renderSite :: Double -> Point -> String
-renderSite activation point =
-  let Point x y = toScreen point
-      radius = 2.5 + 1.5 * activation
-   in "<circle cx=\""
-        <> formatDouble x
-        <> "\" cy=\""
-        <> formatDouble y
-        <> "\" r=\""
-        <> formatDouble radius
-        <> "\" fill=\"#fff8d9\" stroke=\"#ff9ad7\" stroke-width=\"1.2\" opacity=\"0.95\"/>\n"
-
-renderLine :: Segment -> String -> Double -> Double -> String -> String
-renderLine (Segment fromPoint toPoint) color width opacity extraAttributes =
-  let Point fromX fromY = toScreen fromPoint
-      Point toX toY = toScreen toPoint
-   in "<line x1=\""
-        <> formatDouble fromX
-        <> "\" y1=\""
-        <> formatDouble fromY
-        <> "\" x2=\""
-        <> formatDouble toX
-        <> "\" y2=\""
-        <> formatDouble toY
-        <> "\" stroke=\""
-        <> color
-        <> "\" stroke-width=\""
-        <> formatDouble width
-        <> "\" stroke-linecap=\"round\" opacity=\""
-        <> formatDouble opacity
-        <> "\" "
-        <> extraAttributes
-        <> "/>\n"
-
-renderTitle :: CriticalState -> String
-renderTitle state =
-  let alphaFraction = exactFractionSummary (criticalBirth state)
-      alphaDecimal = formatDouble (alphaBirthToDouble (criticalBirth state))
-      bettiMeaning = bettiOneMeaning (criticalBettiOne state)
-   in concat
-        [ "<g font-family=\"Inter, Avenir Next, Helvetica, Arial, sans-serif\" fill=\"#f7f3ff\">\n",
-          "<text x=\"72\" y=\"82\" font-size=\"24\" font-weight=\"600\" letter-spacing=\"8\" fill=\"#9ff7ff\">MOONLIGHT TRIANGULATION</text>\n",
-          "<text x=\"70\" y=\"145\" font-size=\"58\" font-weight=\"780\" letter-spacing=\"-1.5\">PERSISTENCE ROSE</text>\n",
-          "<g transform=\"translate(1500 790)\">\n",
-          "<text x=\"0\" y=\"0\" font-size=\"18\" letter-spacing=\"2.4\" fill=\"#91a8d8\">TOPOLOGICAL HOLES AT THIS SCALE</text>\n",
-          "<text x=\"0\" y=\"74\" font-size=\"78\" font-weight=\"720\" fill=\"#fff5cd\">β₁ = ",
-          show (criticalBettiOne state),
-          "</text>\n",
-          "<text x=\"2\" y=\"116\" font-size=\"17\" fill=\"#c9d3ff\">",
-          bettiMeaning,
-          "</text>\n",
-          "<text x=\"2\" y=\"144\" font-size=\"15\" fill=\"#9ba8d4\">in the exact alpha complex</text>\n",
-          "<text x=\"2\" y=\"176\" font-size=\"14\" fill=\"#7682aa\">critical rank ",
-          show (criticalRank state),
-          " · α² = ",
-          alphaDecimal,
-          "</text>\n",
-          "<text x=\"2\" y=\"200\" font-size=\"12\" fill=\"#596486\">",
-          alphaFraction,
-          "</text>\n",
-          "</g>\n",
-          "</g>\n"
-        ]
-
-bettiOneMeaning :: Int -> String
-bettiOneMeaning holeCount =
-  case holeCount of
-    0 -> "no independent one-dimensional holes"
-    1 -> "1 independent one-dimensional hole"
-    _ -> show holeCount <> " independent one-dimensional holes"
-
-exactFractionSummary :: AlphaBirth -> String
-exactFractionSummary birth =
-  let numeratorDigits = length (show (abs (alphaBirthNumerator birth)))
-      denominatorDigits = length (show (alphaBirthDenominator birth))
-   in "exact " <> show numeratorDigits <> "-digit / " <> show denominatorDigits <> "-digit rational"
-
-renderMaskPath :: [MaskComponent] -> String
-renderMaskPath =
-  intercalate " "
-    . concatMap
-      (\MaskComponent{maskOuterLoop, maskHoleLoops} -> renderClosedScreenPath maskOuterLoop : fmap renderClosedScreenPath maskHoleLoops)
-
-renderClosedWorldPath :: [Point] -> String
-renderClosedWorldPath = renderClosedPath . fmap toScreen
-
-renderClosedScreenPath :: [Point] -> String
-renderClosedScreenPath = renderClosedPath . fmap toScreen
-
-renderClosedPath :: [Point] -> String
-renderClosedPath points =
-  case points of
-    [] -> ""
-    firstPoint : remainingPoints ->
-      "M " <> renderPoint firstPoint <> concatMap (" L " <>) (fmap renderPoint remainingPoints) <> " Z"
-
-renderWorldPoints :: [Point] -> String
-renderWorldPoints = renderScreenPoints . fmap toScreen
-
-renderScreenPoints :: [Point] -> String
-renderScreenPoints = unwords . fmap renderPoint
-
-renderPoint :: Point -> String
-renderPoint (Point x y) = formatDouble x <> "," <> formatDouble y
-
-toScreen :: Point -> Point
-toScreen (Point x y) = Point (960 + x * 101) (552 + y * 101)
-
-formatDouble :: Double -> String
-formatDouble value = showFFloat (Just 3) value ""
-
-clamp :: Double -> Double -> Double -> Double
-clamp lower upper = min upper . max lower
-
-clampInt :: Int -> Int -> Int -> Int
-clampInt lower upper = min upper . max lower
-
-twoPi :: Double
-twoPi = 2 * pi
diff --git a/bench/build/Moonlight/Triangulation/BuildBench.hs b/bench/build/Moonlight/Triangulation/BuildBench.hs
--- a/bench/build/Moonlight/Triangulation/BuildBench.hs
+++ b/bench/build/Moonlight/Triangulation/BuildBench.hs
@@ -19,7 +19,7 @@
 import Moonlight.Triangulation
 import Moonlight.Triangulation.BulkLoad (empty, insert)
 import Moonlight.Triangulation.Cdt (recoverConstraints)
-import Moonlight.Triangulation.Foreign.ABI (insertGeometryBatch)
+import Moonlight.Triangulation.Foreign.Mesh (insertGeometryBatch)
 -- The facade withholds this constructor. The benchmark indexes the builder's
 -- own input mapping, so every handle it forges is one the builder issued, and
 -- it owns that obligation explicitly by naming the module that grants it.
diff --git a/bench/delaunay-compare/Main.hs b/bench/delaunay-compare/Main.hs
deleted file mode 100644
--- a/bench/delaunay-compare/Main.hs
+++ /dev/null
@@ -1,27 +0,0 @@
--- | Haskell owner of the upstream-compatible construction comparison.
-module Main (main) where
-
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain (renderCompareObstruction)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Native (withNativeApi)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Suite
-  ( preflightSuite
-  , suiteAgreementMessage
-  , suiteBenchmarks
-  , withPreparedSuite
-  )
-import System.Exit (die)
-import Test.Tasty.Bench (defaultMain)
-
-main :: IO ()
-main = do
-  outcome <-
-    withNativeApi $ \api ->
-      withPreparedSuite api $ \suite -> do
-        agreement <- preflightSuite api suite
-        case agreement of
-          Left obstruction -> pure (Left obstruction)
-          Right () -> do
-            putStrLn suiteAgreementMessage
-            defaultMain (suiteBenchmarks api suite)
-            pure (Right ())
-  either (die . renderCompareObstruction) pure outcome
diff --git a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs b/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs
deleted file mode 100644
--- a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs
+++ /dev/null
@@ -1,244 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE NumericUnderscores #-}
-
--- | The closed case matrix and typed obstruction surface for the comparison.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-  ( SizeBand (..)
-  , allSizeBands
-  , sizeBandLabel
-  , PointDistribution (..)
-  , allPointDistributions
-  , pointDistributionLabel
-  , Implementation (..)
-  , allImplementations
-  , implementationLabel
-  , NativeImplementation (..)
-  , nativeImplementationLabel
-  , implementationOfNative
-  , PointCount
-  , pointCountValue
-  , pointCountsFor
-  , FixtureSpec (..)
-  , allFixtureSpecs
-  , DelaunaySummary (..)
-  , NativeStatus (..)
-  , NativePreparationFailure (..)
-  , NativeRunFailure (..)
-  , CompareObstruction (..)
-  , renderCompareObstruction
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.List.NonEmpty as NonEmpty
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Types (BuildError)
-
-data SizeBand
-  = Small
-  | Big
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-allSizeBands :: [SizeBand]
-allSizeBands = [Small, Big]
-
-sizeBandLabel :: SizeBand -> String
-sizeBandLabel = \case
-  Small -> "small"
-  Big -> "big"
-
-data PointDistribution
-  = LocalInsertion
-  | Uniform
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-allPointDistributions :: [PointDistribution]
-allPointDistributions = [LocalInsertion, Uniform]
-
-pointDistributionLabel :: PointDistribution -> String
-pointDistributionLabel = \case
-  LocalInsertion -> "local insertion"
-  Uniform -> "uniform"
-
-data Implementation
-  = Spade
-  | SpadeHierarchy
-  | Cdt
-  | Delaunator
-  | Moonlight
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Upstream order, with Moonlight appended rather than interposed.
-allImplementations :: [Implementation]
-allImplementations = [Spade, SpadeHierarchy, Cdt, Delaunator, Moonlight]
-
-implementationLabel :: Implementation -> String
-implementationLabel = \case
-  Spade -> "spade 2"
-  SpadeHierarchy -> "spade 2 hierarchy"
-  Cdt -> "cdt"
-  Delaunator -> "delaunator"
-  Moonlight -> "moonlight-triangulation"
-
-data NativeImplementation
-  = NativeSpade
-  | NativeSpadeHierarchy
-  | NativeCdt
-  | NativeDelaunator
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-nativeImplementationLabel :: NativeImplementation -> String
-nativeImplementationLabel = implementationLabel . implementationOfNative
-
-implementationOfNative :: NativeImplementation -> Implementation
-implementationOfNative = \case
-  NativeSpade -> Spade
-  NativeSpadeHierarchy -> SpadeHierarchy
-  NativeCdt -> Cdt
-  NativeDelaunator -> Delaunator
-
-newtype PointCount = PointCount Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-pointCountValue :: PointCount -> Int
-pointCountValue (PointCount count) = count
-
-data FixtureSpec = FixtureSpec
-  { fixtureSizeBand :: !SizeBand
-  , fixturePointDistribution :: !PointDistribution
-  , fixturePointCount :: !PointCount
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-allFixtureSpecs :: [FixtureSpec]
-allFixtureSpecs =
-  concatMap
-    ( \sizeBand ->
-        liftA2
-          (FixtureSpec sizeBand)
-          allPointDistributions
-          (pointCountsFor sizeBand)
-    )
-    allSizeBands
-
-pointCountsFor :: SizeBand -> [PointCount]
-pointCountsFor = \case
-  Small -> PointCount <$> [2_000, 4_000, 6_000, 8_000, 10_000, 12_000, 14_000]
-  Big -> PointCount <$> [50_000, 100_000, 150_000, 200_000, 250_000]
-
-data DelaunaySummary = DelaunaySummary
-  { summaryVertexCount :: !Int
-  , summaryTriangleCount :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data NativeStatus
-  = NativeNullPointer
-  | NativeUnknownTag
-  | NativeCoordinateCountMismatch
-  | NativeDistributionConstructionFailed
-  | NativeTriangulationFailed
-  | NativePanicked
-  | NativeUnknownStatus !Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data NativePreparationFailure
-  = NativePreparationStatus !NativeStatus
-  | NativePreparationReturnedNull
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data NativeRunFailure
-  = NativeRunStatus !NativeStatus
-  | NativeCountExceedsHaskellInt !Integer
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data CompareObstruction
-  = RustManifestNotFound !(NonEmpty FilePath)
-  | CargoInvocationFailed !FilePath !String
-  | CargoBuildFailed !FilePath !Int !String
-  | UnsupportedDynamicLibraryHost !String
-  | DynamicLibraryOpenFailed !FilePath !String
-  | DynamicSymbolLoadFailed !FilePath !String !String
-  | FixtureGenerationFailed !FixtureSpec !NativeStatus
-  | FixturePreparationFailed !FixtureSpec !NativeImplementation !NativeStatus
-  | NativePreparedNull !FixtureSpec !NativeImplementation
-  | MoonlightConstructionFailed !FixtureSpec !BuildError
-  | NativeConstructionFailed !FixtureSpec !NativeImplementation !NativeRunFailure
-  | SummaryDisagreement !FixtureSpec !Implementation !DelaunaySummary !DelaunaySummary
-  | ComparisonObstructions !(NonEmpty CompareObstruction)
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-renderCompareObstruction :: CompareObstruction -> String
-renderCompareObstruction = \case
-  RustManifestNotFound candidates ->
-    "Rust referent manifest not found; checked " <> show (NonEmpty.toList candidates)
-  CargoInvocationFailed manifest details ->
-    "could not invoke Cargo for " <> manifest <> ": " <> details
-  CargoBuildFailed manifest exitCode details ->
-    "Cargo failed for "
-      <> manifest
-      <> " with exit code "
-      <> show exitCode
-      <> if null details then "" else ":\n" <> details
-  UnsupportedDynamicLibraryHost host ->
-    "delaunay-compare does not know the dynamic-library suffix for host " <> show host
-  DynamicLibraryOpenFailed path details ->
-    "could not load Rust referent library " <> path <> ": " <> details
-  DynamicSymbolLoadFailed path symbol details ->
-    "could not load symbol " <> symbol <> " from " <> path <> ": " <> details
-  FixtureGenerationFailed fixture status ->
-    "upstream fixture generation failed for " <> renderFixture fixture <> ": " <> show status
-  FixturePreparationFailed fixture implementation status ->
-    "native preparation failed for "
-      <> nativeImplementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-      <> ": "
-      <> show status
-  NativePreparedNull fixture implementation ->
-    "native preparation returned a null handle for "
-      <> nativeImplementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-  MoonlightConstructionFailed fixture buildError ->
-    "Moonlight construction failed for " <> renderFixture fixture <> ": " <> show buildError
-  NativeConstructionFailed fixture implementation failure ->
-    "native construction failed for "
-      <> nativeImplementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-      <> ": "
-      <> show failure
-  SummaryDisagreement fixture implementation expected observed ->
-    "construction summary disagreement for "
-      <> implementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-      <> "; Moonlight="
-      <> show expected
-      <> ", observed="
-      <> show observed
-  ComparisonObstructions obstructions ->
-    unlines ("delaunay comparison obstructed:" : fmap (("  - " <>) . renderCompareObstruction) (NonEmpty.toList obstructions))
-
-renderFixture :: FixtureSpec -> String
-renderFixture fixture =
-  sizeBandLabel (fixtureSizeBand fixture)
-    <> "/"
-    <> pointDistributionLabel (fixturePointDistribution fixture)
-    <> "/"
-    <> show (pointCountValue (fixturePointCount fixture))
diff --git a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs b/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
deleted file mode 100644
--- a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
+++ /dev/null
@@ -1,307 +0,0 @@
-{-# LANGUAGE ForeignFunctionInterface #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | Resource-safe dynamic boundary to the Rust crates under comparison.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Native
-  ( NativeApi
-  , NativeFixture
-  , GeneratedCoordinates
-  , withNativeApi
-  , generateCoordinates
-  , generatedCoordinatePairs
-  , prepareNativeFixture
-  , runNativeFixture
-  , releaseNativeFixture
-  ) where
-
-import Control.Exception (IOException, bracket, displayException, try)
-import Control.Monad (filterM)
-import Control.Monad.Trans.Except (ExceptT (ExceptT), runExceptT)
-import Data.Bifunctor (first)
-import Data.Int (Int32)
-import Data.List (unfoldr)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-import Foreign.C.Types (CDouble, CInt (..), CSize (..))
-import Foreign.Marshal.Alloc (alloca)
-import Foreign.Ptr (FunPtr, Ptr, nullPtr)
-import Foreign.Storable (peek, poke)
-import qualified Data.Vector as Vector
-import qualified Data.Vector.Storable as StorableVector
-import qualified Data.Vector.Storable.Mutable as MutableStorableVector
-import System.Directory (doesFileExist, makeAbsolute)
-import System.Environment (lookupEnv)
-import System.Exit (ExitCode (..))
-import System.FilePath ((</>), takeDirectory)
-import System.Info (os)
-import System.Posix.DynamicLinker
-  ( DL
-  , RTLDFlags (RTLD_LOCAL, RTLD_NOW)
-  , dlclose
-  , dlopen
-  , dlsym
-  )
-import System.Process
-  ( CreateProcess (cwd)
-  , proc
-  , readCreateProcessWithExitCode
-  )
-
-type GenerateFunction = CInt -> CSize -> Ptr CDouble -> CSize -> IO CInt
-type PrepareFunction = CInt -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> IO CInt
-type RunFunction = Ptr () -> Ptr CSize -> Ptr CSize -> IO CInt
-type ReleaseFunction = Ptr () -> IO ()
-
-foreign import ccall safe "dynamic"
-  bindGenerateFunction :: FunPtr GenerateFunction -> GenerateFunction
-
-foreign import ccall safe "dynamic"
-  bindPrepareFunction :: FunPtr PrepareFunction -> PrepareFunction
-
-foreign import ccall safe "dynamic"
-  bindRunFunction :: FunPtr RunFunction -> RunFunction
-
-foreign import ccall unsafe "dynamic"
-  bindReleaseFunction :: FunPtr ReleaseFunction -> ReleaseFunction
-
-data NativeApi = NativeApi
-  { nativeGenerate :: !GenerateFunction
-  , nativePrepare :: !PrepareFunction
-  , nativeRun :: !RunFunction
-  , nativeRelease :: !ReleaseFunction
-  }
-
-newtype NativeFixture = NativeFixture (Ptr ())
-
-data GeneratedCoordinates = GeneratedCoordinates
-  { generatedPointCount :: !PointCount
-  , generatedCoordinateValues :: !(StorableVector.Vector CDouble)
-  }
-
-withNativeApi
-  :: (NativeApi -> IO (Either CompareObstruction result))
-  -> IO (Either CompareObstruction result)
-withNativeApi action = do
-  manifestResult <- resolveRustManifest
-  case manifestResult of
-    Left obstruction -> pure (Left obstruction)
-    Right manifest -> do
-      libraryResult <- buildRustLibrary manifest
-      case libraryResult of
-        Left obstruction -> pure (Left obstruction)
-        Right libraryPath -> withLoadedNativeApi libraryPath action
-
-resolveRustManifest :: IO (Either CompareObstruction FilePath)
-resolveRustManifest = do
-  configured <- lookupEnv "MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST"
-  let candidates =
-        case configured of
-          Just path -> path :| []
-          Nothing ->
-            "bench/delaunay-compare/rust/Cargo.toml"
-              :| [ "foundation/moonlight-triangulation/bench/delaunay-compare/rust/Cargo.toml"
-                 , "compiler/foundation/moonlight-triangulation/bench/delaunay-compare/rust/Cargo.toml"
-                 ]
-  existing <- filterM doesFileExist (NonEmpty.toList candidates)
-  case existing of
-    manifest : _ -> Right <$> makeAbsolute manifest
-    [] -> pure (Left (RustManifestNotFound candidates))
-
-buildRustLibrary :: FilePath -> IO (Either CompareObstruction FilePath)
-buildRustLibrary manifest = do
-  libraryNameResult <- dynamicLibraryName
-  case libraryNameResult of
-    Left obstruction -> pure (Left obstruction)
-    Right libraryName -> do
-      let rustRoot = takeDirectory manifest
-          targetDirectory = rustRoot </> "target"
-          command =
-            ( proc
-                "cargo"
-                [ "build"
-                , "--quiet"
-                , "--release"
-                , "--lib"
-                , "--manifest-path"
-                , manifest
-                , "--target-dir"
-                , targetDirectory
-                ]
-            )
-              { cwd = Just rustRoot
-              }
-      invocation <- try @IOException (readCreateProcessWithExitCode command "")
-      pure $
-        case invocation of
-          Left exception ->
-            Left (CargoInvocationFailed manifest (displayException exception))
-          Right (ExitFailure exitCode, standardOutput, standardError) ->
-            Left
-              ( CargoBuildFailed
-                  manifest
-                  exitCode
-                  (standardError <> standardOutput)
-              )
-          Right (ExitSuccess, _, _) ->
-            Right (targetDirectory </> "release" </> libraryName)
-
-dynamicLibraryName :: IO (Either CompareObstruction FilePath)
-dynamicLibraryName =
-  pure $
-    case os of
-      "darwin" -> Right "libmoonlight_delaunay_compare_referents.dylib"
-      "linux" -> Right "libmoonlight_delaunay_compare_referents.so"
-      host -> Left (UnsupportedDynamicLibraryHost host)
-
-withLoadedNativeApi
-  :: FilePath
-  -> (NativeApi -> IO (Either CompareObstruction result))
-  -> IO (Either CompareObstruction result)
-withLoadedNativeApi libraryPath action = do
-  opened <- try @IOException (dlopen libraryPath [RTLD_NOW, RTLD_LOCAL])
-  case opened of
-    Left exception ->
-      pure (Left (DynamicLibraryOpenFailed libraryPath (displayException exception)))
-    Right handle ->
-      bracket
-        (pure handle)
-        dlclose
-        ( \loadedHandle -> do
-            apiResult <- loadNativeApi libraryPath loadedHandle
-            case apiResult of
-              Left obstruction -> pure (Left obstruction)
-              Right api -> action api
-        )
-
-loadNativeApi :: FilePath -> DL -> IO (Either CompareObstruction NativeApi)
-loadNativeApi libraryPath handle =
-  runExceptT $
-    NativeApi
-      <$> loadSymbol libraryPath handle "delaunay_compare_generate" bindGenerateFunction
-      <*> loadSymbol libraryPath handle "delaunay_compare_prepare" bindPrepareFunction
-      <*> loadSymbol libraryPath handle "delaunay_compare_run" bindRunFunction
-      <*> loadSymbol libraryPath handle "delaunay_compare_release" bindReleaseFunction
-
-loadSymbol
-  :: FilePath
-  -> DL
-  -> String
-  -> (FunPtr function -> boundFunction)
-  -> ExceptT CompareObstruction IO boundFunction
-loadSymbol libraryPath handle symbol bind =
-  ExceptT $ do
-    loaded <- try @IOException (dlsym handle symbol)
-    pure $
-      first
-        (DynamicSymbolLoadFailed libraryPath symbol . displayException)
-        (bind <$> loaded)
-
-generateCoordinates
-  :: NativeApi
-  -> FixtureSpec
-  -> IO (Either NativeStatus GeneratedCoordinates)
-generateCoordinates api fixture = do
-  let pointCount = fixturePointCount fixture
-      coordinateCount = 2 * pointCountValue pointCount
-  mutableCoordinates <- MutableStorableVector.new coordinateCount
-  statusCode <-
-    MutableStorableVector.unsafeWith mutableCoordinates $ \coordinatePointer ->
-      nativeGenerate api
-        (distributionTag (fixturePointDistribution fixture))
-        (fromIntegral (pointCountValue pointCount))
-        coordinatePointer
-        (fromIntegral coordinateCount)
-  case decodeNativeStatus statusCode of
-    Left status -> pure (Left status)
-    Right () -> do
-      coordinates <- StorableVector.unsafeFreeze mutableCoordinates
-      pure (Right (GeneratedCoordinates pointCount coordinates))
-
-generatedCoordinatePairs :: GeneratedCoordinates -> Vector.Vector (Double, Double)
-generatedCoordinatePairs generated =
-  Vector.fromList
-    ( unfoldr
-        takeCoordinatePair
-        (realToFrac <$> StorableVector.toList (generatedCoordinateValues generated))
-    )
- where
-  takeCoordinatePair :: [Double] -> Maybe ((Double, Double), [Double])
-  takeCoordinatePair = \case
-    x : y : remaining -> Just ((x, y), remaining)
-    _ -> Nothing
-
-prepareNativeFixture
-  :: NativeApi
-  -> NativeImplementation
-  -> GeneratedCoordinates
-  -> IO (Either NativePreparationFailure NativeFixture)
-prepareNativeFixture api implementation coordinates =
-  alloca $ \preparedOutput -> do
-    poke preparedOutput nullPtr
-    statusCode <-
-      StorableVector.unsafeWith (generatedCoordinateValues coordinates) $ \coordinatePointer ->
-        nativePrepare api
-          (implementationTag implementation)
-          coordinatePointer
-          (fromIntegral (pointCountValue (generatedPointCount coordinates)))
-          preparedOutput
-    case decodeNativeStatus statusCode of
-      Left status -> pure (Left (NativePreparationStatus status))
-      Right () -> do
-        prepared <- peek preparedOutput
-        pure $
-          if prepared == nullPtr
-            then Left NativePreparationReturnedNull
-            else Right (NativeFixture prepared)
-
-runNativeFixture
-  :: NativeApi
-  -> NativeFixture
-  -> IO (Either NativeRunFailure DelaunaySummary)
-runNativeFixture api (NativeFixture prepared) =
-  alloca $ \vertexCountOutput ->
-    alloca $ \triangleCountOutput -> do
-      statusCode <- nativeRun api prepared vertexCountOutput triangleCountOutput
-      case decodeNativeStatus statusCode of
-        Left status -> pure (Left (NativeRunStatus status))
-        Right () -> do
-          vertexCount <- peek vertexCountOutput
-          triangleCount <- peek triangleCountOutput
-          pure $ DelaunaySummary <$> cSizeToInt vertexCount <*> cSizeToInt triangleCount
-
-releaseNativeFixture :: NativeApi -> NativeFixture -> IO ()
-releaseNativeFixture api (NativeFixture prepared) = nativeRelease api prepared
-
-decodeNativeStatus :: CInt -> Either NativeStatus ()
-decodeNativeStatus statusCode =
-  case fromIntegral statusCode :: Int32 of
-    0 -> Right ()
-    1 -> Left NativeNullPointer
-    2 -> Left NativeUnknownTag
-    3 -> Left NativeCoordinateCountMismatch
-    4 -> Left NativeDistributionConstructionFailed
-    5 -> Left NativeTriangulationFailed
-    6 -> Left NativePanicked
-    unknown -> Left (NativeUnknownStatus (fromIntegral unknown))
-
-cSizeToInt :: CSize -> Either NativeRunFailure Int
-cSizeToInt value =
-  let integerValue = toInteger value
-   in if integerValue > toInteger (maxBound :: Int)
-        then Left (NativeCountExceedsHaskellInt integerValue)
-        else Right (fromInteger integerValue)
-
-distributionTag :: PointDistribution -> CInt
-distributionTag = \case
-  LocalInsertion -> 0
-  Uniform -> 1
-
-implementationTag :: NativeImplementation -> CInt
-implementationTag = \case
-  NativeSpade -> 0
-  NativeSpadeHierarchy -> 1
-  NativeCdt -> 2
-  NativeDelaunator -> 3
diff --git a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs b/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs
deleted file mode 100644
--- a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs
+++ /dev/null
@@ -1,466 +0,0 @@
-{-# LANGUAGE LambdaCase #-}
-
--- | Typed receipt parsing and pure Spade-style SVG projection.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Picture
-  ( BenchmarkReceipt
-  , PictureObstruction (..)
-  , parseBenchmarkReceipt
-  , pictureFileName
-  , renderComparisonPicture
-  , renderPictureObstruction
-  ) where
-
-import Data.List (group, sort, sortOn)
-import qualified Data.Map.Strict as Map
-import Data.Maybe (mapMaybe)
-import qualified Data.Set as Set
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-import Numeric (showFFloat)
-import Text.Read (readMaybe)
-
-data ObservationKey = ObservationKey
-  { observationFixture :: !FixtureSpec
-  , observationImplementation :: !Implementation
-  }
-  deriving stock (Eq, Ord, Show)
-
-data BenchmarkObservation = BenchmarkObservation
-  { benchmarkKey :: !ObservationKey
-  , benchmarkMeanPicoseconds :: !Double
-  , benchmarkSpreadPicoseconds :: !Double
-  }
-  deriving stock (Eq, Show)
-
-newtype BenchmarkReceipt = BenchmarkReceipt
-  { receiptObservations :: Map.Map ObservationKey BenchmarkObservation
-  }
-
-data PictureObstruction
-  = MissingCsvHeader
-  | UnexpectedCsvHeader !String
-  | MalformedCsvRow !Int !String
-  | UnknownBenchmarkName !Int !String
-  | InvalidBenchmarkScalar !Int !String !String
-  | DuplicateBenchmarkObservations ![ObservationKey]
-  | MissingBenchmarkObservations ![ObservationKey]
-  deriving stock (Eq, Show)
-
-parseBenchmarkReceipt :: String -> Either PictureObstruction BenchmarkReceipt
-parseBenchmarkReceipt source =
-  case lines source of
-    [] -> Left MissingCsvHeader
-    header : rows
-      | header /= expectedCsvHeader -> Left (UnexpectedCsvHeader header)
-      | otherwise -> do
-          observations <- traverse (uncurry parseObservationRow) (zip [2 ..] rows)
-          let keys = benchmarkKey <$> observations
-              duplicateKeys =
-                mapMaybe
-                  ( \case
-                      key : _ : _ -> Just key
-                      _ -> Nothing
-                  )
-                  (group (sort keys))
-              observationMap = Map.fromList ((\observation -> (benchmarkKey observation, observation)) <$> observations)
-              missingKeys = Set.toList (expectedObservationKeys `Set.difference` Map.keysSet observationMap)
-          case (duplicateKeys, missingKeys) of
-            (duplicate : duplicates, _) -> Left (DuplicateBenchmarkObservations (duplicate : duplicates))
-            ([], missing : remaining) -> Left (MissingBenchmarkObservations (missing : remaining))
-            ([], []) -> Right (BenchmarkReceipt observationMap)
-
-renderComparisonPicture
-  :: SizeBand
-  -> BenchmarkReceipt
-  -> String
-renderComparisonPicture sizeBand receipt =
-  renderSvg sizeBand pointCounts axisMaximum panels
- where
-  pointCounts = pointCountValue <$> pointCountsFor sizeBand
-  panels = picturePanel receipt sizeBand <$> allPointDistributions
-  allObservations = concatMap (concatMap pictureSeriesObservations . picturePanelSeries) panels
-  maximumMilliseconds =
-    foldr
-      (max . observationUpperMilliseconds)
-      0
-      allObservations
-  axisMaximum = niceCeiling maximumMilliseconds
-
-pictureFileName :: SizeBand -> FilePath
-pictureFileName = \case
-  Small -> "moonlight-delaunay-compare-small.svg"
-  Big -> "moonlight-delaunay-compare-big.svg"
-
-renderPictureObstruction :: PictureObstruction -> String
-renderPictureObstruction = \case
-  MissingCsvHeader -> "benchmark CSV has no header"
-  UnexpectedCsvHeader header ->
-    "benchmark CSV header was " <> show header <> "; expected " <> show expectedCsvHeader
-  MalformedCsvRow lineNumber row ->
-    "benchmark CSV row " <> show lineNumber <> " is not a three-column tasty-bench row: " <> show row
-  UnknownBenchmarkName lineNumber name ->
-    "benchmark CSV row " <> show lineNumber <> " names an unknown comparison case: " <> show name
-  InvalidBenchmarkScalar lineNumber field value ->
-    "benchmark CSV row "
-      <> show lineNumber
-      <> " has invalid "
-      <> field
-      <> ": "
-      <> show value
-  DuplicateBenchmarkObservations keys ->
-    "benchmark CSV repeats comparison cases: " <> show keys
-  MissingBenchmarkObservations keys ->
-    "benchmark CSV omits comparison cases: " <> show keys
-
-expectedCsvHeader :: String
-expectedCsvHeader = "Name,Mean (ps),2*Stdev (ps)"
-
-expectedObservationKeys :: Set.Set ObservationKey
-expectedObservationKeys =
-  Set.fromList (liftA2 ObservationKey allFixtureSpecs allImplementations)
-
-expectedBenchmarkNames :: Map.Map String ObservationKey
-expectedBenchmarkNames =
-  Map.fromList ((\key -> (benchmarkName key, key)) <$> Set.toList expectedObservationKeys)
-
-benchmarkName :: ObservationKey -> String
-benchmarkName key =
-  "All.comparison: creation benchmark ("
-    <> sizeBandLabel (fixtureSizeBand fixture)
-    <> ")."
-    <> implementationLabel (observationImplementation key)
-    <> "."
-    <> pointDistributionLabel (fixturePointDistribution fixture)
-    <> "."
-    <> show (pointCountValue (fixturePointCount fixture))
- where
-  fixture = observationFixture key
-
-parseObservationRow :: Int -> String -> Either PictureObstruction BenchmarkObservation
-parseObservationRow lineNumber row =
-  case csvTriple row of
-    Nothing -> Left (MalformedCsvRow lineNumber row)
-    Just (name, rawMean, rawSpread) -> do
-      key <- maybe (Left (UnknownBenchmarkName lineNumber name)) Right (Map.lookup name expectedBenchmarkNames)
-      meanPicoseconds <- parseNonNegativeScalar lineNumber "mean picoseconds" rawMean
-      spreadPicoseconds <- parseNonNegativeScalar lineNumber "two-standard-deviation spread" rawSpread
-      pure
-        BenchmarkObservation
-          { benchmarkKey = key
-          , benchmarkMeanPicoseconds = meanPicoseconds
-          , benchmarkSpreadPicoseconds = spreadPicoseconds
-          }
-
-csvTriple :: String -> Maybe (String, String, String)
-csvTriple row =
-  case break (== ',') row of
-    (firstColumn, ',' : remaining) ->
-      case break (== ',') remaining of
-        (secondColumn, ',' : thirdColumn)
-          | ',' `notElem` thirdColumn -> Just (firstColumn, secondColumn, thirdColumn)
-        _ -> Nothing
-    _ -> Nothing
-
-parseNonNegativeScalar :: Int -> String -> String -> Either PictureObstruction Double
-parseNonNegativeScalar lineNumber field rawValue =
-  case readMaybe rawValue of
-    Just value
-      | value >= 0 && not (isInfinite value) && not (isNaN value) -> Right value
-    _ -> Left (InvalidBenchmarkScalar lineNumber field rawValue)
-
-data PictureSeries = PictureSeries
-  { pictureSeriesImplementation :: !Implementation
-  , pictureSeriesObservations :: ![BenchmarkObservation]
-  }
-
-data PicturePanel = PicturePanel
-  { picturePanelDistribution :: !PointDistribution
-  , picturePanelSeries :: ![PictureSeries]
-  }
-
-picturePanel
-  :: BenchmarkReceipt
-  -> SizeBand
-  -> PointDistribution
-  -> PicturePanel
-picturePanel receipt sizeBand distribution =
-  PicturePanel distribution (pictureSeries receipt sizeBand distribution <$> allImplementations)
-
-pictureSeries
-  :: BenchmarkReceipt
-  -> SizeBand
-  -> PointDistribution
-  -> Implementation
-  -> PictureSeries
-pictureSeries receipt sizeBand distribution implementation =
-  PictureSeries implementation observations
- where
-  observations =
-    sortOn (pointCountValue . fixturePointCount . observationFixture . benchmarkKey)
-      [ observation
-      | observation <- Map.elems (receiptObservations receipt)
-      , let key = benchmarkKey observation
-            fixture = observationFixture key
-      , fixtureSizeBand fixture == sizeBand
-      , fixturePointDistribution fixture == distribution
-      , observationImplementation key == implementation
-      ]
-
-data PictureLayout = PictureLayout
-  { pictureWidth :: !Double
-  , pictureHeight :: !Double
-  , picturePlotTop :: !Double
-  , picturePlotBottom :: !Double
-  , picturePanelWidth :: !Double
-  , picturePanelLefts :: ![Double]
-  }
-
-pictureLayout :: PictureLayout
-pictureLayout =
-  PictureLayout
-    { pictureWidth = 1280
-    , pictureHeight = 720
-    , picturePlotTop = 168
-    , picturePlotBottom = 610
-    , picturePanelWidth = 540
-    , picturePanelLefts = [70, 670]
-    }
-
-renderSvg :: SizeBand -> [Int] -> Double -> [PicturePanel] -> String
-renderSvg sizeBand pointCounts axisMaximum panels =
-  unlines
-    ( [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
-      , "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 "
-          <> coordinate (pictureWidth pictureLayout)
-          <> " "
-          <> coordinate (pictureHeight pictureLayout)
-          <> "\" role=\"img\" aria-labelledby=\"title description\">"
-      , "<title id=\"title\">" <> pictureTitle sizeBand <> "</title>"
-      , "<desc id=\"description\">CPU construction time for Spade, cdt, delaunator, and Moonlight over the upstream Delaunay comparison fixtures. Lower lines are faster.</desc>"
-      , "<defs>"
-      , "  <pattern id=\"triangulation-mesh\" width=\"54\" height=\"46\" patternUnits=\"userSpaceOnUse\">"
-      , "    <path d=\"M0 46L27 0L54 46ZM0 46L54 46M27 0L27 46\" fill=\"none\" stroke=\"rgb(255,140,0)\" stroke-width=\"0.7\"/>"
-      , "  </pattern>"
-      , "  <clipPath id=\"header-mesh-clip\"><rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" rx=\"20\"/></clipPath>"
-      , "</defs>"
-      , "<rect width=\""
-          <> coordinate (pictureWidth pictureLayout)
-          <> "\" height=\""
-          <> coordinate (pictureHeight pictureLayout)
-          <> "\" fill=\"white\"/>"
-      , "<rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" fill=\"url(#triangulation-mesh)\" opacity=\"0.10\" clip-path=\"url(#header-mesh-clip)\"/>"
-      , svgText 640 34 "middle" 24 "500" (pictureTitle sizeBand)
-      , svgText 640 61 "middle" 13 "400" "Moonlight against Spade's construction referents · CPU time · lower is faster"
-      ]
-        <> renderLegend
-        <> concat (zipWith (renderPanel pointCounts axisMaximum) (picturePanelLefts pictureLayout) panels)
-        <> [ svgText 640 704 "middle" 11 "400" "Whiskers show tasty-bench's reported 2× standard-deviation spread."
-           , "</svg>"
-           ]
-    )
-
-pictureTitle :: SizeBand -> String
-pictureTitle = \case
-  Small -> "Moonlight Delaunay construction · small point sets"
-  Big -> "Moonlight Delaunay construction · big point sets"
-
-renderLegend :: [String]
-renderLegend =
-  concat
-    ( zipWith
-        renderLegendItem
-        [105, 315, 560, 735, 940]
-        allImplementations
-    )
-
-renderLegendItem :: Double -> Implementation -> [String]
-renderLegendItem x implementation =
-  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
-  , "  <line x1=\"" <> coordinate x <> "\" y1=\"101\" x2=\"" <> coordinate (x + 34) <> "\" y2=\"101\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\"" <> seriesDash implementation <> "/>"
-  , "  " <> renderMarker implementation (x + 17) 101
-  , "  " <> svgText (x + 43) 105 "start" 12 "400" (implementationLabel implementation)
-  , "</g>"
-  ]
-
-renderPanel :: [Int] -> Double -> Double -> PicturePanel -> [String]
-renderPanel pointCounts axisMaximum panelLeft panel =
-  [ "<g aria-label=\"" <> panelLabel <> "\">"
-  , svgText (panelLeft + panelWidth / 2) 144 "middle" 15 "500" panelLabel
-  ]
-    <> concatMap (renderHorizontalGrid panelLeft panelWidth axisMaximum) [0 .. 5]
-    <> concatMap (renderVerticalTick panelLeft panelWidth pointCounts) pointCounts
-    <> [ "<rect x=\"" <> coordinate panelLeft <> "\" y=\"" <> coordinate plotTop <> "\" width=\"" <> coordinate panelWidth <> "\" height=\"" <> coordinate plotHeight <> "\" fill=\"none\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
-       , svgText (panelLeft + panelWidth / 2) 664 "middle" 12 "400" "input size (points)"
-       , "<text x=\"" <> coordinate (panelLeft - 55) <> "\" y=\"" <> coordinate (plotTop + plotHeight / 2) <> "\" text-anchor=\"middle\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"12\" fill=\"rgb(20,20,20)\" transform=\"rotate(-90 " <> coordinate (panelLeft - 55) <> " " <> coordinate (plotTop + plotHeight / 2) <> ")\">CPU time (ms)</text>"
-       ]
-    <> concatMap (renderSeries panelLeft panelWidth pointCounts axisMaximum) (picturePanelSeries panel)
-    <> ["</g>"]
- where
-  panelLabel = pointDistributionLabel (picturePanelDistribution panel)
-  panelWidth = picturePanelWidth pictureLayout
-  plotTop = picturePlotTop pictureLayout
-  plotHeight = picturePlotBottom pictureLayout - plotTop
-
-renderHorizontalGrid :: Double -> Double -> Double -> Int -> [String]
-renderHorizontalGrid panelLeft panelWidth axisMaximum tickIndex =
-  [ "<line x1=\"" <> coordinate panelLeft <> "\" y1=\"" <> coordinate y <> "\" x2=\"" <> coordinate (panelLeft + panelWidth) <> "\" y2=\"" <> coordinate y <> "\" stroke=\"rgb(170,170,170)\" stroke-width=\"0.7\" stroke-dasharray=\"2 4\"/>"
-  , svgText (panelLeft - 10) (y + 4) "end" 11 "400" (axisLabel tickValue)
-  ]
- where
-  tickValue = axisMaximum * fromIntegral tickIndex / 5
-  y = pictureY axisMaximum tickValue
-
-renderVerticalTick :: Double -> Double -> [Int] -> Int -> [String]
-renderVerticalTick panelLeft panelWidth pointCounts pointCount =
-  [ "<line x1=\"" <> coordinate x <> "\" y1=\"610\" x2=\"" <> coordinate x <> "\" y2=\"616\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
-  , svgText x 635 "middle" 11 "400" (pointCountLabel pointCount)
-  ]
- where
-  x = pictureX panelLeft panelWidth pointCounts pointCount
-
-renderSeries :: Double -> Double -> [Int] -> Double -> PictureSeries -> [String]
-renderSeries panelLeft panelWidth pointCounts axisMaximum series =
-  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
-  , "  <polyline points=\"" <> unwords (observationPoint <$> observations) <> "\" fill=\"none\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\" stroke-linecap=\"round\" stroke-linejoin=\"round\"" <> seriesDash implementation <> "/>"
-  ]
-    <> concatMap renderObservation observations
-    <> ["</g>"]
- where
-  implementation = pictureSeriesImplementation series
-  observations = pictureSeriesObservations series
-  observationPoint observation =
-    coordinate (observationX observation) <> "," <> coordinate (observationY observation)
-  observationX =
-    pictureX panelLeft panelWidth pointCounts
-      . pointCountValue
-      . fixturePointCount
-      . observationFixture
-      . benchmarkKey
-  observationY = pictureY axisMaximum . observationMeanMilliseconds
-  renderObservation observation =
-    [ "  <line x1=\"" <> coordinate x <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate x <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
-    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate upperY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
-    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate lowerY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
-    , "  " <> renderMarker implementation x y
-    ]
-   where
-    x = observationX observation
-    y = observationY observation
-    upperY = pictureY axisMaximum (observationUpperMilliseconds observation)
-    lowerY = pictureY axisMaximum (observationLowerMilliseconds observation)
-
-implementationColor :: Implementation -> String
-implementationColor = \case
-  Spade -> "rgb(0,0,139)"
-  SpadeHierarchy -> "rgb(139,0,139)"
-  Cdt -> "rgb(178,34,34)"
-  Delaunator -> "rgb(0,139,139)"
-  Moonlight -> "rgb(255,140,0)"
-
-seriesStrokeWidth :: Implementation -> String
-seriesStrokeWidth = \case
-  Moonlight -> "3.2"
-  _ -> "2.0"
-
-seriesDash :: Implementation -> String
-seriesDash = \case
-  SpadeHierarchy -> " stroke-dasharray=\"7 4\""
-  _ -> ""
-
-renderMarker :: Implementation -> Double -> Double -> String
-renderMarker implementation x y =
-  case implementation of
-    Spade -> circle 3.2
-    SpadeHierarchy ->
-      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.3) <> " L " <> coordinate (x - 4.1) <> " " <> coordinate (y + 3.5) <> " L " <> coordinate (x + 4.1) <> " " <> coordinate (y + 3.5) <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-    Cdt ->
-      "<rect x=\"" <> coordinate (x - 3.2) <> "\" y=\"" <> coordinate (y - 3.2) <> "\" width=\"6.4\" height=\"6.4\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-    Delaunator ->
-      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.2) <> " L " <> coordinate (x + 4.2) <> " " <> coordinate y <> " L " <> coordinate x <> " " <> coordinate (y + 4.2) <> " L " <> coordinate (x - 4.2) <> " " <> coordinate y <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-    Moonlight ->
-      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 5.4) <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y - 1.7) <> " L " <> coordinate (x + 5.4) <> " " <> coordinate y <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate x <> " " <> coordinate (y + 5.4) <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate (x - 5.4) <> " " <> coordinate y <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y - 1.7) <> " Z\" fill=\"" <> color <> "\" stroke=\"white\" stroke-width=\"1\"/>"
- where
-  color = implementationColor implementation
-  circle radius =
-    "<circle cx=\"" <> coordinate x <> "\" cy=\"" <> coordinate y <> "\" r=\"" <> coordinate radius <> "\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-
-pictureX :: Double -> Double -> [Int] -> Int -> Double
-pictureX panelLeft panelWidth pointCounts pointCount =
-  case pointCounts of
-    [] -> panelLeft
-    firstCount : remainingCounts ->
-      let minimumCount = firstCount
-          maximumCount = foldr max firstCount remainingCounts
-          countRange = maximumCount - minimumCount
-       in if countRange == 0
-            then panelLeft + panelWidth / 2
-            else
-              panelLeft
-                + panelWidth
-                  * fromIntegral (pointCount - minimumCount)
-                  / fromIntegral countRange
-
-pictureY :: Double -> Double -> Double
-pictureY axisMaximum milliseconds =
-  picturePlotBottom pictureLayout
-    - plotHeight * min axisMaximum (max 0 milliseconds) / axisMaximum
- where
-  plotHeight = picturePlotBottom pictureLayout - picturePlotTop pictureLayout
-
-observationMeanMilliseconds :: BenchmarkObservation -> Double
-observationMeanMilliseconds = picosecondsToMilliseconds . benchmarkMeanPicoseconds
-
-observationUpperMilliseconds :: BenchmarkObservation -> Double
-observationUpperMilliseconds observation =
-  picosecondsToMilliseconds
-    (benchmarkMeanPicoseconds observation + benchmarkSpreadPicoseconds observation)
-
-observationLowerMilliseconds :: BenchmarkObservation -> Double
-observationLowerMilliseconds observation =
-  picosecondsToMilliseconds
-    (max 0 (benchmarkMeanPicoseconds observation - benchmarkSpreadPicoseconds observation))
-
-picosecondsToMilliseconds :: Double -> Double
-picosecondsToMilliseconds picoseconds = picoseconds / 1.0e9
-
-niceCeiling :: Double -> Double
-niceCeiling value
-  | value <= 0 = 1
-  | otherwise = niceFraction * magnitude
- where
-  magnitude = 10 ** fromIntegral (floor (logBase 10 value) :: Int)
-  fraction = value / magnitude
-  niceFraction
-    | fraction <= 1 = 1
-    | fraction <= 2 = 2
-    | fraction <= 5 = 5
-    | otherwise = 10
-
-axisLabel :: Double -> String
-axisLabel value
-  | value >= 10 = showFFloat (Just 0) value ""
-  | value >= 1 = showFFloat (Just 1) value ""
-  | otherwise = showFFloat (Just 2) value ""
-
-pointCountLabel :: Int -> String
-pointCountLabel pointCount
-  | pointCount `mod` 1000 == 0 = show (pointCount `div` 1000) <> "k"
-  | otherwise = show pointCount
-
-coordinate :: Double -> String
-coordinate value = showFFloat (Just 1) value ""
-
-svgText :: Double -> Double -> String -> Int -> String -> String -> String
-svgText x y anchor fontSize fontWeight content =
-  "<text x=\""
-    <> coordinate x
-    <> "\" y=\""
-    <> coordinate y
-    <> "\" text-anchor=\""
-    <> anchor
-    <> "\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\""
-    <> show fontSize
-    <> "\" font-weight=\""
-    <> fontWeight
-    <> "\" fill=\"rgb(20,20,20)\">"
-    <> content
-    <> "</text>"
diff --git a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs b/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs
deleted file mode 100644
--- a/bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs
+++ /dev/null
@@ -1,260 +0,0 @@
-{-# LANGUAGE DeriveFoldable #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE DeriveTraversable #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Fixture descent, compatibility gluing, and the authoritative benchmark view.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Suite
-  ( PreparedSuite
-  , withPreparedSuite
-  , preflightSuite
-  , suiteBenchmarks
-  , suiteAgreementMessage
-  ) where
-
-import Control.DeepSeq (force)
-import Control.Exception (bracket, evaluate)
-import Data.Bifunctor (first)
-import Data.Either (lefts, rights)
-import Data.Foldable (toList, traverse_)
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-import Moonlight.Triangulation.Bench.DelaunayCompare.Native
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.Dcel (numInnerFaces, numVertices)
-import Moonlight.Triangulation.Types (BuildError, Point (Point))
-import Test.Tasty.Bench (Benchmark, bench, bgroup, nf, nfIO)
-
-data NativeSections value = NativeSections
-  { nativeSpadeSection :: !value
-  , nativeSpadeHierarchySection :: !value
-  , nativeCdtSection :: !value
-  , nativeDelaunatorSection :: !value
-  }
-  deriving stock (Functor, Foldable, Traversable)
-
-nativeImplementationSections :: NativeSections NativeImplementation
-nativeImplementationSections =
-  NativeSections
-    { nativeSpadeSection = NativeSpade
-    , nativeSpadeHierarchySection = NativeSpadeHierarchy
-    , nativeCdtSection = NativeCdt
-    , nativeDelaunatorSection = NativeDelaunator
-    }
-
-nativeSectionAt :: NativeImplementation -> NativeSections value -> value
-nativeSectionAt implementation sections =
-  case implementation of
-    NativeSpade -> nativeSpadeSection sections
-    NativeSpadeHierarchy -> nativeSpadeHierarchySection sections
-    NativeCdt -> nativeCdtSection sections
-    NativeDelaunator -> nativeDelaunatorSection sections
-
-data PreparedFixture = PreparedFixture
-  { preparedSpec :: !FixtureSpec
-  , preparedMoonlightPoints :: !(Vector.Vector Point)
-  , preparedNativeFixtures :: !(NativeSections NativeFixture)
-  }
-
-newtype PreparedSuite = PreparedSuite [PreparedFixture]
-
-withPreparedSuite
-  :: NativeApi
-  -> (PreparedSuite -> IO (Either CompareObstruction result))
-  -> IO (Either CompareObstruction result)
-withPreparedSuite api action = do
-  prepared <- prepareSuite api
-  case prepared of
-    Left obstruction -> pure (Left obstruction)
-    Right suite -> bracket (pure suite) (releaseSuite api) action
-
-prepareSuite :: NativeApi -> IO (Either CompareObstruction PreparedSuite)
-prepareSuite api = do
-  outcomes <- traverse (prepareFixture api) allFixtureSpecs
-  case NonEmpty.nonEmpty (lefts outcomes) of
-    Just obstructions -> do
-      traverse_ (releasePreparedFixture api) (rights outcomes)
-      pure (Left (ComparisonObstructions obstructions))
-    Nothing -> pure (Right (PreparedSuite (rights outcomes)))
-
-prepareFixture :: NativeApi -> FixtureSpec -> IO (Either CompareObstruction PreparedFixture)
-prepareFixture api fixture = do
-  generated <- generateCoordinates api fixture
-  case generated of
-    Left status -> pure (Left (FixtureGenerationFailed fixture status))
-    Right coordinates -> do
-      nativeFixtures <- prepareNativeFixtures api fixture coordinates
-      pure $
-        PreparedFixture
-          fixture
-          (Vector.map (uncurry Point) (generatedCoordinatePairs coordinates))
-          <$> nativeFixtures
-
-prepareNativeFixtures
-  :: NativeApi
-  -> FixtureSpec
-  -> GeneratedCoordinates
-  -> IO (Either CompareObstruction (NativeSections NativeFixture))
-prepareNativeFixtures api fixture coordinates = do
-  outcomes <- traverse prepareSection nativeImplementationSections
-  case sequenceA outcomes of
-    Right nativeFixtures -> pure (Right nativeFixtures)
-    Left obstruction -> do
-      traverse_ (releaseNativeFixture api) (rights (toList outcomes))
-      pure (Left obstruction)
- where
-  prepareSection implementation = do
-    prepared <- prepareNativeFixture api implementation coordinates
-    pure $
-      first
-        (preparationObstruction implementation)
-        prepared
-
-  preparationObstruction implementation failure =
-    case failure of
-      NativePreparationStatus status -> FixturePreparationFailed fixture implementation status
-      NativePreparationReturnedNull -> NativePreparedNull fixture implementation
-
-releaseSuite :: NativeApi -> PreparedSuite -> IO ()
-releaseSuite api (PreparedSuite fixtures) =
-  traverse_ (releasePreparedFixture api) fixtures
-
-releasePreparedFixture :: NativeApi -> PreparedFixture -> IO ()
-releasePreparedFixture api = releasePreparedNativeFixtures api . preparedNativeFixtures
-
-releasePreparedNativeFixtures :: NativeApi -> NativeSections NativeFixture -> IO ()
-releasePreparedNativeFixtures api = traverse_ (releaseNativeFixture api)
-
-preflightSuite :: NativeApi -> PreparedSuite -> IO (Either CompareObstruction ())
-preflightSuite api (PreparedSuite fixtures) = do
-  outcomes <- traverse (preflightFixture api) fixtures
-  pure $
-    case NonEmpty.nonEmpty (lefts outcomes) of
-      Just obstructions -> Left (ComparisonObstructions obstructions)
-      Nothing -> Right ()
-
-preflightFixture :: NativeApi -> PreparedFixture -> IO (Either CompareObstruction ())
-preflightFixture api fixture = do
-  moonlightResult <-
-    evaluate
-      ( force
-          (moonlightConstructionSummary (preparedMoonlightPoints fixture))
-      )
-  nativeOutcomes <- traverse runSection nativeImplementationSections
-  pure $ do
-    moonlightSummary <-
-      first
-        (MoonlightConstructionFailed spec)
-        moonlightResult
-    nativeSummaries <- sequenceA nativeOutcomes
-    traverse_
-      ( \implementation ->
-          requireSummaryAgreement
-            spec
-            moonlightSummary
-            ( implementationOfNative implementation
-            , nativeSectionAt implementation nativeSummaries
-            )
-      )
-      nativeImplementationSections
- where
-  spec = preparedSpec fixture
-  nativeFixtures = preparedNativeFixtures fixture
-  runSection implementation =
-    first (NativeConstructionFailed spec implementation)
-      <$> runNativeFixture api (nativeSectionAt implementation nativeFixtures)
-
-requireSummaryAgreement
-  :: FixtureSpec
-  -> DelaunaySummary
-  -> (Implementation, DelaunaySummary)
-  -> Either CompareObstruction ()
-requireSummaryAgreement fixture expected (implementation, observed) =
-  if observed == expected
-    then Right ()
-    else Left (SummaryDisagreement fixture implementation expected observed)
-
-moonlightConstructionSummary
-  :: Vector.Vector Point
-  -> Either BuildError DelaunaySummary
-moonlightConstructionSummary points = do
-  triangulation <- delaunayGeometry points
-  pure
-    DelaunaySummary
-      { summaryVertexCount = numVertices triangulation
-      , summaryTriangleCount = numInnerFaces triangulation
-      }
-
-suiteBenchmarks :: NativeApi -> PreparedSuite -> [Benchmark]
-suiteBenchmarks api (PreparedSuite fixtures) =
-  fmap (sizeBandBenchmarks api fixtures) allSizeBands
-
-sizeBandBenchmarks
-  :: NativeApi
-  -> [PreparedFixture]
-  -> SizeBand
-  -> Benchmark
-sizeBandBenchmarks api fixtures sizeBand =
-  bgroup
-    ("comparison: creation benchmark (" <> sizeBandLabel sizeBand <> ")")
-    (fmap (implementationBenchmarks api fixtures sizeBand) allImplementations)
-
-implementationBenchmarks
-  :: NativeApi
-  -> [PreparedFixture]
-  -> SizeBand
-  -> Implementation
-  -> Benchmark
-implementationBenchmarks api fixtures sizeBand implementation =
-  bgroup
-    (implementationLabel implementation)
-    ( fmap
-        (distributionBenchmarks api fixtures sizeBand implementation)
-        allPointDistributions
-    )
-
-distributionBenchmarks
-  :: NativeApi
-  -> [PreparedFixture]
-  -> SizeBand
-  -> Implementation
-  -> PointDistribution
-  -> Benchmark
-distributionBenchmarks api fixtures sizeBand implementation distribution =
-  bgroup
-    (pointDistributionLabel distribution)
-    ( fmap
-        (fixtureBenchmark api implementation)
-        ( filter
-            (matchesFixture sizeBand distribution . preparedSpec)
-            fixtures
-        )
-    )
-
-matchesFixture :: SizeBand -> PointDistribution -> FixtureSpec -> Bool
-matchesFixture sizeBand distribution fixture =
-  fixtureSizeBand fixture == sizeBand
-    && fixturePointDistribution fixture == distribution
-
-fixtureBenchmark :: NativeApi -> Implementation -> PreparedFixture -> Benchmark
-fixtureBenchmark api implementation fixture =
-  bench (show (pointCountValue (fixturePointCount (preparedSpec fixture)))) $
-    case implementation of
-      Moonlight -> nf moonlightConstructionSummary (preparedMoonlightPoints fixture)
-      Spade -> nativeBenchmark NativeSpade
-      SpadeHierarchy -> nativeBenchmark NativeSpadeHierarchy
-      Cdt -> nativeBenchmark NativeCdt
-      Delaunator -> nativeBenchmark NativeDelaunator
- where
-  nativeFixtures = preparedNativeFixtures fixture
-  nativeBenchmark nativeImplementation =
-    nfIO (runNativeFixture api (nativeSectionAt nativeImplementation nativeFixtures))
-
-suiteAgreementMessage :: String
-suiteAgreementMessage =
-  "delaunay-compare agreement: "
-    <> show (length allFixtureSpecs)
-    <> " fixtures agree on vertex and triangle counts across "
-    <> show (length allImplementations)
-    <> " implementations"
diff --git a/bench/delaunay-compare/PictureMain.hs b/bench/delaunay-compare/PictureMain.hs
deleted file mode 100644
--- a/bench/delaunay-compare/PictureMain.hs
+++ /dev/null
@@ -1,75 +0,0 @@
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | Thin effect boundary for rendering benchmark receipts as SVG pictures.
-module Main (main) where
-
-import Control.Exception (IOException, displayException, try)
-import Control.Monad.Trans.Except (ExceptT (ExceptT), except, runExceptT)
-import Data.Bifunctor (first)
-import Data.Foldable (traverse_)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain (allSizeBands)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Picture
-import System.Directory (createDirectoryIfMissing)
-import System.Environment (getArgs)
-import System.Exit (die)
-import System.FilePath ((</>))
-import System.IO (readFile')
-
-data PictureCommandFailure
-  = PictureCommandUsage
-  | PictureCommandReadFailed !FilePath !String
-  | PictureCommandCreateDirectoryFailed !FilePath !String
-  | PictureCommandReceiptObstructed !PictureObstruction
-  | PictureCommandWriteFailed !FilePath !String
-
-main :: IO ()
-main = do
-  arguments <- getArgs
-  outcome <- runExceptT (runPictureCommand arguments)
-  either (die . renderPictureCommandFailure) pure outcome
-
-runPictureCommand :: [String] -> ExceptT PictureCommandFailure IO ()
-runPictureCommand = \case
-  [inputCsv, outputDirectory] -> do
-    source <-
-      ExceptT
-        ( first
-            (PictureCommandReadFailed inputCsv . displayException)
-            <$> try @IOException (readFile' inputCsv)
-        )
-    receipt <- except (first PictureCommandReceiptObstructed (parseBenchmarkReceipt source))
-    let pictures =
-          ( \sizeBand ->
-              (pictureFileName sizeBand, renderComparisonPicture sizeBand receipt)
-          )
-            <$> allSizeBands
-    ExceptT
-      ( first
-          (PictureCommandCreateDirectoryFailed outputDirectory . displayException)
-          <$> try @IOException (createDirectoryIfMissing True outputDirectory)
-      )
-    traverse_ (writePicture outputDirectory) pictures
-  _ -> except (Left PictureCommandUsage)
-
-writePicture :: FilePath -> (FilePath, String) -> ExceptT PictureCommandFailure IO ()
-writePicture outputDirectory (fileName, svgDocument) =
-  let outputPath = outputDirectory </> fileName
-   in ExceptT
-        ( first
-            (PictureCommandWriteFailed outputPath . displayException)
-            <$> try @IOException (writeFile outputPath svgDocument)
-        )
-
-renderPictureCommandFailure :: PictureCommandFailure -> String
-renderPictureCommandFailure = \case
-  PictureCommandUsage ->
-    "usage: moonlight-triangulation-delaunay-pictures INPUT.csv OUTPUT_DIRECTORY"
-  PictureCommandReadFailed path details ->
-    "could not read benchmark CSV " <> path <> ": " <> details
-  PictureCommandCreateDirectoryFailed path details ->
-    "could not create picture directory " <> path <> ": " <> details
-  PictureCommandReceiptObstructed obstruction ->
-    renderPictureObstruction obstruction
-  PictureCommandWriteFailed path details ->
-    "could not write comparison picture " <> path <> ": " <> details
diff --git a/bench/delaunay-compare/README.md b/bench/delaunay-compare/README.md
deleted file mode 100644
--- a/bench/delaunay-compare/README.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Moonlight Delaunay Compare
-
-`moonlight-triangulation-delaunay-compare` is Moonlight's package-owned port of Spade's
-[`delaunay_compare`](https://github.com/Stoeoef/spade/tree/c8befc96bbbc1898a89cb19f9f3104a848936374/delaunay_compare)
-construction suite. It retains the upstream case matrix and appends
-`moonlight-triangulation` to the implementation list:
-
-1. `spade 2`
-2. `spade 2 hierarchy`
-3. `cdt`
-4. `delaunator`
-5. `moonlight-triangulation`
-
-Haskell owns fixture selection, grouping, compatibility descent, timing, and
-reporting. The Rust library under `rust/src/lib.rs` is only a typed foreign
-boundary to the four Rust implementations and to the exact upstream `rand`
-fixture stream; it contains no benchmark policy.
-
-This executable is the construction slice, not the package's overall
-comparison. The [package benchmark overview](../../README.md#overall-moonlightspade-comparison)
-also exposes insertion, removal, nearest-neighbour search, CDT recovery,
-refinement, natural-neighbour interpolation, Voronoi, DCEL, and segment
-traversal lanes.
-
-## Pictures
-
-Both pictures are derived from the single retained board.
-
-![Small-point Delaunay construction comparison](results/moonlight-delaunay-compare-small.svg)
-
-![Big-point Delaunay construction comparison](results/moonlight-delaunay-compare-big.svg)
-
-These retain Spade's plain GNUPLOT idiom: white field, Helvetica, dashed grey
-grid, jewel-coloured point series, and the same small/big split. Moonlight is
-the heavier orange star. The faint triangulation mesh is the sole bit of
-levity; benchmark charts need not resemble tax forms.
-
-## Result
-
-- timed cases: `120`
-- fixture summaries agreeing across all five implementations: `24 / 24`
-- Moonlight mean below plain Spade: `24 / 24`
-- Moonlight mean below Spade hierarchy: `24 / 24`
-- Moonlight below plain Spade with non-overlapping reported 2σ: `18 / 24`
-- Moonlight below Spade hierarchy with non-overlapping reported 2σ: `22 / 24`
-- lowest fixture mean: `delaunator` `21 / 24`; `cdt` `3 / 24`
-- median reported 2σ / mean: Moonlight `6.88%`; plain Spade `6.67%`
-
-Selected means are in milliseconds. Δ is
-`100 × (Moonlight / competitor − 1)`.
-
-| fixture | points | Moonlight | plain Spade | Δ | Spade hierarchy | Δ |
-|---|---:|---:|---:|---:|---:|---:|
-| local insertion | 4,000 | 0.587 | 0.771 | −23.8% | 0.800 | −26.6% |
-| local insertion | 6,000 | 0.971 | 1.300 | −25.3% | 1.365 | −28.9% |
-| local insertion | 8,000 | 1.354 | 1.776 | −23.7% | 1.903 | −28.8% |
-| local insertion | 250,000 | 60.300 | 72.713 | −17.1% | 77.496 | −22.2% |
-| uniform | 4,000 | 0.552 | 0.706 | −21.8% | 0.741 | −25.6% |
-| uniform | 14,000 | 2.663 | 3.067 | −13.2% | 3.318 | −19.7% |
-| uniform | 250,000 | 60.028 | 72.783 | −17.5% | 77.435 | −22.5% |
-
-The measured Moonlight path is the canonical circle-sweep bulk loader. Its
-geometry ingress keeps coordinates in immutable planes, moves only a 32-bit
-vertex-id plane through three radix passes, and restores the complete
-`(distance, x, y, vertex)` order inside equal-prefix fibres before descent.
-Fresh dense vertices are committed once, and the sweep consumes reserved cells
-without constructing a second packed geometry arena.
-
-Receipt identity:
-
-- command completed: `2026-08-23T00:00:15Z`, 318.68 s over 120 cases
-- host: Apple M4 Pro, arm64, macOS 26.5.2, GHC 9.14.1, rustc 1.92.0
-- Haskell build: cabal-install 3.16.1.0, focused `-O1` package profile,
-  comparison executable `-O2`, hot predicate, sweep, and repair modules LLVM
-  `-O3`
-- checkout base HEAD: `41906f44f0`, plus the three optimized runtime-source
-  owners named by the manifest
-- CSV: [`moonlight-delaunay-compare-2026-08-22.csv`](results/moonlight-delaunay-compare-2026-08-22.csv), SHA-256
-  `cce6af4ac7d570970a69124f5648db4e13ca41e91e05383306407a0dca323be7`
-- benchmark runtime-source hashes:
-  [`moonlight-delaunay-compare-2026-08-22.source-sha256`](results/moonlight-delaunay-compare-2026-08-22.source-sha256), SHA-256
-  `069d3bb1639b6d865fe4491b2fbbb9d0cdc8324173e30bebf690ed6592190c20`
-
-The Haskell projection in
-`Moonlight.Triangulation.Bench.DelaunayCompare.Picture` parses the tasty-bench
-CSV against the exact closed 120-case registry and reports typed obstructions
-for malformed, unknown, duplicated, or missing observations before gluing
-either picture.
-
-## Run
-
-From the repository root, list the complete benchmark tree:
-
-```console
-scripts/safe-cabal.sh run \
-  moonlight-triangulation:exe:moonlight-triangulation-delaunay-compare \
-  --project-file=cabal.project.triangulation-dev \
-  -- --list-tests
-```
-
-Run the comparison on one uncontended thread with wall-clock timing:
-
-```console
-scripts/safe-cabal.sh run \
-  moonlight-triangulation:exe:moonlight-triangulation-delaunay-compare \
-  --project-file=cabal.project.triangulation-dev \
-  -- --time-mode wall -j1
-```
-
-The Haskell executable builds the pinned Rust library through Cargo before it
-loads the library. Cargo's incremental no-op is cheap after the first run. Set
-`MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST` only when invoking an installed executable
-outside this repository layout.
-
-## Reproduce a board
-
-Every candidate run uses one CPU-time worker, a 30-second per-case timeout, and
-tasty-bench's `--stdev 5` calibration target. Name the candidate by the date it
-completed; after validation, it replaces the retained CSV and source manifest
-rather than accumulating a benchmark diary.
-
-```console
-caffeinate -i scripts/safe-cabal.sh run \
-  moonlight-triangulation:exe:moonlight-triangulation-delaunay-compare \
-  --project-file=cabal.project.triangulation-dev -- \
-  --stdev 5 --timeout 30s --time-mode cpu -j1 \
-  --csv foundation/moonlight-triangulation/bench/delaunay-compare/results/moonlight-delaunay-compare-YYYY-MM-DD.csv \
-  --color never --hide-progress --min-duration-to-report 1h
-```
-
-Both pictures are then regenerated from the candidate CSV, which is the only
-CSV they may be derived from:
-
-```console
-scripts/safe-cabal.sh run \
-  moonlight-triangulation:exe:moonlight-triangulation-delaunay-pictures \
-  --project-file=cabal.project.triangulation-dev -- \
-  foundation/moonlight-triangulation/bench/delaunay-compare/results/moonlight-delaunay-compare-YYYY-MM-DD.csv \
-  foundation/moonlight-triangulation/bench/delaunay-compare/results
-```
-
-The runtime-source manifest beside the CSV is regenerated from the measured
-worktree immediately after a completed command. It hashes the closed list of
-files that entered timed actions; picture sources and package-only metadata are
-excluded because they did not. The receipt, CSV, and big SVG stay
-benchmark-owned beside the executable and outside Cabal package inputs. The
-small SVG is also the package README projection, so it enters the sdist as
-documentation without entering any compiled component.
-
-## Upstream-compatible fixtures
-
-Both distributions use the 32-byte `StdRng` seed from upstream, including its
-embedded newline byte. Rust unit tests pin the first three points of both
-streams by their exact binary64 bit patterns.
-
-- `local insertion` starts at `(0, 1)` and adds independent inclusive steps
-  from `[-1, 1]`.
-- `uniform` draws each coordinate independently and inclusively from
-  `[-1e9, 1e9]`.
-- `small` contains 2,000 through 14,000 points in increments of 2,000.
-- `big` contains 50,000 through 250,000 points in increments of 50,000.
-
-Native input conversion happens once during suite preparation, outside every
-timed action, exactly as upstream's `DelaunayCrate.init` separates conversion
-from `run_creation`. Spade still clones its owned vertex vector inside each
-construction call because that is what its bulk-load API and upstream adapter
-require; `cdt`, `delaunator`, and Moonlight consume their prepared vectors by
-reference.
-
-## Compatibility gate
-
-Before `tasty-bench` runs, every one of the 24 fixtures descends across all five
-implementations. The sections glue only when vertex and inner-triangle counts
-agree. A generator, preparation, construction, foreign-boundary, or summary
-failure is a typed obstruction and terminates the command before timing.
-
-This count gate is deliberately not a second topology authority. The stronger
-Moonlight-versus-Spade canonical-edge and operation agreement owner is the
-[`spade-compare` operation suite](../spade-compare/README.md); use it when making semantic parity claims. This command answers
-the narrower upstream question: bulk construction time over the upstream point
-distributions and sizes.
-
-The upstream `examples/real_data_benchmark.rs` CDT/shapefile program is a
-separate executable and is not folded into this creation suite. No 45 MB dataset
-or network fetch is concealed in benchmark startup.
-
-## Harness difference
-
-Upstream uses Rust Criterion. This port uses Haskell `tasty-bench`, so it does
-not pretend Criterion's warm-up seconds or sample-count knobs map one-to-one to
-another calibrator. The inputs, sizes, implementation calls, and timed/setup
-boundary are preserved; calibration and reporting are honestly Haskell.
diff --git a/bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg b/bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg
deleted file mode 100644
--- a/bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg
+++ /dev/null
@@ -1,415 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280.0 720.0" role="img" aria-labelledby="title description">
-<title id="title">Moonlight Delaunay construction · small point sets</title>
-<desc id="description">CPU construction time for Spade, cdt, delaunator, and Moonlight over the upstream Delaunay comparison fixtures. Lower lines are faster.</desc>
-<defs>
-  <pattern id="triangulation-mesh" width="54" height="46" patternUnits="userSpaceOnUse">
-    <path d="M0 46L27 0L54 46ZM0 46L54 46M27 0L27 46" fill="none" stroke="rgb(255,140,0)" stroke-width="0.7"/>
-  </pattern>
-  <clipPath id="header-mesh-clip"><rect x="955" y="8" width="290" height="82" rx="20"/></clipPath>
-</defs>
-<rect width="1280.0" height="720.0" fill="white"/>
-<rect x="955" y="8" width="290" height="82" fill="url(#triangulation-mesh)" opacity="0.10" clip-path="url(#header-mesh-clip)"/>
-<text x="640.0" y="34.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="24" font-weight="500" fill="rgb(20,20,20)">Moonlight Delaunay construction · small point sets</text>
-<text x="640.0" y="61.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="13" font-weight="400" fill="rgb(20,20,20)">Moonlight against Spade's construction referents · CPU time · lower is faster</text>
-<g aria-label="spade 2">
-  <line x1="105.0" y1="101" x2="139.0" y2="101" stroke="rgb(0,0,139)" stroke-width="2.0"/>
-  <circle cx="122.0" cy="101.0" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <text x="148.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">spade 2</text>
-</g>
-<g aria-label="spade 2 hierarchy">
-  <line x1="315.0" y1="101" x2="349.0" y2="101" stroke="rgb(139,0,139)" stroke-width="2.0" stroke-dasharray="7 4"/>
-  <path d="M 332.0 96.7 L 327.9 104.5 L 336.1 104.5 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <text x="358.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">spade 2 hierarchy</text>
-</g>
-<g aria-label="cdt">
-  <line x1="560.0" y1="101" x2="594.0" y2="101" stroke="rgb(178,34,34)" stroke-width="2.0"/>
-  <rect x="573.8" y="97.8" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <text x="603.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">cdt</text>
-</g>
-<g aria-label="delaunator">
-  <line x1="735.0" y1="101" x2="769.0" y2="101" stroke="rgb(0,139,139)" stroke-width="2.0"/>
-  <path d="M 752.0 96.8 L 756.2 101.0 L 752.0 105.2 L 747.8 101.0 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <text x="778.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">delaunator</text>
-</g>
-<g aria-label="moonlight-triangulation">
-  <line x1="940.0" y1="101" x2="974.0" y2="101" stroke="rgb(255,140,0)" stroke-width="3.2"/>
-  <path d="M 957.0 95.6 L 958.7 99.3 L 962.4 101.0 L 958.7 102.7 L 957.0 106.4 L 955.3 102.7 L 951.6 101.0 L 955.3 99.3 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <text x="983.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">moonlight-triangulation</text>
-</g>
-<g aria-label="local insertion">
-<text x="340.0" y="144.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="15" font-weight="500" fill="rgb(20,20,20)">local insertion</text>
-<line x1="70.0" y1="610.0" x2="610.0" y2="610.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="60.0" y="614.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">0.00</text>
-<line x1="70.0" y1="521.6" x2="610.0" y2="521.6" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="60.0" y="525.6" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">1.0</text>
-<line x1="70.0" y1="433.2" x2="610.0" y2="433.2" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="60.0" y="437.2" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2.0</text>
-<line x1="70.0" y1="344.8" x2="610.0" y2="344.8" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="60.0" y="348.8" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">3.0</text>
-<line x1="70.0" y1="256.4" x2="610.0" y2="256.4" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="60.0" y="260.4" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4.0</text>
-<line x1="70.0" y1="168.0" x2="610.0" y2="168.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="60.0" y="172.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">5.0</text>
-<line x1="70.0" y1="610" x2="70.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="70.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2k</text>
-<line x1="160.0" y1="610" x2="160.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="160.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4k</text>
-<line x1="250.0" y1="610" x2="250.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="250.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">6k</text>
-<line x1="340.0" y1="610" x2="340.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="340.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">8k</text>
-<line x1="430.0" y1="610" x2="430.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="430.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">10k</text>
-<line x1="520.0" y1="610" x2="520.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="520.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">12k</text>
-<line x1="610.0" y1="610" x2="610.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="610.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">14k</text>
-<rect x="70.0" y="168.0" width="540.0" height="442.0" fill="none" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="340.0" y="664.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">input size (points)</text>
-<text x="15.0" y="389.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" fill="rgb(20,20,20)" transform="rotate(-90 15.0 389.0)">CPU time (ms)</text>
-<g aria-label="spade 2">
-  <polyline points="70.0,583.1 160.0,541.9 250.0,495.1 340.0,453.0 430.0,407.7 520.0,366.9 610.0,314.3" fill="none" stroke="rgb(0,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="70.0" y1="581.7" x2="70.0" y2="584.5" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="581.7" x2="73.0" y2="581.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="584.5" x2="73.0" y2="584.5" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="70.0" cy="583.1" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="160.0" y1="537.6" x2="160.0" y2="546.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="537.6" x2="163.0" y2="537.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="546.2" x2="163.0" y2="546.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="160.0" cy="541.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="250.0" y1="487.9" x2="250.0" y2="502.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="487.9" x2="253.0" y2="487.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="502.2" x2="253.0" y2="502.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="250.0" cy="495.1" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="340.0" y1="445.2" x2="340.0" y2="460.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="445.2" x2="343.0" y2="445.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="460.9" x2="343.0" y2="460.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="340.0" cy="453.0" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="430.0" y1="395.0" x2="430.0" y2="420.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="395.0" x2="433.0" y2="395.0" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="420.4" x2="433.0" y2="420.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="430.0" cy="407.7" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="520.0" y1="354.2" x2="520.0" y2="379.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="354.2" x2="523.0" y2="354.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="379.6" x2="523.0" y2="379.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="520.0" cy="366.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="610.0" y1="291.9" x2="610.0" y2="336.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="291.9" x2="613.0" y2="291.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="336.8" x2="613.0" y2="336.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="610.0" cy="314.3" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-</g>
-<g aria-label="spade 2 hierarchy">
-  <polyline points="70.0,582.7 160.0,539.3 250.0,489.3 340.0,441.8 430.0,391.2 520.0,346.0 610.0,292.9" fill="none" stroke="rgb(139,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 4"/>
-  <line x1="70.0" y1="580.0" x2="70.0" y2="585.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="580.0" x2="73.0" y2="580.0" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="585.3" x2="73.0" y2="585.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 70.0 578.4 L 65.9 586.2 L 74.1 586.2 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="160.0" y1="532.3" x2="160.0" y2="546.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="532.3" x2="163.0" y2="532.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="546.2" x2="163.0" y2="546.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 160.0 535.0 L 155.9 542.8 L 164.1 542.8 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="250.0" y1="482.4" x2="250.0" y2="496.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="482.4" x2="253.0" y2="482.4" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="496.2" x2="253.0" y2="496.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 250.0 485.0 L 245.9 492.8 L 254.1 492.8 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="340.0" y1="430.2" x2="340.0" y2="453.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="430.2" x2="343.0" y2="430.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="453.3" x2="343.0" y2="453.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 340.0 437.5 L 335.9 445.3 L 344.1 445.3 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="430.0" y1="381.5" x2="430.0" y2="400.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="381.5" x2="433.0" y2="381.5" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="400.9" x2="433.0" y2="400.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 430.0 386.9 L 425.9 394.7 L 434.1 394.7 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="520.0" y1="324.9" x2="520.0" y2="367.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="324.9" x2="523.0" y2="324.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="367.2" x2="523.0" y2="367.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 520.0 341.7 L 515.9 349.5 L 524.1 349.5 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="610.0" y1="274.3" x2="610.0" y2="311.5" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="274.3" x2="613.0" y2="274.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="311.5" x2="613.0" y2="311.5" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 610.0 288.6 L 605.9 296.4 L 614.1 296.4 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-</g>
-<g aria-label="cdt">
-  <polyline points="70.0,593.6 160.0,570.7 250.0,538.0 340.0,506.8 430.0,475.6 520.0,445.6 610.0,411.9" fill="none" stroke="rgb(178,34,34)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="70.0" y1="592.9" x2="70.0" y2="594.2" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="592.9" x2="73.0" y2="592.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="594.2" x2="73.0" y2="594.2" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="66.8" y="590.4" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="160.0" y1="567.9" x2="160.0" y2="573.5" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="567.9" x2="163.0" y2="567.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="573.5" x2="163.0" y2="573.5" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="156.8" y="567.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="250.0" y1="532.4" x2="250.0" y2="543.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="532.4" x2="253.0" y2="532.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="543.6" x2="253.0" y2="543.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="246.8" y="534.8" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="340.0" y1="500.2" x2="340.0" y2="513.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="500.2" x2="343.0" y2="500.2" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="513.3" x2="343.0" y2="513.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="336.8" y="503.6" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="430.0" y1="468.6" x2="430.0" y2="482.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="468.6" x2="433.0" y2="468.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="482.6" x2="433.0" y2="482.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="426.8" y="472.4" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="520.0" y1="435.9" x2="520.0" y2="455.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="435.9" x2="523.0" y2="435.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="455.3" x2="523.0" y2="455.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="516.8" y="442.4" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="610.0" y1="392.8" x2="610.0" y2="430.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="392.8" x2="613.0" y2="392.8" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="430.9" x2="613.0" y2="430.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="606.8" y="408.7" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-</g>
-<g aria-label="delaunator">
-  <polyline points="70.0,597.4 160.0,580.1 250.0,554.3 340.0,527.1 430.0,495.7 520.0,461.5 610.0,432.1" fill="none" stroke="rgb(0,139,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="70.0" y1="596.7" x2="70.0" y2="598.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="596.7" x2="73.0" y2="596.7" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="598.0" x2="73.0" y2="598.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 70.0 593.2 L 74.2 597.4 L 70.0 601.6 L 65.8 597.4 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="160.0" y1="577.8" x2="160.0" y2="582.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="577.8" x2="163.0" y2="577.8" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="582.4" x2="163.0" y2="582.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 160.0 575.9 L 164.2 580.1 L 160.0 584.3 L 155.8 580.1 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="250.0" y1="550.2" x2="250.0" y2="558.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="550.2" x2="253.0" y2="550.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="558.5" x2="253.0" y2="558.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 250.0 550.1 L 254.2 554.3 L 250.0 558.5 L 245.8 554.3 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="340.0" y1="522.0" x2="340.0" y2="532.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="522.0" x2="343.0" y2="522.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="532.2" x2="343.0" y2="532.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 340.0 522.9 L 344.2 527.1 L 340.0 531.3 L 335.8 527.1 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="430.0" y1="486.9" x2="430.0" y2="504.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="486.9" x2="433.0" y2="486.9" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="504.5" x2="433.0" y2="504.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 430.0 491.5 L 434.2 495.7 L 430.0 499.9 L 425.8 495.7 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="520.0" y1="453.0" x2="520.0" y2="470.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="453.0" x2="523.0" y2="453.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="470.1" x2="523.0" y2="470.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 520.0 457.3 L 524.2 461.5 L 520.0 465.7 L 515.8 461.5 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="610.0" y1="418.1" x2="610.0" y2="446.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="418.1" x2="613.0" y2="418.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="446.1" x2="613.0" y2="446.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 610.0 427.9 L 614.2 432.1 L 610.0 436.3 L 605.8 432.1 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-</g>
-<g aria-label="moonlight-triangulation">
-  <polyline points="70.0,585.5 160.0,558.1 250.0,524.2 340.0,490.3 430.0,456.2 520.0,417.5 610.0,361.2" fill="none" stroke="rgb(255,140,0)" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="70.0" y1="583.9" x2="70.0" y2="587.1" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="583.9" x2="73.0" y2="583.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="67.0" y1="587.1" x2="73.0" y2="587.1" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 70.0 580.1 L 71.7 583.8 L 75.4 585.5 L 71.7 587.2 L 70.0 590.9 L 68.3 587.2 L 64.6 585.5 L 68.3 583.8 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="160.0" y1="554.3" x2="160.0" y2="561.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="554.3" x2="163.0" y2="554.3" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="157.0" y1="561.9" x2="163.0" y2="561.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 160.0 552.7 L 161.7 556.4 L 165.4 558.1 L 161.7 559.8 L 160.0 563.5 L 158.3 559.8 L 154.6 558.1 L 158.3 556.4 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="250.0" y1="518.4" x2="250.0" y2="529.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="518.4" x2="253.0" y2="518.4" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="247.0" y1="529.9" x2="253.0" y2="529.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 250.0 518.8 L 251.7 522.5 L 255.4 524.2 L 251.7 525.9 L 250.0 529.6 L 248.3 525.9 L 244.6 524.2 L 248.3 522.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="340.0" y1="480.9" x2="340.0" y2="499.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="480.9" x2="343.0" y2="480.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="337.0" y1="499.6" x2="343.0" y2="499.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 340.0 484.9 L 341.7 488.6 L 345.4 490.3 L 341.7 492.0 L 340.0 495.7 L 338.3 492.0 L 334.6 490.3 L 338.3 488.6 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="430.0" y1="449.5" x2="430.0" y2="462.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="449.5" x2="433.0" y2="449.5" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="427.0" y1="462.9" x2="433.0" y2="462.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 430.0 450.8 L 431.7 454.5 L 435.4 456.2 L 431.7 457.9 L 430.0 461.6 L 428.3 457.9 L 424.6 456.2 L 428.3 454.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="520.0" y1="398.7" x2="520.0" y2="436.3" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="398.7" x2="523.0" y2="398.7" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="517.0" y1="436.3" x2="523.0" y2="436.3" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 520.0 412.1 L 521.7 415.8 L 525.4 417.5 L 521.7 419.2 L 520.0 422.9 L 518.3 419.2 L 514.6 417.5 L 518.3 415.8 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="610.0" y1="346.9" x2="610.0" y2="375.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="346.9" x2="613.0" y2="346.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="607.0" y1="375.6" x2="613.0" y2="375.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 610.0 355.8 L 611.7 359.5 L 615.4 361.2 L 611.7 362.9 L 610.0 366.6 L 608.3 362.9 L 604.6 361.2 L 608.3 359.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-</g>
-</g>
-<g aria-label="uniform">
-<text x="940.0" y="144.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="15" font-weight="500" fill="rgb(20,20,20)">uniform</text>
-<line x1="670.0" y1="610.0" x2="1210.0" y2="610.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="660.0" y="614.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">0.00</text>
-<line x1="670.0" y1="521.6" x2="1210.0" y2="521.6" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="660.0" y="525.6" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">1.0</text>
-<line x1="670.0" y1="433.2" x2="1210.0" y2="433.2" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="660.0" y="437.2" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2.0</text>
-<line x1="670.0" y1="344.8" x2="1210.0" y2="344.8" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="660.0" y="348.8" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">3.0</text>
-<line x1="670.0" y1="256.4" x2="1210.0" y2="256.4" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="660.0" y="260.4" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4.0</text>
-<line x1="670.0" y1="168.0" x2="1210.0" y2="168.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
-<text x="660.0" y="172.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">5.0</text>
-<line x1="670.0" y1="610" x2="670.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="670.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2k</text>
-<line x1="760.0" y1="610" x2="760.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="760.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4k</text>
-<line x1="850.0" y1="610" x2="850.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="850.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">6k</text>
-<line x1="940.0" y1="610" x2="940.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="940.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">8k</text>
-<line x1="1030.0" y1="610" x2="1030.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="1030.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">10k</text>
-<line x1="1120.0" y1="610" x2="1120.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="1120.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">12k</text>
-<line x1="1210.0" y1="610" x2="1210.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="1210.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">14k</text>
-<rect x="670.0" y="168.0" width="540.0" height="442.0" fill="none" stroke="rgb(25,25,25)" stroke-width="1"/>
-<text x="940.0" y="664.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">input size (points)</text>
-<text x="615.0" y="389.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" fill="rgb(20,20,20)" transform="rotate(-90 615.0 389.0)">CPU time (ms)</text>
-<g aria-label="spade 2">
-  <polyline points="670.0,583.7 760.0,547.6 850.0,506.8 940.0,466.9 1030.0,426.3 1120.0,383.7 1210.0,338.9" fill="none" stroke="rgb(0,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="670.0" y1="582.4" x2="670.0" y2="585.0" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="582.4" x2="673.0" y2="582.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="585.0" x2="673.0" y2="585.0" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="670.0" cy="583.7" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="760.0" y1="542.4" x2="760.0" y2="552.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="542.4" x2="763.0" y2="542.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="552.8" x2="763.0" y2="552.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="760.0" cy="547.6" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="850.0" y1="497.7" x2="850.0" y2="515.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="497.7" x2="853.0" y2="497.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="515.8" x2="853.0" y2="515.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="850.0" cy="506.8" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="940.0" y1="463.1" x2="940.0" y2="470.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="463.1" x2="943.0" y2="463.1" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="470.7" x2="943.0" y2="470.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="940.0" cy="466.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="1030.0" y1="415.5" x2="1030.0" y2="437.1" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="415.5" x2="1033.0" y2="415.5" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="437.1" x2="1033.0" y2="437.1" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="1030.0" cy="426.3" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="1120.0" y1="370.6" x2="1120.0" y2="396.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="370.6" x2="1123.0" y2="370.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="396.9" x2="1123.0" y2="396.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="1120.0" cy="383.7" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-  <line x1="1210.0" y1="317.9" x2="1210.0" y2="359.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="317.9" x2="1213.0" y2="317.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="359.9" x2="1213.0" y2="359.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <circle cx="1210.0" cy="338.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
-</g>
-<g aria-label="spade 2 hierarchy">
-  <polyline points="670.0,583.3 760.0,544.5 850.0,499.8 940.0,455.4 1030.0,406.4 1120.0,364.4 1210.0,316.7" fill="none" stroke="rgb(139,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 4"/>
-  <line x1="670.0" y1="582.1" x2="670.0" y2="584.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="582.1" x2="673.0" y2="582.1" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="584.6" x2="673.0" y2="584.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 670.0 579.0 L 665.9 586.8 L 674.1 586.8 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="760.0" y1="541.1" x2="760.0" y2="547.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="541.1" x2="763.0" y2="541.1" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="547.8" x2="763.0" y2="547.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 760.0 540.2 L 755.9 548.0 L 764.1 548.0 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="850.0" y1="489.8" x2="850.0" y2="509.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="489.8" x2="853.0" y2="489.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="509.8" x2="853.0" y2="509.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 850.0 495.5 L 845.9 503.3 L 854.1 503.3 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="940.0" y1="443.6" x2="940.0" y2="467.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="443.6" x2="943.0" y2="443.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="467.2" x2="943.0" y2="467.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 940.0 451.1 L 935.9 458.9 L 944.1 458.9 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="1030.0" y1="387.9" x2="1030.0" y2="424.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="387.9" x2="1033.0" y2="387.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="424.9" x2="1033.0" y2="424.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1030.0 402.1 L 1025.9 409.9 L 1034.1 409.9 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="1120.0" y1="344.7" x2="1120.0" y2="384.0" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="344.7" x2="1123.0" y2="344.7" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="384.0" x2="1123.0" y2="384.0" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1120.0 360.1 L 1115.9 367.9 L 1124.1 367.9 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-  <line x1="1210.0" y1="306.8" x2="1210.0" y2="326.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="306.8" x2="1213.0" y2="306.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="326.6" x2="1213.0" y2="326.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1210.0 312.4 L 1205.9 320.2 L 1214.1 320.2 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
-</g>
-<g aria-label="cdt">
-  <polyline points="670.0,593.7 760.0,570.4 850.0,534.1 940.0,500.7 1030.0,465.7 1120.0,431.7 1210.0,396.1" fill="none" stroke="rgb(178,34,34)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="670.0" y1="592.7" x2="670.0" y2="594.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="592.7" x2="673.0" y2="592.7" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="594.6" x2="673.0" y2="594.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="666.8" y="590.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="760.0" y1="569.0" x2="760.0" y2="571.7" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="569.0" x2="763.0" y2="569.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="571.7" x2="763.0" y2="571.7" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="756.8" y="567.2" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="850.0" y1="529.0" x2="850.0" y2="539.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="529.0" x2="853.0" y2="529.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="539.3" x2="853.0" y2="539.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="846.8" y="530.9" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="940.0" y1="491.0" x2="940.0" y2="510.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="491.0" x2="943.0" y2="491.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="510.3" x2="943.0" y2="510.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="936.8" y="497.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="1030.0" y1="455.0" x2="1030.0" y2="476.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="455.0" x2="1033.0" y2="455.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="476.4" x2="1033.0" y2="476.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="1026.8" y="462.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="1120.0" y1="420.1" x2="1120.0" y2="443.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="420.1" x2="1123.0" y2="420.1" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="443.4" x2="1123.0" y2="443.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="1116.8" y="428.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-  <line x1="1210.0" y1="379.4" x2="1210.0" y2="412.8" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="379.4" x2="1213.0" y2="379.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="412.8" x2="1213.0" y2="412.8" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
-  <rect x="1206.8" y="392.9" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
-</g>
-<g aria-label="delaunator">
-  <polyline points="670.0,597.2 760.0,580.7 850.0,558.9 940.0,530.2 1030.0,497.9 1120.0,466.9 1210.0,434.3" fill="none" stroke="rgb(0,139,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="670.0" y1="596.0" x2="670.0" y2="598.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="596.0" x2="673.0" y2="596.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="598.5" x2="673.0" y2="598.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 670.0 593.0 L 674.2 597.2 L 670.0 601.4 L 665.8 597.2 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="760.0" y1="578.8" x2="760.0" y2="582.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="578.8" x2="763.0" y2="578.8" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="582.6" x2="763.0" y2="582.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 760.0 576.5 L 764.2 580.7 L 760.0 584.9 L 755.8 580.7 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="850.0" y1="554.6" x2="850.0" y2="563.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="554.6" x2="853.0" y2="554.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="563.2" x2="853.0" y2="563.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 850.0 554.7 L 854.2 558.9 L 850.0 563.1 L 845.8 558.9 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="940.0" y1="529.0" x2="940.0" y2="531.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="529.0" x2="943.0" y2="529.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="531.5" x2="943.0" y2="531.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 940.0 526.0 L 944.2 530.2 L 940.0 534.4 L 935.8 530.2 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="1030.0" y1="487.6" x2="1030.0" y2="508.3" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="487.6" x2="1033.0" y2="487.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="508.3" x2="1033.0" y2="508.3" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1030.0 493.7 L 1034.2 497.9 L 1030.0 502.1 L 1025.8 497.9 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="1120.0" y1="461.4" x2="1120.0" y2="472.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="461.4" x2="1123.0" y2="461.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="472.4" x2="1123.0" y2="472.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1120.0 462.7 L 1124.2 466.9 L 1120.0 471.1 L 1115.8 466.9 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-  <line x1="1210.0" y1="419.9" x2="1210.0" y2="448.7" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="419.9" x2="1213.0" y2="419.9" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="448.7" x2="1213.0" y2="448.7" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1210.0 430.1 L 1214.2 434.3 L 1210.0 438.5 L 1205.8 434.3 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
-</g>
-<g aria-label="moonlight-triangulation">
-  <polyline points="670.0,585.5 760.0,561.2 850.0,530.7 940.0,496.8 1030.0,462.3 1120.0,419.3 1210.0,374.6" fill="none" stroke="rgb(255,140,0)" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
-  <line x1="670.0" y1="584.0" x2="670.0" y2="587.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="584.0" x2="673.0" y2="584.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="667.0" y1="587.0" x2="673.0" y2="587.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 670.0 580.1 L 671.7 583.8 L 675.4 585.5 L 671.7 587.2 L 670.0 590.9 L 668.3 587.2 L 664.6 585.5 L 668.3 583.8 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="760.0" y1="560.5" x2="760.0" y2="562.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="560.5" x2="763.0" y2="560.5" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="757.0" y1="562.0" x2="763.0" y2="562.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 760.0 555.8 L 761.7 559.5 L 765.4 561.2 L 761.7 562.9 L 760.0 566.6 L 758.3 562.9 L 754.6 561.2 L 758.3 559.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="850.0" y1="522.8" x2="850.0" y2="538.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="522.8" x2="853.0" y2="522.8" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="847.0" y1="538.6" x2="853.0" y2="538.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 850.0 525.3 L 851.7 529.0 L 855.4 530.7 L 851.7 532.4 L 850.0 536.1 L 848.3 532.4 L 844.6 530.7 L 848.3 529.0 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="940.0" y1="485.9" x2="940.0" y2="507.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="485.9" x2="943.0" y2="485.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="937.0" y1="507.6" x2="943.0" y2="507.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 940.0 491.4 L 941.7 495.1 L 945.4 496.8 L 941.7 498.5 L 940.0 502.2 L 938.3 498.5 L 934.6 496.8 L 938.3 495.1 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="1030.0" y1="451.9" x2="1030.0" y2="472.7" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="451.9" x2="1033.0" y2="451.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1027.0" y1="472.7" x2="1033.0" y2="472.7" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1030.0 456.9 L 1031.7 460.6 L 1035.4 462.3 L 1031.7 464.0 L 1030.0 467.7 L 1028.3 464.0 L 1024.6 462.3 L 1028.3 460.6 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="1120.0" y1="401.2" x2="1120.0" y2="437.4" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="401.2" x2="1123.0" y2="401.2" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1117.0" y1="437.4" x2="1123.0" y2="437.4" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1120.0 413.9 L 1121.7 417.6 L 1125.4 419.3 L 1121.7 421.0 L 1120.0 424.7 L 1118.3 421.0 L 1114.6 419.3 L 1118.3 417.6 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-  <line x1="1210.0" y1="364.2" x2="1210.0" y2="385.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="364.2" x2="1213.0" y2="364.2" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <line x1="1207.0" y1="385.0" x2="1213.0" y2="385.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
-  <path d="M 1210.0 369.2 L 1211.7 372.9 L 1215.4 374.6 L 1211.7 376.3 L 1210.0 380.0 L 1208.3 376.3 L 1204.6 374.6 L 1208.3 372.9 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
-</g>
-</g>
-<text x="640.0" y="704.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">Whiskers show tasty-bench's reported 2× standard-deviation spread.</text>
-</svg>
diff --git a/bench/delaunay-compare/rust/Cargo.lock b/bench/delaunay-compare/rust/Cargo.lock
deleted file mode 100644
--- a/bench/delaunay-compare/rust/Cargo.lock
+++ /dev/null
@@ -1,264 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
-[[package]]
-name = "autocfg"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
-
-[[package]]
-name = "cdt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91b872294f63ef586b32aa94141561681aa35ca2d703960cca4f661f4e18184"
-dependencies = [
- "geometry-predicates",
- "thiserror",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "delaunator"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e1ee323c1275374f7e612d3724d12707079fb6a2117349fe144def656f5a880"
-dependencies = [
- "robust",
-]
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "foldhash"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
-
-[[package]]
-name = "geometry-predicates"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dba84198cf199c11b83f1cb9243eaeb70dc50b719d2835ebf34bf2481bca010"
-
-[[package]]
-name = "getrandom"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
-dependencies = [
- "cfg-if",
- "libc",
- "r-efi",
- "wasip2",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.16.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
-dependencies = [
- "allocator-api2",
- "equivalent",
- "foldhash",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.189"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
-
-[[package]]
-name = "moonlight-delaunay-compare-referents"
-version = "0.1.0"
-dependencies = [
- "cdt",
- "delaunator",
- "rand",
- "spade",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
-dependencies = [
- "zerocopy",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.107"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.47"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "r-efi"
-version = "5.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
-
-[[package]]
-name = "rand"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
-dependencies = [
- "rand_chacha",
- "rand_core",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
-dependencies = [
- "getrandom",
-]
-
-[[package]]
-name = "robust"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
-
-[[package]]
-name = "smallvec"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
-
-[[package]]
-name = "spade"
-version = "2.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
-dependencies = [
- "hashbrown",
- "num-traits",
- "robust",
- "smallvec",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.119"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "wasip2"
-version = "1.0.4+wasi-0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
-dependencies = [
- "wit-bindgen",
-]
-
-[[package]]
-name = "wit-bindgen"
-version = "0.57.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-
-[[package]]
-name = "zerocopy"
-version = "0.8.56"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
-dependencies = [
- "zerocopy-derive",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.8.56"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
diff --git a/bench/delaunay-compare/rust/Cargo.toml b/bench/delaunay-compare/rust/Cargo.toml
deleted file mode 100644
--- a/bench/delaunay-compare/rust/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[workspace]
-
-[package]
-name = "moonlight-delaunay-compare-referents"
-version = "0.1.0"
-edition = "2021"
-
-[lib]
-name = "moonlight_delaunay_compare_referents"
-crate-type = ["cdylib"]
-
-[dependencies]
-cdt = "0.1"
-delaunator = "1.0.1"
-rand = "0.9.1"
-spade = "2.15"
-
-[profile.release]
-opt-level = 3
-lto = "thin"
diff --git a/bench/delaunay-compare/rust/src/lib.rs b/bench/delaunay-compare/rust/src/lib.rs
deleted file mode 100644
--- a/bench/delaunay-compare/rust/src/lib.rs
+++ /dev/null
@@ -1,397 +0,0 @@
-//! Rust referent boundary for Moonlight's Delaunay construction comparison.
-//!
-//! Fixture choice, benchmark grouping, agreement, timing, and reporting belong
-//! to Haskell. This library retains only the operations that cannot move there:
-//! Spade's exact seeded generator and direct calls into the Rust crates under
-//! comparison.
-
-use core::ffi::c_void;
-use core::hint::black_box;
-use core::ptr;
-use rand::distr::{Distribution, Uniform};
-use rand::rngs::StdRng;
-use rand::SeedableRng;
-use spade::{HierarchyHintGenerator, Point2, Triangulation};
-use std::panic::{catch_unwind, AssertUnwindSafe};
-
-const UPSTREAM_SEED: [u8; 32] = [
-    0xfb, 0xdc, 0x4e, 0xa0, 0x30, 0xde, 0x82, 0xba, 0x69, 0x97, 0x3c, 0x52, 0x49, 0x4d, 0x00, 0xca,
-    0x0a, 0x5c, 0x21, 0xa3, 0x8d, 0x5c, 0xf2, 0x34, 0x4e, 0x58, 0x7d, 0x80, 0x16, 0x66, 0x23, 0x30,
-];
-
-const UNIFORM_RANGE: f64 = 1.0e9;
-const LOCAL_STEP_RANGE: f64 = 1.0;
-
-type PlainSpade = spade::DelaunayTriangulation<Point2<f64>>;
-type HierarchySpade =
-    spade::DelaunayTriangulation<Point2<f64>, (), (), (), HierarchyHintGenerator<f64>>;
-
-#[repr(i32)]
-#[derive(Clone, Copy, Debug)]
-enum BoundaryStatus {
-    Success = 0,
-    NullPointer = 1,
-    UnknownTag = 2,
-    CoordinateCountMismatch = 3,
-    DistributionConstructionFailed = 4,
-    TriangulationFailed = 5,
-    Panicked = 6,
-}
-
-impl BoundaryStatus {
-    const fn code(self) -> i32 {
-        self as i32
-    }
-}
-
-#[derive(Clone, Copy)]
-enum PointDistribution {
-    LocalInsertion,
-    Uniform,
-}
-
-impl TryFrom<i32> for PointDistribution {
-    type Error = BoundaryStatus;
-
-    fn try_from(tag: i32) -> Result<Self, Self::Error> {
-        match tag {
-            0 => Ok(Self::LocalInsertion),
-            1 => Ok(Self::Uniform),
-            _ => Err(BoundaryStatus::UnknownTag),
-        }
-    }
-}
-
-#[derive(Clone, Copy)]
-enum NativeImplementation {
-    Spade,
-    SpadeHierarchy,
-    Cdt,
-    Delaunator,
-}
-
-impl TryFrom<i32> for NativeImplementation {
-    type Error = BoundaryStatus;
-
-    fn try_from(tag: i32) -> Result<Self, Self::Error> {
-        match tag {
-            0 => Ok(Self::Spade),
-            1 => Ok(Self::SpadeHierarchy),
-            2 => Ok(Self::Cdt),
-            3 => Ok(Self::Delaunator),
-            _ => Err(BoundaryStatus::UnknownTag),
-        }
-    }
-}
-
-enum PreparedImplementation {
-    Spade(Vec<Point2<f64>>),
-    SpadeHierarchy(Vec<Point2<f64>>),
-    Cdt(Vec<(f64, f64)>),
-    Delaunator(Vec<delaunator::Point>),
-}
-
-#[derive(Clone, Copy)]
-struct ConstructionSummary {
-    vertices: usize,
-    triangles: usize,
-}
-
-impl PreparedImplementation {
-    fn from_coordinates(implementation: NativeImplementation, coordinates: &[[f64; 2]]) -> Self {
-        match implementation {
-            NativeImplementation::Spade => {
-                Self::Spade(coordinates.iter().copied().map(Point2::from).collect())
-            }
-            NativeImplementation::SpadeHierarchy => {
-                Self::SpadeHierarchy(coordinates.iter().copied().map(Point2::from).collect())
-            }
-            NativeImplementation::Cdt => {
-                Self::Cdt(coordinates.iter().map(|[x, y]| (*x, *y)).collect())
-            }
-            NativeImplementation::Delaunator => Self::Delaunator(
-                coordinates
-                    .iter()
-                    .map(|[x, y]| delaunator::Point { x: *x, y: *y })
-                    .collect(),
-            ),
-        }
-    }
-
-    fn run(&self) -> Result<ConstructionSummary, BoundaryStatus> {
-        match self {
-            Self::Spade(vertices) => {
-                let triangulation = PlainSpade::bulk_load(vertices.clone())
-                    .map_err(|_| BoundaryStatus::TriangulationFailed)?;
-                let summary = ConstructionSummary {
-                    vertices: triangulation.num_vertices(),
-                    triangles: triangulation.num_inner_faces(),
-                };
-                black_box(triangulation);
-                Ok(summary)
-            }
-            Self::SpadeHierarchy(vertices) => {
-                let triangulation = HierarchySpade::bulk_load(vertices.clone())
-                    .map_err(|_| BoundaryStatus::TriangulationFailed)?;
-                let summary = ConstructionSummary {
-                    vertices: triangulation.num_vertices(),
-                    triangles: triangulation.num_inner_faces(),
-                };
-                black_box(triangulation);
-                Ok(summary)
-            }
-            Self::Cdt(vertices) => {
-                let triangles = cdt::triangulate_points(vertices)
-                    .map_err(|_| BoundaryStatus::TriangulationFailed)?;
-                let summary = ConstructionSummary {
-                    vertices: vertices.len(),
-                    triangles: triangles.len(),
-                };
-                black_box(triangles);
-                Ok(summary)
-            }
-            Self::Delaunator(vertices) => {
-                let triangulation = delaunator::triangulate(vertices);
-                let summary = ConstructionSummary {
-                    vertices: vertices.len(),
-                    triangles: triangulation.triangles.len() / 3,
-                };
-                black_box(triangulation);
-                Ok(summary)
-            }
-        }
-    }
-}
-
-fn boundary_status(action: impl FnOnce() -> Result<(), BoundaryStatus>) -> i32 {
-    match catch_unwind(AssertUnwindSafe(action)) {
-        Ok(Ok(())) => BoundaryStatus::Success.code(),
-        Ok(Err(status)) => status.code(),
-        Err(_) => BoundaryStatus::Panicked.code(),
-    }
-}
-
-fn coordinate_count(point_count: usize) -> Result<usize, BoundaryStatus> {
-    point_count
-        .checked_mul(2)
-        .ok_or(BoundaryStatus::CoordinateCountMismatch)
-}
-
-fn uniform_samples(point_count: usize) -> Result<impl Iterator<Item = [f64; 2]>, BoundaryStatus> {
-    let distribution = Uniform::new_inclusive(-UNIFORM_RANGE, UNIFORM_RANGE)
-        .map_err(|_| BoundaryStatus::DistributionConstructionFailed)?;
-    let mut generator = StdRng::from_seed(UPSTREAM_SEED);
-    Ok(core::iter::from_fn(move || {
-        Some([
-            distribution.sample(&mut generator),
-            distribution.sample(&mut generator),
-        ])
-    })
-    .take(point_count))
-}
-
-fn local_insertion_samples(
-    point_count: usize,
-) -> Result<impl Iterator<Item = [f64; 2]>, BoundaryStatus> {
-    let distribution = Uniform::new_inclusive(-LOCAL_STEP_RANGE, LOCAL_STEP_RANGE)
-        .map_err(|_| BoundaryStatus::DistributionConstructionFailed)?;
-    let mut generator = StdRng::from_seed(UPSTREAM_SEED);
-    let mut previous = [0.0, 1.0];
-    Ok(core::iter::from_fn(move || {
-        previous = [
-            previous[0] + distribution.sample(&mut generator),
-            previous[1] + distribution.sample(&mut generator),
-        ];
-        Some(previous)
-    })
-    .take(point_count))
-}
-
-fn write_coordinates(
-    distribution: PointDistribution,
-    point_count: usize,
-    output: &mut [f64],
-) -> Result<(), BoundaryStatus> {
-    let expected_count = coordinate_count(point_count)?;
-    if output.len() != expected_count {
-        return Err(BoundaryStatus::CoordinateCountMismatch);
-    }
-
-    let samples: Box<dyn Iterator<Item = [f64; 2]>> = match distribution {
-        PointDistribution::LocalInsertion => Box::new(local_insertion_samples(point_count)?),
-        PointDistribution::Uniform => Box::new(uniform_samples(point_count)?),
-    };
-
-    output
-        .chunks_exact_mut(2)
-        .zip(samples)
-        .for_each(|(coordinate_pair, [x, y])| {
-            coordinate_pair.copy_from_slice(&[x, y]);
-        });
-    Ok(())
-}
-
-/// Fill `coordinates` with Spade's exact seeded benchmark distribution.
-///
-/// # Safety
-///
-/// A non-null `coordinates` pointer must be writable for `coordinate_count`
-/// consecutive `f64` values.
-#[no_mangle]
-pub unsafe extern "C" fn delaunay_compare_generate(
-    distribution_tag: i32,
-    point_count: usize,
-    coordinates: *mut f64,
-    supplied_coordinate_count: usize,
-) -> i32 {
-    boundary_status(|| {
-        let distribution = PointDistribution::try_from(distribution_tag)?;
-        let expected_count = coordinate_count(point_count)?;
-        if supplied_coordinate_count != expected_count {
-            return Err(BoundaryStatus::CoordinateCountMismatch);
-        }
-        if coordinates.is_null() && supplied_coordinate_count != 0 {
-            return Err(BoundaryStatus::NullPointer);
-        }
-        let output = if supplied_coordinate_count == 0 {
-            &mut []
-        } else {
-            // SAFETY: the caller contract and checks above establish a non-null
-            // writable region of exactly `supplied_coordinate_count` values.
-            unsafe { core::slice::from_raw_parts_mut(coordinates, supplied_coordinate_count) }
-        };
-        write_coordinates(distribution, point_count, output)
-    })
-}
-
-/// Prepare the implementation-specific native input outside the timed region.
-///
-/// # Safety
-///
-/// A non-null `coordinates` pointer must be readable for `2 * point_count`
-/// consecutive `f64` values. `prepared_output` must be writable for one pointer.
-#[no_mangle]
-pub unsafe extern "C" fn delaunay_compare_prepare(
-    implementation_tag: i32,
-    coordinates: *const f64,
-    point_count: usize,
-    prepared_output: *mut *mut c_void,
-) -> i32 {
-    boundary_status(|| {
-        let implementation = NativeImplementation::try_from(implementation_tag)?;
-        if prepared_output.is_null() {
-            return Err(BoundaryStatus::NullPointer);
-        }
-        let supplied_coordinate_count = coordinate_count(point_count)?;
-        if coordinates.is_null() && supplied_coordinate_count != 0 {
-            return Err(BoundaryStatus::NullPointer);
-        }
-        let flat_coordinates = if supplied_coordinate_count == 0 {
-            &[]
-        } else {
-            // SAFETY: the caller contract and checks above establish a non-null
-            // readable region of exactly `supplied_coordinate_count` values.
-            unsafe { core::slice::from_raw_parts(coordinates, supplied_coordinate_count) }
-        };
-        let coordinate_pairs = flat_coordinates.as_chunks::<2>().0;
-        let prepared = PreparedImplementation::from_coordinates(implementation, coordinate_pairs);
-        // SAFETY: `prepared_output` was checked non-null and points to writable
-        // storage for one opaque pointer by the caller contract.
-        unsafe {
-            ptr::write(prepared_output, Box::into_raw(Box::new(prepared)).cast());
-        }
-        Ok(())
-    })
-}
-
-/// Construct one triangulation from a prepared native input.
-///
-/// # Safety
-///
-/// `prepared` must be a live pointer returned by `delaunay_compare_prepare`.
-/// Both output pointers must be writable for one `usize`.
-#[no_mangle]
-pub unsafe extern "C" fn delaunay_compare_run(
-    prepared: *const c_void,
-    vertex_count_output: *mut usize,
-    triangle_count_output: *mut usize,
-) -> i32 {
-    boundary_status(|| {
-        if prepared.is_null() || vertex_count_output.is_null() || triangle_count_output.is_null() {
-            return Err(BoundaryStatus::NullPointer);
-        }
-        // SAFETY: the caller contract requires a live pointer produced by the
-        // matching prepare function and keeps it alive for this call.
-        let prepared_implementation = unsafe { &*prepared.cast::<PreparedImplementation>() };
-        let summary = prepared_implementation.run()?;
-        // SAFETY: both output pointers were checked non-null and the caller
-        // contract gives writable storage for one value at each address.
-        unsafe {
-            ptr::write(vertex_count_output, summary.vertices);
-            ptr::write(triangle_count_output, summary.triangles);
-        }
-        Ok(())
-    })
-}
-
-/// Release one prepared native input. A null pointer is a no-op.
-///
-/// # Safety
-///
-/// A non-null pointer must have been returned by `delaunay_compare_prepare`
-/// and must not have been released before.
-#[no_mangle]
-pub unsafe extern "C" fn delaunay_compare_release(prepared: *mut c_void) {
-    if !prepared.is_null() {
-        // SAFETY: the caller contract transfers the one remaining ownership of
-        // the allocation produced by `Box::into_raw` in the prepare function.
-        unsafe {
-            drop(Box::from_raw(prepared.cast::<PreparedImplementation>()));
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{local_insertion_samples, uniform_samples};
-
-    #[test]
-    fn local_insertion_prefix_matches_upstream_fixture_bits() {
-        let observed = local_insertion_samples(3)
-            .expect("the fixed local-insertion distribution is valid")
-            .flatten()
-            .map(f64::to_bits)
-            .collect::<Vec<_>>();
-        assert_eq!(
-            observed,
-            [
-                0xbfe28b04856ac6db,
-                0x3ff8c718b563a19a,
-                0x3fc014ab09d22a14,
-                0x4003a1ee45dc4712,
-                0xbfd097c41d82adc4,
-                0x4008d420538d5c3d,
-            ]
-        );
-    }
-
-    #[test]
-    fn uniform_prefix_matches_upstream_fixture_bits() {
-        let observed = uniform_samples(3)
-            .expect("the fixed uniform distribution is valid")
-            .flatten()
-            .map(f64::to_bits)
-            .collect::<Vec<_>>();
-        assert_eq!(
-            observed,
-            [
-                0xc1c1450134a5bd5a,
-                0x41c0598b1e249a72,
-                0x41c5037dbf1c0b38,
-                0x41cafc1cf53845c8,
-                0xc1b6f1036f0b1386,
-                0x41c35b5d4df8b356,
-            ]
-        );
-    }
-}
diff --git a/bench/join/Moonlight/Triangulation/JoinBench.hs b/bench/join/Moonlight/Triangulation/JoinBench.hs
--- a/bench/join/Moonlight/Triangulation/JoinBench.hs
+++ b/bench/join/Moonlight/Triangulation/JoinBench.hs
@@ -575,8 +575,12 @@
   preview <- requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension)
   signature <- requireRight (extensionSignature preview)
   let expectedConstraints = Set.union expectedBaseConstraints expectedExtensionConstraints
-  unless (extensionConstraintGeometry signature == expectedConstraints) $
-    fail "source-preserving seam did not retain the fixed A/B contour constraints"
+      actualConstraints = extensionConstraintGeometry signature
+  unless (expectedConstraints `Set.isSubsetOf` actualConstraints) $
+    fail
+      ( "source-preserving seam did not retain the fixed A/B contour constraints; missing="
+          <> show (Set.toAscList (Set.difference expectedConstraints actualConstraints))
+      )
   pure
     PreparedSeparatedExtension
       { preparedBase = base
@@ -626,7 +630,6 @@
               (const ())
               parameters
               permitted
-              interface
               joined
           )
       )
@@ -679,8 +682,6 @@
         <> show (V.length (constrainedSeamJoinFaces joinedReceipt))
         <> " a-constraints="
         <> show (constrainedSeamLeftConstraintCount joinedReceipt)
-        <> " b-constraints="
-        <> show (V.length (constrainedSeamRightConstraintEvidence joinedReceipt))
         <> " gamma="
         <> show (Set.size interface)
         <> " final-omega="
diff --git a/bench/spade-compare/hs-alpha/Main.hs b/bench/spade-compare/hs-alpha/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs-alpha/Main.hs
@@ -0,0 +1,66 @@
+-- | Narrow Haskell interpreter for the opt-in alpha-persistence diagnostic.
+module Main (main) where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Vector qualified as Vector
+import Moonlight.Triangulation.Bench.SpadeCompare.Alpha
+  ( alphaPersistenceReceipt
+  , renderAlphaPersistenceReceipt
+  )
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Triangulation.Bench.SpadeCompare.Lane
+  ( LaneKind (AlphaPersistenceLane)
+  , LaneObstruction
+  , LaneRequest (..)
+  , parseLaneRequest
+  , renderLaneObstruction
+  )
+import Moonlight.Triangulation.Bench.SpadeCompare.Timing (measureLane)
+import System.Environment (getArgs)
+import System.Exit (ExitCode (ExitFailure), exitWith)
+import System.IO (hPutStrLn, stderr)
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  case arguments of
+    ["alpha-receipt", rawCount] ->
+      either refuseLane reportReceipt
+        (parseLaneRequest "alpha-persistence" rawCount "0")
+    ["bench-one", lane, first, second] ->
+      either refuseLane runAlphaLane (parseLaneRequest lane first second)
+    _ -> refuseUsage
+
+runAlphaLane :: LaneRequest -> IO ()
+runAlphaLane request@(LaneRequest AlphaPersistenceLane pointCount _) = do
+  points <-
+    evaluate
+      (force (Vector.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)))
+  measureLane request (requireResult (alphaPersistenceReceipt points))
+runAlphaLane _ = refuseUsage
+
+reportReceipt :: LaneRequest -> IO ()
+reportReceipt (LaneRequest AlphaPersistenceLane pointCount _) = do
+  let points = Vector.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+  receipt <- requireResult (alphaPersistenceReceipt points)
+  putStrLn (renderAlphaPersistenceReceipt receipt)
+reportReceipt _ = refuseUsage
+
+requireResult :: Show obstruction => Either obstruction value -> IO value
+requireResult result =
+  case result of
+    Left obstruction -> do
+      hPutStrLn stderr ("alpha persistence refused: " <> show obstruction)
+      exitWith (ExitFailure 1)
+    Right value -> pure value
+
+refuseLane :: LaneObstruction -> IO value
+refuseLane obstruction = do
+  hPutStrLn stderr (renderLaneObstruction obstruction)
+  exitWith (ExitFailure 2)
+
+refuseUsage :: IO value
+refuseUsage = do
+  hPutStrLn stderr "usage: moonlight-triangulation-alpha-spade-referent (alpha-receipt COUNT | bench-one alpha-persistence COUNT 0)"
+  exitWith (ExitFailure 2)
diff --git a/bench/spade-compare/hs/Main.hs b/bench/spade-compare/hs/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Main.hs
@@ -0,0 +1,290 @@
+-- | Haskell half of the spade external referent.
+--
+-- @moonlight-triangulation@ is a port of the Rust @spade@ crate, so spade is the
+-- referent that grades it. Both halves generate their inputs from the same LCG
+-- with the same seeds and emit their gate output in the same encoding, so
+-- neither side can reformat a disagreement into agreement.
+--
+-- Vertex handles are NOT comparable across the two implementations — both bulk
+-- loaders reorder. Anything naming a vertex names it by input index or by
+-- coordinate, never by handle.
+{-# LANGUAGE DataKinds #-}
+
+module Main (main) where
+
+import Control.DeepSeq (NFData, force)
+import Control.Exception (evaluate)
+import Control.Monad.ST (runST)
+import Data.Word (Word64)
+import Moonlight.Triangulation.Internal.Mutable (freezeTriangulation, thawTriangulation)
+import System.Environment (getArgs)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.BulkLoad
+import Moonlight.Triangulation.Cdt
+import Moonlight.Triangulation.Handles.HandleDefs
+import Moonlight.Triangulation.HintGenerator
+import Moonlight.Triangulation.Interpolation
+import Moonlight.Triangulation.PointLocation
+import Moonlight.Triangulation.Types
+import Moonlight.Triangulation.Bench.SpadeCompare.Constraint
+import Moonlight.Triangulation.Bench.SpadeCompare.Dcel
+import Moonlight.Triangulation.Bench.SpadeCompare.Delaunay
+import Moonlight.Triangulation.Bench.SpadeCompare.Gates
+import Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy
+import Moonlight.Triangulation.Bench.SpadeCompare.Interpolation
+import Moonlight.Triangulation.Bench.SpadeCompare.Intersection
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Triangulation.Bench.SpadeCompare.Lane
+import Moonlight.Triangulation.Bench.SpadeCompare.Removal
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Timing (measureLane)
+import Moonlight.Triangulation.Bench.SpadeCompare.Voronoi
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  case arguments of
+    ["gate", directory] -> writeGate directory
+    ["divergence", path] -> writeDivergence path
+    ["inventory-csv"] -> putStr renderInventoryCsv
+    ["inventory-human"] -> putStr renderInventoryHuman
+    ["lane-specs", laneClass] -> either refuseLane putStr (renderLaneSpecs laneClass)
+    ["snapshot-specs"] -> putStr renderSnapshotSpecs
+    ["bench-one", lane, first, second] ->
+      either refuseLane runLane (parseLaneRequest lane first second)
+    ["counters", budget] -> reportRefinementCounters (read budget)
+    ["sweep-stats", count] -> reportSweepStats (read count)
+    ["removal-audit", count, removals] -> reportRemovalAudit (read count) (read removals)
+    ["removal-context", count, index] -> reportRemovalContext (read count) (read index)
+    ["fresh-suffix", count, dropped, path] -> writeFreshSuffix (read count) (read dropped) path
+    ["interp-audit", count, queries] -> reportInterpolationAudit (read count) (read queries)
+    ["interp-context", count] -> reportInterpolationContext (read count) Nothing
+    ["interp-context", count, index] -> reportInterpolationContext (read count) (Just (read index))
+    ["interp-locate-audit", count, queries] -> reportInterpolationLocateAudit (read count) (read queries)
+    ["interp-cavity-audit", count, queries] -> reportInterpolationCavityAudit (read count) (read queries)
+    _ -> do
+      hPutStrLn stderr "usage: moonlight-triangulation-spade-referent (gate DIR | divergence FILE | inventory-csv | inventory-human | lane-specs CLASS | snapshot-specs | bench-one LANE A B | counters BUDGET | sweep-stats COUNT | removal-audit COUNT REMOVALS | removal-context COUNT INDEX | fresh-suffix COUNT DROPPED PATH | interp-audit COUNT QUERIES | interp-context COUNT [INDEX])"
+      exitWith (ExitFailure 2)
+
+refuseLane :: LaneObstruction -> IO value
+refuseLane obstruction = do
+  hPutStrLn stderr (renderLaneObstruction obstruction)
+  exitWith (ExitFailure 2)
+
+-- Everything each lane needs before the clock starts. Handle resolution and
+-- input generation are setup, not work.
+runLane :: LaneRequest -> IO ()
+runLane request@(LaneRequest lane first second) = case lane of
+  -- The input vector is forced here rather than inside 'measure'. A lazy
+  -- 'V.fromList' handed to the clock puts the generator, the list, every boxed
+  -- coordinate, and the vector's own construction inside the lane — none of
+  -- which the Rust side pays, because it builds its @Vec@ before its @Instant@.
+  BulkLoadLane -> do
+    points <- evaluate (force (V.fromList (randomPoints 0x9e3779b97f4a7c15 first)))
+    measure (require (delaunayGeometry points))
+  IncrementalLane -> do
+    points <- evaluate (force (V.fromList (randomPoints 0x9e3779b97f4a7c15 first)))
+    measure (insertAllIncrementally points)
+  -- One immutable snapshot per point. This is deliberately not a cross-
+  -- language lane: spade's referent is timed only on the mutable session
+  -- operation, while this stress lane is paired with the Haskell session
+  -- reference by the driver.
+  SnapshotInsertLane -> do
+    points <- evaluate (force (V.fromList (randomPoints 0x9e3779b97f4a7c15 first)))
+    measure (insertAllPersistently points)
+  -- The angular index both bulk loaders open with, given one angle to work
+  -- with. The input stays in general position, so the load runs the ordinary
+  -- face-building path from the third point on and every insertion locates by
+  -- walking; what degrades is the hull lookup that walk starts from, whose
+  -- buckets all collapse into one. This lane is that degradation and nothing
+  -- else — the face-less path it does NOT reach is "degenerate-line" below.
+  SweepAngleCollapseLane -> do
+    points <- evaluate (force (V.fromList (nearCollinearPoints 0x9e3779b97f4a7c15 first)))
+    measure (buildTriangulation <$> require (delaunay unitElementDefaults points))
+  -- Exactly collinear input, which never builds a face, so both loaders fall
+  -- out of their sweep and degrade to plain incremental insertion for the
+  -- whole load — and neither has a sub-linear answer for locating against a
+  -- face-less mesh. This side scans every vertex and then every half-edge per
+  -- insertion; spade collects and sorts all vertices per insertion. Both are
+  -- quadratic in the load, so the ratio reports which quadratic costs more
+  -- rather than whether one exists, and spade's carries the extra log factor.
+  DegenerateLineLane -> do
+    points <- evaluate (force (V.fromList (exactlyCollinearPoints first)))
+    measure (buildTriangulation <$> require (delaunay unitElementDefaults points))
+  BatchSweepLane -> do
+    points <- evaluate (force (V.fromList (randomPoints 0x9e3779b97f4a7c15 first)))
+    measure (buildTriangulation <$> require (insertMany (empty unitElementDefaults) points))
+  NearestLane -> do
+    triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef first)))
+    hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
+    queries <- requireQueryPoints (V.fromList (randomPoints 0x3141592653589793 second)) >>= evaluate . force
+    measure (evaluate (V.sum (V.map (resolve triangulation hierarchy) queries)))
+  CdtRecoveryLane -> do
+    let points = V.fromList (randomPoints 0x94d049bb133111eb first)
+    base <- fromDelaunay . buildTriangulation <$> require (delaunay unitElementDefaults points)
+    queryPoints <- requireQueryPoints points
+    handles <- evaluate (force (V.map (locateVertex base) queryPoints))
+    pairs <-
+      evaluate
+        ( force
+            (V.fromList [(handles V.! a, handles V.! b) | (a, b) <- constraintPairs first second])
+        )
+    measure (evaluate (recover base pairs))
+  -- The same constraint program, requested one edge at a time through the
+  -- singleton entry point instead of as a batch. The endpoints are handed over
+  -- as points rather than handles because that is what this entry point takes;
+  -- the strict side's counterpart takes points too, so both pay the same two
+  -- locates per request.
+  ConstraintIncrementalLane -> do
+    let points = V.fromList (randomPoints 0x94d049bb133111eb first)
+    base <- fromDelaunay . buildTriangulation <$> require (delaunay unitElementDefaults points)
+    requests <-
+      evaluate
+        ( force
+            [ (points V.! fromIndex, points V.! toIndex)
+            | (fromIndex, toIndex) <- constraintPairs first second
+            ]
+        )
+    measure (evaluate (length (snd (addConstraintsIncrementally base requests))))
+  ConstraintSplitLane -> do
+    (base, handles) <- splitBandCdt first
+    resolved <- evaluate (force handles)
+    measure (evaluate (numVertices (splitConstraints base resolved)))
+  PublicationFloorLane -> do
+    triangulation <- delaunayOf first
+    measure $ do
+      reopened <- require
+        ( V.foldl'
+            reopenTriangulation
+            (Right triangulation)
+            (V.enumFromN (0 :: Int) second)
+        )
+      evaluate (numVertices reopened)
+  -- Removal by coordinate, against the same input prefix on both sides. The
+  -- build is setup on both sides; the measured region is the removals alone.
+  RemovalLane -> do
+    let input = randomPoints 0x9e3779b97f4a7c15 first
+    points <- evaluate (force (V.fromList input))
+    base <- buildTriangulation <$> require (delaunay unitElementDefaults points)
+    removals <- evaluate (force (V.fromList (take second input)))
+    measure (removeAllByCoordinate base removals)
+  -- One immutable snapshot per removal. Like snapshot insertion, this is a
+  -- Haskell-only publication stress lane paired with the session reference;
+  -- it is not a spade timing ratio.
+  SnapshotRemovalLane -> do
+    let input = randomPoints 0x9e3779b97f4a7c15 first
+    points <- evaluate (force (V.fromList input))
+    base <- buildTriangulation <$> require (delaunay unitElementDefaults points)
+    removals <- evaluate (force (V.fromList (take second input)))
+    measure (removeAllPersistently base removals)
+  -- The hierarchy lanes. spade's hint generator maintains itself from inside
+  -- the triangulation, so an insert or a removal there is the whole maintenance
+  -- cost; here the hierarchy is a separate value and the maintenance is a
+  -- separate call, and both are inside the clock.
+  HierarchyIncrementalLane -> do
+    points <- evaluate (force (V.fromList (randomPoints 0x9e3779b97f4a7c15 first)))
+    measure (insertAllWithHierarchy points)
+  HierarchyDuplicateLane -> do
+    base <- hierarchyOf first
+    duplicates <- evaluate (force (V.fromList (randomPoints 0x9e3779b97f4a7c15 second)))
+    measure (reinsertAllWithHierarchy base duplicates)
+  HierarchyRemovalLane -> do
+    let input = randomPoints 0x9e3779b97f4a7c15 first
+    base <- hierarchyOf first
+    removals <- evaluate (force (V.fromList (take second input)))
+    measure (removeAllWithHierarchy base removals)
+  HierarchyRemovalOnlyLane -> do
+    let input = randomPoints 0x9e3779b97f4a7c15 first
+    (triangulation, _) <- hierarchyOf first
+    removals <- evaluate (force (V.fromList (take second input)))
+    measure (removeAllByCoordinate triangulation removals)
+  HierarchyRebuildOnlyLane -> do
+    let input = randomPoints 0x9e3779b97f4a7c15 first
+    (triangulation, hierarchy) <- hierarchyOf first
+    removals <- evaluate (force (V.fromList (take second input)))
+    surviving <- removeAllByCoordinate triangulation removals
+    measure (require (rebuildHierarchyHint hierarchy surviving))
+  -- Sibson weights through the reusable workspace; the strict side holds the
+  -- same buffers behind its NaturalNeighbor handle. The sum forces every
+  -- weight, the way the nearest lane's index sum forces every search.
+  InterpolationLane -> do
+    triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef first)))
+    workspace <- newNaturalNeighborWorkspace triangulation
+    queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 second)) >>= evaluate . force
+    measure (interpolationWeightSum workspace queries)
+  -- The dual. Both sides hold it as a view of the primal mesh rather than as a
+  -- second structure, so the mesh is setup on both and the sweep alone is timed.
+  VoronoiSweepLane -> do
+    triangulation <- delaunayOf first
+    measure (evaluate (voronoiSweep triangulation))
+  DcelWalkLane -> do
+    triangulation <- delaunayOf first
+    measure (evaluate (dcelWalk triangulation))
+  AlphaPersistenceLane ->
+    refuseLane AlphaPersistenceRequiresSpecializedRunner
+  -- Corridor walks between interior endpoints. Both sides locate the start on a
+  -- face and step edge to edge from there.
+  IntersectionLane -> do
+    triangulation <- delaunayOf first
+    chords <- requireQueryChords (intersectionChords second) >>= evaluate . force
+    measure (evaluate (intersectionWalk triangulation chords))
+  -- The same walk from outside the hull. spade steps around the hull from the
+  -- edge its locate returned; this side has no such entry and scans every
+  -- vertex and every edge to find where the line goes in, once per query.
+  IntersectionOutsideLane -> do
+    triangulation <- delaunayOf first
+    chords <- requireQueryChords (outsideChords second) >>= evaluate . force
+    measure (evaluate (intersectionWalk triangulation chords))
+  RefineLane -> do
+    let (points, constraints) = refinementInput
+    cdt <- buildTriangulation <$> require (constrainedDelaunay unitElementDefaults points constraints)
+    let parameters = defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just first
+          , refineMaxArea = Just 0.5
+          , refineMaxRadiusEdgeRatio = Just 1.0
+          , refineExcludeOuterFaces = True
+          , refineKeepConstraintEdges = False
+          }
+    measure (refinementAddedVertices <$> require (refine id parameters (cdt :: ConstrainedDelaunayTriangulation Point)))
+ where
+  reopenTriangulation state _ = do
+    triangulation <- state
+    runST $ do
+      mutable <- thawTriangulation (numVertices triangulation) triangulation
+      freezeTriangulation mutable
+
+  measure :: NFData value => IO value -> IO ()
+  measure = measureLane request
+
+  -- The answer is summed rather than merely tested for presence. A 'Maybe'
+  -- scrutinised only to its outer constructor can be a 'Just' wrapping a thunk,
+  -- and the lane would then time the allocation of a search rather than the
+  -- search; the strict side has no such option and must be matched.
+  resolve triangulation hierarchy query =
+    let hint = case hierarchyHint hierarchy query of
+          Just (VertexHint vertex) -> Just vertex
+          _ -> Nothing
+     in case nearestNeighbor triangulation hint query of
+          Nothing -> 0 :: Word64
+          Just (VertexId vertex, _) -> fromIntegral vertex
+
+  locateVertex cdt point = case locatePoint cdt point of
+    OnVertex vertex -> vertex
+    other -> error ("input point is not a vertex: " <> show other)
+
+  recover triangulation pairs =
+    case recoverConstraints triangulation pairs of
+      Left failure -> error (show failure)
+      Right batch ->
+        V.length
+          ( V.filter
+              (\outcome ->
+                case outcome of
+                  ConstraintAccepted _ _ -> True
+                  ConstraintRejected _ -> False
+              )
+              (constraintBatchOutcomes batch)
+          )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Alpha.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Alpha.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Alpha.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Matched end-to-end alpha-persistence work and its canonical receipt.
+-- Input generation remains outside the timed action; Delaunay construction,
+-- exact alpha births, cellular lowering, one reduction, and all critical-birth
+-- Betti queries remain inside it.
+module Moonlight.Triangulation.Bench.SpadeCompare.Alpha
+  ( AlphaPersistenceObstruction (..)
+  , AlphaPersistenceReceipt (..)
+  , alphaPersistenceReceipt
+  , renderAlphaPersistenceReceipt
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import Data.Bits (xor)
+import Data.List (intercalate)
+import Data.Vector (Vector)
+import Data.Vector.Unboxed qualified as UnboxedVector
+import Data.Word (Word64)
+import GHC.Generics (Generic)
+import Moonlight.Homology.Boundary
+  ( degreeCardinality
+  )
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..)
+  , HomologyFailure
+  , PersistencePair (..)
+  )
+import Moonlight.Homology.Persistence
+  ( criticalBettiTableValues
+  , filteredBaseComplex
+  , filteredCriticalValues
+  , mod2PersistentPairsWithCriticalBettiTable
+  )
+import Moonlight.Triangulation.Alpha
+  ( AlphaFiltrationError
+  , alphaFiltration
+  )
+import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
+import Moonlight.Triangulation.CellComplex
+  ( DCELError
+  , filteredAlphaComplex
+  )
+import Moonlight.Triangulation.Types
+  ( BuildError
+  , Point
+  )
+
+data AlphaPersistenceObstruction
+  = AlphaPersistenceBuildRefused !BuildError
+  | AlphaPersistenceBirthRefused !AlphaFiltrationError
+  | AlphaPersistenceLoweringRefused !DCELError
+  | AlphaPersistenceReductionRefused !HomologyFailure
+  deriving stock (Eq, Show)
+
+data AlphaPersistenceReceipt = AlphaPersistenceReceipt
+  { alphaReceiptCriticalBirths :: !Int
+  , alphaReceiptVertexCells :: !Int
+  , alphaReceiptEdgeCells :: !Int
+  , alphaReceiptFaceCells :: !Int
+  , alphaReceiptFinitePairs :: !Int
+  , alphaReceiptEssentialPairs :: !Int
+  , alphaReceiptProfileChecksum :: !Word64
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+alphaPersistenceReceipt
+  :: Vector Point
+  -> Either AlphaPersistenceObstruction AlphaPersistenceReceipt
+alphaPersistenceReceipt points = do
+  triangulation <-
+    first AlphaPersistenceBuildRefused (delaunayGeometry points)
+  filtration <-
+    first AlphaPersistenceBirthRefused (alphaFiltration triangulation)
+  filtered <-
+    first AlphaPersistenceLoweringRefused (filteredAlphaComplex filtration)
+  (pairs, criticalBettiTable) <-
+    first AlphaPersistenceReductionRefused
+      (mod2PersistentPairsWithCriticalBettiTable filtered)
+  let criticalBirths = filteredCriticalValues filtered
+      finite = filteredBaseComplex filtered
+      isFinitePair = maybe False (const True) . persistenceDeath
+  pure
+    AlphaPersistenceReceipt
+      { alphaReceiptCriticalBirths = length criticalBirths
+      , alphaReceiptVertexCells = degreeCardinality finite (HomologicalDegree 0)
+      , alphaReceiptEdgeCells = degreeCardinality finite (HomologicalDegree 1)
+      , alphaReceiptFaceCells = degreeCardinality finite (HomologicalDegree 2)
+      , alphaReceiptFinitePairs = length (filter isFinitePair pairs)
+      , alphaReceiptEssentialPairs = length (filter (not . isFinitePair) pairs)
+      , alphaReceiptProfileChecksum =
+          UnboxedVector.foldl'
+            mixChecksum
+            fnvOffset
+            (criticalBettiTableValues criticalBettiTable)
+      }
+
+mixChecksum :: Word64 -> Int -> Word64
+mixChecksum accumulator value =
+  (accumulator `xor` fromIntegral value) * fnvPrime
+
+fnvOffset :: Word64
+fnvOffset = 14695981039346656037
+
+fnvPrime :: Word64
+fnvPrime = 1099511628211
+
+renderAlphaPersistenceReceipt :: AlphaPersistenceReceipt -> String
+renderAlphaPersistenceReceipt receipt =
+  intercalate
+    ","
+    ( fmap
+        show
+        [ fromIntegral (alphaReceiptCriticalBirths receipt) :: Word64
+        , fromIntegral (alphaReceiptVertexCells receipt)
+        , fromIntegral (alphaReceiptEdgeCells receipt)
+        , fromIntegral (alphaReceiptFaceCells receipt)
+        , fromIntegral (alphaReceiptFinitePairs receipt)
+        , fromIntegral (alphaReceiptEssentialPairs receipt)
+        , alphaReceiptProfileChecksum receipt
+        ]
+    )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Constraint.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Constraint.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Constraint.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Constraint recovery and splitting lanes and their gates.
+module Moonlight.Triangulation.Bench.SpadeCompare.Constraint where
+
+import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
+import System.FilePath ((</>))
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.Cdt
+import Moonlight.Triangulation.Handles.HandleDefs
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+
+-- | Disjoint split cells, stacked in y: the points, the vertical constraints
+-- the build starts from, and the crossing requests. Band @i@ carries a
+-- constraint from (16, 4i) to (16, 4i + 2) and a crossing request from
+-- (0, 4i + a) to (32, 4i + b), with a and b jittered inside the band, so every
+-- request crosses exactly one constraint and no request reaches another band.
+--
+-- The geometry is chosen so the split point is not merely close on the two
+-- sides but identical. spade solves the two line equations by Cramer's rule and
+-- this side walks the parametric form; here every coefficient either vanishes
+-- or is a power of two, both reduce to one rounding of @u + (v - u) / 2@, and
+-- neither can round it differently. A generic crossing would leave the two
+-- formulas free to disagree in the last bit and there would be nothing to gate.
+splitBands :: Int -> (V.Vector Point, V.Vector (Int, Int), [(Int, Int)])
+splitBands bandCount =
+  ( V.fromList (concatMap bandPoints (zip [0 ..] jitter))
+  , V.fromList [(4 * index, 4 * index + 1) | index <- [0 .. bandCount - 1]]
+  , [(4 * index + 2, 4 * index + 3) | index <- [0 .. bandCount - 1]]
+  )
+ where
+  jitter = randomPoints 0x51ed270b7c1fd1a3 bandCount
+
+  bandPoints (index, Point offsetX offsetY) =
+    let base = 4 * fromIntegral (index :: Int)
+     in [ Point 16 base
+        , Point 16 (base + 2)
+        , Point 0 (base + 1 + 0.5 * offsetX)
+        , Point 32 (base + 1 + 0.5 * offsetY)
+        ]
+
+-- | The same constraint program the batch recovery runs, one request at a time
+-- through the persistent singleton entry point. A request whose corridor is
+-- blocked by an existing constraint is refused and the previous triangulation
+-- stands, which is what the strict side's 'can_add_constraint' guard arranges.
+addConstraintsIncrementally
+  :: ConstrainedDelaunayTriangulation Point
+  -> [(Point, Point)]
+  -> (ConstrainedDelaunayTriangulation Point, [Int])
+addConstraintsIncrementally base requests =
+  case foldl' step (base, []) (zip [0 ..] requests) of
+    (triangulation, accepted) -> (triangulation, reverse accepted)
+ where
+  step (triangulation, accepted) (index, (from, to)) =
+    case addConstraintEdge triangulation from to of
+      Left (ConstraintIntersection _) -> (triangulation, accepted)
+      Left failure -> error (show failure)
+      Right result -> (constraintRecoveryTriangulation result, index : accepted)
+
+-- | Splitting requests, one per band, carried by the batch verb: one
+-- transaction for every corridor, published once -- the shape of the strict
+-- side's fold over one mutable CDT, which publishes nothing between splits.
+-- Every crossed constraint is split at the intersection rather than refused,
+-- so every request is accepted. Splitting appends vertices and never removes
+-- one, so the handles resolved before the first request stay valid through
+-- the last.
+splitConstraints
+  :: ConstrainedDelaunayTriangulation Point
+  -> [(VertexId, VertexId)]
+  -> ConstrainedDelaunayTriangulation Point
+splitConstraints base requests =
+  case addConstraintsAndSplit id base (V.fromList requests) of
+    Left failure -> error (show failure)
+    Right result -> constraintRecoveryTriangulation result
+
+-- | The singleton constraint program's accepted request indices and final
+-- constrained edge set. A blocked request must be refused at the same index on
+-- both sides, or the two are not running the same program.
+writeConstraintIncrementalGate :: FilePath -> Int -> Int -> IO ()
+writeConstraintIncrementalGate directory pointCount constraintCount = do
+  let points = V.fromList (randomPoints 0x94d049bb133111eb pointCount)
+      requests =
+        [ (points V.! fromIndex, points V.! toIndex)
+        | (fromIndex, toIndex) <- constraintPairs pointCount constraintCount
+        ]
+  base <- fromDelaunay . buildTriangulation <$> require (delaunay unitElementDefaults points)
+  let (recovered, accepted) = addConstraintsIncrementally base requests
+      prefix =
+        directory
+          </> ("constraint-incremental-" <> show pointCount <> "-" <> show constraintCount)
+  writeFile (prefix <> "-accepted.txt") (unlines (map show accepted))
+  writeFile (prefix <> "-constraints.txt") (unlines (canonicalConstraintEdges recovered))
+
+-- | Both the split vertices and the constrained edges they carve. The edge set
+-- names every vertex by coordinate, so a split point that landed one ulp away
+-- on one side shows up here rather than hiding behind a matching edge count.
+writeConstraintSplitGate :: FilePath -> Int -> IO ()
+writeConstraintSplitGate directory bandCount = do
+  (base, handles) <- splitBandCdt bandCount
+  let split = splitConstraints base handles
+      prefix = directory </> ("constraint-split-" <> show bandCount)
+  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges split))
+  writeFile (prefix <> "-constraints.txt") (unlines (canonicalConstraintEdges split))
+
+-- | The banded CDT and the crossing requests named by handle. Handle resolution
+-- goes through the build's own input mapping rather than a locate, which is the
+-- constraint gate's convention; the strict side has no such mapping and locates
+-- instead. Either way both name the vertex standing at the same coordinate.
+splitBandCdt
+  :: Int
+  -> IO (ConstrainedDelaunayTriangulation Point, [(VertexId, VertexId)])
+splitBandCdt bandCount = do
+  let (points, constraints, crossings) = splitBands bandCount
+  built <- require (constrainedDelaunay unitElementDefaults points constraints)
+  let mapping = buildInputVertices built
+      len = sizeofPrimArray mapping
+      resolve index
+        | index >= 0 && index < len = VertexId (indexPrimArray mapping index)
+        | otherwise = error ("split band endpoint is out of range: " <> show index)
+  pure
+    ( buildTriangulation built
+    , [(resolve fromIndex, resolve toIndex) | (fromIndex, toIndex) <- crossings]
+    )
+
+writeConstraintGate :: FilePath -> IO ()
+writeConstraintGate directory = do
+  let points = V.fromList (randomPoints 0x94d049bb133111eb 8000)
+      requestIndices = V.fromList (constraintPairs 8000 800)
+  built <- require (delaunay unitElementDefaults points)
+  let mapping = buildInputVertices built
+      len = sizeofPrimArray mapping
+  requests <-
+    V.mapM
+      (\(fromIndex, toIndex) ->
+        let mFrom = if fromIndex >= 0 && fromIndex < len then Just (VertexId (indexPrimArray mapping fromIndex)) else Nothing
+            mTo = if toIndex >= 0 && toIndex < len then Just (VertexId (indexPrimArray mapping toIndex)) else Nothing
+        in case (mFrom, mTo) of
+          (Just from, Just to) -> pure (from, to)
+          _ -> fail "constraint gate endpoint is out of range"
+      )
+      requestIndices
+  batch <-
+    require
+      ( recoverConstraints
+          (fromDelaunay (buildTriangulation built))
+          requests
+      )
+  let accepted =
+        V.ifoldr
+          (\index outcome indices ->
+            case outcome of
+              ConstraintAccepted _ _ -> index : indices
+              ConstraintRejected _ -> indices
+          )
+          []
+          (constraintBatchOutcomes batch)
+  writeFile
+    (directory </> "cdt-accepted-8000-800.txt")
+    (unlines (map show accepted))
+  writeFile
+    (directory </> "cdt-constraints-8000-800.txt")
+    (unlines (canonicalConstraintEdges (constraintBatchTriangulation batch)))
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Dcel.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Dcel.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Dcel.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The primal DCEL traversal gate and the walk lane.
+module Moonlight.Triangulation.Bench.SpadeCompare.Dcel where
+
+import Data.List (sort)
+import Moonlight.Triangulation.Handles.Iterators.FixedIterators (allFaces, directedEdges)
+import Moonlight.Triangulation.Handles.Iterators.HullIterator (hullEdges)
+import System.FilePath ((</>))
+import Moonlight.Triangulation
+import Moonlight.Triangulation.Dcel
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+
+-- | Every iterator over the mesh that spade also offers. The hull cycle and
+-- each vertex's counterclockwise link are cycles with no determined starting
+-- point and are rotated; the inner faces are a set and are sorted. The
+-- enumerator lengths are stated because they are the only cross-comparable
+-- claim the fixed and dynamic iterators make: their elements are handles, and
+-- handles do not cross implementations.
+writeDcelWalkGate :: FilePath -> Int -> IO ()
+writeDcelWalkGate directory count = do
+  triangulation <- delaunayOf count
+  writeFile
+    (directory </> ("dcel-hull-" <> show count <> ".txt"))
+    (unlines (hullGateLines triangulation))
+  writeFile
+    (directory </> ("dcel-circular-" <> show count <> ".txt"))
+    (unlines (circularGateLines triangulation))
+  writeFile
+    (directory </> ("dcel-faces-" <> show count <> ".txt"))
+    (unlines (innerFaceGateLines triangulation))
+
+hullGateLines :: DelaunayTriangulation Point -> [String]
+hullGateLines triangulation =
+  ("hull-size " <> show (length ring)) : rotateToSmallest ring
+ where
+  ring =
+    [ coordHex (vertexPoint triangulation (origin triangulation edge))
+        <> coordHex (vertexPoint triangulation (destination triangulation edge))
+    | edge <- hullEdges triangulation
+    ]
+
+circularGateLines :: DelaunayTriangulation Point -> [String]
+circularGateLines triangulation =
+  [ "vertices " <> show (length (vertices triangulation))
+  , "directed-edges " <> show (length (directedEdges triangulation))
+  , "undirected-edges " <> show (length (undirectedEdges triangulation))
+  , "all-faces " <> show (length (allFaces triangulation))
+  , "inner-faces " <> show (length (innerFaces triangulation))
+  ]
+    <> sort
+      [ coordHex (vertexPoint triangulation vertex) <> concat (rotateToSmallest ring)
+      | vertex <- vertices triangulation
+      , let ring =
+              [ coordHex (vertexPoint triangulation (destination triangulation edge))
+              | edge <- vertexOutgoingEdges triangulation vertex
+              ]
+      ]
+
+innerFaceGateLines :: DelaunayTriangulation Point -> [String]
+innerFaceGateLines triangulation =
+  sort
+    [ concat (sort (map (coordHex . vertexPoint triangulation) [a, b, c]))
+    | face <- innerFaces triangulation
+    , Just (a, b, c) <- [innerFaceVertices triangulation face]
+    ]
+
+-- | Every traversal the DCEL offers, in one pass: the hull cycle, each vertex's
+-- counterclockwise link, and each inner face's vertex triple. The coordinates
+-- are summed so that no step can be satisfied by a thunk.
+dcelWalk :: DelaunayTriangulation Point -> (Double, Int)
+dcelWalk triangulation = overFaces (overVertices (overHull (0, 0)))
+ where
+  overHull accumulator =
+    foldl' (\acc edge -> charge acc (origin triangulation edge)) accumulator (hullEdges triangulation)
+
+  overVertices accumulator = foldl' overLink accumulator (vertices triangulation)
+
+  overLink accumulator vertex =
+    foldl'
+      (\acc edge -> charge acc (destination triangulation edge))
+      accumulator
+      (vertexOutgoingEdges triangulation vertex)
+
+  overFaces accumulator = foldl' overCorners accumulator (innerFaces triangulation)
+
+  overCorners accumulator face =
+    case innerFaceVertices triangulation face of
+      Nothing -> accumulator
+      Just (a, b, c) -> foldl' charge accumulator [a, b, c]
+
+  charge (!total, !count) vertex =
+    let Point x y = vertexPoint triangulation vertex
+     in (total + x + y, count + 1)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs
@@ -0,0 +1,261 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Bulk-load, incremental, nearest, and refinement lanes and their gates.
+module Moonlight.Triangulation.Bench.SpadeCompare.Delaunay where
+
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.FilePath ((</>))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.BulkLoad
+import Moonlight.Triangulation.Dcel
+import Moonlight.Triangulation.HintGenerator
+import Moonlight.Triangulation.Session
+import Moonlight.Triangulation.Types
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+
+writeBulkLoadGate :: FilePath -> Int -> IO ()
+writeBulkLoadGate directory count = do
+  triangulation <-
+    require
+      ( delaunayGeometry
+          (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
+      )
+  writeFile
+    (directory </> ("bulk-load-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+writeIncrementalGate :: FilePath -> Int -> IO ()
+writeIncrementalGate directory count = do
+  triangulation <-
+    insertAllIncrementally
+      (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
+  writeFile
+    (directory </> ("incremental-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+writeNearestGate :: FilePath -> IO ()
+writeNearestGate directory = do
+  let pointCount = 20000
+      queryCount = 5000
+  triangulation <-
+    buildTriangulation
+      <$> require
+        ( delaunay
+            unitElementDefaults
+            (V.fromList (randomPoints 0x0123456789abcdef pointCount))
+        )
+  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
+  queries <- requireQueryPoints (randomPoints 0x3141592653589793 queryCount)
+  writeFile
+    (directory </> "nearest-20000-5000.txt")
+    (unlines (map (nearestAnswer triangulation hierarchy) queries))
+
+writeRefinementGate :: FilePath -> Int -> IO ()
+writeRefinementGate directory steinerBudget = do
+  let (points, constraints) = refinementInput
+      parameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just steinerBudget
+          , refineMaxArea = Just 0.5
+          , refineMaxRadiusEdgeRatio = Just 1.0
+          , refineExcludeOuterFaces = True
+          , refineKeepConstraintEdges = False
+          }
+  cdtBuild <- require (constrainedDelaunay unitElementDefaults points constraints)
+  refined <-
+    require
+      ( refine
+          id
+          parameters
+          (buildTriangulation cdtBuild :: ConstrainedDelaunayTriangulation Point)
+      )
+  let triangulation = refinedTriangulation refined
+      prefix = directory </> ("refine-" <> show steinerBudget)
+      completion = if refinementComplete refined then "1" else "0"
+      summary =
+        [ "added " <> show (refinementAddedVertices refined)
+        , "vertices " <> show (numVertices triangulation)
+        , "edges " <> show (numUndirectedEdges triangulation)
+        , "inner-faces " <> show (numInnerFaces triangulation)
+        , "excluded " <> show (V.length (refinementExcludedFaces refined))
+        , "complete " <> completion
+        ]
+  writeFile (prefix <> "-summary.txt") (unlines summary)
+  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges triangulation))
+  writeFile
+    (prefix <> "-constraints.txt")
+    (unlines (canonicalConstraintEdges triangulation))
+
+-- | Arrival-order insertion through immutable snapshots. The artifact proves
+-- final-state agreement with Spade; it is not a cross-language timing row.
+writePersistentGate :: FilePath -> Int -> IO ()
+writePersistentGate directory count = do
+  triangulation <-
+    insertAllPersistently (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
+  writeFile
+    (directory </> ("persistent-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+writeSweepAngleGate :: FilePath -> Int -> IO ()
+writeSweepAngleGate directory count = do
+  triangulation <-
+    buildTriangulation
+      <$> require
+        ( delaunay
+            unitElementDefaults
+            (V.fromList (nearCollinearPoints 0x9e3779b97f4a7c15 count))
+        )
+  writeFile
+    (directory </> ("sweep-angle-collapse-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+-- | The degenerate chain, pinned by the only thing it has: its edges.
+--
+-- There are no faces to compare and no circumcentres to argue about, so unlike
+-- every other gate here this one has no per-side half and no exclusion. The
+-- answer is @n - 1@ consecutive segments and the two sides either both produce
+-- it or one of them is wrong.
+writeDegenerateLineGate :: FilePath -> Int -> IO ()
+writeDegenerateLineGate directory count = do
+  triangulation <-
+    buildTriangulation
+      <$> require
+        (delaunay unitElementDefaults (V.fromList (exactlyCollinearPoints count)))
+  writeFile
+    (directory </> ("degenerate-line-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+-- Arrival-order insertion with one publication: the transactional comparison
+-- against Spade's @&mut self@ insertion. The per-operation snapshot fold is
+-- measured separately as Moonlight publication stress.
+insertAllIncrementally
+  :: V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+insertAllIncrementally points =
+  evaluate
+    ( case either (error . show) id
+        ( withSession
+            (empty unitElementDefaults)
+            (V.length points)
+            (V.mapM_ insertVertex points)
+        ) of
+        (_, frozen, _) -> frozen
+    )
+
+insertAllPersistently
+  :: V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+insertAllPersistently =
+  V.foldM'
+    (\triangulation point ->
+      insertionTriangulation <$> require (insert triangulation point)
+    )
+    (empty unitElementDefaults)
+
+-- | The refinement lane's own counters, so the worklist question is answered by
+-- measurement rather than by assertion.
+--
+-- @faceChecks@ is not a worklist counter: the terminal audit runs the complete
+-- face evaluation over every final inner face without popping anything. So the
+-- amplification ratio is @pops / steiner@, and @faceChecks - pops@ should equal
+-- the final inner-face count less any pop discarded as excluded.
+-- | The circle sweep's placement split, on the same generator the bulk lanes
+-- use. A point takes the fast path when the hull edge its own angle selected
+-- has the point strictly to its left; otherwise it is deferred to a generic
+-- insertion after the sweep finishes. spade takes the identical branch on
+-- @is_on_right_side_or_on_line@ and its own source calls that fallback "very
+-- slow", so a materially non-zero skip count is wasted work in the bulk lane
+-- rather than a curiosity. @seed + fast + skipped@ must exhaust the unique
+-- points; this reports the identity rather than asserting it silently.
+reportSweepStats :: Int -> IO ()
+reportSweepStats count = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 count)
+  built <- require (delaunay unitElementDefaults points)
+  let stats = buildStats (built :: BuildResult 'Unconstrained Point () () ())
+      seeded = statSpatialSeedPoints stats
+      fast = statSweepFastPoints stats
+      skippedPoints = statSweepSkippedPoints stats
+      unique = statUniquePoints stats
+      accounted = seeded + fast + skippedPoints
+      share :: Int -> String
+      share value
+        | unique == 0 = "n/a"
+        | otherwise = show (100 * fromIntegral value / fromIntegral unique :: Double) <> "%"
+  traverse_
+    putStrLn
+    [ "input-points " <> show (statInputPoints stats)
+    , "unique-points " <> show unique
+    , "duplicate-points " <> show (statDuplicatePoints stats)
+    , "seed-points " <> show seeded
+    , "sweep-fast-points " <> show fast
+    , "sweep-skipped-points " <> show skippedPoints
+    , "skipped-share " <> share skippedPoints
+    , "accounted " <> show accounted
+    , "identity-holds " <> show (accounted == unique)
+    , "edge-flips " <> show (statEdgeFlips stats)
+    , "hull-insertions " <> show (statHullInsertions stats)
+    , "legalization-max-stack " <> show (statLegalizationMaxStack stats)
+    ]
+  if accounted == unique
+    then pure ()
+    else do
+      hPutStrLn
+        stderr
+        ( "sweep-stats: seed + fast + skipped = "
+            <> show accounted
+            <> " but unique = "
+            <> show unique
+        )
+      exitWith (ExitFailure 1)
+
+reportRefinementCounters :: Int -> IO ()
+reportRefinementCounters budget = do
+  let (points, constraints) = refinementInput
+      parameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just budget
+          , refineMaxArea = Just 0.5
+          , refineMaxRadiusEdgeRatio = Just 1.0
+          , refineExcludeOuterFaces = True
+          , refineKeepConstraintEdges = False
+          }
+  cdt <- buildTriangulation <$> require (constrainedDelaunay unitElementDefaults points constraints)
+  result <-
+    require (refine id parameters (cdt :: ConstrainedDelaunayTriangulation Point))
+  let stats = refinementStats result
+      steiner = statSteinerPoints stats
+      pops = statRefinementQueuePops stats
+      checks = statRefinementFaceChecks stats
+      inner = numInnerFaces (refinedTriangulation result)
+      excluded = V.length (refinementExcludedFaces result)
+      ratio :: Int -> Int -> String
+      ratio numerator denominator
+        | denominator == 0 = "n/a"
+        | otherwise =
+            show (fromIntegral numerator / fromIntegral denominator :: Double)
+  traverse_
+    putStrLn
+    [ "budget " <> show budget
+    , "steiner " <> show steiner
+    , "queue-pops " <> show pops
+    , "face-checks " <> show checks
+    , "inner-faces " <> show inner
+    , "excluded " <> show excluded
+    , "edge-flips " <> show (statEdgeFlips stats)
+    , "location-walk-steps " <> show (statLocationWalkSteps stats)
+    , "complete " <> show (refinementComplete result)
+    , "pops/steiner " <> ratio pops steiner
+    , "checks/steiner " <> ratio checks steiner
+    , "flips/steiner " <> ratio (statEdgeFlips stats) steiner
+    , "walk-steps/steiner " <> ratio (statLocationWalkSteps stats) steiner
+    , "checks-minus-pops " <> show (checks - pops)
+    -- Every face check is now a queue pop. The terminal audit used to make the
+    -- difference equal the final inner-face count; if this ever reads False
+    -- again, a second whole-mesh quality pass has come back.
+    , "no-second-pass " <> show (checks == pops)
+    ]
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Gates.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Gates.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Gates.hs
@@ -0,0 +1,277 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The artifact rosters: every gate the binary writes, and the divergence set.
+module Moonlight.Triangulation.Bench.SpadeCompare.Gates where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import System.Directory (createDirectoryIfMissing)
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.BulkLoad
+import Moonlight.Triangulation.Dcel
+import Moonlight.Triangulation.Removal
+import Moonlight.Triangulation.Types
+import Moonlight.Triangulation.Bench.SpadeCompare.Constraint
+import Moonlight.Triangulation.Bench.SpadeCompare.Dcel
+import Moonlight.Triangulation.Bench.SpadeCompare.Delaunay
+import Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy
+import Moonlight.Triangulation.Bench.SpadeCompare.Interpolation
+import Moonlight.Triangulation.Bench.SpadeCompare.Intersection
+import Moonlight.Triangulation.Bench.SpadeCompare.Removal
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Triangulation.Bench.SpadeCompare.Voronoi
+
+-- | The exact observable mesh egress retained by the persistent witnesses.
+--
+-- These artifacts validate immutable predecessor and final-state agreement;
+-- they are not Spade timing ratios. Snapshot publication is measured only on
+-- the Haskell side against its session reference.
+--
+-- The cross-language gate already compares the canonical-edge projection.
+-- This witness additionally retains the geometry-labelled circular DCEL
+-- section, so an isolated or face-less predecessor cannot change unseen.  It
+-- deliberately observes geometric topology rather than internal storage, and
+-- remains a gate-only sink outside every timed lane.
+data PersistentEndpoint = PersistentEndpoint
+  { persistentEndpointVertices :: !Int
+  , persistentEndpointUndirectedEdges :: !Int
+  , persistentEndpointInnerFaces :: !Int
+  , persistentEndpointCanonicalEdges :: ![String]
+  , persistentEndpointCircularDcel :: ![String]
+  }
+  deriving (Eq, Show)
+
+-- | Every retained section is named by its operation and completed edit count,
+-- never by a formatting-only string.
+data PersistentWitnessLabel
+  = PersistentInsertionWitness !Int
+  | PersistentRemovalWitness !Int
+  deriving (Eq, Show)
+
+-- | A preserved immutable section together with its authoritative egress at
+-- capture time.  Insert and removal each supply one concrete use, so this is
+-- the shared gate algebra rather than a second triangulation representation.
+data PersistentSnapshotWitness = PersistentSnapshotWitness
+  { persistentSnapshotLabel :: !PersistentWitnessLabel
+  , persistentSnapshotEndpoint :: !PersistentEndpoint
+  , persistentSnapshotTriangulation :: !(DelaunayTriangulation Point)
+  }
+
+-- | Complete typed refusal surface for the persistence gate.  The public
+-- operations already return 'BuildError'; the witness transports it instead
+-- of manufacturing a stringly failure.  A changed snapshot carries its exact
+-- observed endpoint so the gate failure is inspectable without a unilateral
+-- artifact.
+data PersistentSnapshotObstruction
+  = PersistentInitialBuildRefused !BuildError
+  | PersistentInsertionRefused !Int !BuildError
+  | PersistentRemovalRefused !Int !BuildError
+  | PersistentRemovalTargetMissing !Int
+  | PersistentSnapshotChanged
+      !PersistentWitnessLabel
+      !PersistentEndpoint
+      !PersistentEndpoint
+  deriving Show
+
+persistentEndpoint
+  :: DelaunayTriangulation Point
+  -> IO PersistentEndpoint
+persistentEndpoint triangulation = do
+  edges <- evaluate (force (canonicalEdges triangulation))
+  circular <- evaluate (force (circularGateLines triangulation))
+  pure $!
+    PersistentEndpoint
+      { persistentEndpointVertices = numVertices triangulation
+      , persistentEndpointUndirectedEdges = numUndirectedEdges triangulation
+      , persistentEndpointInnerFaces = numInnerFaces triangulation
+      , persistentEndpointCanonicalEdges = edges
+      , persistentEndpointCircularDcel = circular
+      }
+
+capturePersistentSnapshot
+  :: PersistentWitnessLabel
+  -> DelaunayTriangulation Point
+  -> IO PersistentSnapshotWitness
+capturePersistentSnapshot label triangulation = do
+  endpoint <- persistentEndpoint triangulation
+  pure
+    PersistentSnapshotWitness
+      { persistentSnapshotLabel = label
+      , persistentSnapshotEndpoint = endpoint
+      , persistentSnapshotTriangulation = triangulation
+      }
+
+verifyPersistentSnapshot
+  :: PersistentSnapshotWitness
+  -> IO (Either PersistentSnapshotObstruction ())
+verifyPersistentSnapshot witness = do
+  observed <- persistentEndpoint (persistentSnapshotTriangulation witness)
+  pure
+    ( if observed == persistentSnapshotEndpoint witness
+        then Right ()
+        else
+          Left
+            ( PersistentSnapshotChanged
+                (persistentSnapshotLabel witness)
+                (persistentSnapshotEndpoint witness)
+                observed
+            )
+    )
+
+verifyPersistentSnapshots :: [PersistentSnapshotWitness] -> IO (Either PersistentSnapshotObstruction ())
+verifyPersistentSnapshots = fmap sequence_ . traverse verifyPersistentSnapshot
+
+assertPersistentSnapshot
+  :: Either PersistentSnapshotObstruction value
+  -> IO value
+assertPersistentSnapshot = require
+
+-- | Fixed sparse checkpoints of a longer immutable chain.  Each one has later
+-- edits behind it, so the test distinguishes persistence from merely producing
+-- the correct final mesh.
+persistentCheckpoint :: Int -> Bool
+persistentCheckpoint index = index `elem` [0, 1, 2, 7, 31, 63]
+
+verifyPersistentInsertionSnapshots :: IO ()
+verifyPersistentInsertionSnapshots = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 96)
+      initial = empty unitElementDefaults
+  initialWitness <- capturePersistentSnapshot (PersistentInsertionWitness 0) initial
+  (final, witnesses) <-
+    V.ifoldM'
+      (\(current, preserved) index point -> do
+        nextTriangulation <-
+          assertPersistentSnapshot
+            ( insertionTriangulation
+                <$> first (PersistentInsertionRefused index) (insert current point)
+            )
+        if persistentCheckpoint index
+          then do
+            witness <- capturePersistentSnapshot (PersistentInsertionWitness (index + 1)) nextTriangulation
+            pure (nextTriangulation, preserved <> [witness])
+          else pure (nextTriangulation, preserved)
+      )
+      (initial, [initialWitness])
+      points
+  -- Force the terminal endpoint as an out-of-clock comparable sink before
+  -- checking predecessors; its edge projection is the same projection the
+  -- Rust gate emits for the timed lanes.
+  _ <- persistentEndpoint final
+  assertPersistentSnapshot =<< verifyPersistentSnapshots witnesses
+
+verifyPersistentRemovalSnapshots :: IO ()
+verifyPersistentRemovalSnapshots = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 128)
+      removals = V.take 96 points
+  initial <-
+    buildTriangulation
+      <$> assertPersistentSnapshot
+        (first PersistentInitialBuildRefused (delaunay unitElementDefaults points))
+  initialWitness <- capturePersistentSnapshot (PersistentRemovalWitness 0) initial
+  (final, witnesses) <-
+    V.ifoldM'
+      (\(current, preserved) index point -> do
+        outcome <-
+          assertPersistentSnapshot
+            (first (PersistentRemovalRefused index) (locateAndRemove current point))
+        nextTriangulation <-
+          case outcome of
+            Nothing -> assertPersistentSnapshot (Left (PersistentRemovalTargetMissing index))
+            Just removal -> pure (removalTriangulation removal)
+        if persistentCheckpoint index
+          then do
+            witness <- capturePersistentSnapshot (PersistentRemovalWitness (index + 1)) nextTriangulation
+            pure (nextTriangulation, preserved <> [witness])
+          else pure (nextTriangulation, preserved)
+      )
+      (initial, [initialWitness])
+      removals
+  _ <- persistentEndpoint final
+  assertPersistentSnapshot =<< verifyPersistentSnapshots witnesses
+
+-- | The Haskell side's immutable-contract gate.  It emits no extra artifact:
+-- adding a unilateral file would make the cross-language artifact diff fail.
+-- Its selected predecessor sections are instead verified before the existing
+-- shared endpoint artifacts are written.
+verifyPersistentSnapshotWitnesses :: IO ()
+verifyPersistentSnapshotWitnesses = do
+  verifyPersistentInsertionSnapshots
+  verifyPersistentRemovalSnapshots
+
+-- | Pin the exact output of every timed workload before comparing its cost.
+-- Persistent witness artifacts above are final-state agreement gates, not
+-- cross-language timing rows.
+writeGate :: FilePath -> IO ()
+writeGate directory = do
+  createDirectoryIfMissing True directory
+  verifyPersistentSnapshotWitnesses
+  traverse_ (writeBulkLoadGate directory) [1000, 10000]
+  traverse_ (writeIncrementalGate directory) [1000, 10000]
+  writeNearestGate directory
+  writeConstraintGate directory
+  traverse_ (writeRefinementGate directory) [625, 2500]
+  traverse_ (uncurry (writeRemovalGate directory)) [(1000, 250), (10000, 2500)]
+  traverse_ (uncurry (writeInterpolationGate directory)) [(10000, 1000), (100000, 2000)]
+  writeVoronoiGate directory 1000
+  writeDcelWalkGate directory 2000
+  writeIntersectionGate directory 10000 500
+  writeIntersectionVertexGate directory 2000
+  -- Persistent artifacts validate final-state agreement only; they are not
+  -- Spade timing rows. Their canonical egress is retained for the hard gate.
+  writePersistentGate directory 1000
+  writePersistentRemovalGate directory 10000 2500
+  -- The remaining cliff lanes. Each times an entry point of this side that is
+  -- expected to lose badly, which is worth nothing unless the two are first
+  -- shown to compute the same thing — a quadratic path and a linear one that
+  -- disagree are not a comparison.
+  writeConstraintIncrementalGate directory 8000 800
+  writeConstraintSplitGate directory 1000
+  writeSweepAngleGate directory 2000
+  writeDegenerateLineGate directory 2000
+  writeHierarchyIncrementalGate directory 1000
+  writeHierarchyDuplicateGate directory 10000 500
+  writeHierarchyRemovalGate directory 10000 250
+  writeIntersectionOutsideGate directory 2000 100
+
+-- | The shapes on which the two implementations classify the constrained
+-- domain. They do not agree, so this is a characterization rather than a gate:
+-- the committed baselines lock in the known divergence and fail if either side
+-- moves. See @README.md@ for the compatibility rule.
+divergenceShapes :: [(String, [Point], [(Int, Int)])]
+divergenceShapes =
+  [ ("flush", square 8 <> [Point 4 4], loop4)
+  , ("notched", square 8 <> [Point 13 4, Point 4 4], loop4)
+  , ("flush-plus-dangling-segment", square 8 <> [Point 2 2, Point 6 6], loop4 <> [(4, 5)])
+  , ("notched-plus-dangling-segment", square 8 <> [Point 13 4, Point 2 2, Point 6 6], loop4 <> [(5, 6)])
+  , ( "annulus"
+    , square 12 <> [Point 4 4, Point 8 4, Point 8 8, Point 4 8]
+    , loop4 <> [(4, 5), (5, 6), (6, 7), (7, 4)]
+    )
+  ]
+ where
+  square scale = [Point 0 0, Point scale 0, Point scale scale, Point 0 scale]
+  loop4 = [(0, 1), (1, 2), (2, 3), (3, 0)]
+
+writeDivergence :: FilePath -> IO ()
+writeDivergence path = do
+  rows <- mapM row divergenceShapes
+  writeFile path (unlines rows)
+ where
+  row (label, points, constraints) = do
+    built <- require (constrainedDelaunay unitElementDefaults (V.fromList points) (V.fromList constraints))
+    let cdt = buildTriangulation built
+        parameters =
+          defaultRefinementParameters
+            { refineMaxAdditionalVertices = Just 0
+            , refineExcludeOuterFaces = True
+            }
+    classified <- require (refine id parameters cdt)
+    pure
+      ( label
+          <> ": inner " <> show (numInnerFaces cdt)
+          <> ", excluded " <> show (V.length (refinementExcludedFaces classified))
+      )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Hierarchy.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Hierarchy.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Hierarchy.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Hierarchy-hint lanes, their gates, and the maintained-hint step.
+module Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy where
+
+import System.FilePath ((</>))
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.BulkLoad
+import Moonlight.Triangulation.HintGenerator
+import Moonlight.Triangulation.Session
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+
+-- | The step both insertion lanes run: one insert, one hierarchy update, and
+-- nothing between them. It is what spade's @insert@ does internally through
+-- @notify_vertex_inserted@, and both sides carry branch factor 16.
+--
+-- The point handed to 'updateHierarchyAfterInsertion' is the same binding
+-- handed to 'insertVertexAt', never one read back out of the harness's input
+-- vector. The mesh stores 'canonicalPoint' of what it was given, so a
+-- harness-side copy would part company from it on a signed zero, seed a
+-- different nearest neighbour, and move the gate against spade. Same function,
+-- same argument, same bits.
+hierarchyStep
+  :: HierarchyHint
+  -> Point
+  -> Session s Point () () () HierarchyHint
+hierarchyStep hierarchy point = do
+  (vertex, disposition) <- insertVertexAt point point
+  either refuse pure (updateHierarchyAfterInsertion hierarchy point vertex disposition)
+
+-- | Arrival-order insertion with the hierarchy maintained alongside it.
+--
+-- One session spans the whole run. The hierarchy no longer asks for a
+-- triangulation, so the per-point publication that existed only to hand it one
+-- is gone: the mesh is thawed once and published once for the program, where
+-- before it was once per point. The measured operation is unchanged — still an
+-- insert per point with the hint maintained against it, which is spade's loop.
+insertAllWithHierarchy
+  :: V.Vector Point
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+insertAllWithHierarchy points = do
+  let base = empty unitElementDefaults
+  initial <- require (buildHierarchyHint defaultHierarchyBranchFactor base)
+  (hierarchy, mesh, _) <-
+    require (withSession base (V.length points) (V.foldM' hierarchyStep initial points))
+  pure (mesh, hierarchy)
+
+-- | The same step over a mesh that already holds every point, which is the
+-- duplicate lane. Each insert locates a site that is already there and answers
+-- 'AlreadyPresent', so the hierarchy comes back unexamined and the level walk
+-- is never entered. What remains on the clock is the locate and the
+-- disposition test, with no rebuild standing behind either.
+reinsertAllWithHierarchy
+  :: (DelaunayTriangulation Point, HierarchyHint)
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+reinsertAllWithHierarchy (triangulation, hierarchy) points =
+  case withSession triangulation (V.length points) (V.foldM' hierarchyStep hierarchy points) of
+    Left refusal -> fail (show refusal)
+    Right (updated, mesh, _) -> pure (mesh, updated)
+
+-- | Remove a program of coordinates through the hierarchy: every locate
+-- starts at the hierarchy's nearest sample, one base session publishes once,
+-- and the hierarchy of the resulting value is published once.
+--
+-- No query observes an intermediate hierarchy in this lane. A triangulation
+-- and its hierarchy denote their current values rather than the edit history,
+-- so the lawful physical schedule is one base session followed by one
+-- hierarchy publication — the shape of the strict side's
+-- @HierarchyTriangulation@, whose removals also locate through the hierarchy.
+removeAllWithHierarchy
+  :: (DelaunayTriangulation Point, HierarchyHint)
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+removeAllWithHierarchy (triangulation, hierarchy) points =
+  case removeManyWithHierarchy hierarchy triangulation points of
+    Left refusal -> fail (show refusal)
+    Right (outcomes, surviving, repaired) -> do
+      V.imapM_ checkRemoved outcomes
+      pure (surviving, repaired)
+ where
+  checkRemoved index outcome = case outcome of
+    Nothing ->
+      error
+        ( "removal target is not a vertex: index="
+            <> show index
+            <> " point="
+            <> ( case points V.! index of
+                  Point x y -> hex64 x <> "," <> hex64 y
+               )
+        )
+    Just _ -> pure ()
+
+-- | The hierarchy lanes' gate, in two parts. The base mesh alone would not see
+-- a hierarchy whose nesting law has drifted, because a search started from a
+-- bad hint still walks to the right answer; a hint naming a vertex that does
+-- not exist, or one the search cannot walk out of, moves the answers. Both are
+-- pinned.
+hierarchyGateQueries :: Int
+hierarchyGateQueries = 1000
+
+writeHierarchyGate
+  :: FilePath
+  -> String
+  -> (DelaunayTriangulation Point, HierarchyHint)
+  -> IO ()
+writeHierarchyGate directory label (triangulation, hierarchy) = do
+  queries <- requireQueryPoints (randomPoints 0x3141592653589793 hierarchyGateQueries)
+  writeFile
+    (directory </> (label <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+  writeFile
+    (directory </> (label <> "-nearest.txt"))
+    ( unlines
+        ( map
+            (nearestAnswer triangulation hierarchy)
+            queries
+        )
+    )
+
+writeHierarchyIncrementalGate :: FilePath -> Int -> IO ()
+writeHierarchyIncrementalGate directory pointCount = do
+  built <- insertAllWithHierarchy (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount))
+  writeHierarchyGate directory ("hierarchy-incremental-" <> show pointCount) built
+
+writeHierarchyDuplicateGate :: FilePath -> Int -> Int -> IO ()
+writeHierarchyDuplicateGate directory pointCount duplicateCount = do
+  base <- hierarchyOf pointCount
+  built <-
+    reinsertAllWithHierarchy
+      base
+      (V.fromList (randomPoints 0x9e3779b97f4a7c15 duplicateCount))
+  writeHierarchyGate
+    directory
+    ("hierarchy-duplicate-" <> show pointCount <> "-" <> show duplicateCount)
+    built
+
+writeHierarchyRemovalGate :: FilePath -> Int -> Int -> IO ()
+writeHierarchyRemovalGate directory pointCount removalCount = do
+  base <- hierarchyOf pointCount
+  built <-
+    removeAllWithHierarchy
+      base
+      (V.fromList (take removalCount (randomPoints 0x9e3779b97f4a7c15 pointCount)))
+  writeHierarchyGate
+    directory
+    ("hierarchy-removal-" <> show pointCount <> "-" <> show removalCount)
+    built
+
+-- | A bulk-loaded mesh and the hierarchy over it, which is where the duplicate
+-- and removal lanes start. The strict side reaches the same pair through
+-- @HierarchyTriangulation::bulk_load@.
+hierarchyOf
+  :: Int
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+hierarchyOf count = do
+  triangulation <- delaunayOf count
+  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
+  pure (triangulation, hierarchy)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Input.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Input.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Input.hs
@@ -0,0 +1,20 @@
+-- | Cross-language benchmark input programs. Both Haskell interpreters import
+-- this owner directly; neither keeps a private copy of the LCG stream.
+module Moonlight.Triangulation.Bench.SpadeCompare.Input
+  ( randomPoints
+  ) where
+
+import Data.Word (Word64)
+import Moonlight.Triangulation.Types (Point (..))
+
+-- | The Rust generator, bit for bit.
+randomPoints :: Word64 -> Int -> [Point]
+randomPoints seed count = take count (go seed)
+ where
+  go :: Word64 -> [Point]
+  go state =
+    let state1 = state * 6364136223846793005 + 1442695040888963407
+        state2 = state1 * 6364136223846793005 + 1442695040888963407
+        unit :: Word64 -> Double
+        unit value = fromIntegral (value `div` 2048) / 9007199254740992
+     in Point (2 * unit state1 - 1) (2 * unit state2 - 1) : go state2
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Interpolation.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Interpolation.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Interpolation.hs
@@ -0,0 +1,418 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Sibson interpolation lanes, gates, and diagnostics.
+module Moonlight.Triangulation.Bench.SpadeCompare.Interpolation where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad.ST (stToIO)
+import GHC.Exts (RealWorld)
+import Data.Foldable (traverse_)
+import Data.List (sort, sortOn)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.FilePath ((</>))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.Dcel
+import Moonlight.Triangulation.Interpolation
+import Moonlight.Triangulation.Math
+import Moonlight.Triangulation.PointLocation
+import Moonlight.Triangulation.Types
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+
+-- | The Sibson query reports its own cavity size, so the distribution of
+-- (cavity faces, neighbours returned) says which step degenerates: a cavity of
+-- one face means 'discoverCavity' never expanded, while a larger cavity paired
+-- with three neighbours means the cell or the normalization was rejected.
+reportInterpolationAudit :: Int -> Int -> IO ()
+reportInterpolationAudit pointCount queryCount = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
+  queries <- requireQueryPoints (randomPoints 0x2718281828459045 queryCount)
+  pairs <-
+    traverse
+      ( \query -> do
+          (count, _, stats) <-
+            stToIO (foldNaturalNeighborWeights (\total _ _ -> total + (1 :: Int)) 0 workspace Nothing query)
+          pure ((interpolationCavityFaces stats, count), interpolationUsedFallback stats)
+      )
+      queries
+  let tally =
+        [ (representative, 1 + length rest)
+        | representative : rest <- groupSorted (sort (map fst pairs))
+        ]
+      fallbacks = length (filter snd pairs)
+  putStrLn ("queries " <> show (length pairs))
+  putStrLn ("fallbacks " <> show fallbacks)
+  traverse_
+    (\((cavity, neighbors), n) -> putStrLn ("cavityFaces=" <> show cavity <> " neighbors=" <> show neighbors <> " count=" <> show n))
+    tally
+ where
+  groupSorted [] = []
+  groupSorted (x : xs) = let (same, rest) = span (== x) xs in (x : same) : groupSorted rest
+
+-- | Isolates the cavity flood of the Sibson pipeline: per query, the BFS
+-- over 'inCircle' and nothing else. The margin tally counts tests whose
+-- determinant is inside the filtered predicate's error bound — the exact
+-- dyadic path, which allocates Integer arithmetic per call.
+reportInterpolationCavityAudit :: Int -> Int -> IO ()
+reportInterpolationCavityAudit pointCount queryCount = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
+  (faces, tests, margins) <-
+    V.foldM'
+      ( \(faceAcc, testAcc, marginAcc) query ->
+          case locatePoint triangulation query of
+            InFace start -> do
+              let (!cavitySize, !testCount, !marginCount) = flood triangulation (queryPointValue query) start
+              pure (faceAcc + cavitySize, testAcc + testCount, marginAcc + marginCount)
+            _ -> pure (faceAcc, testAcc, marginAcc)
+      )
+      (0 :: Int, 0 :: Int, 0 :: Int)
+      queries
+  putStrLn ("queries " <> show queryCount)
+  putStrLn ("cavity-faces " <> show faces)
+  putStrLn ("incircle-tests " <> show tests)
+  putStrLn ("exact-path-margins " <> show margins)
+ where
+  flood triangulation query start = go [start] [] 0 0
+   where
+    go [] seen tests margins = (length seen, tests, margins)
+    go (face : rest) seen tests margins
+      | face `elem` seen = go rest seen tests margins
+      | otherwise =
+          case innerFaceVertices triangulation face of
+            Nothing -> go rest seen tests margins
+            Just (a, b, c) ->
+              let !pa = vertexPoint triangulation a
+                  !pb = vertexPoint triangulation b
+                  !pc = vertexPoint triangulation c
+                  !verdict = inCircle pa pb pc query
+                  !margins' = if insideMargin pa pb pc query then margins + 1 else margins
+               in if verdict == GT
+                    then go (adjacent triangulation face ++ rest) (face : seen) (tests + 1) margins'
+                    else go rest seen (tests + 1) margins'
+
+  adjacent triangulation face =
+    case innerFaceDirectedEdges triangulation face of
+      Nothing -> []
+      Just (e0, e1, e2) ->
+        [ opposite
+        | edge <- [e0, e1, e2]
+        , let opposite = incidentFace triangulation (reverseEdge edge)
+        , opposite /= outerFace
+        ]
+
+  -- The branch condition of the filtered Double inCircle, recomputed over
+  -- public coordinates: inside the error bound means the exact dyadic path.
+  insideMargin (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
+    abs determinant <= 1.1102230246251577e-15 * permanent
+   where
+    !adx = ax - dx
+    !ady = ay - dy
+    !bdx = bx - dx
+    !bdy = by - dy
+    !cdx = cx - dx
+    !cdy = cy - dy
+    !abdet = adx * bdy - bdx * ady
+    !bcdet = bdx * cdy - cdx * bdy
+    !cadet = cdx * ady - adx * cdy
+    !alift = adx * adx + ady * ady
+    !blift = bdx * bdx + bdy * bdy
+    !clift = cdx * cdx + cdy * cdy
+    !determinant = alift * bcdet + blift * cadet + clift * abdet
+    !permanent =
+      (abs (bdx * cdy) + abs (cdx * bdy)) * alift
+        + (abs (cdx * ady) + abs (adx * cdy)) * blift
+        + (abs (adx * bdy) + abs (bdx * ady)) * clift
+
+-- | Isolates the locate half of the interpolation lane: same triangulation,
+-- same queries, but only 'locatePointWithHint' runs — no cavity, no Sibson.
+-- With the allocation of this in hand against the full lane's, the owner of
+-- the per-query cost is named by subtraction rather than by guesswork.
+reportInterpolationLocateAudit :: Int -> Int -> IO ()
+reportInterpolationLocateAudit pointCount queryCount = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
+  (steps, fallbacks) <-
+    V.foldM'
+      ( \(stepAcc, fallbackAcc) query -> do
+          let (!location, !stats) = locatePointWithHint triangulation Nothing query
+          _ <- evaluate (force (show location))
+          pure (stepAcc + locationWalkSteps stats, fallbackAcc + (if locationUsedFallback stats then 1 else 0))
+      )
+      (0 :: Int, 0 :: Int)
+      queries
+  putStrLn ("queries " <> show queryCount)
+  putStrLn ("walk-steps " <> show steps)
+  putStrLn ("fallbacks " <> show fallbacks)
+
+-- | The Sibson pipeline re-implemented over public API only, stage by stage,
+-- mirroring the library's 'sibsonQuery': cavity flood, boundary collection,
+-- loop ordering, insertion cell, stolen-area walks, normalization. The stage
+-- sets are order-independent, so where the replica succeeds while the library
+-- falls back the workspace layer is convicted, and where it fails the stage's
+-- own algorithm is. With no index it tallies the first 1000 gate queries by
+-- failing stage; with an index it dumps one query's full geometry.
+reportInterpolationContext :: Int -> Maybe Int -> IO ()
+reportInterpolationContext pointCount maybeIndex = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 1000))
+  case maybeIndex of
+    Just index -> case queries V.!? index of
+      Nothing -> do
+        hPutStrLn stderr "interp-context: query index out of range"
+        exitWith (ExitFailure 2)
+      Just query -> do
+        let (_, detail) = replicateQuery triangulation query
+        putStrLn ("query " <> pointHex (queryPointValue query))
+        traverse_ putStrLn detail
+    Nothing -> do
+      let reports = map (fst . replicateQuery triangulation) (V.toList queries)
+          tally =
+            [ (representative, 1 + length rest)
+            | representative : rest <- groupByHead (sort reports)
+            ]
+      putStrLn ("queries " <> show (length reports))
+      traverse_ (\(stage, count) -> putStrLn (stage <> " count=" <> show count)) tally
+ where
+  groupByHead [] = []
+  groupByHead (x : xs) = let (same, rest) = span (== x) xs in (x : same) : groupByHead rest
+
+  replicateQuery triangulation queryPoint =
+    let query = queryPointValue queryPoint
+     in case locatePoint triangulation queryPoint of
+      OnVertex _ -> ("special-on-vertex", ["on-vertex"])
+      OnEdge edge
+        | isBoundaryEdge triangulation (asUndirected edge) -> ("special-boundary-edge", ["boundary-edge"])
+        | otherwise ->
+            let !left = incidentFace triangulation edge
+                !right = incidentFace triangulation (reverseEdge edge)
+                !start = if left /= outerFace then left else right
+             in sibson triangulation query start
+      InFace face -> sibson triangulation query face
+      OutsideConvexHull _ -> ("special-outside-hull", ["outside-hull"])
+      EmptyTriangulation -> ("special-empty", ["empty"])
+
+  sibson triangulation query startFace =
+    let cavity = flood [startFace] []
+        boundary =
+          [ edge
+          | face <- cavity
+          , Just (e0, e1, e2) <- [innerFaceDirectedEdges triangulation face]
+          , edge <- [e0, e1, e2]
+          , let opposite = incidentFace triangulation (reverseEdge edge)
+          , opposite == outerFace || not (opposite `elem` cavity)
+          ]
+        header =
+          [ "cavity " <> show (length cavity)
+          , "boundary " <> show (length boundary)
+          ]
+     in case orderLoop boundary of
+          Left reason -> ("fail-order", header ++ ["order-failure " <> reason])
+          Right ordered
+            | length ordered < 3 -> ("fail-order-small", header ++ ["ordered " <> show (length ordered)])
+            | otherwise ->
+                case insertionCell ordered of
+                  Left reason -> ("fail-cell", header ++ ["ordered " <> show (length ordered), "cell-failure " <> reason])
+                  Right cell ->
+                    let walks = stolenAreas ordered cell
+                     in case sequence walks of
+                          Left reason -> ("fail-stolen", header ++ ["ordered " <> show (length ordered), "stolen-failure " <> reason])
+                          Right areas ->
+                            let !total = sum areas
+                                !minimumWeight = minimum areas
+                                !tolerance = max 1.0e-12 (128 * 2.220446049250313e-16)
+                                !clampedTotal = sum (map (max 0) areas)
+                                verdict
+                                  | any (not . isFinite) areas || total == 0 || minimumWeight < negate tolerance =
+                                      Left ("normalize total=" <> show total <> " min=" <> show minimumWeight)
+                                  | clampedTotal <= 0 || not (isFinite clampedTotal) =
+                                      Left ("clamped-total " <> show clampedTotal)
+                                  | otherwise = Right ()
+                             in case verdict of
+                                  Left reason ->
+                                    ( "fail-normalize"
+                                    , header
+                                        ++ ["ordered " <> show (length ordered)]
+                                        ++ ["area " <> show area | area <- areas]
+                                        ++ ["normalize-failure " <> reason]
+                                    )
+                                  Right () ->
+                                    ( "sibson-success"
+                                    , header
+                                        ++ ["ordered " <> show (length ordered)]
+                                        ++ zipWith areaLine ordered areas
+                                        ++ ["total " <> show total <> " min " <> show minimumWeight]
+                                    )
+   where
+    areaLine edge area =
+      "  vertex "
+        <> pointHex (vertexPoint triangulation (origin triangulation edge))
+        <> " area "
+        <> show area
+
+    contains face =
+      case innerFaceVertices triangulation face of
+        Nothing -> False
+        Just (a, b, c) ->
+          inCircle
+            (vertexPoint triangulation a)
+            (vertexPoint triangulation b)
+            (vertexPoint triangulation c)
+            query
+            /= LT
+
+    adjacent face =
+      case innerFaceDirectedEdges triangulation face of
+        Nothing -> []
+        Just (e0, e1, e2) ->
+          [ opposite
+          | edge <- [e0, e1, e2]
+          , let opposite = incidentFace triangulation (reverseEdge edge)
+          , opposite /= outerFace
+          ]
+
+    flood [] seen = seen
+    flood (face : rest) seen
+      | face `elem` seen = flood rest seen
+      | not (contains face) = flood rest seen
+      | otherwise = flood (adjacent face ++ rest) (face : seen)
+
+    orderLoop [] = Left "empty-boundary"
+    orderLoop boundary@(first : _) = chain first first [] 0
+     where
+      !count = length boundary
+      origins = [(origin triangulation edge, edge) | edge <- boundary]
+      chain start current acc index
+        | index >= count =
+            if current == start then Right (reverse acc) else Left "no-closure"
+        | otherwise =
+            case lookup (destination triangulation current) origins of
+              Nothing ->
+                Left
+                  ( "broken-chain "
+                      <> pointHex (vertexPoint triangulation (destination triangulation current))
+                  )
+              Just nextEdge
+                | nextEdge == start && index + 1 == count -> Right (reverse (current : acc))
+                | nextEdge == start -> Left "premature-closure"
+                | otherwise -> chain start nextEdge (current : acc) (index + 1)
+
+    relative point = case point of Point x y -> Point (x - qx) (y - qy)
+    (qx, qy) = case query of Point x y -> (x, y)
+
+    insertionCell ordered = traverse cellCenter ordered
+     where
+      cellCenter edge =
+        let !from = vertexPoint triangulation (origin triangulation edge)
+            !to = vertexPoint triangulation (destination triangulation edge)
+         in case circumcenter (relative to) (relative from) (Point 0 0) of
+              Nothing ->
+                Left
+                  ( "edge "
+                      <> pointHex from
+                      <> " "
+                      <> pointHex to
+                  )
+              Just center -> Right center
+
+    stolenAreas ordered cell =
+      let !count = length ordered
+          pairs = zip ordered cell
+          predecessors = drop (count - 1) pairs ++ take (count - 1) pairs
+       in zipWith walk pairs predecessors
+     where
+      walk (stopEdge, first) (initialEdge, initialPoint) = go initialEdge initialPoint 0 initialPositive initialNegative
+       where
+        !target = reverseEdge stopEdge
+        !limit = numDirectedEdges triangulation + 1
+        !initialPositive = px first * py initialPoint
+        !initialNegative = py first * px initialPoint
+        !vertex = vertexPoint triangulation (origin triangulation stopEdge)
+        go lastEdge lastPoint steps positive negative
+          | steps >= limit = Left "step-limit"
+          | incidentFace triangulation lastEdge == outerFace =
+              Left ("outer-face vertex=" <> pointHex vertex <> " steps=" <> show steps)
+          | otherwise =
+              case innerFaceVertices triangulation (incidentFace triangulation lastEdge) of
+                Nothing -> Left "no-face-vertices"
+                Just (a, b, c) ->
+                  case circumcenter
+                    (relative (vertexPoint triangulation a))
+                    (relative (vertexPoint triangulation b))
+                    (relative (vertexPoint triangulation c)) of
+                    Nothing ->
+                      Left ("circumcenter vertex=" <> pointHex vertex <> " steps=" <> show steps)
+                    Just current ->
+                      let !positive' = positive + px lastPoint * py current
+                          !negative' = negative + py lastPoint * px current
+                          !nextEdge = reverseEdge (next triangulation lastEdge)
+                       in if nextEdge == target
+                            then Right ((negative' + py current * px first) - (positive' + px current * py first))
+                            else go nextEdge current (steps + 1) positive' negative'
+      px (Point x _) = x
+      py (Point _ y) = y
+
+-- | The interpolation gate, two artifacts per query. The neighbour set is
+-- exact-predicate determined over triangulations that already agree
+-- bit-for-bit, so it crosses implementations. The weights cannot: each
+-- neighbour's stolen area uses the same operation sequence on both sides,
+-- but the normalization total accumulates those areas in each side's own
+-- neighbour order, and floating-point addition is not associative. The
+-- weights are pinned per side in the driver instead.
+writeInterpolationGate :: FilePath -> Int -> Int -> IO ()
+writeInterpolationGate directory pointCount queryCount = do
+  triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
+  (neighborLines, weightLines) <-
+    V.foldM'
+      ( \(neighborAcc, weightAcc) query -> do
+          (entries, _, _) <- stToIO (foldNaturalNeighborWeights (\acc vertex weight -> (vertex, weight) : acc) [] workspace Nothing query)
+          let sortedEntries =
+                sortOn
+                  (\(x, y, _) -> (x, y))
+                  [ case vertexPoint triangulation vertex of
+                      Point x y -> (x, y, weight)
+                  | (vertex, weight) <- entries
+                  ]
+              queryHex = case queryPointValue query of Point x y -> hex64 x <> hex64 y
+          pure
+            ( queryHex <> concatMap (\(x, y, _) -> hex64 x <> hex64 y) sortedEntries : neighborAcc
+            , queryHex <> concatMap (\(x, y, weight) -> hex64 x <> hex64 y <> hex64 weight) sortedEntries : weightAcc
+            )
+      )
+      ([], [])
+      queries
+  writeFile
+    (directory </> ("interpolation-" <> show pointCount <> "-" <> show queryCount <> "-neighbors.txt"))
+    (unlines (reverse neighborLines))
+  writeFile
+    (directory </> ("interpolation-" <> show pointCount <> "-" <> show queryCount <> "-weights.txt"))
+    (unlines (reverse weightLines))
+
+-- | The sum of one query's Sibson weights through the reusable workspace —
+-- the allocation-free path, matching what the strict side's buffer reuse
+-- does inside its measured region.
+interpolationWeightSum
+  :: NaturalNeighborWorkspace RealWorld 'Unconstrained Point () () ()
+  -> V.Vector QueryPoint
+  -> IO Double
+interpolationWeightSum workspace =
+  V.foldM'
+    ( \total query -> do
+        (weightSum, _, _) <- stToIO (foldNaturalNeighborWeights (\acc _ weight -> acc + weight) 0 workspace Nothing query)
+        pure (total + weightSum)
+    )
+    0
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Intersection.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Intersection.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Intersection.hs
@@ -0,0 +1,140 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Corridor-walk lanes, their inputs, and their gates.
+module Moonlight.Triangulation.Bench.SpadeCompare.Intersection where
+
+import Data.List (sortOn)
+import Data.Word (Word64)
+import System.FilePath ((</>))
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.IntersectionIterator
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+
+-- | Query endpoints pulled inward. The generator fills [-1, 1]^2, so a point
+-- with both coordinates inside [-0.5, 0.5] is strictly inside the hull of a
+-- few thousand of them and locating it lands on a face. A chord that starts
+-- OUTSIDE the hull takes a different route on each side — spade walks the hull
+-- from the located hull edge while this side falls back to a scan over every
+-- element — so those chords are the "intersection-outside" cliff lane and are
+-- kept out of this one deliberately.
+interiorQueryPoints :: Word64 -> Int -> [Point]
+interiorQueryPoints seed count =
+  [Point (0.5 * x) (0.5 * y) | Point x y <- randomPoints seed count]
+
+-- | Query origins pushed well clear of the hull, so locating one is certain to
+-- report the outside of the convex hull rather than a face.
+exteriorQueryPoints :: Word64 -> Int -> [Point]
+exteriorQueryPoints seed count =
+  [Point (4 * x) (4 * y) | Point x y <- randomPoints seed count]
+
+intersectionChords :: Int -> [(Point, Point)]
+intersectionChords queryCount =
+  zip
+    (interiorQueryPoints 0x3141592653589793 queryCount)
+    (interiorQueryPoints 0x2718281828459045 queryCount)
+
+outsideChords :: Int -> [(Point, Point)]
+outsideChords queryCount =
+  zip
+    (exteriorQueryPoints 0x243f6a8885a308d3 queryCount)
+    (interiorQueryPoints 0x2718281828459045 queryCount)
+
+-- | One line per query: the line's own endpoints, then its intersection
+-- sequence in the order the walk reports it. Every branch of the walk turns on
+-- an orientation predicate over a mesh that already agrees bit for bit, so the
+-- whole sequence is exactly determined and crosses implementations — order
+-- included, which is why nothing here is sorted.
+writeIntersectionGate :: FilePath -> Int -> Int -> IO ()
+writeIntersectionGate directory pointCount queryCount = do
+  triangulation <- delaunayOf pointCount
+  chords <- requireQueryChords (intersectionChords queryCount)
+  writeFile
+    (directory </> ("intersection-" <> show pointCount <> "-" <> show queryCount <> "-chords.txt"))
+    (unlines (map (intersectionLine triangulation) chords))
+
+writeIntersectionOutsideGate :: FilePath -> Int -> Int -> IO ()
+writeIntersectionOutsideGate directory pointCount queryCount = do
+  triangulation <- delaunayOf pointCount
+  chords <- requireQueryChords (outsideChords queryCount)
+  writeFile
+    ( directory
+        </> ("intersection-outside-" <> show pointCount <> "-" <> show queryCount <> ".txt")
+    )
+    (unlines (map (intersectionLine triangulation) chords))
+
+-- | Lines whose endpoints are mesh vertices, which is the only way to reach the
+-- vertex and overlap branches of the walk at all: over generated doubles no
+-- chord lands exactly on a site, so a gate built from chords alone exercises
+-- one branch of three. The first family joins vertices the constraint
+-- generator's index pairs select, which starts and ends the walk on a vertex;
+-- the second runs along an existing edge, which is the only shape that produces
+-- an overlap. Both name their endpoints by coordinate, the second from a list
+-- both sides sort by the same encoding, because vertex handles do not cross.
+writeIntersectionVertexGate :: FilePath -> Int -> IO ()
+writeIntersectionVertexGate directory pointCount = do
+  triangulation <- delaunayOf pointCount
+  let sites = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+      joins =
+        [ (sites V.! fromIndex, sites V.! toIndex)
+        | (fromIndex, toIndex) <- constraintPairs pointCount intersectionVertexLines
+        ]
+      alongEdges = take intersectionVertexLines (sortedEdgeEndpoints triangulation)
+  queryLines <- requireQueryChords (joins <> alongEdges)
+  writeFile
+    (directory </> ("intersection-vertexlines-" <> show pointCount <> ".txt"))
+    (unlines (map (intersectionLine triangulation) queryLines))
+
+intersectionVertexLines :: Int
+intersectionVertexLines = 100
+
+sortedEdgeEndpoints
+  :: DelaunayTriangulation Point
+  -> [(Point, Point)]
+sortedEdgeEndpoints triangulation =
+  map snd
+    ( sortOn
+        fst
+        [ (coordHex first <> coordHex second, (first, second))
+        | edge <- undirectedEdges triangulation
+        , let (from, to) = undirectedEndpoints triangulation edge
+              p = vertexPoint triangulation from
+              q = vertexPoint triangulation to
+              (first, second) = if pointKey p <= pointKey q then (p, q) else (q, p)
+        ]
+    )
+ where
+  pointKey (Point x y) = (x, y)
+
+intersectionLine
+  :: DelaunayTriangulation Point
+  -> (QueryPoint, QueryPoint)
+  -> String
+intersectionLine triangulation (from, to) =
+  coordHex (queryPointValue from)
+    <> coordHex (queryPointValue to)
+    <> concatMap (encodeIntersection triangulation) (lineIntersections triangulation from to)
+
+encodeIntersection
+  :: DelaunayTriangulation Point
+  -> Intersection
+  -> String
+encodeIntersection triangulation event = case event of
+  VertexIntersection vertex -> "V" <> coordHex (vertexPoint triangulation vertex)
+  EdgeIntersection edge -> "E" <> directedHex edge
+  EdgeOverlap edge -> "O" <> directedHex edge
+ where
+  directedHex edge =
+    coordHex (vertexPoint triangulation (origin triangulation edge))
+      <> coordHex (vertexPoint triangulation (destination triangulation edge))
+
+-- | The corridor walk, counted. 'lineIntersections' materializes its answer, so
+-- forcing the spine forces every step that produced it; spade's iterator is
+-- lazy and is consumed by the same count.
+intersectionWalk
+  :: DelaunayTriangulation Point
+  -> [(QueryPoint, QueryPoint)]
+  -> Int
+intersectionWalk triangulation =
+  foldl' (\total (from, to) -> total + length (lineIntersections triangulation from to)) 0
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Lane.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Lane.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Lane.hs
@@ -0,0 +1,302 @@
+-- | The closed comparison vocabulary and its boundary projections.
+--
+-- The driver executes these requests, the report orders and names them, and
+-- the Haskell runner interprets their command constructors. None of those
+-- boundaries authors another lane universe.
+module Moonlight.Triangulation.Bench.SpadeCompare.Lane
+  ( LaneClass (..)
+  , LaneKind (..)
+  , LaneRequest (..)
+  , LaneObstruction (..)
+  , laneKindName
+  , laneRequestLabel
+  , parseLaneRequest
+  , renderInventoryCsv
+  , renderInventoryHuman
+  , renderLaneSpecs
+  , renderLaneObstruction
+  , renderSnapshotSpecs
+  ) where
+
+import Data.List (find, intercalate)
+import Text.Read (readMaybe)
+
+data LaneClass
+  = ParityLane
+  | CliffLane
+  deriving stock (Eq, Show)
+
+-- | Every benchmark command understood by the Moonlight runner. The board is
+-- a closed subset of this vocabulary; the remaining constructors are focused
+-- diagnostics rather than Spade comparison rows.
+data LaneKind
+  = BulkLoadLane
+  | IncrementalLane
+  | SnapshotInsertLane
+  | SweepAngleCollapseLane
+  | DegenerateLineLane
+  | BatchSweepLane
+  | NearestLane
+  | CdtRecoveryLane
+  | ConstraintIncrementalLane
+  | ConstraintSplitLane
+  | PublicationFloorLane
+  | RemovalLane
+  | SnapshotRemovalLane
+  | HierarchyIncrementalLane
+  | HierarchyDuplicateLane
+  | HierarchyRemovalLane
+  | HierarchyRemovalOnlyLane
+  | HierarchyRebuildOnlyLane
+  | InterpolationLane
+  | VoronoiSweepLane
+  | DcelWalkLane
+  | AlphaPersistenceLane
+  | IntersectionLane
+  | IntersectionOutsideLane
+  | RefineLane
+  deriving stock (Bounded, Enum, Eq, Show)
+
+data LaneRequest = LaneRequest
+  { laneRequestKind :: !LaneKind
+  , laneRequestFirst :: !Int
+  , laneRequestSecond :: !Int
+  }
+  deriving stock (Eq, Show)
+
+data LaneObstruction
+  = UnknownLane !String
+  | UnknownLaneClass !String
+  | MalformedWorkSize !String
+  | NegativeWorkSize !Int
+  | AlphaPersistenceRequiresSpecializedRunner
+  deriving stock (Eq, Show)
+
+data LaneSpec = LaneSpec
+  { laneSpecClass :: !LaneClass
+  , laneSpecRequest :: !LaneRequest
+  , laneSpecDisplay :: !String
+  }
+
+data SnapshotStressSpec = SnapshotStressSpec
+  { snapshotSessionRequest :: !LaneRequest
+  , snapshotPublicationRequest :: !LaneRequest
+  , snapshotDisplay :: !String
+  }
+
+laneKindName :: LaneKind -> String
+laneKindName = \case
+  BulkLoadLane -> "bulk-load"
+  IncrementalLane -> "incremental"
+  SnapshotInsertLane -> "snapshot-insert"
+  SweepAngleCollapseLane -> "sweep-angle-collapse"
+  DegenerateLineLane -> "degenerate-line"
+  BatchSweepLane -> "batch-sweep"
+  NearestLane -> "nearest"
+  CdtRecoveryLane -> "cdt-recovery"
+  ConstraintIncrementalLane -> "constraint-incremental"
+  ConstraintSplitLane -> "constraint-split"
+  PublicationFloorLane -> "publication-floor"
+  RemovalLane -> "removal"
+  SnapshotRemovalLane -> "snapshot-removal"
+  HierarchyIncrementalLane -> "hierarchy-incremental"
+  HierarchyDuplicateLane -> "hierarchy-duplicate"
+  HierarchyRemovalLane -> "hierarchy-removal"
+  HierarchyRemovalOnlyLane -> "hierarchy-removal-only"
+  HierarchyRebuildOnlyLane -> "hierarchy-rebuild-only"
+  InterpolationLane -> "interpolation"
+  VoronoiSweepLane -> "voronoi-sweep"
+  DcelWalkLane -> "dcel-walk"
+  AlphaPersistenceLane -> "alpha-persistence"
+  IntersectionLane -> "intersection"
+  IntersectionOutsideLane -> "intersection-outside"
+  RefineLane -> "refine"
+
+laneRequestLabel :: LaneRequest -> String
+laneRequestLabel request =
+  intercalate
+    "-"
+    [ laneKindName (laneRequestKind request)
+    , show (laneRequestFirst request)
+    , show (laneRequestSecond request)
+    ]
+
+parseLaneRequest :: String -> String -> String -> Either LaneObstruction LaneRequest
+parseLaneRequest rawKind rawFirst rawSecond =
+  LaneRequest
+    <$> parseLaneKind rawKind
+    <*> parseWorkSize rawFirst
+    <*> parseWorkSize rawSecond
+
+renderLaneObstruction :: LaneObstruction -> String
+renderLaneObstruction = \case
+  UnknownLane lane -> "unknown benchmark lane: " <> lane
+  UnknownLaneClass laneClass -> "unknown benchmark lane class: " <> laneClass
+  MalformedWorkSize raw -> "benchmark work size is not an integer: " <> raw
+  NegativeWorkSize size -> "benchmark work size is negative: " <> show size
+  AlphaPersistenceRequiresSpecializedRunner ->
+    "alpha-persistence is owned by moonlight-triangulation-alpha-spade-referent"
+
+renderInventoryCsv :: String
+renderInventoryCsv =
+  unlines
+    ( fmap renderBoardLane boardLaneSpecs
+        <> fmap renderSnapshotLane snapshotStressSpecs
+    )
+ where
+  renderBoardLane specification =
+    csvRow
+      [ "lane"
+      , laneClassName (laneSpecClass specification)
+      , laneRequestLabel (laneSpecRequest specification)
+      , laneSpecDisplay specification
+      ]
+
+  renderSnapshotLane specification =
+    csvRow
+      [ "lane"
+      , "snapshot"
+      , laneRequestLabel (snapshotPublicationRequest specification)
+      , snapshotDisplay specification
+      , laneRequestLabel (snapshotSessionRequest specification)
+      ]
+
+renderInventoryHuman :: String
+renderInventoryHuman =
+  concatMap renderClass [ParityLane, CliffLane]
+    <> "snapshot lanes ("
+    <> show (length snapshotStressSpecs)
+    <> ")\n"
+    <> concatMap renderSnapshot snapshotStressSpecs
+ where
+  renderClass laneClass =
+    let specifications = laneSpecs laneClass
+     in laneClassName laneClass
+          <> " lanes ("
+          <> show (length specifications)
+          <> ")\n"
+          <> concatMap
+            ((<> "\n") . ("  " <>) . laneRequestLabel . laneSpecRequest)
+            specifications
+
+  renderSnapshot specification =
+    "  "
+      <> laneRequestLabel (snapshotPublicationRequest specification)
+      <> " against "
+      <> laneRequestLabel (snapshotSessionRequest specification)
+      <> "\n"
+
+renderLaneSpecs :: String -> Either LaneObstruction String
+renderLaneSpecs rawClass =
+  unlines . fmap (renderRequestWords . laneSpecRequest) . laneSpecs
+    <$> parseLaneClass rawClass
+
+renderSnapshotSpecs :: String
+renderSnapshotSpecs =
+  unlines
+    ( fmap
+        ( \specification ->
+            renderRequestWords (snapshotSessionRequest specification)
+              <> " "
+              <> renderRequestWords (snapshotPublicationRequest specification)
+        )
+        snapshotStressSpecs
+    )
+
+parseLaneKind :: String -> Either LaneObstruction LaneKind
+parseLaneKind raw =
+  maybe
+    (Left (UnknownLane raw))
+    Right
+    (find ((== raw) . laneKindName) [minBound .. maxBound])
+
+parseLaneClass :: String -> Either LaneObstruction LaneClass
+parseLaneClass = \case
+  "parity" -> Right ParityLane
+  "cliff" -> Right CliffLane
+  other -> Left (UnknownLaneClass other)
+
+parseWorkSize :: String -> Either LaneObstruction Int
+parseWorkSize raw =
+  maybe
+    (Left (MalformedWorkSize raw))
+    (\size -> if size < 0 then Left (NegativeWorkSize size) else Right size)
+    (readMaybe raw)
+
+laneClassName :: LaneClass -> String
+laneClassName = \case
+  ParityLane -> "parity"
+  CliffLane -> "cliff"
+
+laneSpecs :: LaneClass -> [LaneSpec]
+laneSpecs laneClass = filter ((== laneClass) . laneSpecClass) boardLaneSpecs
+
+renderRequestWords :: LaneRequest -> String
+renderRequestWords request =
+  unwords
+    [ laneKindName (laneRequestKind request)
+    , show (laneRequestFirst request)
+    , show (laneRequestSecond request)
+    ]
+
+csvRow :: [String] -> String
+csvRow = intercalate "," . fmap csvField
+
+csvField :: String -> String
+csvField field
+  | any (`elem` [',', '"', '\n', '\r']) field =
+      '"' : concatMap escapeQuote field <> "\""
+  | otherwise = field
+ where
+  escapeQuote '"' = "\"\""
+  escapeQuote character = [character]
+
+boardLaneSpecs :: [LaneSpec]
+boardLaneSpecs =
+  [ parity BulkLoadLane 1000 0 "Bulk load, 1k points"
+  , parity BulkLoadLane 10000 0 "Bulk load, 10k"
+  , parity IncrementalLane 1000 0 "Incremental insert, 1k"
+  , parity IncrementalLane 10000 0 "Incremental insert, 10k"
+  , parity IncrementalLane 500000 0 "Incremental insert, 500k"
+  , parity NearestLane 20000 5000 "Nearest, 5k queries / 20k points"
+  , parity CdtRecoveryLane 8000 800 "CDT recovery, 800 / 8k"
+  , parity RefineLane 625 0 "Refinement, 625 Steiner"
+  , parity RefineLane 2500 0 "Refinement, 2,500 Steiner"
+  , parity RemovalLane 1000 250 "Removal, 250 / 1k"
+  , parity RemovalLane 10000 2500 "Removal, 2.5k / 10k"
+  , parity RemovalLane 100000 25000 "Removal, 25k / 100k"
+  , parity InterpolationLane 10000 1000 "Interpolation, 1k queries / 10k"
+  , parity InterpolationLane 100000 2000 "Interpolation, 2k queries / 100k"
+  , parity VoronoiSweepLane 1000 0 "Voronoi dual sweep, 1k sites"
+  , parity DcelWalkLane 2000 0 "DCEL traversal, 2k points"
+  , parity IntersectionLane 10000 500 "Line intersection, 500 chords / 10k"
+  , cliff ConstraintIncrementalLane 8000 800 "Constraint one at a time, 800 / 8k"
+  , cliff ConstraintSplitLane 1000 0 "Constraint split, 1k crossings"
+  -- This deliberately stays at 1k while its parity twin also reaches 10k.
+  , cliff HierarchyIncrementalLane 1000 0 "Hierarchy insert, 1k"
+  , cliff HierarchyDuplicateLane 10000 500 "Hierarchy duplicate insert, 500 / 10k"
+  , cliff HierarchyRemovalLane 10000 250 "Hierarchy removal, 250 / 10k"
+  , cliff SweepAngleCollapseLane 2000 0 "Sweep hull index at one angle, 2k"
+  , cliff DegenerateLineLane 2000 0 "Exactly collinear load, face-less locate, 2k"
+  , cliff IntersectionOutsideLane 2000 100 "Line intersection from outside the hull, 100 / 2k"
+  ]
+ where
+  parity :: LaneKind -> Int -> Int -> String -> LaneSpec
+  parity kind first second display =
+    LaneSpec ParityLane (LaneRequest kind first second) display
+
+  cliff :: LaneKind -> Int -> Int -> String -> LaneSpec
+  cliff kind first second display =
+    LaneSpec CliffLane (LaneRequest kind first second) display
+
+snapshotStressSpecs :: [SnapshotStressSpec]
+snapshotStressSpecs =
+  [ SnapshotStressSpec
+      (LaneRequest IncrementalLane 1000 0)
+      (LaneRequest SnapshotInsertLane 1000 0)
+      "Insert snapshots, 1k points"
+  , SnapshotStressSpec
+      (LaneRequest RemovalLane 10000 2500)
+      (LaneRequest SnapshotRemovalLane 10000 2500)
+      "Removal snapshots, 2.5k / 10k"
+  ]
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs
@@ -0,0 +1,428 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Removal lanes, their gates, and the removal diagnostics.
+module Moonlight.Triangulation.Bench.SpadeCompare.Removal where
+
+import Control.Monad (when)
+import Data.Foldable (traverse_)
+import Data.List (sortOn)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.FilePath ((</>))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.Dcel
+import Moonlight.Triangulation.Handles.HandleDefs
+import Moonlight.Triangulation.Math
+import Moonlight.Triangulation.PointLocation
+import Moonlight.Triangulation.Removal
+import Moonlight.Triangulation.Session
+import Moonlight.Triangulation.Types
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+
+-- | The removal gate: a deterministic prefix of the input, removed by
+-- coordinate, and the surviving canonical edge set. Delaunay triangulations
+-- of point sets in general position are unique, so the survivor is forced
+-- and both implementations must agree bit-for-bit.
+writeRemovalGate :: FilePath -> Int -> Int -> IO ()
+writeRemovalGate directory pointCount removalCount = do
+  let points = randomPoints 0x9e3779b97f4a7c15 pointCount
+  triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
+  surviving <- removeAllByCoordinate triangulation (V.fromList (take removalCount points))
+  writeFile
+    (directory </> ("removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt"))
+    (unlines (canonicalEdges surviving))
+
+-- | Remove vertices by coordinate inside one session: one O(n) dense thaw and
+-- publication for the whole program, then O(degree) topology work per removal.
+-- The snapshot 'locateAndRemove' fold pays that O(n) boundary per removal.
+removeAllByCoordinate
+  :: DelaunayTriangulation Point
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+removeAllByCoordinate triangulation removals =
+  case withSession triangulation 0 (removeManyAt removals) of
+    Left refusal -> fail (show refusal)
+    Right (outcomes, surviving, _) -> do
+      V.imapM_ checkRemoved outcomes
+      pure surviving
+ where
+  checkRemoved index outcome = case outcome of
+    Nothing ->
+      error
+        ( "removal target is not a vertex: index="
+            <> show index
+            <> " point="
+            <> ( case removals V.! index of
+                  Point x y -> hex64 x <> "," <> hex64 y
+               )
+        )
+    Just _ -> pure ()
+
+-- | Removal by coordinate through the persistent entry point: a thaw and a
+-- publication per removal, against the session fold's one of each for the whole
+-- program. Same removals, same order, same survivor — only the price differs.
+removeAllPersistently
+  :: DelaunayTriangulation Point
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+removeAllPersistently =
+  V.foldM'
+    (\current point -> do
+      outcome <- require (locateAndRemove current point)
+      case outcome of
+        Nothing -> fail ("removal target is not a vertex: " <> pointHex point)
+        Just result -> pure (removalTriangulation result)
+    )
+
+writePersistentRemovalGate :: FilePath -> Int -> Int -> IO ()
+writePersistentRemovalGate directory pointCount removalCount = do
+  let points = randomPoints 0x9e3779b97f4a7c15 pointCount
+  base <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
+  surviving <- removeAllPersistently base (V.fromList (take removalCount points))
+  writeFile
+    ( directory
+        </> ("persistent-removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt")
+    )
+    (unlines (canonicalEdges surviving))
+
+-- | Removal audited against the one oracle that needs no second implementation:
+-- the Delaunay triangulation of a point set in general position is unique, so
+-- removing a prefix must leave exactly the triangulation a fresh bulk load of
+-- the survivors produces. Reports the first removal at which that identity
+-- breaks, so a divergence is dated rather than merely observed.
+--
+-- Every removal also reports the handle swap-compaction relocated into the
+-- freed slot, and that handle is the caller's only re-anchor after a removal
+-- invalidates the rest. An out-of-range or misnamed handle is invisible to the
+-- edge-set oracle, so it is checked here against the vertex the mesh moved.
+reportRemovalAudit :: Int -> Int -> IO ()
+reportRemovalAudit pointCount removalCount = do
+  let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+  base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
+  let step current index
+        | index >= removalCount = putStrLn ("clean-through " <> show removalCount)
+        | otherwise =
+            case allPoints V.!? index of
+              Nothing -> putStrLn ("input-exhausted-at-removal " <> show index)
+              Just point -> do
+                outcome <- require (locateAndRemove current point)
+                case outcome of
+                  Nothing -> putStrLn ("located-nothing-at-removal " <> show index)
+                  Just result -> do
+                    let nextTriangulation = removalTriangulation result
+                        movedPoint =
+                          vertexPoint current (VertexId (fromIntegral (numVertices current - 1)))
+                    case removalOutcomeSwap (removalOutcome result) of
+                      Nothing
+                        | vertexPoint current (VertexId (fromIntegral (numVertices current - 1))) /= point ->
+                            putStrLn ("swap-handle-missing-at-removal " <> show index)
+                        | otherwise -> pure ()
+                      Just (swappedIn@(VertexId rawSwappedIn), _)
+                        | toInteger rawSwappedIn >= toInteger (numVertices nextTriangulation) ->
+                            putStrLn
+                              ( "swap-handle-out-of-range-at-removal "
+                                  <> show index
+                                  <> " handle="
+                                  <> show rawSwappedIn
+                                  <> " vertices="
+                                  <> show (numVertices nextTriangulation)
+                              )
+                        | vertexPoint nextTriangulation swappedIn /= movedPoint ->
+                            putStrLn
+                              ( "swap-handle-misnamed-at-removal "
+                                  <> show index
+                                  <> " handle="
+                                  <> show rawSwappedIn
+                              )
+                        | otherwise -> pure ()
+                    fresh <-
+                      buildTriangulation
+                        <$> require (delaunay unitElementDefaults (V.drop (index + 1) allPoints))
+                    let removalEdges = canonicalEdges nextTriangulation
+                        freshEdges = canonicalEdges fresh
+                    if removalEdges == freshEdges
+                      then step nextTriangulation (index + 1)
+                      else do
+                        let (onlyRemoval, onlyFresh) = sortedDifference removalEdges freshEdges
+                        putStrLn ("diverged-at-removal " <> show index)
+                        putStrLn
+                          ( "removed-point "
+                              <> case point of
+                                Point x y -> hex64 x <> "," <> hex64 y
+                          )
+                        putStrLn ("removal-vertices " <> show (numVertices nextTriangulation))
+                        putStrLn ("fresh-vertices " <> show (numVertices fresh))
+                        putStrLn ("removal-edges " <> show (length removalEdges))
+                        putStrLn ("fresh-edges " <> show (length freshEdges))
+                        putStrLn ("only-in-removal " <> show (length onlyRemoval))
+                        putStrLn ("only-in-fresh " <> show (length onlyFresh))
+                        let (invertedRemoval, illegalRemoval) = meshHealth nextTriangulation
+                            (invertedFresh, illegalFresh) = meshHealth fresh
+                        putStrLn ("removal-inverted-faces " <> show invertedRemoval)
+                        putStrLn ("removal-illegal-half-edges " <> show illegalRemoval)
+                        putStrLn ("fresh-inverted-faces " <> show invertedFresh)
+                        putStrLn ("fresh-illegal-half-edges " <> show illegalFresh)
+                        traverse_ (putStrLn . ("  removal-only " <>)) (take 8 onlyRemoval)
+                        traverse_ (putStrLn . ("  fresh-only " <>)) (take 8 onlyFresh)
+  step base 0
+
+-- | The geometry around one removal, reconstructed with public API only. For
+-- an interior removal the cavity border is the cyclic link of the removed
+-- vertex, and nothing but a drain flip can destroy one of those non-star
+-- edges: cleanup removes only the star, and swap-remove touches only the
+-- removed vertex. The reported hull predicate guards that interpretation.
+-- The fan origin 'remeshRing' would pick is also public: 'collectOutgoing'
+-- starts at the stored out-edge, so the origin is the destination of that
+-- edge.
+reportRemovalContext :: Int -> Int -> IO ()
+reportRemovalContext pointCount target = do
+  let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+  case allPoints V.!? target of
+    Nothing -> do
+      hPutStrLn stderr "removal-context: target index out of range"
+      exitWith (ExitFailure 2)
+    Just removedPoint -> do
+      base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
+      before <- removeAllByCoordinate base (V.take target allPoints)
+      after <- removeAllByCoordinate before (V.singleton removedPoint)
+      fresh <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop (target + 1) allPoints))
+      lab <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop target allPoints))
+      removedQuery <- require (mkQueryPoint removedPoint)
+      putStrLn ("lab-clean " <> show (canonicalEdges before == canonicalEdges lab))
+      case locatePoint before removedQuery of
+        OnVertex removedVertex -> do
+          let ring = ringOf before removedVertex
+              ringPoints = map snd ring
+              ringMember point = any (== point) ringPoints
+              rOnHull = any (\(edge, _) -> incidentFace before edge == outerFace) ring
+              afterPairs = canonicalEdgePairs after
+              freshPairs = canonicalEdgePairs fresh
+              borderPairs = cyclicPairs ringPoints
+              destroyedBorder = [pair | pair <- borderPairs, not (any (== pair) afterPairs)]
+              removalOnly = [pair | pair <- afterPairs, not (any (== pair) freshPairs)]
+              freshOnly = [pair | pair <- freshPairs, not (any (== pair) afterPairs)]
+          putStrLn ("r-on-hull " <> show rOnHull)
+          putStrLn ("ring-size " <> show (length ringPoints))
+          traverse_
+            (\(index, (_, point)) -> putStrLn ("  ring " <> show index <> " " <> pointHex point))
+            (zip [(0 :: Int) ..] ring)
+          putStrLn ("border-edges-before " <> show (length borderPairs))
+          putStrLn ("border-edges-destroyed " <> show (length destroyedBorder))
+          traverse_
+            (\(p, q) -> putStrLn ("  destroyed " <> pointHex p <> " " <> pointHex q))
+            destroyedBorder
+          case ringPoints of
+            [] -> putStrLn "fan-unavailable empty-ring"
+            fanOriginPoint : ringTail ->
+              if rOnHull
+                then putStrLn "fan-unavailable hull-removal"
+                else do
+                  let fanOrientations =
+                        zipWith3
+                          (\index p q -> (index, p, q, orient2d fanOriginPoint p q))
+                          [(1 :: Int) ..]
+                          ringTail
+                          (drop 1 ringTail)
+                      fanCandidates =
+                        zipWith3
+                          ( \(index, priorPoint) current following ->
+                              ( index
+                              , current
+                              , orient2d priorPoint following fanOriginPoint == GT
+                                  && orient2d following priorPoint current == GT
+                              , inCircle current fanOriginPoint priorPoint following
+                              )
+                          )
+                          (zip [(2 :: Int) ..] ringTail)
+                          (drop 1 ringTail)
+                          (drop 2 ringTail)
+                  putStrLn ("fan-origin " <> pointHex fanOriginPoint)
+                  putStrLn
+                    ( "fan-inverted "
+                        <> show (length [() | (_, _, _, verdict) <- fanOrientations, verdict == LT])
+                        <> " of "
+                        <> show (length fanOrientations)
+                    )
+                  traverse_
+                    ( \(index, p, q, verdict) ->
+                        when (verdict == LT) $
+                          putStrLn
+                            ( "  fan-inverted-at "
+                                <> show index
+                                <> " "
+                                <> pointHex fanOriginPoint
+                                <> " "
+                                <> pointHex p
+                                <> " "
+                                <> pointHex q
+                            )
+                    )
+                    fanOrientations
+                  traverse_
+                    ( \(index, current, convex, circle) ->
+                        putStrLn
+                          ( "  fan-candidate "
+                              <> show index
+                              <> " "
+                              <> pointHex current
+                              <> "->"
+                              <> pointHex fanOriginPoint
+                              <> " convex="
+                              <> show convex
+                              <> " incircle="
+                              <> show circle
+                          )
+                    )
+                    fanCandidates
+          putStrLn ("only-in-removal " <> show (length removalOnly))
+          traverse_
+            ( \(p, q) ->
+                putStrLn
+                  ( "  removal-only "
+                      <> pointHex p
+                      <> " "
+                      <> pointHex q
+                      <> " ring="
+                      <> show (ringMember p, ringMember q)
+                  )
+            )
+            removalOnly
+          putStrLn ("only-in-fresh " <> show (length freshOnly))
+          traverse_
+            ( \(p, q) ->
+                putStrLn
+                  ( "  fresh-only "
+                      <> pointHex p
+                      <> " "
+                      <> pointHex q
+                      <> " ring="
+                      <> show (ringMember p, ringMember q)
+                  )
+            )
+            freshOnly
+          let inverted = invertedFaces after
+          putStrLn ("inverted-faces " <> show (length inverted))
+          traverse_
+            ( \(p, q, r) ->
+                putStrLn
+                  ( "  inverted-face "
+                      <> pointHex p
+                      <> " "
+                      <> pointHex q
+                      <> " "
+                      <> pointHex r
+                      <> " ring="
+                      <> show (ringMember p, ringMember q, ringMember r)
+                  )
+            )
+            inverted
+        other -> putStrLn ("locate-unexpected " <> show other)
+ where
+  ringOf triangulation vertex =
+    case vertexOutEdge triangulation vertex of
+      Nothing -> []
+      Just start -> walk start start []
+   where
+    walk start current acc =
+      let ringVertex = destination triangulation current
+          nextEdge = counterClockwise triangulation current
+          acc' = (current, vertexPoint triangulation ringVertex) : acc
+       in if nextEdge == start
+            then reverse acc'
+            else walk start nextEdge acc'
+
+  cyclicPairs (first : second : rest) =
+    zipWith canonicalPair (first : second : rest) ((second : rest) ++ [first])
+  cyclicPairs _ = []
+
+  canonicalEdgePairs triangulation =
+    sortOn
+      (\(p, q) -> (pointKey p, pointKey q))
+      [ canonicalPair p q
+      | edge <- undirectedEdges triangulation
+      , let (from, to) = undirectedEndpoints triangulation edge
+            p = vertexPoint triangulation from
+            q = vertexPoint triangulation to
+      ]
+
+  canonicalPair p q =
+    if pointKey p <= pointKey q then (p, q) else (q, p)
+
+  pointKey (Point x y) = (x, y)
+
+  invertedFaces triangulation =
+    [ (vertexPoint triangulation a, vertexPoint triangulation b, vertexPoint triangulation c)
+    | face <- innerFaces triangulation
+    , Just (a, b, c) <- [innerFaceVertices triangulation face]
+    , orient2d
+        (vertexPoint triangulation a)
+        (vertexPoint triangulation b)
+        (vertexPoint triangulation c)
+        == LT
+    ]
+
+-- | Two independent health measures, which together decide what a divergence
+-- means. Delaunay's theorem says a /valid/ triangulation with no locally
+-- illegal edge is globally Delaunay — so a mesh that differs from the fresh
+-- build while carrying no illegal edge cannot be a valid triangulation, and
+-- one carrying illegal edges was simply left under-legalized.
+meshHealth
+  :: Triangulation mode vertex directed undirected face
+  -> (Int, Int)
+meshHealth triangulation =
+  ( length [() | face <- innerFaces triangulation, not (isCounterClockwise face)]
+  , length
+      [ ()
+      | face <- innerFaces triangulation
+      , edge <- faceEdgeList face
+      , edgeIsIllegal edge
+      ]
+  )
+ where
+  faceEdgeList face = case innerFaceDirectedEdges triangulation face of
+    Nothing -> []
+    Just (e0, e1, e2) -> [e0, e1, e2]
+
+  isCounterClockwise face = case innerFaceVertices triangulation face of
+    Nothing -> True
+    Just (a, b, c) ->
+      orient2d
+        (vertexPoint triangulation a)
+        (vertexPoint triangulation b)
+        (vertexPoint triangulation c)
+        == GT
+
+  edgeIsIllegal edge =
+    let twin = reverseEdge edge
+     in incidentFace triangulation twin /= outerFace
+          && inCircle
+            (vertexPoint triangulation (origin triangulation edge))
+            (vertexPoint triangulation (destination triangulation edge))
+            (vertexPoint triangulation (destination triangulation (next triangulation edge)))
+            (vertexPoint triangulation (destination triangulation (next triangulation twin)))
+            == GT
+
+-- | The Delaunay triangulation of the survivors, built from scratch. Diffed
+-- against a removal result it decides whether that result is the Delaunay
+-- triangulation it is required to be.
+writeFreshSuffix :: Int -> Int -> FilePath -> IO ()
+writeFreshSuffix pointCount dropped path = do
+  triangulation <-
+    buildTriangulation
+      <$> require
+        ( delaunay
+            unitElementDefaults
+            (V.drop dropped (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)))
+        )
+  writeFile path (unlines (canonicalEdges triangulation))
+
+-- | Symmetric difference of two ascending lists, by merge.
+sortedDifference :: Ord a => [a] -> [a] -> ([a], [a])
+sortedDifference [] right = ([], right)
+sortedDifference left [] = (left, [])
+sortedDifference left@(l : ls) right@(r : rs)
+  | l == r = sortedDifference ls rs
+  | l < r = let (onlyLeft, onlyRight) = sortedDifference ls right in (l : onlyLeft, onlyRight)
+  | otherwise = let (onlyLeft, onlyRight) = sortedDifference left rs in (onlyLeft, r : onlyRight)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Input generation, canonical encoding, and the shared build helpers.
+module Moonlight.Triangulation.Bench.SpadeCompare.Support where
+
+import Data.List (sort)
+import Data.Word (Word64)
+import GHC.Float (castDoubleToWord64)
+import Numeric (showHex)
+import qualified Data.Vector as V
+import Moonlight.Triangulation
+import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Triangulation.HintGenerator
+
+-- | The same generated points flattened onto a sliver a picometre thick. No
+-- coordinate becomes equal to another and no four points become cocircular, so
+-- the input stays in general position and the load stays on the ordinary
+-- face-building path; what collapses is the angular spread the bulk loaders
+-- index their hulls by. That collapse is the whole of what this input
+-- measures, and it does not reach the face-less location path — that one needs
+-- exact collinearity, which 'exactlyCollinearPoints' supplies and gates.
+nearCollinearPoints :: Word64 -> Int -> [Point]
+nearCollinearPoints seed count =
+  [Point x (y * 1.0e-12) | Point x y <- randomPoints seed count]
+
+-- | Points on one exact line: @(i, 0)@, every coordinate an integer that a
+-- 'Double' holds exactly. No three are in general position, so 'orient2d'
+-- returns an exact zero on both sides and no face is ever built.
+--
+-- Uniqueness does not need general position, only one valid answer. A
+-- collinear set admits exactly one triangulation — the chain of consecutive
+-- segments, @n - 1@ of them and no triangle — so the canonical edge set is
+-- fully determined and gates bit for bit against the strict side. Both
+-- implementations state that same count as their own degenerate Euler
+-- invariant, which is what makes this a shared answer rather than a
+-- coincidence.
+exactlyCollinearPoints :: Int -> [Point]
+exactlyCollinearPoints count = [Point (fromIntegral index) 0 | index <- [0 .. count - 1]]
+
+hex64 :: Double -> String
+hex64 value =
+  let raw = showHex (castDoubleToWord64 value) ""
+   in replicate (16 - length raw) '0' <> raw
+
+-- | Canonical edge set: each undirected edge as its two endpoint coordinates in
+-- IEEE-754 bit patterns, endpoint-ordered then globally sorted. Formatting
+-- cannot introduce a spurious disagreement and rounding cannot hide a real one.
+canonicalEdges :: Triangulation mode vertex directed undirected face -> [String]
+canonicalEdges triangulation =
+  sort
+    [ encodeCanonicalEdge first second
+    | edge <- undirectedEdges triangulation
+    , let (from, to) = undirectedEndpoints triangulation edge
+          p = vertexPoint triangulation from
+          q = vertexPoint triangulation to
+          (first, second) = if key p <= key q then (p, q) else (q, p)
+    ]
+ where
+  key (Point x y) = (x, y)
+
+canonicalConstraintEdges
+  :: ConstrainedDelaunayTriangulation vertex
+  -> [String]
+canonicalConstraintEdges triangulation =
+  sort
+    [ encodeCanonicalEdge first second
+    | edge <- constraintEdges triangulation
+    , let (from, to) = undirectedEndpoints triangulation edge
+          p = vertexPoint triangulation from
+          q = vertexPoint triangulation to
+          (first, second) =
+            if pointKey p <= pointKey q
+              then (p, q)
+              else (q, p)
+    ]
+ where
+  pointKey (Point x y) = (x, y)
+
+encodeCanonicalEdge :: Point -> Point -> String
+encodeCanonicalEdge (Point ax ay) (Point bx by) =
+  concatMap hex64 [ax, ay, bx, by]
+
+coordHex :: Point -> String
+coordHex (Point x y) = hex64 x <> hex64 y
+
+-- | A cycle, rotated to begin at its smallest encoded element.
+--
+-- Both implementations keep one entry edge per vertex and per face — whichever
+-- their builder wrote last — so the two walk the same cycle from different
+-- places. Rotating pins the order, which the mesh determines, and declines to
+-- pin the starting point, which it does not. The rotation compares the encoded
+-- strings rather than the coordinates so that both sides order by the same
+-- comparison: the bit pattern of a negative double does not sort in numeric
+-- order, so a Haskell 'Ord' on 'Point' and a Rust @partial_cmp@ on a tuple
+-- would each have to be argued to agree where two byte strings simply do.
+rotateToSmallest :: [String] -> [String]
+rotateToSmallest [] = []
+rotateToSmallest ring =
+  let !smallest = minimum ring
+      (before, after) = break (== smallest) ring
+   in after <> before
+
+require :: Show failure => Either failure value -> IO value
+require (Left failure) = fail (show failure)
+require (Right value) = pure value
+
+requireQueryPoints
+  :: Traversable container
+  => container Point
+  -> IO (container QueryPoint)
+requireQueryPoints = require . traverse mkQueryPoint
+
+requireQueryChords
+  :: Traversable container
+  => container (Point, Point)
+  -> IO (container (QueryPoint, QueryPoint))
+requireQueryChords =
+  require
+    . traverse
+      (\(from, to) -> (,) <$> mkQueryPoint from <*> mkQueryPoint to)
+
+delaunayOf :: Int -> IO (DelaunayTriangulation Point)
+delaunayOf count =
+  buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x9e3779b97f4a7c15 count)))
+
+-- | One line per query: the coordinates of the nearest vertex the hierarchy
+-- routed the search to, or @none@.
+nearestAnswer
+  :: DelaunayTriangulation Point
+  -> HierarchyHint
+  -> QueryPoint
+  -> String
+nearestAnswer triangulation hierarchy query =
+  let hint = case hierarchyHint hierarchy query of
+        Just (VertexHint vertex) -> Just vertex
+        _ -> Nothing
+   in case nearestNeighbor triangulation hint query of
+        Nothing -> "none"
+        Just (vertex, _) ->
+          let Point x y = vertexPoint triangulation vertex
+           in hex64 x <> hex64 y
+
+pointHex :: Point -> String
+pointHex (Point x y) = hex64 x <> "," <> hex64 y
+
+constraintPairs :: Int -> Int -> [(Int, Int)]
+constraintPairs pointCount constraintCount =
+  take constraintCount
+    [ (a, b)
+    | index <- [0 ..]
+    , let a = index `mod` pointCount
+          b = (index * 6151 + pointCount `quot` 2) `mod` pointCount
+    , a /= b
+    ]
+
+-- | A closed square with no dangling constraint. The interior segment used by
+-- the package's own benchmark is outside spade's documented "closed shape"
+-- contract and makes it refuse the whole domain — not a workload the two can be
+-- timed on.
+refinementInput :: (V.Vector Point, V.Vector (Int, Int))
+refinementInput =
+  ( V.fromList [Point 0 0, Point 64 0, Point 64 64, Point 0 64]
+  , V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
+  )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Timing.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Timing.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Timing.hs
@@ -0,0 +1,23 @@
+-- | The single fresh-process timing boundary shared by both Haskell runners.
+module Moonlight.Triangulation.Bench.SpadeCompare.Timing
+  ( measureLane
+  ) where
+
+import Control.DeepSeq (NFData, force)
+import Control.Exception (evaluate)
+import GHC.Clock (getMonotonicTimeNSec)
+import Moonlight.Triangulation.Bench.SpadeCompare.Lane
+  ( LaneRequest
+  , laneRequestLabel
+  )
+import System.Mem (performGC)
+
+-- | Time exactly one fully forced iteration. Input construction belongs to
+-- each interpreter before this boundary; the driver supplies fresh processes.
+measureLane :: NFData value => LaneRequest -> IO value -> IO ()
+measureLane request work = do
+  performGC
+  start <- getMonotonicTimeNSec
+  _ <- work >>= evaluate . force
+  stop <- getMonotonicTimeNSec
+  putStrLn (laneRequestLabel request <> "," <> show (stop - start))
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs
@@ -0,0 +1,156 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The Voronoi dual gate and the dual sweep lane.
+module Moonlight.Triangulation.Bench.SpadeCompare.Voronoi where
+
+import Data.List (sort)
+import System.FilePath ((</>))
+import Moonlight.Triangulation
+import Moonlight.Triangulation.Dcel
+import Moonlight.Triangulation.Voronoi
+import Moonlight.Triangulation.Bench.SpadeCompare.Support
+
+-- | The dual, pinned by its combinatorics and by nothing else.
+--
+-- Every verb the dual offers resolves a primal element, so its answers are
+-- exact over a mesh the bulk-load gate already pins bit for bit. Voronoi vertex
+-- POSITIONS are the exception and are deliberately absent from every artifact
+-- here: a position is a circumcentre, and the two implementations do not form
+-- one the same way. spade takes the reciprocal of the determinant and
+-- multiplies by it; this side first rescales both difference vectors by their
+-- largest component and only then divides. Each rounds where the other does
+-- not, so the positions cannot be bit-identical, and a gate over them would be
+-- grading the arithmetic rather than the dual. What IS determined about a
+-- position is whether it exists at all, and the vertex artifact pins that.
+--
+-- Direction vectors are pinned in full: each component is one subtraction of
+-- two stored coordinates on both sides, so there is nothing left to round.
+writeVoronoiGate :: FilePath -> Int -> IO ()
+writeVoronoiGate directory count = do
+  triangulation <- delaunayOf count
+  let prefix = directory </> ("voronoi-" <> show count)
+  writeFile
+    (prefix <> "-dual.txt")
+    (unlines (sort (map (dualEdgeRecord triangulation) (directedVoronoiEdges triangulation))))
+  writeFile (prefix <> "-cells.txt") (unlines (voronoiCellLines triangulation))
+  writeFile (prefix <> "-vertices.txt") (unlines (voronoiVertexLines triangulation))
+
+-- | One directed dual edge: the primal edge it names, the site of the dual face
+-- to its left, the dual vertex at each end, the destinations its dual @next@
+-- and @prev@ reach, and its direction vector. @next@ and @prev@ rotate about
+-- the primal origin, so naming them by destination alone loses nothing.
+dualEdgeRecord
+  :: DelaunayTriangulation Point
+  -> DirectedVoronoiEdgeId
+  -> String
+dualEdgeRecord triangulation edge =
+  concat
+    [ coordHex (site (origin triangulation primal))
+    , coordHex (site (destination triangulation primal))
+    , coordHex (site (voronoiFaceSite (voronoiIncidentFace triangulation edge)))
+    , dualVertexName triangulation primal (voronoiFrom triangulation edge)
+    , dualVertexName triangulation primal (voronoiTo triangulation edge)
+    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiNext triangulation edge))))
+    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiPrevious triangulation edge))))
+    , coordHex (voronoiDirectionVector triangulation edge)
+    ]
+ where
+  primal = asDelaunayDirectedEdge edge
+  site = vertexPoint triangulation
+
+-- | An inner dual vertex named by the one corner of its dual face that is not
+-- an endpoint of the edge the question was asked about — the record already
+-- carries the other two, so the apex completes the face. An outer dual vertex
+-- is the edge itself and needs no name.
+--
+-- The @X@ tag cannot be reached: an inner dual vertex names an inner face by
+-- construction, and 'innerFaceVertices' refuses only the outer one. spade
+-- cannot emit it at all, its dual vertex carrying a face handle already typed
+-- inner, so an @X@ appearing here is a disagreement rather than a crash.
+dualVertexName
+  :: DelaunayTriangulation Point
+  -> DirectedEdgeId
+  -> VoronoiVertexId
+  -> String
+dualVertexName _ _ (OuterVoronoiVertex _) = "O"
+dualVertexName triangulation primal (InnerVoronoiVertex face) =
+  case innerFaceVertices triangulation face of
+    Nothing -> "X"
+    Just (a, b, c)
+      | isApex a -> named a
+      | isApex b -> named b
+      | otherwise -> named c
+ where
+  from = origin triangulation primal
+  to = destination triangulation primal
+  isApex vertex = vertex /= from && vertex /= to
+  named vertex = "I" <> coordHex (vertexPoint triangulation vertex)
+
+-- | The dual faces, one line each, plus the three enumerator lengths. The
+-- undirected dual carries no artifact of its own: it is the primal undirected
+-- edge set relabelled, which every existing edge gate already pins, so its
+-- length is the only claim left to make about it.
+voronoiCellLines :: DelaunayTriangulation Point -> [String]
+voronoiCellLines triangulation =
+  [ "faces " <> show (length (voronoiFaces triangulation))
+  , "directed " <> show (length (directedVoronoiEdges triangulation))
+  , "undirected " <> show (length (undirectedVoronoiEdges triangulation))
+  ]
+    <> sort
+      [ coordHex (vertexPoint triangulation (voronoiFaceSite face)) <> concat (rotateToSmallest ring)
+      | face <- voronoiFaces triangulation
+      , let ring =
+              [ coordHex
+                  ( vertexPoint
+                      triangulation
+                      (destination triangulation (asDelaunayDirectedEdge edge))
+                  )
+              | edge <- voronoiFaceAdjacentEdges triangulation face
+              ]
+      ]
+
+-- | The inner dual vertices, keyed by their dual face's three corners. Each
+-- line carries whether the vertex has a position — the only exactly determined
+-- thing about a circumcentre — and the outgoing triple the dual reports.
+voronoiVertexLines :: DelaunayTriangulation Point -> [String]
+voronoiVertexLines triangulation =
+  sort
+    [ concat (sort (map coordHex [site a, site b, site c]))
+        <> existence dual
+        <> concat (rotateToSmallest (outgoing dual))
+    | face <- innerFaces triangulation
+    , Just (a, b, c) <- [innerFaceVertices triangulation face]
+    , let dual = InnerVoronoiVertex face
+    ]
+ where
+  site = vertexPoint triangulation
+
+  existence dual = case voronoiVertexPosition triangulation dual of
+    Nothing -> "N"
+    Just _ -> "J"
+
+  outgoing dual = case voronoiVertexOutgoingEdges triangulation dual of
+    Nothing -> []
+    Just edges ->
+      [ coordHex (site (origin triangulation primal)) <> coordHex (site (destination triangulation primal))
+      | edge <- edges
+      , let primal = asDelaunayDirectedEdge edge
+      ]
+
+-- | The whole dual, walked the way spade's own documentation walks it: every
+-- dual face, its adjacent dual edges, and the dual vertex each of those runs
+-- to. The position of an inner dual vertex is summed rather than merely
+-- reached, because a 'Maybe' scrutinised only to its outer constructor can wrap
+-- a thunk and the lane would then be timing the allocation of a circumcentre
+-- instead of the circumcentre. Unbounded cells are counted, so a dual that
+-- stopped classifying hull edges as outer moves the answer rather than the cost.
+voronoiSweep :: DelaunayTriangulation Point -> (Double, Int)
+voronoiSweep triangulation = foldl' overCell (0, 0) (voronoiFaces triangulation)
+ where
+  overCell accumulator face =
+    foldl' overEdge accumulator (voronoiFaceAdjacentEdges triangulation face)
+
+  overEdge (!total, !unbounded) edge =
+    case voronoiVertexPosition triangulation (voronoiTo triangulation edge) of
+      Nothing -> (total, unbounded + 1)
+      Just (Point x y) -> (total + x + y, unbounded)
diff --git a/bindings/README.md b/bindings/README.md
deleted file mode 100644
--- a/bindings/README.md
+++ /dev/null
@@ -1,95 +0,0 @@
-# Foreign bindings
-
-`moonlight-triangulation.cabal` owns the private Haskell `ffi` sublibrary and
-`moonlight-triangulation-c` foreign library. Python, TypeScript, and Rust are
-thin lifecycle and bulk-layout leaves over that sole ABI; none implements
-triangulation, overlay, publication, valuation, or morphology.
-
-From `compiler`, build and locate the shared library:
-
-```bash
-../scripts/safe-cabal.sh build \
-  moonlight-triangulation:flib:moonlight-triangulation-c \
-  --project-file=cabal.project.triangulation-dev --enable-shared -j1
-cabal list-bin moonlight-triangulation:flib:moonlight-triangulation-c \
-  --project-file=cabal.project.triangulation-dev --enable-shared
-```
-
-Set `MOONLIGHT_TRIANGULATION_LIBRARY` to that `.dylib`, `.so`, or `.dll`; Rust
-linking also reads its directory from `MOONLIGHT_TRIANGULATION_LIB_DIR`.
-
-## ABI version 2
-
-Three opaque immutable carriers cross the boundary:
-
-- `ml_mesh` retains a binary64 Delaunay mesh. Its `ml_mesh_site_*` algebra
-  combines sites, not polygon interiors.
-- `ml_region` retains the authoritative exact rational `PlanarRegion`.
-  `ml_region_create_f64` admits every input coordinate exactly as its binary64
-  dyadic value in one call, using point counts per loop and loop counts per
-  component. The inverse bulk projection returns component/loop offsets and
-  binary64 rendering points; it does not replace the exact retained geometry.
-- `ml_structuring_element` retains one admitted origin-containing convex
-  polygon for repeated offset, inset, opening, and closing calls.
-
-Region union, intersection, difference, and symmetric difference call the one
-Haskell overlay and grouped-publication path. Point location is exact. A single
-valuation call returns Euler characteristic, exact reduced area as
-`numerator/denominator`, and certified conventional-perimeter bounds. Morphology
-returns a new region plus the existing operation/work receipt.
-
-Every status-returning call accepts an optional `ml_obstruction` distinguishing
-pointer/count/buffer failures from region layout, validation, overlay,
-publication, valuation, projection, morphology, and runtime failures. Handles
-remain valid until explicitly freed; every operation publishes a fresh output
-handle. `ml_runtime_initialize` is idempotent and process-lifetime.
-
-Python and Rust owned handles are thread-affine: create, use, and close each
-handle on one thread. This prevents explicit close from racing a foreign call
-after the runtime has received the raw handle.
-
-Batch related mesh edits into one `insert_many` call. Author a whole region with
-one component list; there is deliberately no mutable builder or per-vertex FFI
-surface.
-
-## Python
-
-```python
-from fractions import Fraction
-from moonlight_triangulation import Moonlight, PolygonComponent
-
-moonlight = Moonlight()
-left = moonlight.region([
-    PolygonComponent(((0, 0), (2, 0), (2, 2), (0, 2)))
-])
-right = moonlight.region([
-    PolygonComponent(((1, 0), (3, 0), (3, 2), (1, 2)))
-])
-intersection = left.intersection(right)
-assert intersection.valuations.area == Fraction(2, 1)
-```
-
-## TypeScript
-
-```typescript
-import { Moonlight, RegionLocation } from "@moonlight/triangulation";
-
-const moonlight = new Moonlight();
-const region = moonlight.region([
-  { outer: [[0, 0], [2, 0], [2, 2], [0, 2]] },
-]);
-console.log(region.valuations().area, region.locate([1, 1]) === RegionLocation.Interior);
-```
-
-## Rust
-
-```rust
-use moonlight_triangulation::{Moonlight, PolygonComponent};
-
-let moonlight = Moonlight::initialize()?;
-let region = moonlight.region(&[PolygonComponent {
-    outer: vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]],
-    holes: vec![],
-}])?;
-println!("{:?}", region.valuations()?.area);
-```
diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
deleted file mode 100644
--- a/bindings/python/pyproject.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-[build-system]
-requires = ["hatchling>=1.27,<2"]
-build-backend = "hatchling.build"
-
-[project]
-name = "moonlight-triangulation"
-version = "2.0.0"
-description = "Python bindings for Moonlight exact regions and immutable Delaunay meshes."
-requires-python = ">=3.11"
-license = { text = "MIT" }
-
-[tool.hatch.build.targets.wheel]
-packages = ["src/moonlight_triangulation"]
diff --git a/bindings/python/src/moonlight_triangulation/__init__.py b/bindings/python/src/moonlight_triangulation/__init__.py
deleted file mode 100644
--- a/bindings/python/src/moonlight_triangulation/__init__.py
+++ /dev/null
@@ -1,632 +0,0 @@
-from __future__ import annotations
-
-import ctypes
-import os
-import weakref
-from array import array
-from collections.abc import Callable, Iterable, Sequence
-from dataclasses import dataclass
-from enum import IntEnum
-from fractions import Fraction
-from pathlib import Path
-from threading import get_ident
-from typing import Final, Self
-
-
-class _Obstruction(ctypes.Structure):
-    _fields_ = [
-        ("code", ctypes.c_uint32),
-        ("coordinate_error", ctypes.c_uint32),
-        ("input_index", ctypes.c_uint64),
-        ("first_index", ctypes.c_uint64),
-        ("second_index", ctypes.c_uint64),
-        ("first_value", ctypes.c_double),
-        ("second_value", ctypes.c_double),
-        ("point_x", ctypes.c_double),
-        ("point_y", ctypes.c_double),
-        ("message", ctypes.c_char * 256),
-    ]
-
-
-class _NativeMinkowskiReceipt(ctypes.Structure):
-    _fields_ = [
-        ("operation", ctypes.c_uint32),
-        ("reserved", ctypes.c_uint32),
-        ("input_components", ctypes.c_uint64),
-        ("convex_pieces", ctypes.c_uint64),
-        ("generated_pieces", ctypes.c_uint64),
-        ("generated_convolution_edges", ctypes.c_uint64),
-        ("overlay_passes", ctypes.c_uint64),
-        ("exact_crossings", ctypes.c_uint64),
-        ("output_cells", ctypes.c_uint64),
-        ("exact_coordinate_bit_growth", ctypes.c_uint64),
-    ]
-
-
-class MoonlightError(RuntimeError):
-    def __init__(self, status: int, obstruction: _Obstruction) -> None:
-        self.status = status
-        self.code = obstruction.code
-        self.coordinate_error = obstruction.coordinate_error
-        self.input_index = obstruction.input_index
-        self.first_index = obstruction.first_index
-        self.second_index = obstruction.second_index
-        self.first_value = obstruction.first_value
-        self.second_value = obstruction.second_value
-        self.point = (obstruction.point_x, obstruction.point_y)
-        message = bytes(obstruction.message).split(b"\0", 1)[0].decode("utf-8", errors="replace")
-        super().__init__(message or f"Moonlight ABI failure {status}:{self.code}")
-
-
-class _NativeApi:
-    _OK: Final = 0
-    _ABI_VERSION: Final = 2
-
-    def __init__(self, library_path: Path) -> None:
-        library = ctypes.CDLL(str(library_path))
-        handle = ctypes.c_void_p
-        handle_output = ctypes.POINTER(handle)
-        obstruction = ctypes.POINTER(_Obstruction)
-        count_output = ctypes.POINTER(ctypes.c_size_t)
-        receipt_output = ctypes.POINTER(_NativeMinkowskiReceipt)
-        binary_operation = (handle, handle, handle_output, obstruction)
-        morphology_operation = (handle, handle, handle_output, receipt_output, obstruction)
-
-        _configure(library, "ml_abi_version", ())
-        _configure(library, "ml_runtime_initialize", ())
-        _configure(library, "ml_delaunay_f64", (ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, handle_output, obstruction))
-        _configure(library, "ml_mesh_insert_many_f64", (handle, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, handle_output, obstruction))
-        _configure(library, "ml_mesh_site_union", binary_operation)
-        _configure(library, "ml_mesh_site_intersection", binary_operation)
-        _configure(library, "ml_mesh_site_difference", binary_operation)
-        _configure(library, "ml_mesh_site_symmetric_difference", binary_operation)
-        _configure(library, "ml_mesh_vertex_count", (handle, count_output, obstruction))
-        _configure(library, "ml_mesh_triangle_count", (handle, count_output, obstruction))
-        _configure(library, "ml_mesh_copy_vertices_f64", (handle, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, count_output, obstruction))
-        _configure(library, "ml_mesh_copy_triangles_u32", (handle, ctypes.POINTER(ctypes.c_uint32), ctypes.c_size_t, count_output, obstruction))
-        _configure(library, "ml_mesh_free", (handle,), None)
-
-        _configure(
-            library,
-            "ml_region_create_f64",
-            (
-                ctypes.POINTER(ctypes.c_double),
-                ctypes.c_size_t,
-                ctypes.POINTER(ctypes.c_size_t),
-                ctypes.c_size_t,
-                ctypes.POINTER(ctypes.c_size_t),
-                ctypes.c_size_t,
-                handle_output,
-                obstruction,
-            ),
-        )
-        _configure(library, "ml_region_counts", (handle, count_output, count_output, count_output, obstruction))
-        _configure(
-            library,
-            "ml_region_copy_f64",
-            (
-                handle,
-                ctypes.POINTER(ctypes.c_double),
-                ctypes.c_size_t,
-                ctypes.POINTER(ctypes.c_size_t),
-                ctypes.c_size_t,
-                ctypes.POINTER(ctypes.c_size_t),
-                ctypes.c_size_t,
-                obstruction,
-            ),
-        )
-        _configure(library, "ml_region_union", binary_operation)
-        _configure(library, "ml_region_intersection", binary_operation)
-        _configure(library, "ml_region_difference", binary_operation)
-        _configure(library, "ml_region_symmetric_difference", binary_operation)
-        _configure(library, "ml_region_locate_point_f64", (handle, ctypes.c_double, ctypes.c_double, ctypes.POINTER(ctypes.c_uint32), obstruction))
-        _configure(
-            library,
-            "ml_region_measure",
-            (
-                handle,
-                ctypes.POINTER(ctypes.c_int64),
-                ctypes.POINTER(ctypes.c_char),
-                ctypes.c_size_t,
-                count_output,
-                ctypes.POINTER(ctypes.c_double),
-                ctypes.POINTER(ctypes.c_double),
-                obstruction,
-            ),
-        )
-        _configure(library, "ml_region_free", (handle,), None)
-        _configure(library, "ml_structuring_element_create_f64", (ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, handle_output, obstruction))
-        _configure(library, "ml_structuring_element_free", (handle,), None)
-        _configure(library, "ml_region_minkowski_sum", morphology_operation)
-        _configure(library, "ml_region_offset", morphology_operation)
-        _configure(library, "ml_region_inset", morphology_operation)
-        _configure(library, "ml_region_open", morphology_operation)
-        _configure(library, "ml_region_close", morphology_operation)
-
-        status = int(library.ml_runtime_initialize())
-        if status != self._OK:
-            raise RuntimeError(f"Moonlight runtime initialization failed with status {status}")
-        abi_version = int(library.ml_abi_version())
-        if abi_version != self._ABI_VERSION:
-            raise RuntimeError(f"unsupported Moonlight ABI version {abi_version}")
-        self.library = library
-
-    def check(self, status: int, obstruction: _Obstruction) -> None:
-        if status != self._OK:
-            raise MoonlightError(status, obstruction)
-
-
-Point = tuple[float, float]
-Triangle = tuple[int, int, int]
-_BinaryNativeOperation = Callable[[ctypes.c_void_p, ctypes.c_void_p, object, object], int]
-_MorphologyNativeOperation = Callable[[ctypes.c_void_p, ctypes.c_void_p, object, object, object], int]
-
-
-@dataclass(frozen=True)
-class PolygonComponent:
-    outer: tuple[Point, ...]
-    holes: tuple[tuple[Point, ...], ...] = ()
-
-
-class RegionLocation(IntEnum):
-    EXTERIOR = 0
-    BOUNDARY = 1
-    INTERIOR = 2
-
-
-class MinkowskiOperation(IntEnum):
-    ADDITION = 0
-    EROSION = 1
-    OPENING = 2
-    CLOSING = 3
-
-
-@dataclass(frozen=True)
-class RegionValuations:
-    euler_characteristic: int
-    area: Fraction
-    perimeter_bounds: tuple[float, float]
-
-
-@dataclass(frozen=True)
-class MinkowskiReceipt:
-    operation: MinkowskiOperation
-    input_components: int
-    convex_pieces: int
-    generated_pieces: int
-    generated_convolution_edges: int
-    overlay_passes: int
-    exact_crossings: int
-    output_cells: int
-    exact_coordinate_bit_growth: int
-
-
-class Moonlight:
-    def __init__(self, library_path: str | os.PathLike[str] | None = None) -> None:
-        configured_path = library_path or os.environ.get("MOONLIGHT_TRIANGULATION_LIBRARY")
-        if configured_path is None:
-            raise ValueError("set MOONLIGHT_TRIANGULATION_LIBRARY or pass library_path")
-        self._native = _NativeApi(Path(configured_path))
-
-    def delaunay(self, points: Sequence[Point]) -> Mesh:
-        coordinates, pointer = _coordinate_buffer(points)
-        handle = _produce_handle(
-            self._native,
-            lambda output, obstruction: self._native.library.ml_delaunay_f64(
-                pointer, len(coordinates) // 2, output, obstruction
-            ),
-        )
-        return Mesh(self._native, handle)
-
-    def region(self, components: Sequence[PolygonComponent]) -> Region:
-        loops, loop_counts, component_counts = _component_layout(components)
-        coordinates, coordinate_pointer = _coordinate_buffer(
-            point for loop in loops for point in loop
-        )
-        loop_buffer = _size_buffer(loop_counts)
-        component_buffer = _size_buffer(component_counts)
-        handle = _produce_handle(
-            self._native,
-            lambda output, obstruction: self._native.library.ml_region_create_f64(
-                coordinate_pointer,
-                len(coordinates) // 2,
-                loop_buffer,
-                len(loop_counts),
-                component_buffer,
-                len(component_counts),
-                output,
-                obstruction,
-            ),
-        )
-        return Region(self._native, handle)
-
-    def structuring_element(self, points: Sequence[Point]) -> StructuringElement:
-        coordinates, pointer = _coordinate_buffer(points)
-        handle = _produce_handle(
-            self._native,
-            lambda output, obstruction: self._native.library.ml_structuring_element_create_f64(
-                pointer, len(coordinates) // 2, output, obstruction
-            ),
-        )
-        return StructuringElement(self._native, handle)
-
-
-class _OwnedHandle:
-    __slots__ = ("_native", "_handle", "_finalizer", "_kind", "_owner_thread", "__weakref__")
-
-    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p, free: Callable[[ctypes.c_void_p], None], kind: str) -> None:
-        self._native = native
-        self._handle = handle
-        self._kind = kind
-        self._owner_thread = get_ident()
-        self._finalizer = weakref.finalize(self, free, handle)
-
-    def close(self) -> None:
-        self._require_owner_thread()
-        self._finalizer()
-        self._handle = ctypes.c_void_p()
-
-    def __enter__(self) -> Self:
-        return self
-
-    def __exit__(self, _type: object, _value: object, _traceback: object) -> None:
-        self.close()
-
-    def _live_handle(self) -> ctypes.c_void_p:
-        self._require_owner_thread()
-        if not self._finalizer.alive:
-            raise RuntimeError(f"{self._kind} is closed")
-        return self._handle
-
-    def _require_owner_thread(self) -> None:
-        if get_ident() != self._owner_thread:
-            raise RuntimeError(f"{self._kind} belongs to another thread")
-
-    def _require_same_runtime(self, other: _OwnedHandle) -> None:
-        if self._native is not other._native:
-            raise ValueError("both values must belong to the same Moonlight runtime")
-
-
-class Mesh(_OwnedHandle):
-    __slots__ = ()
-
-    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:
-        super().__init__(native, handle, native.library.ml_mesh_free, "mesh")
-
-    @property
-    def vertex_count(self) -> int:
-        return self._count(self._native.library.ml_mesh_vertex_count)
-
-    @property
-    def triangle_count(self) -> int:
-        return self._count(self._native.library.ml_mesh_triangle_count)
-
-    @property
-    def vertices(self) -> tuple[Point, ...]:
-        count = self.vertex_count
-        output = (ctypes.c_double * (count * 2))()
-        written = ctypes.c_size_t()
-        obstruction = _Obstruction()
-        status = int(
-            self._native.library.ml_mesh_copy_vertices_f64(
-                self._live_handle(), output, count, ctypes.byref(written), ctypes.byref(obstruction)
-            )
-        )
-        self._native.check(status, obstruction)
-        return tuple((float(output[index * 2]), float(output[index * 2 + 1])) for index in range(written.value))
-
-    @property
-    def triangles(self) -> tuple[Triangle, ...]:
-        count = self.triangle_count
-        output = (ctypes.c_uint32 * (count * 3))()
-        written = ctypes.c_size_t()
-        obstruction = _Obstruction()
-        status = int(
-            self._native.library.ml_mesh_copy_triangles_u32(
-                self._live_handle(), output, count, ctypes.byref(written), ctypes.byref(obstruction)
-            )
-        )
-        self._native.check(status, obstruction)
-        return tuple(
-            (int(output[index * 3]), int(output[index * 3 + 1]), int(output[index * 3 + 2]))
-            for index in range(written.value)
-        )
-
-    def insert_many(self, points: Sequence[Point]) -> Mesh:
-        coordinates, pointer = _coordinate_buffer(points)
-        handle = _produce_handle(
-            self._native,
-            lambda output, obstruction: self._native.library.ml_mesh_insert_many_f64(
-                self._live_handle(), pointer, len(coordinates) // 2, output, obstruction
-            ),
-        )
-        return Mesh(self._native, handle)
-
-    def site_union(self, other: Mesh) -> Mesh:
-        return self._binary(other, self._native.library.ml_mesh_site_union)
-
-    def site_intersection(self, other: Mesh) -> Mesh:
-        return self._binary(other, self._native.library.ml_mesh_site_intersection)
-
-    def site_difference(self, other: Mesh) -> Mesh:
-        return self._binary(other, self._native.library.ml_mesh_site_difference)
-
-    def site_symmetric_difference(self, other: Mesh) -> Mesh:
-        return self._binary(other, self._native.library.ml_mesh_site_symmetric_difference)
-
-    def _binary(self, other: Mesh, operation: _BinaryNativeOperation) -> Mesh:
-        self._require_same_runtime(other)
-        handle = _produce_handle(
-            self._native,
-            lambda output, obstruction: operation(
-                self._live_handle(), other._live_handle(), output, obstruction
-            ),
-        )
-        return Mesh(self._native, handle)
-
-    def _count(self, operation: Callable[[ctypes.c_void_p, object, object], int]) -> int:
-        output = ctypes.c_size_t()
-        obstruction = _Obstruction()
-        status = int(operation(self._live_handle(), ctypes.byref(output), ctypes.byref(obstruction)))
-        self._native.check(status, obstruction)
-        return int(output.value)
-
-
-class Region(_OwnedHandle):
-    __slots__ = ()
-
-    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:
-        super().__init__(native, handle, native.library.ml_region_free, "region")
-
-    @property
-    def components(self) -> tuple[PolygonComponent, ...]:
-        component_count, loop_count, point_count = self._counts()
-        coordinates = (ctypes.c_double * (point_count * 2))()
-        loop_offsets = (ctypes.c_size_t * (loop_count + 1))()
-        component_offsets = (ctypes.c_size_t * (component_count + 1))()
-        obstruction = _Obstruction()
-        status = int(
-            self._native.library.ml_region_copy_f64(
-                self._live_handle(),
-                coordinates,
-                point_count,
-                loop_offsets,
-                loop_count + 1,
-                component_offsets,
-                component_count + 1,
-                ctypes.byref(obstruction),
-            )
-        )
-        self._native.check(status, obstruction)
-        points = tuple((float(coordinates[index * 2]), float(coordinates[index * 2 + 1])) for index in range(point_count))
-        loops = tuple(
-            points[int(loop_offsets[index]) : int(loop_offsets[index + 1])]
-            for index in range(loop_count)
-        )
-        component_ranges = tuple(
-            (int(component_offsets[index]), int(component_offsets[index + 1]))
-            for index in range(component_count)
-        )
-        if any(start >= end for start, end in component_ranges):
-            raise RuntimeError("Moonlight returned a component without an outer loop")
-        return tuple(
-            PolygonComponent(loops[start], loops[start + 1 : end])
-            for start, end in component_ranges
-        )
-
-    @property
-    def valuations(self) -> RegionValuations:
-        return self._measure_with_capacity(128)
-
-    def locate(self, point: Point) -> RegionLocation:
-        output = ctypes.c_uint32()
-        obstruction = _Obstruction()
-        status = int(
-            self._native.library.ml_region_locate_point_f64(
-                self._live_handle(), point[0], point[1], ctypes.byref(output), ctypes.byref(obstruction)
-            )
-        )
-        self._native.check(status, obstruction)
-        return RegionLocation(output.value)
-
-    def union(self, other: Region) -> Region:
-        return self._binary(other, self._native.library.ml_region_union)
-
-    def intersection(self, other: Region) -> Region:
-        return self._binary(other, self._native.library.ml_region_intersection)
-
-    def difference(self, other: Region) -> Region:
-        return self._binary(other, self._native.library.ml_region_difference)
-
-    def symmetric_difference(self, other: Region) -> Region:
-        return self._binary(other, self._native.library.ml_region_symmetric_difference)
-
-    def minkowski_sum(self, other: Region) -> tuple[Region, MinkowskiReceipt]:
-        self._require_same_runtime(other)
-        return self._morph(
-            lambda output, receipt, obstruction: self._native.library.ml_region_minkowski_sum(
-                self._live_handle(), other._live_handle(), output, receipt, obstruction
-            )
-        )
-
-    def offset(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
-        return self._with_element(element, self._native.library.ml_region_offset)
-
-    def inset(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
-        return self._with_element(element, self._native.library.ml_region_inset)
-
-    def open(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
-        return self._with_element(element, self._native.library.ml_region_open)
-
-    def close_with(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
-        return self._with_element(element, self._native.library.ml_region_close)
-
-    def _binary(self, other: Region, operation: _BinaryNativeOperation) -> Region:
-        self._require_same_runtime(other)
-        handle = _produce_handle(
-            self._native,
-            lambda output, obstruction: operation(
-                self._live_handle(), other._live_handle(), output, obstruction
-            ),
-        )
-        return Region(self._native, handle)
-
-    def _with_element(
-        self, element: StructuringElement, operation: _MorphologyNativeOperation
-    ) -> tuple[Region, MinkowskiReceipt]:
-        self._require_same_runtime(element)
-        return self._morph(
-            lambda output, receipt, obstruction: operation(
-                element._live_handle(), self._live_handle(), output, receipt, obstruction
-            )
-        )
-
-    def _morph(
-        self,
-        operation: Callable[[object, object, object], int],
-    ) -> tuple[Region, MinkowskiReceipt]:
-        handle, native_receipt = _produce_morphology(self._native, operation)
-        return Region(self._native, handle), _receipt(native_receipt)
-
-    def _counts(self) -> tuple[int, int, int]:
-        component_count = ctypes.c_size_t()
-        loop_count = ctypes.c_size_t()
-        point_count = ctypes.c_size_t()
-        obstruction = _Obstruction()
-        status = int(
-            self._native.library.ml_region_counts(
-                self._live_handle(),
-                ctypes.byref(component_count),
-                ctypes.byref(loop_count),
-                ctypes.byref(point_count),
-                ctypes.byref(obstruction),
-            )
-        )
-        self._native.check(status, obstruction)
-        return int(component_count.value), int(loop_count.value), int(point_count.value)
-
-    def _measure_with_capacity(self, capacity: int) -> RegionValuations:
-        euler = ctypes.c_int64()
-        area = ctypes.create_string_buffer(capacity)
-        area_bytes = ctypes.c_size_t()
-        lower = ctypes.c_double()
-        upper = ctypes.c_double()
-        obstruction = _Obstruction()
-        status = int(
-            self._native.library.ml_region_measure(
-                self._live_handle(),
-                ctypes.byref(euler),
-                area,
-                capacity,
-                ctypes.byref(area_bytes),
-                ctypes.byref(lower),
-                ctypes.byref(upper),
-                ctypes.byref(obstruction),
-            )
-        )
-        if status == 3 and obstruction.code == 102:
-            return self._measure_with_capacity(int(area_bytes.value) + 1)
-        self._native.check(status, obstruction)
-        numerator, separator, denominator = area.value.decode("ascii").partition("/")
-        if separator != "/":
-            raise RuntimeError("Moonlight returned a malformed exact-area ratio")
-        return RegionValuations(int(euler.value), Fraction(int(numerator), int(denominator)), (lower.value, upper.value))
-
-
-class StructuringElement(_OwnedHandle):
-    __slots__ = ()
-
-    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:
-        super().__init__(native, handle, native.library.ml_structuring_element_free, "structuring element")
-
-
-def _component_layout(
-    components: Sequence[PolygonComponent],
-) -> tuple[tuple[Sequence[Point], ...], tuple[int, ...], tuple[int, ...]]:
-    loops = tuple(
-        loop
-        for component in components
-        for loop in (component.outer, *component.holes)
-    )
-    loop_counts = tuple(len(loop) for loop in loops)
-    component_counts = tuple(len(component.holes) + 1 for component in components)
-    return loops, loop_counts, component_counts
-
-
-def _coordinate_buffer(points: Iterable[Point]) -> tuple[array, object]:
-    coordinates = array("d", (component for x, y in points for component in (x, y)))
-    if coordinates.itemsize != ctypes.sizeof(ctypes.c_double):
-        raise RuntimeError("Python's native double width does not match the Moonlight ABI")
-    if not coordinates:
-        return coordinates, None
-    pointer = (ctypes.c_double * len(coordinates)).from_buffer(coordinates)
-    return coordinates, pointer
-
-
-def _size_buffer(values: Sequence[int]) -> object:
-    return (ctypes.c_size_t * len(values))(*values)
-
-
-def _produce_handle(
-    native: _NativeApi,
-    operation: Callable[[object, object], int],
-) -> ctypes.c_void_p:
-    output = ctypes.c_void_p()
-    obstruction = _Obstruction()
-    status = int(operation(ctypes.byref(output), ctypes.byref(obstruction)))
-    native.check(status, obstruction)
-    return _required_handle(output)
-
-
-def _produce_morphology(
-    native: _NativeApi,
-    operation: Callable[[object, object, object], int],
-) -> tuple[ctypes.c_void_p, _NativeMinkowskiReceipt]:
-    output = ctypes.c_void_p()
-    receipt = _NativeMinkowskiReceipt()
-    obstruction = _Obstruction()
-    status = int(operation(ctypes.byref(output), ctypes.byref(receipt), ctypes.byref(obstruction)))
-    native.check(status, obstruction)
-    return _required_handle(output), receipt
-
-
-def _receipt(native: _NativeMinkowskiReceipt) -> MinkowskiReceipt:
-    return MinkowskiReceipt(
-        MinkowskiOperation(native.operation),
-        int(native.input_components),
-        int(native.convex_pieces),
-        int(native.generated_pieces),
-        int(native.generated_convolution_edges),
-        int(native.overlay_passes),
-        int(native.exact_crossings),
-        int(native.output_cells),
-        int(native.exact_coordinate_bit_growth),
-    )
-
-
-def _required_handle(handle: ctypes.c_void_p) -> ctypes.c_void_p:
-    if not handle.value:
-        raise RuntimeError("Moonlight returned success without a handle")
-    return handle
-
-
-def _configure(library: ctypes.CDLL, name: str, parameters: Sequence[object], result: object = ctypes.c_uint32) -> None:
-    function = getattr(library, name)
-    setattr(function, "argtypes", list(parameters))
-    setattr(function, "restype", result)
-
-
-__all__ = [
-    "Mesh",
-    "MinkowskiOperation",
-    "MinkowskiReceipt",
-    "Moonlight",
-    "MoonlightError",
-    "Point",
-    "PolygonComponent",
-    "Region",
-    "RegionLocation",
-    "RegionValuations",
-    "StructuringElement",
-    "Triangle",
-]
diff --git a/bindings/python/tests/test_binding.py b/bindings/python/tests/test_binding.py
deleted file mode 100644
--- a/bindings/python/tests/test_binding.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from __future__ import annotations
-
-import math
-import os
-import unittest
-from collections.abc import Sequence
-from concurrent.futures import ThreadPoolExecutor
-from fractions import Fraction
-from typing import ClassVar
-
-from moonlight_triangulation import (
-    MinkowskiOperation,
-    Moonlight,
-    MoonlightError,
-    PolygonComponent,
-    RegionLocation,
-)
-
-
-class LengthInflatingPoints(Sequence[tuple[float, float]]):
-    def __len__(self) -> int:
-        return 1_000
-
-    def __getitem__(self, index: int) -> tuple[float, float]:
-        if index == 0:
-            return (0.0, 0.0)
-        raise IndexError(index)
-
-
-class MoonlightBindingTest(unittest.TestCase):
-    engine: ClassVar[Moonlight]
-
-    @classmethod
-    def setUpClass(cls) -> None:
-        cls.engine = Moonlight(os.environ["MOONLIGHT_TRIANGULATION_LIBRARY"])
-
-    def test_immutable_site_set_algebra_and_dense_projection(self) -> None:
-        left = self.engine.delaunay([(0, 0), (2, 0), (0, 2), (2, 2)])
-        right = self.engine.delaunay([(2, 0), (4, 0), (2, 2), (4, 2)])
-        union = left.site_union(right)
-        intersection = left.site_intersection(right)
-        difference = left.site_difference(right)
-        symmetric = left.site_symmetric_difference(right)
-        extended = left.insert_many([(1, 1), (3, 1)])
-        self.addCleanup(left.close)
-        self.addCleanup(right.close)
-        self.addCleanup(union.close)
-        self.addCleanup(intersection.close)
-        self.addCleanup(difference.close)
-        self.addCleanup(symmetric.close)
-        self.addCleanup(extended.close)
-
-        self.assertEqual(left.vertex_count, 4)
-        self.assertEqual(union.vertex_count, 6)
-        self.assertEqual(intersection.vertex_count, 2)
-        self.assertEqual(difference.vertex_count, 2)
-        self.assertEqual(symmetric.vertex_count, 4)
-        self.assertEqual(extended.vertex_count, 6)
-        self.assertEqual(len(left.vertices), left.vertex_count)
-        self.assertEqual(len(left.triangles), left.triangle_count)
-        self.assertTrue(all(max(triangle) < left.vertex_count for triangle in left.triangles))
-
-    def test_exact_region_boolean_valuation_and_morphology(self) -> None:
-        left = self.engine.region([PolygonComponent(((0, 0), (2, 0), (2, 2), (0, 2)))])
-        right = self.engine.region([PolygonComponent(((1, 0), (3, 0), (3, 2), (1, 2)))])
-        intersection = left.intersection(right)
-        difference = left.difference(right)
-        symmetric = left.symmetric_difference(right)
-        kernel = self.engine.structuring_element(((-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)))
-        offset, receipt = left.offset(kernel)
-        self.addCleanup(left.close)
-        self.addCleanup(right.close)
-        self.addCleanup(intersection.close)
-        self.addCleanup(difference.close)
-        self.addCleanup(symmetric.close)
-        self.addCleanup(kernel.close)
-        self.addCleanup(offset.close)
-
-        self.assertEqual(len(left.components), 1)
-        self.assertEqual(len(left.components[0].outer), 4)
-        self.assertEqual(intersection.valuations.area, Fraction(2, 1))
-        self.assertEqual(difference.valuations.euler_characteristic, 1)
-        self.assertEqual(symmetric.valuations.euler_characteristic, 2)
-        self.assertEqual(left.locate((1, 1)), RegionLocation.INTERIOR)
-        self.assertEqual(left.locate((0, 1)), RegionLocation.BOUNDARY)
-        self.assertEqual(left.locate((3, 1)), RegionLocation.EXTERIOR)
-        self.assertEqual(offset.valuations.area, Fraction(9, 1))
-        self.assertEqual(receipt.operation, MinkowskiOperation.ADDITION)
-        self.assertGreaterEqual(receipt.generated_pieces, 1)
-
-    def test_invalid_coordinate_preserves_typed_witness(self) -> None:
-        with self.assertRaises(MoonlightError) as raised:
-            self.engine.delaunay([(0, 0), (math.nan, 1), (1, 0)])
-        self.assertEqual(raised.exception.status, 4)
-        self.assertEqual(raised.exception.code, 1)
-        self.assertEqual(raised.exception.coordinate_error, 1)
-        self.assertEqual(raised.exception.input_index, 1)
-
-    def test_native_count_descends_from_materialized_coordinates(self) -> None:
-        mesh = self.engine.delaunay(LengthInflatingPoints())
-        self.addCleanup(mesh.close)
-        self.assertEqual(mesh.vertex_count, 1)
-
-    def test_owned_handles_are_thread_affine(self) -> None:
-        mesh = self.engine.delaunay([(0, 0), (1, 0), (0, 1)])
-        self.addCleanup(mesh.close)
-        with ThreadPoolExecutor(max_workers=1) as executor:
-            foreign_access = executor.submit(lambda: mesh.vertex_count)
-            with self.assertRaisesRegex(RuntimeError, "belongs to another thread"):
-                foreign_access.result()
-        self.assertEqual(mesh.vertex_count, 3)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml
deleted file mode 100644
--- a/bindings/rust/Cargo.toml
+++ /dev/null
@@ -1,14 +0,0 @@
-[package]
-name = "moonlight-triangulation"
-version = "2.0.0"
-edition = "2024"
-license = "MIT"
-description = "Rust bindings for Moonlight exact regions and immutable Delaunay meshes."
-repository = "https://github.com/PaleRoses/moonlight.git"
-build = "build.rs"
-links = "moonlight-triangulation-c"
-
-[lib]
-path = "src/lib.rs"
-
-[workspace]
diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs
deleted file mode 100644
--- a/bindings/rust/build.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use std::env;
-
-fn main() {
-    println!("cargo:rerun-if-env-changed=MOONLIGHT_TRIANGULATION_LIB_DIR");
-    if let Some(directory) = env::var_os("MOONLIGHT_TRIANGULATION_LIB_DIR") {
-        println!(
-            "cargo:rustc-link-search=native={}",
-            directory.to_string_lossy()
-        );
-    }
-}
diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs
deleted file mode 100644
--- a/bindings/rust/src/lib.rs
+++ /dev/null
@@ -1,966 +0,0 @@
-use std::error::Error;
-use std::ffi::{c_char, c_double, c_uint};
-use std::fmt::{Display, Formatter};
-use std::marker::PhantomData;
-use std::ptr::NonNull;
-use std::rc::Rc;
-
-const STATUS_OK: u32 = 0;
-const STATUS_BUFFER_TOO_SMALL: u32 = 3;
-const ABI_VERSION: u32 = 2;
-
-#[repr(C)]
-struct NativeMesh {
-    _private: [u8; 0],
-}
-
-#[repr(C)]
-struct NativeRegion {
-    _private: [u8; 0],
-}
-
-#[repr(C)]
-struct NativeStructuringElement {
-    _private: [u8; 0],
-}
-
-#[repr(C)]
-struct NativeObstruction {
-    code: u32,
-    coordinate_error: u32,
-    input_index: u64,
-    first_index: u64,
-    second_index: u64,
-    first_value: f64,
-    second_value: f64,
-    point_x: f64,
-    point_y: f64,
-    message: [c_char; 256],
-}
-
-impl Default for NativeObstruction {
-    fn default() -> Self {
-        Self {
-            code: 0,
-            coordinate_error: 0,
-            input_index: u64::MAX,
-            first_index: 0,
-            second_index: 0,
-            first_value: 0.0,
-            second_value: 0.0,
-            point_x: 0.0,
-            point_y: 0.0,
-            message: [0; 256],
-        }
-    }
-}
-
-#[repr(C)]
-#[derive(Default)]
-struct NativeMinkowskiReceipt {
-    operation: u32,
-    reserved: u32,
-    input_components: u64,
-    convex_pieces: u64,
-    generated_pieces: u64,
-    generated_convolution_edges: u64,
-    overlay_passes: u64,
-    exact_crossings: u64,
-    output_cells: u64,
-    exact_coordinate_bit_growth: u64,
-}
-
-#[link(name = "moonlight-triangulation-c")]
-unsafe extern "C" {
-    fn ml_abi_version() -> c_uint;
-    fn ml_runtime_initialize() -> c_uint;
-    fn ml_delaunay_f64(
-        coordinates: *const c_double,
-        point_count: usize,
-        result: *mut *mut NativeMesh,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_insert_many_f64(
-        mesh: *const NativeMesh,
-        coordinates: *const c_double,
-        point_count: usize,
-        result: *mut *mut NativeMesh,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_site_union(
-        left: *const NativeMesh,
-        right: *const NativeMesh,
-        result: *mut *mut NativeMesh,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_site_intersection(
-        left: *const NativeMesh,
-        right: *const NativeMesh,
-        result: *mut *mut NativeMesh,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_site_difference(
-        left: *const NativeMesh,
-        right: *const NativeMesh,
-        result: *mut *mut NativeMesh,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_site_symmetric_difference(
-        left: *const NativeMesh,
-        right: *const NativeMesh,
-        result: *mut *mut NativeMesh,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_vertex_count(
-        mesh: *const NativeMesh,
-        count: *mut usize,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_triangle_count(
-        mesh: *const NativeMesh,
-        count: *mut usize,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_copy_vertices_f64(
-        mesh: *const NativeMesh,
-        coordinates: *mut c_double,
-        point_capacity: usize,
-        points_written: *mut usize,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_copy_triangles_u32(
-        mesh: *const NativeMesh,
-        triangles: *mut u32,
-        triangle_capacity: usize,
-        triangles_written: *mut usize,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_mesh_free(mesh: *mut NativeMesh);
-
-    fn ml_region_create_f64(
-        coordinates: *const c_double,
-        point_count: usize,
-        loop_point_counts: *const usize,
-        loop_count: usize,
-        component_loop_counts: *const usize,
-        component_count: usize,
-        result: *mut *mut NativeRegion,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_counts(
-        region: *const NativeRegion,
-        component_count: *mut usize,
-        loop_count: *mut usize,
-        point_count: *mut usize,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_copy_f64(
-        region: *const NativeRegion,
-        coordinates: *mut c_double,
-        point_capacity: usize,
-        loop_point_offsets: *mut usize,
-        loop_offset_capacity: usize,
-        component_loop_offsets: *mut usize,
-        component_offset_capacity: usize,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_union(
-        left: *const NativeRegion,
-        right: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_intersection(
-        left: *const NativeRegion,
-        right: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_difference(
-        left: *const NativeRegion,
-        right: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_symmetric_difference(
-        left: *const NativeRegion,
-        right: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_locate_point_f64(
-        region: *const NativeRegion,
-        x: c_double,
-        y: c_double,
-        location: *mut u32,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_measure(
-        region: *const NativeRegion,
-        euler_characteristic: *mut i64,
-        area_ratio_utf8: *mut c_char,
-        area_capacity: usize,
-        area_bytes_written: *mut usize,
-        perimeter_lower: *mut c_double,
-        perimeter_upper: *mut c_double,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_free(region: *mut NativeRegion);
-
-    fn ml_structuring_element_create_f64(
-        coordinates: *const c_double,
-        point_count: usize,
-        result: *mut *mut NativeStructuringElement,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_structuring_element_free(element: *mut NativeStructuringElement);
-    fn ml_region_minkowski_sum(
-        left: *const NativeRegion,
-        right: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        receipt: *mut NativeMinkowskiReceipt,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_offset(
-        element: *const NativeStructuringElement,
-        region: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        receipt: *mut NativeMinkowskiReceipt,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_inset(
-        element: *const NativeStructuringElement,
-        region: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        receipt: *mut NativeMinkowskiReceipt,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_open(
-        element: *const NativeStructuringElement,
-        region: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        receipt: *mut NativeMinkowskiReceipt,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-    fn ml_region_close(
-        element: *const NativeStructuringElement,
-        region: *const NativeRegion,
-        result: *mut *mut NativeRegion,
-        receipt: *mut NativeMinkowskiReceipt,
-        obstruction: *mut NativeObstruction,
-    ) -> c_uint;
-}
-
-type BinaryMeshOperation = unsafe extern "C" fn(
-    *const NativeMesh,
-    *const NativeMesh,
-    *mut *mut NativeMesh,
-    *mut NativeObstruction,
-) -> c_uint;
-
-type BinaryRegionOperation = unsafe extern "C" fn(
-    *const NativeRegion,
-    *const NativeRegion,
-    *mut *mut NativeRegion,
-    *mut NativeObstruction,
-) -> c_uint;
-
-type ElementRegionOperation = unsafe extern "C" fn(
-    *const NativeStructuringElement,
-    *const NativeRegion,
-    *mut *mut NativeRegion,
-    *mut NativeMinkowskiReceipt,
-    *mut NativeObstruction,
-) -> c_uint;
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct MoonlightError {
-    pub status: u32,
-    pub code: u32,
-    pub coordinate_error: u32,
-    pub input_index: u64,
-    pub first_index: u64,
-    pub second_index: u64,
-    pub first_value: f64,
-    pub second_value: f64,
-    pub point: [f64; 2],
-    pub message: String,
-}
-
-impl Display for MoonlightError {
-    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
-        write!(formatter, "{}", self.message)
-    }
-}
-
-impl Error for MoonlightError {}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct PolygonComponent {
-    pub outer: Vec<[f64; 2]>,
-    pub holes: Vec<Vec<[f64; 2]>>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RegionLocation {
-    Exterior,
-    Boundary,
-    Interior,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ExactRational {
-    pub numerator: String,
-    pub denominator: String,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct RegionValuations {
-    pub euler_characteristic: i64,
-    pub area: ExactRational,
-    pub perimeter_bounds: [f64; 2],
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum MinkowskiOperation {
-    Addition,
-    Erosion,
-    Opening,
-    Closing,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct MinkowskiReceipt {
-    pub operation: MinkowskiOperation,
-    pub input_components: u64,
-    pub convex_pieces: u64,
-    pub generated_pieces: u64,
-    pub generated_convolution_edges: u64,
-    pub overlay_passes: u64,
-    pub exact_crossings: u64,
-    pub output_cells: u64,
-    pub exact_coordinate_bit_growth: u64,
-}
-
-pub struct Moonlight;
-
-impl Moonlight {
-    pub fn initialize() -> Result<Self, MoonlightError> {
-        let status = unsafe { ml_runtime_initialize() };
-        if status != STATUS_OK {
-            return Err(synthetic_error(
-                status,
-                "Moonlight runtime initialization failed",
-                0,
-                0,
-            ));
-        }
-        let version = unsafe { ml_abi_version() };
-        if version != ABI_VERSION {
-            return Err(synthetic_error(
-                5,
-                format!("unsupported Moonlight ABI version {version}"),
-                u64::from(version),
-                u64::from(ABI_VERSION),
-            ));
-        }
-        Ok(Self)
-    }
-
-    pub fn delaunay(&self, points: &[[f64; 2]]) -> Result<Mesh, MoonlightError> {
-        let coordinates = flatten_points(points);
-        create_handle(|output, obstruction| unsafe {
-            ml_delaunay_f64(coordinates.as_ptr(), points.len(), output, obstruction)
-        })
-        .map(Mesh::from_handle)
-    }
-
-    pub fn region(&self, components: &[PolygonComponent]) -> Result<Region, MoonlightError> {
-        let loop_point_counts = components
-            .iter()
-            .flat_map(|component| std::iter::once(&component.outer).chain(component.holes.iter()))
-            .map(Vec::len)
-            .collect::<Vec<_>>();
-        let component_loop_counts = components
-            .iter()
-            .map(|component| component.holes.len() + 1)
-            .collect::<Vec<_>>();
-        let coordinates = components
-            .iter()
-            .flat_map(|component| std::iter::once(&component.outer).chain(component.holes.iter()))
-            .flat_map(|loop_points| loop_points.iter())
-            .flat_map(|[x, y]| [*x, *y])
-            .collect::<Vec<_>>();
-        create_handle(|output, obstruction| unsafe {
-            ml_region_create_f64(
-                coordinates.as_ptr(),
-                coordinates.len() / 2,
-                loop_point_counts.as_ptr(),
-                loop_point_counts.len(),
-                component_loop_counts.as_ptr(),
-                components.len(),
-                output,
-                obstruction,
-            )
-        })
-        .map(Region::from_handle)
-    }
-
-    pub fn structuring_element(
-        &self,
-        points: &[[f64; 2]],
-    ) -> Result<StructuringElement, MoonlightError> {
-        let coordinates = flatten_points(points);
-        create_handle(|output, obstruction| unsafe {
-            ml_structuring_element_create_f64(
-                coordinates.as_ptr(),
-                points.len(),
-                output,
-                obstruction,
-            )
-        })
-        .map(StructuringElement::from_handle)
-    }
-}
-
-pub struct Mesh {
-    handle: NonNull<NativeMesh>,
-    _thread_affinity: PhantomData<Rc<()>>,
-}
-
-impl Mesh {
-    fn from_handle(handle: NonNull<NativeMesh>) -> Self {
-        Self {
-            handle,
-            _thread_affinity: PhantomData,
-        }
-    }
-
-    pub fn vertex_count(&self) -> Result<usize, MoonlightError> {
-        self.count(ml_mesh_vertex_count)
-    }
-
-    pub fn triangle_count(&self) -> Result<usize, MoonlightError> {
-        self.count(ml_mesh_triangle_count)
-    }
-
-    pub fn vertices(&self) -> Result<Vec<[f64; 2]>, MoonlightError> {
-        let count = self.vertex_count()?;
-        let mut coordinates = vec![0.0; count * 2];
-        let mut written = 0;
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe {
-            ml_mesh_copy_vertices_f64(
-                self.handle.as_ptr(),
-                coordinates.as_mut_ptr(),
-                count,
-                &mut written,
-                &mut obstruction,
-            )
-        };
-        status_result(status, obstruction)?;
-        coordinates.truncate(written * 2);
-        coordinate_pairs(&coordinates)
-    }
-
-    pub fn triangles(&self) -> Result<Vec<[u32; 3]>, MoonlightError> {
-        let count = self.triangle_count()?;
-        let mut triangles = vec![0; count * 3];
-        let mut written = 0;
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe {
-            ml_mesh_copy_triangles_u32(
-                self.handle.as_ptr(),
-                triangles.as_mut_ptr(),
-                count,
-                &mut written,
-                &mut obstruction,
-            )
-        };
-        status_result(status, obstruction)?;
-        triangles.truncate(written * 3);
-        triangles
-            .chunks_exact(3)
-            .map(|triangle| {
-                Ok([
-                    *triangle.first().ok_or_else(projection_shape_error)?,
-                    *triangle.get(1).ok_or_else(projection_shape_error)?,
-                    *triangle.get(2).ok_or_else(projection_shape_error)?,
-                ])
-            })
-            .collect()
-    }
-
-    pub fn insert_many(&self, points: &[[f64; 2]]) -> Result<Self, MoonlightError> {
-        let coordinates = flatten_points(points);
-        create_handle(|output, obstruction| unsafe {
-            ml_mesh_insert_many_f64(
-                self.handle.as_ptr(),
-                coordinates.as_ptr(),
-                points.len(),
-                output,
-                obstruction,
-            )
-        })
-        .map(Self::from_handle)
-    }
-
-    pub fn site_union(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_mesh_site_union)
-    }
-
-    pub fn site_intersection(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_mesh_site_intersection)
-    }
-
-    pub fn site_difference(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_mesh_site_difference)
-    }
-
-    pub fn site_symmetric_difference(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_mesh_site_symmetric_difference)
-    }
-
-    fn binary(&self, other: &Self, operation: BinaryMeshOperation) -> Result<Self, MoonlightError> {
-        create_handle(|output, obstruction| unsafe {
-            operation(
-                self.handle.as_ptr(),
-                other.handle.as_ptr(),
-                output,
-                obstruction,
-            )
-        })
-        .map(Self::from_handle)
-    }
-
-    fn count(
-        &self,
-        operation: unsafe extern "C" fn(
-            *const NativeMesh,
-            *mut usize,
-            *mut NativeObstruction,
-        ) -> c_uint,
-    ) -> Result<usize, MoonlightError> {
-        let mut count = 0;
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe { operation(self.handle.as_ptr(), &mut count, &mut obstruction) };
-        status_result(status, obstruction)?;
-        Ok(count)
-    }
-}
-
-impl Drop for Mesh {
-    fn drop(&mut self) {
-        unsafe { ml_mesh_free(self.handle.as_ptr()) }
-    }
-}
-
-pub struct Region {
-    handle: NonNull<NativeRegion>,
-    _thread_affinity: PhantomData<Rc<()>>,
-}
-
-impl Region {
-    fn from_handle(handle: NonNull<NativeRegion>) -> Self {
-        Self {
-            handle,
-            _thread_affinity: PhantomData,
-        }
-    }
-
-    pub fn components(&self) -> Result<Vec<PolygonComponent>, MoonlightError> {
-        let (component_count, loop_count, point_count) = self.counts()?;
-        let mut coordinates = vec![0.0; point_count * 2];
-        let mut loop_point_offsets = vec![0; loop_count + 1];
-        let mut component_loop_offsets = vec![0; component_count + 1];
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe {
-            ml_region_copy_f64(
-                self.handle.as_ptr(),
-                coordinates.as_mut_ptr(),
-                point_count,
-                loop_point_offsets.as_mut_ptr(),
-                loop_point_offsets.len(),
-                component_loop_offsets.as_mut_ptr(),
-                component_loop_offsets.len(),
-                &mut obstruction,
-            )
-        };
-        status_result(status, obstruction)?;
-        let points = coordinate_pairs(&coordinates)?;
-        component_loop_offsets
-            .windows(2)
-            .map(|window| {
-                let (start, end) = offset_window(window)?;
-                if start >= end {
-                    return Err(projection_shape_error());
-                }
-                let outer = projected_loop(&points, &loop_point_offsets, start)?;
-                let holes = (start + 1..end)
-                    .map(|index| projected_loop(&points, &loop_point_offsets, index))
-                    .collect::<Result<Vec<_>, _>>()?;
-                Ok(PolygonComponent { outer, holes })
-            })
-            .collect()
-    }
-
-    pub fn valuations(&self) -> Result<RegionValuations, MoonlightError> {
-        self.measure_with_capacity(128)
-    }
-
-    pub fn locate(&self, [x, y]: [f64; 2]) -> Result<RegionLocation, MoonlightError> {
-        let mut location = 0;
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe {
-            ml_region_locate_point_f64(self.handle.as_ptr(), x, y, &mut location, &mut obstruction)
-        };
-        status_result(status, obstruction)?;
-        region_location(location)
-    }
-
-    pub fn union(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_region_union)
-    }
-
-    pub fn intersection(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_region_intersection)
-    }
-
-    pub fn difference(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_region_difference)
-    }
-
-    pub fn symmetric_difference(&self, other: &Self) -> Result<Self, MoonlightError> {
-        self.binary(other, ml_region_symmetric_difference)
-    }
-
-    pub fn minkowski_sum(&self, other: &Self) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
-        produce_morphology(|output, receipt, obstruction| unsafe {
-            ml_region_minkowski_sum(
-                self.handle.as_ptr(),
-                other.handle.as_ptr(),
-                output,
-                receipt,
-                obstruction,
-            )
-        })
-    }
-
-    pub fn offset(
-        &self,
-        element: &StructuringElement,
-    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
-        self.with_element(element, ml_region_offset)
-    }
-
-    pub fn inset(
-        &self,
-        element: &StructuringElement,
-    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
-        self.with_element(element, ml_region_inset)
-    }
-
-    pub fn open(
-        &self,
-        element: &StructuringElement,
-    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
-        self.with_element(element, ml_region_open)
-    }
-
-    pub fn close_with(
-        &self,
-        element: &StructuringElement,
-    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
-        self.with_element(element, ml_region_close)
-    }
-
-    fn binary(
-        &self,
-        other: &Self,
-        operation: BinaryRegionOperation,
-    ) -> Result<Self, MoonlightError> {
-        create_handle(|output, obstruction| unsafe {
-            operation(
-                self.handle.as_ptr(),
-                other.handle.as_ptr(),
-                output,
-                obstruction,
-            )
-        })
-        .map(Self::from_handle)
-    }
-
-    fn with_element(
-        &self,
-        element: &StructuringElement,
-        operation: ElementRegionOperation,
-    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
-        produce_morphology(|output, receipt, obstruction| unsafe {
-            operation(
-                element.handle.as_ptr(),
-                self.handle.as_ptr(),
-                output,
-                receipt,
-                obstruction,
-            )
-        })
-    }
-
-    fn counts(&self) -> Result<(usize, usize, usize), MoonlightError> {
-        let mut component_count = 0;
-        let mut loop_count = 0;
-        let mut point_count = 0;
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe {
-            ml_region_counts(
-                self.handle.as_ptr(),
-                &mut component_count,
-                &mut loop_count,
-                &mut point_count,
-                &mut obstruction,
-            )
-        };
-        status_result(status, obstruction)?;
-        Ok((component_count, loop_count, point_count))
-    }
-
-    fn measure_with_capacity(&self, capacity: usize) -> Result<RegionValuations, MoonlightError> {
-        let mut euler_characteristic = 0;
-        let mut area_ratio = vec![0; capacity];
-        let mut area_bytes_written = 0;
-        let mut perimeter_lower = 0.0;
-        let mut perimeter_upper = 0.0;
-        let mut obstruction = NativeObstruction::default();
-        let status = unsafe {
-            ml_region_measure(
-                self.handle.as_ptr(),
-                &mut euler_characteristic,
-                area_ratio.as_mut_ptr(),
-                capacity,
-                &mut area_bytes_written,
-                &mut perimeter_lower,
-                &mut perimeter_upper,
-                &mut obstruction,
-            )
-        };
-        if status == STATUS_BUFFER_TOO_SMALL && obstruction.code == 102 {
-            return self.measure_with_capacity(area_bytes_written + 1);
-        }
-        status_result(status, obstruction)?;
-        let area_bytes = area_ratio
-            .get(..area_bytes_written)
-            .ok_or_else(projection_shape_error)?
-            .iter()
-            .map(|byte| *byte as u8)
-            .collect::<Vec<_>>();
-        let area_text = String::from_utf8(area_bytes)
-            .map_err(|failure| synthetic_error(5, failure.to_string(), 0, 0))?;
-        let (numerator, denominator) = area_text
-            .split_once('/')
-            .ok_or_else(|| synthetic_error(5, "malformed exact-area ratio", 0, 0))?;
-        Ok(RegionValuations {
-            euler_characteristic,
-            area: ExactRational {
-                numerator: numerator.to_owned(),
-                denominator: denominator.to_owned(),
-            },
-            perimeter_bounds: [perimeter_lower, perimeter_upper],
-        })
-    }
-}
-
-impl Drop for Region {
-    fn drop(&mut self) {
-        unsafe { ml_region_free(self.handle.as_ptr()) }
-    }
-}
-
-pub struct StructuringElement {
-    handle: NonNull<NativeStructuringElement>,
-    _thread_affinity: PhantomData<Rc<()>>,
-}
-
-impl StructuringElement {
-    fn from_handle(handle: NonNull<NativeStructuringElement>) -> Self {
-        Self {
-            handle,
-            _thread_affinity: PhantomData,
-        }
-    }
-}
-
-impl Drop for StructuringElement {
-    fn drop(&mut self) {
-        unsafe { ml_structuring_element_free(self.handle.as_ptr()) }
-    }
-}
-
-fn flatten_points(points: &[[f64; 2]]) -> Vec<f64> {
-    points.iter().flat_map(|[x, y]| [*x, *y]).collect()
-}
-
-fn coordinate_pairs(coordinates: &[f64]) -> Result<Vec<[f64; 2]>, MoonlightError> {
-    coordinates
-        .chunks_exact(2)
-        .map(|point| {
-            Ok([
-                *point.first().ok_or_else(projection_shape_error)?,
-                *point.get(1).ok_or_else(projection_shape_error)?,
-            ])
-        })
-        .collect()
-}
-
-fn offset_window(window: &[usize]) -> Result<(usize, usize), MoonlightError> {
-    Ok((
-        *window.first().ok_or_else(projection_shape_error)?,
-        *window.get(1).ok_or_else(projection_shape_error)?,
-    ))
-}
-
-fn projected_loop(
-    points: &[[f64; 2]],
-    offsets: &[usize],
-    index: usize,
-) -> Result<Vec<[f64; 2]>, MoonlightError> {
-    let start = *offsets.get(index).ok_or_else(projection_shape_error)?;
-    let end = *offsets.get(index + 1).ok_or_else(projection_shape_error)?;
-    points
-        .get(start..end)
-        .map(<[_]>::to_vec)
-        .ok_or_else(projection_shape_error)
-}
-
-fn create_handle<Native>(
-    operation: impl FnOnce(*mut *mut Native, *mut NativeObstruction) -> u32,
-) -> Result<NonNull<Native>, MoonlightError> {
-    let mut output = std::ptr::null_mut();
-    let mut obstruction = NativeObstruction::default();
-    let status = operation(&mut output, &mut obstruction);
-    status_result(status, obstruction)?;
-    NonNull::new(output).ok_or_else(success_without_handle_error)
-}
-
-fn produce_morphology(
-    operation: impl FnOnce(
-        *mut *mut NativeRegion,
-        *mut NativeMinkowskiReceipt,
-        *mut NativeObstruction,
-    ) -> u32,
-) -> Result<(Region, MinkowskiReceipt), MoonlightError> {
-    let mut output = std::ptr::null_mut();
-    let mut receipt = NativeMinkowskiReceipt::default();
-    let mut obstruction = NativeObstruction::default();
-    let status = operation(&mut output, &mut receipt, &mut obstruction);
-    status_result(status, obstruction)?;
-    let handle = NonNull::new(output).ok_or_else(success_without_handle_error)?;
-    Ok((Region::from_handle(handle), minkowski_receipt(receipt)?))
-}
-
-fn minkowski_receipt(receipt: NativeMinkowskiReceipt) -> Result<MinkowskiReceipt, MoonlightError> {
-    Ok(MinkowskiReceipt {
-        operation: minkowski_operation(receipt.operation)?,
-        input_components: receipt.input_components,
-        convex_pieces: receipt.convex_pieces,
-        generated_pieces: receipt.generated_pieces,
-        generated_convolution_edges: receipt.generated_convolution_edges,
-        overlay_passes: receipt.overlay_passes,
-        exact_crossings: receipt.exact_crossings,
-        output_cells: receipt.output_cells,
-        exact_coordinate_bit_growth: receipt.exact_coordinate_bit_growth,
-    })
-}
-
-fn minkowski_operation(code: u32) -> Result<MinkowskiOperation, MoonlightError> {
-    match code {
-        0 => Ok(MinkowskiOperation::Addition),
-        1 => Ok(MinkowskiOperation::Erosion),
-        2 => Ok(MinkowskiOperation::Opening),
-        3 => Ok(MinkowskiOperation::Closing),
-        _ => Err(synthetic_error(
-            5,
-            format!("unknown morphology operation {code}"),
-            u64::from(code),
-            0,
-        )),
-    }
-}
-
-fn region_location(code: u32) -> Result<RegionLocation, MoonlightError> {
-    match code {
-        0 => Ok(RegionLocation::Exterior),
-        1 => Ok(RegionLocation::Boundary),
-        2 => Ok(RegionLocation::Interior),
-        _ => Err(synthetic_error(
-            5,
-            format!("unknown region location {code}"),
-            u64::from(code),
-            0,
-        )),
-    }
-}
-
-fn status_result(status: u32, obstruction: NativeObstruction) -> Result<(), MoonlightError> {
-    if status == STATUS_OK {
-        Ok(())
-    } else {
-        Err(MoonlightError::from_native(status, obstruction))
-    }
-}
-
-impl MoonlightError {
-    fn from_native(status: u32, obstruction: NativeObstruction) -> Self {
-        let message_end = obstruction
-            .message
-            .iter()
-            .position(|character| *character == 0)
-            .unwrap_or(obstruction.message.len());
-        let message_bytes = obstruction
-            .message
-            .get(..message_end)
-            .unwrap_or_default()
-            .iter()
-            .map(|character| *character as u8)
-            .collect::<Vec<_>>();
-        Self {
-            status,
-            code: obstruction.code,
-            coordinate_error: obstruction.coordinate_error,
-            input_index: obstruction.input_index,
-            first_index: obstruction.first_index,
-            second_index: obstruction.second_index,
-            first_value: obstruction.first_value,
-            second_value: obstruction.second_value,
-            point: [obstruction.point_x, obstruction.point_y],
-            message: String::from_utf8_lossy(&message_bytes).into_owned(),
-        }
-    }
-}
-
-fn projection_shape_error() -> MoonlightError {
-    synthetic_error(5, "Moonlight returned a malformed bulk projection", 0, 0)
-}
-
-fn success_without_handle_error() -> MoonlightError {
-    synthetic_error(5, "Moonlight returned success without a handle", 0, 0)
-}
-
-fn synthetic_error(
-    status: u32,
-    message: impl Into<String>,
-    first_index: u64,
-    second_index: u64,
-) -> MoonlightError {
-    MoonlightError {
-        status,
-        code: 103,
-        coordinate_error: 0,
-        input_index: u64::MAX,
-        first_index,
-        second_index,
-        first_value: 0.0,
-        second_value: 0.0,
-        point: [0.0, 0.0],
-        message: message.into(),
-    }
-}
diff --git a/bindings/rust/tests/binding.rs b/bindings/rust/tests/binding.rs
deleted file mode 100644
--- a/bindings/rust/tests/binding.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-use moonlight_triangulation::{
-    ExactRational, MinkowskiOperation, Moonlight, PolygonComponent, RegionLocation,
-};
-
-#[test]
-fn immutable_site_set_algebra_and_dense_projection() {
-    let engine = Moonlight::initialize().expect("runtime");
-    let left = engine
-        .delaunay(&[[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0]])
-        .expect("left mesh");
-    let right = engine
-        .delaunay(&[[2.0, 0.0], [4.0, 0.0], [2.0, 2.0], [4.0, 2.0]])
-        .expect("right mesh");
-    let union = left.site_union(&right).expect("site union");
-    let intersection = left.site_intersection(&right).expect("site intersection");
-    let difference = left.site_difference(&right).expect("site difference");
-    let symmetric = left
-        .site_symmetric_difference(&right)
-        .expect("site symmetric difference");
-    let extended = left
-        .insert_many(&[[1.0, 1.0], [3.0, 1.0]])
-        .expect("batch insertion");
-
-    assert_eq!(left.vertex_count().expect("left count"), 4);
-    assert_eq!(union.vertex_count().expect("union count"), 6);
-    assert_eq!(intersection.vertex_count().expect("intersection count"), 2);
-    assert_eq!(difference.vertex_count().expect("difference count"), 2);
-    assert_eq!(symmetric.vertex_count().expect("symmetric count"), 4);
-    assert_eq!(extended.vertex_count().expect("extended count"), 6);
-    assert_eq!(
-        left.vertices().expect("vertices").len(),
-        left.vertex_count().expect("vertex count")
-    );
-    assert_eq!(
-        left.triangles().expect("triangles").len(),
-        left.triangle_count().expect("triangle count")
-    );
-}
-
-#[test]
-fn exact_region_boolean_valuation_and_morphology() {
-    let engine = Moonlight::initialize().expect("runtime");
-    let left = engine
-        .region(&[PolygonComponent {
-            outer: vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]],
-            holes: vec![],
-        }])
-        .expect("left region");
-    let right = engine
-        .region(&[PolygonComponent {
-            outer: vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]],
-            holes: vec![],
-        }])
-        .expect("right region");
-    let intersection = left.intersection(&right).expect("intersection");
-    let symmetric = left
-        .symmetric_difference(&right)
-        .expect("symmetric difference");
-    let kernel = engine
-        .structuring_element(&[[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]])
-        .expect("structuring element");
-    let (offset, receipt) = left.offset(&kernel).expect("offset");
-
-    assert_eq!(left.components().expect("components").len(), 1);
-    assert_eq!(
-        intersection
-            .valuations()
-            .expect("intersection valuation")
-            .area,
-        ExactRational {
-            numerator: "2".to_owned(),
-            denominator: "1".to_owned(),
-        }
-    );
-    assert_eq!(
-        symmetric
-            .valuations()
-            .expect("symmetric valuation")
-            .euler_characteristic,
-        2
-    );
-    assert_eq!(
-        left.locate([1.0, 1.0]).expect("interior"),
-        RegionLocation::Interior
-    );
-    assert_eq!(
-        left.locate([0.0, 1.0]).expect("boundary"),
-        RegionLocation::Boundary
-    );
-    assert_eq!(
-        left.locate([3.0, 1.0]).expect("exterior"),
-        RegionLocation::Exterior
-    );
-    assert_eq!(
-        offset.valuations().expect("offset valuation").area,
-        ExactRational {
-            numerator: "9".to_owned(),
-            denominator: "1".to_owned(),
-        }
-    );
-    assert_eq!(receipt.operation, MinkowskiOperation::Addition);
-    assert!(receipt.generated_pieces >= 1);
-}
-
-#[test]
-fn invalid_coordinate_preserves_typed_witness() {
-    let engine = Moonlight::initialize().expect("runtime");
-    let failure = engine
-        .delaunay(&[[0.0, 0.0], [f64::NAN, 1.0], [1.0, 0.0]])
-        .err()
-        .expect("invalid coordinate refusal");
-    assert_eq!(failure.status, 4);
-    assert_eq!(failure.code, 1);
-    assert_eq!(failure.coordinate_error, 1);
-    assert_eq!(failure.input_index, 1);
-}
diff --git a/bindings/typescript/package.json b/bindings/typescript/package.json
deleted file mode 100644
--- a/bindings/typescript/package.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
-  "name": "@moonlight/triangulation",
-  "version": "2.0.0",
-  "description": "TypeScript bindings for Moonlight exact regions and immutable Delaunay meshes.",
-  "type": "module",
-  "main": "./dist/src/index.js",
-  "types": "./dist/src/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./dist/src/index.d.ts",
-      "import": "./dist/src/index.js"
-    }
-  },
-  "files": [
-    "dist/src"
-  ],
-  "scripts": {
-    "build": "tsc -p tsconfig.json",
-    "test": "node --test dist/test/binding.test.js"
-  },
-  "engines": {
-    "node": ">=20"
-  },
-  "dependencies": {
-    "koffi": "3.1.4"
-  },
-  "devDependencies": {
-    "@types/node": "26.2.0",
-    "typescript": "7.0.2"
-  },
-  "license": "MIT",
-  "packageManager": "pnpm@11.21.0"
-}
diff --git a/bindings/typescript/pnpm-workspace.yaml b/bindings/typescript/pnpm-workspace.yaml
deleted file mode 100644
--- a/bindings/typescript/pnpm-workspace.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-packages:
-  - .
-
-allowBuilds:
-  koffi: true
diff --git a/bindings/typescript/src/index.ts b/bindings/typescript/src/index.ts
deleted file mode 100644
--- a/bindings/typescript/src/index.ts
+++ /dev/null
@@ -1,759 +0,0 @@
-import koffi from "koffi";
-
-const STATUS_OK = 0;
-const STATUS_BUFFER_TOO_SMALL = 3;
-const ABI_VERSION = 2;
-const bindingCapability: unique symbol = Symbol("moonlight binding capability");
-type BindingCapability = typeof bindingCapability;
-
-export type Point = readonly [x: number, y: number];
-export type Triangle = readonly [first: number, second: number, third: number];
-
-export interface PolygonComponent {
-  readonly outer: readonly Point[];
-  readonly holes?: readonly (readonly Point[])[];
-}
-
-export enum RegionLocation {
-  Exterior = 0,
-  Boundary = 1,
-  Interior = 2,
-}
-
-export interface ExactRational {
-  readonly numerator: bigint;
-  readonly denominator: bigint;
-}
-
-export interface RegionValuations {
-  readonly eulerCharacteristic: bigint;
-  readonly area: ExactRational;
-  readonly perimeterBounds: readonly [lower: number, upper: number];
-}
-
-export enum MinkowskiOperation {
-  Addition = 0,
-  Erosion = 1,
-  Opening = 2,
-  Closing = 3,
-}
-
-export interface MinkowskiReceipt {
-  readonly operation: MinkowskiOperation;
-  readonly inputComponents: bigint;
-  readonly convexPieces: bigint;
-  readonly generatedPieces: bigint;
-  readonly generatedConvolutionEdges: bigint;
-  readonly overlayPasses: bigint;
-  readonly exactCrossings: bigint;
-  readonly outputCells: bigint;
-  readonly exactCoordinateBitGrowth: bigint;
-}
-
-type NativeHandle = object;
-type NativeInteger = number | bigint;
-type HandleOutput = Array<NativeHandle | null>;
-
-interface NativeObstruction {
-  code?: number;
-  coordinate_error?: number;
-  input_index?: NativeInteger;
-  first_index?: NativeInteger;
-  second_index?: NativeInteger;
-  first_value?: number;
-  second_value?: number;
-  point_x?: number;
-  point_y?: number;
-  message?: string | readonly number[];
-}
-
-interface NativeMinkowskiReceipt {
-  operation?: number;
-  reserved?: number;
-  input_components?: NativeInteger;
-  convex_pieces?: NativeInteger;
-  generated_pieces?: NativeInteger;
-  generated_convolution_edges?: NativeInteger;
-  overlay_passes?: NativeInteger;
-  exact_crossings?: NativeInteger;
-  output_cells?: NativeInteger;
-  exact_coordinate_bit_growth?: NativeInteger;
-}
-
-type BuildOperation = (coordinates: Float64Array, pointCount: number, output: HandleOutput, obstruction: NativeObstruction) => number;
-type InsertOperation = (mesh: NativeHandle, coordinates: Float64Array, pointCount: number, output: HandleOutput, obstruction: NativeObstruction) => number;
-type BinaryOperation = (left: NativeHandle, right: NativeHandle, output: HandleOutput, obstruction: NativeObstruction) => number;
-type CountOperation = (handle: NativeHandle, output: NativeInteger[], obstruction: NativeObstruction) => number;
-type MorphologyOperation = (
-  first: NativeHandle,
-  second: NativeHandle,
-  output: HandleOutput,
-  receipt: NativeMinkowskiReceipt,
-  obstruction: NativeObstruction,
-) => number;
-
-interface NativeApi {
-  readonly abiVersion: () => number;
-  readonly runtimeInitialize: () => number;
-  readonly delaunay: BuildOperation;
-  readonly insertMany: InsertOperation;
-  readonly meshSiteUnion: BinaryOperation;
-  readonly meshSiteIntersection: BinaryOperation;
-  readonly meshSiteDifference: BinaryOperation;
-  readonly meshSiteSymmetricDifference: BinaryOperation;
-  readonly vertexCount: CountOperation;
-  readonly triangleCount: CountOperation;
-  readonly copyVertices: (mesh: NativeHandle, coordinates: Float64Array, capacity: number, written: NativeInteger[], obstruction: NativeObstruction) => number;
-  readonly copyTriangles: (mesh: NativeHandle, triangles: Uint32Array, capacity: number, written: NativeInteger[], obstruction: NativeObstruction) => number;
-  readonly meshFree: (mesh: NativeHandle) => void;
-  readonly regionCreate: (
-    coordinates: Float64Array,
-    pointCount: number,
-    loopPointCounts: BigUint64Array,
-    loopCount: number,
-    componentLoopCounts: BigUint64Array,
-    componentCount: number,
-    output: HandleOutput,
-    obstruction: NativeObstruction,
-  ) => number;
-  readonly regionCounts: (
-    region: NativeHandle,
-    componentCount: NativeInteger[],
-    loopCount: NativeInteger[],
-    pointCount: NativeInteger[],
-    obstruction: NativeObstruction,
-  ) => number;
-  readonly regionCopy: (
-    region: NativeHandle,
-    coordinates: Float64Array,
-    pointCapacity: number,
-    loopPointOffsets: BigUint64Array,
-    loopOffsetCapacity: number,
-    componentLoopOffsets: BigUint64Array,
-    componentOffsetCapacity: number,
-    obstruction: NativeObstruction,
-  ) => number;
-  readonly regionUnion: BinaryOperation;
-  readonly regionIntersection: BinaryOperation;
-  readonly regionDifference: BinaryOperation;
-  readonly regionSymmetricDifference: BinaryOperation;
-  readonly regionLocate: (region: NativeHandle, x: number, y: number, location: number[], obstruction: NativeObstruction) => number;
-  readonly regionMeasure: (
-    region: NativeHandle,
-    euler: NativeInteger[],
-    areaRatio: Buffer,
-    areaCapacity: number,
-    areaBytes: NativeInteger[],
-    perimeterLower: number[],
-    perimeterUpper: number[],
-    obstruction: NativeObstruction,
-  ) => number;
-  readonly regionFree: (region: NativeHandle) => void;
-  readonly structuringElementCreate: BuildOperation;
-  readonly structuringElementFree: (element: NativeHandle) => void;
-  readonly regionMinkowskiSum: MorphologyOperation;
-  readonly regionOffset: MorphologyOperation;
-  readonly regionInset: MorphologyOperation;
-  readonly regionOpen: MorphologyOperation;
-  readonly regionClose: MorphologyOperation;
-}
-
-interface FinalizerState {
-  readonly free: (handle: NativeHandle) => void;
-  readonly handle: NativeHandle;
-}
-
-const handleFinalizer = new FinalizationRegistry<FinalizerState>(({ free, handle }) => free(handle));
-
-function requireBindingCapability(capability: BindingCapability): void {
-  if (capability !== bindingCapability) {
-    throw new Error("native binding capability is unavailable");
-  }
-}
-
-export class MoonlightError extends Error {
-  readonly status: number;
-  readonly code: number;
-  readonly coordinateError: number;
-  readonly inputIndex: bigint;
-  readonly firstIndex: bigint;
-  readonly secondIndex: bigint;
-  readonly firstValue: number;
-  readonly secondValue: number;
-  readonly point: Point;
-
-  constructor(status: number, obstruction: NativeObstruction) {
-    super(obstructionMessage(obstruction));
-    this.name = "MoonlightError";
-    this.status = status;
-    this.code = obstruction.code ?? 0;
-    this.coordinateError = obstruction.coordinate_error ?? 0;
-    this.inputIndex = toBigInt(obstruction.input_index);
-    this.firstIndex = toBigInt(obstruction.first_index);
-    this.secondIndex = toBigInt(obstruction.second_index);
-    this.firstValue = obstruction.first_value ?? 0;
-    this.secondValue = obstruction.second_value ?? 0;
-    this.point = [obstruction.point_x ?? 0, obstruction.point_y ?? 0];
-  }
-}
-
-export class Moonlight {
-  readonly #native: NativeApi;
-
-  constructor(libraryPath = process.env.MOONLIGHT_TRIANGULATION_LIBRARY) {
-    if (libraryPath === undefined || libraryPath.length === 0) {
-      throw new Error("set MOONLIGHT_TRIANGULATION_LIBRARY or pass libraryPath");
-    }
-    const native = createNativeApi(libraryPath);
-    const initializationStatus = native.runtimeInitialize();
-    if (initializationStatus !== STATUS_OK) {
-      throw new Error(`Moonlight runtime initialization failed with status ${initializationStatus}`);
-    }
-    const abiVersion = native.abiVersion();
-    if (abiVersion !== ABI_VERSION) {
-      throw new Error(`unsupported Moonlight ABI version ${abiVersion}`);
-    }
-    this.#native = native;
-  }
-
-  delaunay(points: readonly Point[]): Mesh {
-    const coordinates = flattenPoints(points);
-    return new Mesh(
-      bindingCapability,
-      this.#native,
-      produceHandle((output, obstruction) =>
-        this.#native.delaunay(coordinates, coordinates.length / 2, output, obstruction),
-      ),
-    );
-  }
-
-  region(components: readonly PolygonComponent[]): Region {
-    const loops = components.flatMap(({ outer, holes = [] }) => [outer, ...holes]);
-    const loopPointCounts = BigUint64Array.from(loops.map((loop) => BigInt(loop.length)));
-    const componentLoopCounts = BigUint64Array.from(
-      components.map(({ holes = [] }) => BigInt(holes.length + 1)),
-    );
-    const coordinates = flattenLoops(loops);
-    return new Region(
-      bindingCapability,
-      this.#native,
-      produceHandle((output, obstruction) =>
-        this.#native.regionCreate(
-          coordinates,
-          coordinates.length / 2,
-          loopPointCounts,
-          loopPointCounts.length,
-          componentLoopCounts,
-          componentLoopCounts.length,
-          output,
-          obstruction,
-        ),
-      ),
-    );
-  }
-
-  structuringElement(points: readonly Point[]): StructuringElement {
-    const coordinates = flattenPoints(points);
-    return new StructuringElement(
-      bindingCapability,
-      this.#native,
-      produceHandle((output, obstruction) =>
-        this.#native.structuringElementCreate(coordinates, coordinates.length / 2, output, obstruction),
-      ),
-    );
-  }
-}
-
-abstract class OwnedHandle {
-  readonly #native: NativeApi;
-  readonly #free: (handle: NativeHandle) => void;
-  readonly #kind: string;
-  #handle: NativeHandle | null;
-
-  protected constructor(
-    capability: BindingCapability,
-    native: NativeApi,
-    handle: NativeHandle,
-    free: (handle: NativeHandle) => void,
-    kind: string,
-  ) {
-    requireBindingCapability(capability);
-    this.#native = native;
-    this.#free = free;
-    this.#kind = kind;
-    this.#handle = handle;
-    handleFinalizer.register(this, { free, handle }, this);
-  }
-
-  close(): void {
-    const handle = this.#handle;
-    if (handle !== null) {
-      handleFinalizer.unregister(this);
-      this.#free(handle);
-      this.#handle = null;
-    }
-  }
-
-  protected native(capability: BindingCapability): NativeApi {
-    requireBindingCapability(capability);
-    return this.#native;
-  }
-
-  nativeHandle(capability: BindingCapability): NativeHandle {
-    requireBindingCapability(capability);
-    if (this.#handle === null) {
-      throw new Error(`${this.#kind} is closed`);
-    }
-    return this.#handle;
-  }
-
-  requireSameRuntime(other: OwnedHandle): void {
-    if (this.#native !== other.#native) {
-      throw new Error("both values must belong to the same Moonlight runtime");
-    }
-  }
-}
-
-export class Mesh extends OwnedHandle {
-  constructor(capability: BindingCapability, native: NativeApi, handle: NativeHandle) {
-    super(capability, native, handle, native.meshFree, "mesh");
-  }
-
-  vertexCount(): number {
-    return this.count(this.native(bindingCapability).vertexCount);
-  }
-
-  triangleCount(): number {
-    return this.count(this.native(bindingCapability).triangleCount);
-  }
-
-  vertices(): readonly Point[] {
-    const count = this.vertexCount();
-    const coordinates = new Float64Array(count * 2);
-    const written: NativeInteger[] = [0];
-    const obstruction: NativeObstruction = {};
-    const status = this.native(bindingCapability).copyVertices(this.nativeHandle(bindingCapability), coordinates, count, written, obstruction);
-    checkStatus(status, obstruction);
-    return Array.from({ length: toSafeNumber(written[0]) }, (_unused, index): Point => [
-      coordinates[index * 2] ?? 0,
-      coordinates[index * 2 + 1] ?? 0,
-    ]);
-  }
-
-  triangles(): readonly Triangle[] {
-    const count = this.triangleCount();
-    const triangles = new Uint32Array(count * 3);
-    const written: NativeInteger[] = [0];
-    const obstruction: NativeObstruction = {};
-    const status = this.native(bindingCapability).copyTriangles(this.nativeHandle(bindingCapability), triangles, count, written, obstruction);
-    checkStatus(status, obstruction);
-    return Array.from({ length: toSafeNumber(written[0]) }, (_unused, index): Triangle => [
-      triangles[index * 3] ?? 0,
-      triangles[index * 3 + 1] ?? 0,
-      triangles[index * 3 + 2] ?? 0,
-    ]);
-  }
-
-  insertMany(points: readonly Point[]): Mesh {
-    const coordinates = flattenPoints(points);
-    return new Mesh(
-      bindingCapability,
-      this.native(bindingCapability),
-      produceHandle((output, obstruction) =>
-        this.native(bindingCapability).insertMany(this.nativeHandle(bindingCapability), coordinates, coordinates.length / 2, output, obstruction),
-      ),
-    );
-  }
-
-  siteUnion(other: Mesh): Mesh {
-    return this.binary(other, this.native(bindingCapability).meshSiteUnion);
-  }
-
-  siteIntersection(other: Mesh): Mesh {
-    return this.binary(other, this.native(bindingCapability).meshSiteIntersection);
-  }
-
-  siteDifference(other: Mesh): Mesh {
-    return this.binary(other, this.native(bindingCapability).meshSiteDifference);
-  }
-
-  siteSymmetricDifference(other: Mesh): Mesh {
-    return this.binary(other, this.native(bindingCapability).meshSiteSymmetricDifference);
-  }
-
-  private binary(other: Mesh, operation: BinaryOperation): Mesh {
-    this.requireSameRuntime(other);
-    return new Mesh(
-      bindingCapability,
-      this.native(bindingCapability),
-      produceHandle((output, obstruction) =>
-        operation(this.nativeHandle(bindingCapability), other.nativeHandle(bindingCapability), output, obstruction),
-      ),
-    );
-  }
-
-  private count(operation: CountOperation): number {
-    const output: NativeInteger[] = [0];
-    const obstruction: NativeObstruction = {};
-    const status = operation(this.nativeHandle(bindingCapability), output, obstruction);
-    checkStatus(status, obstruction);
-    return toSafeNumber(output[0]);
-  }
-}
-
-export class Region extends OwnedHandle {
-  constructor(capability: BindingCapability, native: NativeApi, handle: NativeHandle) {
-    super(capability, native, handle, native.regionFree, "region");
-  }
-
-  components(): readonly PolygonComponent[] {
-    const [componentCount, loopCount, pointCount] = this.counts();
-    const coordinates = new Float64Array(pointCount * 2);
-    const loopPointOffsets = new BigUint64Array(loopCount + 1);
-    const componentLoopOffsets = new BigUint64Array(componentCount + 1);
-    const obstruction: NativeObstruction = {};
-    const status = this.native(bindingCapability).regionCopy(
-      this.nativeHandle(bindingCapability),
-      coordinates,
-      pointCount,
-      loopPointOffsets,
-      loopCount + 1,
-      componentLoopOffsets,
-      componentCount + 1,
-      obstruction,
-    );
-    checkStatus(status, obstruction);
-    const points = Array.from({ length: pointCount }, (_unused, index): Point => [
-      coordinates[index * 2] ?? 0,
-      coordinates[index * 2 + 1] ?? 0,
-    ]);
-    const loops = Array.from({ length: loopCount }, (_unused, index) =>
-      points.slice(toSafeNumber(loopPointOffsets[index]), toSafeNumber(loopPointOffsets[index + 1])),
-    );
-    return Array.from({ length: componentCount }, (_unused, index): PolygonComponent => {
-      const start = toSafeNumber(componentLoopOffsets[index]);
-      const end = toSafeNumber(componentLoopOffsets[index + 1]);
-      const outer = loops[start];
-      if (outer === undefined || start >= end) {
-        throw new Error("Moonlight returned a component without an outer loop");
-      }
-      return { outer, holes: loops.slice(start + 1, end) };
-    });
-  }
-
-  valuations(): RegionValuations {
-    return this.measureWithCapacity(128);
-  }
-
-  locate([x, y]: Point): RegionLocation {
-    const location = [0];
-    const obstruction: NativeObstruction = {};
-    const status = this.native(bindingCapability).regionLocate(this.nativeHandle(bindingCapability), x, y, location, obstruction);
-    checkStatus(status, obstruction);
-    return locationCode(location[0]);
-  }
-
-  union(other: Region): Region {
-    return this.binary(other, this.native(bindingCapability).regionUnion);
-  }
-
-  intersection(other: Region): Region {
-    return this.binary(other, this.native(bindingCapability).regionIntersection);
-  }
-
-  difference(other: Region): Region {
-    return this.binary(other, this.native(bindingCapability).regionDifference);
-  }
-
-  symmetricDifference(other: Region): Region {
-    return this.binary(other, this.native(bindingCapability).regionSymmetricDifference);
-  }
-
-  minkowskiSum(other: Region): readonly [Region, MinkowskiReceipt] {
-    this.requireSameRuntime(other);
-    return this.morph(
-      this.native(bindingCapability).regionMinkowskiSum,
-      this.nativeHandle(bindingCapability),
-      other.nativeHandle(bindingCapability),
-    );
-  }
-
-  offset(element: StructuringElement): readonly [Region, MinkowskiReceipt] {
-    return this.withElement(element, this.native(bindingCapability).regionOffset);
-  }
-
-  inset(element: StructuringElement): readonly [Region, MinkowskiReceipt] {
-    return this.withElement(element, this.native(bindingCapability).regionInset);
-  }
-
-  open(element: StructuringElement): readonly [Region, MinkowskiReceipt] {
-    return this.withElement(element, this.native(bindingCapability).regionOpen);
-  }
-
-  closeWith(element: StructuringElement): readonly [Region, MinkowskiReceipt] {
-    return this.withElement(element, this.native(bindingCapability).regionClose);
-  }
-
-  private binary(other: Region, operation: BinaryOperation): Region {
-    this.requireSameRuntime(other);
-    return new Region(
-      bindingCapability,
-      this.native(bindingCapability),
-      produceHandle((output, obstruction) =>
-        operation(this.nativeHandle(bindingCapability), other.nativeHandle(bindingCapability), output, obstruction),
-      ),
-    );
-  }
-
-  private withElement(
-    element: StructuringElement,
-    operation: MorphologyOperation,
-  ): readonly [Region, MinkowskiReceipt] {
-    this.requireSameRuntime(element);
-    return this.morph(
-      operation,
-      element.nativeHandle(bindingCapability),
-      this.nativeHandle(bindingCapability),
-    );
-  }
-
-  private morph(
-    operation: MorphologyOperation,
-    first: NativeHandle,
-    second: NativeHandle,
-  ): readonly [Region, MinkowskiReceipt] {
-    const [handle, receipt] = produceMorphology((output, nativeReceipt, obstruction) =>
-      operation(first, second, output, nativeReceipt, obstruction),
-    );
-    return [new Region(bindingCapability, this.native(bindingCapability), handle), receipt];
-  }
-
-  private counts(): readonly [number, number, number] {
-    const componentCount: NativeInteger[] = [0];
-    const loopCount: NativeInteger[] = [0];
-    const pointCount: NativeInteger[] = [0];
-    const obstruction: NativeObstruction = {};
-    const status = this.native(bindingCapability).regionCounts(
-      this.nativeHandle(bindingCapability),
-      componentCount,
-      loopCount,
-      pointCount,
-      obstruction,
-    );
-    checkStatus(status, obstruction);
-    return [toSafeNumber(componentCount[0]), toSafeNumber(loopCount[0]), toSafeNumber(pointCount[0])];
-  }
-
-  private measureWithCapacity(capacity: number): RegionValuations {
-    const euler: NativeInteger[] = [0];
-    const areaRatio = Buffer.alloc(capacity);
-    const areaBytes: NativeInteger[] = [0];
-    const perimeterLower = [0];
-    const perimeterUpper = [0];
-    const obstruction: NativeObstruction = {};
-    const status = this.native(bindingCapability).regionMeasure(
-      this.nativeHandle(bindingCapability),
-      euler,
-      areaRatio,
-      capacity,
-      areaBytes,
-      perimeterLower,
-      perimeterUpper,
-      obstruction,
-    );
-    if (status === STATUS_BUFFER_TOO_SMALL && obstruction.code === 102) {
-      return this.measureWithCapacity(toSafeNumber(areaBytes[0]) + 1);
-    }
-    checkStatus(status, obstruction);
-    const ratio = areaRatio.subarray(0, toSafeNumber(areaBytes[0])).toString("ascii");
-    const [numerator, denominator, remainder] = ratio.split("/");
-    if (numerator === undefined || denominator === undefined || remainder !== undefined) {
-      throw new Error("Moonlight returned a malformed exact-area ratio");
-    }
-    return {
-      eulerCharacteristic: toBigInt(euler[0]),
-      area: { numerator: BigInt(numerator), denominator: BigInt(denominator) },
-      perimeterBounds: [perimeterLower[0] ?? 0, perimeterUpper[0] ?? 0],
-    };
-  }
-}
-
-export class StructuringElement extends OwnedHandle {
-  constructor(capability: BindingCapability, native: NativeApi, handle: NativeHandle) {
-    super(capability, native, handle, native.structuringElementFree, "structuring element");
-  }
-}
-
-function createNativeApi(libraryPath: string): NativeApi {
-  const library = koffi.load(libraryPath);
-  const mesh = koffi.opaque();
-  const region = koffi.opaque();
-  const structuringElement = koffi.opaque();
-  const meshPointer = koffi.pointer(mesh);
-  const regionPointer = koffi.pointer(region);
-  const structuringElementPointer = koffi.pointer(structuringElement);
-  const meshOutput = koffi.out(koffi.pointer(mesh, 2));
-  const regionOutput = koffi.out(koffi.pointer(region, 2));
-  const structuringElementOutput = koffi.out(koffi.pointer(structuringElement, 2));
-  const obstruction = koffi.struct({
-    code: "uint32_t",
-    coordinate_error: "uint32_t",
-    input_index: "uint64_t",
-    first_index: "uint64_t",
-    second_index: "uint64_t",
-    first_value: "double",
-    second_value: "double",
-    point_x: "double",
-    point_y: "double",
-    message: koffi.array("char", 256),
-  });
-  const receipt = koffi.struct({
-    operation: "uint32_t",
-    reserved: "uint32_t",
-    input_components: "uint64_t",
-    convex_pieces: "uint64_t",
-    generated_pieces: "uint64_t",
-    generated_convolution_edges: "uint64_t",
-    overlay_passes: "uint64_t",
-    exact_crossings: "uint64_t",
-    output_cells: "uint64_t",
-    exact_coordinate_bit_growth: "uint64_t",
-  });
-  const obstructionOutput = koffi.out(koffi.pointer(obstruction));
-  const receiptOutput = koffi.out(koffi.pointer(receipt));
-  const sizeOutput = koffi.out(koffi.pointer("size_t"));
-  const uint32Output = koffi.out(koffi.pointer("uint32_t"));
-  const int64Output = koffi.out(koffi.pointer("int64_t"));
-  const doubleOutput = koffi.out(koffi.pointer("double"));
-  const sizeArray = koffi.pointer("size_t");
-  return {
-    abiVersion: library.func("ml_abi_version", "uint32_t", []),
-    runtimeInitialize: library.func("ml_runtime_initialize", "uint32_t", []),
-    delaunay: library.func("ml_delaunay_f64", "uint32_t", [koffi.pointer("double"), "size_t", meshOutput, obstructionOutput]),
-    insertMany: library.func("ml_mesh_insert_many_f64", "uint32_t", [meshPointer, koffi.pointer("double"), "size_t", meshOutput, obstructionOutput]),
-    meshSiteUnion: library.func("ml_mesh_site_union", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
-    meshSiteIntersection: library.func("ml_mesh_site_intersection", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
-    meshSiteDifference: library.func("ml_mesh_site_difference", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
-    meshSiteSymmetricDifference: library.func("ml_mesh_site_symmetric_difference", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
-    vertexCount: library.func("ml_mesh_vertex_count", "uint32_t", [meshPointer, sizeOutput, obstructionOutput]),
-    triangleCount: library.func("ml_mesh_triangle_count", "uint32_t", [meshPointer, sizeOutput, obstructionOutput]),
-    copyVertices: library.func("ml_mesh_copy_vertices_f64", "uint32_t", [meshPointer, koffi.out(koffi.pointer("double")), "size_t", sizeOutput, obstructionOutput]),
-    copyTriangles: library.func("ml_mesh_copy_triangles_u32", "uint32_t", [meshPointer, koffi.out(koffi.pointer("uint32_t")), "size_t", sizeOutput, obstructionOutput]),
-    meshFree: library.func("ml_mesh_free", "void", [meshPointer]),
-    regionCreate: library.func("ml_region_create_f64", "uint32_t", [koffi.pointer("double"), "size_t", sizeArray, "size_t", sizeArray, "size_t", regionOutput, obstructionOutput]),
-    regionCounts: library.func("ml_region_counts", "uint32_t", [regionPointer, sizeOutput, sizeOutput, sizeOutput, obstructionOutput]),
-    regionCopy: library.func("ml_region_copy_f64", "uint32_t", [regionPointer, koffi.out(koffi.pointer("double")), "size_t", koffi.out(sizeArray), "size_t", koffi.out(sizeArray), "size_t", obstructionOutput]),
-    regionUnion: library.func("ml_region_union", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
-    regionIntersection: library.func("ml_region_intersection", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
-    regionDifference: library.func("ml_region_difference", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
-    regionSymmetricDifference: library.func("ml_region_symmetric_difference", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
-    regionLocate: library.func("ml_region_locate_point_f64", "uint32_t", [regionPointer, "double", "double", uint32Output, obstructionOutput]),
-    regionMeasure: library.func("ml_region_measure", "uint32_t", [regionPointer, int64Output, koffi.out(koffi.pointer("char")), "size_t", sizeOutput, doubleOutput, doubleOutput, obstructionOutput]),
-    regionFree: library.func("ml_region_free", "void", [regionPointer]),
-    structuringElementCreate: library.func("ml_structuring_element_create_f64", "uint32_t", [koffi.pointer("double"), "size_t", structuringElementOutput, obstructionOutput]),
-    structuringElementFree: library.func("ml_structuring_element_free", "void", [structuringElementPointer]),
-    regionMinkowskiSum: library.func("ml_region_minkowski_sum", "uint32_t", [regionPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
-    regionOffset: library.func("ml_region_offset", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
-    regionInset: library.func("ml_region_inset", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
-    regionOpen: library.func("ml_region_open", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
-    regionClose: library.func("ml_region_close", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
-  };
-}
-
-function flattenPoints(points: readonly Point[]): Float64Array {
-  return Float64Array.from(points.flatMap(([x, y]) => [x, y]));
-}
-
-function flattenLoops(loops: readonly (readonly Point[])[]): Float64Array {
-  return Float64Array.from(loops.flatMap((loop) => loop.flatMap(([x, y]) => [x, y])));
-}
-
-function produceHandle(
-  operation: (output: HandleOutput, obstruction: NativeObstruction) => number,
-): NativeHandle {
-  const output: HandleOutput = [null];
-  const obstruction: NativeObstruction = {};
-  checkStatus(operation(output, obstruction), obstruction);
-  return requiredHandle(output[0]);
-}
-
-function produceMorphology(
-  operation: (
-    output: HandleOutput,
-    receipt: NativeMinkowskiReceipt,
-    obstruction: NativeObstruction,
-  ) => number,
-): readonly [NativeHandle, MinkowskiReceipt] {
-  const output: HandleOutput = [null];
-  const receipt: NativeMinkowskiReceipt = {};
-  const obstruction: NativeObstruction = {};
-  checkStatus(operation(output, receipt, obstruction), obstruction);
-  return [requiredHandle(output[0]), morphologyReceipt(receipt)];
-}
-
-function morphologyReceipt(receipt: NativeMinkowskiReceipt): MinkowskiReceipt {
-  return {
-    operation: operationCode(receipt.operation),
-    inputComponents: toBigInt(receipt.input_components),
-    convexPieces: toBigInt(receipt.convex_pieces),
-    generatedPieces: toBigInt(receipt.generated_pieces),
-    generatedConvolutionEdges: toBigInt(receipt.generated_convolution_edges),
-    overlayPasses: toBigInt(receipt.overlay_passes),
-    exactCrossings: toBigInt(receipt.exact_crossings),
-    outputCells: toBigInt(receipt.output_cells),
-    exactCoordinateBitGrowth: toBigInt(receipt.exact_coordinate_bit_growth),
-  };
-}
-
-function operationCode(value: number | undefined): MinkowskiOperation {
-  if (value === undefined || value < MinkowskiOperation.Addition || value > MinkowskiOperation.Closing) {
-    throw new Error(`Moonlight returned an unknown morphology operation ${value ?? "missing"}`);
-  }
-  return value;
-}
-
-function locationCode(value: number | undefined): RegionLocation {
-  if (value === undefined || value < RegionLocation.Exterior || value > RegionLocation.Interior) {
-    throw new Error(`Moonlight returned an unknown region location ${value ?? "missing"}`);
-  }
-  return value;
-}
-
-function checkStatus(status: number, obstruction: NativeObstruction): void {
-  if (status !== STATUS_OK) {
-    throw new MoonlightError(status, obstruction);
-  }
-}
-
-function requiredHandle(handle: NativeHandle | null | undefined): NativeHandle {
-  if (handle === null || handle === undefined) {
-    throw new Error("Moonlight returned success without a handle");
-  }
-  return handle;
-}
-
-function toBigInt(value: NativeInteger | undefined): bigint {
-  return value === undefined ? 0n : BigInt(value);
-}
-
-function toSafeNumber(value: NativeInteger | undefined): number {
-  const exact = toBigInt(value);
-  const projected = Number(exact);
-  if (!Number.isSafeInteger(projected)) {
-    throw new Error(`Moonlight count ${exact} exceeds JavaScript's safe integer range`);
-  }
-  return projected;
-}
-
-function obstructionMessage(obstruction: NativeObstruction): string {
-  const message = obstruction.message;
-  if (message === undefined) {
-    return `Moonlight ABI failure ${obstruction.code ?? 0}`;
-  }
-  if (typeof message === "string") {
-    return message.split("\0", 1)[0] ?? "";
-  }
-  const terminator = message.indexOf(0);
-  const length = terminator === -1 ? message.length : terminator;
-  return new TextDecoder().decode(Uint8Array.from(message.slice(0, length)));
-}
diff --git a/bindings/typescript/test/binding.test.ts b/bindings/typescript/test/binding.test.ts
deleted file mode 100644
--- a/bindings/typescript/test/binding.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import assert from "node:assert/strict";
-import test from "node:test";
-
-import {
-  MinkowskiOperation,
-  Moonlight,
-  MoonlightError,
-  RegionLocation,
-} from "../src/index.js";
-
-test("immutable site-set algebra and dense projection", () => {
-  const engine = new Moonlight();
-  const left = engine.delaunay([[0, 0], [2, 0], [0, 2], [2, 2]]);
-  const right = engine.delaunay([[2, 0], [4, 0], [2, 2], [4, 2]]);
-  const union = left.siteUnion(right);
-  const intersection = left.siteIntersection(right);
-  const difference = left.siteDifference(right);
-  const symmetric = left.siteSymmetricDifference(right);
-  const extended = left.insertMany([[1, 1], [3, 1]]);
-
-  assert.equal(left.vertexCount(), 4);
-  assert.equal(union.vertexCount(), 6);
-  assert.equal(intersection.vertexCount(), 2);
-  assert.equal(difference.vertexCount(), 2);
-  assert.equal(symmetric.vertexCount(), 4);
-  assert.equal(extended.vertexCount(), 6);
-  assert.equal(left.vertices().length, left.vertexCount());
-  assert.equal(left.triangles().length, left.triangleCount());
-
-  left.close();
-  right.close();
-  union.close();
-  intersection.close();
-  difference.close();
-  symmetric.close();
-  extended.close();
-});
-
-test("exact region Boolean, valuation, and morphology", () => {
-  const engine = new Moonlight();
-  const left = engine.region([{ outer: [[0, 0], [2, 0], [2, 2], [0, 2]] }]);
-  const right = engine.region([{ outer: [[1, 0], [3, 0], [3, 2], [1, 2]] }]);
-  const intersection = left.intersection(right);
-  const symmetric = left.symmetricDifference(right);
-  const kernel = engine.structuringElement([[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]]);
-  const [offset, receipt] = left.offset(kernel);
-
-  assert.equal(left.components().length, 1);
-  assert.equal(left.components()[0]?.outer.length, 4);
-  assert.deepEqual(intersection.valuations().area, { numerator: 2n, denominator: 1n });
-  assert.equal(symmetric.valuations().eulerCharacteristic, 2n);
-  assert.equal(left.locate([1, 1]), RegionLocation.Interior);
-  assert.equal(left.locate([0, 1]), RegionLocation.Boundary);
-  assert.equal(left.locate([3, 1]), RegionLocation.Exterior);
-  assert.deepEqual(offset.valuations().area, { numerator: 9n, denominator: 1n });
-  assert.equal(receipt.operation, MinkowskiOperation.Addition);
-  assert(receipt.generatedPieces >= 1n);
-
-  left.close();
-  right.close();
-  intersection.close();
-  symmetric.close();
-  kernel.close();
-  offset.close();
-});
-
-test("invalid coordinate preserves typed witness", () => {
-  const engine = new Moonlight();
-  assert.throws(
-    () => engine.delaunay([[0, 0], [Number.NaN, 1], [1, 0]]),
-    (failure: unknown) =>
-      failure instanceof MoonlightError &&
-      failure.status === 4 &&
-      failure.code === 1 &&
-      failure.coordinateError === 1 &&
-      failure.inputIndex === 1n,
-  );
-});
-
-test("native count descends from materialized coordinates", () => {
-  const engine = new Moonlight();
-  const points = new Proxy<readonly (readonly [number, number])[]>([[0, 0]], {
-    get(target, property, receiver) {
-      return property === "length" ? 1_000 : Reflect.get(target, property, receiver);
-    },
-  });
-  const mesh = engine.delaunay(points);
-  assert.equal(mesh.vertexCount(), 1);
-  mesh.close();
-});
-
-test("raw native handles require the module capability", () => {
-  const engine = new Moonlight();
-  const mesh = engine.delaunay([[0, 0]]);
-  const capabilityGuarded = mesh as unknown as {
-    nativeHandle(capability: symbol): object;
-  };
-  assert.throws(
-    () => capabilityGuarded.nativeHandle(Symbol("forged capability")),
-    /native binding capability is unavailable/,
-  );
-  mesh.close();
-});
diff --git a/bindings/typescript/tsconfig.json b/bindings/typescript/tsconfig.json
deleted file mode 100644
--- a/bindings/typescript/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "ES2023",
-    "module": "NodeNext",
-    "moduleResolution": "NodeNext",
-    "strict": true,
-    "noImplicitAny": true,
-    "strictNullChecks": true,
-    "noUncheckedIndexedAccess": true,
-    "exactOptionalPropertyTypes": true,
-    "types": [
-      "node"
-    ],
-    "declaration": true,
-    "rootDir": ".",
-    "outDir": "dist",
-    "skipLibCheck": false
-  },
-  "include": [
-    "src/**/*.ts",
-    "test/**/*.ts"
-  ]
-}
diff --git a/cbits/moonlight-triangulation.def b/cbits/moonlight-triangulation.def
deleted file mode 100644
--- a/cbits/moonlight-triangulation.def
+++ /dev/null
@@ -1,32 +0,0 @@
-LIBRARY moonlight-triangulation-c
-EXPORTS
-  ml_abi_version
-  ml_runtime_initialize
-  ml_delaunay_f64
-  ml_mesh_insert_many_f64
-  ml_mesh_site_union
-  ml_mesh_site_intersection
-  ml_mesh_site_difference
-  ml_mesh_site_symmetric_difference
-  ml_mesh_vertex_count
-  ml_mesh_triangle_count
-  ml_mesh_copy_vertices_f64
-  ml_mesh_copy_triangles_u32
-  ml_mesh_free
-  ml_region_create_f64
-  ml_region_counts
-  ml_region_copy_f64
-  ml_region_union
-  ml_region_intersection
-  ml_region_difference
-  ml_region_symmetric_difference
-  ml_region_locate_point_f64
-  ml_region_measure
-  ml_region_free
-  ml_structuring_element_create_f64
-  ml_structuring_element_free
-  ml_region_minkowski_sum
-  ml_region_offset
-  ml_region_inset
-  ml_region_open
-  ml_region_close
diff --git a/cbits/moonlight_runtime.c b/cbits/moonlight_runtime.c
deleted file mode 100644
--- a/cbits/moonlight_runtime.c
+++ /dev/null
@@ -1,55 +0,0 @@
-#include "moonlight_triangulation.h"
-#include "HsFFI.h"
-
-_Static_assert(ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE == 60, "maximum-edge ABI code drift");
-_Static_assert(ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE == 61, "maximum-edge ABI code drift");
-_Static_assert(ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE == 62, "maximum-edge ABI code drift");
-_Static_assert(ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED == 63, "seam-bridge ABI code drift");
-_Static_assert(ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED == 64, "seam-bridge ABI code drift");
-_Static_assert(ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE == 65, "boundary-split ABI code drift");
-
-#if defined(_WIN32)
-#include <windows.h>
-
-static INIT_ONCE moonlight_runtime_once = INIT_ONCE_STATIC_INIT;
-
-static BOOL CALLBACK moonlight_initialize_runtime(PINIT_ONCE once, PVOID parameter, PVOID *context) {
-  int argc = 1;
-  char program_name[] = "moonlight-triangulation";
-  char *argv[] = {program_name, NULL};
-  char **argv_pointer = argv;
-  (void)once;
-  (void)parameter;
-  (void)context;
-  hs_init(&argc, &argv_pointer);
-  return TRUE;
-}
-
-ML_API ml_status ml_runtime_initialize(void) {
-  return InitOnceExecuteOnce(&moonlight_runtime_once, moonlight_initialize_runtime, NULL, NULL)
-    ? ML_STATUS_OK
-    : ML_STATUS_RUNTIME_FAILURE;
-}
-#else
-#include <pthread.h>
-
-static pthread_once_t moonlight_runtime_once = PTHREAD_ONCE_INIT;
-
-static void moonlight_initialize_runtime(void) {
-  int argc = 1;
-  char program_name[] = "moonlight-triangulation";
-  char *argv[] = {program_name, NULL};
-  char **argv_pointer = argv;
-  hs_init(&argc, &argv_pointer);
-}
-
-ML_API ml_status ml_runtime_initialize(void) {
-  return pthread_once(&moonlight_runtime_once, moonlight_initialize_runtime) == 0
-    ? ML_STATUS_OK
-    : ML_STATUS_RUNTIME_FAILURE;
-}
-#endif
-
-ML_API uint32_t ml_abi_version(void) {
-  return 2;
-}
diff --git a/docs/README.md b/docs/README.md
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,29 +1,19 @@
-# Moonlight Triangulation exhibits
-
-These are the full-size visual artifacts shipped with `moonlight-triangulation`.
-They are derived observations of the package's existing geometry and Homology
-owners, not a second visualization data model.
-
-## Persistence rose
-
-![Animated Moonlight persistence rose](./social/moonlight-triangulation-persistence-rose.gif)
-
-[Open the animation directly.](./social/moonlight-triangulation-persistence-rose.gif)
-· [Open the static vector poster.](./moonlight-triangulation-persistence-rose.svg)
-· [Read its provenance and reproduction commands.](../README.md#persistence-rose)
-
-The GIF carries the source fingerprint of the package-owned 65-frame SVG
-section. `moonlight-triangulation-persistence-rose verify-artifacts` checks it
-beside byte-exact regeneration of both retained SVGs.
-
-## Alpha eclipse
-
-![Moonlight alpha eclipse](./moonlight-triangulation-alpha-eclipse.svg)
+# Moonlight Triangulation documentation
 
-[Open the full-size vector.](./moonlight-triangulation-alpha-eclipse.svg)
-· [Read the exact-filtration semantics.](../README.md#exact-alpha-filtration-and-persistent-topology)
+This page is navigation only. Each question below has one canonical owner;
+indexes and retained pictures are derived views of those owners.
 
-This is the real four-site square alpha complex: four resident boundary edges
-create one hole at exact `α² = 1`, then the resident diagonal and two resident
-faces kill it at exact `α² = 2`. No decorative centre site is smuggled into the
-final panel. Geometry is not improv theatre.
+| Question | Canonical owner |
+| --- | --- |
+| Which public operation should I call? | [Package operation index](../README.md#operations) |
+| What algebraic laws do meshes satisfy? | [Package algebraic contract](../README.md#algebraic-contract) |
+| What are the set-operation types? | [Package type signatures](../README.md#set-operation-types) |
+| Which execution schedule publishes each result? | [Mesh publication guide](./mesh-publication.md) |
+| How do I build and validate components locally? | [Development guide](./development.md) |
+| Which workflows are compile-checked? | [Example index](./examples/README.md) |
+| How does the C ABI serve Python, TypeScript, and Rust? | [Foreign bindings](../ffi/bindings/README.md) |
+| How are exact alpha filtrations lowered into persistence? | [Persistence rose](./persistence-rose/README.md) |
+| What does one finite alpha interval look like? | [Alpha eclipse](./persistence-rose/alpha-eclipse.md) |
+| How does an exact cell set become an incidence category and nerve? | [Category observatory](./category-observatory/README.md) |
+| How does Moonlight compare on Spade's construction suite? | [Delaunay construction comparison](./delaunay-compare/README.md) |
+| How does Moonlight compare across the broader operation surface? | [Moonlight–Spade operation comparison](../bench/spade-compare/README.md) |
diff --git a/docs/category-observatory/Main.hs b/docs/category-observatory/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Main.hs
@@ -0,0 +1,160 @@
+module Main (main) where
+
+import Control.Exception (IOException, try)
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Except
+  ( ExceptT (..),
+    except,
+    runExceptT,
+  )
+import Data.Aeson qualified as Aeson
+import Data.Bifunctor (first)
+import Data.ByteString.Lazy.Char8 qualified as LazyByteString
+import Moonlight.Triangulation.Exhibit.CategoryObservatory
+  ( Exhibit,
+    ExhibitError,
+    buildExhibit,
+    renderExhibitError,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Picture
+  ( PictureObstruction,
+    categoryObservatoryPictureFileName,
+    renderCategoryObservatoryPicture,
+    renderPictureObstruction,
+  )
+import System.Directory (createDirectoryIfMissing)
+import System.Environment (getArgs)
+import System.Exit (die)
+import System.FilePath ((</>), takeDirectory)
+
+data CategoryObservatoryCommand
+  = ExportJson
+  | WritePicture !FilePath
+  | VerifyArtifacts !FilePath
+
+data CategoryObservatoryCommandError
+  = CategoryObservatoryUsage ![String]
+  | CategoryObservatoryConstructionFailed !ExhibitError
+  | CategoryObservatoryPictureFailed !PictureObstruction
+  | CategoryObservatoryDirectoryFailed !FilePath !IOException
+  | CategoryObservatoryReadFailed !FilePath !IOException
+  | CategoryObservatoryWriteFailed !FilePath !IOException
+  | CategoryObservatoryArtifactMismatch !FilePath
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  result <- runExceptT (runCategoryObservatoryCommand arguments)
+  either (die . renderCommandError) pure result
+
+runCategoryObservatoryCommand ::
+  [String] ->
+  ExceptT CategoryObservatoryCommandError IO ()
+runCategoryObservatoryCommand arguments = do
+  command <- except (parseCommand arguments)
+  exhibit <- requireExhibit
+  case command of
+    ExportJson -> lift (LazyByteString.putStrLn (Aeson.encode exhibit))
+    WritePicture outputPath -> do
+      picture <- requirePicture exhibit
+      createOutputDirectory (takeDirectory outputPath)
+      writeOutput outputPath picture
+      lift (putStrLn ("wrote category-observatory picture " <> outputPath))
+    VerifyArtifacts packageRoot -> do
+      picture <- requirePicture exhibit
+      let picturePath =
+            packageRoot
+              </> "docs"
+              </> "category-observatory"
+              </> categoryObservatoryPictureFileName
+      actualPicture <- readOutput picturePath
+      except
+        ( if actualPicture == picture
+            then Right ()
+            else Left (CategoryObservatoryArtifactMismatch picturePath)
+        )
+      lift (putStrLn ("verified category-observatory picture " <> picturePath))
+
+parseCommand ::
+  [String] ->
+  Either CategoryObservatoryCommandError CategoryObservatoryCommand
+parseCommand arguments =
+  case arguments of
+    [] -> Right ExportJson
+    ["picture", outputPath] -> Right (WritePicture outputPath)
+    ["verify-artifacts", packageRoot] -> Right (VerifyArtifacts packageRoot)
+    _ -> Left (CategoryObservatoryUsage arguments)
+
+requireExhibit :: ExceptT CategoryObservatoryCommandError IO Exhibit
+requireExhibit =
+  except (first CategoryObservatoryConstructionFailed buildExhibit)
+
+requirePicture ::
+  Exhibit ->
+  ExceptT CategoryObservatoryCommandError IO String
+requirePicture exhibit =
+  except
+    ( first
+        CategoryObservatoryPictureFailed
+        (renderCategoryObservatoryPicture exhibit)
+    )
+
+createOutputDirectory ::
+  FilePath ->
+  ExceptT CategoryObservatoryCommandError IO ()
+createOutputDirectory outputDirectory =
+  liftIOException
+    (CategoryObservatoryDirectoryFailed outputDirectory)
+    (createDirectoryIfMissing True outputDirectory)
+
+readOutput :: FilePath -> ExceptT CategoryObservatoryCommandError IO String
+readOutput inputPath =
+  liftIOException
+    (CategoryObservatoryReadFailed inputPath)
+    (readFile inputPath)
+
+writeOutput ::
+  FilePath ->
+  String ->
+  ExceptT CategoryObservatoryCommandError IO ()
+writeOutput outputPath contents =
+  liftIOException
+    (CategoryObservatoryWriteFailed outputPath)
+    (writeFile outputPath contents)
+
+liftIOException ::
+  (IOException -> CategoryObservatoryCommandError) ->
+  IO value ->
+  ExceptT CategoryObservatoryCommandError IO value
+liftIOException wrapFailure =
+  ExceptT . fmap (first wrapFailure) . try
+
+renderCommandError :: CategoryObservatoryCommandError -> String
+renderCommandError commandError =
+  case commandError of
+    CategoryObservatoryUsage arguments ->
+      "invalid category-observatory command "
+        <> show arguments
+        <> "; expected no arguments (JSON) | picture OUTPUT.svg | verify-artifacts PACKAGE_ROOT"
+    CategoryObservatoryConstructionFailed exhibitError ->
+      renderExhibitError exhibitError
+    CategoryObservatoryPictureFailed pictureError ->
+      renderPictureObstruction pictureError
+    CategoryObservatoryDirectoryFailed path caughtError ->
+      "could not create category-observatory output directory "
+        <> show path
+        <> ": "
+        <> show caughtError
+    CategoryObservatoryReadFailed path caughtError ->
+      "could not read category-observatory artifact "
+        <> show path
+        <> ": "
+        <> show caughtError
+    CategoryObservatoryWriteFailed path caughtError ->
+      "could not write category-observatory artifact "
+        <> show path
+        <> ": "
+        <> show caughtError
+    CategoryObservatoryArtifactMismatch path ->
+      "tracked category-observatory artifact differs from its canonical renderer: "
+        <> show path
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory.hs
@@ -0,0 +1,96 @@
+module Moonlight.Triangulation.Exhibit.CategoryObservatory
+  ( Exhibit,
+    ExhibitError,
+    buildExhibit,
+    renderExhibitError,
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.Vector qualified as Vector
+import Moonlight.Homology.Pure.Topology.CellCategory (complexCategory)
+import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
+import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
+import Moonlight.Triangulation.CellComplex
+  ( DCELComplex,
+    fromExactCellSet,
+  )
+import Moonlight.Triangulation.CellSet
+  ( ExactCellSet,
+    closeFaceCellSet,
+    exactCellSetEdgeCount,
+    exactCellSetFaceCount,
+    exactCellSetVertexCount,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Category
+  ( projectCategory,
+    validateCategoryEnumeration,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry (projectMesh)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( CellInventory (..),
+    Exhibit (..),
+    ExhibitError (..),
+    renderExhibitError,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Nerve
+  ( projectNerve,
+    validateNerveCount,
+  )
+import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
+import Moonlight.Triangulation.Types (Point (..))
+
+buildExhibit :: Either ExhibitError Exhibit
+buildExhibit = do
+  triangulation <- first ExhibitTriangulationBuildFailed (delaunayGeometry exhibitPoints)
+  selectedCells <-
+    first ExhibitCellSelectionFailed
+      (closeFaceCellSet triangulation (innerFaces triangulation))
+  let complexValue = fromExactCellSet selectedCells
+      categoryValue = complexCategory complexValue
+  validateInventory selectedCells complexValue
+  validateCategoryEnumeration complexValue categoryValue
+  categoryProjection <- projectCategory triangulation categoryValue
+  nerveProjection <- projectNerve categoryValue
+  validateNerveCount complexValue nerveProjection
+  pure
+    Exhibit
+      { schemaVersion = 2,
+        exhibitTitle = "Triangulation, incidence category, and flag nerve",
+        mesh = projectMesh triangulation complexValue,
+        category = categoryProjection,
+        nerve = nerveProjection
+      }
+
+validateInventory :: ExactCellSet -> DCELComplex -> Either ExhibitError ()
+validateInventory cellSet complexValue =
+  let expectedInventory =
+        CellInventory
+          { cellVertexCount = exactCellSetVertexCount cellSet,
+            cellEdgeCount = exactCellSetEdgeCount cellSet,
+            cellFaceCount = exactCellSetFaceCount cellSet
+          }
+      observedInventory =
+        CellInventory
+          { cellVertexCount = length (vertices complexValue),
+            cellEdgeCount = length (edges complexValue),
+            cellFaceCount = length (faces complexValue)
+          }
+   in if observedInventory == expectedInventory
+        then Right ()
+        else Left (ExhibitInventoryMismatch expectedInventory observedInventory)
+
+exhibitPoints :: Vector.Vector Point
+exhibitPoints =
+  Vector.fromList
+    [ Point 80 72,
+      Point 222 48,
+      Point 362 92,
+      Point 52 208,
+      Point 176 166,
+      Point 310 190,
+      Point 414 232,
+      Point 112 330,
+      Point 258 314,
+      Point 380 354
+    ]
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Category.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Category.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Category.hs
@@ -0,0 +1,127 @@
+module Moonlight.Triangulation.Exhibit.CategoryObservatory.Category
+  ( incidenceFlagCount,
+    projectCategory,
+    validateCategoryEnumeration,
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.Containers.ListUtils (nubOrd)
+import Moonlight.Category
+  ( Category (..),
+    FiniteComposableCategory (..),
+  )
+import Moonlight.Homology.Pure.Topology.CellCategory
+  ( ComplexCat,
+    ComplexMor (..),
+    ComplexOb,
+  )
+import Moonlight.Homology.Pure.Topology.CellComplex
+  ( CellComplex2D (..),
+    CellTypes (..),
+    OrientedEdge (..),
+  )
+import Moonlight.Triangulation.CellComplex (DCELComplex)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry (positionForObject)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
+  ( morphismKind,
+    objectIdentifier,
+    objectKind,
+    objectLabel,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( CategoryMorphism (..),
+    CategoryObject (..),
+    CategoryProjection (..),
+    ExhibitError (..),
+    MorphismEndpoint (..),
+    MorphismInventory (..),
+  )
+import Moonlight.Triangulation.Types (DelaunayTriangulation)
+
+projectCategory ::
+  DelaunayTriangulation () ->
+  ComplexCat DCELComplex ->
+  Either ExhibitError CategoryProjection
+projectCategory triangulation categoryValue =
+  CategoryProjection
+    <$> traverse
+      (projectCategoryObject triangulation)
+      (enumerateObjects categoryValue)
+    <*> traverse
+      (projectCategoryMorphism categoryValue)
+      (zip [0 ..] (enumerateMorphisms categoryValue))
+
+projectCategoryObject ::
+  DelaunayTriangulation () ->
+  ComplexOb DCELComplex ->
+  Either ExhibitError CategoryObject
+projectCategoryObject triangulation objectValue =
+  CategoryObject
+    (objectIdentifier objectValue)
+    (objectKind objectValue)
+    (objectLabel objectValue)
+    <$> positionForObject triangulation objectValue
+
+projectCategoryMorphism ::
+  ComplexCat DCELComplex ->
+  (Int, ComplexMor DCELComplex) ->
+  Either ExhibitError CategoryMorphism
+projectCategoryMorphism categoryValue (morphismIndex, morphismValue) =
+  CategoryMorphism
+    ("morphism-" <> show morphismIndex)
+    (morphismKind morphismValue)
+    <$> projectedEndpoint MorphismSource source
+    <*> projectedEndpoint MorphismTarget target
+  where
+    projectedEndpoint endpoint endpointProjection =
+      objectIdentifier
+        <$> first
+          (const (ExhibitMorphismEndpointUnavailable endpoint morphismIndex))
+          (endpointProjection categoryValue morphismValue)
+
+validateCategoryEnumeration ::
+  DCELComplex ->
+  ComplexCat DCELComplex ->
+  Either ExhibitError ()
+validateCategoryEnumeration complexValue categoryValue =
+  let morphisms = enumerateMorphisms categoryValue
+      expectedInventory =
+        MorphismInventory
+          { identityMorphismCount =
+              length (vertices complexValue)
+                + length (edges complexValue)
+                + length (faces complexValue),
+            faceEdgeMorphismCount =
+              sum (fmap (length . faceBoundary complexValue) (faces complexValue)),
+            edgeVertexMorphismCount =
+              sum (fmap (edgeEndpointCount complexValue) (edges complexValue)),
+            faceVertexMorphismCount =
+              sum (fmap (incidenceFlagCount complexValue) (faces complexValue))
+          }
+      observedInventory =
+        foldMap morphismInventory morphisms
+   in if observedInventory == expectedInventory
+        then Right ()
+        else Left (ExhibitCategoryEnumerationMismatch expectedInventory observedInventory)
+
+edgeEndpointCount :: DCELComplex -> Edge DCELComplex -> Int
+edgeEndpointCount complexValue edgeValue =
+  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
+   in length (nubOrd [sourceVertex, targetVertex])
+
+incidenceFlagCount :: DCELComplex -> Face DCELComplex -> Int
+incidenceFlagCount complexValue faceValue =
+  sum
+    ( fmap
+        (edgeEndpointCount complexValue . orientedEdge)
+        (faceBoundary complexValue faceValue)
+    )
+
+morphismInventory :: ComplexMor DCELComplex -> MorphismInventory
+morphismInventory morphismValue =
+  case morphismValue of
+    IdentityMor _ -> MorphismInventory 1 0 0 0
+    FaceToEdge {} -> MorphismInventory 0 1 0 0
+    EdgeToVertex _ _ -> MorphismInventory 0 0 1 0
+    FaceToVertex {} -> MorphismInventory 0 0 0 1
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Geometry.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Geometry.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Geometry.hs
@@ -0,0 +1,127 @@
+module Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry
+  ( positionForObject,
+    projectMesh,
+  )
+where
+
+import Data.Foldable (foldMap')
+import Moonlight.Homology.Pure.Topology.CellCategory (ComplexOb (..))
+import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
+import Moonlight.Triangulation.CellComplex (DCELComplex)
+import Moonlight.Triangulation.Dcel
+  ( faceVertices,
+    undirectedEndpoints,
+    vertexPoint,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
+  ( edgeIdentifier,
+    edgeObjectIdentifier,
+    faceIdentifier,
+    faceObjectIdentifier,
+    vertexIdentifier,
+    vertexObjectIdentifier,
+  )
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( ExhibitError (..),
+    MeshEdge (..),
+    MeshFace (..),
+    MeshProjection (..),
+    MeshVertex (..),
+    Position (..),
+  )
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( FaceId,
+    UndirectedEdgeId,
+    VertexId,
+  )
+import Moonlight.Triangulation.Types
+  ( DelaunayTriangulation,
+    Point (..),
+  )
+
+projectMesh :: DelaunayTriangulation () -> DCELComplex -> MeshProjection
+projectMesh triangulation complexValue =
+  MeshProjection
+    { meshVertices = fmap (projectMeshVertex triangulation) (vertices complexValue),
+      meshEdges = fmap (projectMeshEdge triangulation) (edges complexValue),
+      meshFaces = fmap (projectMeshFace triangulation) (faces complexValue)
+    }
+
+projectMeshVertex :: DelaunayTriangulation () -> VertexId -> MeshVertex
+projectMeshVertex triangulation vertexValue =
+  MeshVertex
+    { meshVertexIdentifier = vertexIdentifier vertexValue,
+      meshVertexObjectIdentifier = vertexObjectIdentifier vertexValue,
+      meshVertexPosition = pointPosition (vertexPoint triangulation vertexValue)
+    }
+
+projectMeshEdge :: DelaunayTriangulation () -> UndirectedEdgeId -> MeshEdge
+projectMeshEdge triangulation edgeValue =
+  let (sourceVertex, targetVertex) = undirectedEndpoints triangulation edgeValue
+   in MeshEdge
+        { meshEdgeIdentifier = edgeIdentifier edgeValue,
+          meshEdgeObjectIdentifier = edgeObjectIdentifier edgeValue,
+          meshEdgeVertexIdentifiers = fmap vertexIdentifier [sourceVertex, targetVertex]
+        }
+
+projectMeshFace :: DelaunayTriangulation () -> FaceId -> MeshFace
+projectMeshFace triangulation faceValue =
+  MeshFace
+    { meshFaceIdentifier = faceIdentifier faceValue,
+      meshFaceObjectIdentifier = faceObjectIdentifier faceValue,
+      meshFaceVertexIdentifiers = fmap vertexIdentifier (faceVertices triangulation faceValue)
+    }
+
+positionForObject ::
+  DelaunayTriangulation () ->
+  ComplexOb DCELComplex ->
+  Either ExhibitError Position
+positionForObject triangulation objectValue =
+  case objectValue of
+    VertexOb vertexValue ->
+      Right (pointPosition (vertexPoint triangulation vertexValue))
+    EdgeOb edgeValue ->
+      let (sourceVertex, targetVertex) = undirectedEndpoints triangulation edgeValue
+       in Right
+            ( midpoint
+                (pointPosition (vertexPoint triangulation sourceVertex))
+                (pointPosition (vertexPoint triangulation targetVertex))
+            )
+    FaceOb faceValue ->
+      centroid faceValue
+        (fmap (pointPosition . vertexPoint triangulation) (faceVertices triangulation faceValue))
+
+data PositionSum = PositionSum Double Double
+
+instance Semigroup PositionSum where
+  PositionSum leftX leftY <> PositionSum rightX rightY =
+    PositionSum (leftX + rightX) (leftY + rightY)
+
+instance Monoid PositionSum where
+  mempty = PositionSum 0 0
+
+centroid :: FaceId -> [Position] -> Either ExhibitError Position
+centroid faceValue positions =
+  case positions of
+    [] -> Left (ExhibitFaceWithoutVertices faceValue)
+    _ ->
+      let PositionSum summedX summedY =
+            foldMap'
+              (\positionValue -> PositionSum (positionX positionValue) (positionY positionValue))
+              positions
+          positionCount = fromIntegral (length positions)
+       in Right (Position (summedX / positionCount) (summedY / positionCount))
+
+midpoint :: Position -> Position -> Position
+midpoint firstPosition secondPosition =
+  Position
+    { positionX = (positionX firstPosition + positionX secondPosition) / 2,
+      positionY = (positionY firstPosition + positionY secondPosition) / 2
+    }
+
+pointPosition :: Point -> Position
+pointPosition pointValue =
+  Position
+    { positionX = pointX pointValue,
+      positionY = pointY pointValue
+    }
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Identifier.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Identifier.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Identifier.hs
@@ -0,0 +1,78 @@
+module Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
+  ( objectIdentifier,
+    objectKind,
+    objectLabel,
+    morphismKind,
+    vertexIdentifier,
+    edgeIdentifier,
+    faceIdentifier,
+    vertexObjectIdentifier,
+    edgeObjectIdentifier,
+    faceObjectIdentifier,
+  )
+where
+
+import Moonlight.Homology.Pure.Topology.CellCategory
+  ( ComplexMor (..),
+    ComplexOb (..),
+  )
+import Moonlight.Triangulation.CellComplex (DCELComplex)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( CategoryMorphismKind (..),
+    CategoryObjectKind (..),
+  )
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( FaceId,
+    UndirectedEdgeId,
+    VertexId,
+    unFaceId,
+    unUndirectedEdgeId,
+    unVertexId,
+  )
+
+objectIdentifier :: ComplexOb DCELComplex -> String
+objectIdentifier objectValue =
+  case objectValue of
+    VertexOb vertexValue -> vertexObjectIdentifier vertexValue
+    EdgeOb edgeValue -> edgeObjectIdentifier edgeValue
+    FaceOb faceValue -> faceObjectIdentifier faceValue
+
+objectKind :: ComplexOb DCELComplex -> CategoryObjectKind
+objectKind objectValue =
+  case objectValue of
+    VertexOb _ -> VertexObject
+    EdgeOb _ -> EdgeObject
+    FaceOb _ -> FaceObject
+
+objectLabel :: ComplexOb DCELComplex -> String
+objectLabel objectValue =
+  case objectValue of
+    VertexOb vertexValue -> "v" <> show (unVertexId vertexValue)
+    EdgeOb edgeValue -> "e" <> show (unUndirectedEdgeId edgeValue)
+    FaceOb faceValue -> "f" <> show (unFaceId faceValue)
+
+morphismKind :: ComplexMor DCELComplex -> CategoryMorphismKind
+morphismKind morphismValue =
+  case morphismValue of
+    IdentityMor _ -> IdentityMorphism
+    FaceToEdge {} -> FaceEdgeMorphism
+    EdgeToVertex _ _ -> EdgeVertexMorphism
+    FaceToVertex {} -> FaceVertexComposite
+
+vertexIdentifier :: VertexId -> String
+vertexIdentifier vertexValue = "vertex-" <> show (unVertexId vertexValue)
+
+edgeIdentifier :: UndirectedEdgeId -> String
+edgeIdentifier edgeValue = "edge-" <> show (unUndirectedEdgeId edgeValue)
+
+faceIdentifier :: FaceId -> String
+faceIdentifier faceValue = "face-" <> show (unFaceId faceValue)
+
+vertexObjectIdentifier :: VertexId -> String
+vertexObjectIdentifier vertexValue = "object-" <> vertexIdentifier vertexValue
+
+edgeObjectIdentifier :: UndirectedEdgeId -> String
+edgeObjectIdentifier edgeValue = "object-" <> edgeIdentifier edgeValue
+
+faceObjectIdentifier :: FaceId -> String
+faceObjectIdentifier faceValue = "object-" <> faceIdentifier faceValue
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Model.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Model.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Model.hs
@@ -0,0 +1,188 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( Exhibit (..),
+    MeshProjection (..),
+    MeshVertex (..),
+    MeshEdge (..),
+    MeshFace (..),
+    Position (..),
+    CategoryProjection (..),
+    CategoryObject (..),
+    CategoryObjectKind (..),
+    CategoryMorphism (..),
+    CategoryMorphismKind (..),
+    NerveProjection (..),
+    NerveSimplexCount (..),
+    NerveTwoSimplex (..),
+    CellInventory (..),
+    MorphismInventory (..),
+    ExhibitError (..),
+    MorphismEndpoint (..),
+    renderExhibitError,
+  )
+where
+
+import Data.Aeson (ToJSON)
+import GHC.Generics (Generic)
+import Moonlight.Triangulation.CellSet (CellSelectionError)
+import Moonlight.Triangulation.Handles.HandleDefs (FaceId)
+import Moonlight.Triangulation.Types (BuildError)
+import Numeric.Natural (Natural)
+
+data Exhibit = Exhibit
+  { schemaVersion :: Int,
+    exhibitTitle :: String,
+    mesh :: MeshProjection,
+    category :: CategoryProjection,
+    nerve :: NerveProjection
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshProjection = MeshProjection
+  { meshVertices :: [MeshVertex],
+    meshEdges :: [MeshEdge],
+    meshFaces :: [MeshFace]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshVertex = MeshVertex
+  { meshVertexIdentifier :: String,
+    meshVertexObjectIdentifier :: String,
+    meshVertexPosition :: Position
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshEdge = MeshEdge
+  { meshEdgeIdentifier :: String,
+    meshEdgeObjectIdentifier :: String,
+    meshEdgeVertexIdentifiers :: [String]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshFace = MeshFace
+  { meshFaceIdentifier :: String,
+    meshFaceObjectIdentifier :: String,
+    meshFaceVertexIdentifiers :: [String]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data Position = Position
+  { positionX :: Double,
+    positionY :: Double
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryProjection = CategoryProjection
+  { categoryObjects :: [CategoryObject],
+    categoryMorphisms :: [CategoryMorphism]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryObject = CategoryObject
+  { categoryObjectIdentifier :: String,
+    categoryObjectKind :: CategoryObjectKind,
+    categoryObjectLabel :: String,
+    categoryObjectPosition :: Position
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryObjectKind
+  = VertexObject
+  | EdgeObject
+  | FaceObject
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryMorphism = CategoryMorphism
+  { categoryMorphismIdentifier :: String,
+    categoryMorphismKind :: CategoryMorphismKind,
+    categoryMorphismSourceObject :: String,
+    categoryMorphismTargetObject :: String
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryMorphismKind
+  = IdentityMorphism
+  | FaceEdgeMorphism
+  | EdgeVertexMorphism
+  | FaceVertexComposite
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data NerveProjection = NerveProjection
+  { nerveTwoSimplices :: [NerveTwoSimplex],
+    nerveSimplexCounts :: [NerveSimplexCount]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data NerveSimplexCount = NerveSimplexCount
+  { nerveDimension :: Natural,
+    normalizedSimplexCount :: Int,
+    unnormalizedSimplexCount :: Int
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data NerveTwoSimplex = NerveTwoSimplex
+  { nerveSimplexIdentifier :: String,
+    nerveSimplexObjectIdentifiers :: [String]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CellInventory = CellInventory
+  { cellVertexCount :: Int,
+    cellEdgeCount :: Int,
+    cellFaceCount :: Int
+  }
+  deriving stock (Eq, Show)
+
+data MorphismInventory = MorphismInventory
+  { identityMorphismCount :: Int,
+    faceEdgeMorphismCount :: Int,
+    edgeVertexMorphismCount :: Int,
+    faceVertexMorphismCount :: Int
+  }
+  deriving stock (Eq, Show)
+
+instance Semigroup MorphismInventory where
+  MorphismInventory leftIdentities leftFaceEdges leftEdgeVertices leftFaceVertices
+    <> MorphismInventory rightIdentities rightFaceEdges rightEdgeVertices rightFaceVertices =
+      MorphismInventory
+        (leftIdentities + rightIdentities)
+        (leftFaceEdges + rightFaceEdges)
+        (leftEdgeVertices + rightEdgeVertices)
+        (leftFaceVertices + rightFaceVertices)
+
+instance Monoid MorphismInventory where
+  mempty = MorphismInventory 0 0 0 0
+
+data ExhibitError
+  = ExhibitTriangulationBuildFailed BuildError
+  | ExhibitCellSelectionFailed CellSelectionError
+  | ExhibitMorphismEndpointUnavailable MorphismEndpoint Int
+  | ExhibitFaceWithoutVertices FaceId
+  | ExhibitMalformedTwoSimplex Int
+  | ExhibitInventoryMismatch CellInventory CellInventory
+  | ExhibitCategoryEnumerationMismatch MorphismInventory MorphismInventory
+  | ExhibitNerveCountMismatch Int Int
+  deriving stock (Show)
+
+data MorphismEndpoint
+  = MorphismSource
+  | MorphismTarget
+  deriving stock (Show)
+
+renderExhibitError :: ExhibitError -> String
+renderExhibitError = show
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Nerve.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Nerve.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Nerve.hs
@@ -0,0 +1,84 @@
+module Moonlight.Triangulation.Exhibit.CategoryObservatory.Nerve
+  ( projectNerve,
+    validateNerveCount,
+  )
+where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Moonlight.Category (chainVertices)
+import Moonlight.Category.Simplicial
+  ( GeneratedSSet,
+    NerveSimplex,
+    TruncatedNormalizedSSet,
+    generatedSimplicesAtDimension,
+    nerveSimplexChain,
+    normalizedNerve,
+    simplicesAtDimension,
+    unnormalizedNerve,
+  )
+import Moonlight.Homology.Pure.Topology.CellCategory (ComplexCat)
+import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
+import Moonlight.Triangulation.CellComplex (DCELComplex)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Category (incidenceFlagCount)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier (objectIdentifier)
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( ExhibitError (..),
+    NerveProjection (..),
+    NerveSimplexCount (..),
+    NerveTwoSimplex (..),
+  )
+import Numeric.Natural (Natural)
+
+projectNerve :: ComplexCat DCELComplex -> Either ExhibitError NerveProjection
+projectNerve categoryValue =
+  let normalizedNerveValue = normalizedNerve categoryValue nerveObservationBound
+      unnormalizedNerveValue = unnormalizedNerve categoryValue nerveObservationBound
+   in NerveProjection
+        <$> traverse
+          projectTwoSimplex
+          (zip [0 ..] (simplicesAtDimension normalizedNerveValue nerveObservationBound))
+        <*> pure
+          ( fmap
+              (projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue)
+              nerveObservationDimensions
+          )
+
+validateNerveCount :: DCELComplex -> NerveProjection -> Either ExhibitError ()
+validateNerveCount complexValue nerveProjection =
+  let expectedCount = sum (fmap (incidenceFlagCount complexValue) (faces complexValue))
+      observedCount = length (nerveTwoSimplices nerveProjection)
+   in if observedCount == expectedCount
+        then Right ()
+        else Left (ExhibitNerveCountMismatch expectedCount observedCount)
+
+projectNerveSimplexCount ::
+  TruncatedNormalizedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
+  GeneratedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
+  Natural ->
+  NerveSimplexCount
+projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue dimensionValue =
+  NerveSimplexCount
+    { nerveDimension = dimensionValue,
+      normalizedSimplexCount = length (simplicesAtDimension normalizedNerveValue dimensionValue),
+      unnormalizedSimplexCount = length (generatedSimplicesAtDimension unnormalizedNerveValue dimensionValue)
+    }
+
+projectTwoSimplex ::
+  (Int, NerveSimplex (ComplexCat DCELComplex)) ->
+  Either ExhibitError NerveTwoSimplex
+projectTwoSimplex (simplexIndex, simplexValue) =
+  case chainVertices (nerveSimplexChain simplexValue) of
+    faceObject :| [edgeObject, vertexObject] ->
+      Right
+        NerveTwoSimplex
+          { nerveSimplexIdentifier = "simplex-" <> show simplexIndex,
+            nerveSimplexObjectIdentifiers =
+              fmap objectIdentifier [faceObject, edgeObject, vertexObject]
+          }
+    _ -> Left (ExhibitMalformedTwoSimplex simplexIndex)
+
+nerveObservationBound :: Natural
+nerveObservationBound = 2
+
+nerveObservationDimensions :: [Natural]
+nerveObservationDimensions = [0, 1, 2]
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Picture.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Picture.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Picture.hs
@@ -0,0 +1,663 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | Pure, compact SVG projection of the checked category-observatory exhibit.
+module Moonlight.Triangulation.Exhibit.CategoryObservatory.Picture
+  ( PictureObstruction (..),
+    categoryObservatoryPictureFileName,
+    renderCategoryObservatoryPicture,
+    renderPictureObstruction,
+  )
+where
+
+import Data.Map.Strict qualified as Map
+import Data.Set qualified as Set
+import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+  ( CategoryObject (..),
+    CategoryObjectKind (..),
+    CategoryProjection (..),
+    Exhibit (..),
+    MeshEdge (..),
+    MeshFace (..),
+    MeshProjection (..),
+    MeshVertex (..),
+    NerveProjection (..),
+    NerveSimplexCount (..),
+    NerveTwoSimplex (..),
+    Position (..),
+  )
+import Numeric (showFFloat)
+
+data PictureObstruction
+  = PictureHasNoMeshVertices
+  | PictureHasNoNerveTwoSimplex
+  | PictureMissingMeshVertex !String
+  | PictureMissingCategoryObject !String
+  | PictureMalformedMeshEdge !String ![String]
+  | PictureMalformedMeshFace !String ![String]
+  | PictureMalformedNerveTwoSimplex !String ![String]
+  | PictureMalformedFlagKinds ![CategoryObjectKind]
+  deriving stock (Eq, Show)
+
+data PictureBounds = PictureBounds
+  { minimumX :: !Double,
+    minimumY :: !Double,
+    maximumX :: !Double,
+    maximumY :: !Double
+  }
+
+data PictureFrame = PictureFrame
+  { frameLeft :: !Double,
+    frameTop :: !Double,
+    frameWidth :: !Double,
+    frameHeight :: !Double
+  }
+
+data ResolvedMeshEdge = ResolvedMeshEdge
+  { resolvedEdge :: !MeshEdge,
+    resolvedEdgeSource :: !Position,
+    resolvedEdgeTarget :: !Position
+  }
+
+data ResolvedMeshFace = ResolvedMeshFace
+  { resolvedFace :: !MeshFace,
+    resolvedFaceVertices :: ![Position]
+  }
+
+data ResolvedNerveSimplex = ResolvedNerveSimplex
+  { resolvedSimplex :: !NerveTwoSimplex,
+    resolvedSimplexVertices :: ![Position]
+  }
+
+data FlagSelection = FlagSelection
+  { selectedSimplexIdentifier :: !String,
+    selectedFace :: !CategoryObject,
+    selectedEdge :: !CategoryObject,
+    selectedVertex :: !CategoryObject,
+    selectedObjectIdentifiers :: !(Set.Set String)
+  }
+
+data PictureContext = PictureContext
+  { contextExhibit :: !Exhibit,
+    contextBounds :: !PictureBounds,
+    contextEdges :: ![ResolvedMeshEdge],
+    contextFaces :: ![ResolvedMeshFace],
+    contextNerveSimplices :: ![ResolvedNerveSimplex],
+    contextFlag :: !FlagSelection
+  }
+
+categoryObservatoryPictureFileName :: FilePath
+categoryObservatoryPictureFileName =
+  "moonlight-triangulation-category-observatory.svg"
+
+renderCategoryObservatoryPicture :: Exhibit -> Either PictureObstruction String
+renderCategoryObservatoryPicture exhibit =
+  renderPicture <$> preparePicture exhibit
+
+renderPictureObstruction :: PictureObstruction -> String
+renderPictureObstruction = \case
+  PictureHasNoMeshVertices ->
+    "category-observatory picture requires at least one mesh vertex"
+  PictureHasNoNerveTwoSimplex ->
+    "category-observatory picture requires at least one admitted two-simplex"
+  PictureMissingMeshVertex identifier ->
+    "category-observatory picture cannot resolve mesh vertex " <> show identifier
+  PictureMissingCategoryObject identifier ->
+    "category-observatory picture cannot resolve category object " <> show identifier
+  PictureMalformedMeshEdge identifier vertices ->
+    "category-observatory mesh edge "
+      <> show identifier
+      <> " does not name exactly two vertices: "
+      <> show vertices
+  PictureMalformedMeshFace identifier vertices ->
+    "category-observatory mesh face "
+      <> show identifier
+      <> " does not name exactly three vertices: "
+      <> show vertices
+  PictureMalformedNerveTwoSimplex identifier objects ->
+    "category-observatory nerve simplex "
+      <> show identifier
+      <> " does not name one face, edge, and vertex: "
+      <> show objects
+  PictureMalformedFlagKinds kinds ->
+    "category-observatory selected flag has incompatible object kinds: " <> show kinds
+
+preparePicture :: Exhibit -> Either PictureObstruction PictureContext
+preparePicture exhibit = do
+  let meshProjection = mesh exhibit
+      categoryProjection = category exhibit
+      nerveProjection = nerve exhibit
+      meshVerticesValue = meshVertices meshProjection
+      vertexPositions =
+        Map.fromList
+          ( fmap
+              (\vertex -> (meshVertexIdentifier vertex, meshVertexPosition vertex))
+              meshVerticesValue
+          )
+      categoryObjectsByIdentifier =
+        Map.fromList
+          ( fmap
+              (\objectValue -> (categoryObjectIdentifier objectValue, objectValue))
+              (categoryObjects categoryProjection)
+          )
+  pictureBounds <- boundsFromPositions (meshVertexPosition <$> meshVerticesValue)
+  resolvedEdges <- traverse (resolveMeshEdge vertexPositions) (meshEdges meshProjection)
+  resolvedFaces <- traverse (resolveMeshFace vertexPositions) (meshFaces meshProjection)
+  resolvedSimplices <-
+    traverse
+      (resolveNerveSimplex categoryObjectsByIdentifier)
+      (nerveTwoSimplices nerveProjection)
+  flagSelection <- selectFlag categoryObjectsByIdentifier (nerveTwoSimplices nerveProjection)
+  pure
+    PictureContext
+      { contextExhibit = exhibit,
+        contextBounds = pictureBounds,
+        contextEdges = resolvedEdges,
+        contextFaces = resolvedFaces,
+        contextNerveSimplices = resolvedSimplices,
+        contextFlag = flagSelection
+      }
+
+boundsFromPositions :: [Position] -> Either PictureObstruction PictureBounds
+boundsFromPositions positions =
+  case positions of
+    [] -> Left PictureHasNoMeshVertices
+    firstPosition : remainingPositions ->
+      Right
+        ( foldl'
+            extendBounds
+            ( PictureBounds
+                (positionX firstPosition)
+                (positionY firstPosition)
+                (positionX firstPosition)
+                (positionY firstPosition)
+            )
+            remainingPositions
+        )
+
+extendBounds :: PictureBounds -> Position -> PictureBounds
+extendBounds bounds positionValue =
+  PictureBounds
+    { minimumX = min (minimumX bounds) (positionX positionValue),
+      minimumY = min (minimumY bounds) (positionY positionValue),
+      maximumX = max (maximumX bounds) (positionX positionValue),
+      maximumY = max (maximumY bounds) (positionY positionValue)
+    }
+
+resolveMeshEdge ::
+  Map.Map String Position ->
+  MeshEdge ->
+  Either PictureObstruction ResolvedMeshEdge
+resolveMeshEdge vertexPositions edgeValue =
+  case meshEdgeVertexIdentifiers edgeValue of
+    [sourceIdentifier, targetIdentifier] ->
+      ResolvedMeshEdge edgeValue
+        <$> lookupMeshVertex vertexPositions sourceIdentifier
+        <*> lookupMeshVertex vertexPositions targetIdentifier
+    identifiers ->
+      Left (PictureMalformedMeshEdge (meshEdgeIdentifier edgeValue) identifiers)
+
+resolveMeshFace ::
+  Map.Map String Position ->
+  MeshFace ->
+  Either PictureObstruction ResolvedMeshFace
+resolveMeshFace vertexPositions faceValue =
+  case meshFaceVertexIdentifiers faceValue of
+    [firstIdentifier, secondIdentifier, thirdIdentifier] ->
+      ResolvedMeshFace faceValue
+        <$> traverse
+          (lookupMeshVertex vertexPositions)
+          [firstIdentifier, secondIdentifier, thirdIdentifier]
+    identifiers ->
+      Left (PictureMalformedMeshFace (meshFaceIdentifier faceValue) identifiers)
+
+lookupMeshVertex ::
+  Map.Map String Position ->
+  String ->
+  Either PictureObstruction Position
+lookupMeshVertex vertexPositions identifier =
+  maybe
+    (Left (PictureMissingMeshVertex identifier))
+    Right
+    (Map.lookup identifier vertexPositions)
+
+resolveNerveSimplex ::
+  Map.Map String CategoryObject ->
+  NerveTwoSimplex ->
+  Either PictureObstruction ResolvedNerveSimplex
+resolveNerveSimplex categoryObjectsByIdentifier simplexValue =
+  case nerveSimplexObjectIdentifiers simplexValue of
+    [faceIdentifier, edgeIdentifier, vertexIdentifier] ->
+      ResolvedNerveSimplex simplexValue
+        . fmap categoryObjectPosition
+        <$> traverse
+          (lookupCategoryObject categoryObjectsByIdentifier)
+          [faceIdentifier, edgeIdentifier, vertexIdentifier]
+    identifiers ->
+      Left
+        ( PictureMalformedNerveTwoSimplex
+            (nerveSimplexIdentifier simplexValue)
+            identifiers
+        )
+
+selectFlag ::
+  Map.Map String CategoryObject ->
+  [NerveTwoSimplex] ->
+  Either PictureObstruction FlagSelection
+selectFlag categoryObjectsByIdentifier simplices =
+  case simplices of
+    [] -> Left PictureHasNoNerveTwoSimplex
+    simplexValue : _ ->
+      case nerveSimplexObjectIdentifiers simplexValue of
+        identifiers@[faceIdentifier, edgeIdentifier, vertexIdentifier] -> do
+          faceObject <- lookupCategoryObject categoryObjectsByIdentifier faceIdentifier
+          edgeObject <- lookupCategoryObject categoryObjectsByIdentifier edgeIdentifier
+          vertexObject <- lookupCategoryObject categoryObjectsByIdentifier vertexIdentifier
+          let kinds = categoryObjectKind <$> [faceObject, edgeObject, vertexObject]
+          case kinds of
+            [FaceObject, EdgeObject, VertexObject] ->
+              Right
+                FlagSelection
+                  { selectedSimplexIdentifier = nerveSimplexIdentifier simplexValue,
+                    selectedFace = faceObject,
+                    selectedEdge = edgeObject,
+                    selectedVertex = vertexObject,
+                    selectedObjectIdentifiers = Set.fromList identifiers
+                  }
+            _ -> Left (PictureMalformedFlagKinds kinds)
+        identifiers ->
+          Left
+            ( PictureMalformedNerveTwoSimplex
+                (nerveSimplexIdentifier simplexValue)
+                identifiers
+            )
+
+lookupCategoryObject ::
+  Map.Map String CategoryObject ->
+  String ->
+  Either PictureObstruction CategoryObject
+lookupCategoryObject categoryObjectsByIdentifier identifier =
+  maybe
+    (Left (PictureMissingCategoryObject identifier))
+    Right
+    (Map.lookup identifier categoryObjectsByIdentifier)
+
+renderPicture :: PictureContext -> String
+renderPicture context =
+  unlines
+    ( renderDocumentOpening
+        <> renderHeader context
+        <> renderPanels context
+        <> renderNerveProfile context
+        <> ["</svg>"]
+    )
+
+renderDocumentOpening :: [String]
+renderDocumentOpening =
+  [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>",
+    "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 920\" role=\"img\" aria-labelledby=\"title description\">",
+    "<title id=\"title\">Moonlight category observatory</title>",
+    "<desc id=\"description\">One checked Delaunay section shown as geometry, a local face-edge-vertex incidence flag, and the full path-sensitive flag nerve.</desc>",
+    "<defs>",
+    "  <linearGradient id=\"background\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop stop-color=\"#10262a\"/><stop offset=\"0.48\" stop-color=\"#071217\"/><stop offset=\"1\" stop-color=\"#120f20\"/></linearGradient>",
+    "  <radialGradient id=\"cyan-glow\"><stop stop-color=\"#72d6c2\" stop-opacity=\"0.22\"/><stop offset=\"1\" stop-color=\"#72d6c2\" stop-opacity=\"0\"/></radialGradient>",
+    "  <radialGradient id=\"violet-glow\"><stop stop-color=\"#bc8cff\" stop-opacity=\"0.22\"/><stop offset=\"1\" stop-color=\"#bc8cff\" stop-opacity=\"0\"/></radialGradient>",
+    "  <pattern id=\"grid\" width=\"28\" height=\"28\" patternUnits=\"userSpaceOnUse\"><path d=\"M28 0H0V28\" fill=\"none\" stroke=\"#9bc7bd\" stroke-opacity=\"0.07\" stroke-width=\"1\"/></pattern>",
+    "  <marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0L10 5L0 10Z\" fill=\"#72d6c2\"/></marker>",
+    "  <filter id=\"soft-shadow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feDropShadow dx=\"0\" dy=\"8\" stdDeviation=\"12\" flood-color=\"#000\" flood-opacity=\"0.28\"/></filter>",
+    "  <style><![CDATA[",
+    "    .sans{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,sans-serif}",
+    "    .serif{font-family:Georgia,'Times New Roman',serif}",
+    "    .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}",
+    "    .panel{fill:#0a181d;fill-opacity:.93;stroke:#8bbeb2;stroke-opacity:.22;stroke-width:1.2}",
+    "    .kicker{fill:#72d6c2;font-size:12px;font-weight:700;letter-spacing:2.8px}",
+    "    .title{fill:#f1eee7;font-size:48px;font-weight:400;letter-spacing:-1.4px}",
+    "    .subtitle{fill:#9ab0aa;font-size:16px}",
+    "    .panel-number{fill:#72d6c2;font-size:11px;font-weight:700;letter-spacing:1.6px}",
+    "    .panel-title{fill:#f1eee7;font-size:25px}",
+    "    .callout-title{fill:#f1eee7;font-size:21px}",
+    "    .panel-note{fill:#8ca49f;font-size:12px}",
+    "    .metric-value{fill:#f1eee7;font-size:18px;font-weight:700}",
+    "    .metric-label{fill:#8ca49f;font-size:9px;font-weight:700;letter-spacing:1.2px}",
+    "    .mesh-face{fill:#72d6c2;fill-opacity:.035;stroke:none}",
+    "    .mesh-face.selected{fill:#ffad66;fill-opacity:.28}",
+    "    .mesh-edge{stroke:#72d6c2;stroke-opacity:.58;stroke-width:1.7}",
+    "    .mesh-edge.selected{stroke:#ffad66;stroke-opacity:1;stroke-width:5}",
+    "    .mesh-vertex{fill:#071217;stroke:#f2e37e;stroke-width:2.2}",
+    "    .mesh-vertex.selected{fill:#f2e37e;stroke:#fff7b0;stroke-width:4}",
+    "    .nerve-simplex{fill:#bc8cff;fill-opacity:.065;stroke:#bc8cff;stroke-opacity:.42;stroke-width:1.1}",
+    "    .nerve-simplex.selected{fill:#ffad66;fill-opacity:.48;stroke:#ffd1a8;stroke-opacity:1;stroke-width:3}",
+    "    .object-face{fill:#ffad66}.object-edge{fill:#72d6c2}.object-vertex{fill:#f2e37e}",
+    "    .node-label{fill:#f1eee7;font-size:23px;font-weight:700}.node-kind{fill:#8ca49f;font-size:10px;font-weight:700;letter-spacing:1.4px}",
+    "    .profile-label{fill:#8ca49f;font-size:11px;font-weight:700;letter-spacing:1px}.profile-value{fill:#f1eee7;font-size:12px;font-weight:700}",
+    "  ]]></style>",
+    "</defs>",
+    "<rect width=\"1440\" height=\"920\" fill=\"url(#background)\"/>",
+    "<circle cx=\"180\" cy=\"110\" r=\"290\" fill=\"url(#cyan-glow)\"/>",
+    "<circle cx=\"1290\" cy=\"520\" r=\"360\" fill=\"url(#violet-glow)\"/>"
+  ]
+
+renderHeader :: PictureContext -> [String]
+renderHeader context =
+  [ svgText "mono kicker" 56 38 "start" "MOONLIGHT / FINITE DESCENT",
+    svgText "serif title" 56 94 "start" "One section, three faithful projections",
+    svgText
+      "sans subtitle"
+      57
+      127
+      "start"
+      "Delaunay geometry → incidence category → path-sensitive flag nerve"
+  ]
+    <> concat
+      ( zipWith
+          renderMetric
+          [56, 212, 368, 524, 680, 856]
+          (pictureMetrics context)
+      )
+    <> [ "<path d=\"M1040 163H1378\" stroke=\"#72d6c2\" stroke-opacity=\".24\"/>",
+         svgText "mono panel-note" 1378 168 "end" "ONE HIGHLIGHTED FLAG GLUES ALL THREE PANELS"
+       ]
+
+data PictureMetric = PictureMetric
+  { metricValue :: !Int,
+    metricLabel :: !String
+  }
+
+pictureMetrics :: PictureContext -> [PictureMetric]
+pictureMetrics context =
+  let exhibit = contextExhibit context
+      meshProjection = mesh exhibit
+      categoryProjection = category exhibit
+      nerveProjection = nerve exhibit
+   in [ PictureMetric (length (meshVertices meshProjection)) "VERTICES",
+        PictureMetric (length (meshEdges meshProjection)) "EDGES",
+        PictureMetric (length (meshFaces meshProjection)) "FACES",
+        PictureMetric (length (categoryObjects categoryProjection)) "OBJECTS",
+        PictureMetric (length (categoryMorphisms categoryProjection)) "MORPHISMS",
+        PictureMetric (length (nerveTwoSimplices nerveProjection)) "FLAGS"
+      ]
+
+renderMetric :: Double -> PictureMetric -> [String]
+renderMetric x metric =
+  [ "<g transform=\"translate(" <> coordinate x <> " 151)\">",
+    "  <rect width=\"136\" height=\"48\" rx=\"13\" fill=\"#0b1b20\" stroke=\"#8bbeb2\" stroke-opacity=\".17\"/>",
+    "  " <> svgText "sans metric-value" 16 22 "start" (show (metricValue metric)),
+    "  " <> svgText "mono metric-label" 16 38 "start" (metricLabel metric),
+    "</g>"
+  ]
+
+renderPanels :: PictureContext -> [String]
+renderPanels context =
+  renderPanelBackgrounds
+    <> renderGeometryPanel context
+    <> renderCategoryPanel (contextFlag context)
+    <> renderNervePanel context
+
+renderPanelBackgrounds :: [String]
+renderPanelBackgrounds =
+  concatMap
+    (\x -> ["<rect class=\"panel\" x=\"" <> coordinate x <> "\" y=\"222\" width=\"410\" height=\"470\" rx=\"22\" filter=\"url(#soft-shadow)\"/>"])
+    [50, 515, 980]
+
+renderGeometryPanel :: PictureContext -> [String]
+renderGeometryPanel context =
+  renderPanelHeader 50 "01" "Geometric section" "the admitted Delaunay cells"
+    <> ["<rect x=\"70\" y=\"326\" width=\"370\" height=\"330\" rx=\"15\" fill=\"url(#grid)\"/>"]
+    <> fmap (renderMeshFace context geometryFrame) (contextFaces context)
+    <> fmap (renderMeshEdge context geometryFrame) (contextEdges context)
+    <> fmap (renderMeshVertex context geometryFrame) (meshVertices (mesh (contextExhibit context)))
+    <> renderGeometrySelectionLabels context
+
+geometryFrame :: PictureFrame
+geometryFrame = PictureFrame 83 341 344 290
+
+renderMeshFace :: PictureContext -> PictureFrame -> ResolvedMeshFace -> String
+renderMeshFace context frame faceValue =
+  let selected =
+        Set.member
+          (meshFaceObjectIdentifier (resolvedFace faceValue))
+          (selectedObjectIdentifiers (contextFlag context))
+   in "<polygon class=\"mesh-face"
+        <> selectedClass selected
+        <> "\" points=\""
+        <> renderPointList (contextBounds context) frame (resolvedFaceVertices faceValue)
+        <> "\"/>"
+
+renderMeshEdge :: PictureContext -> PictureFrame -> ResolvedMeshEdge -> String
+renderMeshEdge context frame edgeValue =
+  let selected =
+        Set.member
+          (meshEdgeObjectIdentifier (resolvedEdge edgeValue))
+          (selectedObjectIdentifiers (contextFlag context))
+      (sourceX, sourceY) = projectPosition (contextBounds context) frame (resolvedEdgeSource edgeValue)
+      (targetX, targetY) = projectPosition (contextBounds context) frame (resolvedEdgeTarget edgeValue)
+   in "<line class=\"mesh-edge"
+        <> selectedClass selected
+        <> "\" x1=\""
+        <> coordinate sourceX
+        <> "\" y1=\""
+        <> coordinate sourceY
+        <> "\" x2=\""
+        <> coordinate targetX
+        <> "\" y2=\""
+        <> coordinate targetY
+        <> "\"/>"
+
+renderMeshVertex :: PictureContext -> PictureFrame -> MeshVertex -> String
+renderMeshVertex context frame vertexValue =
+  let selected =
+        Set.member
+          (meshVertexObjectIdentifier vertexValue)
+          (selectedObjectIdentifiers (contextFlag context))
+      (x, y) = projectPosition (contextBounds context) frame (meshVertexPosition vertexValue)
+   in "<circle class=\"mesh-vertex"
+        <> selectedClass selected
+        <> "\" cx=\""
+        <> coordinate x
+        <> "\" cy=\""
+        <> coordinate y
+        <> "\" r=\""
+        <> if selected then "7\"/>" else "4.5\"/>"
+
+renderGeometrySelectionLabels :: PictureContext -> [String]
+renderGeometrySelectionLabels context =
+  let flag = contextFlag context
+      labelledObjects = [selectedFace flag, selectedEdge flag, selectedVertex flag]
+   in fmap
+        (\objectValue ->
+           let (x, y) = projectPosition (contextBounds context) geometryFrame (categoryObjectPosition objectValue)
+            in svgText
+                 "mono profile-value"
+                 (x + 10)
+                 (y - 10)
+                 "start"
+                 (categoryObjectLabel objectValue)
+        )
+        labelledObjects
+
+renderCategoryPanel :: FlagSelection -> [String]
+renderCategoryPanel flag =
+  renderPanelHeader 515 "02" "Incidence category" "one local section; no arrow thicket"
+    <> [ svgText "mono panel-note" 720 334 "middle" "SELECTED NONDEGENERATE CHAIN",
+         "<path d=\"M621 463H686\" stroke=\"#72d6c2\" stroke-width=\"2.5\" marker-end=\"url(#arrow)\"/>",
+         "<path d=\"M754 463H817\" stroke=\"#72d6c2\" stroke-width=\"2.5\" marker-end=\"url(#arrow)\"/>",
+         "<path d=\"M609 421C665 365 794 365 850 421\" fill=\"none\" stroke=\"#bc8cff\" stroke-opacity=\".68\" stroke-width=\"1.6\" stroke-dasharray=\"6 7\" marker-end=\"url(#arrow)\"/>",
+         "<rect x=\"556\" y=\"430\" width=\"66\" height=\"66\" rx=\"14\" fill=\"#ffad66\" fill-opacity=\".18\" stroke=\"#ffad66\" stroke-width=\"2\"/>",
+         "<rect x=\"696\" y=\"439\" width=\"48\" height=\"48\" transform=\"rotate(45 720 463)\" fill=\"#72d6c2\" fill-opacity=\".16\" stroke=\"#72d6c2\" stroke-width=\"2\"/>",
+         "<circle cx=\"850\" cy=\"463\" r=\"31\" fill=\"#f2e37e\" fill-opacity=\".14\" stroke=\"#f2e37e\" stroke-width=\"2\"/>",
+         svgText "mono node-kind" 589 407 "middle" "FACE OBJECT",
+         svgText "sans node-label" 589 471 "middle" (categoryObjectLabel (selectedFace flag)),
+         svgText "mono node-kind" 720 407 "middle" "EDGE OBJECT",
+         svgText "sans node-label" 720 471 "middle" (categoryObjectLabel (selectedEdge flag)),
+         svgText "mono node-kind" 850 407 "middle" "VERTEX OBJECT",
+         svgText "sans node-label" 850 471 "middle" (categoryObjectLabel (selectedVertex flag)),
+         svgText "mono panel-note" 720 535 "middle" "face → edge → vertex",
+         "<rect x=\"552\" y=\"570\" width=\"336\" height=\"78\" rx=\"14\" fill=\"#071217\" stroke=\"#bc8cff\" stroke-opacity=\".28\"/>",
+         svgText "serif callout-title" 720 602 "middle" "The intermediate edge survives.",
+         svgText "sans panel-note" 720 628 "middle" "Same endpoints, different paths, distinct flags."
+       ]
+
+renderNervePanel :: PictureContext -> [String]
+renderNervePanel context =
+  renderPanelHeader 980 "03" "Flag nerve" "all 66 admitted two-simplices"
+    <> ["<rect x=\"1000\" y=\"326\" width=\"370\" height=\"330\" rx=\"15\" fill=\"url(#grid)\"/>"]
+    <> fmap (renderNerveSimplex context nerveFrame) (contextNerveSimplices context)
+    <> fmap (renderNerveObject context nerveFrame) (categoryObjects (category (contextExhibit context)))
+    <> renderNerveSelectionLabel context
+
+nerveFrame :: PictureFrame
+nerveFrame = PictureFrame 1013 341 344 290
+
+renderNerveSimplex :: PictureContext -> PictureFrame -> ResolvedNerveSimplex -> String
+renderNerveSimplex context frame simplexValue =
+  let selected =
+        nerveSimplexIdentifier (resolvedSimplex simplexValue)
+          == selectedSimplexIdentifier (contextFlag context)
+   in "<polygon class=\"nerve-simplex"
+        <> selectedClass selected
+        <> "\" points=\""
+        <> renderPointList (contextBounds context) frame (resolvedSimplexVertices simplexValue)
+        <> "\"/>"
+
+renderNerveObject :: PictureContext -> PictureFrame -> CategoryObject -> String
+renderNerveObject context frame objectValue =
+  let selected =
+        Set.member
+          (categoryObjectIdentifier objectValue)
+          (selectedObjectIdentifiers (contextFlag context))
+      (x, y) = projectPosition (contextBounds context) frame (categoryObjectPosition objectValue)
+      radius = if selected then 6 else 3.4
+   in "<circle class=\""
+        <> objectKindClass (categoryObjectKind objectValue)
+        <> "\" cx=\""
+        <> coordinate x
+        <> "\" cy=\""
+        <> coordinate y
+        <> "\" r=\""
+        <> coordinate radius
+        <> "\" opacity=\""
+        <> if selected then "1\" stroke=\"#fff\" stroke-width=\"2\"/>" else ".78\"/>"
+
+renderNerveSelectionLabel :: PictureContext -> [String]
+renderNerveSelectionLabel context =
+  let flag = contextFlag context
+      faceObject = selectedFace flag
+      (x, y) = projectPosition (contextBounds context) nerveFrame (categoryObjectPosition faceObject)
+   in [ svgText "mono profile-value" (x + 12) (y - 12) "start" "selected flag",
+        svgText "mono panel-note" 1352 642 "end" (selectedSimplexIdentifier flag)
+      ]
+
+renderPanelHeader :: Double -> String -> String -> String -> [String]
+renderPanelHeader x number titleText note =
+  [ svgText "mono panel-number" (x + 22) 254 "start" number,
+    svgText "serif panel-title" (x + 22) 282 "start" titleText,
+    svgText "sans panel-note" (x + 22) 304 "start" note,
+    "<path d=\"M"
+      <> coordinate (x + 20)
+      <> " 314H"
+      <> coordinate (x + 390)
+      <> "\" stroke=\"#8bbeb2\" stroke-opacity=\".14\"/>"
+  ]
+
+renderNerveProfile :: PictureContext -> [String]
+renderNerveProfile context =
+  let counts = nerveSimplexCounts (nerve (contextExhibit context))
+      maximumCount =
+        foldl'
+          (\current count -> max current (unnormalizedSimplexCount count))
+          1
+          counts
+   in [ "<rect x=\"50\" y=\"720\" width=\"1340\" height=\"156\" rx=\"22\" fill=\"#09171c\" stroke=\"#8bbeb2\" stroke-opacity=\".20\"/>",
+        svgText "mono kicker" 76 752 "start" "NORMALIZATION PROFILE",
+        svgText "serif panel-title" 76 789 "start" "Identity insertions are cost, not geometry.",
+        svgText "sans panel-note" 76 817 "start" "Identity insertions swell dimension two to 385.",
+        "<rect x=\"76\" y=\"838\" width=\"12\" height=\"4\" rx=\"2\" fill=\"#72d6c2\"/>",
+        svgText "mono profile-label" 96 845 "start" "NORMALIZED",
+        "<rect x=\"196\" y=\"838\" width=\"12\" height=\"4\" rx=\"2\" fill=\"#bc8cff\"/>",
+        svgText "mono profile-label" 216 845 "start" "UNNORMALIZED"
+      ]
+        <> concat
+          ( zipWith
+              (renderProfileRow maximumCount)
+              [770, 814, 858]
+              counts
+          )
+
+renderProfileRow :: Int -> Double -> NerveSimplexCount -> [String]
+renderProfileRow maximumCount y count =
+  let normalizedWidth = profileWidth maximumCount (normalizedSimplexCount count)
+      unnormalizedWidth = profileWidth maximumCount (unnormalizedSimplexCount count)
+   in [ svgText "mono profile-label" 590 (y + 5) "end" ("DIM " <> show (nerveDimension count)),
+        "<rect x=\"615\" y=\"" <> coordinate (y - 8) <> "\" width=\"230\" height=\"11\" rx=\"5.5\" fill=\"#13272d\"/>",
+        "<rect x=\"615\" y=\"" <> coordinate (y - 8) <> "\" width=\"" <> coordinate normalizedWidth <> "\" height=\"11\" rx=\"5.5\" fill=\"#72d6c2\"/>",
+        svgText "mono profile-value" 858 (y + 3) "start" (show (normalizedSimplexCount count)),
+        "<rect x=\"965\" y=\"" <> coordinate (y - 8) <> "\" width=\"310\" height=\"11\" rx=\"5.5\" fill=\"#1d1830\"/>",
+        "<rect x=\"965\" y=\"" <> coordinate (y - 8) <> "\" width=\"" <> coordinate (unnormalizedWidth * 310 / 230) <> "\" height=\"11\" rx=\"5.5\" fill=\"#bc8cff\"/>",
+        svgText "mono profile-value" 1288 (y + 3) "start" (show (unnormalizedSimplexCount count))
+      ]
+
+profileWidth :: Int -> Int -> Double
+profileWidth maximumCount count =
+  230 * fromIntegral count / fromIntegral maximumCount
+
+projectPosition :: PictureBounds -> PictureFrame -> Position -> (Double, Double)
+projectPosition bounds frame positionValue =
+  ( frameLeft frame
+      + frameWidth frame
+        * normalizedCoordinate (positionX positionValue) (minimumX bounds) (maximumX bounds),
+    frameTop frame
+      + frameHeight frame
+        * normalizedCoordinate (positionY positionValue) (minimumY bounds) (maximumY bounds)
+  )
+
+normalizedCoordinate :: Double -> Double -> Double -> Double
+normalizedCoordinate value minimumValue maximumValue =
+  if maximumValue == minimumValue
+    then 0.5
+    else (value - minimumValue) / (maximumValue - minimumValue)
+
+renderPointList :: PictureBounds -> PictureFrame -> [Position] -> String
+renderPointList bounds frame =
+  unwords
+    . fmap
+      (\positionValue ->
+         let (x, y) = projectPosition bounds frame positionValue
+          in coordinate x <> "," <> coordinate y
+      )
+
+selectedClass :: Bool -> String
+selectedClass selected = if selected then " selected" else ""
+
+objectKindClass :: CategoryObjectKind -> String
+objectKindClass = \case
+  FaceObject -> "object-face"
+  EdgeObject -> "object-edge"
+  VertexObject -> "object-vertex"
+
+svgText :: String -> Double -> Double -> String -> String -> String
+svgText classes x y anchor contents =
+  "<text class=\""
+    <> classes
+    <> "\" x=\""
+    <> coordinate x
+    <> "\" y=\""
+    <> coordinate y
+    <> "\" text-anchor=\""
+    <> anchor
+    <> "\">"
+    <> escapeXml contents
+    <> "</text>"
+
+coordinate :: Double -> String
+coordinate value = showFFloat (Just 1) value ""
+
+escapeXml :: String -> String
+escapeXml = concatMap escapeCharacter
+
+escapeCharacter :: Char -> String
+escapeCharacter = \case
+  '&' -> "&amp;"
+  '<' -> "&lt;"
+  '>' -> "&gt;"
+  '\"' -> "&quot;"
+  '\'' -> "&apos;"
+  character -> [character]
diff --git a/docs/category-observatory/README.md b/docs/category-observatory/README.md
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/README.md
@@ -0,0 +1,142 @@
+# Moonlight category observatory
+
+![The Moonlight category observatory showing one Delaunay cell selection as geometry, its incidence category, and its flag nerve](./moonlight-triangulation-category-observatory.svg)
+
+[Browse all exhibits.](../README.md)
+· [Read the package operation index.](../../README.md#operations)
+
+The category observatory follows one checked finite section through three
+synchronized views: the selected Delaunay cells, their incidence category, and
+the normalized nerve of that category. The visual above is the package-owned
+compact SVG projection of the same admitted `Exhibit`: one face-edge-vertex
+flag is restricted through all three panels while the full mesh and 66
+two-simplices remain visible. It is neither a viewport screenshot nor a second
+mathematical model.
+
+## Cell-complex and category interpretation
+
+The `cell-complex` component interprets an `ExactCellSet` as Homology's generic
+`CellComplex2D`. The opaque carrier retains the authoritative selected handles
+and resident DCEL. `fromExactCellSet` is total because `ExactCellSet`
+construction has already discharged downward closure and handle validity.
+
+The interpretation is available on GHC 9.14 and newer. Existing Triangulation
+components retain the package's GHC 9.8–9.14 support range: below 9.14 Cabal
+marks only `cell-complex`, its focused test, and the category-observatory
+exporter unbuildable. Hackage aggregates dependencies from every component on
+the package page, but an ordinary main-library consumer does not inherit the
+Homology or Category closure.
+
+```cabal
+-- the consumer package
+build-depends:
+  base >= 4.22 && < 5,
+  moonlight-triangulation >= 1.4 && < 1.5,
+  moonlight-triangulation:cell-complex >= 1.4 && < 1.5,
+  moonlight-homology:cell-category >= 0.1 && < 0.2
+```
+
+```haskell
+import Moonlight.Homology.Pure.Topology.CellCategory (complexCategory)
+import Moonlight.Triangulation.CellComplex (fromExactCellSet)
+
+incidenceCategory = complexCategory . fromExactCellSet
+```
+
+## What owns what
+
+The executable composes existing owners in one direction:
+
+```text
+ExactCellSet
+  -> Moonlight.Triangulation.CellComplex.DCELComplex
+  -> Moonlight.Homology.Pure.Topology.CellCategory.ComplexCat
+  -> Moonlight.Category.Simplicial.normalizedNerve
+  -> checked schema-version-2 JSON
+  -> Melusine's /category browser projection
+```
+
+`moonlight-triangulation-category-observatory-export` owns only the checked
+projection boundary. Geometry remains in Triangulation, the generic
+two-dimensional cell interface and incidence category remain in Homology, and
+the browser remains a derived view. There is no observatory-specific mesh,
+category implementation, or nerve implementation lurking behind the scenery.
+
+## Deterministic witness
+
+The retained ten-point section yields:
+
+| observation | count |
+| --- | ---: |
+| vertices | 10 |
+| undirected edges | 20 |
+| bounded faces | 11 |
+| category objects | 41 |
+| category morphisms | 180 |
+| face-edge-vertex incidence flags | 66 |
+
+The normalized and identity-saturated unnormalized nerves contain respectively
+`41 / 41`, `139 / 180`, and `66 / 385` simplices in dimensions 0, 1, and 2.
+Each rendered 2-simplex retains its intermediate edge; this is the
+path-sensitive incidence flag nerve, not a thinned face poset.
+
+Before encoding, the pure builder checks the selected-cell inventory, the four
+morphism classes, every morphism endpoint, every 2-simplex shape, and equality
+between the face-incidence and rendered-flag counts. Failures inhabit
+`ExhibitError`; stdout receives JSON only after those local sections glue.
+
+## Run the exporter
+
+The Homology-backed component requires GHC 9.14 or newer. From the repository
+root:
+
+```console
+scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-category-observatory-export \
+  --project-file=cabal.project.triangulation-dev
+```
+
+That command emits the deterministic schema-version-2 document to stdout. To
+regenerate or verify the retained documentation picture:
+
+```console
+scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-category-observatory-export \
+  --project-file=cabal.project.triangulation-dev -- \
+  picture compiler/foundation/moonlight-triangulation/docs/category-observatory/moonlight-triangulation-category-observatory.svg
+scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-category-observatory-export \
+  --project-file=cabal.project.triangulation-dev -- \
+  verify-artifacts compiler/foundation/moonlight-triangulation
+```
+
+To run the interactive browser consumer:
+
+```console
+cd compiler/engine/melusine-visualization
+pnpm dev:category
+```
+
+Open `http://127.0.0.1:5173/category`. Hovering a cell, category arrow, or flag
+restricts the other two panels to the same semantic objects; the dimension
+control reveals the normalized 0-, 1-, and 2-simplex sections.
+
+## Source decomposition
+
+`Main.hs` is only the `IO` edge: render a typed obstruction or encode the
+admitted exhibit. The pure implementation is split by mathematical
+responsibility:
+
+| module | responsibility |
+| --- | --- |
+| `Moonlight.Triangulation.Exhibit.CategoryObservatory` | deterministic witness and final descent |
+| `.Model` | JSON projection types and typed obstructions |
+| `.Geometry` | mesh projection and geometric object placement |
+| `.Category` | finite object/morphism projection and inventory law |
+| `.Nerve` | normalized/unnormalized nerve projection and flag law |
+| `.Identifier` | the single identifier/label interpretation shared by all projections |
+| `.Picture` | compact pure SVG projection of the admitted exhibit |
+
+The split is semantic rather than ceremonial: no wrapper modules, no duplicate
+projection types, no hand-maintained duplicate mesh coordinates, and no mutable
+orchestration wearing an expensive hat.
diff --git a/docs/category-observatory/moonlight-triangulation-category-observatory.svg b/docs/category-observatory/moonlight-triangulation-category-observatory.svg
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/moonlight-triangulation-category-observatory.svg
@@ -0,0 +1,293 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 920" role="img" aria-labelledby="title description">
+<title id="title">Moonlight category observatory</title>
+<desc id="description">One checked Delaunay section shown as geometry, a local face-edge-vertex incidence flag, and the full path-sensitive flag nerve.</desc>
+<defs>
+  <linearGradient id="background" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#10262a"/><stop offset="0.48" stop-color="#071217"/><stop offset="1" stop-color="#120f20"/></linearGradient>
+  <radialGradient id="cyan-glow"><stop stop-color="#72d6c2" stop-opacity="0.22"/><stop offset="1" stop-color="#72d6c2" stop-opacity="0"/></radialGradient>
+  <radialGradient id="violet-glow"><stop stop-color="#bc8cff" stop-opacity="0.22"/><stop offset="1" stop-color="#bc8cff" stop-opacity="0"/></radialGradient>
+  <pattern id="grid" width="28" height="28" patternUnits="userSpaceOnUse"><path d="M28 0H0V28" fill="none" stroke="#9bc7bd" stroke-opacity="0.07" stroke-width="1"/></pattern>
+  <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10Z" fill="#72d6c2"/></marker>
+  <filter id="soft-shadow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="8" stdDeviation="12" flood-color="#000" flood-opacity="0.28"/></filter>
+  <style><![CDATA[
+    .sans{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,sans-serif}
+    .serif{font-family:Georgia,'Times New Roman',serif}
+    .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
+    .panel{fill:#0a181d;fill-opacity:.93;stroke:#8bbeb2;stroke-opacity:.22;stroke-width:1.2}
+    .kicker{fill:#72d6c2;font-size:12px;font-weight:700;letter-spacing:2.8px}
+    .title{fill:#f1eee7;font-size:48px;font-weight:400;letter-spacing:-1.4px}
+    .subtitle{fill:#9ab0aa;font-size:16px}
+    .panel-number{fill:#72d6c2;font-size:11px;font-weight:700;letter-spacing:1.6px}
+    .panel-title{fill:#f1eee7;font-size:25px}
+    .callout-title{fill:#f1eee7;font-size:21px}
+    .panel-note{fill:#8ca49f;font-size:12px}
+    .metric-value{fill:#f1eee7;font-size:18px;font-weight:700}
+    .metric-label{fill:#8ca49f;font-size:9px;font-weight:700;letter-spacing:1.2px}
+    .mesh-face{fill:#72d6c2;fill-opacity:.035;stroke:none}
+    .mesh-face.selected{fill:#ffad66;fill-opacity:.28}
+    .mesh-edge{stroke:#72d6c2;stroke-opacity:.58;stroke-width:1.7}
+    .mesh-edge.selected{stroke:#ffad66;stroke-opacity:1;stroke-width:5}
+    .mesh-vertex{fill:#071217;stroke:#f2e37e;stroke-width:2.2}
+    .mesh-vertex.selected{fill:#f2e37e;stroke:#fff7b0;stroke-width:4}
+    .nerve-simplex{fill:#bc8cff;fill-opacity:.065;stroke:#bc8cff;stroke-opacity:.42;stroke-width:1.1}
+    .nerve-simplex.selected{fill:#ffad66;fill-opacity:.48;stroke:#ffd1a8;stroke-opacity:1;stroke-width:3}
+    .object-face{fill:#ffad66}.object-edge{fill:#72d6c2}.object-vertex{fill:#f2e37e}
+    .node-label{fill:#f1eee7;font-size:23px;font-weight:700}.node-kind{fill:#8ca49f;font-size:10px;font-weight:700;letter-spacing:1.4px}
+    .profile-label{fill:#8ca49f;font-size:11px;font-weight:700;letter-spacing:1px}.profile-value{fill:#f1eee7;font-size:12px;font-weight:700}
+  ]]></style>
+</defs>
+<rect width="1440" height="920" fill="url(#background)"/>
+<circle cx="180" cy="110" r="290" fill="url(#cyan-glow)"/>
+<circle cx="1290" cy="520" r="360" fill="url(#violet-glow)"/>
+<text class="mono kicker" x="56.0" y="38.0" text-anchor="start">MOONLIGHT / FINITE DESCENT</text>
+<text class="serif title" x="56.0" y="94.0" text-anchor="start">One section, three faithful projections</text>
+<text class="sans subtitle" x="57.0" y="127.0" text-anchor="start">Delaunay geometry → incidence category → path-sensitive flag nerve</text>
+<g transform="translate(56.0 151)">
+  <rect width="136" height="48" rx="13" fill="#0b1b20" stroke="#8bbeb2" stroke-opacity=".17"/>
+  <text class="sans metric-value" x="16.0" y="22.0" text-anchor="start">10</text>
+  <text class="mono metric-label" x="16.0" y="38.0" text-anchor="start">VERTICES</text>
+</g>
+<g transform="translate(212.0 151)">
+  <rect width="136" height="48" rx="13" fill="#0b1b20" stroke="#8bbeb2" stroke-opacity=".17"/>
+  <text class="sans metric-value" x="16.0" y="22.0" text-anchor="start">20</text>
+  <text class="mono metric-label" x="16.0" y="38.0" text-anchor="start">EDGES</text>
+</g>
+<g transform="translate(368.0 151)">
+  <rect width="136" height="48" rx="13" fill="#0b1b20" stroke="#8bbeb2" stroke-opacity=".17"/>
+  <text class="sans metric-value" x="16.0" y="22.0" text-anchor="start">11</text>
+  <text class="mono metric-label" x="16.0" y="38.0" text-anchor="start">FACES</text>
+</g>
+<g transform="translate(524.0 151)">
+  <rect width="136" height="48" rx="13" fill="#0b1b20" stroke="#8bbeb2" stroke-opacity=".17"/>
+  <text class="sans metric-value" x="16.0" y="22.0" text-anchor="start">41</text>
+  <text class="mono metric-label" x="16.0" y="38.0" text-anchor="start">OBJECTS</text>
+</g>
+<g transform="translate(680.0 151)">
+  <rect width="136" height="48" rx="13" fill="#0b1b20" stroke="#8bbeb2" stroke-opacity=".17"/>
+  <text class="sans metric-value" x="16.0" y="22.0" text-anchor="start">180</text>
+  <text class="mono metric-label" x="16.0" y="38.0" text-anchor="start">MORPHISMS</text>
+</g>
+<g transform="translate(856.0 151)">
+  <rect width="136" height="48" rx="13" fill="#0b1b20" stroke="#8bbeb2" stroke-opacity=".17"/>
+  <text class="sans metric-value" x="16.0" y="22.0" text-anchor="start">66</text>
+  <text class="mono metric-label" x="16.0" y="38.0" text-anchor="start">FLAGS</text>
+</g>
+<path d="M1040 163H1378" stroke="#72d6c2" stroke-opacity=".24"/>
+<text class="mono panel-note" x="1378.0" y="168.0" text-anchor="end">ONE HIGHLIGHTED FLAG GLUES ALL THREE PANELS</text>
+<rect class="panel" x="50.0" y="222" width="410" height="470" rx="22" filter="url(#soft-shadow)"/>
+<rect class="panel" x="515.0" y="222" width="410" height="470" rx="22" filter="url(#soft-shadow)"/>
+<rect class="panel" x="980.0" y="222" width="410" height="470" rx="22" filter="url(#soft-shadow)"/>
+<text class="mono panel-number" x="72.0" y="254.0" text-anchor="start">01</text>
+<text class="serif panel-title" x="72.0" y="282.0" text-anchor="start">Geometric section</text>
+<text class="sans panel-note" x="72.0" y="304.0" text-anchor="start">the admitted Delaunay cells</text>
+<path d="M70.0 314H440.0" stroke="#8bbeb2" stroke-opacity=".14"/>
+<rect x="70" y="326" width="370" height="330" rx="15" fill="url(#grid)"/>
+<polygon class="mesh-face selected" points="200.8,452.8 328.2,475.6 278.8,593.1"/>
+<polygon class="mesh-face" points="328.2,475.6 200.8,452.8 244.5,341.0"/>
+<polygon class="mesh-face" points="328.2,475.6 244.5,341.0 377.6,382.7"/>
+<polygon class="mesh-face" points="200.8,452.8 278.8,593.1 140.0,608.3"/>
+<polygon class="mesh-face" points="278.8,593.1 328.2,475.6 427.0,515.4"/>
+<polygon class="mesh-face" points="427.0,515.4 328.2,475.6 377.6,382.7"/>
+<polygon class="mesh-face" points="200.8,452.8 140.0,608.3 83.0,492.6"/>
+<polygon class="mesh-face" points="244.5,341.0 200.8,452.8 109.6,363.7"/>
+<polygon class="mesh-face" points="109.6,363.7 200.8,452.8 83.0,492.6"/>
+<polygon class="mesh-face" points="278.8,593.1 427.0,515.4 394.7,631.0"/>
+<polygon class="mesh-face" points="140.0,608.3 278.8,593.1 394.7,631.0"/>
+<line class="mesh-edge selected" x1="200.8" y1="452.8" x2="328.2" y2="475.6"/>
+<line class="mesh-edge" x1="200.8" y1="452.8" x2="278.8" y2="593.1"/>
+<line class="mesh-edge" x1="328.2" y1="475.6" x2="278.8" y2="593.1"/>
+<line class="mesh-edge" x1="328.2" y1="475.6" x2="244.5" y2="341.0"/>
+<line class="mesh-edge" x1="200.8" y1="452.8" x2="244.5" y2="341.0"/>
+<line class="mesh-edge" x1="328.2" y1="475.6" x2="377.6" y2="382.7"/>
+<line class="mesh-edge" x1="244.5" y1="341.0" x2="377.6" y2="382.7"/>
+<line class="mesh-edge" x1="200.8" y1="452.8" x2="140.0" y2="608.3"/>
+<line class="mesh-edge" x1="278.8" y1="593.1" x2="140.0" y2="608.3"/>
+<line class="mesh-edge" x1="278.8" y1="593.1" x2="427.0" y2="515.4"/>
+<line class="mesh-edge" x1="328.2" y1="475.6" x2="427.0" y2="515.4"/>
+<line class="mesh-edge" x1="427.0" y1="515.4" x2="377.6" y2="382.7"/>
+<line class="mesh-edge" x1="200.8" y1="452.8" x2="83.0" y2="492.6"/>
+<line class="mesh-edge" x1="140.0" y1="608.3" x2="83.0" y2="492.6"/>
+<line class="mesh-edge" x1="244.5" y1="341.0" x2="109.6" y2="363.7"/>
+<line class="mesh-edge" x1="200.8" y1="452.8" x2="109.6" y2="363.7"/>
+<line class="mesh-edge" x1="109.6" y1="363.7" x2="83.0" y2="492.6"/>
+<line class="mesh-edge" x1="278.8" y1="593.1" x2="394.7" y2="631.0"/>
+<line class="mesh-edge" x1="427.0" y1="515.4" x2="394.7" y2="631.0"/>
+<line class="mesh-edge" x1="140.0" y1="608.3" x2="394.7" y2="631.0"/>
+<circle class="mesh-vertex selected" cx="200.8" cy="452.8" r="7"/>
+<circle class="mesh-vertex" cx="328.2" cy="475.6" r="4.5"/>
+<circle class="mesh-vertex" cx="278.8" cy="593.1" r="4.5"/>
+<circle class="mesh-vertex" cx="244.5" cy="341.0" r="4.5"/>
+<circle class="mesh-vertex" cx="377.6" cy="382.7" r="4.5"/>
+<circle class="mesh-vertex" cx="140.0" cy="608.3" r="4.5"/>
+<circle class="mesh-vertex" cx="427.0" cy="515.4" r="4.5"/>
+<circle class="mesh-vertex" cx="83.0" cy="492.6" r="4.5"/>
+<circle class="mesh-vertex" cx="109.6" cy="363.7" r="4.5"/>
+<circle class="mesh-vertex" cx="394.7" cy="631.0" r="4.5"/>
+<text class="mono profile-value" x="279.3" y="497.2" text-anchor="start">f1</text>
+<text class="mono profile-value" x="274.5" y="454.2" text-anchor="start">e0</text>
+<text class="mono profile-value" x="210.8" y="442.8" text-anchor="start">v0</text>
+<text class="mono panel-number" x="537.0" y="254.0" text-anchor="start">02</text>
+<text class="serif panel-title" x="537.0" y="282.0" text-anchor="start">Incidence category</text>
+<text class="sans panel-note" x="537.0" y="304.0" text-anchor="start">one local section; no arrow thicket</text>
+<path d="M535.0 314H905.0" stroke="#8bbeb2" stroke-opacity=".14"/>
+<text class="mono panel-note" x="720.0" y="334.0" text-anchor="middle">SELECTED NONDEGENERATE CHAIN</text>
+<path d="M621 463H686" stroke="#72d6c2" stroke-width="2.5" marker-end="url(#arrow)"/>
+<path d="M754 463H817" stroke="#72d6c2" stroke-width="2.5" marker-end="url(#arrow)"/>
+<path d="M609 421C665 365 794 365 850 421" fill="none" stroke="#bc8cff" stroke-opacity=".68" stroke-width="1.6" stroke-dasharray="6 7" marker-end="url(#arrow)"/>
+<rect x="556" y="430" width="66" height="66" rx="14" fill="#ffad66" fill-opacity=".18" stroke="#ffad66" stroke-width="2"/>
+<rect x="696" y="439" width="48" height="48" transform="rotate(45 720 463)" fill="#72d6c2" fill-opacity=".16" stroke="#72d6c2" stroke-width="2"/>
+<circle cx="850" cy="463" r="31" fill="#f2e37e" fill-opacity=".14" stroke="#f2e37e" stroke-width="2"/>
+<text class="mono node-kind" x="589.0" y="407.0" text-anchor="middle">FACE OBJECT</text>
+<text class="sans node-label" x="589.0" y="471.0" text-anchor="middle">f1</text>
+<text class="mono node-kind" x="720.0" y="407.0" text-anchor="middle">EDGE OBJECT</text>
+<text class="sans node-label" x="720.0" y="471.0" text-anchor="middle">e0</text>
+<text class="mono node-kind" x="850.0" y="407.0" text-anchor="middle">VERTEX OBJECT</text>
+<text class="sans node-label" x="850.0" y="471.0" text-anchor="middle">v0</text>
+<text class="mono panel-note" x="720.0" y="535.0" text-anchor="middle">face → edge → vertex</text>
+<rect x="552" y="570" width="336" height="78" rx="14" fill="#071217" stroke="#bc8cff" stroke-opacity=".28"/>
+<text class="serif callout-title" x="720.0" y="602.0" text-anchor="middle">The intermediate edge survives.</text>
+<text class="sans panel-note" x="720.0" y="628.0" text-anchor="middle">Same endpoints, different paths, distinct flags.</text>
+<text class="mono panel-number" x="1002.0" y="254.0" text-anchor="start">03</text>
+<text class="serif panel-title" x="1002.0" y="282.0" text-anchor="start">Flag nerve</text>
+<text class="sans panel-note" x="1002.0" y="304.0" text-anchor="start">all 66 admitted two-simplices</text>
+<path d="M1000.0 314H1370.0" stroke="#8bbeb2" stroke-opacity=".14"/>
+<rect x="1000" y="326" width="370" height="330" rx="15" fill="url(#grid)"/>
+<polygon class="nerve-simplex selected" points="1199.3,507.2 1194.5,464.2 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1199.3,507.2 1194.5,464.2 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1199.3,507.2 1233.5,534.3 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1199.3,507.2 1233.5,534.3 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1199.3,507.2 1169.8,523.0 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1199.3,507.2 1169.8,523.0 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1187.9,423.1 1194.5,464.2 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1187.9,423.1 1194.5,464.2 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1187.9,423.1 1152.7,396.9 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1187.9,423.1 1152.7,396.9 1174.5,341.0"/>
+<polygon class="nerve-simplex" points="1187.9,423.1 1216.4,408.3 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1187.9,423.1 1216.4,408.3 1174.5,341.0"/>
+<polygon class="nerve-simplex" points="1246.8,399.8 1216.4,408.3 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1246.8,399.8 1216.4,408.3 1174.5,341.0"/>
+<polygon class="nerve-simplex" points="1246.8,399.8 1241.1,361.8 1174.5,341.0"/>
+<polygon class="nerve-simplex" points="1246.8,399.8 1241.1,361.8 1307.6,382.7"/>
+<polygon class="nerve-simplex" points="1246.8,399.8 1282.9,429.1 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1246.8,399.8 1282.9,429.1 1307.6,382.7"/>
+<polygon class="nerve-simplex" points="1136.5,551.4 1169.8,523.0 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1136.5,551.4 1169.8,523.0 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1136.5,551.4 1139.4,600.7 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1136.5,551.4 1139.4,600.7 1070.0,608.3"/>
+<polygon class="nerve-simplex" points="1136.5,551.4 1100.4,530.5 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1136.5,551.4 1100.4,530.5 1070.0,608.3"/>
+<polygon class="nerve-simplex" points="1274.6,528.0 1233.5,534.3 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1274.6,528.0 1233.5,534.3 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1274.6,528.0 1307.6,495.5 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1274.6,528.0 1307.6,495.5 1357.0,515.4"/>
+<polygon class="nerve-simplex" points="1274.6,528.0 1282.9,554.2 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1274.6,528.0 1282.9,554.2 1357.0,515.4"/>
+<polygon class="nerve-simplex" points="1307.6,457.9 1307.6,495.5 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1307.6,457.9 1307.6,495.5 1357.0,515.4"/>
+<polygon class="nerve-simplex" points="1307.6,457.9 1282.9,429.1 1258.2,475.6"/>
+<polygon class="nerve-simplex" points="1307.6,457.9 1282.9,429.1 1307.6,382.7"/>
+<polygon class="nerve-simplex" points="1307.6,457.9 1332.3,449.0 1357.0,515.4"/>
+<polygon class="nerve-simplex" points="1307.6,457.9 1332.3,449.0 1307.6,382.7"/>
+<polygon class="nerve-simplex" points="1071.3,517.9 1100.4,530.5 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1071.3,517.9 1100.4,530.5 1070.0,608.3"/>
+<polygon class="nerve-simplex" points="1071.3,517.9 1041.5,550.4 1070.0,608.3"/>
+<polygon class="nerve-simplex" points="1071.3,517.9 1041.5,550.4 1013.0,492.6"/>
+<polygon class="nerve-simplex" points="1071.3,517.9 1071.9,472.7 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1071.3,517.9 1071.9,472.7 1013.0,492.6"/>
+<polygon class="nerve-simplex" points="1115.0,385.9 1152.7,396.9 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1115.0,385.9 1152.7,396.9 1174.5,341.0"/>
+<polygon class="nerve-simplex" points="1115.0,385.9 1085.2,408.3 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1115.0,385.9 1085.2,408.3 1039.6,363.7"/>
+<polygon class="nerve-simplex" points="1115.0,385.9 1107.1,352.4 1174.5,341.0"/>
+<polygon class="nerve-simplex" points="1115.0,385.9 1107.1,352.4 1039.6,363.7"/>
+<polygon class="nerve-simplex" points="1061.1,436.4 1085.2,408.3 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1061.1,436.4 1085.2,408.3 1039.6,363.7"/>
+<polygon class="nerve-simplex" points="1061.1,436.4 1071.9,472.7 1130.8,452.8"/>
+<polygon class="nerve-simplex" points="1061.1,436.4 1071.9,472.7 1013.0,492.6"/>
+<polygon class="nerve-simplex" points="1061.1,436.4 1026.3,428.2 1039.6,363.7"/>
+<polygon class="nerve-simplex" points="1061.1,436.4 1026.3,428.2 1013.0,492.6"/>
+<polygon class="nerve-simplex" points="1296.8,579.8 1282.9,554.2 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1296.8,579.8 1282.9,554.2 1357.0,515.4"/>
+<polygon class="nerve-simplex" points="1296.8,579.8 1340.8,573.2 1357.0,515.4"/>
+<polygon class="nerve-simplex" points="1296.8,579.8 1340.8,573.2 1324.7,631.0"/>
+<polygon class="nerve-simplex" points="1296.8,579.8 1266.7,612.0 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1296.8,579.8 1266.7,612.0 1324.7,631.0"/>
+<polygon class="nerve-simplex" points="1201.2,610.8 1139.4,600.7 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1201.2,610.8 1139.4,600.7 1070.0,608.3"/>
+<polygon class="nerve-simplex" points="1201.2,610.8 1266.7,612.0 1208.8,593.1"/>
+<polygon class="nerve-simplex" points="1201.2,610.8 1266.7,612.0 1324.7,631.0"/>
+<polygon class="nerve-simplex" points="1201.2,610.8 1197.4,619.6 1070.0,608.3"/>
+<polygon class="nerve-simplex" points="1201.2,610.8 1197.4,619.6 1324.7,631.0"/>
+<circle class="object-vertex" cx="1130.8" cy="452.8" r="6.0" opacity="1" stroke="#fff" stroke-width="2"/>
+<circle class="object-vertex" cx="1258.2" cy="475.6" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1208.8" cy="593.1" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1174.5" cy="341.0" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1307.6" cy="382.7" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1070.0" cy="608.3" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1357.0" cy="515.4" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1013.0" cy="492.6" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1039.6" cy="363.7" r="3.4" opacity=".78"/>
+<circle class="object-vertex" cx="1324.7" cy="631.0" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1194.5" cy="464.2" r="6.0" opacity="1" stroke="#fff" stroke-width="2"/>
+<circle class="object-edge" cx="1169.8" cy="523.0" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1233.5" cy="534.3" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1216.4" cy="408.3" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1152.7" cy="396.9" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1282.9" cy="429.1" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1241.1" cy="361.8" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1100.4" cy="530.5" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1139.4" cy="600.7" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1282.9" cy="554.2" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1307.6" cy="495.5" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1332.3" cy="449.0" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1071.9" cy="472.7" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1041.5" cy="550.4" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1107.1" cy="352.4" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1085.2" cy="408.3" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1026.3" cy="428.2" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1266.7" cy="612.0" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1340.8" cy="573.2" r="3.4" opacity=".78"/>
+<circle class="object-edge" cx="1197.4" cy="619.6" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1199.3" cy="507.2" r="6.0" opacity="1" stroke="#fff" stroke-width="2"/>
+<circle class="object-face" cx="1187.9" cy="423.1" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1246.8" cy="399.8" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1136.5" cy="551.4" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1274.6" cy="528.0" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1307.6" cy="457.9" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1071.3" cy="517.9" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1115.0" cy="385.9" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1061.1" cy="436.4" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1296.8" cy="579.8" r="3.4" opacity=".78"/>
+<circle class="object-face" cx="1201.2" cy="610.8" r="3.4" opacity=".78"/>
+<text class="mono profile-value" x="1211.3" y="495.2" text-anchor="start">selected flag</text>
+<text class="mono panel-note" x="1352.0" y="642.0" text-anchor="end">simplex-0</text>
+<rect x="50" y="720" width="1340" height="156" rx="22" fill="#09171c" stroke="#8bbeb2" stroke-opacity=".20"/>
+<text class="mono kicker" x="76.0" y="752.0" text-anchor="start">NORMALIZATION PROFILE</text>
+<text class="serif panel-title" x="76.0" y="789.0" text-anchor="start">Identity insertions are cost, not geometry.</text>
+<text class="sans panel-note" x="76.0" y="817.0" text-anchor="start">Identity insertions swell dimension two to 385.</text>
+<rect x="76" y="838" width="12" height="4" rx="2" fill="#72d6c2"/>
+<text class="mono profile-label" x="96.0" y="845.0" text-anchor="start">NORMALIZED</text>
+<rect x="196" y="838" width="12" height="4" rx="2" fill="#bc8cff"/>
+<text class="mono profile-label" x="216.0" y="845.0" text-anchor="start">UNNORMALIZED</text>
+<text class="mono profile-label" x="590.0" y="775.0" text-anchor="end">DIM 0</text>
+<rect x="615" y="762.0" width="230" height="11" rx="5.5" fill="#13272d"/>
+<rect x="615" y="762.0" width="24.5" height="11" rx="5.5" fill="#72d6c2"/>
+<text class="mono profile-value" x="858.0" y="773.0" text-anchor="start">41</text>
+<rect x="965" y="762.0" width="310" height="11" rx="5.5" fill="#1d1830"/>
+<rect x="965" y="762.0" width="33.0" height="11" rx="5.5" fill="#bc8cff"/>
+<text class="mono profile-value" x="1288.0" y="773.0" text-anchor="start">41</text>
+<text class="mono profile-label" x="590.0" y="819.0" text-anchor="end">DIM 1</text>
+<rect x="615" y="806.0" width="230" height="11" rx="5.5" fill="#13272d"/>
+<rect x="615" y="806.0" width="83.0" height="11" rx="5.5" fill="#72d6c2"/>
+<text class="mono profile-value" x="858.0" y="817.0" text-anchor="start">139</text>
+<rect x="965" y="806.0" width="310" height="11" rx="5.5" fill="#1d1830"/>
+<rect x="965" y="806.0" width="144.9" height="11" rx="5.5" fill="#bc8cff"/>
+<text class="mono profile-value" x="1288.0" y="817.0" text-anchor="start">180</text>
+<text class="mono profile-label" x="590.0" y="863.0" text-anchor="end">DIM 2</text>
+<rect x="615" y="850.0" width="230" height="11" rx="5.5" fill="#13272d"/>
+<rect x="615" y="850.0" width="39.4" height="11" rx="5.5" fill="#72d6c2"/>
+<text class="mono profile-value" x="858.0" y="861.0" text-anchor="start">66</text>
+<rect x="965" y="850.0" width="310" height="11" rx="5.5" fill="#1d1830"/>
+<rect x="965" y="850.0" width="310.0" height="11" rx="5.5" fill="#bc8cff"/>
+<text class="mono profile-value" x="1288.0" y="861.0" text-anchor="start">385</text>
+</svg>
diff --git a/docs/delaunay-compare/Main.hs b/docs/delaunay-compare/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Main.hs
@@ -0,0 +1,27 @@
+-- | Haskell owner of the upstream-compatible construction comparison.
+module Main (main) where
+
+import Moonlight.Triangulation.Bench.DelaunayCompare.Domain (renderCompareObstruction)
+import Moonlight.Triangulation.Bench.DelaunayCompare.Native (withNativeApi)
+import Moonlight.Triangulation.Bench.DelaunayCompare.Suite
+  ( preflightSuite
+  , suiteAgreementMessage
+  , suiteBenchmarks
+  , withPreparedSuite
+  )
+import System.Exit (die)
+import Test.Tasty.Bench (defaultMain)
+
+main :: IO ()
+main = do
+  outcome <-
+    withNativeApi $ \api ->
+      withPreparedSuite api $ \suite -> do
+        agreement <- preflightSuite api suite
+        case agreement of
+          Left obstruction -> pure (Left obstruction)
+          Right () -> do
+            putStrLn suiteAgreementMessage
+            defaultMain (suiteBenchmarks api suite)
+            pure (Right ())
+  either (die . renderCompareObstruction) pure outcome
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs
@@ -0,0 +1,244 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | The closed case matrix and typed obstruction surface for the comparison.
+module Moonlight.Triangulation.Bench.DelaunayCompare.Domain
+  ( SizeBand (..)
+  , allSizeBands
+  , sizeBandLabel
+  , PointDistribution (..)
+  , allPointDistributions
+  , pointDistributionLabel
+  , Implementation (..)
+  , allImplementations
+  , implementationLabel
+  , NativeImplementation (..)
+  , nativeImplementationLabel
+  , implementationOfNative
+  , PointCount
+  , pointCountValue
+  , pointCountsFor
+  , FixtureSpec (..)
+  , allFixtureSpecs
+  , DelaunaySummary (..)
+  , NativeStatus (..)
+  , NativePreparationFailure (..)
+  , NativeRunFailure (..)
+  , CompareObstruction (..)
+  , renderCompareObstruction
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NonEmpty
+import GHC.Generics (Generic)
+import Moonlight.Triangulation.Types (BuildError)
+
+data SizeBand
+  = Small
+  | Big
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+allSizeBands :: [SizeBand]
+allSizeBands = [Small, Big]
+
+sizeBandLabel :: SizeBand -> String
+sizeBandLabel = \case
+  Small -> "small"
+  Big -> "big"
+
+data PointDistribution
+  = LocalInsertion
+  | Uniform
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+allPointDistributions :: [PointDistribution]
+allPointDistributions = [LocalInsertion, Uniform]
+
+pointDistributionLabel :: PointDistribution -> String
+pointDistributionLabel = \case
+  LocalInsertion -> "local insertion"
+  Uniform -> "uniform"
+
+data Implementation
+  = Spade
+  | SpadeHierarchy
+  | Cdt
+  | Delaunator
+  | Moonlight
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Upstream order, with Moonlight appended rather than interposed.
+allImplementations :: [Implementation]
+allImplementations = [Spade, SpadeHierarchy, Cdt, Delaunator, Moonlight]
+
+implementationLabel :: Implementation -> String
+implementationLabel = \case
+  Spade -> "spade 2"
+  SpadeHierarchy -> "spade 2 hierarchy"
+  Cdt -> "cdt"
+  Delaunator -> "delaunator"
+  Moonlight -> "moonlight-triangulation"
+
+data NativeImplementation
+  = NativeSpade
+  | NativeSpadeHierarchy
+  | NativeCdt
+  | NativeDelaunator
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+nativeImplementationLabel :: NativeImplementation -> String
+nativeImplementationLabel = implementationLabel . implementationOfNative
+
+implementationOfNative :: NativeImplementation -> Implementation
+implementationOfNative = \case
+  NativeSpade -> Spade
+  NativeSpadeHierarchy -> SpadeHierarchy
+  NativeCdt -> Cdt
+  NativeDelaunator -> Delaunator
+
+newtype PointCount = PointCount Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+pointCountValue :: PointCount -> Int
+pointCountValue (PointCount count) = count
+
+data FixtureSpec = FixtureSpec
+  { fixtureSizeBand :: !SizeBand
+  , fixturePointDistribution :: !PointDistribution
+  , fixturePointCount :: !PointCount
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+allFixtureSpecs :: [FixtureSpec]
+allFixtureSpecs =
+  concatMap
+    ( \sizeBand ->
+        liftA2
+          (FixtureSpec sizeBand)
+          allPointDistributions
+          (pointCountsFor sizeBand)
+    )
+    allSizeBands
+
+pointCountsFor :: SizeBand -> [PointCount]
+pointCountsFor = \case
+  Small -> PointCount <$> [2_000, 4_000, 6_000, 8_000, 10_000, 12_000, 14_000]
+  Big -> PointCount <$> [50_000, 100_000, 150_000, 200_000, 250_000]
+
+data DelaunaySummary = DelaunaySummary
+  { summaryVertexCount :: !Int
+  , summaryTriangleCount :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data NativeStatus
+  = NativeNullPointer
+  | NativeUnknownTag
+  | NativeCoordinateCountMismatch
+  | NativeDistributionConstructionFailed
+  | NativeTriangulationFailed
+  | NativePanicked
+  | NativeUnknownStatus !Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data NativePreparationFailure
+  = NativePreparationStatus !NativeStatus
+  | NativePreparationReturnedNull
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data NativeRunFailure
+  = NativeRunStatus !NativeStatus
+  | NativeCountExceedsHaskellInt !Integer
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data CompareObstruction
+  = RustManifestNotFound !(NonEmpty FilePath)
+  | CargoInvocationFailed !FilePath !String
+  | CargoBuildFailed !FilePath !Int !String
+  | UnsupportedDynamicLibraryHost !String
+  | DynamicLibraryOpenFailed !FilePath !String
+  | DynamicSymbolLoadFailed !FilePath !String !String
+  | FixtureGenerationFailed !FixtureSpec !NativeStatus
+  | FixturePreparationFailed !FixtureSpec !NativeImplementation !NativeStatus
+  | NativePreparedNull !FixtureSpec !NativeImplementation
+  | MoonlightConstructionFailed !FixtureSpec !BuildError
+  | NativeConstructionFailed !FixtureSpec !NativeImplementation !NativeRunFailure
+  | SummaryDisagreement !FixtureSpec !Implementation !DelaunaySummary !DelaunaySummary
+  | ComparisonObstructions !(NonEmpty CompareObstruction)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+renderCompareObstruction :: CompareObstruction -> String
+renderCompareObstruction = \case
+  RustManifestNotFound candidates ->
+    "Rust referent manifest not found; checked " <> show (NonEmpty.toList candidates)
+  CargoInvocationFailed manifest details ->
+    "could not invoke Cargo for " <> manifest <> ": " <> details
+  CargoBuildFailed manifest exitCode details ->
+    "Cargo failed for "
+      <> manifest
+      <> " with exit code "
+      <> show exitCode
+      <> if null details then "" else ":\n" <> details
+  UnsupportedDynamicLibraryHost host ->
+    "delaunay-compare does not know the dynamic-library suffix for host " <> show host
+  DynamicLibraryOpenFailed path details ->
+    "could not load Rust referent library " <> path <> ": " <> details
+  DynamicSymbolLoadFailed path symbol details ->
+    "could not load symbol " <> symbol <> " from " <> path <> ": " <> details
+  FixtureGenerationFailed fixture status ->
+    "upstream fixture generation failed for " <> renderFixture fixture <> ": " <> show status
+  FixturePreparationFailed fixture implementation status ->
+    "native preparation failed for "
+      <> nativeImplementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+      <> ": "
+      <> show status
+  NativePreparedNull fixture implementation ->
+    "native preparation returned a null handle for "
+      <> nativeImplementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+  MoonlightConstructionFailed fixture buildError ->
+    "Moonlight construction failed for " <> renderFixture fixture <> ": " <> show buildError
+  NativeConstructionFailed fixture implementation failure ->
+    "native construction failed for "
+      <> nativeImplementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+      <> ": "
+      <> show failure
+  SummaryDisagreement fixture implementation expected observed ->
+    "construction summary disagreement for "
+      <> implementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+      <> "; Moonlight="
+      <> show expected
+      <> ", observed="
+      <> show observed
+  ComparisonObstructions obstructions ->
+    unlines ("delaunay comparison obstructed:" : fmap (("  - " <>) . renderCompareObstruction) (NonEmpty.toList obstructions))
+
+renderFixture :: FixtureSpec -> String
+renderFixture fixture =
+  sizeBandLabel (fixtureSizeBand fixture)
+    <> "/"
+    <> pointDistributionLabel (fixturePointDistribution fixture)
+    <> "/"
+    <> show (pointCountValue (fixturePointCount fixture))
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
@@ -0,0 +1,307 @@
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Resource-safe dynamic boundary to the Rust crates under comparison.
+module Moonlight.Triangulation.Bench.DelaunayCompare.Native
+  ( NativeApi
+  , NativeFixture
+  , GeneratedCoordinates
+  , withNativeApi
+  , generateCoordinates
+  , generatedCoordinatePairs
+  , prepareNativeFixture
+  , runNativeFixture
+  , releaseNativeFixture
+  ) where
+
+import Control.Exception (IOException, bracket, displayException, try)
+import Control.Monad (filterM)
+import Control.Monad.Trans.Except (ExceptT (ExceptT), runExceptT)
+import Data.Bifunctor (first)
+import Data.Int (Int32)
+import Data.List (unfoldr)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
+import Foreign.C.Types (CDouble, CInt (..), CSize (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (FunPtr, Ptr, nullPtr)
+import Foreign.Storable (peek, poke)
+import qualified Data.Vector as Vector
+import qualified Data.Vector.Storable as StorableVector
+import qualified Data.Vector.Storable.Mutable as MutableStorableVector
+import System.Directory (doesFileExist, makeAbsolute)
+import System.Environment (lookupEnv)
+import System.Exit (ExitCode (..))
+import System.FilePath ((</>), takeDirectory)
+import System.Info (os)
+import System.Posix.DynamicLinker
+  ( DL
+  , RTLDFlags (RTLD_LOCAL, RTLD_NOW)
+  , dlclose
+  , dlopen
+  , dlsym
+  )
+import System.Process
+  ( CreateProcess (cwd)
+  , proc
+  , readCreateProcessWithExitCode
+  )
+
+type GenerateFunction = CInt -> CSize -> Ptr CDouble -> CSize -> IO CInt
+type PrepareFunction = CInt -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> IO CInt
+type RunFunction = Ptr () -> Ptr CSize -> Ptr CSize -> IO CInt
+type ReleaseFunction = Ptr () -> IO ()
+
+foreign import ccall safe "dynamic"
+  bindGenerateFunction :: FunPtr GenerateFunction -> GenerateFunction
+
+foreign import ccall safe "dynamic"
+  bindPrepareFunction :: FunPtr PrepareFunction -> PrepareFunction
+
+foreign import ccall safe "dynamic"
+  bindRunFunction :: FunPtr RunFunction -> RunFunction
+
+foreign import ccall unsafe "dynamic"
+  bindReleaseFunction :: FunPtr ReleaseFunction -> ReleaseFunction
+
+data NativeApi = NativeApi
+  { nativeGenerate :: !GenerateFunction
+  , nativePrepare :: !PrepareFunction
+  , nativeRun :: !RunFunction
+  , nativeRelease :: !ReleaseFunction
+  }
+
+newtype NativeFixture = NativeFixture (Ptr ())
+
+data GeneratedCoordinates = GeneratedCoordinates
+  { generatedPointCount :: !PointCount
+  , generatedCoordinateValues :: !(StorableVector.Vector CDouble)
+  }
+
+withNativeApi
+  :: (NativeApi -> IO (Either CompareObstruction result))
+  -> IO (Either CompareObstruction result)
+withNativeApi action = do
+  manifestResult <- resolveRustManifest
+  case manifestResult of
+    Left obstruction -> pure (Left obstruction)
+    Right manifest -> do
+      libraryResult <- buildRustLibrary manifest
+      case libraryResult of
+        Left obstruction -> pure (Left obstruction)
+        Right libraryPath -> withLoadedNativeApi libraryPath action
+
+resolveRustManifest :: IO (Either CompareObstruction FilePath)
+resolveRustManifest = do
+  configured <- lookupEnv "MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST"
+  let candidates =
+        case configured of
+          Just path -> path :| []
+          Nothing ->
+            "docs/delaunay-compare/rust/Cargo.toml"
+              :| [ "foundation/moonlight-triangulation/docs/delaunay-compare/rust/Cargo.toml"
+                 , "compiler/foundation/moonlight-triangulation/docs/delaunay-compare/rust/Cargo.toml"
+                 ]
+  existing <- filterM doesFileExist (NonEmpty.toList candidates)
+  case existing of
+    manifest : _ -> Right <$> makeAbsolute manifest
+    [] -> pure (Left (RustManifestNotFound candidates))
+
+buildRustLibrary :: FilePath -> IO (Either CompareObstruction FilePath)
+buildRustLibrary manifest = do
+  libraryNameResult <- dynamicLibraryName
+  case libraryNameResult of
+    Left obstruction -> pure (Left obstruction)
+    Right libraryName -> do
+      let rustRoot = takeDirectory manifest
+          targetDirectory = rustRoot </> "target"
+          command =
+            ( proc
+                "cargo"
+                [ "build"
+                , "--quiet"
+                , "--release"
+                , "--lib"
+                , "--manifest-path"
+                , manifest
+                , "--target-dir"
+                , targetDirectory
+                ]
+            )
+              { cwd = Just rustRoot
+              }
+      invocation <- try @IOException (readCreateProcessWithExitCode command "")
+      pure $
+        case invocation of
+          Left exception ->
+            Left (CargoInvocationFailed manifest (displayException exception))
+          Right (ExitFailure exitCode, standardOutput, standardError) ->
+            Left
+              ( CargoBuildFailed
+                  manifest
+                  exitCode
+                  (standardError <> standardOutput)
+              )
+          Right (ExitSuccess, _, _) ->
+            Right (targetDirectory </> "release" </> libraryName)
+
+dynamicLibraryName :: IO (Either CompareObstruction FilePath)
+dynamicLibraryName =
+  pure $
+    case os of
+      "darwin" -> Right "libmoonlight_delaunay_compare_referents.dylib"
+      "linux" -> Right "libmoonlight_delaunay_compare_referents.so"
+      host -> Left (UnsupportedDynamicLibraryHost host)
+
+withLoadedNativeApi
+  :: FilePath
+  -> (NativeApi -> IO (Either CompareObstruction result))
+  -> IO (Either CompareObstruction result)
+withLoadedNativeApi libraryPath action = do
+  opened <- try @IOException (dlopen libraryPath [RTLD_NOW, RTLD_LOCAL])
+  case opened of
+    Left exception ->
+      pure (Left (DynamicLibraryOpenFailed libraryPath (displayException exception)))
+    Right handle ->
+      bracket
+        (pure handle)
+        dlclose
+        ( \loadedHandle -> do
+            apiResult <- loadNativeApi libraryPath loadedHandle
+            case apiResult of
+              Left obstruction -> pure (Left obstruction)
+              Right api -> action api
+        )
+
+loadNativeApi :: FilePath -> DL -> IO (Either CompareObstruction NativeApi)
+loadNativeApi libraryPath handle =
+  runExceptT $
+    NativeApi
+      <$> loadSymbol libraryPath handle "delaunay_compare_generate" bindGenerateFunction
+      <*> loadSymbol libraryPath handle "delaunay_compare_prepare" bindPrepareFunction
+      <*> loadSymbol libraryPath handle "delaunay_compare_run" bindRunFunction
+      <*> loadSymbol libraryPath handle "delaunay_compare_release" bindReleaseFunction
+
+loadSymbol
+  :: FilePath
+  -> DL
+  -> String
+  -> (FunPtr function -> boundFunction)
+  -> ExceptT CompareObstruction IO boundFunction
+loadSymbol libraryPath handle symbol bind =
+  ExceptT $ do
+    loaded <- try @IOException (dlsym handle symbol)
+    pure $
+      first
+        (DynamicSymbolLoadFailed libraryPath symbol . displayException)
+        (bind <$> loaded)
+
+generateCoordinates
+  :: NativeApi
+  -> FixtureSpec
+  -> IO (Either NativeStatus GeneratedCoordinates)
+generateCoordinates api fixture = do
+  let pointCount = fixturePointCount fixture
+      coordinateCount = 2 * pointCountValue pointCount
+  mutableCoordinates <- MutableStorableVector.new coordinateCount
+  statusCode <-
+    MutableStorableVector.unsafeWith mutableCoordinates $ \coordinatePointer ->
+      nativeGenerate api
+        (distributionTag (fixturePointDistribution fixture))
+        (fromIntegral (pointCountValue pointCount))
+        coordinatePointer
+        (fromIntegral coordinateCount)
+  case decodeNativeStatus statusCode of
+    Left status -> pure (Left status)
+    Right () -> do
+      coordinates <- StorableVector.unsafeFreeze mutableCoordinates
+      pure (Right (GeneratedCoordinates pointCount coordinates))
+
+generatedCoordinatePairs :: GeneratedCoordinates -> Vector.Vector (Double, Double)
+generatedCoordinatePairs generated =
+  Vector.fromList
+    ( unfoldr
+        takeCoordinatePair
+        (realToFrac <$> StorableVector.toList (generatedCoordinateValues generated))
+    )
+ where
+  takeCoordinatePair :: [Double] -> Maybe ((Double, Double), [Double])
+  takeCoordinatePair = \case
+    x : y : remaining -> Just ((x, y), remaining)
+    _ -> Nothing
+
+prepareNativeFixture
+  :: NativeApi
+  -> NativeImplementation
+  -> GeneratedCoordinates
+  -> IO (Either NativePreparationFailure NativeFixture)
+prepareNativeFixture api implementation coordinates =
+  alloca $ \preparedOutput -> do
+    poke preparedOutput nullPtr
+    statusCode <-
+      StorableVector.unsafeWith (generatedCoordinateValues coordinates) $ \coordinatePointer ->
+        nativePrepare api
+          (implementationTag implementation)
+          coordinatePointer
+          (fromIntegral (pointCountValue (generatedPointCount coordinates)))
+          preparedOutput
+    case decodeNativeStatus statusCode of
+      Left status -> pure (Left (NativePreparationStatus status))
+      Right () -> do
+        prepared <- peek preparedOutput
+        pure $
+          if prepared == nullPtr
+            then Left NativePreparationReturnedNull
+            else Right (NativeFixture prepared)
+
+runNativeFixture
+  :: NativeApi
+  -> NativeFixture
+  -> IO (Either NativeRunFailure DelaunaySummary)
+runNativeFixture api (NativeFixture prepared) =
+  alloca $ \vertexCountOutput ->
+    alloca $ \triangleCountOutput -> do
+      statusCode <- nativeRun api prepared vertexCountOutput triangleCountOutput
+      case decodeNativeStatus statusCode of
+        Left status -> pure (Left (NativeRunStatus status))
+        Right () -> do
+          vertexCount <- peek vertexCountOutput
+          triangleCount <- peek triangleCountOutput
+          pure $ DelaunaySummary <$> cSizeToInt vertexCount <*> cSizeToInt triangleCount
+
+releaseNativeFixture :: NativeApi -> NativeFixture -> IO ()
+releaseNativeFixture api (NativeFixture prepared) = nativeRelease api prepared
+
+decodeNativeStatus :: CInt -> Either NativeStatus ()
+decodeNativeStatus statusCode =
+  case fromIntegral statusCode :: Int32 of
+    0 -> Right ()
+    1 -> Left NativeNullPointer
+    2 -> Left NativeUnknownTag
+    3 -> Left NativeCoordinateCountMismatch
+    4 -> Left NativeDistributionConstructionFailed
+    5 -> Left NativeTriangulationFailed
+    6 -> Left NativePanicked
+    unknown -> Left (NativeUnknownStatus (fromIntegral unknown))
+
+cSizeToInt :: CSize -> Either NativeRunFailure Int
+cSizeToInt value =
+  let integerValue = toInteger value
+   in if integerValue > toInteger (maxBound :: Int)
+        then Left (NativeCountExceedsHaskellInt integerValue)
+        else Right (fromInteger integerValue)
+
+distributionTag :: PointDistribution -> CInt
+distributionTag = \case
+  LocalInsertion -> 0
+  Uniform -> 1
+
+implementationTag :: NativeImplementation -> CInt
+implementationTag = \case
+  NativeSpade -> 0
+  NativeSpadeHierarchy -> 1
+  NativeCdt -> 2
+  NativeDelaunator -> 3
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs
@@ -0,0 +1,466 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | Typed receipt parsing and pure Spade-style SVG projection.
+module Moonlight.Triangulation.Bench.DelaunayCompare.Picture
+  ( BenchmarkReceipt
+  , PictureObstruction (..)
+  , parseBenchmarkReceipt
+  , pictureFileName
+  , renderComparisonPicture
+  , renderPictureObstruction
+  ) where
+
+import Data.List (group, sort, sortOn)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (mapMaybe)
+import qualified Data.Set as Set
+import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
+import Numeric (showFFloat)
+import Text.Read (readMaybe)
+
+data ObservationKey = ObservationKey
+  { observationFixture :: !FixtureSpec
+  , observationImplementation :: !Implementation
+  }
+  deriving stock (Eq, Ord, Show)
+
+data BenchmarkObservation = BenchmarkObservation
+  { benchmarkKey :: !ObservationKey
+  , benchmarkMeanPicoseconds :: !Double
+  , benchmarkSpreadPicoseconds :: !Double
+  }
+  deriving stock (Eq, Show)
+
+newtype BenchmarkReceipt = BenchmarkReceipt
+  { receiptObservations :: Map.Map ObservationKey BenchmarkObservation
+  }
+
+data PictureObstruction
+  = MissingCsvHeader
+  | UnexpectedCsvHeader !String
+  | MalformedCsvRow !Int !String
+  | UnknownBenchmarkName !Int !String
+  | InvalidBenchmarkScalar !Int !String !String
+  | DuplicateBenchmarkObservations ![ObservationKey]
+  | MissingBenchmarkObservations ![ObservationKey]
+  deriving stock (Eq, Show)
+
+parseBenchmarkReceipt :: String -> Either PictureObstruction BenchmarkReceipt
+parseBenchmarkReceipt source =
+  case lines source of
+    [] -> Left MissingCsvHeader
+    header : rows
+      | header /= expectedCsvHeader -> Left (UnexpectedCsvHeader header)
+      | otherwise -> do
+          observations <- traverse (uncurry parseObservationRow) (zip [2 ..] rows)
+          let keys = benchmarkKey <$> observations
+              duplicateKeys =
+                mapMaybe
+                  ( \case
+                      key : _ : _ -> Just key
+                      _ -> Nothing
+                  )
+                  (group (sort keys))
+              observationMap = Map.fromList ((\observation -> (benchmarkKey observation, observation)) <$> observations)
+              missingKeys = Set.toList (expectedObservationKeys `Set.difference` Map.keysSet observationMap)
+          case (duplicateKeys, missingKeys) of
+            (duplicate : duplicates, _) -> Left (DuplicateBenchmarkObservations (duplicate : duplicates))
+            ([], missing : remaining) -> Left (MissingBenchmarkObservations (missing : remaining))
+            ([], []) -> Right (BenchmarkReceipt observationMap)
+
+renderComparisonPicture
+  :: SizeBand
+  -> BenchmarkReceipt
+  -> String
+renderComparisonPicture sizeBand receipt =
+  renderSvg sizeBand pointCounts axisMaximum panels
+ where
+  pointCounts = pointCountValue <$> pointCountsFor sizeBand
+  panels = picturePanel receipt sizeBand <$> allPointDistributions
+  allObservations = concatMap (concatMap pictureSeriesObservations . picturePanelSeries) panels
+  maximumMilliseconds =
+    foldr
+      (max . observationUpperMilliseconds)
+      0
+      allObservations
+  axisMaximum = niceCeiling maximumMilliseconds
+
+pictureFileName :: SizeBand -> FilePath
+pictureFileName = \case
+  Small -> "moonlight-delaunay-compare-small.svg"
+  Big -> "moonlight-delaunay-compare-big.svg"
+
+renderPictureObstruction :: PictureObstruction -> String
+renderPictureObstruction = \case
+  MissingCsvHeader -> "benchmark CSV has no header"
+  UnexpectedCsvHeader header ->
+    "benchmark CSV header was " <> show header <> "; expected " <> show expectedCsvHeader
+  MalformedCsvRow lineNumber row ->
+    "benchmark CSV row " <> show lineNumber <> " is not a three-column tasty-bench row: " <> show row
+  UnknownBenchmarkName lineNumber name ->
+    "benchmark CSV row " <> show lineNumber <> " names an unknown comparison case: " <> show name
+  InvalidBenchmarkScalar lineNumber field value ->
+    "benchmark CSV row "
+      <> show lineNumber
+      <> " has invalid "
+      <> field
+      <> ": "
+      <> show value
+  DuplicateBenchmarkObservations keys ->
+    "benchmark CSV repeats comparison cases: " <> show keys
+  MissingBenchmarkObservations keys ->
+    "benchmark CSV omits comparison cases: " <> show keys
+
+expectedCsvHeader :: String
+expectedCsvHeader = "Name,Mean (ps),2*Stdev (ps)"
+
+expectedObservationKeys :: Set.Set ObservationKey
+expectedObservationKeys =
+  Set.fromList (liftA2 ObservationKey allFixtureSpecs allImplementations)
+
+expectedBenchmarkNames :: Map.Map String ObservationKey
+expectedBenchmarkNames =
+  Map.fromList ((\key -> (benchmarkName key, key)) <$> Set.toList expectedObservationKeys)
+
+benchmarkName :: ObservationKey -> String
+benchmarkName key =
+  "All.comparison: creation benchmark ("
+    <> sizeBandLabel (fixtureSizeBand fixture)
+    <> ")."
+    <> implementationLabel (observationImplementation key)
+    <> "."
+    <> pointDistributionLabel (fixturePointDistribution fixture)
+    <> "."
+    <> show (pointCountValue (fixturePointCount fixture))
+ where
+  fixture = observationFixture key
+
+parseObservationRow :: Int -> String -> Either PictureObstruction BenchmarkObservation
+parseObservationRow lineNumber row =
+  case csvTriple row of
+    Nothing -> Left (MalformedCsvRow lineNumber row)
+    Just (name, rawMean, rawSpread) -> do
+      key <- maybe (Left (UnknownBenchmarkName lineNumber name)) Right (Map.lookup name expectedBenchmarkNames)
+      meanPicoseconds <- parseNonNegativeScalar lineNumber "mean picoseconds" rawMean
+      spreadPicoseconds <- parseNonNegativeScalar lineNumber "two-standard-deviation spread" rawSpread
+      pure
+        BenchmarkObservation
+          { benchmarkKey = key
+          , benchmarkMeanPicoseconds = meanPicoseconds
+          , benchmarkSpreadPicoseconds = spreadPicoseconds
+          }
+
+csvTriple :: String -> Maybe (String, String, String)
+csvTriple row =
+  case break (== ',') row of
+    (firstColumn, ',' : remaining) ->
+      case break (== ',') remaining of
+        (secondColumn, ',' : thirdColumn)
+          | ',' `notElem` thirdColumn -> Just (firstColumn, secondColumn, thirdColumn)
+        _ -> Nothing
+    _ -> Nothing
+
+parseNonNegativeScalar :: Int -> String -> String -> Either PictureObstruction Double
+parseNonNegativeScalar lineNumber field rawValue =
+  case readMaybe rawValue of
+    Just value
+      | value >= 0 && not (isInfinite value) && not (isNaN value) -> Right value
+    _ -> Left (InvalidBenchmarkScalar lineNumber field rawValue)
+
+data PictureSeries = PictureSeries
+  { pictureSeriesImplementation :: !Implementation
+  , pictureSeriesObservations :: ![BenchmarkObservation]
+  }
+
+data PicturePanel = PicturePanel
+  { picturePanelDistribution :: !PointDistribution
+  , picturePanelSeries :: ![PictureSeries]
+  }
+
+picturePanel
+  :: BenchmarkReceipt
+  -> SizeBand
+  -> PointDistribution
+  -> PicturePanel
+picturePanel receipt sizeBand distribution =
+  PicturePanel distribution (pictureSeries receipt sizeBand distribution <$> allImplementations)
+
+pictureSeries
+  :: BenchmarkReceipt
+  -> SizeBand
+  -> PointDistribution
+  -> Implementation
+  -> PictureSeries
+pictureSeries receipt sizeBand distribution implementation =
+  PictureSeries implementation observations
+ where
+  observations =
+    sortOn (pointCountValue . fixturePointCount . observationFixture . benchmarkKey)
+      [ observation
+      | observation <- Map.elems (receiptObservations receipt)
+      , let key = benchmarkKey observation
+            fixture = observationFixture key
+      , fixtureSizeBand fixture == sizeBand
+      , fixturePointDistribution fixture == distribution
+      , observationImplementation key == implementation
+      ]
+
+data PictureLayout = PictureLayout
+  { pictureWidth :: !Double
+  , pictureHeight :: !Double
+  , picturePlotTop :: !Double
+  , picturePlotBottom :: !Double
+  , picturePanelWidth :: !Double
+  , picturePanelLefts :: ![Double]
+  }
+
+pictureLayout :: PictureLayout
+pictureLayout =
+  PictureLayout
+    { pictureWidth = 1280
+    , pictureHeight = 720
+    , picturePlotTop = 168
+    , picturePlotBottom = 610
+    , picturePanelWidth = 540
+    , picturePanelLefts = [70, 670]
+    }
+
+renderSvg :: SizeBand -> [Int] -> Double -> [PicturePanel] -> String
+renderSvg sizeBand pointCounts axisMaximum panels =
+  unlines
+    ( [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+      , "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 "
+          <> coordinate (pictureWidth pictureLayout)
+          <> " "
+          <> coordinate (pictureHeight pictureLayout)
+          <> "\" role=\"img\" aria-labelledby=\"title description\">"
+      , "<title id=\"title\">" <> pictureTitle sizeBand <> "</title>"
+      , "<desc id=\"description\">CPU construction time for Spade, cdt, delaunator, and Moonlight over the upstream Delaunay comparison fixtures. Lower lines are faster.</desc>"
+      , "<defs>"
+      , "  <pattern id=\"triangulation-mesh\" width=\"54\" height=\"46\" patternUnits=\"userSpaceOnUse\">"
+      , "    <path d=\"M0 46L27 0L54 46ZM0 46L54 46M27 0L27 46\" fill=\"none\" stroke=\"rgb(255,140,0)\" stroke-width=\"0.7\"/>"
+      , "  </pattern>"
+      , "  <clipPath id=\"header-mesh-clip\"><rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" rx=\"20\"/></clipPath>"
+      , "</defs>"
+      , "<rect width=\""
+          <> coordinate (pictureWidth pictureLayout)
+          <> "\" height=\""
+          <> coordinate (pictureHeight pictureLayout)
+          <> "\" fill=\"white\"/>"
+      , "<rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" fill=\"url(#triangulation-mesh)\" opacity=\"0.10\" clip-path=\"url(#header-mesh-clip)\"/>"
+      , svgText 640 34 "middle" 24 "500" (pictureTitle sizeBand)
+      , svgText 640 61 "middle" 13 "400" "Moonlight against Spade's construction referents · CPU time · lower is faster"
+      ]
+        <> renderLegend
+        <> concat (zipWith (renderPanel pointCounts axisMaximum) (picturePanelLefts pictureLayout) panels)
+        <> [ svgText 640 704 "middle" 11 "400" "Whiskers show tasty-bench's reported 2× standard-deviation spread."
+           , "</svg>"
+           ]
+    )
+
+pictureTitle :: SizeBand -> String
+pictureTitle = \case
+  Small -> "Moonlight Delaunay construction · small point sets"
+  Big -> "Moonlight Delaunay construction · big point sets"
+
+renderLegend :: [String]
+renderLegend =
+  concat
+    ( zipWith
+        renderLegendItem
+        [105, 315, 560, 735, 940]
+        allImplementations
+    )
+
+renderLegendItem :: Double -> Implementation -> [String]
+renderLegendItem x implementation =
+  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
+  , "  <line x1=\"" <> coordinate x <> "\" y1=\"101\" x2=\"" <> coordinate (x + 34) <> "\" y2=\"101\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\"" <> seriesDash implementation <> "/>"
+  , "  " <> renderMarker implementation (x + 17) 101
+  , "  " <> svgText (x + 43) 105 "start" 12 "400" (implementationLabel implementation)
+  , "</g>"
+  ]
+
+renderPanel :: [Int] -> Double -> Double -> PicturePanel -> [String]
+renderPanel pointCounts axisMaximum panelLeft panel =
+  [ "<g aria-label=\"" <> panelLabel <> "\">"
+  , svgText (panelLeft + panelWidth / 2) 144 "middle" 15 "500" panelLabel
+  ]
+    <> concatMap (renderHorizontalGrid panelLeft panelWidth axisMaximum) [0 .. 5]
+    <> concatMap (renderVerticalTick panelLeft panelWidth pointCounts) pointCounts
+    <> [ "<rect x=\"" <> coordinate panelLeft <> "\" y=\"" <> coordinate plotTop <> "\" width=\"" <> coordinate panelWidth <> "\" height=\"" <> coordinate plotHeight <> "\" fill=\"none\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
+       , svgText (panelLeft + panelWidth / 2) 664 "middle" 12 "400" "input size (points)"
+       , "<text x=\"" <> coordinate (panelLeft - 55) <> "\" y=\"" <> coordinate (plotTop + plotHeight / 2) <> "\" text-anchor=\"middle\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"12\" fill=\"rgb(20,20,20)\" transform=\"rotate(-90 " <> coordinate (panelLeft - 55) <> " " <> coordinate (plotTop + plotHeight / 2) <> ")\">CPU time (ms)</text>"
+       ]
+    <> concatMap (renderSeries panelLeft panelWidth pointCounts axisMaximum) (picturePanelSeries panel)
+    <> ["</g>"]
+ where
+  panelLabel = pointDistributionLabel (picturePanelDistribution panel)
+  panelWidth = picturePanelWidth pictureLayout
+  plotTop = picturePlotTop pictureLayout
+  plotHeight = picturePlotBottom pictureLayout - plotTop
+
+renderHorizontalGrid :: Double -> Double -> Double -> Int -> [String]
+renderHorizontalGrid panelLeft panelWidth axisMaximum tickIndex =
+  [ "<line x1=\"" <> coordinate panelLeft <> "\" y1=\"" <> coordinate y <> "\" x2=\"" <> coordinate (panelLeft + panelWidth) <> "\" y2=\"" <> coordinate y <> "\" stroke=\"rgb(170,170,170)\" stroke-width=\"0.7\" stroke-dasharray=\"2 4\"/>"
+  , svgText (panelLeft - 10) (y + 4) "end" 11 "400" (axisLabel tickValue)
+  ]
+ where
+  tickValue = axisMaximum * fromIntegral tickIndex / 5
+  y = pictureY axisMaximum tickValue
+
+renderVerticalTick :: Double -> Double -> [Int] -> Int -> [String]
+renderVerticalTick panelLeft panelWidth pointCounts pointCount =
+  [ "<line x1=\"" <> coordinate x <> "\" y1=\"610\" x2=\"" <> coordinate x <> "\" y2=\"616\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
+  , svgText x 635 "middle" 11 "400" (pointCountLabel pointCount)
+  ]
+ where
+  x = pictureX panelLeft panelWidth pointCounts pointCount
+
+renderSeries :: Double -> Double -> [Int] -> Double -> PictureSeries -> [String]
+renderSeries panelLeft panelWidth pointCounts axisMaximum series =
+  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
+  , "  <polyline points=\"" <> unwords (observationPoint <$> observations) <> "\" fill=\"none\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\" stroke-linecap=\"round\" stroke-linejoin=\"round\"" <> seriesDash implementation <> "/>"
+  ]
+    <> concatMap renderObservation observations
+    <> ["</g>"]
+ where
+  implementation = pictureSeriesImplementation series
+  observations = pictureSeriesObservations series
+  observationPoint observation =
+    coordinate (observationX observation) <> "," <> coordinate (observationY observation)
+  observationX =
+    pictureX panelLeft panelWidth pointCounts
+      . pointCountValue
+      . fixturePointCount
+      . observationFixture
+      . benchmarkKey
+  observationY = pictureY axisMaximum . observationMeanMilliseconds
+  renderObservation observation =
+    [ "  <line x1=\"" <> coordinate x <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate x <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
+    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate upperY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
+    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate lowerY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
+    , "  " <> renderMarker implementation x y
+    ]
+   where
+    x = observationX observation
+    y = observationY observation
+    upperY = pictureY axisMaximum (observationUpperMilliseconds observation)
+    lowerY = pictureY axisMaximum (observationLowerMilliseconds observation)
+
+implementationColor :: Implementation -> String
+implementationColor = \case
+  Spade -> "rgb(0,0,139)"
+  SpadeHierarchy -> "rgb(139,0,139)"
+  Cdt -> "rgb(178,34,34)"
+  Delaunator -> "rgb(0,139,139)"
+  Moonlight -> "rgb(255,140,0)"
+
+seriesStrokeWidth :: Implementation -> String
+seriesStrokeWidth = \case
+  Moonlight -> "3.2"
+  _ -> "2.0"
+
+seriesDash :: Implementation -> String
+seriesDash = \case
+  SpadeHierarchy -> " stroke-dasharray=\"7 4\""
+  _ -> ""
+
+renderMarker :: Implementation -> Double -> Double -> String
+renderMarker implementation x y =
+  case implementation of
+    Spade -> circle 3.2
+    SpadeHierarchy ->
+      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.3) <> " L " <> coordinate (x - 4.1) <> " " <> coordinate (y + 3.5) <> " L " <> coordinate (x + 4.1) <> " " <> coordinate (y + 3.5) <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+    Cdt ->
+      "<rect x=\"" <> coordinate (x - 3.2) <> "\" y=\"" <> coordinate (y - 3.2) <> "\" width=\"6.4\" height=\"6.4\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+    Delaunator ->
+      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.2) <> " L " <> coordinate (x + 4.2) <> " " <> coordinate y <> " L " <> coordinate x <> " " <> coordinate (y + 4.2) <> " L " <> coordinate (x - 4.2) <> " " <> coordinate y <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+    Moonlight ->
+      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 5.4) <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y - 1.7) <> " L " <> coordinate (x + 5.4) <> " " <> coordinate y <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate x <> " " <> coordinate (y + 5.4) <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate (x - 5.4) <> " " <> coordinate y <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y - 1.7) <> " Z\" fill=\"" <> color <> "\" stroke=\"white\" stroke-width=\"1\"/>"
+ where
+  color = implementationColor implementation
+  circle radius =
+    "<circle cx=\"" <> coordinate x <> "\" cy=\"" <> coordinate y <> "\" r=\"" <> coordinate radius <> "\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+
+pictureX :: Double -> Double -> [Int] -> Int -> Double
+pictureX panelLeft panelWidth pointCounts pointCount =
+  case pointCounts of
+    [] -> panelLeft
+    firstCount : remainingCounts ->
+      let minimumCount = firstCount
+          maximumCount = foldr max firstCount remainingCounts
+          countRange = maximumCount - minimumCount
+       in if countRange == 0
+            then panelLeft + panelWidth / 2
+            else
+              panelLeft
+                + panelWidth
+                  * fromIntegral (pointCount - minimumCount)
+                  / fromIntegral countRange
+
+pictureY :: Double -> Double -> Double
+pictureY axisMaximum milliseconds =
+  picturePlotBottom pictureLayout
+    - plotHeight * min axisMaximum (max 0 milliseconds) / axisMaximum
+ where
+  plotHeight = picturePlotBottom pictureLayout - picturePlotTop pictureLayout
+
+observationMeanMilliseconds :: BenchmarkObservation -> Double
+observationMeanMilliseconds = picosecondsToMilliseconds . benchmarkMeanPicoseconds
+
+observationUpperMilliseconds :: BenchmarkObservation -> Double
+observationUpperMilliseconds observation =
+  picosecondsToMilliseconds
+    (benchmarkMeanPicoseconds observation + benchmarkSpreadPicoseconds observation)
+
+observationLowerMilliseconds :: BenchmarkObservation -> Double
+observationLowerMilliseconds observation =
+  picosecondsToMilliseconds
+    (max 0 (benchmarkMeanPicoseconds observation - benchmarkSpreadPicoseconds observation))
+
+picosecondsToMilliseconds :: Double -> Double
+picosecondsToMilliseconds picoseconds = picoseconds / 1.0e9
+
+niceCeiling :: Double -> Double
+niceCeiling value
+  | value <= 0 = 1
+  | otherwise = niceFraction * magnitude
+ where
+  magnitude = 10 ** fromIntegral (floor (logBase 10 value) :: Int)
+  fraction = value / magnitude
+  niceFraction
+    | fraction <= 1 = 1
+    | fraction <= 2 = 2
+    | fraction <= 5 = 5
+    | otherwise = 10
+
+axisLabel :: Double -> String
+axisLabel value
+  | value >= 10 = showFFloat (Just 0) value ""
+  | value >= 1 = showFFloat (Just 1) value ""
+  | otherwise = showFFloat (Just 2) value ""
+
+pointCountLabel :: Int -> String
+pointCountLabel pointCount
+  | pointCount `mod` 1000 == 0 = show (pointCount `div` 1000) <> "k"
+  | otherwise = show pointCount
+
+coordinate :: Double -> String
+coordinate value = showFFloat (Just 1) value ""
+
+svgText :: Double -> Double -> String -> Int -> String -> String -> String
+svgText x y anchor fontSize fontWeight content =
+  "<text x=\""
+    <> coordinate x
+    <> "\" y=\""
+    <> coordinate y
+    <> "\" text-anchor=\""
+    <> anchor
+    <> "\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\""
+    <> show fontSize
+    <> "\" font-weight=\""
+    <> fontWeight
+    <> "\" fill=\"rgb(20,20,20)\">"
+    <> content
+    <> "</text>"
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs
@@ -0,0 +1,258 @@
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Fixture descent, compatibility gluing, and the authoritative benchmark view.
+module Moonlight.Triangulation.Bench.DelaunayCompare.Suite
+  ( PreparedSuite
+  , withPreparedSuite
+  , preflightSuite
+  , suiteBenchmarks
+  , suiteAgreementMessage
+  ) where
+
+import Control.DeepSeq (force)
+import Control.Exception (bracket, evaluate)
+import Data.Bifunctor (first)
+import Data.Either (lefts, rights)
+import Data.Foldable (toList, traverse_)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Vector as Vector
+import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
+import Moonlight.Triangulation.Bench.DelaunayCompare.Native
+import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
+import Moonlight.Triangulation.Dcel (numInnerFaces, numVertices)
+import Moonlight.Triangulation.Types (BuildError, Point (Point))
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf, nfIO)
+
+data NativeSections value = NativeSections
+  { nativeSpadeSection :: !value
+  , nativeSpadeHierarchySection :: !value
+  , nativeCdtSection :: !value
+  , nativeDelaunatorSection :: !value
+  }
+  deriving stock (Functor, Foldable, Traversable)
+
+nativeImplementationSections :: NativeSections NativeImplementation
+nativeImplementationSections =
+  NativeSections
+    { nativeSpadeSection = NativeSpade
+    , nativeSpadeHierarchySection = NativeSpadeHierarchy
+    , nativeCdtSection = NativeCdt
+    , nativeDelaunatorSection = NativeDelaunator
+    }
+
+nativeSectionAt :: NativeImplementation -> NativeSections value -> value
+nativeSectionAt implementation sections =
+  case implementation of
+    NativeSpade -> nativeSpadeSection sections
+    NativeSpadeHierarchy -> nativeSpadeHierarchySection sections
+    NativeCdt -> nativeCdtSection sections
+    NativeDelaunator -> nativeDelaunatorSection sections
+
+data PreparedFixture = PreparedFixture
+  { preparedSpec :: !FixtureSpec
+  , preparedMoonlightPoints :: !(Vector.Vector Point)
+  , preparedNativeFixtures :: !(NativeSections NativeFixture)
+  }
+
+newtype PreparedSuite = PreparedSuite [PreparedFixture]
+
+withPreparedSuite
+  :: NativeApi
+  -> (PreparedSuite -> IO (Either CompareObstruction result))
+  -> IO (Either CompareObstruction result)
+withPreparedSuite api action = do
+  prepared <- prepareSuite api
+  case prepared of
+    Left obstruction -> pure (Left obstruction)
+    Right suite -> bracket (pure suite) (releaseSuite api) action
+
+prepareSuite :: NativeApi -> IO (Either CompareObstruction PreparedSuite)
+prepareSuite api = do
+  outcomes <- traverse (prepareFixture api) allFixtureSpecs
+  case NonEmpty.nonEmpty (lefts outcomes) of
+    Just obstructions -> do
+      traverse_ (releasePreparedFixture api) (rights outcomes)
+      pure (Left (ComparisonObstructions obstructions))
+    Nothing -> pure (Right (PreparedSuite (rights outcomes)))
+
+prepareFixture :: NativeApi -> FixtureSpec -> IO (Either CompareObstruction PreparedFixture)
+prepareFixture api fixture = do
+  generated <- generateCoordinates api fixture
+  case generated of
+    Left status -> pure (Left (FixtureGenerationFailed fixture status))
+    Right coordinates -> do
+      nativeFixtures <- prepareNativeFixtures api fixture coordinates
+      pure $
+        PreparedFixture
+          fixture
+          (Vector.map (uncurry Point) (generatedCoordinatePairs coordinates))
+          <$> nativeFixtures
+
+prepareNativeFixtures
+  :: NativeApi
+  -> FixtureSpec
+  -> GeneratedCoordinates
+  -> IO (Either CompareObstruction (NativeSections NativeFixture))
+prepareNativeFixtures api fixture coordinates = do
+  outcomes <- traverse prepareSection nativeImplementationSections
+  case sequenceA outcomes of
+    Right nativeFixtures -> pure (Right nativeFixtures)
+    Left obstruction -> do
+      traverse_ (releaseNativeFixture api) (rights (toList outcomes))
+      pure (Left obstruction)
+ where
+  prepareSection implementation = do
+    prepared <- prepareNativeFixture api implementation coordinates
+    pure $
+      first
+        (preparationObstruction implementation)
+        prepared
+
+  preparationObstruction implementation failure =
+    case failure of
+      NativePreparationStatus status -> FixturePreparationFailed fixture implementation status
+      NativePreparationReturnedNull -> NativePreparedNull fixture implementation
+
+releaseSuite :: NativeApi -> PreparedSuite -> IO ()
+releaseSuite api (PreparedSuite fixtures) =
+  traverse_ (releasePreparedFixture api) fixtures
+
+releasePreparedFixture :: NativeApi -> PreparedFixture -> IO ()
+releasePreparedFixture api = releasePreparedNativeFixtures api . preparedNativeFixtures
+
+releasePreparedNativeFixtures :: NativeApi -> NativeSections NativeFixture -> IO ()
+releasePreparedNativeFixtures api = traverse_ (releaseNativeFixture api)
+
+preflightSuite :: NativeApi -> PreparedSuite -> IO (Either CompareObstruction ())
+preflightSuite api (PreparedSuite fixtures) = do
+  outcomes <- traverse (preflightFixture api) fixtures
+  pure $
+    case NonEmpty.nonEmpty (lefts outcomes) of
+      Just obstructions -> Left (ComparisonObstructions obstructions)
+      Nothing -> Right ()
+
+preflightFixture :: NativeApi -> PreparedFixture -> IO (Either CompareObstruction ())
+preflightFixture api fixture = do
+  moonlightResult <-
+    evaluate
+      ( force
+          (moonlightConstructionSummary (preparedMoonlightPoints fixture))
+      )
+  nativeOutcomes <- traverse runSection nativeImplementationSections
+  pure $ do
+    moonlightSummary <-
+      first
+        (MoonlightConstructionFailed spec)
+        moonlightResult
+    nativeSummaries <- sequenceA nativeOutcomes
+    traverse_
+      ( \implementation ->
+          requireSummaryAgreement
+            spec
+            moonlightSummary
+            ( implementationOfNative implementation
+            , nativeSectionAt implementation nativeSummaries
+            )
+      )
+      nativeImplementationSections
+ where
+  spec = preparedSpec fixture
+  nativeFixtures = preparedNativeFixtures fixture
+  runSection implementation =
+    first (NativeConstructionFailed spec implementation)
+      <$> runNativeFixture api (nativeSectionAt implementation nativeFixtures)
+
+requireSummaryAgreement
+  :: FixtureSpec
+  -> DelaunaySummary
+  -> (Implementation, DelaunaySummary)
+  -> Either CompareObstruction ()
+requireSummaryAgreement fixture expected (implementation, observed) =
+  if observed == expected
+    then Right ()
+    else Left (SummaryDisagreement fixture implementation expected observed)
+
+moonlightConstructionSummary
+  :: Vector.Vector Point
+  -> Either BuildError DelaunaySummary
+moonlightConstructionSummary points = do
+  triangulation <- delaunayGeometry points
+  pure
+    DelaunaySummary
+      { summaryVertexCount = numVertices triangulation
+      , summaryTriangleCount = numInnerFaces triangulation
+      }
+
+suiteBenchmarks :: NativeApi -> PreparedSuite -> [Benchmark]
+suiteBenchmarks api (PreparedSuite fixtures) =
+  fmap (sizeBandBenchmarks api fixtures) allSizeBands
+
+sizeBandBenchmarks
+  :: NativeApi
+  -> [PreparedFixture]
+  -> SizeBand
+  -> Benchmark
+sizeBandBenchmarks api fixtures sizeBand =
+  bgroup
+    ("comparison: creation benchmark (" <> sizeBandLabel sizeBand <> ")")
+    (fmap (implementationBenchmarks api fixtures sizeBand) allImplementations)
+
+implementationBenchmarks
+  :: NativeApi
+  -> [PreparedFixture]
+  -> SizeBand
+  -> Implementation
+  -> Benchmark
+implementationBenchmarks api fixtures sizeBand implementation =
+  bgroup
+    (implementationLabel implementation)
+    ( fmap
+        (distributionBenchmarks api fixtures sizeBand implementation)
+        allPointDistributions
+    )
+
+distributionBenchmarks
+  :: NativeApi
+  -> [PreparedFixture]
+  -> SizeBand
+  -> Implementation
+  -> PointDistribution
+  -> Benchmark
+distributionBenchmarks api fixtures sizeBand implementation distribution =
+  bgroup
+    (pointDistributionLabel distribution)
+    ( fmap
+        (fixtureBenchmark api implementation)
+        ( filter
+            (matchesFixture sizeBand distribution . preparedSpec)
+            fixtures
+        )
+    )
+
+matchesFixture :: SizeBand -> PointDistribution -> FixtureSpec -> Bool
+matchesFixture sizeBand distribution fixture =
+  fixtureSizeBand fixture == sizeBand
+    && fixturePointDistribution fixture == distribution
+
+fixtureBenchmark :: NativeApi -> Implementation -> PreparedFixture -> Benchmark
+fixtureBenchmark api implementation fixture =
+  bench (show (pointCountValue (fixturePointCount (preparedSpec fixture)))) $
+    case implementation of
+      Moonlight -> nf moonlightConstructionSummary (preparedMoonlightPoints fixture)
+      Spade -> nativeBenchmark NativeSpade
+      SpadeHierarchy -> nativeBenchmark NativeSpadeHierarchy
+      Cdt -> nativeBenchmark NativeCdt
+      Delaunator -> nativeBenchmark NativeDelaunator
+ where
+  nativeFixtures = preparedNativeFixtures fixture
+  nativeBenchmark nativeImplementation =
+    nfIO (runNativeFixture api (nativeSectionAt nativeImplementation nativeFixtures))
+
+suiteAgreementMessage :: String
+suiteAgreementMessage =
+  "delaunay-compare agreement: "
+    <> show (length allFixtureSpecs)
+    <> " fixtures agree on vertex and triangle counts across "
+    <> show (length allImplementations)
+    <> " implementations"
diff --git a/docs/delaunay-compare/PictureMain.hs b/docs/delaunay-compare/PictureMain.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/PictureMain.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Thin effect boundary for rendering benchmark receipts as SVG pictures.
+module Main (main) where
+
+import Control.Exception (IOException, displayException, try)
+import Control.Monad.Trans.Except (ExceptT (ExceptT), except, runExceptT)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import Moonlight.Triangulation.Bench.DelaunayCompare.Domain (allSizeBands)
+import Moonlight.Triangulation.Bench.DelaunayCompare.Picture
+import System.Directory (createDirectoryIfMissing)
+import System.Environment (getArgs)
+import System.Exit (die)
+import System.FilePath ((</>))
+import System.IO (readFile')
+
+data PictureCommandFailure
+  = PictureCommandUsage
+  | PictureCommandReadFailed !FilePath !String
+  | PictureCommandCreateDirectoryFailed !FilePath !String
+  | PictureCommandReceiptObstructed !PictureObstruction
+  | PictureCommandWriteFailed !FilePath !String
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  outcome <- runExceptT (runPictureCommand arguments)
+  either (die . renderPictureCommandFailure) pure outcome
+
+runPictureCommand :: [String] -> ExceptT PictureCommandFailure IO ()
+runPictureCommand = \case
+  [inputCsv, outputDirectory] -> do
+    source <-
+      ExceptT
+        ( first
+            (PictureCommandReadFailed inputCsv . displayException)
+            <$> try @IOException (readFile' inputCsv)
+        )
+    receipt <- except (first PictureCommandReceiptObstructed (parseBenchmarkReceipt source))
+    let pictures =
+          ( \sizeBand ->
+              (pictureFileName sizeBand, renderComparisonPicture sizeBand receipt)
+          )
+            <$> allSizeBands
+    ExceptT
+      ( first
+          (PictureCommandCreateDirectoryFailed outputDirectory . displayException)
+          <$> try @IOException (createDirectoryIfMissing True outputDirectory)
+      )
+    traverse_ (writePicture outputDirectory) pictures
+  _ -> except (Left PictureCommandUsage)
+
+writePicture :: FilePath -> (FilePath, String) -> ExceptT PictureCommandFailure IO ()
+writePicture outputDirectory (fileName, svgDocument) =
+  let outputPath = outputDirectory </> fileName
+   in ExceptT
+        ( first
+            (PictureCommandWriteFailed outputPath . displayException)
+            <$> try @IOException (writeFile outputPath svgDocument)
+        )
+
+renderPictureCommandFailure :: PictureCommandFailure -> String
+renderPictureCommandFailure = \case
+  PictureCommandUsage ->
+    "usage: moonlight-triangulation-delaunay-pictures INPUT.csv OUTPUT_DIRECTORY"
+  PictureCommandReadFailed path details ->
+    "could not read benchmark CSV " <> path <> ": " <> details
+  PictureCommandCreateDirectoryFailed path details ->
+    "could not create picture directory " <> path <> ": " <> details
+  PictureCommandReceiptObstructed obstruction ->
+    renderPictureObstruction obstruction
+  PictureCommandWriteFailed path details ->
+    "could not write comparison picture " <> path <> ": " <> details
diff --git a/docs/delaunay-compare/README.md b/docs/delaunay-compare/README.md
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/README.md
@@ -0,0 +1,191 @@
+# Moonlight Delaunay Compare
+
+[Browse all exhibits.](../README.md)
+
+`moonlight-triangulation-delaunay-compare` is Moonlight's package-owned port of Spade's
+[`delaunay_compare`](https://github.com/Stoeoef/spade/tree/c8befc96bbbc1898a89cb19f9f3104a848936374/delaunay_compare)
+construction suite. It retains the upstream case matrix and appends
+`moonlight-triangulation` to the implementation list:
+
+1. `spade 2`
+2. `spade 2 hierarchy`
+3. `cdt`
+4. `delaunator`
+5. `moonlight-triangulation`
+
+Haskell owns fixture selection, grouping, compatibility descent, timing, and
+reporting. The Rust library under `rust/src/lib.rs` is only a typed foreign
+boundary to the four Rust implementations and to the exact upstream `rand`
+fixture stream; it contains no benchmark policy.
+
+This page owns only the construction slice. The
+[operation comparison](../../bench/spade-compare/README.md) owns the broader
+Moonlight–Spade board.
+
+## Pictures
+
+Both pictures are derived from the single retained board.
+
+![Small-point Delaunay construction comparison](results/moonlight-delaunay-compare-small.svg)
+
+![Big-point Delaunay construction comparison](results/moonlight-delaunay-compare-big.svg)
+
+These retain Spade's plain GNUPLOT idiom: white field, Helvetica, dashed grey
+grid, jewel-coloured point series, and the same small/big split. Moonlight is
+the heavier orange star. The faint triangulation mesh is the sole bit of
+levity; benchmark charts need not resemble tax forms.
+
+## Result
+
+- timed cases: `120`
+- fixture summaries agreeing across all five implementations: `24 / 24`
+- Moonlight mean below plain Spade: `24 / 24`
+- Moonlight mean below Spade hierarchy: `24 / 24`
+- Moonlight below plain Spade with non-overlapping reported 2σ: `18 / 24`
+- Moonlight below Spade hierarchy with non-overlapping reported 2σ: `22 / 24`
+- lowest fixture mean: `delaunator` `21 / 24`; `cdt` `3 / 24`
+- median reported 2σ / mean: Moonlight `6.88%`; plain Spade `6.67%`
+
+Selected means are in milliseconds. Δ is
+`100 × (Moonlight / competitor − 1)`.
+
+| fixture | points | Moonlight | plain Spade | Δ | Spade hierarchy | Δ |
+|---|---:|---:|---:|---:|---:|---:|
+| local insertion | 4,000 | 0.587 | 0.771 | −23.8% | 0.800 | −26.6% |
+| local insertion | 6,000 | 0.971 | 1.300 | −25.3% | 1.365 | −28.9% |
+| local insertion | 8,000 | 1.354 | 1.776 | −23.7% | 1.903 | −28.8% |
+| local insertion | 250,000 | 60.300 | 72.713 | −17.1% | 77.496 | −22.2% |
+| uniform | 4,000 | 0.552 | 0.706 | −21.8% | 0.741 | −25.6% |
+| uniform | 14,000 | 2.663 | 3.067 | −13.2% | 3.318 | −19.7% |
+| uniform | 250,000 | 60.028 | 72.783 | −17.5% | 77.435 | −22.5% |
+
+The measured Moonlight path is the canonical circle-sweep bulk loader. Its
+geometry ingress keeps coordinates in immutable planes, moves only a 32-bit
+vertex-id plane through three radix passes, and restores the complete
+`(distance, x, y, vertex)` order inside equal-prefix fibres before descent.
+Fresh dense vertices are committed once, and the sweep consumes reserved cells
+without constructing a second packed geometry arena.
+
+Receipt identity:
+
+- command completed: `2026-08-23T00:00:15Z`, 318.68 s over 120 cases
+- host: Apple M4 Pro, arm64, macOS 26.5.2, GHC 9.14.1, rustc 1.92.0
+- Haskell build: cabal-install 3.16.1.0, focused `-O1` package profile,
+  comparison executable `-O2`, hot predicate, sweep, and repair modules LLVM
+  `-O3`
+- checkout base HEAD: `41906f44f0`, plus the three optimized runtime-source
+  owners named by the manifest
+- CSV: [`moonlight-delaunay-compare-2026-08-22.csv`](results/moonlight-delaunay-compare-2026-08-22.csv), SHA-256
+  `cce6af4ac7d570970a69124f5648db4e13ca41e91e05383306407a0dca323be7`
+- benchmark runtime-source hashes:
+  [`moonlight-delaunay-compare-2026-08-22.source-sha256`](results/moonlight-delaunay-compare-2026-08-22.source-sha256), SHA-256
+  `069d3bb1639b6d865fe4491b2fbbb9d0cdc8324173e30bebf690ed6592190c20`
+
+The Haskell projection in
+`Moonlight.Triangulation.Bench.DelaunayCompare.Picture` parses the tasty-bench
+CSV against the exact closed 120-case registry and reports typed obstructions
+for malformed, unknown, duplicated, or missing observations before gluing
+either picture.
+
+## Run
+
+From the repository root, list the complete benchmark tree:
+
+```console
+scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-delaunay-compare \
+  --project-file=cabal.project.triangulation-dev \
+  -- --list-tests
+```
+
+Run the comparison on one uncontended thread with wall-clock timing:
+
+```console
+scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-delaunay-compare \
+  --project-file=cabal.project.triangulation-dev \
+  -- --time-mode wall -j1
+```
+
+The Haskell executable builds the pinned Rust library through Cargo before it
+loads the library. Cargo's incremental no-op is cheap after the first run. Set
+`MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST` only when invoking an installed executable
+outside this repository layout.
+
+## Reproduce a board
+
+Every candidate run uses one CPU-time worker, a 30-second per-case timeout, and
+tasty-bench's `--stdev 5` calibration target. Name the candidate by the date it
+completed; after validation, it replaces the retained CSV and source manifest
+rather than accumulating a benchmark diary.
+
+```console
+caffeinate -i scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-delaunay-compare \
+  --project-file=cabal.project.triangulation-dev -- \
+  --stdev 5 --timeout 30s --time-mode cpu -j1 \
+  --csv foundation/moonlight-triangulation/docs/delaunay-compare/results/moonlight-delaunay-compare-YYYY-MM-DD.csv \
+  --color never --hide-progress --min-duration-to-report 1h
+```
+
+Both pictures are then regenerated from the candidate CSV, which is the only
+CSV they may be derived from:
+
+```console
+scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-delaunay-pictures \
+  --project-file=cabal.project.triangulation-dev -- \
+  foundation/moonlight-triangulation/docs/delaunay-compare/results/moonlight-delaunay-compare-YYYY-MM-DD.csv \
+  foundation/moonlight-triangulation/docs/delaunay-compare/results
+```
+
+The runtime-source manifest beside the CSV is regenerated from the measured
+worktree immediately after a completed command. It hashes the closed list of
+files that entered timed actions; picture sources and package-only metadata are
+excluded because they did not. The receipt, CSV, and big SVG stay
+benchmark-owned beside the executable and outside Cabal package inputs. The
+small SVG is also the package documentation projection, so it enters the sdist
+without entering any compiled component.
+
+## Upstream-compatible fixtures
+
+Both distributions use the 32-byte `StdRng` seed from upstream, including its
+embedded newline byte. Rust unit tests pin the first three points of both
+streams by their exact binary64 bit patterns.
+
+- `local insertion` starts at `(0, 1)` and adds independent inclusive steps
+  from `[-1, 1]`.
+- `uniform` draws each coordinate independently and inclusively from
+  `[-1e9, 1e9]`.
+- `small` contains 2,000 through 14,000 points in increments of 2,000.
+- `big` contains 50,000 through 250,000 points in increments of 50,000.
+
+Native input conversion happens once during suite preparation, outside every
+timed action, exactly as upstream's `DelaunayCrate.init` separates conversion
+from `run_creation`. Spade still clones its owned vertex vector inside each
+construction call because that is what its bulk-load API and upstream adapter
+require; `cdt`, `delaunator`, and Moonlight consume their prepared vectors by
+reference.
+
+## Compatibility gate
+
+Before `tasty-bench` runs, every one of the 24 fixtures descends across all five
+implementations. The sections glue only when vertex and inner-triangle counts
+agree. A generator, preparation, construction, foreign-boundary, or summary
+failure is a typed obstruction and terminates the command before timing.
+
+This count gate is deliberately not a second topology authority. The broader
+operation comparison owns canonical-edge and operation agreement. This command
+answers the narrower upstream question: bulk construction time over the
+upstream point distributions and sizes.
+
+The upstream `examples/real_data_benchmark.rs` CDT/shapefile program is a
+separate executable and is not folded into this creation suite. No 45 MB dataset
+or network fetch is concealed in benchmark startup.
+
+## Harness difference
+
+Upstream uses Rust Criterion. This port uses Haskell `tasty-bench`, so it does
+not pretend Criterion's warm-up seconds or sample-count knobs map one-to-one to
+another calibrator. The inputs, sizes, implementation calls, and timed/setup
+boundary are preserved; calibration and reporting are honestly Haskell.
diff --git a/docs/delaunay-compare/results/moonlight-delaunay-compare-small.svg b/docs/delaunay-compare/results/moonlight-delaunay-compare-small.svg
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/results/moonlight-delaunay-compare-small.svg
@@ -0,0 +1,415 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280.0 720.0" role="img" aria-labelledby="title description">
+<title id="title">Moonlight Delaunay construction · small point sets</title>
+<desc id="description">CPU construction time for Spade, cdt, delaunator, and Moonlight over the upstream Delaunay comparison fixtures. Lower lines are faster.</desc>
+<defs>
+  <pattern id="triangulation-mesh" width="54" height="46" patternUnits="userSpaceOnUse">
+    <path d="M0 46L27 0L54 46ZM0 46L54 46M27 0L27 46" fill="none" stroke="rgb(255,140,0)" stroke-width="0.7"/>
+  </pattern>
+  <clipPath id="header-mesh-clip"><rect x="955" y="8" width="290" height="82" rx="20"/></clipPath>
+</defs>
+<rect width="1280.0" height="720.0" fill="white"/>
+<rect x="955" y="8" width="290" height="82" fill="url(#triangulation-mesh)" opacity="0.10" clip-path="url(#header-mesh-clip)"/>
+<text x="640.0" y="34.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="24" font-weight="500" fill="rgb(20,20,20)">Moonlight Delaunay construction · small point sets</text>
+<text x="640.0" y="61.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="13" font-weight="400" fill="rgb(20,20,20)">Moonlight against Spade's construction referents · CPU time · lower is faster</text>
+<g aria-label="spade 2">
+  <line x1="105.0" y1="101" x2="139.0" y2="101" stroke="rgb(0,0,139)" stroke-width="2.0"/>
+  <circle cx="122.0" cy="101.0" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <text x="148.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">spade 2</text>
+</g>
+<g aria-label="spade 2 hierarchy">
+  <line x1="315.0" y1="101" x2="349.0" y2="101" stroke="rgb(139,0,139)" stroke-width="2.0" stroke-dasharray="7 4"/>
+  <path d="M 332.0 96.7 L 327.9 104.5 L 336.1 104.5 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <text x="358.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">spade 2 hierarchy</text>
+</g>
+<g aria-label="cdt">
+  <line x1="560.0" y1="101" x2="594.0" y2="101" stroke="rgb(178,34,34)" stroke-width="2.0"/>
+  <rect x="573.8" y="97.8" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <text x="603.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">cdt</text>
+</g>
+<g aria-label="delaunator">
+  <line x1="735.0" y1="101" x2="769.0" y2="101" stroke="rgb(0,139,139)" stroke-width="2.0"/>
+  <path d="M 752.0 96.8 L 756.2 101.0 L 752.0 105.2 L 747.8 101.0 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <text x="778.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">delaunator</text>
+</g>
+<g aria-label="moonlight-triangulation">
+  <line x1="940.0" y1="101" x2="974.0" y2="101" stroke="rgb(255,140,0)" stroke-width="3.2"/>
+  <path d="M 957.0 95.6 L 958.7 99.3 L 962.4 101.0 L 958.7 102.7 L 957.0 106.4 L 955.3 102.7 L 951.6 101.0 L 955.3 99.3 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <text x="983.0" y="105.0" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">moonlight-triangulation</text>
+</g>
+<g aria-label="local insertion">
+<text x="340.0" y="144.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="15" font-weight="500" fill="rgb(20,20,20)">local insertion</text>
+<line x1="70.0" y1="610.0" x2="610.0" y2="610.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="60.0" y="614.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">0.00</text>
+<line x1="70.0" y1="521.6" x2="610.0" y2="521.6" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="60.0" y="525.6" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">1.0</text>
+<line x1="70.0" y1="433.2" x2="610.0" y2="433.2" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="60.0" y="437.2" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2.0</text>
+<line x1="70.0" y1="344.8" x2="610.0" y2="344.8" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="60.0" y="348.8" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">3.0</text>
+<line x1="70.0" y1="256.4" x2="610.0" y2="256.4" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="60.0" y="260.4" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4.0</text>
+<line x1="70.0" y1="168.0" x2="610.0" y2="168.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="60.0" y="172.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">5.0</text>
+<line x1="70.0" y1="610" x2="70.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="70.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2k</text>
+<line x1="160.0" y1="610" x2="160.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="160.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4k</text>
+<line x1="250.0" y1="610" x2="250.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="250.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">6k</text>
+<line x1="340.0" y1="610" x2="340.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="340.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">8k</text>
+<line x1="430.0" y1="610" x2="430.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="430.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">10k</text>
+<line x1="520.0" y1="610" x2="520.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="520.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">12k</text>
+<line x1="610.0" y1="610" x2="610.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="610.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">14k</text>
+<rect x="70.0" y="168.0" width="540.0" height="442.0" fill="none" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="340.0" y="664.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">input size (points)</text>
+<text x="15.0" y="389.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" fill="rgb(20,20,20)" transform="rotate(-90 15.0 389.0)">CPU time (ms)</text>
+<g aria-label="spade 2">
+  <polyline points="70.0,583.1 160.0,541.9 250.0,495.1 340.0,453.0 430.0,407.7 520.0,366.9 610.0,314.3" fill="none" stroke="rgb(0,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="70.0" y1="581.7" x2="70.0" y2="584.5" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="581.7" x2="73.0" y2="581.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="584.5" x2="73.0" y2="584.5" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="70.0" cy="583.1" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="160.0" y1="537.6" x2="160.0" y2="546.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="537.6" x2="163.0" y2="537.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="546.2" x2="163.0" y2="546.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="160.0" cy="541.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="250.0" y1="487.9" x2="250.0" y2="502.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="487.9" x2="253.0" y2="487.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="502.2" x2="253.0" y2="502.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="250.0" cy="495.1" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="340.0" y1="445.2" x2="340.0" y2="460.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="445.2" x2="343.0" y2="445.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="460.9" x2="343.0" y2="460.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="340.0" cy="453.0" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="430.0" y1="395.0" x2="430.0" y2="420.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="395.0" x2="433.0" y2="395.0" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="420.4" x2="433.0" y2="420.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="430.0" cy="407.7" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="520.0" y1="354.2" x2="520.0" y2="379.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="354.2" x2="523.0" y2="354.2" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="379.6" x2="523.0" y2="379.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="520.0" cy="366.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="610.0" y1="291.9" x2="610.0" y2="336.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="291.9" x2="613.0" y2="291.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="336.8" x2="613.0" y2="336.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="610.0" cy="314.3" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+</g>
+<g aria-label="spade 2 hierarchy">
+  <polyline points="70.0,582.7 160.0,539.3 250.0,489.3 340.0,441.8 430.0,391.2 520.0,346.0 610.0,292.9" fill="none" stroke="rgb(139,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 4"/>
+  <line x1="70.0" y1="580.0" x2="70.0" y2="585.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="580.0" x2="73.0" y2="580.0" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="585.3" x2="73.0" y2="585.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 70.0 578.4 L 65.9 586.2 L 74.1 586.2 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="160.0" y1="532.3" x2="160.0" y2="546.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="532.3" x2="163.0" y2="532.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="546.2" x2="163.0" y2="546.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 160.0 535.0 L 155.9 542.8 L 164.1 542.8 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="250.0" y1="482.4" x2="250.0" y2="496.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="482.4" x2="253.0" y2="482.4" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="496.2" x2="253.0" y2="496.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 250.0 485.0 L 245.9 492.8 L 254.1 492.8 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="340.0" y1="430.2" x2="340.0" y2="453.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="430.2" x2="343.0" y2="430.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="453.3" x2="343.0" y2="453.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 340.0 437.5 L 335.9 445.3 L 344.1 445.3 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="430.0" y1="381.5" x2="430.0" y2="400.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="381.5" x2="433.0" y2="381.5" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="400.9" x2="433.0" y2="400.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 430.0 386.9 L 425.9 394.7 L 434.1 394.7 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="520.0" y1="324.9" x2="520.0" y2="367.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="324.9" x2="523.0" y2="324.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="367.2" x2="523.0" y2="367.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 520.0 341.7 L 515.9 349.5 L 524.1 349.5 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="610.0" y1="274.3" x2="610.0" y2="311.5" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="274.3" x2="613.0" y2="274.3" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="311.5" x2="613.0" y2="311.5" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 610.0 288.6 L 605.9 296.4 L 614.1 296.4 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+</g>
+<g aria-label="cdt">
+  <polyline points="70.0,593.6 160.0,570.7 250.0,538.0 340.0,506.8 430.0,475.6 520.0,445.6 610.0,411.9" fill="none" stroke="rgb(178,34,34)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="70.0" y1="592.9" x2="70.0" y2="594.2" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="592.9" x2="73.0" y2="592.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="594.2" x2="73.0" y2="594.2" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="66.8" y="590.4" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="160.0" y1="567.9" x2="160.0" y2="573.5" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="567.9" x2="163.0" y2="567.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="573.5" x2="163.0" y2="573.5" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="156.8" y="567.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="250.0" y1="532.4" x2="250.0" y2="543.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="532.4" x2="253.0" y2="532.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="543.6" x2="253.0" y2="543.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="246.8" y="534.8" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="340.0" y1="500.2" x2="340.0" y2="513.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="500.2" x2="343.0" y2="500.2" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="513.3" x2="343.0" y2="513.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="336.8" y="503.6" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="430.0" y1="468.6" x2="430.0" y2="482.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="468.6" x2="433.0" y2="468.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="482.6" x2="433.0" y2="482.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="426.8" y="472.4" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="520.0" y1="435.9" x2="520.0" y2="455.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="435.9" x2="523.0" y2="435.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="455.3" x2="523.0" y2="455.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="516.8" y="442.4" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="610.0" y1="392.8" x2="610.0" y2="430.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="392.8" x2="613.0" y2="392.8" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="430.9" x2="613.0" y2="430.9" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="606.8" y="408.7" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+</g>
+<g aria-label="delaunator">
+  <polyline points="70.0,597.4 160.0,580.1 250.0,554.3 340.0,527.1 430.0,495.7 520.0,461.5 610.0,432.1" fill="none" stroke="rgb(0,139,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="70.0" y1="596.7" x2="70.0" y2="598.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="596.7" x2="73.0" y2="596.7" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="598.0" x2="73.0" y2="598.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 70.0 593.2 L 74.2 597.4 L 70.0 601.6 L 65.8 597.4 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="160.0" y1="577.8" x2="160.0" y2="582.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="577.8" x2="163.0" y2="577.8" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="582.4" x2="163.0" y2="582.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 160.0 575.9 L 164.2 580.1 L 160.0 584.3 L 155.8 580.1 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="250.0" y1="550.2" x2="250.0" y2="558.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="550.2" x2="253.0" y2="550.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="558.5" x2="253.0" y2="558.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 250.0 550.1 L 254.2 554.3 L 250.0 558.5 L 245.8 554.3 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="340.0" y1="522.0" x2="340.0" y2="532.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="522.0" x2="343.0" y2="522.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="532.2" x2="343.0" y2="532.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 340.0 522.9 L 344.2 527.1 L 340.0 531.3 L 335.8 527.1 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="430.0" y1="486.9" x2="430.0" y2="504.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="486.9" x2="433.0" y2="486.9" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="504.5" x2="433.0" y2="504.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 430.0 491.5 L 434.2 495.7 L 430.0 499.9 L 425.8 495.7 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="520.0" y1="453.0" x2="520.0" y2="470.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="453.0" x2="523.0" y2="453.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="470.1" x2="523.0" y2="470.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 520.0 457.3 L 524.2 461.5 L 520.0 465.7 L 515.8 461.5 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="610.0" y1="418.1" x2="610.0" y2="446.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="418.1" x2="613.0" y2="418.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="446.1" x2="613.0" y2="446.1" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 610.0 427.9 L 614.2 432.1 L 610.0 436.3 L 605.8 432.1 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+</g>
+<g aria-label="moonlight-triangulation">
+  <polyline points="70.0,585.5 160.0,558.1 250.0,524.2 340.0,490.3 430.0,456.2 520.0,417.5 610.0,361.2" fill="none" stroke="rgb(255,140,0)" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="70.0" y1="583.9" x2="70.0" y2="587.1" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="583.9" x2="73.0" y2="583.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="67.0" y1="587.1" x2="73.0" y2="587.1" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 70.0 580.1 L 71.7 583.8 L 75.4 585.5 L 71.7 587.2 L 70.0 590.9 L 68.3 587.2 L 64.6 585.5 L 68.3 583.8 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="160.0" y1="554.3" x2="160.0" y2="561.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="554.3" x2="163.0" y2="554.3" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="157.0" y1="561.9" x2="163.0" y2="561.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 160.0 552.7 L 161.7 556.4 L 165.4 558.1 L 161.7 559.8 L 160.0 563.5 L 158.3 559.8 L 154.6 558.1 L 158.3 556.4 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="250.0" y1="518.4" x2="250.0" y2="529.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="518.4" x2="253.0" y2="518.4" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="247.0" y1="529.9" x2="253.0" y2="529.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 250.0 518.8 L 251.7 522.5 L 255.4 524.2 L 251.7 525.9 L 250.0 529.6 L 248.3 525.9 L 244.6 524.2 L 248.3 522.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="340.0" y1="480.9" x2="340.0" y2="499.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="480.9" x2="343.0" y2="480.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="337.0" y1="499.6" x2="343.0" y2="499.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 340.0 484.9 L 341.7 488.6 L 345.4 490.3 L 341.7 492.0 L 340.0 495.7 L 338.3 492.0 L 334.6 490.3 L 338.3 488.6 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="430.0" y1="449.5" x2="430.0" y2="462.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="449.5" x2="433.0" y2="449.5" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="427.0" y1="462.9" x2="433.0" y2="462.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 430.0 450.8 L 431.7 454.5 L 435.4 456.2 L 431.7 457.9 L 430.0 461.6 L 428.3 457.9 L 424.6 456.2 L 428.3 454.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="520.0" y1="398.7" x2="520.0" y2="436.3" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="398.7" x2="523.0" y2="398.7" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="517.0" y1="436.3" x2="523.0" y2="436.3" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 520.0 412.1 L 521.7 415.8 L 525.4 417.5 L 521.7 419.2 L 520.0 422.9 L 518.3 419.2 L 514.6 417.5 L 518.3 415.8 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="610.0" y1="346.9" x2="610.0" y2="375.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="346.9" x2="613.0" y2="346.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="607.0" y1="375.6" x2="613.0" y2="375.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 610.0 355.8 L 611.7 359.5 L 615.4 361.2 L 611.7 362.9 L 610.0 366.6 L 608.3 362.9 L 604.6 361.2 L 608.3 359.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+</g>
+</g>
+<g aria-label="uniform">
+<text x="940.0" y="144.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="15" font-weight="500" fill="rgb(20,20,20)">uniform</text>
+<line x1="670.0" y1="610.0" x2="1210.0" y2="610.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="660.0" y="614.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">0.00</text>
+<line x1="670.0" y1="521.6" x2="1210.0" y2="521.6" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="660.0" y="525.6" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">1.0</text>
+<line x1="670.0" y1="433.2" x2="1210.0" y2="433.2" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="660.0" y="437.2" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2.0</text>
+<line x1="670.0" y1="344.8" x2="1210.0" y2="344.8" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="660.0" y="348.8" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">3.0</text>
+<line x1="670.0" y1="256.4" x2="1210.0" y2="256.4" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="660.0" y="260.4" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4.0</text>
+<line x1="670.0" y1="168.0" x2="1210.0" y2="168.0" stroke="rgb(170,170,170)" stroke-width="0.7" stroke-dasharray="2 4"/>
+<text x="660.0" y="172.0" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">5.0</text>
+<line x1="670.0" y1="610" x2="670.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="670.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">2k</text>
+<line x1="760.0" y1="610" x2="760.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="760.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">4k</text>
+<line x1="850.0" y1="610" x2="850.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="850.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">6k</text>
+<line x1="940.0" y1="610" x2="940.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="940.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">8k</text>
+<line x1="1030.0" y1="610" x2="1030.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="1030.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">10k</text>
+<line x1="1120.0" y1="610" x2="1120.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="1120.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">12k</text>
+<line x1="1210.0" y1="610" x2="1210.0" y2="616" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="1210.0" y="635.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">14k</text>
+<rect x="670.0" y="168.0" width="540.0" height="442.0" fill="none" stroke="rgb(25,25,25)" stroke-width="1"/>
+<text x="940.0" y="664.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="400" fill="rgb(20,20,20)">input size (points)</text>
+<text x="615.0" y="389.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="12" fill="rgb(20,20,20)" transform="rotate(-90 615.0 389.0)">CPU time (ms)</text>
+<g aria-label="spade 2">
+  <polyline points="670.0,583.7 760.0,547.6 850.0,506.8 940.0,466.9 1030.0,426.3 1120.0,383.7 1210.0,338.9" fill="none" stroke="rgb(0,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="670.0" y1="582.4" x2="670.0" y2="585.0" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="582.4" x2="673.0" y2="582.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="585.0" x2="673.0" y2="585.0" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="670.0" cy="583.7" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="760.0" y1="542.4" x2="760.0" y2="552.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="542.4" x2="763.0" y2="542.4" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="552.8" x2="763.0" y2="552.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="760.0" cy="547.6" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="850.0" y1="497.7" x2="850.0" y2="515.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="497.7" x2="853.0" y2="497.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="515.8" x2="853.0" y2="515.8" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="850.0" cy="506.8" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="940.0" y1="463.1" x2="940.0" y2="470.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="463.1" x2="943.0" y2="463.1" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="470.7" x2="943.0" y2="470.7" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="940.0" cy="466.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="1030.0" y1="415.5" x2="1030.0" y2="437.1" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="415.5" x2="1033.0" y2="415.5" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="437.1" x2="1033.0" y2="437.1" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="1030.0" cy="426.3" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="1120.0" y1="370.6" x2="1120.0" y2="396.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="370.6" x2="1123.0" y2="370.6" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="396.9" x2="1123.0" y2="396.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="1120.0" cy="383.7" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+  <line x1="1210.0" y1="317.9" x2="1210.0" y2="359.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="317.9" x2="1213.0" y2="317.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="359.9" x2="1213.0" y2="359.9" stroke="rgb(0,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <circle cx="1210.0" cy="338.9" r="3.2" fill="white" stroke="rgb(0,0,139)" stroke-width="1.6"/>
+</g>
+<g aria-label="spade 2 hierarchy">
+  <polyline points="670.0,583.3 760.0,544.5 850.0,499.8 940.0,455.4 1030.0,406.4 1120.0,364.4 1210.0,316.7" fill="none" stroke="rgb(139,0,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 4"/>
+  <line x1="670.0" y1="582.1" x2="670.0" y2="584.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="582.1" x2="673.0" y2="582.1" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="584.6" x2="673.0" y2="584.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 670.0 579.0 L 665.9 586.8 L 674.1 586.8 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="760.0" y1="541.1" x2="760.0" y2="547.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="541.1" x2="763.0" y2="541.1" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="547.8" x2="763.0" y2="547.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 760.0 540.2 L 755.9 548.0 L 764.1 548.0 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="850.0" y1="489.8" x2="850.0" y2="509.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="489.8" x2="853.0" y2="489.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="509.8" x2="853.0" y2="509.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 850.0 495.5 L 845.9 503.3 L 854.1 503.3 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="940.0" y1="443.6" x2="940.0" y2="467.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="443.6" x2="943.0" y2="443.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="467.2" x2="943.0" y2="467.2" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 940.0 451.1 L 935.9 458.9 L 944.1 458.9 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="1030.0" y1="387.9" x2="1030.0" y2="424.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="387.9" x2="1033.0" y2="387.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="424.9" x2="1033.0" y2="424.9" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1030.0 402.1 L 1025.9 409.9 L 1034.1 409.9 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="1120.0" y1="344.7" x2="1120.0" y2="384.0" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="344.7" x2="1123.0" y2="344.7" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="384.0" x2="1123.0" y2="384.0" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1120.0 360.1 L 1115.9 367.9 L 1124.1 367.9 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+  <line x1="1210.0" y1="306.8" x2="1210.0" y2="326.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="306.8" x2="1213.0" y2="306.8" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="326.6" x2="1213.0" y2="326.6" stroke="rgb(139,0,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1210.0 312.4 L 1205.9 320.2 L 1214.1 320.2 Z" fill="white" stroke="rgb(139,0,139)" stroke-width="1.6"/>
+</g>
+<g aria-label="cdt">
+  <polyline points="670.0,593.7 760.0,570.4 850.0,534.1 940.0,500.7 1030.0,465.7 1120.0,431.7 1210.0,396.1" fill="none" stroke="rgb(178,34,34)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="670.0" y1="592.7" x2="670.0" y2="594.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="592.7" x2="673.0" y2="592.7" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="594.6" x2="673.0" y2="594.6" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="666.8" y="590.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="760.0" y1="569.0" x2="760.0" y2="571.7" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="569.0" x2="763.0" y2="569.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="571.7" x2="763.0" y2="571.7" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="756.8" y="567.2" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="850.0" y1="529.0" x2="850.0" y2="539.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="529.0" x2="853.0" y2="529.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="539.3" x2="853.0" y2="539.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="846.8" y="530.9" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="940.0" y1="491.0" x2="940.0" y2="510.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="491.0" x2="943.0" y2="491.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="510.3" x2="943.0" y2="510.3" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="936.8" y="497.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="1030.0" y1="455.0" x2="1030.0" y2="476.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="455.0" x2="1033.0" y2="455.0" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="476.4" x2="1033.0" y2="476.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="1026.8" y="462.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="1120.0" y1="420.1" x2="1120.0" y2="443.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="420.1" x2="1123.0" y2="420.1" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="443.4" x2="1123.0" y2="443.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="1116.8" y="428.5" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+  <line x1="1210.0" y1="379.4" x2="1210.0" y2="412.8" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="379.4" x2="1213.0" y2="379.4" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="412.8" x2="1213.0" y2="412.8" stroke="rgb(178,34,34)" stroke-width="0.9" opacity="0.48"/>
+  <rect x="1206.8" y="392.9" width="6.4" height="6.4" fill="white" stroke="rgb(178,34,34)" stroke-width="1.6"/>
+</g>
+<g aria-label="delaunator">
+  <polyline points="670.0,597.2 760.0,580.7 850.0,558.9 940.0,530.2 1030.0,497.9 1120.0,466.9 1210.0,434.3" fill="none" stroke="rgb(0,139,139)" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="670.0" y1="596.0" x2="670.0" y2="598.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="596.0" x2="673.0" y2="596.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="598.5" x2="673.0" y2="598.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 670.0 593.0 L 674.2 597.2 L 670.0 601.4 L 665.8 597.2 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="760.0" y1="578.8" x2="760.0" y2="582.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="578.8" x2="763.0" y2="578.8" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="582.6" x2="763.0" y2="582.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 760.0 576.5 L 764.2 580.7 L 760.0 584.9 L 755.8 580.7 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="850.0" y1="554.6" x2="850.0" y2="563.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="554.6" x2="853.0" y2="554.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="563.2" x2="853.0" y2="563.2" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 850.0 554.7 L 854.2 558.9 L 850.0 563.1 L 845.8 558.9 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="940.0" y1="529.0" x2="940.0" y2="531.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="529.0" x2="943.0" y2="529.0" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="531.5" x2="943.0" y2="531.5" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 940.0 526.0 L 944.2 530.2 L 940.0 534.4 L 935.8 530.2 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="1030.0" y1="487.6" x2="1030.0" y2="508.3" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="487.6" x2="1033.0" y2="487.6" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="508.3" x2="1033.0" y2="508.3" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1030.0 493.7 L 1034.2 497.9 L 1030.0 502.1 L 1025.8 497.9 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="1120.0" y1="461.4" x2="1120.0" y2="472.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="461.4" x2="1123.0" y2="461.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="472.4" x2="1123.0" y2="472.4" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1120.0 462.7 L 1124.2 466.9 L 1120.0 471.1 L 1115.8 466.9 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+  <line x1="1210.0" y1="419.9" x2="1210.0" y2="448.7" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="419.9" x2="1213.0" y2="419.9" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="448.7" x2="1213.0" y2="448.7" stroke="rgb(0,139,139)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1210.0 430.1 L 1214.2 434.3 L 1210.0 438.5 L 1205.8 434.3 Z" fill="white" stroke="rgb(0,139,139)" stroke-width="1.6"/>
+</g>
+<g aria-label="moonlight-triangulation">
+  <polyline points="670.0,585.5 760.0,561.2 850.0,530.7 940.0,496.8 1030.0,462.3 1120.0,419.3 1210.0,374.6" fill="none" stroke="rgb(255,140,0)" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="670.0" y1="584.0" x2="670.0" y2="587.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="584.0" x2="673.0" y2="584.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="667.0" y1="587.0" x2="673.0" y2="587.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 670.0 580.1 L 671.7 583.8 L 675.4 585.5 L 671.7 587.2 L 670.0 590.9 L 668.3 587.2 L 664.6 585.5 L 668.3 583.8 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="760.0" y1="560.5" x2="760.0" y2="562.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="560.5" x2="763.0" y2="560.5" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="757.0" y1="562.0" x2="763.0" y2="562.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 760.0 555.8 L 761.7 559.5 L 765.4 561.2 L 761.7 562.9 L 760.0 566.6 L 758.3 562.9 L 754.6 561.2 L 758.3 559.5 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="850.0" y1="522.8" x2="850.0" y2="538.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="522.8" x2="853.0" y2="522.8" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="847.0" y1="538.6" x2="853.0" y2="538.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 850.0 525.3 L 851.7 529.0 L 855.4 530.7 L 851.7 532.4 L 850.0 536.1 L 848.3 532.4 L 844.6 530.7 L 848.3 529.0 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="940.0" y1="485.9" x2="940.0" y2="507.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="485.9" x2="943.0" y2="485.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="937.0" y1="507.6" x2="943.0" y2="507.6" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 940.0 491.4 L 941.7 495.1 L 945.4 496.8 L 941.7 498.5 L 940.0 502.2 L 938.3 498.5 L 934.6 496.8 L 938.3 495.1 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="1030.0" y1="451.9" x2="1030.0" y2="472.7" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="451.9" x2="1033.0" y2="451.9" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1027.0" y1="472.7" x2="1033.0" y2="472.7" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1030.0 456.9 L 1031.7 460.6 L 1035.4 462.3 L 1031.7 464.0 L 1030.0 467.7 L 1028.3 464.0 L 1024.6 462.3 L 1028.3 460.6 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="1120.0" y1="401.2" x2="1120.0" y2="437.4" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="401.2" x2="1123.0" y2="401.2" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1117.0" y1="437.4" x2="1123.0" y2="437.4" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1120.0 413.9 L 1121.7 417.6 L 1125.4 419.3 L 1121.7 421.0 L 1120.0 424.7 L 1118.3 421.0 L 1114.6 419.3 L 1118.3 417.6 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+  <line x1="1210.0" y1="364.2" x2="1210.0" y2="385.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="364.2" x2="1213.0" y2="364.2" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <line x1="1207.0" y1="385.0" x2="1213.0" y2="385.0" stroke="rgb(255,140,0)" stroke-width="0.9" opacity="0.48"/>
+  <path d="M 1210.0 369.2 L 1211.7 372.9 L 1215.4 374.6 L 1211.7 376.3 L 1210.0 380.0 L 1208.3 376.3 L 1204.6 374.6 L 1208.3 372.9 Z" fill="rgb(255,140,0)" stroke="white" stroke-width="1"/>
+</g>
+</g>
+<text x="640.0" y="704.0" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="400" fill="rgb(20,20,20)">Whiskers show tasty-bench's reported 2× standard-deviation spread.</text>
+</svg>
diff --git a/docs/delaunay-compare/rust/Cargo.lock b/docs/delaunay-compare/rust/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/rust/Cargo.lock
@@ -0,0 +1,264 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "cdt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91b872294f63ef586b32aa94141561681aa35ca2d703960cca4f661f4e18184"
+dependencies = [
+ "geometry-predicates",
+ "thiserror",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "delaunator"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e1ee323c1275374f7e612d3724d12707079fb6a2117349fe144def656f5a880"
+dependencies = [
+ "robust",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "geometry-predicates"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dba84198cf199c11b83f1cb9243eaeb70dc50b719d2835ebf34bf2481bca010"
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "moonlight-delaunay-compare-referents"
+version = "0.1.0"
+dependencies = [
+ "cdt",
+ "delaunator",
+ "rand",
+ "spade",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "robust"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "spade"
+version = "2.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
+dependencies = [
+ "hashbrown",
+ "num-traits",
+ "robust",
+ "smallvec",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/docs/delaunay-compare/rust/Cargo.toml b/docs/delaunay-compare/rust/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/rust/Cargo.toml
@@ -0,0 +1,20 @@
+[workspace]
+
+[package]
+name = "moonlight-delaunay-compare-referents"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "moonlight_delaunay_compare_referents"
+crate-type = ["cdylib"]
+
+[dependencies]
+cdt = "0.1"
+delaunator = "1.0.1"
+rand = "0.9.1"
+spade = "2.15"
+
+[profile.release]
+opt-level = 3
+lto = "thin"
diff --git a/docs/delaunay-compare/rust/src/lib.rs b/docs/delaunay-compare/rust/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/rust/src/lib.rs
@@ -0,0 +1,397 @@
+//! Rust referent boundary for Moonlight's Delaunay construction comparison.
+//!
+//! Fixture choice, benchmark grouping, agreement, timing, and reporting belong
+//! to Haskell. This library retains only the operations that cannot move there:
+//! Spade's exact seeded generator and direct calls into the Rust crates under
+//! comparison.
+
+use core::ffi::c_void;
+use core::hint::black_box;
+use core::ptr;
+use rand::distr::{Distribution, Uniform};
+use rand::rngs::StdRng;
+use rand::SeedableRng;
+use spade::{HierarchyHintGenerator, Point2, Triangulation};
+use std::panic::{catch_unwind, AssertUnwindSafe};
+
+const UPSTREAM_SEED: [u8; 32] = [
+    0xfb, 0xdc, 0x4e, 0xa0, 0x30, 0xde, 0x82, 0xba, 0x69, 0x97, 0x3c, 0x52, 0x49, 0x4d, 0x00, 0xca,
+    0x0a, 0x5c, 0x21, 0xa3, 0x8d, 0x5c, 0xf2, 0x34, 0x4e, 0x58, 0x7d, 0x80, 0x16, 0x66, 0x23, 0x30,
+];
+
+const UNIFORM_RANGE: f64 = 1.0e9;
+const LOCAL_STEP_RANGE: f64 = 1.0;
+
+type PlainSpade = spade::DelaunayTriangulation<Point2<f64>>;
+type HierarchySpade =
+    spade::DelaunayTriangulation<Point2<f64>, (), (), (), HierarchyHintGenerator<f64>>;
+
+#[repr(i32)]
+#[derive(Clone, Copy, Debug)]
+enum BoundaryStatus {
+    Success = 0,
+    NullPointer = 1,
+    UnknownTag = 2,
+    CoordinateCountMismatch = 3,
+    DistributionConstructionFailed = 4,
+    TriangulationFailed = 5,
+    Panicked = 6,
+}
+
+impl BoundaryStatus {
+    const fn code(self) -> i32 {
+        self as i32
+    }
+}
+
+#[derive(Clone, Copy)]
+enum PointDistribution {
+    LocalInsertion,
+    Uniform,
+}
+
+impl TryFrom<i32> for PointDistribution {
+    type Error = BoundaryStatus;
+
+    fn try_from(tag: i32) -> Result<Self, Self::Error> {
+        match tag {
+            0 => Ok(Self::LocalInsertion),
+            1 => Ok(Self::Uniform),
+            _ => Err(BoundaryStatus::UnknownTag),
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+enum NativeImplementation {
+    Spade,
+    SpadeHierarchy,
+    Cdt,
+    Delaunator,
+}
+
+impl TryFrom<i32> for NativeImplementation {
+    type Error = BoundaryStatus;
+
+    fn try_from(tag: i32) -> Result<Self, Self::Error> {
+        match tag {
+            0 => Ok(Self::Spade),
+            1 => Ok(Self::SpadeHierarchy),
+            2 => Ok(Self::Cdt),
+            3 => Ok(Self::Delaunator),
+            _ => Err(BoundaryStatus::UnknownTag),
+        }
+    }
+}
+
+enum PreparedImplementation {
+    Spade(Vec<Point2<f64>>),
+    SpadeHierarchy(Vec<Point2<f64>>),
+    Cdt(Vec<(f64, f64)>),
+    Delaunator(Vec<delaunator::Point>),
+}
+
+#[derive(Clone, Copy)]
+struct ConstructionSummary {
+    vertices: usize,
+    triangles: usize,
+}
+
+impl PreparedImplementation {
+    fn from_coordinates(implementation: NativeImplementation, coordinates: &[[f64; 2]]) -> Self {
+        match implementation {
+            NativeImplementation::Spade => {
+                Self::Spade(coordinates.iter().copied().map(Point2::from).collect())
+            }
+            NativeImplementation::SpadeHierarchy => {
+                Self::SpadeHierarchy(coordinates.iter().copied().map(Point2::from).collect())
+            }
+            NativeImplementation::Cdt => {
+                Self::Cdt(coordinates.iter().map(|[x, y]| (*x, *y)).collect())
+            }
+            NativeImplementation::Delaunator => Self::Delaunator(
+                coordinates
+                    .iter()
+                    .map(|[x, y]| delaunator::Point { x: *x, y: *y })
+                    .collect(),
+            ),
+        }
+    }
+
+    fn run(&self) -> Result<ConstructionSummary, BoundaryStatus> {
+        match self {
+            Self::Spade(vertices) => {
+                let triangulation = PlainSpade::bulk_load(vertices.clone())
+                    .map_err(|_| BoundaryStatus::TriangulationFailed)?;
+                let summary = ConstructionSummary {
+                    vertices: triangulation.num_vertices(),
+                    triangles: triangulation.num_inner_faces(),
+                };
+                black_box(triangulation);
+                Ok(summary)
+            }
+            Self::SpadeHierarchy(vertices) => {
+                let triangulation = HierarchySpade::bulk_load(vertices.clone())
+                    .map_err(|_| BoundaryStatus::TriangulationFailed)?;
+                let summary = ConstructionSummary {
+                    vertices: triangulation.num_vertices(),
+                    triangles: triangulation.num_inner_faces(),
+                };
+                black_box(triangulation);
+                Ok(summary)
+            }
+            Self::Cdt(vertices) => {
+                let triangles = cdt::triangulate_points(vertices)
+                    .map_err(|_| BoundaryStatus::TriangulationFailed)?;
+                let summary = ConstructionSummary {
+                    vertices: vertices.len(),
+                    triangles: triangles.len(),
+                };
+                black_box(triangles);
+                Ok(summary)
+            }
+            Self::Delaunator(vertices) => {
+                let triangulation = delaunator::triangulate(vertices);
+                let summary = ConstructionSummary {
+                    vertices: vertices.len(),
+                    triangles: triangulation.triangles.len() / 3,
+                };
+                black_box(triangulation);
+                Ok(summary)
+            }
+        }
+    }
+}
+
+fn boundary_status(action: impl FnOnce() -> Result<(), BoundaryStatus>) -> i32 {
+    match catch_unwind(AssertUnwindSafe(action)) {
+        Ok(Ok(())) => BoundaryStatus::Success.code(),
+        Ok(Err(status)) => status.code(),
+        Err(_) => BoundaryStatus::Panicked.code(),
+    }
+}
+
+fn coordinate_count(point_count: usize) -> Result<usize, BoundaryStatus> {
+    point_count
+        .checked_mul(2)
+        .ok_or(BoundaryStatus::CoordinateCountMismatch)
+}
+
+fn uniform_samples(point_count: usize) -> Result<impl Iterator<Item = [f64; 2]>, BoundaryStatus> {
+    let distribution = Uniform::new_inclusive(-UNIFORM_RANGE, UNIFORM_RANGE)
+        .map_err(|_| BoundaryStatus::DistributionConstructionFailed)?;
+    let mut generator = StdRng::from_seed(UPSTREAM_SEED);
+    Ok(core::iter::from_fn(move || {
+        Some([
+            distribution.sample(&mut generator),
+            distribution.sample(&mut generator),
+        ])
+    })
+    .take(point_count))
+}
+
+fn local_insertion_samples(
+    point_count: usize,
+) -> Result<impl Iterator<Item = [f64; 2]>, BoundaryStatus> {
+    let distribution = Uniform::new_inclusive(-LOCAL_STEP_RANGE, LOCAL_STEP_RANGE)
+        .map_err(|_| BoundaryStatus::DistributionConstructionFailed)?;
+    let mut generator = StdRng::from_seed(UPSTREAM_SEED);
+    let mut previous = [0.0, 1.0];
+    Ok(core::iter::from_fn(move || {
+        previous = [
+            previous[0] + distribution.sample(&mut generator),
+            previous[1] + distribution.sample(&mut generator),
+        ];
+        Some(previous)
+    })
+    .take(point_count))
+}
+
+fn write_coordinates(
+    distribution: PointDistribution,
+    point_count: usize,
+    output: &mut [f64],
+) -> Result<(), BoundaryStatus> {
+    let expected_count = coordinate_count(point_count)?;
+    if output.len() != expected_count {
+        return Err(BoundaryStatus::CoordinateCountMismatch);
+    }
+
+    let samples: Box<dyn Iterator<Item = [f64; 2]>> = match distribution {
+        PointDistribution::LocalInsertion => Box::new(local_insertion_samples(point_count)?),
+        PointDistribution::Uniform => Box::new(uniform_samples(point_count)?),
+    };
+
+    output
+        .chunks_exact_mut(2)
+        .zip(samples)
+        .for_each(|(coordinate_pair, [x, y])| {
+            coordinate_pair.copy_from_slice(&[x, y]);
+        });
+    Ok(())
+}
+
+/// Fill `coordinates` with Spade's exact seeded benchmark distribution.
+///
+/// # Safety
+///
+/// A non-null `coordinates` pointer must be writable for `coordinate_count`
+/// consecutive `f64` values.
+#[no_mangle]
+pub unsafe extern "C" fn delaunay_compare_generate(
+    distribution_tag: i32,
+    point_count: usize,
+    coordinates: *mut f64,
+    supplied_coordinate_count: usize,
+) -> i32 {
+    boundary_status(|| {
+        let distribution = PointDistribution::try_from(distribution_tag)?;
+        let expected_count = coordinate_count(point_count)?;
+        if supplied_coordinate_count != expected_count {
+            return Err(BoundaryStatus::CoordinateCountMismatch);
+        }
+        if coordinates.is_null() && supplied_coordinate_count != 0 {
+            return Err(BoundaryStatus::NullPointer);
+        }
+        let output = if supplied_coordinate_count == 0 {
+            &mut []
+        } else {
+            // SAFETY: the caller contract and checks above establish a non-null
+            // writable region of exactly `supplied_coordinate_count` values.
+            unsafe { core::slice::from_raw_parts_mut(coordinates, supplied_coordinate_count) }
+        };
+        write_coordinates(distribution, point_count, output)
+    })
+}
+
+/// Prepare the implementation-specific native input outside the timed region.
+///
+/// # Safety
+///
+/// A non-null `coordinates` pointer must be readable for `2 * point_count`
+/// consecutive `f64` values. `prepared_output` must be writable for one pointer.
+#[no_mangle]
+pub unsafe extern "C" fn delaunay_compare_prepare(
+    implementation_tag: i32,
+    coordinates: *const f64,
+    point_count: usize,
+    prepared_output: *mut *mut c_void,
+) -> i32 {
+    boundary_status(|| {
+        let implementation = NativeImplementation::try_from(implementation_tag)?;
+        if prepared_output.is_null() {
+            return Err(BoundaryStatus::NullPointer);
+        }
+        let supplied_coordinate_count = coordinate_count(point_count)?;
+        if coordinates.is_null() && supplied_coordinate_count != 0 {
+            return Err(BoundaryStatus::NullPointer);
+        }
+        let flat_coordinates = if supplied_coordinate_count == 0 {
+            &[]
+        } else {
+            // SAFETY: the caller contract and checks above establish a non-null
+            // readable region of exactly `supplied_coordinate_count` values.
+            unsafe { core::slice::from_raw_parts(coordinates, supplied_coordinate_count) }
+        };
+        let coordinate_pairs = flat_coordinates.as_chunks::<2>().0;
+        let prepared = PreparedImplementation::from_coordinates(implementation, coordinate_pairs);
+        // SAFETY: `prepared_output` was checked non-null and points to writable
+        // storage for one opaque pointer by the caller contract.
+        unsafe {
+            ptr::write(prepared_output, Box::into_raw(Box::new(prepared)).cast());
+        }
+        Ok(())
+    })
+}
+
+/// Construct one triangulation from a prepared native input.
+///
+/// # Safety
+///
+/// `prepared` must be a live pointer returned by `delaunay_compare_prepare`.
+/// Both output pointers must be writable for one `usize`.
+#[no_mangle]
+pub unsafe extern "C" fn delaunay_compare_run(
+    prepared: *const c_void,
+    vertex_count_output: *mut usize,
+    triangle_count_output: *mut usize,
+) -> i32 {
+    boundary_status(|| {
+        if prepared.is_null() || vertex_count_output.is_null() || triangle_count_output.is_null() {
+            return Err(BoundaryStatus::NullPointer);
+        }
+        // SAFETY: the caller contract requires a live pointer produced by the
+        // matching prepare function and keeps it alive for this call.
+        let prepared_implementation = unsafe { &*prepared.cast::<PreparedImplementation>() };
+        let summary = prepared_implementation.run()?;
+        // SAFETY: both output pointers were checked non-null and the caller
+        // contract gives writable storage for one value at each address.
+        unsafe {
+            ptr::write(vertex_count_output, summary.vertices);
+            ptr::write(triangle_count_output, summary.triangles);
+        }
+        Ok(())
+    })
+}
+
+/// Release one prepared native input. A null pointer is a no-op.
+///
+/// # Safety
+///
+/// A non-null pointer must have been returned by `delaunay_compare_prepare`
+/// and must not have been released before.
+#[no_mangle]
+pub unsafe extern "C" fn delaunay_compare_release(prepared: *mut c_void) {
+    if !prepared.is_null() {
+        // SAFETY: the caller contract transfers the one remaining ownership of
+        // the allocation produced by `Box::into_raw` in the prepare function.
+        unsafe {
+            drop(Box::from_raw(prepared.cast::<PreparedImplementation>()));
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{local_insertion_samples, uniform_samples};
+
+    #[test]
+    fn local_insertion_prefix_matches_upstream_fixture_bits() {
+        let observed = local_insertion_samples(3)
+            .expect("the fixed local-insertion distribution is valid")
+            .flatten()
+            .map(f64::to_bits)
+            .collect::<Vec<_>>();
+        assert_eq!(
+            observed,
+            [
+                0xbfe28b04856ac6db,
+                0x3ff8c718b563a19a,
+                0x3fc014ab09d22a14,
+                0x4003a1ee45dc4712,
+                0xbfd097c41d82adc4,
+                0x4008d420538d5c3d,
+            ]
+        );
+    }
+
+    #[test]
+    fn uniform_prefix_matches_upstream_fixture_bits() {
+        let observed = uniform_samples(3)
+            .expect("the fixed uniform distribution is valid")
+            .flatten()
+            .map(f64::to_bits)
+            .collect::<Vec<_>>();
+        assert_eq!(
+            observed,
+            [
+                0xc1c1450134a5bd5a,
+                0x41c0598b1e249a72,
+                0x41c5037dbf1c0b38,
+                0x41cafc1cf53845c8,
+                0xc1b6f1036f0b1386,
+                0x41c35b5d4df8b356,
+            ]
+        );
+    }
+}
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,25 @@
+# Local development
+
+[Return to the package overview.](../README.md#consumer-libraries)
+· [Browse the documentation.](./README.md)
+
+Run component-qualified commands from the repository's `compiler/` directory.
+The optimized construction kernel uses the triangulation development project:
+
+```console
+../scripts/safe-cabal.sh build moonlight-triangulation:lib:build \
+  --project-file=cabal.project.triangulation-dev \
+  --builddir=dist-newstyle-triangulation-dev
+```
+
+For type and interface feedback, use the same component at `-O0`:
+
+```console
+../scripts/safe-cabal.sh build moonlight-triangulation:lib:build \
+  --project-file=cabal.project.triangulation-dev \
+  --builddir=dist-newstyle-triangulation-fast \
+  --disable-optimization
+```
+
+Performance evidence uses the optimized profile; the `-O0` profile is the
+shorter feedback path.
diff --git a/docs/examples/Moonlight/Triangulation/Example/AlphaBoundary.hs b/docs/examples/Moonlight/Triangulation/Example/AlphaBoundary.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Triangulation/Example/AlphaBoundary.hs
@@ -0,0 +1,54 @@
+module Moonlight.Triangulation.Example.AlphaBoundary
+  ( AlphaBoundaryExampleError (..)
+  , BoundaryShape (..)
+  , alphaBoundaryShapes
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.Vector as Vector
+import Moonlight.Triangulation
+
+data AlphaBoundaryExampleError
+  = AlphaBoundaryBuildFailed !BuildError
+  | AlphaBoundaryRadiusFailed !RadiusSquaredError
+  | AlphaBoundaryDescentFailed !BoundaryObstruction
+  deriving stock (Eq, Show)
+
+data BoundaryShape = BoundaryShape
+  { boundaryOuterVertexCount :: !Int
+  , boundaryHoleCount :: !Int
+  }
+  deriving stock (Eq, Show)
+
+-- | Descend the closed alpha-shape face section of one square to its boundary.
+alphaBoundaryShapes :: Either AlphaBoundaryExampleError [BoundaryShape]
+alphaBoundaryShapes = do
+  mesh <-
+    first AlphaBoundaryBuildFailed
+      ( delaunayGeometry
+          ( Vector.fromList
+              [ Point 0 0
+              , Point 2 0
+              , Point 2 2
+              , Point 0 2
+              ]
+          )
+      )
+  threshold <- first AlphaBoundaryRadiusFailed (mkRadiusSquared 2)
+  let selectedComponents =
+        fmap snd
+          (filter fst (faceComponents mesh (alphaShapeContainsFace threshold mesh)))
+  traverse
+    ( fmap boundaryShape
+        . first AlphaBoundaryDescentFailed
+        . componentBoundary mesh
+    )
+    selectedComponents
+
+boundaryShape :: RegionBoundary -> BoundaryShape
+boundaryShape boundary =
+  BoundaryShape
+    { boundaryOuterVertexCount =
+        length (boundaryLoopVertices (regionBoundaryOuterLoop boundary))
+    , boundaryHoleCount = length (regionBoundaryHoleLoops boundary)
+    }
diff --git a/docs/examples/Moonlight/Triangulation/Example/ConstrainedRegion.hs b/docs/examples/Moonlight/Triangulation/Example/ConstrainedRegion.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Triangulation/Example/ConstrainedRegion.hs
@@ -0,0 +1,37 @@
+module Moonlight.Triangulation.Example.ConstrainedRegion
+  ( constrainedRegionCounts
+  ) where
+
+import qualified Data.Vector as Vector
+import Moonlight.Triangulation
+
+-- | Count every face and only the bounded odd-depth domain faces of an annulus.
+constrainedRegionCounts :: Either CdtError (Int, Int)
+constrainedRegionCounts = do
+  result <-
+    constrainedDelaunay
+      unitElementDefaults
+      (Vector.fromList annulusPoints)
+      (Vector.fromList (closedLoop 0 <> closedLoop 4))
+  let annulus = buildTriangulation result
+  pure (numFaces annulus, length (boundedRegionFaces annulus))
+
+annulusPoints :: [Point]
+annulusPoints =
+  [ Point (-2) (-2)
+  , Point 2 (-2)
+  , Point 2 2
+  , Point (-2) 2
+  , Point (-1) (-1)
+  , Point 1 (-1)
+  , Point 1 1
+  , Point (-1) 1
+  ]
+
+closedLoop :: Int -> [(Int, Int)]
+closedLoop offset =
+  [ (offset, offset + 1)
+  , (offset + 1, offset + 2)
+  , (offset + 2, offset + 3)
+  , (offset + 3, offset)
+  ]
diff --git a/docs/examples/Moonlight/Triangulation/Example/PlanarRegion.hs b/docs/examples/Moonlight/Triangulation/Example/PlanarRegion.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Triangulation/Example/PlanarRegion.hs
@@ -0,0 +1,137 @@
+module Moonlight.Triangulation.Example.PlanarRegion
+  ( ExactFractionSummary (..)
+  , CellValuationSummary (..)
+  , PlanarExampleError (..)
+  , PlanarExampleSummary (..)
+  , planarExample
+  ) where
+
+import Data.Bifunctor (first)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import Moonlight.Triangulation
+
+data ExactFractionSummary = ExactFractionSummary
+  { fractionNumerator :: !Integer
+  , fractionDenominator :: !Integer
+  }
+  deriving stock (Eq, Show)
+
+data CellValuationSummary = CellValuationSummary
+  { cellEulerCharacteristic :: !Int
+  , cellExactArea :: !ExactFractionSummary
+  }
+  deriving stock (Eq, Show)
+
+data PlanarExampleSummary = PlanarExampleSummary
+  { booleanValuations :: ![CellValuationSummary]
+  , morphologyAreas :: ![ExactFractionSummary]
+  }
+  deriving stock (Eq, Show)
+
+data PlanarExampleError
+  = PlanarRegionFailed !RegionValidationError
+  | PlanarOverlayFailed !(OverlayError Bool Bool)
+  | PlanarSelectionFailed !OverlaySelectionError
+  | PlanarValuationFailed !ValuationError
+  | PlanarMinkowskiFailed !MinkowskiError
+  deriving stock (Eq, Show)
+
+-- | Compute exact Boolean valuations and morphology through the public facade.
+planarExample :: Either PlanarExampleError PlanarExampleSummary
+planarExample = do
+  left <- rectangleRegion 0 0 4 4
+  right <- rectangleRegion 2 1 6 3
+  leftLayer <- insideLayer left
+  rightLayer <- insideLayer right
+  overlay <- first PlanarOverlayFailed (overlayLayers leftLayer rightLayer)
+  unionCells <- first PlanarSelectionFailed (overlayClosedUnion id id overlay)
+  intersectionCells <-
+    first PlanarSelectionFailed (overlayClosedIntersection id id overlay)
+  differenceCells <-
+    first PlanarSelectionFailed (overlayRegularizedDifference id id overlay)
+  cellSummaries <-
+    traverse
+      cellValuationSummary
+      [unionCells, intersectionCells, differenceCells]
+
+  kernelLoop <- rectangleLoop (-1) (-1) 1 1
+  kernelPolygon <-
+    first PlanarMinkowskiFailed (convexPolygon (exactLoopPoints kernelLoop))
+  kernel <- first PlanarMinkowskiFailed (structuringElement kernelPolygon)
+  (sumRegion, _) <- first PlanarMinkowskiFailed (minkowskiSum left right)
+  (offsetRegion, _) <- first PlanarMinkowskiFailed (polygonOffset kernel left)
+  (insetRegion, _) <- first PlanarMinkowskiFailed (polygonInset kernel left)
+  (openedRegion, _) <- first PlanarMinkowskiFailed (openWith kernel left)
+  (closedRegion, _) <- first PlanarMinkowskiFailed (closeWith kernel left)
+  areaSummaries <-
+    traverse
+      regionAreaSummary
+      [sumRegion, offsetRegion, insetRegion, openedRegion, closedRegion]
+
+  pure
+    PlanarExampleSummary
+      { booleanValuations = cellSummaries
+      , morphologyAreas = areaSummaries
+      }
+
+rectangleLoop
+  :: Integer
+  -> Integer
+  -> Integer
+  -> Integer
+  -> Either PlanarExampleError ExactLoop
+rectangleLoop minimumX minimumY maximumX maximumY =
+  first PlanarRegionFailed
+    ( exactLoop
+        ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
+            :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
+               , exactPoint (fromInteger maximumX) (fromInteger maximumY)
+               , exactPoint (fromInteger minimumX) (fromInteger maximumY)
+               ]
+        )
+    )
+
+rectangleRegion
+  :: Integer
+  -> Integer
+  -> Integer
+  -> Integer
+  -> Either PlanarExampleError PlanarRegion
+rectangleRegion minimumX minimumY maximumX maximumY = do
+  outer <- rectangleLoop minimumX minimumY maximumX maximumY
+  component <- first PlanarRegionFailed (polygonComponent outer [])
+  first PlanarRegionFailed (planarRegion [component])
+
+insideLayer :: PlanarRegion -> Either PlanarExampleError (PlanarLayer Bool)
+insideLayer =
+  first PlanarRegionFailed
+    . planarLayer False
+    . Map.singleton True
+
+cellValuationSummary
+  :: ExactCellSet
+  -> Either PlanarExampleError CellValuationSummary
+cellValuationSummary cells = do
+  valuations <- first PlanarValuationFailed (cellValuations cells)
+  pure
+    CellValuationSummary
+      { cellEulerCharacteristic =
+          eulerCharacteristicValue (valuationEuler valuations)
+      , cellExactArea = exactAreaSummary valuations
+      }
+
+regionAreaSummary
+  :: PlanarRegion
+  -> Either PlanarExampleError ExactFractionSummary
+regionAreaSummary region =
+  exactAreaSummary
+    <$> first PlanarValuationFailed (regionValuations region)
+
+exactAreaSummary :: PlanarValuations -> ExactFractionSummary
+exactAreaSummary valuations =
+  let area = exactAreaValue (valuationArea valuations)
+   in ExactFractionSummary
+        { fractionNumerator = exactRationalNumerator area
+        , fractionDenominator = exactRationalDenominator area
+        }
diff --git a/docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs b/docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs
@@ -0,0 +1,26 @@
+module Moonlight.Triangulation.Example.SetAlgebra
+  ( siteCounts
+  ) where
+
+import qualified Data.Vector as Vector
+import Moonlight.Triangulation
+
+-- | Join and meet two overlapping site sets through the public facade.
+siteCounts :: Either BuildError (Int, Int)
+siteCounts = do
+  left <- unitSquareAt 0
+  right <- unitSquareAt 1
+  joined <- left `union` right
+  shared <- intersection left right
+  pure (numVertices joined, numVertices shared)
+
+unitSquareAt :: Double -> Either BuildError (DelaunayTriangulation ())
+unitSquareAt minimumX =
+  delaunayGeometry
+    ( Vector.fromList
+        [ Point minimumX 0
+        , Point (minimumX + 1) 0
+        , Point (minimumX + 1) 1
+        , Point minimumX 1
+        ]
+    )
diff --git a/docs/examples/README.md b/docs/examples/README.md
new file mode 100644
--- /dev/null
+++ b/docs/examples/README.md
@@ -0,0 +1,18 @@
+# Compile-checked usage examples
+
+[Return to the package overview.](../../README.md#use)
+· [Browse the documentation.](../README.md)
+
+Each example imports only the public `Moonlight.Triangulation` facade. The
+`moonlight-triangulation-examples-test` component compiles the modules and
+owns their deterministic expected results.
+
+| Workflow | Canonical source |
+| --- | --- |
+| Join and meet overlapping site sets | [`SetAlgebra.hs`](./Moonlight/Triangulation/Example/SetAlgebra.hs) |
+| Recover a constrained annulus interior | [`ConstrainedRegion.hs`](./Moonlight/Triangulation/Example/ConstrainedRegion.hs) |
+| Descend an alpha face section to its boundary | [`AlphaBoundary.hs`](./Moonlight/Triangulation/Example/AlphaBoundary.hs) |
+| Overlay, measure, and morph exact regions | [`PlanarRegion.hs`](./Moonlight/Triangulation/Example/PlanarRegion.hs) |
+
+[The focused test owner](../../test/examples/Main.hs) checks every published
+summary against these sources.
diff --git a/docs/mesh-publication.md b/docs/mesh-publication.md
new file mode 100644
--- /dev/null
+++ b/docs/mesh-publication.md
@@ -0,0 +1,56 @@
+# Mesh publication schedules
+
+[Read the set-operation types.](../README.md#set-operation-types)
+· [Choose an operation.](../README.md#operations)
+
+Programs choose an operation by its semantic result; Moonlight chooses its
+execution schedule. The schedule changes identity reuse, allocation, and
+throughput, not the represented site set.
+
+## Persistent publication schedules
+
+These are the currently measured publication choices. `canonicalize` remains
+the explicit global observation when construction-independent numbering is
+required.
+
+| Operation context | Publication schedule |
+| --- | --- |
+| `siteRelation left right` | Index the smaller support in one transient exact open-addressed section, scan the other operand, then discard the index; no support maps or published cache state. |
+| `difference mesh empty`, `symmetricDifference mesh empty`, `symmetricDifference empty mesh` | Return the surviving representative verbatim. |
+| `difference left right` with `size right <= (size left - size right) / 128` | Remove the right support through one local copy-on-write session; return `left` verbatim when the supports are disjoint. Larger masks rebuild. |
+| Geometry-only `intersection left right` | Return an existing operand for equality or subset, return the empty mesh for disjoint supports, and locally remove the smaller complement when it is at most `overlap / 128`. Other partial overlaps rebuild. |
+| `intersectionWith` | Rebuild, because the annotation combiner may rewrite every surviving payload even when topology changes locally. |
+| `symmetricDifference left right` with `small <= (large - small) / 128` | Toggle the smaller operand through one local session. Comparable operands and small-output, large-input cases rebuild. |
+| Comparable `symmetricDifference left right` | Partition through one transient exact index and a matched bitset, then rebuild only the exclusive output section. |
+| Package-internal `BulkLoad.insert` / `BulkLoad.insertAt` | Dense publication below 10,000 resident sites; copy-on-write publication at 10,000 and above. Internal edit sequences descend through one `Session`. |
+| `extendConstrainedWith` | Copy-on-write only for a base of at least 200,000 sites, at most 128 incoming sites, exactly one incoming segment, and a pre-thaw corridor with no resident intersection. Every unmeasured or resident-corridor case stays dense. |
+| `refineWithinDomain` | Dense publication. The measured local transaction candidate did not improve wall time. |
+
+## Persistent values at scale
+
+`union` classifies a pair before selecting its schedule: empty and repeated
+operands return an existing value, separated operands merge at their seam,
+subsets reuse their superset, skewed pairs insert the smaller support, and the
+remaining cases rebuild.
+
+```haskell
+insertionIsCheaper addition base = addition <= 64 || addition <= base `quot` 8
+```
+
+Local schedules publish a fresh immutable mesh from the pages they touched, so
+readers continue observing the previous value while the next is built. A failed
+operation returns its obstruction with both operands unchanged.
+
+`Moonlight.Triangulation.Parallel.unionsConcurrently` interprets the same pure
+join tournament with bounded concurrency.
+
+## Source authority
+
+The executable schedule decisions live in
+[`SetAlgebra`](../src-build/Moonlight/Triangulation/SetAlgebra.hs),
+[`BulkLoad`](../src-build/Moonlight/Triangulation/BulkLoad.hs),
+[`Join.Plan`](../src-build/Moonlight/Triangulation/Internal/Join/Plan.hs), and
+[`Cdt.Union`](../src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs). The
+focused measurements live in
+[`JoinBench`](../bench/join/Moonlight/Triangulation/JoinBench.hs); this page is
+their consumer-facing projection.
diff --git a/docs/moonlight-triangulation-alpha-eclipse.svg b/docs/moonlight-triangulation-alpha-eclipse.svg
deleted file mode 100644
--- a/docs/moonlight-triangulation-alpha-eclipse.svg
+++ /dev/null
@@ -1,91 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 620" width="1440" height="620" role="img" aria-labelledby="title description">
-<title id="title">Moonlight Triangulation alpha eclipse</title>
-<desc id="description">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius 1, and its resident diagonal and faces kill that hole at squared radius 2.</desc>
-<defs>
-<radialGradient id="eclipse-background" cx="50%" cy="38%" r="78%"><stop offset="0" stop-color="#111744"/><stop offset="0.55" stop-color="#070b25"/><stop offset="1" stop-color="#02040f"/></radialGradient>
-<linearGradient id="eclipse-corona" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#66efff"/><stop offset="0.48" stop-color="#ff72c6"/><stop offset="1" stop-color="#ffd86b"/></linearGradient>
-<linearGradient id="eclipse-face" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c4dff"/><stop offset="0.5" stop-color="#ff4fa3"/><stop offset="1" stop-color="#ffbf5b"/></linearGradient>
-<filter id="eclipse-soft-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="7" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-<filter id="eclipse-dot-glow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-</defs>
-<rect width="1440" height="620" fill="url(#eclipse-background)"/>
-<circle cx="720" cy="300" r="246" fill="#ff68bb" opacity="0.035" filter="url(#eclipse-soft-glow)"/>
-<text x="58" y="58" fill="#84f3ff" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="17" font-weight="600" letter-spacing="6">MOONLIGHT TRIANGULATION</text>
-<text x="58" y="101" fill="#f7f3ff" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="38" font-weight="420">ALPHA ECLIPSE</text>
-<text x="1382" y="61" text-anchor="end" fill="#9baedb" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.5">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>
-<g transform="translate(245.000 292)">
-<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#344070" stroke-opacity="0.55"/>
-<text x="-162" y="-114" fill="#8fa4d6" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">BEFORE BIRTH</text>
-<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>
-<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"></g>
-<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"></g>
-<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"></g>
-<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>
-<circle cx="-101.823" cy="0.000" r="6"/>
-<circle cx="101.823" cy="0.000" r="6"/>
-<circle cx="0.000" cy="101.823" r="6"/>
-</g>
-<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>
-<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">four isolated sites</text>
-<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 0</text>
-<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 0</text>
-</g>
-<g transform="translate(720.000 292)">
-<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#ff6fbd" stroke-opacity="0.72"/>
-<text x="-162" y="-114" fill="#ff89cb" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">LOOP BORN</text>
-<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>
-<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"></g>
-<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"><line x1="0.000" y1="-101.823" x2="-101.823" y2="0.000"/>
-<line x1="0.000" y1="-101.823" x2="101.823" y2="0.000"/>
-<line x1="-101.823" y1="0.000" x2="0.000" y2="101.823"/>
-<line x1="101.823" y1="0.000" x2="0.000" y2="101.823"/>
-</g>
-<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"><path d="M -101.823,0.000 L 0.000,-101.823 L 101.823,0.000 L 0.000,101.823 Z"/>
-</g>
-<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>
-<circle cx="-101.823" cy="0.000" r="6"/>
-<circle cx="101.823" cy="0.000" r="6"/>
-<circle cx="0.000" cy="101.823" r="6"/>
-</g>
-<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>
-<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">resident boundary closes</text>
-<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 1</text>
-<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 1</text>
-</g>
-<g transform="translate(1195.000 292)">
-<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#344070" stroke-opacity="0.55"/>
-<text x="-162" y="-114" fill="#8fa4d6" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">CYCLE KILLED</text>
-<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>
-<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"><polygon points="0.000,-101.823 0.000,101.823 -101.823,0.000"/>
-<polygon points="0.000,101.823 0.000,-101.823 101.823,0.000"/>
-</g>
-<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"><line x1="0.000" y1="-101.823" x2="-101.823" y2="0.000"/>
-<line x1="0.000" y1="101.823" x2="0.000" y2="-101.823"/>
-<line x1="0.000" y1="-101.823" x2="101.823" y2="0.000"/>
-<line x1="-101.823" y1="0.000" x2="0.000" y2="101.823"/>
-<line x1="101.823" y1="0.000" x2="0.000" y2="101.823"/>
-</g>
-<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"></g>
-<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>
-<circle cx="-101.823" cy="0.000" r="6"/>
-<circle cx="101.823" cy="0.000" r="6"/>
-<circle cx="0.000" cy="101.823" r="6"/>
-</g>
-<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>
-<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">resident faces fill it</text>
-<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 0</text>
-<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 2</text>
-</g>
-<g font-family="Inter,ui-sans-serif,system-ui,sans-serif">
-<text x="58" y="516" fill="#9badd8" font-size="13" letter-spacing="2.5">ONE EXACT H₁ INTERVAL</text>
-<line x1="58" y1="559" x2="1382" y2="559" stroke="#344070" stroke-width="3"/>
-<line x1="535" y1="559" x2="1042" y2="559" stroke="#ff72c6" stroke-width="9" stroke-linecap="round"/>
-<circle cx="535" cy="559" r="8" fill="#fff6d6" stroke="#ff72c6" stroke-width="3"/>
-<rect x="1034" y="551" width="16" height="16" fill="#fff6d6" stroke="#66efff" stroke-width="3"/>
-<text x="58" y="589" fill="#7588ba" font-size="12">α² = 0</text>
-<text x="535" y="589" text-anchor="middle" fill="#f7f3ff" font-size="12">birth α² = 1</text>
-<text x="788" y="548" text-anchor="middle" fill="#fff6d6" font-size="14">the eclipse hole persists</text>
-<text x="1042" y="589" text-anchor="middle" fill="#f7f3ff" font-size="12">death α² = 2</text>
-<text x="1382" y="589" text-anchor="end" fill="#7588ba" font-size="12">exact α² increases →</text>
-</g>
-</svg>
diff --git a/docs/moonlight-triangulation-persistence-rose.svg b/docs/moonlight-triangulation-persistence-rose.svg
deleted file mode 100644
--- a/docs/moonlight-triangulation-persistence-rose.svg
+++ /dev/null
@@ -1,1895 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" width="1920" height="1080" role="img" aria-labelledby="title description">
-<title id="title">Moonlight Triangulation Persistence Rose</title>
-<desc id="description">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>
-<defs>
-<radialGradient id="background" cx="52%" cy="48%" r="72%"><stop offset="0" stop-color="#10143d"/><stop offset="0.48" stop-color="#070b24"/><stop offset="1" stop-color="#02040e"/></radialGradient>
-<radialGradient id="rose-wash" cx="48%" cy="42%" r="63%"><stop offset="0" stop-color="#ffe7a3"/><stop offset="0.38" stop-color="#ff5fb7"/><stop offset="0.72" stop-color="#7c4dff"/><stop offset="1" stop-color="#00d9ff"/></radialGradient>
-<linearGradient id="mask-stroke" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffd86b"/><stop offset="0.48" stop-color="#ff72c6"/><stop offset="1" stop-color="#66efff"/></linearGradient>
-<linearGradient id="hole-stroke" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff9db"/><stop offset="0.52" stop-color="#ff9ad7"/><stop offset="1" stop-color="#9ff7ff"/></linearGradient>
-<filter id="field-soften" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="24"/></filter>
-<filter id="hole-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="7" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-<filter id="edge-glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-<clipPath id="rose-clip"><path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill-rule="evenodd"/></clipPath>
-</defs>
-<rect width="1920" height="1080" fill="url(#background)"/>
-<circle cx="960" cy="552" r="322.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
-<circle cx="960" cy="552" r="356.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
-<circle cx="960" cy="552" r="390.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
-<circle cx="960" cy="552" r="424.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
-<circle cx="960" cy="552" r="458.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
-<circle cx="960" cy="552" r="492.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
-<g fill="none" stroke-linecap="round">
-<polyline points="1077.000,949.123 1051.592,955.741 1025.816,960.735 999.775,964.085 973.574,965.777 947.318,965.806 921.113,964.170 895.065,960.876 869.278,955.937 843.856,949.374 818.900,941.213 794.513,931.486 770.791,920.233 747.830,907.499 725.722,893.336 704.557,877.799 684.419,860.952 665.389,842.862 647.545,823.602 630.957,803.250 615.692,781.888 601.813,759.600 589.374,736.478 578.426,712.614 569.012,688.104 561.171,663.046 554.934,637.542 550.326,611.694 547.366,585.606 546.066,559.382 546.430,533.129 548.458,506.952 552.141,480.956 557.465,455.245 564.407,429.924 572.941,405.094 583.032,380.854 594.638,357.303 607.714,334.536 622.207,312.642 638.059,291.712 655.206,271.828 673.578,253.071" stroke="hsl(188 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="1232.734,230.630 1252.266,248.285 1270.657,267.124 1287.837,287.075 1303.737,308.059 1318.296,329.995 1331.457,352.798 1343.169,376.377 1353.387,400.642 1362.069,425.497 1369.182,450.846 1374.699,476.589 1378.598,502.627 1380.864,528.857 1381.488,555.178 1380.467,581.486 1377.806,607.679 1373.515,633.655 1367.610,659.313 1360.115,684.551 1351.059,709.273 1340.478,733.381 1328.411,756.781 1314.908,779.382 1300.020,801.096 1283.805,821.839 1266.326,841.528 1247.653,860.088 1227.857,877.446 1207.016,893.534 1185.212,908.289 1162.529,921.655 1139.055,933.578 1114.883,944.012 1090.107,952.917 1064.823,960.258 1039.130,966.006 1013.128,970.138 986.919,972.640 960.605,973.500 934.289,972.715 908.073,970.289 882.060,966.231" stroke="hsl(205 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1035.085,974.378 1008.772,978.219 982.273,980.421 955.687,980.978 929.118,979.887 902.668,977.152 876.438,972.783 850.530,966.798 825.041,959.219 800.072,950.075 775.716,939.402 752.069,927.241 729.221,913.638 707.259,898.645 686.268,882.321 666.329,864.728 647.519,845.933 629.908,826.008 613.567,805.031 598.556,783.082 584.933,760.245 572.752,736.608 562.059,712.262 552.894,687.300 545.294,661.818 539.287,635.914 534.896,609.688 532.139,583.240 531.025,556.672 531.560,530.086 533.741,503.585 537.559,477.269 543.001,451.240 550.044,425.599 558.663,400.443 568.823,375.870 580.487,351.973 593.608,328.845 608.138,306.574 624.019,285.246 641.191,264.943 659.588,245.743 679.139,227.719" stroke="hsl(222 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="1040.714,980.973 1014.190,985.123 987.460,987.635 960.627,988.500 933.791,987.712 907.055,985.277 880.518,981.203 854.283,975.505 828.447,968.204 803.109,959.330 778.365,948.914 754.307,936.997 731.028,923.624 708.615,908.844 687.153,892.715 666.723,875.297 647.403,856.656 629.265,836.862 612.378,815.991 596.806,794.121 582.608,771.335 569.838,747.719 558.544,723.363 548.769,698.359 540.549,672.801 533.916,646.786 528.894,620.413 525.504,593.780 523.757,566.990 523.661,540.143 525.215,513.341 528.414,486.685 533.246,460.276 539.692,434.214 547.728,408.598 557.324,383.524 568.443,359.087 581.043,335.381 595.077,312.493 610.491,290.512 627.228,269.520 645.223,249.597 664.409,230.817" stroke="hsl(239 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="1076.891,980.337 1050.961,986.583 1024.703,991.260 998.212,994.353 971.583,995.849 944.912,995.744 918.295,994.037 891.829,990.735 865.609,985.851 839.730,979.400 814.284,971.408 789.365,961.902 765.061,950.917 741.460,938.492 718.648,924.673 696.707,909.509 675.716,893.056 655.751,875.371 636.884,856.519 619.183,836.569 602.711,815.592 587.529,793.664 573.691,770.863 561.247,747.273 550.241,722.979 540.714,698.067 532.701,672.628 526.229,646.754 521.322,620.538 517.998,594.075 516.269,567.460 516.142,540.789 517.616,514.159 520.686,487.665 525.341,461.403 531.565,435.468 539.335,409.954 548.623,384.952 559.395,360.553 571.613,336.845 585.232,313.913 600.204,291.841 616.474,270.707" stroke="hsl(256 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1052.392,993.946 1026.117,998.633 999.611,1001.759 972.967,1003.314 946.277,1003.291 919.635,1001.692 893.135,998.521 866.868,993.790 840.926,987.515 815.401,979.719 790.381,970.428 765.954,959.674 742.204,947.496 719.216,933.936 697.069,919.042 675.841,902.864 655.606,885.461 636.434,866.893 618.393,847.224 601.546,826.523 585.952,804.864 571.664,782.320 558.734,758.972 547.205,734.901 537.119,710.190 528.511,684.927 521.411,659.199 515.843,633.097 511.827,606.711 509.377,580.134 508.502,553.459 509.205,526.779 511.483,500.186 515.328,473.775 520.727,447.637 527.661,421.864 536.106,396.546 546.032,371.771 557.404,347.625 570.184,324.194 584.325,301.559 599.780,279.799 616.493,258.990" stroke="hsl(273 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="1071.913,997.148 1045.954,1002.880 1019.707,1007.100 993.260,1009.793 966.702,1010.951 940.120,1010.569 913.606,1008.649 887.247,1005.198 861.132,1000.226 835.349,993.750 809.984,985.793 785.123,976.381 760.847,965.545 737.240,953.322 714.380,939.752 692.344,924.883 671.206,908.762 651.036,891.444 631.903,872.988 613.871,853.456 596.999,832.912 581.345,811.426 566.962,789.069 553.896,765.918 542.193,742.048 531.891,717.542 523.026,692.480 515.626,666.947 509.717,641.028 505.318,614.810 502.444,588.382 501.105,561.832 501.306,535.249 503.045,508.722 506.317,482.341 511.110,456.192 517.410,430.366 525.194,404.947 534.436,380.022 545.106,355.673 557.168,331.983 570.581,309.031 585.300,286.894" stroke="hsl(290 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1052.848,955.454 1029.335,960.153 1005.590,963.482 981.692,965.431 957.721,965.994 933.757,965.167 909.882,962.955 886.175,959.365 862.715,954.407 839.582,948.100 816.853,940.465 794.604,931.526 772.909,921.314 751.843,909.864 731.474,897.213 711.872,883.404 693.102,868.483 675.228,852.501 658.309,835.511 642.402,817.570 627.560,798.738 613.833,779.078 601.267,758.657 589.905,737.542 579.784,715.805 570.939,693.519 563.399,670.758 557.189,647.598 552.330,624.118 548.838,600.396 546.726,576.512 546.000,552.545 546.663,528.577 548.712,504.687 552.141,480.956 556.938,457.463 563.087,434.287 570.568,411.506 579.354,389.197 589.418,367.433 600.725,346.289 613.236,325.834 626.911,306.139" stroke="hsl(307 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1245.433,241.855 1255.448,251.380 1265.146,261.228 1274.515,271.389 1283.546,281.852 1292.229,292.605 1300.555,303.637 1308.515,314.936 1316.100,326.490 1323.303,338.286 1330.114,350.313 1336.528,362.556 1342.536,375.003 1348.134,387.640 1353.314,400.454 1358.071,413.430 1362.400,426.556 1366.297,439.817 1369.757,453.198 1372.776,466.686 1375.351,480.265 1377.479,493.921 1379.159,507.640 1380.388,521.407 1381.165,535.206 1381.489,549.024 1381.360,562.844 1380.778,576.654 1379.744,590.436 1378.258,604.177 1376.322,617.862 1373.939,631.477 1371.111,645.006 1367.841,658.434 1364.132,671.749 1359.988,684.934 1355.415,697.977 1350.416,710.863 1344.998,723.578 1339.165,736.108 1332.925,748.441 1326.284,760.562 1319.249,772.459" stroke="hsl(202 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1302.579,293.774 1309.377,303.049 1315.925,312.504 1322.217,322.130 1328.248,331.922 1334.015,341.872 1339.513,351.973 1344.739,362.218 1349.687,372.599 1354.356,383.109 1358.742,393.740 1362.841,404.486 1366.650,415.337 1370.167,426.286 1373.389,437.326 1376.315,448.448 1378.941,459.645 1381.266,470.907 1383.288,482.229 1385.006,493.600 1386.419,505.013 1387.525,516.460 1388.324,527.933 1388.816,539.423 1388.999,550.922 1388.873,562.422 1388.440,573.914 1387.699,585.390 1386.650,596.843 1385.295,608.263 1383.634,619.643 1381.668,630.974 1379.400,642.248 1376.830,653.458 1373.961,664.595 1370.794,675.651 1367.332,686.617 1363.577,697.488 1359.532,708.253 1355.200,718.907 1350.584,729.440 1345.688,739.846 1340.514,750.117" stroke="hsl(219 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1313.780,296.320 1320.034,305.205 1326.065,314.243 1331.870,323.428 1337.444,332.755 1342.784,342.218 1347.887,351.811 1352.750,361.528 1357.369,371.363 1361.743,381.310 1365.867,391.362 1369.739,401.514 1373.358,411.760 1376.721,422.092 1379.825,432.505 1382.669,442.992 1385.252,453.546 1387.571,464.161 1389.624,474.831 1391.412,485.549 1392.933,496.307 1394.185,507.101 1395.168,517.922 1395.881,528.764 1396.324,539.621 1396.497,550.485 1396.400,561.350 1396.032,572.210 1395.394,583.057 1394.486,593.884 1393.309,604.686 1391.863,615.455 1390.150,626.185 1388.170,636.869 1385.925,647.500 1383.416,658.072 1380.644,668.578 1377.612,679.012 1374.322,689.368 1370.774,699.638 1366.972,709.817 1362.918,719.898 1358.614,729.875" stroke="hsl(236 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1356.866,352.918 1360.834,361.033 1364.636,369.227 1368.270,377.497 1371.736,385.839 1375.031,394.250 1378.155,402.726 1381.105,411.264 1383.881,419.860 1386.481,428.511 1388.905,437.213 1391.152,445.962 1393.220,454.756 1395.109,463.589 1396.817,472.459 1398.345,481.362 1399.691,490.295 1400.856,499.253 1401.838,508.233 1402.637,517.230 1403.252,526.243 1403.685,535.266 1403.933,544.295 1403.998,553.328 1403.879,562.361 1403.576,571.389 1403.090,580.409 1402.421,589.418 1401.568,598.411 1400.532,607.384 1399.314,616.335 1397.914,625.259 1396.333,634.153 1394.572,643.013 1392.630,651.835 1390.510,660.616 1388.211,669.352 1385.735,678.039 1383.083,686.674 1380.255,695.254 1377.254,703.774 1374.080,712.231 1370.734,720.622" stroke="hsl(253 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1343.627,313.919 1348.226,321.494 1352.675,329.158 1356.973,336.907 1361.118,344.740 1365.108,352.652 1368.943,360.641 1372.619,368.704 1376.137,376.837 1379.495,385.038 1382.691,393.304 1385.724,401.630 1388.593,410.014 1391.298,418.453 1393.835,426.944 1396.206,435.482 1398.409,444.066 1400.443,452.691 1402.307,461.354 1404.001,470.053 1405.524,478.783 1406.875,487.541 1408.054,496.324 1409.060,505.128 1409.894,513.950 1410.554,522.787 1411.041,531.636 1411.353,540.492 1411.492,549.352 1411.457,558.214 1411.248,567.073 1410.866,575.926 1410.309,584.771 1409.579,593.602 1408.676,602.418 1407.600,611.214 1406.352,619.987 1404.932,628.734 1403.340,637.452 1401.577,646.136 1399.645,654.785 1397.543,663.394 1395.272,671.959" stroke="hsl(270 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1374.328,354.482 1377.670,361.652 1380.889,368.878 1383.982,376.158 1386.949,383.491 1389.790,390.873 1392.503,398.304 1395.087,405.780 1397.542,413.299 1399.867,420.860 1402.062,428.460 1404.125,436.096 1406.057,443.767 1407.856,451.470 1409.522,459.203 1411.054,466.963 1412.453,474.749 1413.717,482.558 1414.846,490.387 1415.840,498.234 1416.699,506.098 1417.422,513.975 1418.010,521.863 1418.461,529.761 1418.776,537.665 1418.955,545.573 1418.998,553.483 1418.904,561.393 1418.674,569.300 1418.308,577.202 1417.805,585.096 1417.167,592.980 1416.393,600.853 1415.483,608.710 1414.438,616.551 1413.258,624.373 1411.944,632.173 1410.495,639.950 1408.913,647.700 1407.197,655.422 1405.348,663.113 1403.367,670.771 1401.254,678.394" stroke="hsl(287 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1324.284,355.295 1327.575,361.514 1330.759,367.788 1333.835,374.115 1336.804,380.494 1339.665,386.922 1342.415,393.397 1345.055,399.919 1347.584,406.484 1350.001,413.092 1352.305,419.740 1354.496,426.425 1356.573,433.147 1358.535,439.904 1360.383,446.693 1362.115,453.512 1363.730,460.359 1365.229,467.234 1366.611,474.132 1367.876,481.053 1369.022,487.995 1370.051,494.955 1370.961,501.931 1371.753,508.922 1372.425,515.926 1372.979,522.940 1373.413,529.962 1373.728,536.990 1373.923,544.023 1373.999,551.059 1373.955,558.094 1373.792,565.128 1373.509,572.158 1373.107,579.182 1372.585,586.198 1371.944,593.205 1371.185,600.199 1370.306,607.180 1369.309,614.144 1368.194,621.091 1366.961,628.018 1365.610,634.923 1364.143,641.804" stroke="hsl(304 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="546.368,470.939 547.596,464.908 548.911,458.896 550.314,452.904 551.805,446.933 553.383,440.984 555.047,435.059 556.797,429.159 558.634,423.286 560.556,417.439 562.563,411.621 564.655,405.834 566.831,400.077 569.091,394.353 571.435,388.662 573.861,383.006 576.369,377.387 578.960,371.804 581.631,366.260 584.384,360.755 587.216,355.292 590.128,349.870 593.118,344.491 596.187,339.157 599.333,333.867 602.557,328.625 605.856,323.430 609.231,318.283 612.681,313.187 616.205,308.141 619.802,303.148 623.471,298.207 627.213,293.321 631.025,288.490 634.907,283.715 638.859,278.997 642.879,274.337 646.967,269.737 651.122,265.196 655.342,260.717 659.627,256.300 663.977,251.946 668.389,247.656" stroke="hsl(199 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="536.712,482.229 537.739,476.258 538.851,470.303 540.047,464.363 541.326,458.442 542.689,452.539 544.135,446.655 545.664,440.793 547.276,434.953 548.970,429.136 550.746,423.344 552.604,417.578 554.543,411.838 556.563,406.126 558.663,400.443 560.843,394.791 563.103,389.170 565.442,383.581 567.860,378.026 570.356,372.506 572.929,367.021 575.580,361.573 578.307,356.164 581.111,350.793 583.990,345.463 586.944,340.173 589.973,334.926 593.075,329.722 596.251,324.563 599.499,319.449 602.819,314.381 606.210,309.361 609.672,304.389 613.204,299.466 616.804,294.594 620.474,289.773 624.211,285.005 628.014,280.290 631.885,275.628 635.820,271.022 639.820,266.472 643.884,261.979 648.012,257.544" stroke="hsl(216 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="1391.710,616.489 1390.790,622.373 1389.790,628.243 1388.710,634.100 1387.550,639.941 1386.310,645.766 1384.991,651.573 1383.593,657.362 1382.116,663.132 1380.561,668.880 1378.927,674.607 1377.215,680.311 1375.426,685.991 1373.559,691.647 1371.615,697.276 1369.595,702.878 1367.498,708.452 1365.326,713.997 1363.078,719.512 1360.755,724.995 1358.358,730.447 1355.886,735.865 1353.341,741.249 1350.722,746.598 1348.031,751.910 1345.268,757.186 1342.432,762.423 1339.526,767.621 1336.549,772.779 1333.502,777.895 1330.385,782.970 1327.199,788.002 1323.946,792.989 1320.624,797.932 1317.235,802.829 1313.780,807.680 1310.258,812.483 1306.672,817.237 1303.021,821.942 1299.306,826.597 1295.528,831.200 1291.688,835.752 1287.786,840.251" stroke="hsl(233 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="521.911,479.789 522.937,473.819 524.045,467.863 525.233,461.923 526.503,456.000 527.853,450.095 529.283,444.209 530.794,438.342 532.384,432.497 534.055,426.674 535.804,420.875 537.632,415.100 539.540,409.350 541.525,403.627 543.588,397.931 545.729,392.265 547.947,386.628 550.241,381.021 552.612,375.447 555.059,369.905 557.581,364.398 560.178,358.925 562.849,353.488 565.594,348.088 568.413,342.726 571.304,337.403 574.268,332.120 577.304,326.878 580.411,321.678 583.589,316.521 586.836,311.407 590.154,306.338 593.540,301.315 596.994,296.339 600.516,291.410 604.104,286.530 607.759,281.699 611.480,276.919 615.265,272.190 619.115,267.512 623.028,262.888 627.003,258.318 631.041,253.802" stroke="hsl(250 92% 72%)" stroke-width="2.600" opacity="0.720"/>
-<polyline points="1394.664,429.854 1396.010,434.752 1397.302,439.664 1398.538,444.591 1399.718,449.531 1400.843,454.484 1401.913,459.450 1402.926,464.427 1403.883,469.415 1404.784,474.414 1405.628,479.423 1406.417,484.440 1407.149,489.467 1407.824,494.501 1408.442,499.542 1409.004,504.590 1409.509,509.645 1409.957,514.704 1410.348,519.768 1410.682,524.837 1410.959,529.908 1411.179,534.983 1411.342,540.060 1411.448,545.138 1411.496,550.217 1411.488,555.296 1411.422,560.375 1411.300,565.453 1411.120,570.529 1410.883,575.603 1410.589,580.673 1410.238,585.741 1409.829,590.804 1409.364,595.861 1408.843,600.914 1408.264,605.960 1407.629,611.000 1406.936,616.031 1406.188,621.055 1405.383,626.070 1404.521,631.076 1403.604,636.072 1402.630,641.057" stroke="hsl(267 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1231.657,182.022 1235.714,185.035 1239.737,188.093 1243.727,191.195 1247.682,194.341 1251.603,197.530 1255.488,200.763 1259.337,204.037 1263.150,207.354 1266.927,210.713 1270.666,214.113 1274.367,217.554 1278.031,221.035 1281.655,224.557 1285.241,228.118 1288.787,231.719 1292.294,235.359 1295.760,239.036 1299.185,242.752 1302.570,246.505 1305.913,250.296 1309.214,254.123 1312.472,257.986 1315.688,261.884 1318.861,265.818 1321.990,269.787 1325.075,273.789 1328.116,277.826 1331.113,281.896 1334.064,285.998 1336.970,290.133 1339.831,294.299 1342.645,298.497 1345.413,302.725 1348.134,306.984 1350.808,311.272 1353.435,315.590 1356.014,319.936 1358.545,324.310 1361.028,328.712 1363.462,333.141 1365.847,337.597 1368.184,342.078" stroke="hsl(284 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1265.330,831.588 1262.369,834.788 1259.374,837.957 1256.346,841.094 1253.285,844.199 1250.191,847.271 1247.065,850.311 1243.908,853.318 1240.719,856.291 1237.498,859.231 1234.247,862.136 1230.966,865.007 1227.654,867.843 1224.313,870.645 1220.943,873.411 1217.543,876.141 1214.115,878.835 1210.659,881.494 1207.175,884.115 1203.664,886.700 1200.125,889.248 1196.560,891.758 1192.969,894.230 1189.352,896.665 1185.709,899.061 1182.041,901.419 1178.349,903.738 1174.633,906.018 1170.892,908.259 1167.129,910.460 1163.342,912.622 1159.533,914.743 1155.702,916.825 1151.848,918.865 1147.974,920.865 1144.079,922.825 1140.163,924.743 1136.228,926.619 1132.273,928.455 1128.298,930.248 1124.306,932.000 1120.294,933.709 1116.266,935.376" stroke="hsl(301 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1369.037,450.258 1370.062,454.474 1371.044,458.699 1371.983,462.935 1372.877,467.180 1373.729,471.434 1374.536,475.696 1375.299,479.967 1376.019,484.245 1376.694,488.530 1377.325,492.822 1377.912,497.121 1378.455,501.425 1378.953,505.734 1379.407,510.049 1379.817,514.368 1380.182,518.691 1380.502,523.017 1380.778,527.346 1381.010,531.679 1381.197,536.013 1381.339,540.349 1381.437,544.686 1381.489,549.024 1381.498,553.362 1381.461,557.700 1381.380,562.038 1381.255,566.374 1381.085,570.709 1380.870,575.042 1380.610,579.372 1380.306,583.700 1379.958,588.024 1379.565,592.345 1379.127,596.661 1378.645,600.972 1378.119,605.278 1377.549,609.579 1376.934,613.873 1376.275,618.161 1375.572,622.442 1374.825,626.716 1374.034,630.981" stroke="hsl(196 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="543.685,448.448 544.773,444.169 545.905,439.901 547.081,435.645 548.300,431.401 549.563,427.170 550.870,422.952 552.219,418.748 553.613,414.558 555.049,410.383 556.528,406.223 558.049,402.078 559.614,397.949 561.220,393.836 562.869,389.740 564.561,385.661 566.293,381.600 568.068,377.557 569.884,373.532 571.742,369.527 573.640,365.540 575.580,361.573 577.560,357.627 579.581,353.701 581.642,349.796 583.743,345.913 585.884,342.051 588.065,338.212 590.285,334.395 592.544,330.601 594.842,326.831 597.179,323.085 599.555,319.363 601.968,315.665 604.419,311.993 606.909,308.346 609.435,304.725 611.999,301.130 614.599,297.561 617.236,294.020 619.909,290.505 622.619,287.019 625.364,283.561" stroke="hsl(213 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1221.864,202.773 1225.279,205.360 1228.668,207.980 1232.031,210.633 1235.368,213.319 1238.678,216.038 1241.962,218.789 1245.218,221.572 1248.447,224.387 1251.648,227.233 1254.821,230.111 1257.966,233.020 1261.082,235.959 1264.169,238.929 1267.227,241.929 1270.255,244.959 1273.253,248.018 1276.221,251.107 1279.159,254.225 1282.066,257.371 1284.941,260.546 1287.786,263.749 1290.599,266.980 1293.380,270.238 1296.129,273.523 1298.846,276.835 1301.530,280.174 1304.181,283.538 1306.799,286.929 1309.384,290.345 1311.935,293.786 1314.452,297.252 1316.935,300.743 1319.383,304.258 1321.797,307.797 1324.176,311.359 1326.520,314.945 1328.829,318.553 1331.102,322.184 1333.339,325.837 1335.541,329.511 1337.706,333.207 1339.835,336.925" stroke="hsl(230 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="522.660,475.389 523.432,471.100 524.247,466.820 525.104,462.548 526.003,458.284 526.944,454.030 527.926,449.785 528.950,445.549 530.015,441.324 531.122,437.110 532.270,432.907 533.460,428.715 534.690,424.535 535.961,420.367 537.274,416.212 538.626,412.070 540.020,407.942 541.454,403.827 542.928,399.727 544.443,395.641 545.997,391.571 547.591,387.516 549.225,383.476 550.899,379.453 552.612,375.447 554.364,371.457 556.156,367.485 557.986,363.531 559.855,359.595 561.762,355.677 563.708,351.779 565.692,347.899 567.714,344.040 569.774,340.200 571.871,336.381 574.006,332.582 576.177,328.805 578.386,325.049 580.632,321.315 582.914,317.603 585.232,313.913 587.587,310.247 589.977,306.604" stroke="hsl(247 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="516.078,469.628 516.907,465.275 517.780,460.931 518.695,456.596 519.652,452.269 520.652,447.953 521.694,443.646 522.779,439.350 523.905,435.065 525.074,430.791 526.284,426.529 527.537,422.278 528.831,418.041 530.166,413.816 531.543,409.604 532.961,405.406 534.420,401.223 535.920,397.053 537.461,392.899 539.043,388.760 540.665,384.637 542.328,380.530 544.030,376.439 545.773,372.365 547.556,368.309 549.379,364.270 551.241,360.250 553.142,356.248 555.083,352.264 557.062,348.300 559.081,344.356 561.138,340.431 563.233,336.527 565.367,332.644 567.539,328.782 569.748,324.941 571.995,321.122 574.280,317.326 576.601,313.552 578.960,309.801 581.355,306.073 583.787,302.369 586.255,298.689" stroke="hsl(264 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1403.110,432.274 1404.208,436.415 1405.268,440.567 1406.289,444.728 1407.271,448.899 1408.214,453.079 1409.118,457.267 1409.982,461.464 1410.808,465.668 1411.594,469.880 1412.341,474.099 1413.049,478.325 1413.717,482.558 1414.345,486.796 1414.934,491.040 1415.483,495.290 1415.993,499.544 1416.463,503.803 1416.893,508.066 1417.283,512.333 1417.633,516.603 1417.944,520.877 1418.214,525.153 1418.445,529.432 1418.636,533.712 1418.786,537.994 1418.897,542.278 1418.968,546.562 1418.999,550.846 1418.989,555.131 1418.940,559.416 1418.851,563.699 1418.722,567.982 1418.552,572.264 1418.343,576.543 1418.094,580.821 1417.805,585.096 1417.476,589.368 1417.108,593.637 1416.699,597.902 1416.251,602.163 1415.762,606.420 1415.235,610.672" stroke="hsl(281 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1362.256,649.911 1361.324,653.661 1360.358,657.403 1359.356,661.136 1358.320,664.859 1357.249,668.572 1356.144,672.276 1355.004,675.968 1353.829,679.650 1352.620,683.321 1351.377,686.980 1350.100,690.628 1348.789,694.263 1347.444,697.887 1346.066,701.497 1344.653,705.094 1343.207,708.678 1341.728,712.249 1340.216,715.805 1338.670,719.347 1337.091,722.875 1335.480,726.387 1333.835,729.885 1332.159,733.367 1330.449,736.833 1328.708,740.283 1326.934,743.717 1325.129,747.134 1323.291,750.534 1321.422,753.916 1319.521,757.281 1317.589,760.628 1315.626,763.957 1313.632,767.268 1311.607,770.560 1309.552,773.832 1307.466,777.086 1305.349,780.319 1303.203,783.533 1301.027,786.727 1298.821,789.900 1296.585,793.053 1294.320,796.184" stroke="hsl(298 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1216.243,217.333 1218.878,219.367 1221.497,221.423 1224.100,223.499 1226.687,225.595 1229.257,227.712 1231.810,229.848 1234.346,232.005 1236.865,234.182 1239.367,236.379 1241.851,238.596 1244.318,240.832 1246.767,243.087 1249.198,245.362 1251.611,247.656 1254.006,249.969 1256.382,252.300 1258.740,254.651 1261.080,257.020 1263.400,259.407 1265.702,261.813 1267.984,264.237 1270.248,266.678 1272.492,269.138 1274.716,271.615 1276.921,274.109 1279.106,276.621 1281.272,279.151 1283.417,281.697 1285.542,284.260 1287.646,286.839 1289.731,289.436 1291.794,292.048 1293.837,294.677 1295.859,297.322 1297.860,299.983 1299.841,302.660 1301.799,305.352 1303.737,308.059 1305.653,310.782 1307.548,313.520 1309.421,316.272 1311.272,319.040" stroke="hsl(193 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1199.794,196.276 1202.512,198.123 1205.215,199.991 1207.904,201.879 1210.579,203.789 1213.239,205.718 1215.884,207.668 1218.513,209.638 1221.128,211.628 1223.727,213.638 1226.311,215.668 1228.880,217.718 1231.432,219.787 1233.969,221.876 1236.489,223.984 1238.994,226.111 1241.482,228.258 1243.953,230.423 1246.408,232.607 1248.846,234.811 1251.267,237.032 1253.671,239.272 1256.057,241.531 1258.427,243.808 1260.779,246.103 1263.113,248.415 1265.429,250.746 1267.728,253.094 1270.008,255.460 1272.270,257.843 1274.514,260.244 1276.740,262.661 1278.947,265.096 1281.135,267.547 1283.304,270.015 1285.454,272.500 1287.586,275.001 1289.698,277.518 1291.790,280.051 1293.864,282.601 1295.917,285.166 1297.951,287.746 1299.965,290.343" stroke="hsl(210 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1138.876,153.835 1141.825,155.173 1144.765,156.533 1147.694,157.915 1150.613,159.318 1153.522,160.743 1156.419,162.190 1159.306,163.658 1162.182,165.148 1165.047,166.659 1167.901,168.191 1170.743,169.744 1173.574,171.318 1176.392,172.914 1179.199,174.530 1181.994,176.166 1184.777,177.824 1187.547,179.502 1190.305,181.201 1193.050,182.920 1195.782,184.659 1198.501,186.419 1201.207,188.199 1203.900,189.999 1206.579,191.818 1209.245,193.658 1211.897,195.517 1214.535,197.396 1217.160,199.294 1219.770,201.212 1222.365,203.149 1224.947,205.106 1227.513,207.081 1230.065,209.076 1232.602,211.089 1235.124,213.121 1237.631,215.172 1240.123,217.241 1242.599,219.329 1245.060,221.435 1247.505,223.559 1249.934,225.702 1252.347,227.862" stroke="hsl(227 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="516.618,528.577 516.804,525.288 517.015,522.000 517.249,518.714 517.509,515.430 517.792,512.147 518.100,508.867 518.432,505.589 518.789,502.314 519.170,499.042 519.575,495.772 520.004,492.506 520.458,489.243 520.935,485.983 521.437,482.727 521.963,479.475 522.514,476.226 523.088,472.982 523.686,469.742 524.309,466.507 524.955,463.277 525.625,460.051 526.320,456.831 527.038,453.615 527.780,450.405 528.545,447.201 529.335,444.002 530.148,440.810 530.985,437.623 531.845,434.443 532.729,431.269 533.637,428.102 534.568,424.942 535.523,421.789 536.500,418.643 537.502,415.504 538.526,412.373 539.574,409.249 540.644,406.134 541.738,403.026 542.855,399.927 543.995,396.836 545.158,393.753" stroke="hsl(244 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="509.391,523.650 509.614,520.307 509.861,516.966 510.134,513.627 510.431,510.290 510.753,506.956 511.099,503.623 511.470,500.294 511.867,496.967 512.287,493.643 512.733,490.323 513.202,487.006 513.697,483.692 514.216,480.383 514.760,477.077 515.328,473.775 515.921,470.478 516.538,467.185 517.179,463.897 517.845,460.614 518.536,457.335 519.250,454.062 519.989,450.794 520.752,447.532 521.539,444.276 522.351,441.025 523.186,437.781 524.046,434.543 524.929,431.311 525.837,428.087 526.768,424.868 527.723,421.657 528.702,418.453 529.705,415.257 530.732,412.068 531.782,408.886 532.856,405.713 533.953,402.548 535.073,399.390 536.218,396.242 537.385,393.101 538.576,389.970 539.790,386.847" stroke="hsl(261 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1418.477,573.910 1418.314,577.092 1418.128,580.273 1417.921,583.452 1417.692,586.630 1417.440,589.806 1417.167,592.980 1416.871,596.153 1416.554,599.323 1416.214,602.491 1415.853,605.656 1415.470,608.819 1415.064,611.980 1414.637,615.137 1414.188,618.291 1413.717,621.442 1413.224,624.590 1412.709,627.734 1412.172,630.875 1411.614,634.012 1411.034,637.145 1410.432,640.273 1409.808,643.398 1409.163,646.518 1408.496,649.633 1407.808,652.744 1407.097,655.850 1406.366,658.951 1405.613,662.047 1404.838,665.137 1404.042,668.222 1403.225,671.302 1402.386,674.376 1401.526,677.443 1400.644,680.505 1399.742,683.561 1398.818,686.610 1397.873,689.653 1396.907,692.689 1395.920,695.718 1394.912,698.741 1393.883,701.756 1392.833,704.764" stroke="hsl(278 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-<polyline points="1238.820,858.032 1236.689,859.960 1234.544,861.873 1232.387,863.772 1230.216,865.655 1228.032,867.523 1225.836,869.376 1223.626,871.213 1221.404,873.036 1219.169,874.842 1216.922,876.634 1214.663,878.409 1212.391,880.169 1210.107,881.913 1207.811,883.641 1205.503,885.353 1203.183,887.049 1200.851,888.729 1198.508,890.393 1196.153,892.041 1193.787,893.672 1191.410,895.286 1189.022,896.884 1186.622,898.466 1184.212,900.030 1181.790,901.578 1179.359,903.109 1176.916,904.624 1174.463,906.121 1172.000,907.601 1169.526,909.064 1167.043,910.510 1164.549,911.938 1162.046,913.349 1159.533,914.743 1157.010,916.120 1154.478,917.478 1151.936,918.819 1149.385,920.143 1146.825,921.449 1144.256,922.737 1141.679,924.007 1139.092,925.259" stroke="hsl(295 92% 72%)" stroke-width="1.100" opacity="0.160"/>
-</g>
-<g clip-path="url(#rose-clip)">
-<path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill="url(#rose-wash)" opacity="0.28"/>
-<g filter="url(#field-soften)"><circle cx="925.660" cy="139.920" r="35.000" fill="hsl(271 94% 61%)" opacity="0.268"/>
-<circle cx="960.000" cy="139.920" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
-<circle cx="994.340" cy="139.920" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="856.980" cy="174.260" r="35.000" fill="hsl(258 94% 61%)" opacity="0.251"/>
-<circle cx="891.320" cy="174.260" r="35.000" fill="hsl(266 94% 61%)" opacity="0.262"/>
-<circle cx="925.660" cy="174.260" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>
-<circle cx="960.000" cy="174.260" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="994.340" cy="174.260" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="1028.680" cy="174.260" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="1063.020" cy="174.260" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
-<circle cx="788.300" cy="208.600" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="822.640" cy="208.600" r="35.000" fill="hsl(252 94% 61%)" opacity="0.243"/>
-<circle cx="856.980" cy="208.600" r="35.000" fill="hsl(257 94% 61%)" opacity="0.250"/>
-<circle cx="891.320" cy="208.600" r="35.000" fill="hsl(265 94% 61%)" opacity="0.260"/>
-<circle cx="925.660" cy="208.600" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
-<circle cx="960.000" cy="208.600" r="35.000" fill="hsl(249 94% 61%)" opacity="0.241"/>
-<circle cx="994.340" cy="208.600" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="1028.680" cy="208.600" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>
-<circle cx="1063.020" cy="208.600" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
-<circle cx="1097.360" cy="208.600" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="1131.700" cy="208.600" r="35.000" fill="hsl(243 94% 61%)" opacity="0.233"/>
-<circle cx="719.620" cy="242.940" r="35.000" fill="hsl(238 94% 61%)" opacity="0.225"/>
-<circle cx="753.960" cy="242.940" r="35.000" fill="hsl(238 94% 61%)" opacity="0.227"/>
-<circle cx="788.300" cy="242.940" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
-<circle cx="822.640" cy="242.940" r="35.000" fill="hsl(245 94% 61%)" opacity="0.235"/>
-<circle cx="856.980" cy="242.940" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
-<circle cx="891.320" cy="242.940" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>
-<circle cx="925.660" cy="242.940" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>
-<circle cx="960.000" cy="242.940" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="994.340" cy="242.940" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
-<circle cx="1028.680" cy="242.940" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>
-<circle cx="1063.020" cy="242.940" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
-<circle cx="1097.360" cy="242.940" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
-<circle cx="1131.700" cy="242.940" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>
-<circle cx="1166.040" cy="242.940" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>
-<circle cx="1200.380" cy="242.940" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
-<circle cx="650.940" cy="277.280" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
-<circle cx="685.280" cy="277.280" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>
-<circle cx="719.620" cy="277.280" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
-<circle cx="753.960" cy="277.280" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
-<circle cx="788.300" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
-<circle cx="822.640" cy="277.280" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="856.980" cy="277.280" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
-<circle cx="891.320" cy="277.280" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
-<circle cx="925.660" cy="277.280" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
-<circle cx="960.000" cy="277.280" r="35.000" fill="hsl(249 94% 61%)" opacity="0.240"/>
-<circle cx="994.340" cy="277.280" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
-<circle cx="1028.680" cy="277.280" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="1063.020" cy="277.280" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
-<circle cx="1097.360" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
-<circle cx="1131.700" cy="277.280" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="1166.040" cy="277.280" r="35.000" fill="hsl(226 94% 61%)" opacity="0.211"/>
-<circle cx="1200.380" cy="277.280" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="1234.720" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
-<circle cx="1269.060" cy="277.280" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="616.600" cy="311.620" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
-<circle cx="650.940" cy="311.620" r="35.000" fill="hsl(229 94% 61%)" opacity="0.214"/>
-<circle cx="685.280" cy="311.620" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
-<circle cx="719.620" cy="311.620" r="35.000" fill="hsl(213 94% 61%)" opacity="0.195"/>
-<circle cx="753.960" cy="311.620" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
-<circle cx="788.300" cy="311.620" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
-<circle cx="822.640" cy="311.620" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="856.980" cy="311.620" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
-<circle cx="891.320" cy="311.620" r="34.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="925.660" cy="311.620" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="960.000" cy="311.620" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="994.340" cy="311.620" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
-<circle cx="1028.680" cy="311.620" r="35.000" fill="hsl(249 94% 61%)" opacity="0.239"/>
-<circle cx="1063.020" cy="311.620" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
-<circle cx="1097.360" cy="311.620" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>
-<circle cx="1131.700" cy="311.620" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>
-<circle cx="1166.040" cy="311.620" r="35.000" fill="hsl(224 94% 61%)" opacity="0.208"/>
-<circle cx="1200.380" cy="311.620" r="35.000" fill="hsl(225 94% 61%)" opacity="0.209"/>
-<circle cx="1234.720" cy="311.620" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
-<circle cx="1269.060" cy="311.620" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
-<circle cx="1303.400" cy="311.620" r="35.000" fill="hsl(263 94% 61%)" opacity="0.258"/>
-<circle cx="616.600" cy="345.960" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
-<circle cx="650.940" cy="345.960" r="35.000" fill="hsl(225 94% 61%)" opacity="0.209"/>
-<circle cx="685.280" cy="345.960" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
-<circle cx="719.620" cy="345.960" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
-<circle cx="753.960" cy="345.960" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>
-<circle cx="788.300" cy="345.960" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>
-<circle cx="822.640" cy="345.960" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
-<circle cx="856.980" cy="345.960" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
-<circle cx="891.320" cy="345.960" r="34.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
-<circle cx="925.660" cy="345.960" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="960.000" cy="345.960" r="35.000" fill="hsl(256 94% 61%)" opacity="0.250"/>
-<circle cx="994.340" cy="345.960" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
-<circle cx="1028.680" cy="345.960" r="34.000" fill="hsl(261 94% 61%)" opacity="0.255"/>
-<circle cx="1063.020" cy="345.960" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
-<circle cx="1097.360" cy="345.960" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
-<circle cx="1131.700" cy="345.960" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="1166.040" cy="345.960" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>
-<circle cx="1200.380" cy="345.960" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
-<circle cx="1234.720" cy="345.960" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
-<circle cx="1269.060" cy="345.960" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
-<circle cx="1303.400" cy="345.960" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>
-<circle cx="616.600" cy="380.300" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
-<circle cx="650.940" cy="380.300" r="35.000" fill="hsl(223 94% 61%)" opacity="0.206"/>
-<circle cx="685.280" cy="380.300" r="35.000" fill="hsl(212 94% 61%)" opacity="0.193"/>
-<circle cx="719.620" cy="380.300" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>
-<circle cx="753.960" cy="380.300" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>
-<circle cx="788.300" cy="380.300" r="35.000" fill="hsl(210 94% 61%)" opacity="0.190"/>
-<circle cx="822.640" cy="380.300" r="35.000" fill="hsl(215 94% 61%)" opacity="0.196"/>
-<circle cx="856.980" cy="380.300" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="891.320" cy="380.300" r="35.000" fill="hsl(242 94% 61%)" opacity="0.232"/>
-<circle cx="925.660" cy="380.300" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
-<circle cx="960.000" cy="380.300" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>
-<circle cx="994.340" cy="380.300" r="35.000" fill="hsl(270 94% 61%)" opacity="0.267"/>
-<circle cx="1028.680" cy="380.300" r="35.000" fill="hsl(274 94% 61%)" opacity="0.271"/>
-<circle cx="1063.020" cy="380.300" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
-<circle cx="1097.360" cy="380.300" r="34.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
-<circle cx="1131.700" cy="380.300" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="1166.040" cy="380.300" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
-<circle cx="1200.380" cy="380.300" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
-<circle cx="1234.720" cy="380.300" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
-<circle cx="1269.060" cy="380.300" r="34.000" fill="hsl(245 94% 61%)" opacity="0.235"/>
-<circle cx="1303.400" cy="380.300" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="616.600" cy="414.640" r="35.000" fill="hsl(241 94% 61%)" opacity="0.229"/>
-<circle cx="650.940" cy="414.640" r="35.000" fill="hsl(228 94% 61%)" opacity="0.214"/>
-<circle cx="685.280" cy="414.640" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
-<circle cx="719.620" cy="414.640" r="35.000" fill="hsl(206 94% 61%)" opacity="0.186"/>
-<circle cx="753.960" cy="414.640" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
-<circle cx="788.300" cy="414.640" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
-<circle cx="822.640" cy="414.640" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
-<circle cx="856.980" cy="414.640" r="35.000" fill="hsl(227 94% 61%)" opacity="0.213"/>
-<circle cx="891.320" cy="414.640" r="34.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
-<circle cx="925.660" cy="414.640" r="35.000" fill="hsl(258 94% 61%)" opacity="0.251"/>
-<circle cx="960.000" cy="414.640" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
-<circle cx="994.340" cy="414.640" r="35.000" fill="hsl(280 94% 61%)" opacity="0.280"/>
-<circle cx="1028.680" cy="414.640" r="34.000" fill="hsl(285 94% 61%)" opacity="0.286"/>
-<circle cx="1063.020" cy="414.640" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>
-<circle cx="1097.360" cy="414.640" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
-<circle cx="1131.700" cy="414.640" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>
-<circle cx="1166.040" cy="414.640" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="1200.380" cy="414.640" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="1234.720" cy="414.640" r="34.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
-<circle cx="1269.060" cy="414.640" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
-<circle cx="1303.400" cy="414.640" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
-<circle cx="582.260" cy="448.980" r="35.000" fill="hsl(248 94% 61%)" opacity="0.238"/>
-<circle cx="616.600" cy="448.980" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
-<circle cx="650.940" cy="448.980" r="35.000" fill="hsl(235 94% 61%)" opacity="0.222"/>
-<circle cx="685.280" cy="448.980" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
-<circle cx="719.620" cy="448.980" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
-<circle cx="753.960" cy="448.980" r="34.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
-<circle cx="788.300" cy="448.980" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
-<circle cx="822.640" cy="448.980" r="35.000" fill="hsl(221 94% 61%)" opacity="0.204"/>
-<circle cx="856.980" cy="448.980" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
-<circle cx="891.320" cy="448.980" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
-<circle cx="925.660" cy="448.980" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>
-<circle cx="960.000" cy="448.980" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
-<circle cx="994.340" cy="448.980" r="35.000" fill="hsl(289 94% 61%)" opacity="0.291"/>
-<circle cx="1028.680" cy="448.980" r="35.000" fill="hsl(294 94% 61%)" opacity="0.297"/>
-<circle cx="1063.020" cy="448.980" r="35.000" fill="hsl(289 94% 61%)" opacity="0.291"/>
-<circle cx="1097.360" cy="448.980" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>
-<circle cx="1131.700" cy="448.980" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
-<circle cx="1166.040" cy="448.980" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
-<circle cx="1200.380" cy="448.980" r="34.000" fill="hsl(250 94% 61%)" opacity="0.242"/>
-<circle cx="1234.720" cy="448.980" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
-<circle cx="1269.060" cy="448.980" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="1303.400" cy="448.980" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
-<circle cx="1337.740" cy="448.980" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>
-<circle cx="582.260" cy="483.320" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="616.600" cy="483.320" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="650.940" cy="483.320" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
-<circle cx="685.280" cy="483.320" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="719.620" cy="483.320" r="35.000" fill="hsl(224 94% 61%)" opacity="0.209"/>
-<circle cx="753.960" cy="483.320" r="35.000" fill="hsl(216 94% 61%)" opacity="0.199"/>
-<circle cx="788.300" cy="483.320" r="35.000" fill="hsl(214 94% 61%)" opacity="0.196"/>
-<circle cx="822.640" cy="483.320" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
-<circle cx="856.980" cy="483.320" r="35.000" fill="hsl(234 94% 61%)" opacity="0.220"/>
-<circle cx="891.320" cy="483.320" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
-<circle cx="925.660" cy="483.320" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>
-<circle cx="960.000" cy="483.320" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
-<circle cx="994.340" cy="483.320" r="35.000" fill="hsl(292 94% 61%)" opacity="0.294"/>
-<circle cx="1028.680" cy="483.320" r="35.000" fill="hsl(297 94% 61%)" opacity="0.301"/>
-<circle cx="1063.020" cy="483.320" r="35.000" fill="hsl(296 94% 61%)" opacity="0.299"/>
-<circle cx="1097.360" cy="483.320" r="35.000" fill="hsl(291 94% 61%)" opacity="0.293"/>
-<circle cx="1131.700" cy="483.320" r="34.000" fill="hsl(283 94% 61%)" opacity="0.283"/>
-<circle cx="1166.040" cy="483.320" r="35.000" fill="hsl(267 94% 61%)" opacity="0.262"/>
-<circle cx="1200.380" cy="483.320" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
-<circle cx="1234.720" cy="483.320" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="1269.060" cy="483.320" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>
-<circle cx="1303.400" cy="483.320" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
-<circle cx="1337.740" cy="483.320" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="582.260" cy="517.660" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
-<circle cx="616.600" cy="517.660" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
-<circle cx="650.940" cy="517.660" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="685.280" cy="517.660" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="719.620" cy="517.660" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
-<circle cx="753.960" cy="517.660" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
-<circle cx="788.300" cy="517.660" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
-<circle cx="822.640" cy="517.660" r="34.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="856.980" cy="517.660" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="891.320" cy="517.660" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
-<circle cx="925.660" cy="517.660" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>
-<circle cx="960.000" cy="517.660" r="35.000" fill="hsl(276 94% 61%)" opacity="0.274"/>
-<circle cx="994.340" cy="517.660" r="35.000" fill="hsl(290 94% 61%)" opacity="0.292"/>
-<circle cx="1028.680" cy="517.660" r="33.000" fill="hsl(299 94% 61%)" opacity="0.303"/>
-<circle cx="1063.020" cy="517.660" r="34.000" fill="hsl(301 94% 61%)" opacity="0.306"/>
-<circle cx="1097.360" cy="517.660" r="34.000" fill="hsl(296 94% 61%)" opacity="0.300"/>
-<circle cx="1131.700" cy="517.660" r="35.000" fill="hsl(284 94% 61%)" opacity="0.285"/>
-<circle cx="1166.040" cy="517.660" r="34.000" fill="hsl(268 94% 61%)" opacity="0.265"/>
-<circle cx="1200.380" cy="517.660" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
-<circle cx="1234.720" cy="517.660" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>
-<circle cx="1269.060" cy="517.660" r="35.000" fill="hsl(224 94% 61%)" opacity="0.209"/>
-<circle cx="1303.400" cy="517.660" r="35.000" fill="hsl(219 94% 61%)" opacity="0.201"/>
-<circle cx="1337.740" cy="517.660" r="35.000" fill="hsl(219 94% 61%)" opacity="0.201"/>
-<circle cx="582.260" cy="552.000" r="35.000" fill="hsl(259 94% 61%)" opacity="0.252"/>
-<circle cx="616.600" cy="552.000" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
-<circle cx="650.940" cy="552.000" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
-<circle cx="685.280" cy="552.000" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>
-<circle cx="719.620" cy="552.000" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
-<circle cx="753.960" cy="552.000" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
-<circle cx="788.300" cy="552.000" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="822.640" cy="552.000" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="856.980" cy="552.000" r="35.000" fill="hsl(238 94% 61%)" opacity="0.225"/>
-<circle cx="891.320" cy="552.000" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
-<circle cx="925.660" cy="552.000" r="35.000" fill="hsl(258 94% 61%)" opacity="0.252"/>
-<circle cx="960.000" cy="552.000" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
-<circle cx="994.340" cy="552.000" r="35.000" fill="hsl(283 94% 61%)" opacity="0.283"/>
-<circle cx="1028.680" cy="552.000" r="35.000" fill="hsl(293 94% 61%)" opacity="0.296"/>
-<circle cx="1063.020" cy="552.000" r="35.000" fill="hsl(295 94% 61%)" opacity="0.298"/>
-<circle cx="1097.360" cy="552.000" r="35.000" fill="hsl(290 94% 61%)" opacity="0.293"/>
-<circle cx="1131.700" cy="552.000" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>
-<circle cx="1166.040" cy="552.000" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
-<circle cx="1200.380" cy="552.000" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="1234.720" cy="552.000" r="35.000" fill="hsl(235 94% 61%)" opacity="0.222"/>
-<circle cx="1269.060" cy="552.000" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
-<circle cx="1303.400" cy="552.000" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
-<circle cx="1337.740" cy="552.000" r="35.000" fill="hsl(209 94% 61%)" opacity="0.190"/>
-<circle cx="547.920" cy="586.340" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
-<circle cx="582.260" cy="586.340" r="34.000" fill="hsl(268 94% 61%)" opacity="0.264"/>
-<circle cx="616.600" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
-<circle cx="650.940" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
-<circle cx="685.280" cy="586.340" r="35.000" fill="hsl(271 94% 61%)" opacity="0.267"/>
-<circle cx="719.620" cy="586.340" r="35.000" fill="hsl(263 94% 61%)" opacity="0.258"/>
-<circle cx="753.960" cy="586.340" r="35.000" fill="hsl(254 94% 61%)" opacity="0.247"/>
-<circle cx="788.300" cy="586.340" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
-<circle cx="822.640" cy="586.340" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
-<circle cx="856.980" cy="586.340" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
-<circle cx="891.320" cy="586.340" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
-<circle cx="925.660" cy="586.340" r="34.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="960.000" cy="586.340" r="35.000" fill="hsl(262 94% 61%)" opacity="0.256"/>
-<circle cx="994.340" cy="586.340" r="35.000" fill="hsl(273 94% 61%)" opacity="0.270"/>
-<circle cx="1028.680" cy="586.340" r="35.000" fill="hsl(283 94% 61%)" opacity="0.283"/>
-<circle cx="1063.020" cy="586.340" r="35.000" fill="hsl(284 94% 61%)" opacity="0.285"/>
-<circle cx="1097.360" cy="586.340" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>
-<circle cx="1131.700" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
-<circle cx="1166.040" cy="586.340" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
-<circle cx="1200.380" cy="586.340" r="35.000" fill="hsl(250 94% 61%)" opacity="0.242"/>
-<circle cx="1234.720" cy="586.340" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>
-<circle cx="1269.060" cy="586.340" r="35.000" fill="hsl(220 94% 61%)" opacity="0.204"/>
-<circle cx="1303.400" cy="586.340" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
-<circle cx="1337.740" cy="586.340" r="35.000" fill="hsl(200 94% 61%)" opacity="0.177"/>
-<circle cx="1372.080" cy="586.340" r="35.000" fill="hsl(207 94% 61%)" opacity="0.186"/>
-<circle cx="547.920" cy="620.680" r="35.000" fill="hsl(259 94% 61%)" opacity="0.252"/>
-<circle cx="582.260" cy="620.680" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
-<circle cx="616.600" cy="620.680" r="35.000" fill="hsl(280 94% 61%)" opacity="0.280"/>
-<circle cx="650.940" cy="620.680" r="35.000" fill="hsl(284 94% 61%)" opacity="0.284"/>
-<circle cx="685.280" cy="620.680" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>
-<circle cx="719.620" cy="620.680" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
-<circle cx="753.960" cy="620.680" r="35.000" fill="hsl(266 94% 61%)" opacity="0.262"/>
-<circle cx="788.300" cy="620.680" r="35.000" fill="hsl(257 94% 61%)" opacity="0.250"/>
-<circle cx="822.640" cy="620.680" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="856.980" cy="620.680" r="33.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
-<circle cx="891.320" cy="620.680" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
-<circle cx="925.660" cy="620.680" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
-<circle cx="960.000" cy="620.680" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
-<circle cx="994.340" cy="620.680" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
-<circle cx="1028.680" cy="620.680" r="34.000" fill="hsl(268 94% 61%)" opacity="0.265"/>
-<circle cx="1063.020" cy="620.680" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
-<circle cx="1097.360" cy="620.680" r="35.000" fill="hsl(274 94% 61%)" opacity="0.272"/>
-<circle cx="1131.700" cy="620.680" r="35.000" fill="hsl(270 94% 61%)" opacity="0.266"/>
-<circle cx="1166.040" cy="620.680" r="35.000" fill="hsl(261 94% 61%)" opacity="0.255"/>
-<circle cx="1200.380" cy="620.680" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="1234.720" cy="620.680" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="1269.060" cy="620.680" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
-<circle cx="1303.400" cy="620.680" r="35.000" fill="hsl(212 94% 61%)" opacity="0.192"/>
-<circle cx="1337.740" cy="620.680" r="35.000" fill="hsl(203 94% 61%)" opacity="0.181"/>
-<circle cx="1372.080" cy="620.680" r="35.000" fill="hsl(198 94% 61%)" opacity="0.175"/>
-<circle cx="547.920" cy="655.020" r="35.000" fill="hsl(261 94% 61%)" opacity="0.255"/>
-<circle cx="582.260" cy="655.020" r="35.000" fill="hsl(275 94% 61%)" opacity="0.274"/>
-<circle cx="616.600" cy="655.020" r="35.000" fill="hsl(286 94% 61%)" opacity="0.287"/>
-<circle cx="650.940" cy="655.020" r="35.000" fill="hsl(293 94% 61%)" opacity="0.295"/>
-<circle cx="685.280" cy="655.020" r="35.000" fill="hsl(293 94% 61%)" opacity="0.296"/>
-<circle cx="719.620" cy="655.020" r="35.000" fill="hsl(288 94% 61%)" opacity="0.290"/>
-<circle cx="753.960" cy="655.020" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
-<circle cx="788.300" cy="655.020" r="35.000" fill="hsl(266 94% 61%)" opacity="0.261"/>
-<circle cx="822.640" cy="655.020" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="856.980" cy="655.020" r="34.000" fill="hsl(242 94% 61%)" opacity="0.232"/>
-<circle cx="891.320" cy="655.020" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
-<circle cx="925.660" cy="655.020" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="960.000" cy="655.020" r="34.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
-<circle cx="994.340" cy="655.020" r="35.000" fill="hsl(245 94% 61%)" opacity="0.234"/>
-<circle cx="1028.680" cy="655.020" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
-<circle cx="1063.020" cy="655.020" r="34.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
-<circle cx="1097.360" cy="655.020" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
-<circle cx="1131.700" cy="655.020" r="34.000" fill="hsl(263 94% 61%)" opacity="0.258"/>
-<circle cx="1166.040" cy="655.020" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
-<circle cx="1200.380" cy="655.020" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="1234.720" cy="655.020" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="1269.060" cy="655.020" r="35.000" fill="hsl(226 94% 61%)" opacity="0.211"/>
-<circle cx="1303.400" cy="655.020" r="35.000" fill="hsl(215 94% 61%)" opacity="0.197"/>
-<circle cx="1337.740" cy="655.020" r="35.000" fill="hsl(206 94% 61%)" opacity="0.186"/>
-<circle cx="1372.080" cy="655.020" r="35.000" fill="hsl(201 94% 61%)" opacity="0.179"/>
-<circle cx="582.260" cy="689.360" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
-<circle cx="616.600" cy="689.360" r="35.000" fill="hsl(287 94% 61%)" opacity="0.288"/>
-<circle cx="650.940" cy="689.360" r="35.000" fill="hsl(294 94% 61%)" opacity="0.297"/>
-<circle cx="685.280" cy="689.360" r="35.000" fill="hsl(295 94% 61%)" opacity="0.298"/>
-<circle cx="719.620" cy="689.360" r="35.000" fill="hsl(290 94% 61%)" opacity="0.292"/>
-<circle cx="753.960" cy="689.360" r="35.000" fill="hsl(280 94% 61%)" opacity="0.279"/>
-<circle cx="788.300" cy="689.360" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>
-<circle cx="822.640" cy="689.360" r="34.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
-<circle cx="856.980" cy="689.360" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
-<circle cx="891.320" cy="689.360" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
-<circle cx="925.660" cy="689.360" r="35.000" fill="hsl(225 94% 61%)" opacity="0.210"/>
-<circle cx="960.000" cy="689.360" r="34.000" fill="hsl(224 94% 61%)" opacity="0.208"/>
-<circle cx="994.340" cy="689.360" r="35.000" fill="hsl(230 94% 61%)" opacity="0.215"/>
-<circle cx="1028.680" cy="689.360" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="1063.020" cy="689.360" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="1097.360" cy="689.360" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="1131.700" cy="689.360" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
-<circle cx="1166.040" cy="689.360" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
-<circle cx="1200.380" cy="689.360" r="35.000" fill="hsl(245 94% 61%)" opacity="0.235"/>
-<circle cx="1234.720" cy="689.360" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
-<circle cx="1269.060" cy="689.360" r="35.000" fill="hsl(228 94% 61%)" opacity="0.213"/>
-<circle cx="1303.400" cy="689.360" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
-<circle cx="1337.740" cy="689.360" r="35.000" fill="hsl(210 94% 61%)" opacity="0.191"/>
-<circle cx="616.600" cy="723.700" r="35.000" fill="hsl(271 94% 61%)" opacity="0.268"/>
-<circle cx="650.940" cy="723.700" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>
-<circle cx="685.280" cy="723.700" r="35.000" fill="hsl(286 94% 61%)" opacity="0.287"/>
-<circle cx="719.620" cy="723.700" r="35.000" fill="hsl(285 94% 61%)" opacity="0.285"/>
-<circle cx="753.960" cy="723.700" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
-<circle cx="788.300" cy="723.700" r="35.000" fill="hsl(268 94% 61%)" opacity="0.265"/>
-<circle cx="822.640" cy="723.700" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
-<circle cx="856.980" cy="723.700" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
-<circle cx="891.320" cy="723.700" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="925.660" cy="723.700" r="35.000" fill="hsl(216 94% 61%)" opacity="0.198"/>
-<circle cx="960.000" cy="723.700" r="35.000" fill="hsl(213 94% 61%)" opacity="0.195"/>
-<circle cx="994.340" cy="723.700" r="35.000" fill="hsl(216 94% 61%)" opacity="0.198"/>
-<circle cx="1028.680" cy="723.700" r="35.000" fill="hsl(225 94% 61%)" opacity="0.210"/>
-<circle cx="1063.020" cy="723.700" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="1097.360" cy="723.700" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
-<circle cx="1131.700" cy="723.700" r="35.000" fill="hsl(248 94% 61%)" opacity="0.238"/>
-<circle cx="1166.040" cy="723.700" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
-<circle cx="1200.380" cy="723.700" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
-<circle cx="1234.720" cy="723.700" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
-<circle cx="1269.060" cy="723.700" r="35.000" fill="hsl(229 94% 61%)" opacity="0.214"/>
-<circle cx="1303.400" cy="723.700" r="35.000" fill="hsl(221 94% 61%)" opacity="0.205"/>
-<circle cx="616.600" cy="758.040" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
-<circle cx="650.940" cy="758.040" r="35.000" fill="hsl(269 94% 61%)" opacity="0.266"/>
-<circle cx="685.280" cy="758.040" r="35.000" fill="hsl(277 94% 61%)" opacity="0.275"/>
-<circle cx="719.620" cy="758.040" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
-<circle cx="753.960" cy="758.040" r="35.000" fill="hsl(276 94% 61%)" opacity="0.274"/>
-<circle cx="788.300" cy="758.040" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
-<circle cx="822.640" cy="758.040" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
-<circle cx="856.980" cy="758.040" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="891.320" cy="758.040" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>
-<circle cx="925.660" cy="758.040" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>
-<circle cx="960.000" cy="758.040" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
-<circle cx="994.340" cy="758.040" r="35.000" fill="hsl(205 94% 61%)" opacity="0.184"/>
-<circle cx="1028.680" cy="758.040" r="35.000" fill="hsl(214 94% 61%)" opacity="0.196"/>
-<circle cx="1063.020" cy="758.040" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
-<circle cx="1097.360" cy="758.040" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
-<circle cx="1131.700" cy="758.040" r="34.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="1166.040" cy="758.040" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
-<circle cx="1200.380" cy="758.040" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
-<circle cx="1234.720" cy="758.040" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="1269.060" cy="758.040" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>
-<circle cx="650.940" cy="792.380" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>
-<circle cx="685.280" cy="792.380" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>
-<circle cx="719.620" cy="792.380" r="35.000" fill="hsl(272 94% 61%)" opacity="0.270"/>
-<circle cx="753.960" cy="792.380" r="35.000" fill="hsl(273 94% 61%)" opacity="0.270"/>
-<circle cx="788.300" cy="792.380" r="34.000" fill="hsl(266 94% 61%)" opacity="0.262"/>
-<circle cx="822.640" cy="792.380" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
-<circle cx="856.980" cy="792.380" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
-<circle cx="891.320" cy="792.380" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
-<circle cx="925.660" cy="792.380" r="35.000" fill="hsl(211 94% 61%)" opacity="0.191"/>
-<circle cx="960.000" cy="792.380" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
-<circle cx="994.340" cy="792.380" r="35.000" fill="hsl(203 94% 61%)" opacity="0.182"/>
-<circle cx="1028.680" cy="792.380" r="35.000" fill="hsl(207 94% 61%)" opacity="0.186"/>
-<circle cx="1063.020" cy="792.380" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
-<circle cx="1097.360" cy="792.380" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
-<circle cx="1131.700" cy="792.380" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="1166.040" cy="792.380" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="1200.380" cy="792.380" r="35.000" fill="hsl(247 94% 61%)" opacity="0.238"/>
-<circle cx="1234.720" cy="792.380" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
-<circle cx="1269.060" cy="792.380" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>
-<circle cx="685.280" cy="826.720" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>
-<circle cx="719.620" cy="826.720" r="35.000" fill="hsl(265 94% 61%)" opacity="0.261"/>
-<circle cx="753.960" cy="826.720" r="35.000" fill="hsl(268 94% 61%)" opacity="0.264"/>
-<circle cx="788.300" cy="826.720" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
-<circle cx="822.640" cy="826.720" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
-<circle cx="856.980" cy="826.720" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
-<circle cx="891.320" cy="826.720" r="34.000" fill="hsl(221 94% 61%)" opacity="0.205"/>
-<circle cx="925.660" cy="826.720" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
-<circle cx="960.000" cy="826.720" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
-<circle cx="994.340" cy="826.720" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>
-<circle cx="1028.680" cy="826.720" r="34.000" fill="hsl(203 94% 61%)" opacity="0.181"/>
-<circle cx="1063.020" cy="826.720" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
-<circle cx="1097.360" cy="826.720" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
-<circle cx="1131.700" cy="826.720" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
-<circle cx="1166.040" cy="826.720" r="35.000" fill="hsl(255 94% 61%)" opacity="0.248"/>
-<circle cx="1200.380" cy="826.720" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
-<circle cx="1234.720" cy="826.720" r="35.000" fill="hsl(242 94% 61%)" opacity="0.232"/>
-<circle cx="719.620" cy="861.060" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>
-<circle cx="753.960" cy="861.060" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>
-<circle cx="788.300" cy="861.060" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>
-<circle cx="822.640" cy="861.060" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
-<circle cx="856.980" cy="861.060" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
-<circle cx="891.320" cy="861.060" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
-<circle cx="925.660" cy="861.060" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="960.000" cy="861.060" r="35.000" fill="hsl(223 94% 61%)" opacity="0.206"/>
-<circle cx="994.340" cy="861.060" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
-<circle cx="1028.680" cy="861.060" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
-<circle cx="1063.020" cy="861.060" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
-<circle cx="1097.360" cy="861.060" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>
-<circle cx="1131.700" cy="861.060" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="1166.040" cy="861.060" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
-<circle cx="1200.380" cy="861.060" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
-<circle cx="753.960" cy="895.400" r="35.000" fill="hsl(256 94% 61%)" opacity="0.248"/>
-<circle cx="788.300" cy="895.400" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>
-<circle cx="822.640" cy="895.400" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
-<circle cx="856.980" cy="895.400" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="891.320" cy="895.400" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
-<circle cx="925.660" cy="895.400" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
-<circle cx="960.000" cy="895.400" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="994.340" cy="895.400" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
-<circle cx="1028.680" cy="895.400" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
-<circle cx="1063.020" cy="895.400" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>
-<circle cx="1097.360" cy="895.400" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
-<circle cx="1131.700" cy="895.400" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="1166.040" cy="895.400" r="35.000" fill="hsl(266 94% 61%)" opacity="0.261"/>
-<circle cx="753.960" cy="929.740" r="34.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
-<circle cx="788.300" cy="929.740" r="35.000" fill="hsl(256 94% 61%)" opacity="0.248"/>
-<circle cx="822.640" cy="929.740" r="34.000" fill="hsl(257 94% 61%)" opacity="0.250"/>
-<circle cx="856.980" cy="929.740" r="35.000" fill="hsl(253 94% 61%)" opacity="0.246"/>
-<circle cx="891.320" cy="929.740" r="35.000" fill="hsl(250 94% 61%)" opacity="0.242"/>
-<circle cx="925.660" cy="929.740" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
-<circle cx="960.000" cy="929.740" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
-<circle cx="994.340" cy="929.740" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
-<circle cx="1028.680" cy="929.740" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
-<circle cx="1063.020" cy="929.740" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
-<circle cx="1097.360" cy="929.740" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>
-<circle cx="1131.700" cy="929.740" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
-<circle cx="1166.040" cy="929.740" r="34.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
-</g>
-<g opacity="0.58"><line x1="983.332" y1="534.766" x2="973.544" y2="526.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="988.499" y1="546.596" x2="983.332" y2="534.766" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="983.332" y1="534.766" x2="1006.907" y2="517.352" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="981.994" y1="570.912" x2="988.022" y2="559.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="988.022" y1="559.496" x2="988.499" y2="546.596" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1016.824" y1="567.202" x2="988.022" y2="559.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="988.499" y1="546.596" x2="1027.405" y2="539.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="991.808" y1="491.757" x2="973.544" y2="526.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="973.544" y1="526.349" x2="961.073" y2="523.013" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="962.146" y1="494.029" x2="961.073" y2="523.013" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="961.073" y1="523.013" x2="948.390" y2="525.418" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="971.610" y1="578.582" x2="981.994" y2="570.912" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="981.994" y1="570.912" x2="1012.465" y2="597.114" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="932.876" y1="489.898" x2="948.390" y2="525.418" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="948.390" y1="525.418" x2="938.006" y2="533.088" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.927" y1="580.987" x2="971.610" y2="578.582" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="971.610" y1="578.582" x2="983.824" y2="606.545" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="913.124" y1="511.692" x2="938.006" y2="533.088" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="938.006" y1="533.088" x2="931.978" y2="544.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="946.456" y1="577.651" x2="958.927" y2="580.987" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.927" y1="580.987" x2="957.415" y2="621.827" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="890.959" y1="533.530" x2="931.978" y2="544.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="931.978" y1="544.504" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="936.668" y1="569.234" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="899.643" y1="563.445" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="936.668" y1="569.234" x2="946.456" y2="577.651" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="903.183" y1="593.968" x2="936.668" y2="569.234" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="946.456" y1="577.651" x2="931.800" y2="605.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="972.412" y1="484.686" x2="962.146" y2="494.029" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="962.146" y1="494.029" x2="947.830" y2="484.058" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1018.788" y1="518.699" x2="1006.907" y2="517.352" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1006.907" y1="517.352" x2="1004.363" y2="498.501" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1016.824" y1="567.202" x2="1029.925" y2="551.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1029.925" y1="551.208" x2="1027.405" y2="539.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1022.389" y1="575.335" x2="1016.824" y2="567.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="983.824" y1="606.545" x2="1006.276" y2="604.953" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1006.276" y1="604.953" x2="1012.465" y2="597.114" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="981.859" y1="613.815" x2="983.824" y2="606.545" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="927.316" y1="607.504" x2="931.800" y2="605.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="931.800" y1="605.409" x2="950.032" y2="621.996" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="897.765" y1="562.566" x2="899.643" y2="563.445" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="899.643" y1="563.445" x2="900.347" y2="590.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="912.015" y1="511.701" x2="913.124" y2="511.692" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="912.015" y1="511.701" x2="891.092" y2="532.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="926.036" y1="486.913" x2="932.876" y2="489.898" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="926.036" y1="486.913" x2="913.124" y2="511.692" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="879.746" y1="488.235" x2="912.015" y2="511.701" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="888.385" y1="535.872" x2="897.765" y2="562.566" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="897.765" y1="562.566" x2="861.003" y2="574.923" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="903.269" y1="597.046" x2="927.316" y2="607.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="927.316" y1="607.504" x2="916.556" y2="643.053" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="959.674" y1="623.670" x2="981.859" y2="613.815" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="981.859" y1="613.815" x2="1003.828" y2="642.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1015.388" y1="596.606" x2="1022.389" y2="575.335" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1022.389" y1="575.335" x2="1057.456" y2="573.982" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1029.059" y1="536.487" x2="1018.788" y2="518.699" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1018.788" y1="518.699" x2="1037.830" y2="489.758" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="990.734" y1="488.378" x2="991.808" y2="491.757" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="990.734" y1="488.378" x2="972.412" y2="484.686" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="452.426" x2="972.412" y2="484.686" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="452.426" x2="947.830" y2="484.058" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="947.830" y1="484.058" x2="932.876" y2="489.898" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="924.801" y1="485.323" x2="926.036" y2="486.913" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1037.830" y1="489.758" x2="1004.363" y2="498.501" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1004.363" y1="498.501" x2="991.808" y2="491.757" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="998.095" y1="470.935" x2="990.734" y2="488.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1057.456" y1="573.982" x2="1029.925" y2="551.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1027.405" y1="539.218" x2="1029.059" y2="536.487" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1044.753" y1="530.377" x2="1029.059" y2="536.487" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1003.828" y1="642.233" x2="1006.276" y2="604.953" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1012.465" y1="597.114" x2="1015.388" y2="596.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1028.621" y1="602.722" x2="1015.388" y2="596.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="950.032" y1="621.996" x2="957.415" y2="621.827" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="957.415" y1="621.827" x2="959.674" y2="623.670" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="916.556" y1="643.053" x2="950.032" y2="621.996" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="963.524" y1="635.004" x2="959.674" y2="623.670" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="900.347" y1="590.671" x2="903.183" y2="593.968" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="903.183" y1="593.968" x2="903.269" y2="597.046" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="861.003" y1="574.923" x2="900.347" y2="590.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="899.115" y1="604.826" x2="903.269" y2="597.046" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="879.746" y1="488.235" x2="891.092" y2="532.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="891.092" y1="532.785" x2="890.959" y2="533.530" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="888.385" y1="535.872" x2="890.959" y2="533.530" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="883.649" y1="537.102" x2="888.385" y2="535.872" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="924.690" y1="468.840" x2="924.801" y2="485.323" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="924.801" y1="485.323" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="858.806" y1="525.718" x2="883.649" y2="537.102" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="883.649" y1="537.102" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="877.335" y1="612.112" x2="899.115" y2="604.826" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="899.115" y1="604.826" x2="913.901" y2="646.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="954.849" y1="651.750" x2="963.524" y2="635.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="963.524" y1="635.004" x2="1005.424" y2="647.755" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1033.065" y1="616.531" x2="1028.621" y2="602.722" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1028.621" y1="602.722" x2="1064.264" y2="576.363" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1053.303" y1="534.525" x2="1044.753" y2="530.377" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1044.753" y1="530.377" x2="1044.903" y2="484.740" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="442.292" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="442.292" x2="924.690" y2="468.840" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="907.607" y1="456.774" x2="924.690" y2="468.840" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1001.349" y1="469.664" x2="998.095" y2="470.935" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="998.095" y1="470.935" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1046.205" y1="482.726" x2="1001.349" y2="469.664" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1001.349" y1="469.664" x2="1012.440" y2="438.687" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1069.001" y1="576.148" x2="1053.303" y2="534.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1053.303" y1="534.525" x2="1077.607" y2="520.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1009.761" y1="653.301" x2="1033.065" y2="616.531" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1033.065" y1="616.531" x2="1056.813" y2="621.206" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="911.243" y1="655.315" x2="954.849" y2="651.750" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="954.849" y1="651.750" x2="966.607" y2="668.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="847.298" y1="578.490" x2="877.335" y2="612.112" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="877.335" y1="612.112" x2="874.824" y2="627.402" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="867.916" y1="479.092" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="867.916" y1="479.092" x2="858.806" y2="525.718" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="850.800" y1="531.184" x2="858.806" y2="525.718" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="912.200" y1="437.122" x2="907.607" y2="456.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="907.607" y1="456.774" x2="866.518" y2="476.847" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="822.234" y1="515.708" x2="850.800" y2="531.184" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="850.800" y1="531.184" x2="841.435" y2="578.126" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="847.328" y1="633.483" x2="874.824" y2="627.402" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="874.824" y1="627.402" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1005.424" y1="647.755" x2="1009.761" y2="653.301" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1013.053" y1="664.981" x2="1009.761" y2="653.301" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="966.607" y1="668.164" x2="1013.053" y2="664.981" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="952.990" y1="687.766" x2="966.607" y2="668.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1064.264" y1="576.363" x2="1069.001" y2="576.148" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1083.326" y1="581.231" x2="1069.001" y2="576.148" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1056.813" y1="621.206" x2="1083.326" y2="581.231" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1059.998" y1="639.787" x2="1056.813" y2="621.206" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1077.607" y1="520.313" x2="1061.111" y2="472.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1061.111" y1="472.202" x2="1046.205" y2="482.726" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1088.004" y1="527.396" x2="1077.607" y2="520.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="420.997" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="420.997" x2="912.200" y2="437.122" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="890.064" y1="422.515" x2="912.200" y2="437.122" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1016.991" y1="437.643" x2="1012.440" y2="438.687" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1012.440" y1="438.687" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1063.356" y1="468.620" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1044.903" y1="484.740" x2="1037.830" y2="489.758" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1016.991" y1="437.643" x2="1063.356" y2="468.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1031.409" y1="399.015" x2="1016.991" y2="437.643" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1091.646" y1="580.750" x2="1088.004" y2="527.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1088.004" y1="527.396" x2="1119.190" y2="510.664" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1020.755" y1="674.626" x2="1059.998" y2="639.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1059.998" y1="639.787" x2="1089.392" y2="647.174" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="901.037" y1="677.977" x2="952.990" y2="687.766" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="952.990" y1="687.766" x2="966.417" y2="709.035" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="841.435" y1="578.126" x2="822.368" y2="584.702" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="822.368" y1="584.702" x2="847.328" y2="633.483" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="843.098" y1="652.260" x2="847.328" y2="633.483" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="866.518" y1="476.847" x2="867.916" y2="479.092" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="879.746" y1="488.235" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="847.026" y1="462.778" x2="866.518" y2="476.847" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="844.902" y1="459.236" x2="847.026" y2="462.778" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="822.234" y1="515.708" x2="847.026" y2="462.778" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="811.924" y1="521.940" x2="822.234" y2="515.708" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="892.538" y1="398.415" x2="890.064" y2="422.515" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="890.064" y1="422.515" x2="844.902" y2="459.236" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="817.477" y1="439.555" x2="844.902" y2="459.236" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="774.927" y1="505.831" x2="811.924" y2="521.940" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="811.924" y1="521.940" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="911.243" y1="655.315" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="901.037" y1="677.977" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="843.098" y1="652.260" x2="892.095" y2="688.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="811.028" y1="664.055" x2="843.098" y2="652.260" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="966.417" y1="709.035" x2="1025.682" y2="692.759" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1020.755" y1="674.626" x2="1013.053" y2="664.981" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="954.383" y1="734.417" x2="966.417" y2="709.035" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1089.392" y1="647.174" x2="1113.737" y2="588.747" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1091.646" y1="580.750" x2="1083.326" y2="581.231" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1096.731" y1="667.435" x2="1089.392" y2="647.174" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1063.356" y1="468.620" x2="1061.111" y2="472.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1046.205" y1="482.726" x2="1044.903" y2="484.740" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1119.190" y1="510.664" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1089.518" y1="447.313" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="420.997" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="452.426" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="388.149" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="388.149" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="892.538" y1="398.415" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="870.081" y1="377.240" x2="892.538" y2="398.415" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1031.409" y1="399.015" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="388.149" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1119.476" y1="426.455" x2="1089.518" y2="447.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1089.518" y1="447.313" x2="1036.135" y2="397.016" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1125.495" y1="587.895" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1125.495" y1="587.895" x2="1132.028" y2="516.372" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1036.713" y1="706.233" x2="1096.731" y2="667.435" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1096.731" y1="667.435" x2="1129.234" y2="681.108" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="874.114" y1="724.752" x2="885.923" y2="710.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="885.923" y1="710.844" x2="954.383" y2="734.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="964.714" y1="760.668" x2="954.383" y2="734.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="786.510" y1="593.409" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="822.368" y1="584.702" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="811.028" y1="664.055" x2="786.510" y2="593.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="814.773" y1="434.875" x2="817.477" y2="439.555" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="817.477" y1="439.555" x2="774.927" y2="505.831" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="870.081" y1="377.240" x2="814.773" y2="434.875" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="814.773" y1="434.875" x2="781.631" y2="411.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="867.820" y1="350.841" x2="870.081" y2="377.240" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="847.298" y1="578.490" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="841.435" y1="578.126" x2="847.298" y2="578.490" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="763.070" y1="509.820" x2="774.629" y2="592.324" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="802.354" y1="682.911" x2="874.114" y2="724.752" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="901.037" y1="677.977" x2="892.095" y2="688.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="964.714" y1="760.668" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1020.755" y1="674.626" x2="1025.682" y2="692.759" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1003.828" y1="642.233" x2="1005.424" y2="647.755" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1025.682" y1="692.759" x2="1036.713" y2="706.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1036.713" y1="706.233" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1056.399" y1="746.009" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1091.646" y1="580.750" x2="1113.737" y2="588.747" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1064.264" y1="576.363" x2="1057.456" y2="573.982" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1129.234" y1="681.108" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1113.737" y1="588.747" x2="1125.495" y2="587.895" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1168.319" y1="597.246" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1171.121" y1="500.189" x2="1119.476" y2="426.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1119.476" y1="426.455" x2="1123.156" y2="420.161" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="345.580" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="345.580" x2="867.820" y2="350.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1055.165" y1="351.011" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="345.580" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1157.051" y1="396.671" x2="1123.156" y2="420.161" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1123.156" y1="420.161" x2="1059.283" y2="348.098" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1200.567" y1="609.315" x2="1168.319" y2="597.246" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1168.319" y1="597.246" x2="1185.153" y2="503.235" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1063.217" y1="772.753" x2="1056.399" y2="746.009" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1056.399" y1="746.009" x2="1140.858" y2="700.809" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="866.867" y1="751.471" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="874.114" y1="724.752" x2="866.867" y2="751.471" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="956.014" y1="790.291" x2="866.867" y2="751.471" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="774.629" y1="592.324" x2="786.510" y2="593.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="742.414" y1="603.842" x2="767.921" y2="700.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="778.616" y1="405.859" x2="781.631" y2="411.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="781.631" y1="411.208" x2="719.271" y2="494.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="846.478" y1="324.667" x2="778.616" y2="405.859" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="778.616" y1="405.859" x2="743.401" y2="380.760" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="706.813" y1="496.189" x2="728.954" y2="602.485" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="742.414" y1="603.842" x2="728.954" y2="602.485" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="892.095" y1="688.774" x2="885.923" y2="710.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="916.556" y1="643.053" x2="913.901" y2="646.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="755.980" y1="718.378" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="913.901" y1="646.396" x2="911.243" y2="655.315" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="845.762" y1="795.518" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="963.309" y1="819.481" x2="1063.217" y2="772.753" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1063.217" y1="772.753" x2="1077.647" y2="789.877" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1174.640" y1="719.101" x2="1200.567" y2="609.315" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1200.567" y1="609.315" x2="1215.710" y2="607.957" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1229.348" y1="488.029" x2="1157.051" y2="396.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1157.051" y1="396.671" x2="1160.932" y2="389.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="297.480" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="297.480" x2="840.722" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1081.464" y1="297.480" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="297.480" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1194.827" y1="366.457" x2="1160.932" y2="389.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1160.932" y1="389.948" x2="1085.015" y2="294.014" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1247.958" y1="620.026" x2="1215.710" y2="607.957" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1215.710" y1="607.957" x2="1243.784" y2="489.085" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1084.465" y1="816.620" x2="1077.647" y2="789.877" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1077.647" y1="789.877" x2="1188.966" y2="737.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="832.269" y1="811.159" x2="845.762" y2="795.518" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="845.762" y1="795.518" x2="957.220" y2="851.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="742.414" y1="603.842" x2="774.629" y2="592.324" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="694.769" y1="614.787" x2="720.749" y2="739.517" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="740.385" y1="375.411" x2="743.401" y2="380.760" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="743.401" y1="380.760" x2="659.219" y2="481.477" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="820.635" y1="268.446" x2="740.385" y2="375.411" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="740.385" y1="375.411" x2="707.243" y2="351.744" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="728.954" y1="602.485" x2="694.769" y2="614.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="681.309" y1="613.430" x2="694.769" y2="614.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="646.566" y1="481.855" x2="681.309" y2="613.430" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="706.783" y1="756.515" x2="832.269" y2="811.159" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="832.269" y1="811.159" x2="825.021" y2="837.878" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="962.311" y1="881.993" x2="1084.465" y2="816.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1084.465" y1="816.620" x2="1097.977" y2="832.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1223.079" y1="758.972" x2="1247.958" y2="620.026" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1247.958" y1="620.026" x2="1262.200" y2="618.818" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1290.828" y1="474.767" x2="1194.827" y2="366.457" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1194.827" y1="366.457" x2="1198.507" y2="360.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="249.380" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="249.380" x2="812.520" y2="240.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1109.071" y1="240.873" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="249.380" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1228.466" y1="339.305" x2="1198.507" y2="360.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1198.507" y1="360.164" x2="1112.243" y2="237.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1290.782" y1="629.377" x2="1262.200" y2="618.818" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1262.200" y1="618.818" x2="1305.599" y2="474.431" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1104.151" y1="856.396" x2="1097.977" y2="832.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1097.977" y1="832.785" x2="1239.349" y2="777.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="813.213" y1="851.786" x2="825.021" y2="837.878" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="825.021" y1="837.878" x2="958.045" y2="915.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="649.094" y1="624.948" x2="681.309" y2="613.430" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="861.003" y1="574.923" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="637.213" y1="623.863" x2="649.094" y2="624.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="670.857" y1="780.283" x2="649.094" y2="624.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="704.539" y1="347.063" x2="707.243" y2="351.744" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="707.243" y1="351.744" x2="596.046" y2="467.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="792.977" y1="209.314" x2="704.539" y2="347.063" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="704.539" y1="347.063" x2="677.115" y2="327.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="582.735" y1="466.892" x2="637.213" y2="623.863" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="637.213" y1="623.863" x2="610.472" y2="633.269" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="654.802" y1="797.239" x2="813.213" y2="851.786" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="813.213" y1="851.786" x2="807.041" y2="873.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="961.660" y1="948.507" x2="1104.151" y2="856.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1104.151" y1="856.396" x2="1115.182" y2="869.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1275.085" y1="801.240" x2="1290.782" y2="629.377" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1290.782" y1="629.377" x2="1302.541" y2="628.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1356.776" y1="460.198" x2="1228.466" y2="339.305" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1228.466" y1="339.305" x2="1231.566" y2="334.170" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="206.811" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="206.811" x2="782.440" y2="179.793" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1138.787" y1="179.606" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="206.811" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1254.627" y1="317.999" x2="1231.566" y2="334.170" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1231.566" y1="334.170" x2="1141.961" y2="174.998" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1324.631" y1="636.522" x2="1302.541" y2="628.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1302.541" y1="628.525" x2="1373.328" y2="458.637" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1120.109" y1="888.003" x2="1115.182" y2="869.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1115.182" y1="869.870" x2="1294.663" y2="820.579" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="798.099" y1="884.652" x2="807.041" y2="873.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="807.041" y1="873.855" x2="958.533" y2="986.753" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="601.355" y1="632.570" x2="610.472" y2="633.269" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="610.472" y1="633.269" x2="614.586" y2="825.722" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="674.990" y1="323.841" x2="677.115" y2="327.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="677.115" y1="327.383" x2="524.370" y2="451.576" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="760.577" y1="141.117" x2="674.990" y2="323.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="674.990" y1="323.841" x2="655.498" y2="309.772" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="507.715" y1="449.567" x2="601.355" y2="632.570" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="601.355" y1="632.570" x2="582.288" y2="639.146" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="593.158" y1="846.038" x2="798.099" y2="884.652" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="798.099" y1="884.652" x2="793.570" y2="900.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="961.349" y1="1028.947" x2="1120.109" y2="888.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1120.109" y1="888.003" x2="1127.812" y2="897.649" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1339.402" y1="852.893" x2="1324.631" y2="636.522" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1324.631" y1="636.522" x2="1332.952" y2="636.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1439.513" y1="441.523" x2="1254.627" y2="317.999" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1254.627" y1="317.999" x2="1256.873" y2="314.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="173.963" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="173.963" x2="744.361" y2="101.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1176.815" y1="100.821" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="173.963" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1271.779" y1="303.892" x2="1256.873" y2="314.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1256.873" y1="314.417" x2="1181.142" y2="93.546" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1347.277" y1="641.124" x2="1332.952" y2="636.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1332.952" y1="636.041" x2="1464.468" y2="437.742" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1131.103" y1="909.328" x2="1127.812" y2="897.649" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1127.812" y1="897.649" x2="1370.608" y2="881.008" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="787.893" y1="907.314" x2="793.570" y2="900.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="793.570" y1="900.289" x2="958.625" y2="1087.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="576.425" y1="638.782" x2="582.288" y2="639.146" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="582.288" y1="639.146" x2="531.894" y2="891.743" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="654.101" y1="307.526" x2="655.498" y2="309.772" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="655.498" y1="309.772" x2="415.868" y2="426.834" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="707.944" y1="31.960" x2="654.101" y2="307.526" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="654.101" y1="307.526" x2="642.940" y2="299.432" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="381.778" y1="420.908" x2="576.425" y2="638.782" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="576.425" y1="638.782" x2="565.486" y2="642.493" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="485.594" y1="932.026" x2="787.893" y2="907.314" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="787.893" y1="907.314" x2="785.235" y2="916.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="961.588" y1="1176.797" x2="1131.103" y2="909.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1131.103" y1="909.328" x2="1135.441" y2="914.874" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1464.690" y1="952.440" x2="1347.277" y2="641.124" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1347.277" y1="641.124" x2="1352.013" y2="640.909" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1609.901" y1="402.354" x2="1271.779" y2="303.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1271.779" y1="303.892" x2="1273.081" y2="301.879" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="152.668" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="152.668" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1260.498" y1="-73.265" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="960.000" y1="152.668" x2="960.000" y2="142.534" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1280.154" y1="296.861" x2="1273.081" y2="301.879" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1273.081" y1="301.879" x2="1274.399" y2="-99.719" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1358.822" y1="643.290" x2="1352.013" y2="640.909" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1352.013" y1="640.909" x2="1698.162" y2="384.929" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1137.036" y1="920.396" x2="1135.441" y2="914.874" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1135.441" y1="914.874" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="782.580" y1="919.577" x2="785.235" y2="916.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="785.235" y1="916.233" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="562.720" y1="642.349" x2="565.486" y2="642.493" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="565.486" y1="642.493" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="642.270" y1="298.383" x2="642.940" y2="299.432" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="642.940" y1="299.432" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="428.039" y1="-542.846" x2="642.270" y2="298.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="-470.272" y1="228.865" x2="562.720" y2="642.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="-509.558" y1="1732.361" x2="782.580" y2="919.577" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="972.408" y1="3428.116" x2="1137.036" y2="920.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="7397.757" y1="5638.793" x2="1358.822" y2="643.290" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="846.478" y1="324.667" x2="867.820" y2="350.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="846.478" y1="324.667" x2="840.722" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="840.722" y1="297.480" x2="820.635" y2="268.446" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="812.520" y1="240.963" x2="820.635" y2="268.446" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="792.977" y1="209.314" x2="812.520" y2="240.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="782.440" y1="179.793" x2="792.977" y2="209.314" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="782.440" y1="179.793" x2="760.577" y2="141.117" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="744.361" y1="101.218" x2="760.577" y2="141.117" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="744.361" y1="101.218" x2="707.944" y2="31.960" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="707.944" y1="31.960" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="428.039" y1="-542.846" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="763.070" y1="509.820" x2="774.927" y2="505.831" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="763.070" y1="509.820" x2="719.271" y2="494.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="719.271" y1="494.328" x2="706.813" y2="496.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="659.219" y1="481.477" x2="706.813" y2="496.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="659.219" y1="481.477" x2="646.566" y2="481.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="596.046" y1="467.620" x2="646.566" y2="481.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="596.046" y1="467.620" x2="582.735" y2="466.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="524.370" y1="451.576" x2="582.735" y2="466.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="524.370" y1="451.576" x2="507.715" y2="449.567" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="507.715" y1="449.567" x2="415.868" y2="426.834" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="415.868" y1="426.834" x2="381.778" y2="420.908" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="381.778" y1="420.908" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="-470.272" y1="228.865" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="802.354" y1="682.911" x2="811.028" y2="664.055" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="802.354" y1="682.911" x2="767.921" y2="700.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="767.921" y1="700.439" x2="755.980" y2="718.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="720.749" y1="739.517" x2="755.980" y2="718.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="706.783" y1="756.515" x2="720.749" y2="739.517" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="670.857" y1="780.283" x2="706.783" y2="756.515" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="670.857" y1="780.283" x2="654.802" y2="797.239" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="614.586" y1="825.722" x2="654.802" y2="797.239" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="614.586" y1="825.722" x2="593.158" y2="846.038" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="593.158" y1="846.038" x2="531.894" y2="891.743" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="531.894" y1="891.743" x2="485.594" y2="932.026" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="485.594" y1="932.026" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="-509.558" y1="1732.361" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="964.714" y1="760.668" x2="956.014" y2="790.291" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="956.014" y1="790.291" x2="963.309" y2="819.481" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="957.220" y1="851.289" x2="963.309" y2="819.481" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="962.311" y1="881.993" x2="957.220" y2="851.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.045" y1="915.439" x2="962.311" y2="881.993" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="961.660" y1="948.507" x2="958.045" y2="915.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.533" y1="986.753" x2="961.660" y2="948.507" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.533" y1="986.753" x2="961.349" y2="1028.947" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.625" y1="1087.870" x2="961.349" y2="1028.947" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="958.625" y1="1087.870" x2="961.588" y2="1176.797" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="961.588" y1="1176.797" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="972.408" y1="3428.116" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1129.234" y1="681.108" x2="1140.858" y2="700.809" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1140.858" y1="700.809" x2="1174.640" y2="719.101" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1188.966" y1="737.811" x2="1174.640" y2="719.101" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1223.079" y1="758.972" x2="1188.966" y2="737.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1239.349" y1="777.041" x2="1223.079" y2="758.972" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1275.085" y1="801.240" x2="1239.349" y2="777.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1294.663" y1="820.579" x2="1275.085" y2="801.240" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1294.663" y1="820.579" x2="1339.402" y2="852.893" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1370.608" y1="881.008" x2="1339.402" y2="852.893" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1370.608" y1="881.008" x2="1464.690" y2="952.440" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1464.690" y1="952.440" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="7397.757" y1="5638.793" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1119.190" y1="510.664" x2="1132.028" y2="516.372" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1132.028" y1="516.372" x2="1171.121" y2="500.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1185.153" y1="503.235" x2="1171.121" y2="500.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1229.348" y1="488.029" x2="1185.153" y2="503.235" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1243.784" y1="489.085" x2="1229.348" y2="488.029" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1290.828" y1="474.767" x2="1243.784" y2="489.085" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1305.599" y1="474.431" x2="1290.828" y2="474.767" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1356.776" y1="460.198" x2="1305.599" y2="474.431" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1373.328" y1="458.637" x2="1356.776" y2="460.198" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1373.328" y1="458.637" x2="1439.513" y2="441.523" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1464.468" y1="437.742" x2="1439.513" y2="441.523" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1464.468" y1="437.742" x2="1609.901" y2="402.354" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1609.901" y1="402.354" x2="1698.162" y2="384.929" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1031.409" y1="399.015" x2="1036.135" y2="397.016" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1036.135" y1="397.016" x2="1055.165" y2="351.011" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1059.283" y1="348.098" x2="1055.165" y2="351.011" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1081.464" y1="297.480" x2="1059.283" y2="348.098" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1085.015" y1="294.014" x2="1081.464" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1109.071" y1="240.873" x2="1085.015" y2="294.014" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1112.243" y1="237.004" x2="1109.071" y2="240.873" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1138.787" y1="179.606" x2="1112.243" y2="237.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1141.961" y1="174.998" x2="1138.787" y2="179.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1141.961" y1="174.998" x2="1176.815" y2="100.821" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1181.142" y1="93.546" x2="1176.815" y2="100.821" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1181.142" y1="93.546" x2="1260.498" y2="-73.265" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-<line x1="1260.498" y1="-73.265" x2="1274.399" y2="-99.719" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
-</g>
-<polygon points="960.000,552.000 996.876,509.114 1011.832,529.361" fill="hsl(232 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,552.000 1016.521,554.092 1010.016,578.409" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="1016.521,554.092 960.000,552.000 1011.832,529.361" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="996.876,509.114 960.000,552.000 974.617,497.361" fill="hsl(232 88% 60%)" opacity="0.230"/>
-<polygon points="974.617,497.361 960.000,552.000 949.463,496.430" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,552.000 1010.016,578.409 993.605,597.495" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="949.463,496.430 960.000,552.000 926.395,506.505" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,552.000 993.605,597.495 970.537,607.570" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="926.395,506.505 960.000,552.000 909.984,525.591" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,552.000 970.537,607.570 945.383,606.639" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="909.984,525.591 960.000,552.000 903.479,549.908" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,552.000 908.168,574.639 903.479,549.908" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,552.000 923.124,594.886 908.168,574.639" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="923.124,594.886 960.000,552.000 945.383,606.639" fill="hsl(233 88% 60%)" opacity="0.230"/>
-<polygon points="974.617,497.361 949.463,496.430 960.000,481.300" fill="hsl(270 88% 60%)" opacity="0.230"/>
-<polygon points="1011.832,529.361 996.876,509.114 1014.395,506.750" fill="hsl(270 88% 60%)" opacity="0.230"/>
-<polygon points="1016.521,554.092 1043.355,548.453 1029.736,564.916" fill="hsl(267 88% 60%)" opacity="0.230"/>
-<polygon points="1010.016,578.409 1016.521,554.092 1029.736,564.916" fill="hsl(269 88% 60%)" opacity="0.230"/>
-<polygon points="993.605,597.495 1016.470,615.548 994.784,614.125" fill="hsl(265 88% 60%)" opacity="0.230"/>
-<polygon points="970.537,607.570 993.605,597.495 994.784,614.125" fill="hsl(268 88% 60%)" opacity="0.230"/>
-<polygon points="923.124,594.886 945.383,606.639 934.305,618.816" fill="hsl(267 88% 60%)" opacity="0.230"/>
-<polygon points="903.479,549.908 908.168,574.639 891.708,575.064" fill="hsl(266 88% 60%)" opacity="0.230"/>
-<polygon points="909.984,525.591 909.772,497.843 926.395,506.505" fill="hsl(266 88% 60%)" opacity="0.230"/>
-<polygon points="926.395,506.505 940.647,473.854 949.463,496.430" fill="hsl(258 88% 60%)" opacity="0.230"/>
-<polygon points="909.772,497.843 909.984,525.591 898.140,513.838" fill="hsl(266 88% 60%)" opacity="0.230"/>
-<polygon points="903.479,549.908 891.708,575.064 885.389,556.265" fill="hsl(267 88% 60%)" opacity="0.230"/>
-<polygon points="923.124,594.886 934.305,618.816 915.227,613.042" fill="hsl(268 88% 60%)" opacity="0.230"/>
-<polygon points="970.537,607.570 994.784,614.125 978.903,626.402" fill="hsl(270 88% 60%)" opacity="0.230"/>
-<polygon points="1010.016,578.409 1029.736,564.916 1030.517,585.156" fill="hsl(271 88% 60%)" opacity="0.230"/>
-<polygon points="1011.832,529.361 1014.395,506.750 1031.501,518.004" fill="hsl(271 88% 60%)" opacity="0.230"/>
-<polygon points="974.617,497.361 1009.078,486.399 996.876,509.114" fill="hsl(257 88% 60%)" opacity="0.230"/>
-<polygon points="979.353,473.854 974.617,497.361 960.000,481.300" fill="hsl(270 88% 60%)" opacity="0.230"/>
-<polygon points="960.000,481.300 949.463,496.430 940.647,473.854" fill="hsl(274 88% 60%)" opacity="0.230"/>
-<polygon points="940.647,473.854 926.395,506.505 909.772,497.843" fill="hsl(252 88% 60%)" opacity="0.230"/>
-<polygon points="1014.395,506.750 996.876,509.114 1009.078,486.399" fill="hsl(270 88% 60%)" opacity="0.230"/>
-<polygon points="1009.078,486.399 974.617,497.361 979.353,473.854" fill="hsl(256 88% 60%)" opacity="0.230"/>
-<polygon points="1043.355,548.453 1016.521,554.092 1011.832,529.361" fill="hsl(256 88% 60%)" opacity="0.230"/>
-<polygon points="1043.355,548.453 1011.832,529.361 1031.501,518.004" fill="hsl(255 88% 60%)" opacity="0.230"/>
-<polygon points="1016.470,615.548 993.605,597.495 1010.016,578.409" fill="hsl(255 88% 60%)" opacity="0.230"/>
-<polygon points="1016.470,615.548 1010.016,578.409 1030.517,585.156" fill="hsl(254 88% 60%)" opacity="0.230"/>
-<polygon points="945.383,606.639 970.537,607.570 946.088,637.549" fill="hsl(253 88% 60%)" opacity="0.230"/>
-<polygon points="934.305,618.816 945.383,606.639 946.088,637.549" fill="hsl(263 88% 60%)" opacity="0.230"/>
-<polygon points="946.088,637.549 970.537,607.570 978.903,626.402" fill="hsl(253 88% 60%)" opacity="0.230"/>
-<polygon points="908.168,574.639 923.124,594.886 883.325,596.007" fill="hsl(251 88% 60%)" opacity="0.230"/>
-<polygon points="891.708,575.064 908.168,574.639 883.325,596.007" fill="hsl(258 88% 60%)" opacity="0.230"/>
-<polygon points="883.325,596.007 923.124,594.886 915.227,613.042" fill="hsl(251 88% 60%)" opacity="0.230"/>
-<polygon points="898.140,513.838 909.984,525.591 875.839,519.518" fill="hsl(251 88% 60%)" opacity="0.230"/>
-<polygon points="875.839,519.518 909.984,525.591 903.479,549.908" fill="hsl(250 88% 60%)" opacity="0.230"/>
-<polygon points="875.839,519.518 903.479,549.908 885.389,556.265" fill="hsl(250 88% 60%)" opacity="0.230"/>
-<polygon points="940.647,473.854 909.772,497.843 908.803,474.067" fill="hsl(252 88% 60%)" opacity="0.230"/>
-<polygon points="875.839,519.518 885.389,556.265 865.231,542.668" fill="hsl(254 88% 60%)" opacity="0.230"/>
-<polygon points="883.325,596.007 915.227,613.042 891.810,621.371" fill="hsl(257 88% 60%)" opacity="0.230"/>
-<polygon points="946.088,637.549 978.903,626.402 971.504,650.715" fill="hsl(259 88% 60%)" opacity="0.230"/>
-<polygon points="1016.470,615.548 1030.517,585.156 1045.972,606.054" fill="hsl(259 88% 60%)" opacity="0.230"/>
-<polygon points="1043.355,548.453 1031.501,518.004 1058.086,518.092" fill="hsl(257 88% 60%)" opacity="0.230"/>
-<polygon points="940.647,473.854 979.353,473.854 960.000,481.300" fill="hsl(235 88% 60%)" opacity="0.230"/>
-<polygon points="924.305,452.119 940.647,473.854 908.803,474.067" fill="hsl(261 88% 60%)" opacity="0.230"/>
-<polygon points="1009.078,486.399 979.353,473.854 995.695,452.119" fill="hsl(254 88% 60%)" opacity="0.230"/>
-<polygon points="1009.078,486.399 995.695,452.119 1016.855,459.695" fill="hsl(256 88% 60%)" opacity="0.230"/>
-<polygon points="1043.355,548.453 1058.086,518.092 1069.970,538.415" fill="hsl(260 88% 60%)" opacity="0.230"/>
-<polygon points="1016.470,615.548 1045.972,606.054 1041.038,631.119" fill="hsl(262 88% 60%)" opacity="0.230"/>
-<polygon points="946.088,637.549 971.504,650.715 948.510,667.186" fill="hsl(262 88% 60%)" opacity="0.230"/>
-<polygon points="883.325,596.007 891.810,621.371 860.659,616.256" fill="hsl(260 88% 60%)" opacity="0.230"/>
-<polygon points="875.839,519.518 909.772,497.843 898.140,513.838" fill="hsl(230 88% 60%)" opacity="0.230"/>
-<polygon points="845.359,513.562 875.839,519.518 865.231,542.668" fill="hsl(257 88% 60%)" opacity="0.230"/>
-<polygon points="924.305,452.119 908.803,474.067 894.702,445.202" fill="hsl(260 88% 60%)" opacity="0.230"/>
-<polygon points="845.359,513.562 865.231,542.668 833.067,536.251" fill="hsl(256 88% 60%)" opacity="0.230"/>
-<polygon points="860.659,616.256 891.810,621.371 866.681,643.482" fill="hsl(258 88% 60%)" opacity="0.230"/>
-<polygon points="971.504,650.715 1016.470,615.548 1041.038,631.119" fill="hsl(226 88% 60%)" opacity="0.230"/>
-<polygon points="971.504,650.715 1041.038,631.119 973.839,684.781" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="948.510,667.186 971.504,650.715 973.839,684.781" fill="hsl(257 88% 60%)" opacity="0.230"/>
-<polygon points="1045.972,606.054 1043.355,548.453 1069.970,538.415" fill="hsl(226 88% 60%)" opacity="0.230"/>
-<polygon points="1045.972,606.054 1069.970,538.415 1074.989,625.299" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="1041.038,631.119 1045.972,606.054 1074.989,625.299" fill="hsl(255 88% 60%)" opacity="0.230"/>
-<polygon points="1058.086,518.092 1016.855,459.695 1091.658,506.581" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="1069.970,538.415 1058.086,518.092 1091.658,506.581" fill="hsl(252 88% 60%)" opacity="0.230"/>
-<polygon points="995.695,452.119 940.647,473.854 924.305,452.119" fill="hsl(226 88% 60%)" opacity="0.230"/>
-<polygon points="912.747,417.857 924.305,452.119 894.702,445.202" fill="hsl(253 88% 60%)" opacity="0.230"/>
-<polygon points="1016.855,459.695 995.695,452.119 1007.253,417.857" fill="hsl(249 88% 60%)" opacity="0.230"/>
-<polygon points="1009.078,486.399 1058.086,518.092 1031.501,518.004" fill="hsl(227 88% 60%)" opacity="0.230"/>
-<polygon points="1009.078,486.399 1031.501,518.004 1014.395,506.750" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="1037.312,429.076 1016.855,459.695 1007.253,417.857" fill="hsl(248 88% 60%)" opacity="0.230"/>
-<polygon points="1069.970,538.415 1091.658,506.581 1107.369,535.862" fill="hsl(249 88% 60%)" opacity="0.230"/>
-<polygon points="1041.038,631.119 1074.989,625.299 1066.359,659.641" fill="hsl(250 88% 60%)" opacity="0.230"/>
-<polygon points="948.510,667.186 973.839,684.781 941.328,705.304" fill="hsl(250 88% 60%)" opacity="0.230"/>
-<polygon points="883.325,596.007 885.389,556.265 891.708,575.064" fill="hsl(231 88% 60%)" opacity="0.230"/>
-<polygon points="825.558,634.217 860.659,616.256 866.681,643.482" fill="hsl(248 88% 60%)" opacity="0.230"/>
-<polygon points="908.803,474.067 875.839,519.518 845.359,513.562" fill="hsl(224 88% 60%)" opacity="0.230"/>
-<polygon points="845.359,513.562 894.702,445.202 908.803,474.067" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="894.702,445.202 845.359,513.562 809.079,496.569" fill="hsl(218 88% 60%)" opacity="0.230"/>
-<polygon points="909.772,497.843 875.839,519.518 908.803,474.067" fill="hsl(229 88% 60%)" opacity="0.230"/>
-<polygon points="809.079,496.569 845.359,513.562 833.067,536.251" fill="hsl(245 88% 60%)" opacity="0.230"/>
-<polygon points="912.747,417.857 894.702,445.202 868.801,413.347" fill="hsl(246 88% 60%)" opacity="0.230"/>
-<polygon points="868.801,413.347 894.702,445.202 809.079,496.569" fill="hsl(216 88% 60%)" opacity="0.230"/>
-<polygon points="809.079,496.569 833.067,536.251 791.413,537.142" fill="hsl(241 88% 60%)" opacity="0.230"/>
-<polygon points="891.810,621.371 948.510,667.186 866.681,643.482" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="866.681,643.482 941.328,705.304 841.428,677.368" fill="hsl(216 88% 60%)" opacity="0.230"/>
-<polygon points="825.558,634.217 866.681,643.482 841.428,677.368" fill="hsl(242 88% 60%)" opacity="0.230"/>
-<polygon points="973.839,684.781 1066.359,659.641 985.185,726.096" fill="hsl(215 88% 60%)" opacity="0.230"/>
-<polygon points="941.328,705.304 973.839,684.781 985.185,726.096" fill="hsl(241 88% 60%)" opacity="0.230"/>
-<polygon points="1074.989,625.299 1107.369,535.862 1115.066,641.998" fill="hsl(215 88% 60%)" opacity="0.230"/>
-<polygon points="1066.359,659.641 1074.989,625.299 1115.066,641.998" fill="hsl(237 88% 60%)" opacity="0.230"/>
-<polygon points="1058.086,518.092 1009.078,486.399 1016.855,459.695" fill="hsl(226 88% 60%)" opacity="0.230"/>
-<polygon points="1091.658,506.581 1037.312,429.076 1129.979,485.008" fill="hsl(214 88% 60%)" opacity="0.230"/>
-<polygon points="1037.312,429.076 1091.658,506.581 1016.855,459.695" fill="hsl(221 88% 60%)" opacity="0.230"/>
-<polygon points="995.695,452.119 924.305,452.119 912.747,417.857" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="912.747,417.857 1007.253,417.857 995.695,452.119" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="1007.253,417.857 912.747,417.857 906.049,373.842" fill="hsl(211 88% 60%)" opacity="0.230"/>
-<polygon points="979.353,473.854 940.647,473.854 995.695,452.119" fill="hsl(227 88% 60%)" opacity="0.230"/>
-<polygon points="906.049,373.842 912.747,417.857 868.801,413.347" fill="hsl(237 88% 60%)" opacity="0.230"/>
-<polygon points="1007.253,417.857 906.049,373.842 1013.951,373.842" fill="hsl(211 88% 60%)" opacity="0.230"/>
-<polygon points="1129.979,485.008 1037.312,429.076 1068.207,396.285" fill="hsl(212 88% 60%)" opacity="0.230"/>
-<polygon points="1107.369,535.862 1074.989,625.299 1069.970,538.415" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="1066.359,659.641 1115.066,641.998 1091.369,698.330" fill="hsl(230 88% 60%)" opacity="0.230"/>
-<polygon points="946.088,637.549 915.227,613.042 934.305,618.816" fill="hsl(231 88% 60%)" opacity="0.230"/>
-<polygon points="926.172,749.320 941.328,705.304 985.185,726.096" fill="hsl(230 88% 60%)" opacity="0.230"/>
-<polygon points="825.558,634.217 833.067,536.251 860.659,616.256" fill="hsl(218 88% 60%)" opacity="0.230"/>
-<polygon points="860.659,616.256 865.231,542.668 883.325,596.007" fill="hsl(224 88% 60%)" opacity="0.230"/>
-<polygon points="868.801,413.347 809.079,496.569 769.133,470.923" fill="hsl(207 88% 60%)" opacity="0.230"/>
-<polygon points="868.801,413.347 769.133,470.923 834.058,380.006" fill="hsl(206 88% 60%)" opacity="0.230"/>
-<polygon points="906.049,373.842 868.801,413.347 834.058,380.006" fill="hsl(227 88% 60%)" opacity="0.230"/>
-<polygon points="833.067,536.251 825.558,634.217 791.413,537.142" fill="hsl(217 88% 60%)" opacity="0.230"/>
-<polygon points="731.345,662.781 692.378,554.491 743.326,543.879" fill="hsl(201 88% 60%)" opacity="0.230"/>
-<polygon points="841.428,677.368 926.172,749.320 816.777,719.646" fill="hsl(208 88% 60%)" opacity="0.230"/>
-<polygon points="985.185,726.096 1091.369,698.330 1003.272,771.965" fill="hsl(207 88% 60%)" opacity="0.230"/>
-<polygon points="1016.470,615.548 971.504,650.715 978.903,626.402" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="1016.470,615.548 978.903,626.402 994.784,614.125" fill="hsl(231 88% 60%)" opacity="0.230"/>
-<polygon points="1066.359,659.641 973.839,684.781 1041.038,631.119" fill="hsl(219 88% 60%)" opacity="0.230"/>
-<polygon points="1091.369,698.330 985.185,726.096 1066.359,659.641" fill="hsl(213 88% 60%)" opacity="0.230"/>
-<polygon points="1043.355,548.453 1045.972,606.054 1030.517,585.156" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="1115.066,641.998 1152.748,540.007 1162.616,656.290" fill="hsl(205 88% 60%)" opacity="0.230"/>
-<polygon points="1043.355,548.453 1030.517,585.156 1029.736,564.916" fill="hsl(232 88% 60%)" opacity="0.230"/>
-<polygon points="1152.748,540.007 1115.066,641.998 1107.369,535.862" fill="hsl(213 88% 60%)" opacity="0.230"/>
-<polygon points="1129.979,485.008 1068.207,396.285 1170.914,456.336" fill="hsl(203 88% 60%)" opacity="0.230"/>
-<polygon points="1170.914,456.336 1068.207,396.285 1106.050,362.743" fill="hsl(203 88% 60%)" opacity="0.230"/>
-<polygon points="1162.616,656.290 1152.748,540.007 1202.785,548.966" fill="hsl(204 88% 60%)" opacity="0.230"/>
-<polygon points="1003.272,771.965 1091.369,698.330 1115.469,743.362" fill="hsl(205 88% 60%)" opacity="0.230"/>
-<polygon points="926.172,749.320 841.428,677.368 941.328,705.304" fill="hsl(212 88% 60%)" opacity="0.230"/>
-<polygon points="941.328,705.304 866.681,643.482 948.510,667.186" fill="hsl(219 88% 60%)" opacity="0.230"/>
-<polygon points="791.413,537.142 825.558,634.217 781.137,649.633" fill="hsl(210 88% 60%)" opacity="0.230"/>
-<polygon points="692.378,554.491 731.345,662.781 680.396,673.393" fill="hsl(201 88% 60%)" opacity="0.230"/>
-<polygon points="891.810,621.371 946.088,637.549 948.510,667.186" fill="hsl(225 88% 60%)" opacity="0.230"/>
-<polygon points="816.777,719.646 905.691,796.353 793.444,766.277" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="915.227,613.042 946.088,637.549 891.810,621.371" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="905.691,796.353 816.777,719.646 926.172,749.320" fill="hsl(204 88% 60%)" opacity="0.230"/>
-<polygon points="1003.272,771.965 1115.469,743.362 1025.396,819.267" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="1213.493,668.306 1202.785,548.966 1253.661,560.982" fill="hsl(201 88% 60%)" opacity="0.170"/>
-<polygon points="1025.396,819.267 1115.469,743.362 1137.592,790.664" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="793.444,766.277 905.691,796.353 882.359,842.983" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="781.137,649.633 743.326,543.879 791.413,537.142" fill="hsl(207 88% 60%)" opacity="0.230"/>
-<polygon points="865.231,542.668 860.659,616.256 833.067,536.251" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="692.378,554.491 680.396,673.393 642.585,567.639" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="793.444,766.277 882.359,842.983 772.964,813.309" fill="hsl(204 88% 60%)" opacity="0.230"/>
-<polygon points="1025.396,819.267 1137.592,790.664 1049.496,864.300" fill="hsl(205 88% 60%)" opacity="0.230"/>
-<polygon points="1213.493,668.306 1253.661,560.982 1263.530,677.265" fill="hsl(204 88% 60%)" opacity="0.230"/>
-<polygon points="1211.934,423.876 1147.069,330.282 1249.776,390.334" fill="hsl(203 88% 60%)" opacity="0.230"/>
-<polygon points="1249.776,390.334 1147.069,330.282 1188.004,301.611" fill="hsl(203 88% 60%)" opacity="0.230"/>
-<polygon points="1263.530,677.265 1253.661,560.982 1301.211,575.274" fill="hsl(205 88% 60%)" opacity="0.230"/>
-<polygon points="1049.496,864.300 1137.592,790.664 1155.679,836.533" fill="hsl(207 88% 60%)" opacity="0.230"/>
-<polygon points="772.964,813.309 882.359,842.983 857.708,885.262" fill="hsl(208 88% 60%)" opacity="0.230"/>
-<polygon points="743.326,543.879 781.137,649.633 731.345,662.781" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="642.585,567.639 680.396,673.393 632.310,680.130" fill="hsl(207 88% 60%)" opacity="0.230"/>
-<polygon points="883.325,596.007 865.231,542.668 885.389,556.265" fill="hsl(229 88% 60%)" opacity="0.230"/>
-<polygon points="752.883,315.696 687.959,406.613 653.215,373.272" fill="hsl(206 88% 60%)" opacity="0.230"/>
-<polygon points="752.883,315.696 653.215,373.272 712.938,290.050" fill="hsl(207 88% 60%)" opacity="0.230"/>
-<polygon points="642.585,567.639 632.310,680.130 598.165,583.055" fill="hsl(210 88% 60%)" opacity="0.230"/>
-<polygon points="772.964,813.309 857.708,885.262 757.808,857.325" fill="hsl(211 88% 60%)" opacity="0.230"/>
-<polygon points="1049.496,864.300 1155.679,836.533 1074.505,902.988" fill="hsl(212 88% 60%)" opacity="0.230"/>
-<polygon points="1263.530,677.265 1301.211,575.274 1308.909,681.410" fill="hsl(213 88% 60%)" opacity="0.230"/>
-<polygon points="1249.776,390.334 1188.004,301.611 1280.671,357.543" fill="hsl(212 88% 60%)" opacity="0.230"/>
-<polygon points="1013.951,221.118 906.049,221.118 912.747,177.103" fill="hsl(211 88% 60%)" opacity="0.230"/>
-<polygon points="1013.951,221.118 912.747,177.103 1007.253,177.103" fill="hsl(211 88% 60%)" opacity="0.230"/>
-<polygon points="1280.671,357.543 1188.004,301.611 1226.325,280.038" fill="hsl(213 88% 60%)" opacity="0.230"/>
-<polygon points="1308.909,681.410 1301.211,575.274 1341.288,591.973" fill="hsl(215 88% 60%)" opacity="0.230"/>
-<polygon points="1074.505,902.988 1155.679,836.533 1167.025,877.848" fill="hsl(215 88% 60%)" opacity="0.230"/>
-<polygon points="757.808,857.325 857.708,885.262 832.455,919.147" fill="hsl(216 88% 60%)" opacity="0.230"/>
-<polygon points="598.165,583.055 632.310,680.130 590.656,681.021" fill="hsl(217 88% 60%)" opacity="0.230"/>
-<polygon points="712.938,290.050 653.215,373.272 627.315,341.417" fill="hsl(216 88% 60%)" opacity="0.230"/>
-<polygon points="712.938,290.050 627.315,341.417 676.658,273.057" fill="hsl(217 88% 60%)" opacity="0.230"/>
-<polygon points="598.165,583.055 590.656,681.021 563.063,601.016" fill="hsl(218 88% 60%)" opacity="0.230"/>
-<polygon points="757.808,857.325 832.455,919.147 750.625,895.443" fill="hsl(219 88% 60%)" opacity="0.230"/>
-<polygon points="1074.505,902.988 1167.025,877.848 1099.827,931.510" fill="hsl(219 88% 60%)" opacity="0.230"/>
-<polygon points="1308.909,681.410 1341.288,591.973 1346.307,678.857" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="1280.671,357.543 1226.325,280.038 1301.129,326.923" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="1007.253,177.103 912.747,177.103 924.305,142.841" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="1007.253,177.103 924.305,142.841 995.695,142.841" fill="hsl(220 88% 60%)" opacity="0.230"/>
-<polygon points="1301.129,326.923 1226.325,280.038 1259.898,268.527" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="1346.307,678.857 1341.288,591.973 1370.306,611.218" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="1099.827,931.510 1167.025,877.848 1169.360,911.914" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="750.625,895.443 832.455,919.147 807.326,941.258" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="563.063,601.016 590.656,681.021 558.492,674.604" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="676.658,273.057 627.315,341.417 613.214,312.551" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="676.658,273.057 613.214,312.551 646.178,267.101" fill="hsl(224 88% 60%)" opacity="0.230"/>
-<polygon points="563.063,601.016 558.492,674.604 540.398,621.265" fill="hsl(224 88% 60%)" opacity="0.230"/>
-<polygon points="750.625,895.443 807.326,941.258 753.048,925.081" fill="hsl(225 88% 60%)" opacity="0.230"/>
-<polygon points="1099.827,931.510 1169.360,911.914 1124.394,947.081" fill="hsl(225 88% 60%)" opacity="0.230"/>
-<polygon points="1346.307,678.857 1370.306,611.218 1372.922,668.819" fill="hsl(226 88% 60%)" opacity="0.230"/>
-<polygon points="1301.129,326.923 1259.898,268.527 1308.905,300.219" fill="hsl(226 88% 60%)" opacity="0.230"/>
-<polygon points="924.305,142.841 979.353,121.106 995.695,142.841" fill="hsl(227 88% 60%)" opacity="0.230"/>
-<polygon points="979.353,121.106 924.305,142.841 940.647,121.106" fill="hsl(227 88% 60%)" opacity="0.230"/>
-<polygon points="1308.905,300.219 1259.898,268.527 1286.483,268.614" fill="hsl(227 88% 60%)" opacity="0.230"/>
-<polygon points="1372.922,668.819 1370.306,611.218 1385.760,632.116" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="1124.394,947.081 1169.360,911.914 1161.961,936.228" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="753.048,925.081 807.326,941.258 783.908,949.587" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="540.398,621.265 558.492,674.604 538.334,661.008" fill="hsl(229 88% 60%)" opacity="0.230"/>
-<polygon points="646.178,267.101 613.214,312.551 612.245,288.776" fill="hsl(229 88% 60%)" opacity="0.230"/>
-<polygon points="646.178,267.101 612.245,288.776 623.876,272.780" fill="hsl(230 88% 60%)" opacity="0.230"/>
-<polygon points="540.398,621.265 538.334,661.008 532.015,642.208" fill="hsl(230 88% 60%)" opacity="0.230"/>
-<polygon points="753.048,925.081 783.908,949.587 764.831,943.813" fill="hsl(231 88% 60%)" opacity="0.230"/>
-<polygon points="1124.394,947.081 1161.961,936.228 1146.080,948.504" fill="hsl(232 88% 60%)" opacity="0.230"/>
-<polygon points="1372.922,668.819 1385.760,632.116 1386.541,652.356" fill="hsl(232 88% 60%)" opacity="0.230"/>
-<polygon points="1308.905,300.219 1286.483,268.614 1303.589,279.869" fill="hsl(234 88% 60%)" opacity="0.230"/>
-<polygon points="979.353,121.106 940.647,121.106 960.000,113.660" fill="hsl(235 88% 60%)" opacity="0.230"/>
-<polygon points="903.184,323.640 906.049,373.842 834.058,380.006" fill="hsl(222 88% 60%)" opacity="0.230"/>
-<polygon points="794.221,346.709 903.184,323.640 834.058,380.006" fill="hsl(209 88% 60%)" opacity="0.230"/>
-<polygon points="769.133,470.923 809.079,496.569 791.413,537.142" fill="hsl(228 88% 60%)" opacity="0.230"/>
-<polygon points="769.133,470.923 743.326,543.879 727.796,439.910" fill="hsl(213 88% 60%)" opacity="0.230"/>
-<polygon points="743.326,543.879 769.133,470.923 791.413,537.142" fill="hsl(225 88% 60%)" opacity="0.230"/>
-<polygon points="692.378,554.491 727.796,439.910 743.326,543.879" fill="hsl(202 88% 60%)" opacity="0.230"/>
-<polygon points="781.137,649.633 825.558,634.217 841.428,677.368" fill="hsl(229 88% 60%)" opacity="0.230"/>
-<polygon points="781.137,649.633 816.777,719.646 731.345,662.781" fill="hsl(215 88% 60%)" opacity="0.230"/>
-<polygon points="816.777,719.646 781.137,649.633 841.428,677.368" fill="hsl(225 88% 60%)" opacity="0.230"/>
-<polygon points="926.172,749.320 1003.272,771.965 905.691,796.353" fill="hsl(218 88% 60%)" opacity="0.230"/>
-<polygon points="1003.272,771.965 926.172,749.320 985.185,726.096" fill="hsl(224 88% 60%)" opacity="0.230"/>
-<polygon points="1091.369,698.330 1162.616,656.290 1115.469,743.362" fill="hsl(218 88% 60%)" opacity="0.230"/>
-<polygon points="1162.616,656.290 1091.369,698.330 1115.066,641.998" fill="hsl(224 88% 60%)" opacity="0.230"/>
-<polygon points="1107.369,535.862 1129.979,485.008 1152.748,540.007" fill="hsl(230 88% 60%)" opacity="0.230"/>
-<polygon points="1152.748,540.007 1170.914,456.336 1202.785,548.966" fill="hsl(218 88% 60%)" opacity="0.230"/>
-<polygon points="1129.979,485.008 1107.369,535.862 1091.658,506.581" fill="hsl(237 88% 60%)" opacity="0.230"/>
-<polygon points="1170.914,456.336 1152.748,540.007 1129.979,485.008" fill="hsl(223 88% 60%)" opacity="0.230"/>
-<polygon points="1037.312,429.076 1013.951,373.842 1068.207,396.285" fill="hsl(229 88% 60%)" opacity="0.230"/>
-<polygon points="1068.207,396.285 1016.816,323.640 1106.050,362.743" fill="hsl(219 88% 60%)" opacity="0.230"/>
-<polygon points="1013.951,373.842 1037.312,429.076 1007.253,417.857" fill="hsl(231 88% 60%)" opacity="0.230"/>
-<polygon points="1016.816,323.640 1068.207,396.285 1013.951,373.842" fill="hsl(221 88% 60%)" opacity="0.230"/>
-<line x1="960.000" y1="552.000" x2="996.876" y2="509.114" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="1011.832" y2="529.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="996.876" y1="509.114" x2="1011.832" y2="529.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="1010.016" y2="578.409" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1010.016" y1="578.409" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1011.832" y1="529.361" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="996.876" y1="509.114" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="974.617" y1="497.361" x2="949.463" y2="496.430" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="949.463" y2="496.430" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="993.605" y2="597.495" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1010.016" y1="578.409" x2="993.605" y2="597.495" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="949.463" y1="496.430" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="970.537" y2="607.570" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="993.605" y1="597.495" x2="970.537" y2="607.570" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="926.395" y1="506.505" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="945.383" y2="606.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="970.537" y1="607.570" x2="945.383" y2="606.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="909.984" y1="525.591" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.168" y1="574.639" x2="960.000" y2="552.000" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="903.479" y1="549.908" x2="908.168" y2="574.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="552.000" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.168" y1="574.639" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="945.383" y1="606.639" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="974.617" y1="497.361" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="949.463" y1="496.430" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1011.832" y1="529.361" x2="1014.395" y2="506.750" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="996.876" y1="509.114" x2="1014.395" y2="506.750" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.521" y1="554.092" x2="1029.736" y2="564.916" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.521" y1="554.092" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1010.016" y1="578.409" x2="1029.736" y2="564.916" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="993.605" y1="597.495" x2="994.784" y2="614.125" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="993.605" y1="597.495" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="970.537" y1="607.570" x2="994.784" y2="614.125" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="923.124" y1="594.886" x2="934.305" y2="618.816" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="945.383" y1="606.639" x2="934.305" y2="618.816" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="903.479" y1="549.908" x2="891.708" y2="575.064" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.168" y1="574.639" x2="891.708" y2="575.064" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="909.772" y1="497.843" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="909.984" y1="525.591" x2="898.140" y2="513.838" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="473.854" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="926.395" y1="506.505" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="909.772" y1="497.843" x2="898.140" y2="513.838" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="903.479" y1="549.908" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="891.708" y1="575.064" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="923.124" y1="594.886" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="934.305" y1="618.816" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="970.537" y1="607.570" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="994.784" y1="614.125" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1010.016" y1="578.409" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1029.736" y1="564.916" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1011.832" y1="529.361" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1014.395" y1="506.750" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1009.078" y1="486.399" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="974.617" y1="497.361" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="979.353" y1="473.854" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="960.000" y1="481.300" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="949.463" y1="496.430" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="473.854" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1014.395" y1="506.750" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="996.876" y1="509.114" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1009.078" y1="486.399" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1029.736" y1="564.916" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1011.832" y1="529.361" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1043.355" y1="548.453" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="994.784" y1="614.125" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1010.016" y1="578.409" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.470" y1="615.548" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="945.383" y1="606.639" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="970.537" y1="607.570" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="934.305" y1="618.816" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="946.088" y1="637.549" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.168" y1="574.639" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="923.124" y1="594.886" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="891.708" y1="575.064" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="883.325" y1="596.007" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="898.140" y1="513.838" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="909.984" y1="525.591" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="875.839" y1="519.518" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="875.839" y1="519.518" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="473.854" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="909.772" y1="497.843" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="875.839" y1="519.518" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="885.389" y1="556.265" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="883.325" y1="596.007" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="915.227" y1="613.042" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="946.088" y1="637.549" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="978.903" y1="626.402" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.470" y1="615.548" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1030.517" y1="585.156" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1043.355" y1="548.453" x2="1058.086" y2="518.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1031.501" y1="518.004" x2="1058.086" y2="518.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="452.119" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="473.854" x2="924.305" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="924.305" y1="452.119" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1009.078" y1="486.399" x2="995.695" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="979.353" y1="473.854" x2="995.695" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1009.078" y1="486.399" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="452.119" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1043.355" y1="548.453" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1058.086" y1="518.092" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.470" y1="615.548" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1045.972" y1="606.054" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="946.088" y1="637.549" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="971.504" y1="650.715" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="883.325" y1="596.007" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="891.810" y1="621.371" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.803" y1="474.067" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="875.839" y1="519.518" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="845.359" y1="513.562" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="924.305" y1="452.119" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.803" y1="474.067" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="845.359" y1="513.562" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="865.231" y1="542.668" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="860.659" y1="616.256" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="891.810" y1="621.371" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.470" y1="615.548" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="971.504" y1="650.715" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="971.504" y1="650.715" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="948.510" y1="667.186" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1043.355" y1="548.453" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1045.972" y1="606.054" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1045.972" y1="606.054" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1041.038" y1="631.119" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1058.086" y1="518.092" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1058.086" y1="518.092" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1069.970" y1="538.415" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="452.119" x2="924.305" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="924.305" y1="452.119" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="912.747" y1="417.857" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.855" y1="459.695" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="452.119" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1037.312" y1="429.076" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1031.501" y1="518.004" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1037.312" y1="429.076" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1037.312" y1="429.076" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1069.970" y1="538.415" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1091.658" y1="506.581" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1041.038" y1="631.119" x2="1066.359" y2="659.641" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1074.989" y1="625.299" x2="1066.359" y2="659.641" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="948.510" y1="667.186" x2="941.328" y2="705.304" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="973.839" y1="684.781" x2="941.328" y2="705.304" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="860.659" y1="616.256" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="860.659" y1="616.256" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="825.558" y1="634.217" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="908.803" y1="474.067" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="875.839" y1="519.518" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="894.702" y1="445.202" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="894.702" y1="445.202" x2="809.079" y2="496.569" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="809.079" y1="496.569" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="809.079" y1="496.569" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="912.747" y1="417.857" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="894.702" y1="445.202" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="868.801" y1="413.347" x2="809.079" y2="496.569" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="809.079" y1="496.569" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="833.067" y1="536.251" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="948.510" y1="667.186" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="866.681" y1="643.482" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="866.681" y1="643.482" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="825.558" y1="634.217" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="973.839" y1="684.781" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="973.839" y1="684.781" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="941.328" y1="705.304" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1074.989" y1="625.299" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1074.989" y1="625.299" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1066.359" y1="659.641" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1091.658" y1="506.581" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1058.086" y1="518.092" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1091.658" y1="506.581" x2="1129.979" y2="485.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1129.979" y1="485.008" x2="1037.312" y2="429.076" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="452.119" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="473.854" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1007.253" y1="417.857" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1007.253" y1="417.857" x2="906.049" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="373.842" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="373.842" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1007.253" y1="417.857" x2="1013.951" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="373.842" x2="1013.951" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1129.979" y1="485.008" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1037.312" y1="429.076" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1152.748" y1="540.007" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1107.369" y1="535.862" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1066.359" y1="659.641" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1115.066" y1="641.998" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="841.428" y1="677.368" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="941.328" y1="705.304" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="926.172" y1="749.320" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="791.413" y1="537.142" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="825.558" y1="634.217" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="781.137" y1="649.633" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="868.801" y1="413.347" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="809.079" y1="496.569" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="868.801" y1="413.347" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="769.133" y1="470.923" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="373.842" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="865.231" y1="542.668" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="865.231" y1="542.668" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="791.413" y1="537.142" x2="743.326" y2="543.879" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="841.428" y1="677.368" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="941.328" y1="705.304" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="985.185" y1="726.096" x2="1003.272" y2="771.965" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1066.359" y1="659.641" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.470" y1="615.548" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1066.359" y1="659.641" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1091.369" y1="698.330" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1003.272" y1="771.965" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1107.369" y1="535.862" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1030.517" y1="585.156" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1115.066" y1="641.998" x2="1162.616" y2="656.290" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1107.369" y1="535.862" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1162.616" y1="656.290" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1129.979" y1="485.008" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1068.207" y1="396.285" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1013.951" y1="373.842" x2="903.184" y2="323.640" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="373.842" x2="903.184" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1013.951" y1="373.842" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="903.184" y1="323.640" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1170.914" y1="456.336" x2="1106.050" y2="362.743" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1068.207" y1="396.285" x2="1106.050" y2="362.743" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1162.616" y1="656.290" x2="1202.785" y2="548.966" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1152.748" y1="540.007" x2="1202.785" y2="548.966" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1003.272" y1="771.965" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1091.369" y1="698.330" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="905.691" y1="796.353" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="926.172" y1="749.320" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="905.691" y1="796.353" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="791.413" y1="537.142" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="781.137" y1="649.633" x2="731.345" y2="662.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="834.058" y1="380.006" x2="727.796" y2="439.910" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="769.133" y1="470.923" x2="727.796" y2="439.910" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="834.058" y1="380.006" x2="794.221" y2="346.709" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="727.796" y1="439.910" x2="794.221" y2="346.709" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="743.326" y1="543.879" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="731.345" y1="662.781" x2="743.326" y2="543.879" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="941.328" y1="705.304" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="946.088" y1="637.549" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="816.777" y1="719.646" x2="793.444" y2="766.277" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="946.088" y1="637.549" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="793.444" y1="766.277" x2="905.691" y2="796.353" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1003.272" y1="771.965" x2="1025.396" y2="819.267" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1115.469" y1="743.362" x2="1025.396" y2="819.267" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1162.616" y1="656.290" x2="1213.493" y2="668.306" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1202.785" y1="548.966" x2="1213.493" y2="668.306" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1170.914" y1="456.336" x2="1211.934" y2="423.876" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1106.050" y1="362.743" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1016.816" y1="323.640" x2="903.184" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="903.184" y1="323.640" x2="903.184" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.816" y1="323.640" x2="1016.816" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="903.184" y1="271.320" x2="1016.816" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1211.934" y1="423.876" x2="1147.069" y2="330.282" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1106.050" y1="362.743" x2="1147.069" y2="330.282" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1213.493" y1="668.306" x2="1253.661" y2="560.982" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1202.785" y1="548.966" x2="1253.661" y2="560.982" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1025.396" y1="819.267" x2="1137.592" y2="790.664" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1115.469" y1="743.362" x2="1137.592" y2="790.664" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="793.444" y1="766.277" x2="882.359" y2="842.983" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="905.691" y1="796.353" x2="882.359" y2="842.983" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="743.326" y1="543.879" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="731.345" y1="662.781" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="794.221" y1="346.709" x2="687.959" y2="406.613" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="727.796" y1="439.910" x2="687.959" y2="406.613" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="794.221" y1="346.709" x2="752.883" y2="315.696" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="687.959" y1="406.613" x2="752.883" y2="315.696" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="731.345" y1="662.781" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="692.378" y1="554.491" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="692.378" y1="554.491" x2="642.585" y2="567.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="793.444" y1="766.277" x2="772.964" y2="813.309" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="882.359" y1="842.983" x2="772.964" y2="813.309" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1025.396" y1="819.267" x2="1049.496" y2="864.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1137.592" y1="790.664" x2="1049.496" y2="864.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1213.493" y1="668.306" x2="1263.530" y2="677.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1253.661" y1="560.982" x2="1263.530" y2="677.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1211.934" y1="423.876" x2="1249.776" y2="390.334" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1147.069" y1="330.282" x2="1249.776" y2="390.334" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.816" y1="271.320" x2="906.049" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="903.184" y1="271.320" x2="906.049" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1016.816" y1="271.320" x2="1013.951" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="221.118" x2="1013.951" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1249.776" y1="390.334" x2="1188.004" y2="301.611" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1147.069" y1="330.282" x2="1188.004" y2="301.611" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1263.530" y1="677.265" x2="1301.211" y2="575.274" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1253.661" y1="560.982" x2="1301.211" y2="575.274" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1049.496" y1="864.300" x2="1155.679" y2="836.533" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1137.592" y1="790.664" x2="1155.679" y2="836.533" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="772.964" y1="813.309" x2="857.708" y2="885.262" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="882.359" y1="842.983" x2="857.708" y2="885.262" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="642.585" y1="567.639" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="883.325" y1="596.007" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="642.585" y1="567.639" x2="632.310" y2="680.130" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="632.310" y1="680.130" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="752.883" y1="315.696" x2="653.215" y2="373.272" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="687.959" y1="406.613" x2="653.215" y2="373.272" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="752.883" y1="315.696" x2="712.938" y2="290.050" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="653.215" y1="373.272" x2="712.938" y2="290.050" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="642.585" y1="567.639" x2="598.165" y2="583.055" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="632.310" y1="680.130" x2="598.165" y2="583.055" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="772.964" y1="813.309" x2="757.808" y2="857.325" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="857.708" y1="885.262" x2="757.808" y2="857.325" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1049.496" y1="864.300" x2="1074.505" y2="902.988" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1155.679" y1="836.533" x2="1074.505" y2="902.988" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1263.530" y1="677.265" x2="1308.909" y2="681.410" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1301.211" y1="575.274" x2="1308.909" y2="681.410" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1249.776" y1="390.334" x2="1280.671" y2="357.543" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1188.004" y1="301.611" x2="1280.671" y2="357.543" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1013.951" y1="221.118" x2="912.747" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="221.118" x2="912.747" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1013.951" y1="221.118" x2="1007.253" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="912.747" y1="177.103" x2="1007.253" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1280.671" y1="357.543" x2="1226.325" y2="280.038" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1188.004" y1="301.611" x2="1226.325" y2="280.038" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1308.909" y1="681.410" x2="1341.288" y2="591.973" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1301.211" y1="575.274" x2="1341.288" y2="591.973" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1074.505" y1="902.988" x2="1167.025" y2="877.848" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1155.679" y1="836.533" x2="1167.025" y2="877.848" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="757.808" y1="857.325" x2="832.455" y2="919.147" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="857.708" y1="885.262" x2="832.455" y2="919.147" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="598.165" y1="583.055" x2="590.656" y2="681.021" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="632.310" y1="680.130" x2="590.656" y2="681.021" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="712.938" y1="290.050" x2="627.315" y2="341.417" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="653.215" y1="373.272" x2="627.315" y2="341.417" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="712.938" y1="290.050" x2="676.658" y2="273.057" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="627.315" y1="341.417" x2="676.658" y2="273.057" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="598.165" y1="583.055" x2="563.063" y2="601.016" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="590.656" y1="681.021" x2="563.063" y2="601.016" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="757.808" y1="857.325" x2="750.625" y2="895.443" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="832.455" y1="919.147" x2="750.625" y2="895.443" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1074.505" y1="902.988" x2="1099.827" y2="931.510" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1167.025" y1="877.848" x2="1099.827" y2="931.510" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1308.909" y1="681.410" x2="1346.307" y2="678.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1341.288" y1="591.973" x2="1346.307" y2="678.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1280.671" y1="357.543" x2="1301.129" y2="326.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1226.325" y1="280.038" x2="1301.129" y2="326.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1007.253" y1="177.103" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="912.747" y1="177.103" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1007.253" y1="177.103" x2="995.695" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="924.305" y1="142.841" x2="995.695" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1301.129" y1="326.923" x2="1259.898" y2="268.527" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1226.325" y1="280.038" x2="1259.898" y2="268.527" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1346.307" y1="678.857" x2="1370.306" y2="611.218" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1341.288" y1="591.973" x2="1370.306" y2="611.218" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1099.827" y1="931.510" x2="1169.360" y2="911.914" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1167.025" y1="877.848" x2="1169.360" y2="911.914" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="750.625" y1="895.443" x2="807.326" y2="941.258" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="832.455" y1="919.147" x2="807.326" y2="941.258" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="563.063" y1="601.016" x2="558.492" y2="674.604" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="590.656" y1="681.021" x2="558.492" y2="674.604" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="676.658" y1="273.057" x2="613.214" y2="312.551" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="627.315" y1="341.417" x2="613.214" y2="312.551" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="676.658" y1="273.057" x2="646.178" y2="267.101" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="613.214" y1="312.551" x2="646.178" y2="267.101" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="563.063" y1="601.016" x2="540.398" y2="621.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="558.492" y1="674.604" x2="540.398" y2="621.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="750.625" y1="895.443" x2="753.048" y2="925.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="807.326" y1="941.258" x2="753.048" y2="925.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1099.827" y1="931.510" x2="1124.394" y2="947.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1169.360" y1="911.914" x2="1124.394" y2="947.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1346.307" y1="678.857" x2="1372.922" y2="668.819" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1370.306" y1="611.218" x2="1372.922" y2="668.819" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1301.129" y1="326.923" x2="1308.905" y2="300.219" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1259.898" y1="268.527" x2="1308.905" y2="300.219" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="979.353" y1="121.106" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="924.305" y1="142.841" x2="940.647" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="142.841" x2="979.353" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="121.106" x2="979.353" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1308.905" y1="300.219" x2="1286.483" y2="268.614" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1259.898" y1="268.527" x2="1286.483" y2="268.614" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1372.922" y1="668.819" x2="1385.760" y2="632.116" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1370.306" y1="611.218" x2="1385.760" y2="632.116" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1124.394" y1="947.081" x2="1161.961" y2="936.228" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1169.360" y1="911.914" x2="1161.961" y2="936.228" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="753.048" y1="925.081" x2="783.908" y2="949.587" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="807.326" y1="941.258" x2="783.908" y2="949.587" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="540.398" y1="621.265" x2="538.334" y2="661.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="558.492" y1="674.604" x2="538.334" y2="661.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="646.178" y1="267.101" x2="612.245" y2="288.776" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="613.214" y1="312.551" x2="612.245" y2="288.776" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="646.178" y1="267.101" x2="623.876" y2="272.780" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="612.245" y1="288.776" x2="623.876" y2="272.780" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="540.398" y1="621.265" x2="532.015" y2="642.208" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="538.334" y1="661.008" x2="532.015" y2="642.208" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="753.048" y1="925.081" x2="764.831" y2="943.813" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="783.908" y1="949.587" x2="764.831" y2="943.813" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1124.394" y1="947.081" x2="1146.080" y2="948.504" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1161.961" y1="936.228" x2="1146.080" y2="948.504" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1372.922" y1="668.819" x2="1386.541" y2="652.356" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1385.760" y1="632.116" x2="1386.541" y2="652.356" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1308.905" y1="300.219" x2="1303.589" y2="279.869" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1286.483" y1="268.614" x2="1303.589" y2="279.869" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="979.353" y1="121.106" x2="960.000" y2="113.660" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="121.106" x2="960.000" y2="113.660" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="903.184" y1="323.640" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="794.221" y1="346.709" x2="903.184" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="794.221" y1="346.709" x2="903.184" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="903.184" y1="271.320" x2="752.883" y2="315.696" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="221.118" x2="752.883" y2="315.696" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="906.049" y1="221.118" x2="712.938" y2="290.050" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="712.938" y1="290.050" x2="912.747" y2="177.103" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="912.747" y1="177.103" x2="676.658" y2="273.057" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="676.658" y1="273.057" x2="924.305" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="646.178" y1="267.101" x2="924.305" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="121.106" x2="646.178" y2="267.101" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="940.647" y1="121.106" x2="623.876" y2="272.780" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="769.133" y1="470.923" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="743.326" y1="543.879" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="743.326" y1="543.879" x2="727.796" y2="439.910" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="727.796" y1="439.910" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="692.378" y1="554.491" x2="687.959" y2="406.613" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="687.959" y1="406.613" x2="642.585" y2="567.639" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="642.585" y1="567.639" x2="653.215" y2="373.272" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="653.215" y1="373.272" x2="598.165" y2="583.055" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="598.165" y1="583.055" x2="627.315" y2="341.417" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="563.063" y1="601.016" x2="627.315" y2="341.417" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="563.063" y1="601.016" x2="613.214" y2="312.551" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="540.398" y1="621.265" x2="613.214" y2="312.551" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="612.245" y1="288.776" x2="540.398" y2="621.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="612.245" y1="288.776" x2="532.015" y2="642.208" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="781.137" y1="649.633" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="816.777" y1="719.646" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="816.777" y1="719.646" x2="731.345" y2="662.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="731.345" y1="662.781" x2="793.444" y2="766.277" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="680.396" y1="673.393" x2="793.444" y2="766.277" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="680.396" y1="673.393" x2="772.964" y2="813.309" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="772.964" y1="813.309" x2="632.310" y2="680.130" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="632.310" y1="680.130" x2="757.808" y2="857.325" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="757.808" y1="857.325" x2="590.656" y2="681.021" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="750.625" y1="895.443" x2="590.656" y2="681.021" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="750.625" y1="895.443" x2="558.492" y2="674.604" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="753.048" y1="925.081" x2="558.492" y2="674.604" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="538.334" y1="661.008" x2="753.048" y2="925.081" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="538.334" y1="661.008" x2="764.831" y2="943.813" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1003.272" y1="771.965" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1003.272" y1="771.965" x2="905.691" y2="796.353" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="905.691" y1="796.353" x2="1025.396" y2="819.267" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="882.359" y1="842.983" x2="1025.396" y2="819.267" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="882.359" y1="842.983" x2="1049.496" y2="864.300" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="857.708" y1="885.262" x2="1049.496" y2="864.300" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="857.708" y1="885.262" x2="1074.505" y2="902.988" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1074.505" y1="902.988" x2="832.455" y2="919.147" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="832.455" y1="919.147" x2="1099.827" y2="931.510" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1099.827" y1="931.510" x2="807.326" y2="941.258" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1124.394" y1="947.081" x2="807.326" y2="941.258" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="783.908" y1="949.587" x2="1124.394" y2="947.081" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="783.908" y1="949.587" x2="1146.080" y2="948.504" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1162.616" y1="656.290" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1162.616" y1="656.290" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1115.469" y1="743.362" x2="1213.493" y2="668.306" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1137.592" y1="790.664" x2="1213.493" y2="668.306" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1137.592" y1="790.664" x2="1263.530" y2="677.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1155.679" y1="836.533" x2="1263.530" y2="677.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1155.679" y1="836.533" x2="1308.909" y2="681.410" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1308.909" y1="681.410" x2="1167.025" y2="877.848" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1167.025" y1="877.848" x2="1346.307" y2="678.857" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1346.307" y1="678.857" x2="1169.360" y2="911.914" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1372.922" y1="668.819" x2="1169.360" y2="911.914" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1161.961" y1="936.228" x2="1372.922" y2="668.819" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1161.961" y1="936.228" x2="1386.541" y2="652.356" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1129.979" y1="485.008" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1129.979" y1="485.008" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1152.748" y1="540.007" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1202.785" y1="548.966" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1202.785" y1="548.966" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1253.661" y1="560.982" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1253.661" y1="560.982" x2="1249.776" y2="390.334" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1301.211" y1="575.274" x2="1249.776" y2="390.334" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1301.211" y1="575.274" x2="1280.671" y2="357.543" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1280.671" y1="357.543" x2="1341.288" y2="591.973" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1341.288" y1="591.973" x2="1301.129" y2="326.923" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1301.129" y1="326.923" x2="1370.306" y2="611.218" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1308.905" y1="300.219" x2="1370.306" y2="611.218" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1385.760" y1="632.116" x2="1308.905" y2="300.219" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1013.951" y1="373.842" x2="1037.312" y2="429.076" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1013.951" y1="373.842" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1068.207" y1="396.285" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1106.050" y1="362.743" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
-<line x1="1106.050" y1="362.743" x2="1016.816" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1147.069" y1="330.282" x2="1016.816" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1147.069" y1="330.282" x2="1013.951" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1188.004" y1="301.611" x2="1013.951" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1188.004" y1="301.611" x2="1007.253" y2="177.103" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1007.253" y1="177.103" x2="1226.325" y2="280.038" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1226.325" y1="280.038" x2="995.695" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="995.695" y1="142.841" x2="1259.898" y2="268.527" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="979.353" y1="121.106" x2="1259.898" y2="268.527" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<line x1="1286.483" y1="268.614" x2="979.353" y2="121.106" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
-<g filter="url(#hole-glow)"><path d="M 1013.951,373.842 L 906.049,373.842 L 903.184,323.640 L 1016.816,323.640 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-<path d="M 834.058,380.006 L 769.133,470.923 L 727.796,439.910 L 794.221,346.709 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-<path d="M 1162.616,656.290 L 1202.785,548.966 L 1213.493,668.306 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-<path d="M 1170.914,456.336 L 1106.050,362.743 L 1147.069,330.282 L 1211.934,423.876 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-<path d="M 1016.816,323.640 L 903.184,323.640 L 903.184,271.320 L 1016.816,271.320 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-<path d="M 794.221,346.709 L 727.796,439.910 L 687.959,406.613 L 752.883,315.696 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-<path d="M 1016.816,271.320 L 903.184,271.320 L 906.049,221.118 L 1013.951,221.118 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
-</g>
-<circle cx="960.000" cy="552.000" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="996.876" cy="509.114" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1011.832" cy="529.361" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1010.016" cy="578.409" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1016.521" cy="554.092" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="974.617" cy="497.361" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="949.463" cy="496.430" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="993.605" cy="597.495" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="926.395" cy="506.505" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="970.537" cy="607.570" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="909.984" cy="525.591" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="945.383" cy="606.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="903.479" cy="549.908" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="908.168" cy="574.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="923.124" cy="594.886" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="960.000" cy="481.300" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1014.395" cy="506.750" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1029.736" cy="564.916" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="994.784" cy="614.125" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="934.305" cy="618.816" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="891.708" cy="575.064" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="898.140" cy="513.838" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="909.772" cy="497.843" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="885.389" cy="556.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="915.227" cy="613.042" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="978.903" cy="626.402" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1030.517" cy="585.156" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1031.501" cy="518.004" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="979.353" cy="473.854" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="940.647" cy="473.854" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1009.078" cy="486.399" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1043.355" cy="548.453" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1016.470" cy="615.548" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="946.088" cy="637.549" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="883.325" cy="596.007" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="875.839" cy="519.518" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="908.803" cy="474.067" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="865.231" cy="542.668" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="891.810" cy="621.371" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="971.504" cy="650.715" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1045.972" cy="606.054" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1058.086" cy="518.092" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="924.305" cy="452.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="995.695" cy="452.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1016.855" cy="459.695" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1069.970" cy="538.415" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1041.038" cy="631.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="948.510" cy="667.186" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="860.659" cy="616.256" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="845.359" cy="513.562" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="894.702" cy="445.202" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="833.067" cy="536.251" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="866.681" cy="643.482" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="973.839" cy="684.781" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1074.989" cy="625.299" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1091.658" cy="506.581" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="912.747" cy="417.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1007.253" cy="417.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1037.312" cy="429.076" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1107.369" cy="535.862" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1066.359" cy="659.641" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="941.328" cy="705.304" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="825.558" cy="634.217" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="809.079" cy="496.569" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="868.801" cy="413.347" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="791.413" cy="537.142" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="841.428" cy="677.368" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="985.185" cy="726.096" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1115.066" cy="641.998" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1129.979" cy="485.008" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="906.049" cy="373.842" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1013.951" cy="373.842" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1068.207" cy="396.285" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1152.748" cy="540.007" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1091.369" cy="698.330" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="926.172" cy="749.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="781.137" cy="649.633" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="769.133" cy="470.923" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="834.058" cy="380.006" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="743.326" cy="543.879" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="816.777" cy="719.646" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1003.272" cy="771.965" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1162.616" cy="656.290" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1170.914" cy="456.336" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="903.184" cy="323.640" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1016.816" cy="323.640" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1106.050" cy="362.743" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1202.785" cy="548.966" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1115.469" cy="743.362" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="905.691" cy="796.353" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="731.345" cy="662.781" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="727.796" cy="439.910" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="794.221" cy="346.709" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="692.378" cy="554.491" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="793.444" cy="766.277" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1025.396" cy="819.267" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1213.493" cy="668.306" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1211.934" cy="423.876" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="903.184" cy="271.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1016.816" cy="271.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1147.069" cy="330.282" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1253.661" cy="560.982" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1137.592" cy="790.664" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="882.359" cy="842.983" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="680.396" cy="673.393" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="687.959" cy="406.613" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="752.883" cy="315.696" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="642.585" cy="567.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="772.964" cy="813.309" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1049.496" cy="864.300" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1263.530" cy="677.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1249.776" cy="390.334" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="906.049" cy="221.118" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1013.951" cy="221.118" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1188.004" cy="301.611" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1301.211" cy="575.274" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1155.679" cy="836.533" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="857.708" cy="885.262" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="632.310" cy="680.130" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="653.215" cy="373.272" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="712.938" cy="290.050" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="598.165" cy="583.055" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="757.808" cy="857.325" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1074.505" cy="902.988" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1308.909" cy="681.410" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1280.671" cy="357.543" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="912.747" cy="177.103" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1007.253" cy="177.103" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1226.325" cy="280.038" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1341.288" cy="591.973" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1167.025" cy="877.848" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="832.455" cy="919.147" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="590.656" cy="681.021" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="627.315" cy="341.417" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="676.658" cy="273.057" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="563.063" cy="601.016" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="750.625" cy="895.443" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1099.827" cy="931.510" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1346.307" cy="678.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1301.129" cy="326.923" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="924.305" cy="142.841" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="995.695" cy="142.841" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1259.898" cy="268.527" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1370.306" cy="611.218" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1169.360" cy="911.914" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="807.326" cy="941.258" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="558.492" cy="674.604" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="613.214" cy="312.551" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="646.178" cy="267.101" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="540.398" cy="621.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="753.048" cy="925.081" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1124.394" cy="947.081" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1372.922" cy="668.819" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1308.905" cy="300.219" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="940.647" cy="121.106" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="979.353" cy="121.106" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1286.483" cy="268.614" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1385.760" cy="632.116" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1161.961" cy="936.228" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="783.908" cy="949.587" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="538.334" cy="661.008" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="612.245" cy="288.776" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="623.876" cy="272.780" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="532.015" cy="642.208" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="764.831" cy="943.813" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1146.080" cy="948.504" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1386.541" cy="652.356" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="1303.589" cy="279.869" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-<circle cx="960.000" cy="113.660" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
-</g>
-<path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill="none" stroke="url(#mask-stroke)" stroke-width="2.2" opacity="0.92" filter="url(#edge-glow)"/>
-<g font-family="Inter, Avenir Next, Helvetica, Arial, sans-serif" fill="#f7f3ff">
-<text x="72" y="82" font-size="24" font-weight="600" letter-spacing="8" fill="#9ff7ff">MOONLIGHT TRIANGULATION</text>
-<text x="70" y="145" font-size="58" font-weight="780" letter-spacing="-1.5">PERSISTENCE ROSE</text>
-<g transform="translate(1500 790)">
-<text x="0" y="0" font-size="18" letter-spacing="2.4" fill="#91a8d8">TOPOLOGICAL HOLES AT THIS SCALE</text>
-<text x="0" y="74" font-size="78" font-weight="720" fill="#fff5cd">β₁ = 7</text>
-<text x="2" y="116" font-size="17" fill="#c9d3ff">7 independent one-dimensional holes</text>
-<text x="2" y="144" font-size="15" fill="#9ba8d4">in the exact alpha complex</text>
-<text x="2" y="176" font-size="14" fill="#7682aa">critical rank 538 · α² = 0.358</text>
-<text x="2" y="200" font-size="12" fill="#596486">exact 94-digit / 94-digit rational</text>
-</g>
-</g>
-</svg>
diff --git a/docs/persistence-rose/Main.hs b/docs/persistence-rose/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/Main.hs
@@ -0,0 +1,212 @@
+module Main (main) where
+
+import Control.Exception (IOException, try)
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Except
+  ( ExceptT (..),
+    except,
+    runExceptT,
+  )
+import Data.Bifunctor (first)
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as ByteString
+import Data.Foldable (traverse_)
+import Moonlight.Triangulation.Exhibit.PersistenceRose
+  ( PersistenceRoseError,
+    PersistenceRoseScene,
+    alphaEclipseReceipt,
+    buildAlphaEclipseScene,
+    buildPersistenceRoseScene,
+    persistenceRoseFrameSpecs,
+    persistenceRoseReceipt,
+    renderAlphaEclipse,
+    renderAlphaEclipseReceipt,
+    renderPersistenceRoseAnimationReceipt,
+    renderPersistenceRoseFrame,
+    renderPersistenceRosePoster,
+    renderPersistenceRoseReceipt,
+    stampPersistenceRoseGif,
+    verifyPersistenceRoseGif,
+  )
+import System.Directory (createDirectoryIfMissing)
+import System.Environment (getArgs)
+import System.Exit (die)
+import System.FilePath ((</>), takeDirectory)
+import Text.Printf (printf)
+import Text.Read (readMaybe)
+
+data PersistenceRoseCommand
+  = WritePoster !FilePath
+  | WriteEclipse !FilePath
+  | WriteFrames !Int !FilePath
+  | StampGif !FilePath !FilePath
+  | VerifyArtifacts !FilePath
+  | PrintReceipt
+
+data PersistenceRoseCommandError
+  = PersistenceRoseUsage ![String]
+  | PersistenceRoseInvalidFrameCount !String
+  | PersistenceRoseConstructionFailed !PersistenceRoseError
+  | PersistenceRoseDirectoryFailed !FilePath !IOException
+  | PersistenceRoseReadFailed !FilePath !IOException
+  | PersistenceRoseWriteFailed !FilePath !IOException
+  | PersistenceRoseArtifactMismatch !FilePath
+
+instance Show PersistenceRoseCommandError where
+  show commandError =
+    case commandError of
+      PersistenceRoseUsage arguments ->
+        "invalid persistence-rose command "
+          <> show arguments
+          <> "; expected: poster OUTPUT.svg | eclipse OUTPUT.svg | frames FRAME_COUNT OUTPUT_DIRECTORY | stamp-gif INPUT.gif OUTPUT.gif | verify-artifacts PACKAGE_ROOT | receipt"
+      PersistenceRoseInvalidFrameCount submitted ->
+        "frame count must be an integer greater than one, received " <> show submitted
+      PersistenceRoseConstructionFailed sceneError -> show sceneError
+      PersistenceRoseDirectoryFailed path caughtError ->
+        "could not create output directory " <> show path <> ": " <> show caughtError
+      PersistenceRoseReadFailed path caughtError ->
+        "could not read " <> show path <> ": " <> show caughtError
+      PersistenceRoseWriteFailed path caughtError ->
+        "could not write " <> show path <> ": " <> show caughtError
+      PersistenceRoseArtifactMismatch path ->
+        "tracked exhibit differs from its canonical renderer: " <> show path
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  result <- runExceptT (runPersistenceRoseCommand arguments)
+  either (die . show) pure result
+
+runPersistenceRoseCommand :: [String] -> ExceptT PersistenceRoseCommandError IO ()
+runPersistenceRoseCommand arguments = do
+  command <- except (parseCommand arguments)
+  case command of
+    WriteEclipse outputPath -> do
+      scene <- except (first PersistenceRoseConstructionFailed buildAlphaEclipseScene)
+      createOutputDirectory (takeDirectory outputPath)
+      writeOutput outputPath (renderAlphaEclipse scene)
+      lift
+        ( putStrLn
+            (renderAlphaEclipseReceipt (alphaEclipseReceipt scene) <> "\nwrote alpha eclipse " <> outputPath)
+        )
+    StampGif inputPath outputPath -> do
+      scene <- requirePersistenceRoseScene
+      inputBytes <- readBytes inputPath
+      (receipt, stamped) <- except (first PersistenceRoseConstructionFailed (stampPersistenceRoseGif scene inputBytes))
+      createOutputDirectory (takeDirectory outputPath)
+      writeBytes outputPath stamped
+      lift
+        ( putStrLn
+            (renderPersistenceRoseAnimationReceipt receipt <> "\nwrote stamped GIF " <> outputPath)
+        )
+    VerifyArtifacts packageRoot -> verifyTrackedArtifacts packageRoot
+    PrintReceipt -> do
+      scene <- requirePersistenceRoseScene
+      lift (putStrLn (renderPersistenceRoseReceipt (persistenceRoseReceipt scene)))
+    WritePoster outputPath -> do
+      scene <- requirePersistenceRoseScene
+      let receiptText = renderPersistenceRoseReceipt (persistenceRoseReceipt scene)
+      createOutputDirectory (takeDirectory outputPath)
+      writeOutput outputPath (renderPersistenceRosePoster scene)
+      lift (putStrLn (receiptText <> "\nwrote poster " <> outputPath))
+    WriteFrames frameCount outputDirectory -> do
+      scene <- requirePersistenceRoseScene
+      let receiptText = renderPersistenceRoseReceipt (persistenceRoseReceipt scene)
+      frames <- except (first PersistenceRoseConstructionFailed (persistenceRoseFrameSpecs frameCount))
+      createOutputDirectory outputDirectory
+      traverse_
+        (\(frameIndex, frame) -> writeOutput (framePath outputDirectory frameIndex) (renderPersistenceRoseFrame scene frame))
+        (zip [0 :: Int ..] frames)
+      lift
+        ( putStrLn
+            ( receiptText
+                <> "\nwrote "
+                <> show (length frames)
+                <> " frames to "
+                <> outputDirectory
+            )
+        )
+
+requirePersistenceRoseScene :: ExceptT PersistenceRoseCommandError IO PersistenceRoseScene
+requirePersistenceRoseScene =
+  except (first PersistenceRoseConstructionFailed buildPersistenceRoseScene)
+
+parseCommand :: [String] -> Either PersistenceRoseCommandError PersistenceRoseCommand
+parseCommand arguments =
+  case arguments of
+    ["poster", outputPath] -> Right (WritePoster outputPath)
+    ["eclipse", outputPath] -> Right (WriteEclipse outputPath)
+    ["stamp-gif", inputPath, outputPath] -> Right (StampGif inputPath outputPath)
+    ["verify-artifacts", packageRoot] -> Right (VerifyArtifacts packageRoot)
+    ["frames", submittedCount, outputDirectory] ->
+      case readMaybe submittedCount of
+        Just frameCount
+          | frameCount > 1 -> Right (WriteFrames frameCount outputDirectory)
+        _ -> Left (PersistenceRoseInvalidFrameCount submittedCount)
+    ["receipt"] -> Right PrintReceipt
+    _ -> Left (PersistenceRoseUsage arguments)
+
+createOutputDirectory :: FilePath -> ExceptT PersistenceRoseCommandError IO ()
+createOutputDirectory outputDirectory =
+  liftIOException
+    (PersistenceRoseDirectoryFailed outputDirectory)
+    (createDirectoryIfMissing True outputDirectory)
+
+writeOutput :: FilePath -> String -> ExceptT PersistenceRoseCommandError IO ()
+writeOutput outputPath contents =
+  liftIOException (PersistenceRoseWriteFailed outputPath) (writeFile outputPath contents)
+
+readOutput :: FilePath -> ExceptT PersistenceRoseCommandError IO String
+readOutput inputPath =
+  liftIOException (PersistenceRoseReadFailed inputPath) (readFile inputPath)
+
+readBytes :: FilePath -> ExceptT PersistenceRoseCommandError IO ByteString
+readBytes inputPath =
+  liftIOException (PersistenceRoseReadFailed inputPath) (ByteString.readFile inputPath)
+
+writeBytes :: FilePath -> ByteString -> ExceptT PersistenceRoseCommandError IO ()
+writeBytes outputPath contents =
+  liftIOException (PersistenceRoseWriteFailed outputPath) (ByteString.writeFile outputPath contents)
+
+verifyTrackedArtifacts :: FilePath -> ExceptT PersistenceRoseCommandError IO ()
+verifyTrackedArtifacts packageRoot = do
+  roseScene <- requirePersistenceRoseScene
+  eclipseScene <- except (first PersistenceRoseConstructionFailed buildAlphaEclipseScene)
+  let exhibitRoot = packageRoot </> "docs" </> "persistence-rose"
+      posterPath = exhibitRoot </> "moonlight-triangulation-persistence-rose.svg"
+      eclipsePath = exhibitRoot </> "moonlight-triangulation-alpha-eclipse.svg"
+      gifPath = exhibitRoot </> "moonlight-triangulation-persistence-rose.gif"
+  verifyTextArtifact posterPath (renderPersistenceRosePoster roseScene)
+  verifyTextArtifact eclipsePath (renderAlphaEclipse eclipseScene)
+  gifBytes <- readBytes gifPath
+  animationReceipt <-
+    except
+      (first PersistenceRoseConstructionFailed (verifyPersistenceRoseGif roseScene gifBytes))
+  lift
+    ( putStrLn
+        ( "verified poster, alpha eclipse, and stamped GIF\n"
+            <> renderPersistenceRoseAnimationReceipt animationReceipt
+        )
+    )
+
+verifyTextArtifact
+  :: FilePath
+  -> String
+  -> ExceptT PersistenceRoseCommandError IO ()
+verifyTextArtifact artifactPath expectedContents = do
+  actualContents <- readOutput artifactPath
+  except
+    ( if actualContents == expectedContents
+        then Right ()
+        else Left (PersistenceRoseArtifactMismatch artifactPath)
+    )
+
+liftIOException
+  :: (IOException -> PersistenceRoseCommandError)
+  -> IO value
+  -> ExceptT PersistenceRoseCommandError IO value
+liftIOException wrapFailure = ExceptT . fmap (first wrapFailure) . try
+
+framePath :: FilePath -> Int -> FilePath
+framePath outputDirectory frameIndex =
+  outputDirectory </> printf "frame-%04d.svg" frameIndex
diff --git a/docs/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs b/docs/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs
@@ -0,0 +1,1625 @@
+{-# LANGUAGE NamedFieldPuns #-}
+
+-- | A deterministic, pure observatory assembled from Moonlight's existing
+-- geometry, exact filtration, interpolation, and persistence owners.
+module Moonlight.Triangulation.Exhibit.PersistenceRose
+  ( PersistenceRoseError (..),
+    PersistenceRoseReceipt (..),
+    PersistenceRoseScene,
+    PersistenceRoseFrame,
+    PersistenceRoseAnimationReceipt (..),
+    AlphaEclipseReceipt (..),
+    AlphaEclipseScene,
+    buildPersistenceRoseScene,
+    buildAlphaEclipseScene,
+    persistenceRoseReceipt,
+    alphaEclipseReceipt,
+    renderPersistenceRoseReceipt,
+    renderAlphaEclipseReceipt,
+    renderPersistenceRosePoster,
+    renderAlphaEclipse,
+    persistenceRoseFrameSpecs,
+    renderPersistenceRoseFrame,
+    persistenceRoseAnimationReceipt,
+    renderPersistenceRoseAnimationReceipt,
+    stampPersistenceRoseGif,
+    verifyPersistenceRoseGif,
+  )
+where
+
+import Control.Monad (foldM)
+import Control.Monad.ST (runST)
+import Data.Bifunctor (first)
+import Data.Bits ((.&.), xor)
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as ByteString
+import Data.ByteString.Char8 qualified as ByteString.Char8
+import Data.List (find, intercalate, sortOn)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Maybe (mapMaybe)
+import Data.Ord (Down (..))
+import Data.Set qualified as Set
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as Text.Encoding
+import Data.Vector qualified as Vector
+import Data.Word (Word64, Word8)
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..),
+    HomologyFailure,
+    PersistencePair (..),
+  )
+import Moonlight.Homology.Persistence
+  ( criticalBettiVectors,
+    mod2PersistentPairsWithCriticalBettiTable,
+  )
+import Moonlight.Triangulation
+  ( AlphaBirth,
+    AlphaFiltration,
+    AlphaFiltrationError,
+    BuildError,
+    DelaunayTriangulation,
+    ExactLoop,
+    MinkowskiError,
+    OverlayError,
+    PlanarRegion,
+    Point (..),
+    PointValidationError,
+    PolygonComponent,
+    QueryPoint,
+    RegionPublicationError,
+    RegionValidationError,
+    alphaBirthDenominator,
+    alphaBirthNumerator,
+    alphaBirthToDouble,
+    alphaEdgeBirth,
+    alphaFaceBirth,
+    alphaFiltration,
+    alphaFiltrationCriticalBirths,
+    convexPolygon,
+    delaunayGeometry,
+    exactLoop,
+    exactLoopPoints,
+    exactPointFromPoint,
+    exactPointToEmbeddingCandidate,
+    innerFaces,
+    innerFaceVertexTriples,
+    mkQueryPoint,
+    overlayLayers,
+    overlaySelectedRegion,
+    planarLayer,
+    planarRegion,
+    planarRegionComponents,
+    polygonComponent,
+    polygonHoleLoops,
+    polygonOffset,
+    polygonOuterLoop,
+    queryPointValue,
+    structuringElement,
+    undirectedEdges,
+    undirectedEndpoints,
+    vertexPoint,
+    vertices,
+  )
+import Moonlight.Triangulation.CellComplex
+  ( DCELError,
+    filteredAlphaComplex,
+  )
+import Moonlight.Triangulation.FloodFillIterator
+  ( BoundaryObstruction,
+    FaceComponent,
+    boundaryLoopVertices,
+    componentBoundaryLoops,
+    faceComponentFaces,
+    faceComponentsBy,
+    outerFaceFloodFill,
+  )
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( FaceId (..),
+    UndirectedEdgeId,
+    VertexId,
+    normalizedDirected,
+  )
+import Moonlight.Triangulation.Interpolation
+  ( InterpolationStats (..),
+    NaturalNeighborResult (..),
+    naturalNeighborWeights,
+    newNaturalNeighborWorkspace,
+  )
+import Moonlight.Triangulation.Voronoi
+  ( VoronoiEdgeGeometry (..),
+    asDirectedVoronoiEdge,
+    voronoiEdgeGeometry,
+  )
+import Numeric (showFFloat, showHex)
+
+-- | Every boundary at which the exhibit may be refused. The renderer never
+-- invents a fallback geometry or topology state.
+data PersistenceRoseError
+  = RoseDelaunayBuildFailed !BuildError
+  | RoseAlphaFiltrationFailed !AlphaFiltrationError
+  | RoseCellComplexLoweringFailed !DCELError
+  | RosePersistenceFailed !HomologyFailure
+  | RoseCriticalProfileCountMismatch !Int !Int
+  | RoseCriticalProfileDegreeCountMismatch !Int !Int
+  | RoseBirthRankMissing !AlphaBirth
+  | RoseEdgeBirthMissing !UndirectedEdgeId
+  | RoseFaceBirthMissing !FaceId
+  | RoseBoundaryDescentFailed !Int !BoundaryObstruction
+  | RoseNoCriticalStates
+  | RoseNoMaskRegions
+  | RoseNoPersistentHole
+  | RoseNoCoherentHero !Int ![(Int, Int)]
+  | RoseVoronoiGeometryMissing !UndirectedEdgeId
+  | RoseNaturalNeighborFallbacks !Int
+  | RosePointInvalid !PointValidationError
+  | RoseRegionInvalid !RegionValidationError
+  | RoseOverlayFailed !(OverlayError Bool Bool)
+  | RoseOverlayPublicationFailed !RegionPublicationError
+  | RoseMinkowskiFailed !MinkowskiError
+  | RoseFrameCountTooSmall !Int
+  | RoseEclipseFiniteHoleCountMismatch !Int
+  | RoseEclipseFiniteHoleMissingDeath
+  | RoseEclipseBettiProfileMismatch !Int !Int !Int
+  | RoseGifInvalidSignature
+  | RoseGifTruncated !Int
+  | RoseGifUnexpectedBlock !Int !Word8
+  | RoseGifDimensionMismatch !Int !Int !Int !Int
+  | RoseGifFrameCountMismatch !Int !Int
+  | RoseGifFrameDelayCountMismatch !Int !Int
+  | RoseGifDurationMismatch !Int !Int
+  | RoseGifProvenanceMismatch
+  | RoseGifMarkerTooLong !Int
+  deriving stock (Show)
+
+-- | A compact, test-readable projection of everything the scene actually
+-- computed. It is intentionally not another geometry representation.
+data PersistenceRoseReceipt = PersistenceRoseReceipt
+  { roseSiteCount :: !Int,
+    roseEdgeCount :: !Int,
+    roseFaceCount :: !Int,
+    roseCriticalBirthCount :: !Int,
+    roseFinitePersistentHoleCount :: !Int,
+    roseEssentialPersistentHoleCount :: !Int,
+    roseHeroCriticalRank :: !Int,
+    roseHeroBettiOne :: !Int,
+    roseHeroGeometricHoleCount :: !Int,
+    roseVoronoiSegmentCount :: !Int,
+    roseNaturalNeighborSampleCount :: !Int,
+    roseNaturalNeighborFallbackCount :: !Int,
+    roseMaskComponentCount :: !Int
+  }
+  deriving stock (Eq, Show)
+
+data PersistenceRoseScene = PersistenceRoseScene
+  { sceneSites :: ![Point],
+    sceneEdges :: ![RankedSegment],
+    sceneFaces :: ![RankedTriangle],
+    sceneCriticalStates :: !(NonEmpty CriticalState),
+    sceneHeroState :: !CriticalState,
+    sceneVoronoiSegments :: ![Segment],
+    sceneFieldSamples :: ![FieldSample],
+    sceneMaskComponents :: ![MaskComponent],
+    scenePersistenceArcs :: ![PersistenceArc],
+    sceneReceipt :: !PersistenceRoseReceipt
+  }
+
+-- | The canonical source projection carried inside the retained GIF. The GIF
+-- codec remains an effect-boundary view; this receipt binds that binary view
+-- to the exact deterministic SVG frame section used to produce it.
+data PersistenceRoseAnimationReceipt = PersistenceRoseAnimationReceipt
+  { roseAnimationFrameCount :: !Int,
+    roseAnimationFramesPerSecond :: !Int,
+    roseAnimationWidth :: !Int,
+    roseAnimationHeight :: !Int,
+    roseAnimationSourceFingerprint :: !Word64
+  }
+  deriving stock (Eq, Show)
+
+-- | The exact, test-readable facts shown by the alpha eclipse. The numerator
+-- and denominator fields are projections of opaque 'AlphaBirth' values; the
+-- renderer never chooses thresholds independently of the filtration.
+data AlphaEclipseReceipt = AlphaEclipseReceipt
+  { eclipseSiteCount :: !Int,
+    eclipseEdgeCount :: !Int,
+    eclipseFaceCount :: !Int,
+    eclipseHoleBirthNumerator :: !Integer,
+    eclipseHoleBirthDenominator :: !Integer,
+    eclipseHoleDeathNumerator :: !Integer,
+    eclipseHoleDeathDenominator :: !Integer,
+    eclipseBettiBeforeBirth :: !Int,
+    eclipseBettiAtBirth :: !Int,
+    eclipseBettiAtDeath :: !Int
+  }
+  deriving stock (Eq, Show)
+
+-- | A three-section observation of one fixed resident Delaunay DCEL. Every
+-- displayed edge and face is projected from that DCEL with its exact birth
+-- rank; only the selected sublevel differs between sections.
+data AlphaEclipseScene = AlphaEclipseScene
+  { eclipseSites :: ![Point],
+    eclipseEdges :: ![RankedSegment],
+    eclipseFaces :: ![RankedTriangle],
+    eclipseBeforeState :: !CriticalState,
+    eclipseBirthState :: !CriticalState,
+    eclipseDeathState :: !CriticalState,
+    eclipseReceipt :: !AlphaEclipseReceipt
+  }
+
+-- | The shared local descent used by both exhibits. This is not a second
+-- filtration carrier: it retains the canonical mesh and filtration beside
+-- their checked Homology projection and only the ranked rendering views.
+data AlphaProjection = AlphaProjection
+  { projectionTriangulation :: !(DelaunayTriangulation ()),
+    projectionPersistencePairs :: ![PersistencePair AlphaBirth],
+    projectionCriticalStates :: !(NonEmpty CriticalState),
+    projectionBirthRanks :: !(Map AlphaBirth Int),
+    projectionEdges :: ![RankedSegment],
+    projectionFaces :: ![RankedTriangle]
+  }
+
+newtype PersistenceRoseFrame = PersistenceRoseFrame
+  { frameProgress :: Double
+  }
+  deriving stock (Eq, Show)
+
+data CriticalState = CriticalState
+  { criticalRank :: !Int,
+    criticalBirth :: !AlphaBirth,
+    criticalBettiOne :: !Int,
+    criticalHoles :: ![HoleComponent]
+  }
+
+data RankedSegment = RankedSegment
+  { rankedSegmentGeometry :: !Segment,
+    rankedSegmentBirthRank :: !Int
+  }
+
+data RankedTriangle = RankedTriangle
+  { rankedTrianglePoints :: !(Point, Point, Point),
+    rankedTriangleBirthRank :: !Int
+  }
+
+data Segment = Segment !Point !Point
+
+newtype HoleComponent = HoleComponent [[Point]]
+
+data FieldSample = FieldSample
+  { fieldSamplePoint :: !Point,
+    fieldSampleValue :: !Double,
+    fieldSampleNeighborCount :: !Int
+  }
+
+data MaskComponent = MaskComponent
+  { maskOuterLoop :: ![Point],
+    maskHoleLoops :: ![[Point]]
+  }
+
+data PersistenceArc = PersistenceArc
+  { persistenceArcBirthRank :: !Int,
+    persistenceArcDeathRank :: !Int
+  }
+
+buildPersistenceRoseScene :: Either PersistenceRoseError PersistenceRoseScene
+buildPersistenceRoseScene = do
+  projection <- buildAlphaProjection roseSites
+  let triangulation = projectionTriangulation projection
+      criticalStates = projectionCriticalStates projection
+      persistencePairs = projectionPersistencePairs projection
+      criticalBirthCount = NonEmpty.length criticalStates
+  heroState <- chooseHeroState criticalStates
+  finiteVoronoiSegments <- projectFiniteVoronoiSegments triangulation
+  (fieldSamples, fallbackCount) <- naturalNeighborField triangulation
+  if fallbackCount == 0
+    then pure ()
+    else Left (RoseNaturalNeighborFallbacks fallbackCount)
+  maskComponents <- buildRoseMask
+  persistenceArcs <-
+    projectPersistenceArcs
+      (projectionBirthRanks projection)
+      criticalBirthCount
+      persistencePairs
+  let finiteHoles = filter isFinitePositiveHole persistencePairs
+      essentialHoles = filter isEssentialHole persistencePairs
+      heroHoleCount = length (criticalHoles heroState)
+      receipt =
+        PersistenceRoseReceipt
+          { roseSiteCount = length (vertices triangulation),
+            roseEdgeCount = length (projectionEdges projection),
+            roseFaceCount = length (projectionFaces projection),
+            roseCriticalBirthCount = criticalBirthCount,
+            roseFinitePersistentHoleCount = length finiteHoles,
+            roseEssentialPersistentHoleCount = length essentialHoles,
+            roseHeroCriticalRank = criticalRank heroState,
+            roseHeroBettiOne = criticalBettiOne heroState,
+            roseHeroGeometricHoleCount = heroHoleCount,
+            roseVoronoiSegmentCount = length finiteVoronoiSegments,
+            roseNaturalNeighborSampleCount = length fieldSamples,
+            roseNaturalNeighborFallbackCount = fallbackCount,
+            roseMaskComponentCount = length maskComponents
+          }
+  pure
+    PersistenceRoseScene
+      { sceneSites = fmap (vertexPoint triangulation) (vertices triangulation),
+        sceneEdges = projectionEdges projection,
+        sceneFaces = projectionFaces projection,
+        sceneCriticalStates = criticalStates,
+        sceneHeroState = heroState,
+        sceneVoronoiSegments = finiteVoronoiSegments,
+        sceneFieldSamples = fieldSamples,
+        sceneMaskComponents = maskComponents,
+        scenePersistenceArcs = persistenceArcs,
+        sceneReceipt = receipt
+      }
+
+buildAlphaProjection :: [Point] -> Either PersistenceRoseError AlphaProjection
+buildAlphaProjection sites = do
+  triangulation <- first RoseDelaunayBuildFailed (delaunayGeometry (Vector.fromList sites))
+  filtration <- first RoseAlphaFiltrationFailed (alphaFiltration triangulation)
+  filteredComplex <- first RoseCellComplexLoweringFailed (filteredAlphaComplex filtration)
+  (persistencePairs, criticalBettiTable) <-
+    first RosePersistenceFailed
+      (mod2PersistentPairsWithCriticalBettiTable filteredComplex)
+  let criticalBirths = alphaFiltrationCriticalBirths filtration
+      criticalProfiles = criticalBettiVectors criticalBettiTable
+      birthRanks = Map.fromDistinctAscList (zip criticalBirths [0 ..])
+  if length criticalBirths == length criticalProfiles
+    then pure ()
+    else Left (RoseCriticalProfileCountMismatch (length criticalBirths) (length criticalProfiles))
+  criticalStates <-
+    traverse
+      (buildCriticalState triangulation filtration)
+      (zip3 [0 ..] criticalBirths criticalProfiles)
+      >>= maybe (Left RoseNoCriticalStates) Right . NonEmpty.nonEmpty
+  rankedEdges <-
+    traverse
+      (projectRankedEdge triangulation filtration birthRanks)
+      (undirectedEdges triangulation)
+  rankedFaces <-
+    traverse
+      (projectRankedFace triangulation filtration birthRanks)
+      (zip [1 ..] (Vector.toList (innerFaceVertexTriples triangulation)))
+  pure
+    AlphaProjection
+      { projectionTriangulation = triangulation,
+        projectionPersistencePairs = persistencePairs,
+        projectionCriticalStates = criticalStates,
+        projectionBirthRanks = birthRanks,
+        projectionEdges = rankedEdges,
+        projectionFaces = rankedFaces
+      }
+
+buildAlphaEclipseScene :: Either PersistenceRoseError AlphaEclipseScene
+buildAlphaEclipseScene = do
+  projection <- buildAlphaProjection alphaEclipseSites
+  finiteHoles <-
+    case filter isFinitePositiveHole (projectionPersistencePairs projection) of
+      [finiteHole] -> Right finiteHole
+      holes -> Left (RoseEclipseFiniteHoleCountMismatch (length holes))
+  deathBirth <-
+    maybe
+      (Left RoseEclipseFiniteHoleMissingDeath)
+      Right
+      (persistenceDeath finiteHoles)
+  let birth = persistenceBirth finiteHoles
+      birthRanks = projectionBirthRanks projection
+      states = projectionCriticalStates projection
+  birthRank <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+  deathRank <- maybe (Left (RoseBirthRankMissing deathBirth)) Right (Map.lookup deathBirth birthRanks)
+  let beforeState = criticalStateAt (max 0 (birthRank - 1)) states
+      birthState = criticalStateAt birthRank states
+      deathState = criticalStateAt deathRank states
+      bettiProfile =
+        ( criticalBettiOne beforeState,
+          criticalBettiOne birthState,
+          criticalBettiOne deathState
+        )
+  case bettiProfile of
+    (0, 1, 0) -> pure ()
+    (beforeBetti, birthBetti, deathBetti) ->
+      Left (RoseEclipseBettiProfileMismatch beforeBetti birthBetti deathBetti)
+  let triangulation = projectionTriangulation projection
+      receipt =
+        AlphaEclipseReceipt
+          { eclipseSiteCount = length (vertices triangulation),
+            eclipseEdgeCount = length (projectionEdges projection),
+            eclipseFaceCount = length (projectionFaces projection),
+            eclipseHoleBirthNumerator = alphaBirthNumerator birth,
+            eclipseHoleBirthDenominator = alphaBirthDenominator birth,
+            eclipseHoleDeathNumerator = alphaBirthNumerator deathBirth,
+            eclipseHoleDeathDenominator = alphaBirthDenominator deathBirth,
+            eclipseBettiBeforeBirth = criticalBettiOne beforeState,
+            eclipseBettiAtBirth = criticalBettiOne birthState,
+            eclipseBettiAtDeath = criticalBettiOne deathState
+          }
+  pure
+    AlphaEclipseScene
+      { eclipseSites = fmap (vertexPoint triangulation) (vertices triangulation),
+        eclipseEdges = projectionEdges projection,
+        eclipseFaces = projectionFaces projection,
+        eclipseBeforeState = beforeState,
+        eclipseBirthState = birthState,
+        eclipseDeathState = deathState,
+        eclipseReceipt = receipt
+      }
+
+persistenceRoseReceipt :: PersistenceRoseScene -> PersistenceRoseReceipt
+persistenceRoseReceipt = sceneReceipt
+
+alphaEclipseReceipt :: AlphaEclipseScene -> AlphaEclipseReceipt
+alphaEclipseReceipt = eclipseReceipt
+
+renderPersistenceRoseReceipt :: PersistenceRoseReceipt -> String
+renderPersistenceRoseReceipt receipt =
+  intercalate
+    " · "
+    [ "sites " <> show (roseSiteCount receipt),
+      "edges " <> show (roseEdgeCount receipt),
+      "faces " <> show (roseFaceCount receipt),
+      "critical births " <> show (roseCriticalBirthCount receipt),
+      "finite H1 intervals " <> show (roseFinitePersistentHoleCount receipt),
+      "essential H1 intervals " <> show (roseEssentialPersistentHoleCount receipt),
+      "hero rank " <> show (roseHeroCriticalRank receipt),
+      "beta1 " <> show (roseHeroBettiOne receipt),
+      "geometric holes " <> show (roseHeroGeometricHoleCount receipt),
+      "Voronoi segments " <> show (roseVoronoiSegmentCount receipt),
+      "Sibson samples " <> show (roseNaturalNeighborSampleCount receipt),
+      "fallbacks " <> show (roseNaturalNeighborFallbackCount receipt),
+      "mask components " <> show (roseMaskComponentCount receipt)
+    ]
+
+renderAlphaEclipseReceipt :: AlphaEclipseReceipt -> String
+renderAlphaEclipseReceipt receipt =
+  intercalate
+    " · "
+    [ "sites " <> show (eclipseSiteCount receipt),
+      "edges " <> show (eclipseEdgeCount receipt),
+      "faces " <> show (eclipseFaceCount receipt),
+      "H1 ["
+        <> renderExactRatio (eclipseHoleBirthNumerator receipt) (eclipseHoleBirthDenominator receipt)
+        <> ", "
+        <> renderExactRatio (eclipseHoleDeathNumerator receipt) (eclipseHoleDeathDenominator receipt)
+        <> ")",
+      "beta1 "
+        <> show
+          ( eclipseBettiBeforeBirth receipt,
+            eclipseBettiAtBirth receipt,
+            eclipseBettiAtDeath receipt
+          )
+    ]
+
+buildCriticalState
+  :: DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> (Int, AlphaBirth, [Int])
+  -> Either PersistenceRoseError CriticalState
+buildCriticalState triangulation filtration (rankValue, birthValue, profile) = do
+  bettiOne <-
+    case profile of
+      _ : secondDegree : _ : _ -> Right secondDegree
+      _ -> Left (RoseCriticalProfileDegreeCountMismatch rankValue (length profile))
+  holes <- holeComponentsAt rankValue triangulation filtration birthValue
+  pure
+    CriticalState
+      { criticalRank = rankValue,
+        criticalBirth = birthValue,
+        criticalBettiOne = bettiOne,
+        criticalHoles = holes
+      }
+
+chooseHeroState :: NonEmpty CriticalState -> Either PersistenceRoseError CriticalState
+chooseHeroState states =
+  let stateList = NonEmpty.toList states
+      maximumBettiOne = foldl' (\best state -> max best (criticalBettiOne state)) 0 stateList
+      maximalStates = filter ((== maximumBettiOne) . criticalBettiOne) stateList
+      agreements = fmap (\state -> (criticalBettiOne state, length (criticalHoles state))) maximalStates
+      coherentStates =
+        filter
+          (\state -> criticalBettiOne state > 0 && criticalBettiOne state == length (criticalHoles state))
+          stateList
+      sevenPetalStates = filter ((== rosePetalCount) . criticalBettiOne) coherentStates
+   in if maximumBettiOne <= 0
+        then Left RoseNoPersistentHole
+        else
+          case NonEmpty.nonEmpty sevenPetalStates of
+            Just preferredStates -> Right (latestCriticalState preferredStates)
+            Nothing ->
+              maybe
+                (Left (RoseNoCoherentHero maximumBettiOne agreements))
+                Right
+                (find (\state -> criticalBettiOne state == length (criticalHoles state)) maximalStates)
+
+latestCriticalState :: NonEmpty CriticalState -> CriticalState
+latestCriticalState (firstState :| remainingStates) =
+  foldl'
+    (\latest candidate -> if criticalRank candidate > criticalRank latest then candidate else latest)
+    firstState
+    remainingStates
+
+holeComponentsAt
+  :: Int
+  -> DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> AlphaBirth
+  -> Either PersistenceRoseError [HoleComponent]
+holeComponentsAt rankValue triangulation filtration threshold = do
+  inactiveEdges <-
+    inactiveHandlesAt
+      RoseEdgeBirthMissing
+      (alphaEdgeBirth filtration)
+      (undirectedEdges triangulation)
+      threshold
+  inactiveFaces <-
+    inactiveHandlesAt
+      RoseFaceBirthMissing
+      (alphaFaceBirth filtration)
+      (innerFaces triangulation)
+      threshold
+  let edgeInactive edge = Set.member edge inactiveEdges
+      faceInactive face = Set.member face inactiveFaces
+      exteriorFaces = Set.fromList (outerFaceFloodFill triangulation edgeInactive)
+      inactiveComponents =
+        fmap snd
+          ( faceComponentsBy
+              triangulation
+              (\face -> if faceInactive face then Just () else Nothing)
+              edgeInactive
+          )
+      boundedComponents =
+        filter
+          (all (`Set.notMember` exteriorFaces) . faceComponentFaces)
+          inactiveComponents
+  traverse
+    (projectHoleComponent rankValue triangulation)
+    boundedComponents
+
+inactiveHandlesAt
+  :: Ord handle
+  => (handle -> PersistenceRoseError)
+  -> (handle -> Maybe AlphaBirth)
+  -> [handle]
+  -> AlphaBirth
+  -> Either PersistenceRoseError (Set.Set handle)
+inactiveHandlesAt missingBirth birthAt handles threshold =
+  Set.fromList . mapMaybe id
+    <$> traverse
+      ( \handle ->
+          maybe
+            (Left (missingBirth handle))
+            (\birth -> Right (if birth > threshold then Just handle else Nothing))
+            (birthAt handle)
+      )
+      handles
+
+projectHoleComponent
+  :: Int
+  -> DelaunayTriangulation ()
+  -> FaceComponent
+  -> Either PersistenceRoseError HoleComponent
+projectHoleComponent rankValue triangulation component = do
+  loops <-
+    first (RoseBoundaryDescentFailed rankValue)
+      (componentBoundaryLoops triangulation component)
+  pure
+    ( HoleComponent
+        ( fmap
+            (fmap (vertexPoint triangulation) . NonEmpty.toList . boundaryLoopVertices)
+            (NonEmpty.toList loops)
+        )
+    )
+
+projectRankedEdge
+  :: DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> Map AlphaBirth Int
+  -> UndirectedEdgeId
+  -> Either PersistenceRoseError RankedSegment
+projectRankedEdge triangulation filtration birthRanks edge = do
+  birth <- maybe (Left (RoseEdgeBirthMissing edge)) Right (alphaEdgeBirth filtration edge)
+  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+  let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
+  pure
+    RankedSegment
+      { rankedSegmentGeometry = Segment (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex),
+        rankedSegmentBirthRank = rankValue
+      }
+
+projectRankedFace
+  :: DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> Map AlphaBirth Int
+  -> (Int, (VertexId, VertexId, VertexId))
+  -> Either PersistenceRoseError RankedTriangle
+projectRankedFace triangulation filtration birthRanks (rawFace, (firstVertex, secondVertex, thirdVertex)) = do
+  let face = toFaceId rawFace
+  birth <- maybe (Left (RoseFaceBirthMissing face)) Right (alphaFaceBirth filtration face)
+  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+  pure
+    RankedTriangle
+      { rankedTrianglePoints =
+          ( vertexPoint triangulation firstVertex,
+            vertexPoint triangulation secondVertex,
+            vertexPoint triangulation thirdVertex
+          ),
+        rankedTriangleBirthRank = rankValue
+      }
+
+toFaceId :: Int -> FaceId
+toFaceId = FaceId . fromIntegral
+
+projectFiniteVoronoiSegments
+  :: DelaunayTriangulation ()
+  -> Either PersistenceRoseError [Segment]
+projectFiniteVoronoiSegments triangulation =
+  fmap (mapMaybe id)
+    . traverse projectSegment
+    $ undirectedEdges triangulation
+ where
+  projectSegment :: UndirectedEdgeId -> Either PersistenceRoseError (Maybe Segment)
+  projectSegment edge =
+    case voronoiEdgeGeometry triangulation (asDirectedVoronoiEdge (normalizedDirected edge)) of
+      Nothing -> Left (RoseVoronoiGeometryMissing edge)
+      Just (VoronoiSegment fromPoint toPoint) -> Right (Just (Segment fromPoint toPoint))
+      Just (VoronoiRay _ _) -> Right Nothing
+      Just (VoronoiLine _ _) -> Right Nothing
+
+naturalNeighborField
+  :: DelaunayTriangulation ()
+  -> Either PersistenceRoseError ([FieldSample], Int)
+naturalNeighborField triangulation = do
+  admittedQueries <- traverse (first RosePointInvalid . mkQueryPoint) fieldQueryPoints
+  let results = runST $ do
+        workspace <- newNaturalNeighborWorkspace triangulation
+        traverse (naturalNeighborWeights workspace Nothing) admittedQueries
+      pairedResults = zip admittedQueries results
+      fallbackCount = length (filter (interpolationUsedFallback . naturalNeighborStats . snd) pairedResults)
+      samples = mapMaybe (fieldSampleFromResult triangulation) pairedResults
+  pure (samples, fallbackCount)
+
+fieldSampleFromResult
+  :: DelaunayTriangulation ()
+  -> (QueryPoint, NaturalNeighborResult)
+  -> Maybe FieldSample
+fieldSampleFromResult triangulation (query, result)
+  | Vector.null weights = Nothing
+  | interpolationUsedFallback (naturalNeighborStats result) = Nothing
+  | otherwise =
+      let weightedValue =
+            Vector.foldl'
+              (\total (vertex, weight) -> total + weight * siteSignal (vertexPoint triangulation vertex))
+              0
+              weights
+       in Just
+            FieldSample
+              { fieldSamplePoint = queryPointValue query,
+                fieldSampleValue = clamp 0 1 weightedValue,
+                fieldSampleNeighborCount = Vector.length weights
+              }
+ where
+  weights = naturalNeighborValues result
+
+siteSignal :: Point -> Double
+siteSignal (Point x y) =
+  clamp 0 1 (0.5 + 0.24 * sin (1.31 * x - 0.73 * y) + 0.18 * cos (0.61 * x + 1.17 * y))
+
+fieldQueryPoints :: [Point]
+fieldQueryPoints =
+  [ Point (fromIntegral column * 0.34 - 5.1) (fromIntegral row * 0.34 - 4.42)
+  | row <- [0 .. 26 :: Int],
+    column <- [0 .. 30 :: Int]
+  ]
+
+projectPersistenceArcs
+  :: Map AlphaBirth Int
+  -> Int
+  -> [PersistencePair AlphaBirth]
+  -> Either PersistenceRoseError [PersistenceArc]
+projectPersistenceArcs birthRanks criticalCount pairs = do
+  arcs <- traverse pairArc (filter isPositiveHole pairs)
+  pure (take 36 (sortOn (Down . persistenceArcLifetime) arcs))
+ where
+  finalRank = max 0 (criticalCount - 1)
+  pairArc pair = do
+    birthRank <- rankOf (persistenceBirth pair)
+    deathRank <- maybe (Right finalRank) rankOf (persistenceDeath pair)
+    pure
+      PersistenceArc
+        { persistenceArcBirthRank = birthRank,
+          persistenceArcDeathRank = deathRank
+        }
+  rankOf birth = maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+
+persistenceArcLifetime :: PersistenceArc -> Int
+persistenceArcLifetime PersistenceArc{persistenceArcBirthRank, persistenceArcDeathRank} =
+  max 0 (persistenceArcDeathRank - persistenceArcBirthRank)
+
+isPositiveHole :: PersistencePair AlphaBirth -> Bool
+isPositiveHole pair =
+  persistenceDegree pair == HomologicalDegree 1
+    && maybe True (> persistenceBirth pair) (persistenceDeath pair)
+
+isFinitePositiveHole :: PersistencePair AlphaBirth -> Bool
+isFinitePositiveHole pair = isPositiveHole pair && maybe False (> persistenceBirth pair) (persistenceDeath pair)
+
+isEssentialHole :: PersistencePair AlphaBirth -> Bool
+isEssentialHole pair = persistenceDegree pair == HomologicalDegree 1 && persistenceDeath pair == Nothing
+
+buildRoseMask :: Either PersistenceRoseError [MaskComponent]
+buildRoseMask = do
+  kernelPoints <- traverse (first RosePointInvalid . exactPointFromPoint) minkowskiKernel
+  kernel <- first RoseMinkowskiFailed (convexPolygon kernelPoints >>= structuringElement)
+  authoredRegions <- traverse authoredRegion roseMaskLoops
+  offsetRegions <-
+    traverse
+      (fmap fst . first RoseMinkowskiFailed . polygonOffset kernel)
+      authoredRegions
+  unioned <-
+    case NonEmpty.nonEmpty offsetRegions of
+      Nothing -> Left RoseNoMaskRegions
+      Just (firstRegion :| remainingRegions) -> foldM exactRegionUnion firstRegion remainingRegions
+  traverse projectMaskComponent (planarRegionComponents unioned)
+
+authoredRegion :: NonEmpty Point -> Either PersistenceRoseError PlanarRegion
+authoredRegion points = do
+  exactPoints <- traverse (first RosePointInvalid . exactPointFromPoint) points
+  loop <- first RoseRegionInvalid (exactLoop exactPoints)
+  component <- first RoseRegionInvalid (polygonComponent loop [])
+  first RoseRegionInvalid (planarRegion [component])
+
+exactRegionUnion :: PlanarRegion -> PlanarRegion -> Either PersistenceRoseError PlanarRegion
+exactRegionUnion leftRegion rightRegion = do
+  leftLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True leftRegion))
+  rightLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True rightRegion))
+  overlaid <- first RoseOverlayFailed (overlayLayers leftLayer rightLayer)
+  first RoseOverlayPublicationFailed (overlaySelectedRegion (uncurry (||)) overlaid)
+
+projectMaskComponent :: PolygonComponent -> Either PersistenceRoseError MaskComponent
+projectMaskComponent component =
+  MaskComponent
+    <$> projectExactLoop (polygonOuterLoop component)
+    <*> traverse projectExactLoop (polygonHoleLoops component)
+
+projectExactLoop :: ExactLoop -> Either PersistenceRoseError [Point]
+projectExactLoop =
+  traverse
+    (fmap queryPointValue . first RosePointInvalid . exactPointToEmbeddingCandidate)
+    . NonEmpty.toList
+    . exactLoopPoints
+
+roseSites :: [Point]
+roseSites =
+  concatMap petalSites [0 .. rosePetalCount - 1]
+    <> centerSites
+
+petalSites :: Int -> [Point]
+petalSites petalIndex =
+  fmap
+    (petalPoint petalIndex 2.52 1.82 0.69)
+    (sampleAngles 22 (0.021 * fromIntegral petalIndex))
+
+centerSites :: [Point]
+centerSites =
+  Point 0 0
+    : fmap
+      (\angle -> Point (0.56 * cos angle) (0.56 * sin angle))
+      (sampleAngles 14 0.037)
+
+roseMaskLoops :: [NonEmpty Point]
+roseMaskLoops =
+  fmap
+    (\petalIndex -> fmap (petalPoint petalIndex 2.42 2.12 0.9) (sampleAnglesNonEmpty 34 0))
+    [0 .. rosePetalCount - 1]
+    <> [fmap (\angle -> Point (1.08 * cos angle) (1.08 * sin angle)) (sampleAnglesNonEmpty 30 0)]
+
+petalPoint :: Int -> Double -> Double -> Double -> Double -> Point
+petalPoint petalIndex centerRadius radialRadius tangentialRadius localAngle =
+  let orientation = twoPi * fromIntegral petalIndex / fromIntegral rosePetalCount - pi / 2
+      radial = centerRadius + radialRadius * cos localAngle
+      tangential = tangentialRadius * sin localAngle * (0.91 + 0.09 * cos (2 * localAngle))
+      radialX = cos orientation
+      radialY = sin orientation
+      tangentX = -radialY
+      tangentY = radialX
+   in Point
+        (radial * radialX + tangential * tangentX)
+        (radial * radialY + tangential * tangentY)
+
+sampleAngles :: Int -> Double -> [Double]
+sampleAngles sampleCount phase =
+  fmap
+    (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
+    [0 .. sampleCount - 1]
+
+sampleAnglesNonEmpty :: Int -> Double -> NonEmpty Double
+sampleAnglesNonEmpty sampleCount phase =
+  phase
+    :| fmap
+      (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
+      [1 .. max 1 sampleCount - 1]
+
+minkowskiKernel :: NonEmpty Point
+minkowskiKernel =
+  Point 0.085 0
+    :| [ Point 0.043 0.074,
+         Point (-0.043) 0.074,
+         Point (-0.085) 0,
+         Point (-0.043) (-0.074),
+         Point 0.043 (-0.074)
+       ]
+
+rosePetalCount :: Int
+rosePetalCount = 7
+
+-- | The package's canonical exact persistence witness. Its four hull edges
+-- enter at squared radius one; the resident diagonal and two resident faces
+-- enter at squared radius two. Homology therefore observes exactly one finite
+-- degree-one interval @[1,2)@ over this fixed DCEL.
+alphaEclipseSites :: [Point]
+alphaEclipseSites =
+  [ Point (-1) (-1),
+    Point 1 (-1),
+    Point 1 1,
+    Point (-1) 1
+  ]
+
+renderPersistenceRosePoster :: PersistenceRoseScene -> String
+renderPersistenceRosePoster scene =
+  renderScene scene (criticalRank (sceneHeroState scene)) 1 True
+
+renderAlphaEclipse :: AlphaEclipseScene -> String
+renderAlphaEclipse scene =
+  let receipt = eclipseReceipt scene
+      birthLabel =
+        renderExactRatio
+          (eclipseHoleBirthNumerator receipt)
+          (eclipseHoleBirthDenominator receipt)
+      deathLabel =
+        renderExactRatio
+          (eclipseHoleDeathNumerator receipt)
+          (eclipseHoleDeathDenominator receipt)
+   in concat
+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 620\" width=\"1440\" height=\"620\" role=\"img\" aria-labelledby=\"title description\">\n",
+          "<title id=\"title\">Moonlight Triangulation alpha eclipse</title>\n",
+          "<desc id=\"description\">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius ",
+          birthLabel,
+          ", and its resident diagonal and faces kill that hole at squared radius ",
+          deathLabel,
+          ".</desc>\n",
+          renderAlphaEclipseDefinitions,
+          "<rect width=\"1440\" height=\"620\" fill=\"url(#eclipse-background)\"/>\n",
+          "<circle cx=\"720\" cy=\"300\" r=\"246\" fill=\"#ff68bb\" opacity=\"0.035\" filter=\"url(#eclipse-soft-glow)\"/>\n",
+          "<text x=\"58\" y=\"58\" fill=\"#84f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"17\" font-weight=\"600\" letter-spacing=\"6\">MOONLIGHT TRIANGULATION</text>\n",
+          "<text x=\"58\" y=\"101\" fill=\"#f7f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"38\" font-weight=\"420\">ALPHA ECLIPSE</text>\n",
+          "<text x=\"1382\" y=\"61\" text-anchor=\"end\" fill=\"#9baedb\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.5\">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>\n",
+          renderAlphaEclipseStage
+            scene
+            245
+            "BEFORE BIRTH"
+            "four isolated sites"
+            (eclipseBeforeState scene),
+          renderAlphaEclipseStage
+            scene
+            720
+            "LOOP BORN"
+            "resident boundary closes"
+            (eclipseBirthState scene),
+          renderAlphaEclipseStage
+            scene
+            1195
+            "CYCLE KILLED"
+            "resident faces fill it"
+            (eclipseDeathState scene),
+          "<g font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\">\n",
+          "<text x=\"58\" y=\"516\" fill=\"#9badd8\" font-size=\"13\" letter-spacing=\"2.5\">ONE EXACT H₁ INTERVAL</text>\n",
+          "<line x1=\"58\" y1=\"559\" x2=\"1382\" y2=\"559\" stroke=\"#344070\" stroke-width=\"3\"/>\n",
+          "<line x1=\"535\" y1=\"559\" x2=\"1042\" y2=\"559\" stroke=\"#ff72c6\" stroke-width=\"9\" stroke-linecap=\"round\"/>\n",
+          "<circle cx=\"535\" cy=\"559\" r=\"8\" fill=\"#fff6d6\" stroke=\"#ff72c6\" stroke-width=\"3\"/>\n",
+          "<rect x=\"1034\" y=\"551\" width=\"16\" height=\"16\" fill=\"#fff6d6\" stroke=\"#66efff\" stroke-width=\"3\"/>\n",
+          "<text x=\"58\" y=\"589\" fill=\"#7588ba\" font-size=\"12\">α² = 0</text>\n",
+          "<text x=\"535\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">birth α² = ",
+          birthLabel,
+          "</text>\n",
+          "<text x=\"788\" y=\"548\" text-anchor=\"middle\" fill=\"#fff6d6\" font-size=\"14\">the eclipse hole persists</text>\n",
+          "<text x=\"1042\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">death α² = ",
+          deathLabel,
+          "</text>\n",
+          "<text x=\"1382\" y=\"589\" text-anchor=\"end\" fill=\"#7588ba\" font-size=\"12\">exact α² increases →</text>\n",
+          "</g>\n",
+          "</svg>\n"
+        ]
+
+renderAlphaEclipseDefinitions :: String
+renderAlphaEclipseDefinitions =
+  concat
+    [ "<defs>\n",
+      "<radialGradient id=\"eclipse-background\" cx=\"50%\" cy=\"38%\" r=\"78%\"><stop offset=\"0\" stop-color=\"#111744\"/><stop offset=\"0.55\" stop-color=\"#070b25\"/><stop offset=\"1\" stop-color=\"#02040f\"/></radialGradient>\n",
+      "<linearGradient id=\"eclipse-corona\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#66efff\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#ffd86b\"/></linearGradient>\n",
+      "<linearGradient id=\"eclipse-face\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#7c4dff\"/><stop offset=\"0.5\" stop-color=\"#ff4fa3\"/><stop offset=\"1\" stop-color=\"#ffbf5b\"/></linearGradient>\n",
+      "<filter id=\"eclipse-soft-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "<filter id=\"eclipse-dot-glow\" x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "</defs>\n"
+    ]
+
+renderAlphaEclipseStage
+  :: AlphaEclipseScene
+  -> Double
+  -> String
+  -> String
+  -> CriticalState
+  -> String
+renderAlphaEclipseStage scene centerX stageLabel explanation state =
+  let rankValue = criticalRank state
+      activeFaces = filter ((<= rankValue) . rankedTriangleBirthRank) (eclipseFaces scene)
+      activeEdges = filter ((<= rankValue) . rankedSegmentBirthRank) (eclipseEdges scene)
+      highlighted = criticalBettiOne state > 0
+      frameStroke = if highlighted then "#ff6fbd" else "#344070"
+      frameOpacity = if highlighted then "0.72" else "0.55"
+   in concat
+        [ "<g transform=\"translate(",
+          formatDouble centerX,
+          " 292)\">\n",
+          "<rect x=\"-194\" y=\"-150\" width=\"388\" height=\"292\" rx=\"28\" fill=\"#090d26\" fill-opacity=\"0.72\" stroke=\"",
+          frameStroke,
+          "\" stroke-opacity=\"",
+          frameOpacity,
+          "\"/>\n",
+          "<text x=\"-162\" y=\"-114\" fill=\"",
+          if highlighted then "#ff89cb" else "#8fa4d6",
+          "\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"13\" letter-spacing=\"2.4\">",
+          stageLabel,
+          "</text>\n",
+          "<circle r=\"104\" fill=\"#01020a\" stroke=\"#7fefff\" stroke-opacity=\"0.18\" stroke-width=\"2\"/>\n",
+          "<g fill=\"url(#eclipse-face)\" fill-opacity=\"0.58\" stroke=\"#ffd56d\" stroke-opacity=\"0.48\" stroke-width=\"1.2\">",
+          concatMap renderAlphaEclipseTriangle activeFaces,
+          "</g>\n",
+          "<g fill=\"none\" stroke=\"url(#eclipse-corona)\" stroke-width=\"5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" filter=\"url(#eclipse-soft-glow)\">",
+          concatMap renderAlphaEclipseSegment activeEdges,
+          "</g>\n",
+          "<g fill=\"none\" stroke=\"#fff7da\" stroke-width=\"3.2\" stroke-linejoin=\"round\" opacity=\"0.86\">",
+          concatMap renderAlphaEclipseHole (criticalHoles state),
+          "</g>\n",
+          "<g fill=\"#fff8df\" stroke=\"#ff75c5\" stroke-width=\"2\" filter=\"url(#eclipse-dot-glow)\">",
+          concatMap renderAlphaEclipseSite (eclipseSites scene),
+          "</g>\n",
+          "<rect x=\"-126\" y=\"-18\" width=\"252\" height=\"36\" rx=\"18\" fill=\"#05071a\" fill-opacity=\"0.82\"/>\n",
+          "<text y=\"5\" text-anchor=\"middle\" fill=\"#fff7da\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.2\">",
+          explanation,
+          "</text>\n",
+          "<text x=\"-162\" y=\"121\" fill=\"#fff4cc\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"25\">β₁ = ",
+          show (criticalBettiOne state),
+          "</text>\n",
+          "<text x=\"162\" y=\"121\" text-anchor=\"end\" fill=\"#8495c4\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"12\">α² = ",
+          renderAlphaBirth (criticalBirth state),
+          "</text>\n",
+          "</g>\n"
+        ]
+
+renderAlphaEclipseTriangle :: RankedTriangle -> String
+renderAlphaEclipseTriangle RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint)} =
+  "<polygon points=\""
+    <> renderScreenPoints (fmap toAlphaEclipseLocal [firstPoint, secondPoint, thirdPoint])
+    <> "\"/>\n"
+
+renderAlphaEclipseSegment :: RankedSegment -> String
+renderAlphaEclipseSegment RankedSegment{rankedSegmentGeometry = Segment firstPoint secondPoint} =
+  let Point firstX firstY = toAlphaEclipseLocal firstPoint
+      Point secondX secondY = toAlphaEclipseLocal secondPoint
+   in "<line x1=\""
+        <> formatDouble firstX
+        <> "\" y1=\""
+        <> formatDouble firstY
+        <> "\" x2=\""
+        <> formatDouble secondX
+        <> "\" y2=\""
+        <> formatDouble secondY
+        <> "\"/>\n"
+
+renderAlphaEclipseHole :: HoleComponent -> String
+renderAlphaEclipseHole (HoleComponent loops) =
+  concatMap
+    (\points -> "<path d=\"" <> renderClosedPath (fmap toAlphaEclipseLocal points) <> "\"/>\n")
+    loops
+
+renderAlphaEclipseSite :: Point -> String
+renderAlphaEclipseSite point =
+  let Point x y = toAlphaEclipseLocal point
+   in "<circle cx=\""
+        <> formatDouble x
+        <> "\" cy=\""
+        <> formatDouble y
+        <> "\" r=\"6\"/>\n"
+
+toAlphaEclipseLocal :: Point -> Point
+toAlphaEclipseLocal (Point x y) =
+  let scale = 72
+      inverseSquareRootTwo = sqrt 0.5
+   in Point
+        (scale * inverseSquareRootTwo * (x - y))
+        (scale * inverseSquareRootTwo * (x + y))
+
+renderAlphaBirth :: AlphaBirth -> String
+renderAlphaBirth birth =
+  renderExactRatio (alphaBirthNumerator birth) (alphaBirthDenominator birth)
+
+renderExactRatio :: Integer -> Integer -> String
+renderExactRatio numerator denominator
+  | denominator == 1 = show numerator
+  | otherwise = show numerator <> "/" <> show denominator
+
+persistenceRoseFrameSpecs :: Int -> Either PersistenceRoseError [PersistenceRoseFrame]
+persistenceRoseFrameSpecs frameCount
+  | frameCount < 2 = Left (RoseFrameCountTooSmall frameCount)
+  | otherwise =
+      Right
+        ( fmap
+            (\frameIndex ->
+               PersistenceRoseFrame
+                 { frameProgress = frameProgressAt frameCount frameIndex
+                 })
+            [0 .. frameCount - 1]
+        )
+
+renderPersistenceRoseFrame :: PersistenceRoseScene -> PersistenceRoseFrame -> String
+renderPersistenceRoseFrame scene frame =
+  let finalRank = criticalRank (NonEmpty.last (sceneCriticalStates scene))
+      selectedRank = clampInt 0 finalRank (floor (frameProgress frame * fromIntegral finalRank))
+   in renderScene scene selectedRank (frameProgress frame) False
+
+persistenceRoseAnimationReceipt
+  :: PersistenceRoseScene
+  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
+persistenceRoseAnimationReceipt scene = do
+  frames <- persistenceRoseFrameSpecs canonicalAnimationFrameCount
+  let renderedFrames = fmap (renderPersistenceRoseFrame scene) frames
+  pure
+    PersistenceRoseAnimationReceipt
+      { roseAnimationFrameCount = canonicalAnimationFrameCount,
+        roseAnimationFramesPerSecond = canonicalAnimationFramesPerSecond,
+        roseAnimationWidth = canonicalAnimationWidth,
+        roseAnimationHeight = canonicalAnimationHeight,
+        roseAnimationSourceFingerprint = fingerprintSections renderedFrames
+      }
+
+renderPersistenceRoseAnimationReceipt :: PersistenceRoseAnimationReceipt -> String
+renderPersistenceRoseAnimationReceipt receipt =
+  intercalate
+    " "
+    [ "MOONLIGHT_PERSISTENCE_ROSE",
+      "frame-count=" <> show (roseAnimationFrameCount receipt),
+      "fps=" <> show (roseAnimationFramesPerSecond receipt),
+      "size=" <> show (roseAnimationWidth receipt) <> "x" <> show (roseAnimationHeight receipt),
+      "source-fnv1a64=" <> renderWord64Hex (roseAnimationSourceFingerprint receipt)
+    ]
+
+stampPersistenceRoseGif
+  :: PersistenceRoseScene
+  -> ByteString
+  -> Either PersistenceRoseError (PersistenceRoseAnimationReceipt, ByteString)
+stampPersistenceRoseGif scene gifBytes = do
+  receipt <- persistenceRoseAnimationReceipt scene
+  summary <- inspectGif gifBytes
+  validateGifSummary receipt summary
+  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
+      markerLength = ByteString.length marker
+  if markerLength > 255
+    then Left (RoseGifMarkerTooLong markerLength)
+    else
+      if marker `ByteString.isInfixOf` gifBytes
+        then Right (receipt, gifBytes)
+        else
+          if gifMarkerPrefix `ByteString.isInfixOf` gifBytes
+            then Left RoseGifProvenanceMismatch
+            else do
+              insertionOffset <- gifDataStart gifBytes
+              let (prefix, suffix) = ByteString.splitAt insertionOffset gifBytes
+                  commentExtension =
+                    ByteString.pack [0x21, 0xfe, fromIntegral markerLength]
+                      <> marker
+                      <> ByteString.singleton 0
+              pure (receipt, prefix <> commentExtension <> suffix)
+
+verifyPersistenceRoseGif
+  :: PersistenceRoseScene
+  -> ByteString
+  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
+verifyPersistenceRoseGif scene gifBytes = do
+  receipt <- persistenceRoseAnimationReceipt scene
+  summary <- inspectGif gifBytes
+  validateGifSummary receipt summary
+  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
+  if marker `ByteString.isInfixOf` gifBytes
+    then Right receipt
+    else Left RoseGifProvenanceMismatch
+
+data GifSummary = GifSummary
+  { gifSummaryWidth :: !Int,
+    gifSummaryHeight :: !Int,
+    gifSummaryFrameCount :: !Int,
+    gifSummaryFrameDelayCount :: !Int,
+    gifSummaryDurationCentiseconds :: !Int
+  }
+
+inspectGif :: ByteString -> Either PersistenceRoseError GifSummary
+inspectGif gifBytes = do
+  dataStart <- gifDataStart gifBytes
+  width <- gifLittleEndianWord16 gifBytes 6
+  height <- gifLittleEndianWord16 gifBytes 8
+  (frameCount, frameDelayCount, durationCentiseconds) <-
+    countGifFrames gifBytes dataStart 0 0 0
+  pure
+    GifSummary
+      { gifSummaryWidth = width,
+        gifSummaryHeight = height,
+        gifSummaryFrameCount = frameCount,
+        gifSummaryFrameDelayCount = frameDelayCount,
+        gifSummaryDurationCentiseconds = durationCentiseconds
+      }
+
+gifDataStart :: ByteString -> Either PersistenceRoseError Int
+gifDataStart gifBytes = do
+  signature <- maybe (Left (RoseGifTruncated 0)) Right (gifSlice gifBytes 0 6)
+  if signature == ByteString.Char8.pack "GIF87a" || signature == ByteString.Char8.pack "GIF89a"
+    then pure ()
+    else Left RoseGifInvalidSignature
+  packed <- maybe (Left (RoseGifTruncated 10)) Right (gifByteAt gifBytes 10)
+  let globalColourTableBytes =
+        if packed .&. 0x80 == 0
+          then 0
+          else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
+      dataStart = 13 + globalColourTableBytes
+  maybe (Left (RoseGifTruncated dataStart)) (const (Right dataStart)) (gifSlice gifBytes 0 dataStart)
+
+countGifFrames
+  :: ByteString
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Either PersistenceRoseError (Int, Int, Int)
+countGifFrames gifBytes offset frameCount frameDelayCount durationCentiseconds = do
+  block <- maybe (Left (RoseGifTruncated offset)) Right (gifByteAt gifBytes offset)
+  case block of
+    0x3b -> Right (frameCount, frameDelayCount, durationCentiseconds)
+    0x21 -> do
+      extensionLabel <- maybe (Left (RoseGifTruncated (offset + 1))) Right (gifByteAt gifBytes (offset + 1))
+      nextOffset <- skipGifSubBlocks gifBytes (offset + 2)
+      if extensionLabel == 0xf9
+        then do
+          delay <- gifLittleEndianWord16 gifBytes (offset + 4)
+          countGifFrames
+            gifBytes
+            nextOffset
+            frameCount
+            (frameDelayCount + 1)
+            (durationCentiseconds + delay)
+        else
+          countGifFrames
+            gifBytes
+            nextOffset
+            frameCount
+            frameDelayCount
+            durationCentiseconds
+    0x2c -> do
+      packed <- maybe (Left (RoseGifTruncated (offset + 9))) Right (gifByteAt gifBytes (offset + 9))
+      let localColourTableBytes =
+            if packed .&. 0x80 == 0
+              then 0
+              else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
+          imageDataStart = offset + 11 + localColourTableBytes
+      maybe (Left (RoseGifTruncated imageDataStart)) (const (Right ())) (gifByteAt gifBytes (imageDataStart - 1))
+      skipGifSubBlocks gifBytes imageDataStart >>= \next ->
+        countGifFrames
+          gifBytes
+          next
+          (frameCount + 1)
+          frameDelayCount
+          durationCentiseconds
+    0x00 ->
+      countGifFrames
+        gifBytes
+        (offset + 1)
+        frameCount
+        frameDelayCount
+        durationCentiseconds
+    unexpected -> Left (RoseGifUnexpectedBlock offset unexpected)
+
+skipGifSubBlocks :: ByteString -> Int -> Either PersistenceRoseError Int
+skipGifSubBlocks gifBytes offset = do
+  blockLength <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
+  if blockLength == 0
+    then Right (offset + 1)
+    else do
+      let nextOffset = offset + 1 + blockLength
+      maybe (Left (RoseGifTruncated nextOffset)) (const (Right ())) (gifSlice gifBytes offset (1 + blockLength))
+      skipGifSubBlocks gifBytes nextOffset
+
+validateGifSummary
+  :: PersistenceRoseAnimationReceipt
+  -> GifSummary
+  -> Either PersistenceRoseError ()
+validateGifSummary receipt summary
+  | actualWidth /= expectedWidth || actualHeight /= expectedHeight =
+      Left (RoseGifDimensionMismatch expectedWidth expectedHeight actualWidth actualHeight)
+  | actualFrameCount /= expectedFrameCount =
+      Left (RoseGifFrameCountMismatch expectedFrameCount actualFrameCount)
+  | actualFrameDelayCount /= expectedFrameCount =
+      Left (RoseGifFrameDelayCountMismatch expectedFrameCount actualFrameDelayCount)
+  | actualDurationCentiseconds /= canonicalAnimationDurationCentiseconds =
+      Left
+        ( RoseGifDurationMismatch
+            canonicalAnimationDurationCentiseconds
+            actualDurationCentiseconds
+        )
+  | otherwise = Right ()
+ where
+  expectedWidth = roseAnimationWidth receipt
+  expectedHeight = roseAnimationHeight receipt
+  actualWidth = gifSummaryWidth summary
+  actualHeight = gifSummaryHeight summary
+  expectedFrameCount = roseAnimationFrameCount receipt
+  actualFrameCount = gifSummaryFrameCount summary
+  actualFrameDelayCount = gifSummaryFrameDelayCount summary
+  actualDurationCentiseconds = gifSummaryDurationCentiseconds summary
+
+gifLittleEndianWord16 :: ByteString -> Int -> Either PersistenceRoseError Int
+gifLittleEndianWord16 gifBytes offset = do
+  low <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
+  high <- maybe (Left (RoseGifTruncated (offset + 1))) (Right . fromIntegral) (gifByteAt gifBytes (offset + 1))
+  pure (low + 256 * high)
+
+gifByteAt :: ByteString -> Int -> Maybe Word8
+gifByteAt gifBytes offset = fst <$> ByteString.uncons (ByteString.drop offset gifBytes)
+
+gifSlice :: ByteString -> Int -> Int -> Maybe ByteString
+gifSlice gifBytes offset width =
+  let selected = ByteString.take width (ByteString.drop offset gifBytes)
+   in if ByteString.length selected == width then Just selected else Nothing
+
+fingerprintSections :: [String] -> Word64
+fingerprintSections =
+  foldl'
+    (\fingerprint section -> fingerprintByte (fingerprintText fingerprint section) 0)
+    fnvOffsetBasis
+
+fingerprintText :: Word64 -> String -> Word64
+fingerprintText fingerprint =
+  ByteString.foldl' fingerprintByte fingerprint
+    . Text.Encoding.encodeUtf8
+    . Text.pack
+
+fingerprintByte :: Word64 -> Word8 -> Word64
+fingerprintByte fingerprint byte =
+  (fingerprint `xor` fromIntegral byte) * fnvPrime
+
+renderWord64Hex :: Word64 -> String
+renderWord64Hex value =
+  let rendered = showHex value ""
+   in replicate (16 - length rendered) '0' <> rendered
+
+gifMarkerPrefix :: ByteString
+gifMarkerPrefix = ByteString.Char8.pack "MOONLIGHT_PERSISTENCE_ROSE "
+
+canonicalAnimationFrameCount :: Int
+canonicalAnimationFrameCount = 65
+
+canonicalAnimationFramesPerSecond :: Int
+canonicalAnimationFramesPerSecond = 8
+
+canonicalAnimationWidth :: Int
+canonicalAnimationWidth = 960
+
+canonicalAnimationHeight :: Int
+canonicalAnimationHeight = 540
+
+canonicalAnimationDurationCentiseconds :: Int
+canonicalAnimationDurationCentiseconds = 813
+
+fnvOffsetBasis :: Word64
+fnvOffsetBasis = 14695981039346656037
+
+fnvPrime :: Word64
+fnvPrime = 1099511628211
+
+frameProgressAt :: Int -> Int -> Double
+frameProgressAt frameCount frameIndex
+  | frameIndex <= 0 || frameIndex >= frameCount - 1 = 0
+  | otherwise =
+      let phase = fromIntegral frameIndex / fromIntegral (frameCount - 1)
+       in 0.5 - 0.5 * cos (twoPi * phase)
+
+renderScene :: PersistenceRoseScene -> Int -> Double -> Bool -> String
+renderScene scene selectedRank progress poster =
+  let selectedState = criticalStateAt selectedRank (sceneCriticalStates scene)
+      finalRank = max 1 (criticalRank (NonEmpty.last (sceneCriticalStates scene)))
+      activation = if poster then fromIntegral selectedRank / fromIntegral finalRank else progress
+   in concat
+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 1080\" width=\"1920\" height=\"1080\" role=\"img\" aria-labelledby=\"title description\">\n",
+          "<title id=\"title\">Moonlight Triangulation Persistence Rose</title>\n",
+          "<desc id=\"description\">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>\n",
+          renderDefinitions (sceneMaskComponents scene),
+          "<rect width=\"1920\" height=\"1080\" fill=\"url(#background)\"/>\n",
+          renderAmbientOrbits activation,
+          renderPersistenceHalo finalRank selectedRank (scenePersistenceArcs scene),
+          "<g clip-path=\"url(#rose-clip)\">\n",
+          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"url(#rose-wash)\" opacity=\"0.28\"/>\n",
+          "<g filter=\"url(#field-soften)\">", concatMap renderFieldSample (sceneFieldSamples scene), "</g>\n",
+          "<g opacity=\"0.58\">", concatMap renderVoronoiSegment (sceneVoronoiSegments scene), "</g>\n",
+          concatMap (renderRankedTriangle finalRank activation) (sceneFaces scene),
+          concatMap (renderRankedSegment finalRank activation) (sceneEdges scene),
+          "<g filter=\"url(#hole-glow)\">", concatMap renderHoleComponent (criticalHoles selectedState), "</g>\n",
+          concatMap (renderSite activation) (sceneSites scene),
+          "</g>\n",
+          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"none\" stroke=\"url(#mask-stroke)\" stroke-width=\"2.2\" opacity=\"0.92\" filter=\"url(#edge-glow)\"/>\n",
+          renderTitle selectedState,
+          "</svg>\n"
+        ]
+
+criticalStateAt :: Int -> NonEmpty CriticalState -> CriticalState
+criticalStateAt target (firstState :| remainingStates) =
+  foldl'
+    (\chosen candidate -> if criticalRank candidate <= target then candidate else chosen)
+    firstState
+    remainingStates
+
+renderDefinitions :: [MaskComponent] -> String
+renderDefinitions maskComponents =
+  concat
+    [ "<defs>\n",
+      "<radialGradient id=\"background\" cx=\"52%\" cy=\"48%\" r=\"72%\"><stop offset=\"0\" stop-color=\"#10143d\"/><stop offset=\"0.48\" stop-color=\"#070b24\"/><stop offset=\"1\" stop-color=\"#02040e\"/></radialGradient>\n",
+      "<radialGradient id=\"rose-wash\" cx=\"48%\" cy=\"42%\" r=\"63%\"><stop offset=\"0\" stop-color=\"#ffe7a3\"/><stop offset=\"0.38\" stop-color=\"#ff5fb7\"/><stop offset=\"0.72\" stop-color=\"#7c4dff\"/><stop offset=\"1\" stop-color=\"#00d9ff\"/></radialGradient>\n",
+      "<linearGradient id=\"mask-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#ffd86b\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#66efff\"/></linearGradient>\n",
+      "<linearGradient id=\"hole-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#fff9db\"/><stop offset=\"0.52\" stop-color=\"#ff9ad7\"/><stop offset=\"1\" stop-color=\"#9ff7ff\"/></linearGradient>\n",
+      "<filter id=\"field-soften\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"24\"/></filter>\n",
+      "<filter id=\"hole-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "<filter id=\"edge-glow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "<clipPath id=\"rose-clip\"><path d=\"",
+      renderMaskPath maskComponents,
+      "\" fill-rule=\"evenodd\"/></clipPath>\n",
+      "</defs>\n"
+    ]
+
+renderAmbientOrbits :: Double -> String
+renderAmbientOrbits activation =
+  concatMap
+    ( \orbit ->
+        let radius = 322 + fromIntegral orbit * 34
+            opacity = 0.055 + activation * 0.025
+         in "<circle cx=\"960\" cy=\"552\" r=\""
+              <> formatDouble radius
+              <> "\" fill=\"none\" stroke=\"#8ba5ff\" stroke-width=\"0.8\" stroke-dasharray=\"2 12\" opacity=\""
+              <> formatDouble opacity
+              <> "\"/>\n"
+    )
+    [0 .. 5 :: Int]
+
+renderPersistenceHalo :: Int -> Int -> [PersistenceArc] -> String
+renderPersistenceHalo finalRank selectedRank arcs =
+  "<g fill=\"none\" stroke-linecap=\"round\">\n"
+    <> concatMap renderArc (zip ([0 :: Int ..]) arcs)
+    <> "</g>\n"
+ where
+  renderArc :: (Int, PersistenceArc) -> String
+  renderArc (index, arc) =
+    let active = persistenceArcBirthRank arc <= selectedRank && selectedRank < persistenceArcDeathRank arc
+        radius = 414 + fromIntegral (index `mod` 7) * 7.5
+        start = rankAngle finalRank (persistenceArcBirthRank arc)
+        finish = rankAngle finalRank (persistenceArcDeathRank arc)
+        points = arcPolyline radius start finish 42
+        hue = 188 + (index * 17 `mod` 122)
+        opacity = if active then 0.72 else 0.16
+        width = if active then 2.6 else 1.1
+     in "<polyline points=\""
+          <> renderScreenPoints points
+          <> "\" stroke=\"hsl("
+          <> show hue
+          <> " 92% 72%)\" stroke-width=\""
+          <> formatDouble width
+          <> "\" opacity=\""
+          <> formatDouble opacity
+          <> "\"/>\n"
+
+rankAngle :: Int -> Int -> Double
+rankAngle finalRank rankValue =
+  -pi / 2 + twoPi * fromIntegral rankValue / fromIntegral (max 1 finalRank)
+
+arcPolyline :: Double -> Double -> Double -> Int -> [Point]
+arcPolyline radius start finish sampleCount =
+  fmap
+    ( \sampleIndex ->
+        let fraction = fromIntegral sampleIndex / fromIntegral sampleCount
+            angle = start + fraction * (finish - start)
+         in screenPointFromPolar radius angle
+    )
+    [0 .. sampleCount]
+
+screenPointFromPolar :: Double -> Double -> Point
+screenPointFromPolar radius angle =
+  Point (960 + radius * cos angle) (552 + radius * sin angle)
+
+renderFieldSample :: FieldSample -> String
+renderFieldSample FieldSample{fieldSamplePoint, fieldSampleValue, fieldSampleNeighborCount} =
+  let Point x y = toScreen fieldSamplePoint
+      hue = round (186 + 126 * fieldSampleValue) :: Int
+      radius = 30 + min 5 (fromIntegral fieldSampleNeighborCount)
+      opacity = 0.16 + 0.16 * fieldSampleValue
+   in "<circle cx=\""
+        <> formatDouble x
+        <> "\" cy=\""
+        <> formatDouble y
+        <> "\" r=\""
+        <> formatDouble radius
+        <> "\" fill=\"hsl("
+        <> show hue
+        <> " 94% 61%)\" opacity=\""
+        <> formatDouble opacity
+        <> "\"/>\n"
+
+renderVoronoiSegment :: Segment -> String
+renderVoronoiSegment segment =
+  renderLine segment "#61efff" 1.15 0.5 ""
+
+renderRankedTriangle :: Int -> Double -> RankedTriangle -> String
+renderRankedTriangle finalRank activation RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint), rankedTriangleBirthRank} =
+  let opacity = activationOpacity finalRank activation rankedTriangleBirthRank
+      hue :: Int
+      hue = 282 - round (94 * fromIntegral rankedTriangleBirthRank / fromIntegral (max 1 finalRank) :: Double)
+   in if opacity <= 0.001
+        then ""
+        else
+          "<polygon points=\""
+            <> renderWorldPoints [firstPoint, secondPoint, thirdPoint]
+            <> "\" fill=\"hsl("
+            <> show hue
+            <> " 88% 60%)\" opacity=\""
+            <> formatDouble (0.06 + 0.17 * opacity)
+            <> "\"/>\n"
+
+renderRankedSegment :: Int -> Double -> RankedSegment -> String
+renderRankedSegment finalRank activation RankedSegment{rankedSegmentGeometry, rankedSegmentBirthRank} =
+  let opacity = activationOpacity finalRank activation rankedSegmentBirthRank
+      width = 0.75 + 1.25 * opacity
+      color = if opacity > 0.72 then "#ffd56a" else "#dd78ff"
+   in renderLine rankedSegmentGeometry color width (0.06 + 0.82 * opacity) "filter=\"url(#edge-glow)\""
+
+activationOpacity :: Int -> Double -> Int -> Double
+activationOpacity finalRank activation birthRank =
+  let level = activation * fromIntegral finalRank
+      distance = level - fromIntegral birthRank
+   in smoothstep (distance + 0.6)
+
+smoothstep :: Double -> Double
+smoothstep value =
+  let bounded = clamp 0 1 value
+   in bounded * bounded * (3 - 2 * bounded)
+
+renderHoleComponent :: HoleComponent -> String
+renderHoleComponent (HoleComponent loops) =
+  concatMap
+    ( \points ->
+        "<path d=\""
+          <> renderClosedWorldPath points
+          <> "\" fill=\"none\" stroke=\"url(#hole-stroke)\" stroke-width=\"5.4\" stroke-linejoin=\"round\" opacity=\"0.98\"/>\n"
+    )
+    loops
+
+renderSite :: Double -> Point -> String
+renderSite activation point =
+  let Point x y = toScreen point
+      radius = 2.5 + 1.5 * activation
+   in "<circle cx=\""
+        <> formatDouble x
+        <> "\" cy=\""
+        <> formatDouble y
+        <> "\" r=\""
+        <> formatDouble radius
+        <> "\" fill=\"#fff8d9\" stroke=\"#ff9ad7\" stroke-width=\"1.2\" opacity=\"0.95\"/>\n"
+
+renderLine :: Segment -> String -> Double -> Double -> String -> String
+renderLine (Segment fromPoint toPoint) color width opacity extraAttributes =
+  let Point fromX fromY = toScreen fromPoint
+      Point toX toY = toScreen toPoint
+   in "<line x1=\""
+        <> formatDouble fromX
+        <> "\" y1=\""
+        <> formatDouble fromY
+        <> "\" x2=\""
+        <> formatDouble toX
+        <> "\" y2=\""
+        <> formatDouble toY
+        <> "\" stroke=\""
+        <> color
+        <> "\" stroke-width=\""
+        <> formatDouble width
+        <> "\" stroke-linecap=\"round\" opacity=\""
+        <> formatDouble opacity
+        <> "\" "
+        <> extraAttributes
+        <> "/>\n"
+
+renderTitle :: CriticalState -> String
+renderTitle state =
+  let alphaFraction = exactFractionSummary (criticalBirth state)
+      alphaDecimal = formatDouble (alphaBirthToDouble (criticalBirth state))
+      bettiMeaning = bettiOneMeaning (criticalBettiOne state)
+   in concat
+        [ "<g font-family=\"Inter, Avenir Next, Helvetica, Arial, sans-serif\" fill=\"#f7f3ff\">\n",
+          "<text x=\"72\" y=\"82\" font-size=\"24\" font-weight=\"600\" letter-spacing=\"8\" fill=\"#9ff7ff\">MOONLIGHT TRIANGULATION</text>\n",
+          "<text x=\"70\" y=\"145\" font-size=\"58\" font-weight=\"780\" letter-spacing=\"-1.5\">PERSISTENCE ROSE</text>\n",
+          "<g transform=\"translate(1500 790)\">\n",
+          "<text x=\"0\" y=\"0\" font-size=\"18\" letter-spacing=\"2.4\" fill=\"#91a8d8\">TOPOLOGICAL HOLES AT THIS SCALE</text>\n",
+          "<text x=\"0\" y=\"74\" font-size=\"78\" font-weight=\"720\" fill=\"#fff5cd\">β₁ = ",
+          show (criticalBettiOne state),
+          "</text>\n",
+          "<text x=\"2\" y=\"116\" font-size=\"17\" fill=\"#c9d3ff\">",
+          bettiMeaning,
+          "</text>\n",
+          "<text x=\"2\" y=\"144\" font-size=\"15\" fill=\"#9ba8d4\">in the exact alpha complex</text>\n",
+          "<text x=\"2\" y=\"176\" font-size=\"14\" fill=\"#7682aa\">critical rank ",
+          show (criticalRank state),
+          " · α² = ",
+          alphaDecimal,
+          "</text>\n",
+          "<text x=\"2\" y=\"200\" font-size=\"12\" fill=\"#596486\">",
+          alphaFraction,
+          "</text>\n",
+          "</g>\n",
+          "</g>\n"
+        ]
+
+bettiOneMeaning :: Int -> String
+bettiOneMeaning holeCount =
+  case holeCount of
+    0 -> "no independent one-dimensional holes"
+    1 -> "1 independent one-dimensional hole"
+    _ -> show holeCount <> " independent one-dimensional holes"
+
+exactFractionSummary :: AlphaBirth -> String
+exactFractionSummary birth =
+  let numeratorDigits = length (show (abs (alphaBirthNumerator birth)))
+      denominatorDigits = length (show (alphaBirthDenominator birth))
+   in "exact " <> show numeratorDigits <> "-digit / " <> show denominatorDigits <> "-digit rational"
+
+renderMaskPath :: [MaskComponent] -> String
+renderMaskPath =
+  intercalate " "
+    . concatMap
+      (\MaskComponent{maskOuterLoop, maskHoleLoops} -> renderClosedScreenPath maskOuterLoop : fmap renderClosedScreenPath maskHoleLoops)
+
+renderClosedWorldPath :: [Point] -> String
+renderClosedWorldPath = renderClosedPath . fmap toScreen
+
+renderClosedScreenPath :: [Point] -> String
+renderClosedScreenPath = renderClosedPath . fmap toScreen
+
+renderClosedPath :: [Point] -> String
+renderClosedPath points =
+  case points of
+    [] -> ""
+    firstPoint : remainingPoints ->
+      "M " <> renderPoint firstPoint <> concatMap (" L " <>) (fmap renderPoint remainingPoints) <> " Z"
+
+renderWorldPoints :: [Point] -> String
+renderWorldPoints = renderScreenPoints . fmap toScreen
+
+renderScreenPoints :: [Point] -> String
+renderScreenPoints = unwords . fmap renderPoint
+
+renderPoint :: Point -> String
+renderPoint (Point x y) = formatDouble x <> "," <> formatDouble y
+
+toScreen :: Point -> Point
+toScreen (Point x y) = Point (960 + x * 101) (552 + y * 101)
+
+formatDouble :: Double -> String
+formatDouble value = showFFloat (Just 3) value ""
+
+clamp :: Double -> Double -> Double -> Double
+clamp lower upper = min upper . max lower
+
+clampInt :: Int -> Int -> Int -> Int
+clampInt lower upper = min upper . max lower
+
+twoPi :: Double
+twoPi = 2 * pi
diff --git a/docs/persistence-rose/README.md b/docs/persistence-rose/README.md
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/README.md
@@ -0,0 +1,110 @@
+# Moonlight persistence rose
+
+![Animated Moonlight persistence rose](./moonlight-triangulation-persistence-rose.gif)
+
+[Browse all exhibits.](../README.md)
+· [Open the animation directly.](./moonlight-triangulation-persistence-rose.gif)
+· [Open the static vector poster.](./moonlight-triangulation-persistence-rose.svg)
+· [Open the alpha eclipse.](./alpha-eclipse.md)
+· [Read the package overview.](../../README.md#persistence-rose)
+
+## Exact alpha filtration and persistent topology
+
+The full alpha path is a different observation of the same resident Delaunay
+DCEL. `alphaFiltration` computes exact squared-radius births once: vertices at
+zero, Gabriel edges at one quarter of their squared length, other edges at their
+least incident-face witness, and faces at their circumradius squared.
+`filteredAlphaComplex` lowers that section to the one Homology persistence
+owner. `AlphaBirth` is an opaque reduced rational; equality and ordering never
+round through `Double`.
+
+This is the planar alpha-complex shortcut to the Cech filtration: the Delaunay
+subcomplex has the same homotopy type as the corresponding union of discs, but
+has only the resident planar cells. One reduction therefore yields the barcode
+for every critical radius. `persistentBettiAtCriticalValues` sweeps all of
+those sublevels through dense complex-relative ranks without rebuilding
+geometry, reducing another boundary matrix, or rescanning the barcode per
+radius.
+
+The external component path uses the main facade for Delaunay construction and
+exact alpha births, then adds only the opt-in cell-complex lowering and
+Homology.
+
+```cabal
+build-depends:
+  base >= 4.22 && < 5,
+  moonlight-triangulation >= 1.4 && < 1.5,
+  moonlight-triangulation:cell-complex >= 1.4 && < 1.5,
+  moonlight-homology >= 0.1 && < 0.2,
+  vector >= 0.13 && < 0.14
+```
+
+```haskell
+import Data.Foldable (traverse_)
+import qualified Data.Vector as Vector
+import Moonlight.Homology.Persistence
+  ( filteredCriticalValues
+  , mod2PersistentPairs
+  , persistentBettiAtCriticalValues
+  )
+import Moonlight.Triangulation
+  ( Point (..)
+  , alphaFiltration
+  , delaunayGeometry
+  )
+import Moonlight.Triangulation.CellComplex (filteredAlphaComplex)
+
+main :: IO ()
+main = do
+  mesh <- requireRight (delaunayGeometry (Vector.fromList [Point (-1) (-1), Point 1 (-1), Point 1 1, Point (-1) 1]))
+  filtration <- requireRight (alphaFiltration mesh)
+  filtered <- requireRight (filteredAlphaComplex filtration)
+  pairs <- requireRight (mod2PersistentPairs filtered)
+  profiles <- requireRight (persistentBettiAtCriticalValues filtered pairs)
+  traverse_ print (zip (filteredCriticalValues filtered) profiles)
+
+requireRight :: Show failure => Either failure value -> IO value
+requireRight = either (fail . show) pure
+```
+
+The focused `moonlight-triangulation-alpha-bench` measures exact preparation,
+one persistence reduction plus all critical-radius queries, and repeated
+threshold homology as separate lanes over the same prebuilt Delaunay mesh.
+
+## Source SVG generation
+
+On GHC 9.14, the package-owned generator reproduces the poster or the canonical
+65-frame seamless SVG sweep:
+
+```console
+cabal run moonlight-triangulation-persistence-rose -- poster persistence-rose.svg
+cabal run moonlight-triangulation-persistence-rose -- frames 65 persistence-rose-frames
+```
+
+## Retained GIF reproduction
+
+The retained GIF is the 960×540, 8 fps projection of those 65 source frames.
+Its exact encoding boundary is deliberately outside geometry and Homology:
+rasterize the SVG section, encode it, then let the exhibit owner stamp the GIF
+with the deterministic source-frame fingerprint it computed itself.
+
+```console
+mkdir -p persistence-rose-png
+sips -s format png persistence-rose-frames/frame-*.svg --out persistence-rose-png
+ffmpeg -framerate 8 -start_number 0 -i persistence-rose-png/frame-%04d.png \
+  -vf "scale=960:-2:flags=lanczos,palettegen=max_colors=96:stats_mode=diff" \
+  -y persistence-rose-palette.png
+ffmpeg -framerate 8 -start_number 0 -i persistence-rose-png/frame-%04d.png \
+  -i persistence-rose-palette.png \
+  -lavfi "[0:v]scale=960:-2:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \
+  -loop 0 -y persistence-rose-raw.gif
+cabal run moonlight-triangulation-persistence-rose -- \
+  stamp-gif persistence-rose-raw.gif docs/persistence-rose/moonlight-triangulation-persistence-rose.gif
+cabal run moonlight-triangulation-persistence-rose -- verify-artifacts .
+```
+
+`verify-artifacts` compares both retained SVGs byte-for-byte with their pure
+renderers, checks the GIF dimensions, timing, and image-frame inventory, and
+requires the GIF's embedded source fingerprint to equal the freshly rendered
+canonical frame section. A plausible binary can no longer wander in wearing a
+nice hat.
diff --git a/docs/persistence-rose/alpha-eclipse.md b/docs/persistence-rose/alpha-eclipse.md
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/alpha-eclipse.md
@@ -0,0 +1,23 @@
+# Moonlight alpha eclipse
+
+![Alpha eclipse: a fixed Delaunay site set closes one hole and later fills it across exact critical radii](./moonlight-triangulation-alpha-eclipse.svg)
+
+[Open the full-size vector.](./moonlight-triangulation-alpha-eclipse.svg)
+· [Browse all exhibits.](../README.md)
+· [Read the exact-filtration API semantics.](./README.md#exact-alpha-filtration-and-persistent-topology)
+
+The eclipse uses the package's canonical four-site square witness and isolates
+its one finite `H₁` interval. The four sites, five resident Delaunay edges, and
+two resident faces remain fixed: the four hull edges close one hole at exact
+`α² = 1`; the resident diagonal and both resident faces enter at exact
+`α² = 2`, making that cycle a boundary. Only the alpha sublevel changes. Every
+visible cell is projected from the resident DCEL, and the `β₁ = 0 → 1 → 0`
+profile comes from Moonlight Homology rather than a hand-authored diagram.
+
+## Reproduce
+
+On GHC 9.14, the package-owned generator reproduces the retained vector:
+
+```console
+cabal run moonlight-triangulation-persistence-rose -- eclipse alpha-eclipse.svg
+```
diff --git a/docs/persistence-rose/moonlight-triangulation-alpha-eclipse.svg b/docs/persistence-rose/moonlight-triangulation-alpha-eclipse.svg
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/moonlight-triangulation-alpha-eclipse.svg
@@ -0,0 +1,91 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 620" width="1440" height="620" role="img" aria-labelledby="title description">
+<title id="title">Moonlight Triangulation alpha eclipse</title>
+<desc id="description">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius 1, and its resident diagonal and faces kill that hole at squared radius 2.</desc>
+<defs>
+<radialGradient id="eclipse-background" cx="50%" cy="38%" r="78%"><stop offset="0" stop-color="#111744"/><stop offset="0.55" stop-color="#070b25"/><stop offset="1" stop-color="#02040f"/></radialGradient>
+<linearGradient id="eclipse-corona" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#66efff"/><stop offset="0.48" stop-color="#ff72c6"/><stop offset="1" stop-color="#ffd86b"/></linearGradient>
+<linearGradient id="eclipse-face" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c4dff"/><stop offset="0.5" stop-color="#ff4fa3"/><stop offset="1" stop-color="#ffbf5b"/></linearGradient>
+<filter id="eclipse-soft-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="7" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+<filter id="eclipse-dot-glow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+</defs>
+<rect width="1440" height="620" fill="url(#eclipse-background)"/>
+<circle cx="720" cy="300" r="246" fill="#ff68bb" opacity="0.035" filter="url(#eclipse-soft-glow)"/>
+<text x="58" y="58" fill="#84f3ff" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="17" font-weight="600" letter-spacing="6">MOONLIGHT TRIANGULATION</text>
+<text x="58" y="101" fill="#f7f3ff" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="38" font-weight="420">ALPHA ECLIPSE</text>
+<text x="1382" y="61" text-anchor="end" fill="#9baedb" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.5">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>
+<g transform="translate(245.000 292)">
+<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#344070" stroke-opacity="0.55"/>
+<text x="-162" y="-114" fill="#8fa4d6" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">BEFORE BIRTH</text>
+<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>
+<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"></g>
+<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"></g>
+<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"></g>
+<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>
+<circle cx="-101.823" cy="0.000" r="6"/>
+<circle cx="101.823" cy="0.000" r="6"/>
+<circle cx="0.000" cy="101.823" r="6"/>
+</g>
+<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>
+<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">four isolated sites</text>
+<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 0</text>
+<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 0</text>
+</g>
+<g transform="translate(720.000 292)">
+<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#ff6fbd" stroke-opacity="0.72"/>
+<text x="-162" y="-114" fill="#ff89cb" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">LOOP BORN</text>
+<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>
+<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"></g>
+<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"><line x1="0.000" y1="-101.823" x2="-101.823" y2="0.000"/>
+<line x1="0.000" y1="-101.823" x2="101.823" y2="0.000"/>
+<line x1="-101.823" y1="0.000" x2="0.000" y2="101.823"/>
+<line x1="101.823" y1="0.000" x2="0.000" y2="101.823"/>
+</g>
+<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"><path d="M -101.823,0.000 L 0.000,-101.823 L 101.823,0.000 L 0.000,101.823 Z"/>
+</g>
+<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>
+<circle cx="-101.823" cy="0.000" r="6"/>
+<circle cx="101.823" cy="0.000" r="6"/>
+<circle cx="0.000" cy="101.823" r="6"/>
+</g>
+<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>
+<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">resident boundary closes</text>
+<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 1</text>
+<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 1</text>
+</g>
+<g transform="translate(1195.000 292)">
+<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#344070" stroke-opacity="0.55"/>
+<text x="-162" y="-114" fill="#8fa4d6" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">CYCLE KILLED</text>
+<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>
+<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"><polygon points="0.000,-101.823 0.000,101.823 -101.823,0.000"/>
+<polygon points="0.000,101.823 0.000,-101.823 101.823,0.000"/>
+</g>
+<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"><line x1="0.000" y1="-101.823" x2="-101.823" y2="0.000"/>
+<line x1="0.000" y1="101.823" x2="0.000" y2="-101.823"/>
+<line x1="0.000" y1="-101.823" x2="101.823" y2="0.000"/>
+<line x1="-101.823" y1="0.000" x2="0.000" y2="101.823"/>
+<line x1="101.823" y1="0.000" x2="0.000" y2="101.823"/>
+</g>
+<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"></g>
+<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>
+<circle cx="-101.823" cy="0.000" r="6"/>
+<circle cx="101.823" cy="0.000" r="6"/>
+<circle cx="0.000" cy="101.823" r="6"/>
+</g>
+<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>
+<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">resident faces fill it</text>
+<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 0</text>
+<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 2</text>
+</g>
+<g font-family="Inter,ui-sans-serif,system-ui,sans-serif">
+<text x="58" y="516" fill="#9badd8" font-size="13" letter-spacing="2.5">ONE EXACT H₁ INTERVAL</text>
+<line x1="58" y1="559" x2="1382" y2="559" stroke="#344070" stroke-width="3"/>
+<line x1="535" y1="559" x2="1042" y2="559" stroke="#ff72c6" stroke-width="9" stroke-linecap="round"/>
+<circle cx="535" cy="559" r="8" fill="#fff6d6" stroke="#ff72c6" stroke-width="3"/>
+<rect x="1034" y="551" width="16" height="16" fill="#fff6d6" stroke="#66efff" stroke-width="3"/>
+<text x="58" y="589" fill="#7588ba" font-size="12">α² = 0</text>
+<text x="535" y="589" text-anchor="middle" fill="#f7f3ff" font-size="12">birth α² = 1</text>
+<text x="788" y="548" text-anchor="middle" fill="#fff6d6" font-size="14">the eclipse hole persists</text>
+<text x="1042" y="589" text-anchor="middle" fill="#f7f3ff" font-size="12">death α² = 2</text>
+<text x="1382" y="589" text-anchor="end" fill="#7588ba" font-size="12">exact α² increases →</text>
+</g>
+</svg>
diff --git a/docs/persistence-rose/moonlight-triangulation-persistence-rose.gif b/docs/persistence-rose/moonlight-triangulation-persistence-rose.gif
new file mode 100644
Binary files /dev/null and b/docs/persistence-rose/moonlight-triangulation-persistence-rose.gif differ
diff --git a/docs/persistence-rose/moonlight-triangulation-persistence-rose.svg b/docs/persistence-rose/moonlight-triangulation-persistence-rose.svg
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/moonlight-triangulation-persistence-rose.svg
@@ -0,0 +1,1895 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" width="1920" height="1080" role="img" aria-labelledby="title description">
+<title id="title">Moonlight Triangulation Persistence Rose</title>
+<desc id="description">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>
+<defs>
+<radialGradient id="background" cx="52%" cy="48%" r="72%"><stop offset="0" stop-color="#10143d"/><stop offset="0.48" stop-color="#070b24"/><stop offset="1" stop-color="#02040e"/></radialGradient>
+<radialGradient id="rose-wash" cx="48%" cy="42%" r="63%"><stop offset="0" stop-color="#ffe7a3"/><stop offset="0.38" stop-color="#ff5fb7"/><stop offset="0.72" stop-color="#7c4dff"/><stop offset="1" stop-color="#00d9ff"/></radialGradient>
+<linearGradient id="mask-stroke" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffd86b"/><stop offset="0.48" stop-color="#ff72c6"/><stop offset="1" stop-color="#66efff"/></linearGradient>
+<linearGradient id="hole-stroke" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff9db"/><stop offset="0.52" stop-color="#ff9ad7"/><stop offset="1" stop-color="#9ff7ff"/></linearGradient>
+<filter id="field-soften" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="24"/></filter>
+<filter id="hole-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="7" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+<filter id="edge-glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+<clipPath id="rose-clip"><path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill-rule="evenodd"/></clipPath>
+</defs>
+<rect width="1920" height="1080" fill="url(#background)"/>
+<circle cx="960" cy="552" r="322.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
+<circle cx="960" cy="552" r="356.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
+<circle cx="960" cy="552" r="390.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
+<circle cx="960" cy="552" r="424.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
+<circle cx="960" cy="552" r="458.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
+<circle cx="960" cy="552" r="492.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>
+<g fill="none" stroke-linecap="round">
+<polyline points="1077.000,949.123 1051.592,955.741 1025.816,960.735 999.775,964.085 973.574,965.777 947.318,965.806 921.113,964.170 895.065,960.876 869.278,955.937 843.856,949.374 818.900,941.213 794.513,931.486 770.791,920.233 747.830,907.499 725.722,893.336 704.557,877.799 684.419,860.952 665.389,842.862 647.545,823.602 630.957,803.250 615.692,781.888 601.813,759.600 589.374,736.478 578.426,712.614 569.012,688.104 561.171,663.046 554.934,637.542 550.326,611.694 547.366,585.606 546.066,559.382 546.430,533.129 548.458,506.952 552.141,480.956 557.465,455.245 564.407,429.924 572.941,405.094 583.032,380.854 594.638,357.303 607.714,334.536 622.207,312.642 638.059,291.712 655.206,271.828 673.578,253.071" stroke="hsl(188 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="1232.734,230.630 1252.266,248.285 1270.657,267.124 1287.837,287.075 1303.737,308.059 1318.296,329.995 1331.457,352.798 1343.169,376.377 1353.387,400.642 1362.069,425.497 1369.182,450.846 1374.699,476.589 1378.598,502.627 1380.864,528.857 1381.488,555.178 1380.467,581.486 1377.806,607.679 1373.515,633.655 1367.610,659.313 1360.115,684.551 1351.059,709.273 1340.478,733.381 1328.411,756.781 1314.908,779.382 1300.020,801.096 1283.805,821.839 1266.326,841.528 1247.653,860.088 1227.857,877.446 1207.016,893.534 1185.212,908.289 1162.529,921.655 1139.055,933.578 1114.883,944.012 1090.107,952.917 1064.823,960.258 1039.130,966.006 1013.128,970.138 986.919,972.640 960.605,973.500 934.289,972.715 908.073,970.289 882.060,966.231" stroke="hsl(205 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1035.085,974.378 1008.772,978.219 982.273,980.421 955.687,980.978 929.118,979.887 902.668,977.152 876.438,972.783 850.530,966.798 825.041,959.219 800.072,950.075 775.716,939.402 752.069,927.241 729.221,913.638 707.259,898.645 686.268,882.321 666.329,864.728 647.519,845.933 629.908,826.008 613.567,805.031 598.556,783.082 584.933,760.245 572.752,736.608 562.059,712.262 552.894,687.300 545.294,661.818 539.287,635.914 534.896,609.688 532.139,583.240 531.025,556.672 531.560,530.086 533.741,503.585 537.559,477.269 543.001,451.240 550.044,425.599 558.663,400.443 568.823,375.870 580.487,351.973 593.608,328.845 608.138,306.574 624.019,285.246 641.191,264.943 659.588,245.743 679.139,227.719" stroke="hsl(222 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="1040.714,980.973 1014.190,985.123 987.460,987.635 960.627,988.500 933.791,987.712 907.055,985.277 880.518,981.203 854.283,975.505 828.447,968.204 803.109,959.330 778.365,948.914 754.307,936.997 731.028,923.624 708.615,908.844 687.153,892.715 666.723,875.297 647.403,856.656 629.265,836.862 612.378,815.991 596.806,794.121 582.608,771.335 569.838,747.719 558.544,723.363 548.769,698.359 540.549,672.801 533.916,646.786 528.894,620.413 525.504,593.780 523.757,566.990 523.661,540.143 525.215,513.341 528.414,486.685 533.246,460.276 539.692,434.214 547.728,408.598 557.324,383.524 568.443,359.087 581.043,335.381 595.077,312.493 610.491,290.512 627.228,269.520 645.223,249.597 664.409,230.817" stroke="hsl(239 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="1076.891,980.337 1050.961,986.583 1024.703,991.260 998.212,994.353 971.583,995.849 944.912,995.744 918.295,994.037 891.829,990.735 865.609,985.851 839.730,979.400 814.284,971.408 789.365,961.902 765.061,950.917 741.460,938.492 718.648,924.673 696.707,909.509 675.716,893.056 655.751,875.371 636.884,856.519 619.183,836.569 602.711,815.592 587.529,793.664 573.691,770.863 561.247,747.273 550.241,722.979 540.714,698.067 532.701,672.628 526.229,646.754 521.322,620.538 517.998,594.075 516.269,567.460 516.142,540.789 517.616,514.159 520.686,487.665 525.341,461.403 531.565,435.468 539.335,409.954 548.623,384.952 559.395,360.553 571.613,336.845 585.232,313.913 600.204,291.841 616.474,270.707" stroke="hsl(256 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1052.392,993.946 1026.117,998.633 999.611,1001.759 972.967,1003.314 946.277,1003.291 919.635,1001.692 893.135,998.521 866.868,993.790 840.926,987.515 815.401,979.719 790.381,970.428 765.954,959.674 742.204,947.496 719.216,933.936 697.069,919.042 675.841,902.864 655.606,885.461 636.434,866.893 618.393,847.224 601.546,826.523 585.952,804.864 571.664,782.320 558.734,758.972 547.205,734.901 537.119,710.190 528.511,684.927 521.411,659.199 515.843,633.097 511.827,606.711 509.377,580.134 508.502,553.459 509.205,526.779 511.483,500.186 515.328,473.775 520.727,447.637 527.661,421.864 536.106,396.546 546.032,371.771 557.404,347.625 570.184,324.194 584.325,301.559 599.780,279.799 616.493,258.990" stroke="hsl(273 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="1071.913,997.148 1045.954,1002.880 1019.707,1007.100 993.260,1009.793 966.702,1010.951 940.120,1010.569 913.606,1008.649 887.247,1005.198 861.132,1000.226 835.349,993.750 809.984,985.793 785.123,976.381 760.847,965.545 737.240,953.322 714.380,939.752 692.344,924.883 671.206,908.762 651.036,891.444 631.903,872.988 613.871,853.456 596.999,832.912 581.345,811.426 566.962,789.069 553.896,765.918 542.193,742.048 531.891,717.542 523.026,692.480 515.626,666.947 509.717,641.028 505.318,614.810 502.444,588.382 501.105,561.832 501.306,535.249 503.045,508.722 506.317,482.341 511.110,456.192 517.410,430.366 525.194,404.947 534.436,380.022 545.106,355.673 557.168,331.983 570.581,309.031 585.300,286.894" stroke="hsl(290 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1052.848,955.454 1029.335,960.153 1005.590,963.482 981.692,965.431 957.721,965.994 933.757,965.167 909.882,962.955 886.175,959.365 862.715,954.407 839.582,948.100 816.853,940.465 794.604,931.526 772.909,921.314 751.843,909.864 731.474,897.213 711.872,883.404 693.102,868.483 675.228,852.501 658.309,835.511 642.402,817.570 627.560,798.738 613.833,779.078 601.267,758.657 589.905,737.542 579.784,715.805 570.939,693.519 563.399,670.758 557.189,647.598 552.330,624.118 548.838,600.396 546.726,576.512 546.000,552.545 546.663,528.577 548.712,504.687 552.141,480.956 556.938,457.463 563.087,434.287 570.568,411.506 579.354,389.197 589.418,367.433 600.725,346.289 613.236,325.834 626.911,306.139" stroke="hsl(307 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1245.433,241.855 1255.448,251.380 1265.146,261.228 1274.515,271.389 1283.546,281.852 1292.229,292.605 1300.555,303.637 1308.515,314.936 1316.100,326.490 1323.303,338.286 1330.114,350.313 1336.528,362.556 1342.536,375.003 1348.134,387.640 1353.314,400.454 1358.071,413.430 1362.400,426.556 1366.297,439.817 1369.757,453.198 1372.776,466.686 1375.351,480.265 1377.479,493.921 1379.159,507.640 1380.388,521.407 1381.165,535.206 1381.489,549.024 1381.360,562.844 1380.778,576.654 1379.744,590.436 1378.258,604.177 1376.322,617.862 1373.939,631.477 1371.111,645.006 1367.841,658.434 1364.132,671.749 1359.988,684.934 1355.415,697.977 1350.416,710.863 1344.998,723.578 1339.165,736.108 1332.925,748.441 1326.284,760.562 1319.249,772.459" stroke="hsl(202 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1302.579,293.774 1309.377,303.049 1315.925,312.504 1322.217,322.130 1328.248,331.922 1334.015,341.872 1339.513,351.973 1344.739,362.218 1349.687,372.599 1354.356,383.109 1358.742,393.740 1362.841,404.486 1366.650,415.337 1370.167,426.286 1373.389,437.326 1376.315,448.448 1378.941,459.645 1381.266,470.907 1383.288,482.229 1385.006,493.600 1386.419,505.013 1387.525,516.460 1388.324,527.933 1388.816,539.423 1388.999,550.922 1388.873,562.422 1388.440,573.914 1387.699,585.390 1386.650,596.843 1385.295,608.263 1383.634,619.643 1381.668,630.974 1379.400,642.248 1376.830,653.458 1373.961,664.595 1370.794,675.651 1367.332,686.617 1363.577,697.488 1359.532,708.253 1355.200,718.907 1350.584,729.440 1345.688,739.846 1340.514,750.117" stroke="hsl(219 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1313.780,296.320 1320.034,305.205 1326.065,314.243 1331.870,323.428 1337.444,332.755 1342.784,342.218 1347.887,351.811 1352.750,361.528 1357.369,371.363 1361.743,381.310 1365.867,391.362 1369.739,401.514 1373.358,411.760 1376.721,422.092 1379.825,432.505 1382.669,442.992 1385.252,453.546 1387.571,464.161 1389.624,474.831 1391.412,485.549 1392.933,496.307 1394.185,507.101 1395.168,517.922 1395.881,528.764 1396.324,539.621 1396.497,550.485 1396.400,561.350 1396.032,572.210 1395.394,583.057 1394.486,593.884 1393.309,604.686 1391.863,615.455 1390.150,626.185 1388.170,636.869 1385.925,647.500 1383.416,658.072 1380.644,668.578 1377.612,679.012 1374.322,689.368 1370.774,699.638 1366.972,709.817 1362.918,719.898 1358.614,729.875" stroke="hsl(236 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1356.866,352.918 1360.834,361.033 1364.636,369.227 1368.270,377.497 1371.736,385.839 1375.031,394.250 1378.155,402.726 1381.105,411.264 1383.881,419.860 1386.481,428.511 1388.905,437.213 1391.152,445.962 1393.220,454.756 1395.109,463.589 1396.817,472.459 1398.345,481.362 1399.691,490.295 1400.856,499.253 1401.838,508.233 1402.637,517.230 1403.252,526.243 1403.685,535.266 1403.933,544.295 1403.998,553.328 1403.879,562.361 1403.576,571.389 1403.090,580.409 1402.421,589.418 1401.568,598.411 1400.532,607.384 1399.314,616.335 1397.914,625.259 1396.333,634.153 1394.572,643.013 1392.630,651.835 1390.510,660.616 1388.211,669.352 1385.735,678.039 1383.083,686.674 1380.255,695.254 1377.254,703.774 1374.080,712.231 1370.734,720.622" stroke="hsl(253 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1343.627,313.919 1348.226,321.494 1352.675,329.158 1356.973,336.907 1361.118,344.740 1365.108,352.652 1368.943,360.641 1372.619,368.704 1376.137,376.837 1379.495,385.038 1382.691,393.304 1385.724,401.630 1388.593,410.014 1391.298,418.453 1393.835,426.944 1396.206,435.482 1398.409,444.066 1400.443,452.691 1402.307,461.354 1404.001,470.053 1405.524,478.783 1406.875,487.541 1408.054,496.324 1409.060,505.128 1409.894,513.950 1410.554,522.787 1411.041,531.636 1411.353,540.492 1411.492,549.352 1411.457,558.214 1411.248,567.073 1410.866,575.926 1410.309,584.771 1409.579,593.602 1408.676,602.418 1407.600,611.214 1406.352,619.987 1404.932,628.734 1403.340,637.452 1401.577,646.136 1399.645,654.785 1397.543,663.394 1395.272,671.959" stroke="hsl(270 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1374.328,354.482 1377.670,361.652 1380.889,368.878 1383.982,376.158 1386.949,383.491 1389.790,390.873 1392.503,398.304 1395.087,405.780 1397.542,413.299 1399.867,420.860 1402.062,428.460 1404.125,436.096 1406.057,443.767 1407.856,451.470 1409.522,459.203 1411.054,466.963 1412.453,474.749 1413.717,482.558 1414.846,490.387 1415.840,498.234 1416.699,506.098 1417.422,513.975 1418.010,521.863 1418.461,529.761 1418.776,537.665 1418.955,545.573 1418.998,553.483 1418.904,561.393 1418.674,569.300 1418.308,577.202 1417.805,585.096 1417.167,592.980 1416.393,600.853 1415.483,608.710 1414.438,616.551 1413.258,624.373 1411.944,632.173 1410.495,639.950 1408.913,647.700 1407.197,655.422 1405.348,663.113 1403.367,670.771 1401.254,678.394" stroke="hsl(287 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1324.284,355.295 1327.575,361.514 1330.759,367.788 1333.835,374.115 1336.804,380.494 1339.665,386.922 1342.415,393.397 1345.055,399.919 1347.584,406.484 1350.001,413.092 1352.305,419.740 1354.496,426.425 1356.573,433.147 1358.535,439.904 1360.383,446.693 1362.115,453.512 1363.730,460.359 1365.229,467.234 1366.611,474.132 1367.876,481.053 1369.022,487.995 1370.051,494.955 1370.961,501.931 1371.753,508.922 1372.425,515.926 1372.979,522.940 1373.413,529.962 1373.728,536.990 1373.923,544.023 1373.999,551.059 1373.955,558.094 1373.792,565.128 1373.509,572.158 1373.107,579.182 1372.585,586.198 1371.944,593.205 1371.185,600.199 1370.306,607.180 1369.309,614.144 1368.194,621.091 1366.961,628.018 1365.610,634.923 1364.143,641.804" stroke="hsl(304 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="546.368,470.939 547.596,464.908 548.911,458.896 550.314,452.904 551.805,446.933 553.383,440.984 555.047,435.059 556.797,429.159 558.634,423.286 560.556,417.439 562.563,411.621 564.655,405.834 566.831,400.077 569.091,394.353 571.435,388.662 573.861,383.006 576.369,377.387 578.960,371.804 581.631,366.260 584.384,360.755 587.216,355.292 590.128,349.870 593.118,344.491 596.187,339.157 599.333,333.867 602.557,328.625 605.856,323.430 609.231,318.283 612.681,313.187 616.205,308.141 619.802,303.148 623.471,298.207 627.213,293.321 631.025,288.490 634.907,283.715 638.859,278.997 642.879,274.337 646.967,269.737 651.122,265.196 655.342,260.717 659.627,256.300 663.977,251.946 668.389,247.656" stroke="hsl(199 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="536.712,482.229 537.739,476.258 538.851,470.303 540.047,464.363 541.326,458.442 542.689,452.539 544.135,446.655 545.664,440.793 547.276,434.953 548.970,429.136 550.746,423.344 552.604,417.578 554.543,411.838 556.563,406.126 558.663,400.443 560.843,394.791 563.103,389.170 565.442,383.581 567.860,378.026 570.356,372.506 572.929,367.021 575.580,361.573 578.307,356.164 581.111,350.793 583.990,345.463 586.944,340.173 589.973,334.926 593.075,329.722 596.251,324.563 599.499,319.449 602.819,314.381 606.210,309.361 609.672,304.389 613.204,299.466 616.804,294.594 620.474,289.773 624.211,285.005 628.014,280.290 631.885,275.628 635.820,271.022 639.820,266.472 643.884,261.979 648.012,257.544" stroke="hsl(216 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="1391.710,616.489 1390.790,622.373 1389.790,628.243 1388.710,634.100 1387.550,639.941 1386.310,645.766 1384.991,651.573 1383.593,657.362 1382.116,663.132 1380.561,668.880 1378.927,674.607 1377.215,680.311 1375.426,685.991 1373.559,691.647 1371.615,697.276 1369.595,702.878 1367.498,708.452 1365.326,713.997 1363.078,719.512 1360.755,724.995 1358.358,730.447 1355.886,735.865 1353.341,741.249 1350.722,746.598 1348.031,751.910 1345.268,757.186 1342.432,762.423 1339.526,767.621 1336.549,772.779 1333.502,777.895 1330.385,782.970 1327.199,788.002 1323.946,792.989 1320.624,797.932 1317.235,802.829 1313.780,807.680 1310.258,812.483 1306.672,817.237 1303.021,821.942 1299.306,826.597 1295.528,831.200 1291.688,835.752 1287.786,840.251" stroke="hsl(233 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="521.911,479.789 522.937,473.819 524.045,467.863 525.233,461.923 526.503,456.000 527.853,450.095 529.283,444.209 530.794,438.342 532.384,432.497 534.055,426.674 535.804,420.875 537.632,415.100 539.540,409.350 541.525,403.627 543.588,397.931 545.729,392.265 547.947,386.628 550.241,381.021 552.612,375.447 555.059,369.905 557.581,364.398 560.178,358.925 562.849,353.488 565.594,348.088 568.413,342.726 571.304,337.403 574.268,332.120 577.304,326.878 580.411,321.678 583.589,316.521 586.836,311.407 590.154,306.338 593.540,301.315 596.994,296.339 600.516,291.410 604.104,286.530 607.759,281.699 611.480,276.919 615.265,272.190 619.115,267.512 623.028,262.888 627.003,258.318 631.041,253.802" stroke="hsl(250 92% 72%)" stroke-width="2.600" opacity="0.720"/>
+<polyline points="1394.664,429.854 1396.010,434.752 1397.302,439.664 1398.538,444.591 1399.718,449.531 1400.843,454.484 1401.913,459.450 1402.926,464.427 1403.883,469.415 1404.784,474.414 1405.628,479.423 1406.417,484.440 1407.149,489.467 1407.824,494.501 1408.442,499.542 1409.004,504.590 1409.509,509.645 1409.957,514.704 1410.348,519.768 1410.682,524.837 1410.959,529.908 1411.179,534.983 1411.342,540.060 1411.448,545.138 1411.496,550.217 1411.488,555.296 1411.422,560.375 1411.300,565.453 1411.120,570.529 1410.883,575.603 1410.589,580.673 1410.238,585.741 1409.829,590.804 1409.364,595.861 1408.843,600.914 1408.264,605.960 1407.629,611.000 1406.936,616.031 1406.188,621.055 1405.383,626.070 1404.521,631.076 1403.604,636.072 1402.630,641.057" stroke="hsl(267 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1231.657,182.022 1235.714,185.035 1239.737,188.093 1243.727,191.195 1247.682,194.341 1251.603,197.530 1255.488,200.763 1259.337,204.037 1263.150,207.354 1266.927,210.713 1270.666,214.113 1274.367,217.554 1278.031,221.035 1281.655,224.557 1285.241,228.118 1288.787,231.719 1292.294,235.359 1295.760,239.036 1299.185,242.752 1302.570,246.505 1305.913,250.296 1309.214,254.123 1312.472,257.986 1315.688,261.884 1318.861,265.818 1321.990,269.787 1325.075,273.789 1328.116,277.826 1331.113,281.896 1334.064,285.998 1336.970,290.133 1339.831,294.299 1342.645,298.497 1345.413,302.725 1348.134,306.984 1350.808,311.272 1353.435,315.590 1356.014,319.936 1358.545,324.310 1361.028,328.712 1363.462,333.141 1365.847,337.597 1368.184,342.078" stroke="hsl(284 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1265.330,831.588 1262.369,834.788 1259.374,837.957 1256.346,841.094 1253.285,844.199 1250.191,847.271 1247.065,850.311 1243.908,853.318 1240.719,856.291 1237.498,859.231 1234.247,862.136 1230.966,865.007 1227.654,867.843 1224.313,870.645 1220.943,873.411 1217.543,876.141 1214.115,878.835 1210.659,881.494 1207.175,884.115 1203.664,886.700 1200.125,889.248 1196.560,891.758 1192.969,894.230 1189.352,896.665 1185.709,899.061 1182.041,901.419 1178.349,903.738 1174.633,906.018 1170.892,908.259 1167.129,910.460 1163.342,912.622 1159.533,914.743 1155.702,916.825 1151.848,918.865 1147.974,920.865 1144.079,922.825 1140.163,924.743 1136.228,926.619 1132.273,928.455 1128.298,930.248 1124.306,932.000 1120.294,933.709 1116.266,935.376" stroke="hsl(301 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1369.037,450.258 1370.062,454.474 1371.044,458.699 1371.983,462.935 1372.877,467.180 1373.729,471.434 1374.536,475.696 1375.299,479.967 1376.019,484.245 1376.694,488.530 1377.325,492.822 1377.912,497.121 1378.455,501.425 1378.953,505.734 1379.407,510.049 1379.817,514.368 1380.182,518.691 1380.502,523.017 1380.778,527.346 1381.010,531.679 1381.197,536.013 1381.339,540.349 1381.437,544.686 1381.489,549.024 1381.498,553.362 1381.461,557.700 1381.380,562.038 1381.255,566.374 1381.085,570.709 1380.870,575.042 1380.610,579.372 1380.306,583.700 1379.958,588.024 1379.565,592.345 1379.127,596.661 1378.645,600.972 1378.119,605.278 1377.549,609.579 1376.934,613.873 1376.275,618.161 1375.572,622.442 1374.825,626.716 1374.034,630.981" stroke="hsl(196 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="543.685,448.448 544.773,444.169 545.905,439.901 547.081,435.645 548.300,431.401 549.563,427.170 550.870,422.952 552.219,418.748 553.613,414.558 555.049,410.383 556.528,406.223 558.049,402.078 559.614,397.949 561.220,393.836 562.869,389.740 564.561,385.661 566.293,381.600 568.068,377.557 569.884,373.532 571.742,369.527 573.640,365.540 575.580,361.573 577.560,357.627 579.581,353.701 581.642,349.796 583.743,345.913 585.884,342.051 588.065,338.212 590.285,334.395 592.544,330.601 594.842,326.831 597.179,323.085 599.555,319.363 601.968,315.665 604.419,311.993 606.909,308.346 609.435,304.725 611.999,301.130 614.599,297.561 617.236,294.020 619.909,290.505 622.619,287.019 625.364,283.561" stroke="hsl(213 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1221.864,202.773 1225.279,205.360 1228.668,207.980 1232.031,210.633 1235.368,213.319 1238.678,216.038 1241.962,218.789 1245.218,221.572 1248.447,224.387 1251.648,227.233 1254.821,230.111 1257.966,233.020 1261.082,235.959 1264.169,238.929 1267.227,241.929 1270.255,244.959 1273.253,248.018 1276.221,251.107 1279.159,254.225 1282.066,257.371 1284.941,260.546 1287.786,263.749 1290.599,266.980 1293.380,270.238 1296.129,273.523 1298.846,276.835 1301.530,280.174 1304.181,283.538 1306.799,286.929 1309.384,290.345 1311.935,293.786 1314.452,297.252 1316.935,300.743 1319.383,304.258 1321.797,307.797 1324.176,311.359 1326.520,314.945 1328.829,318.553 1331.102,322.184 1333.339,325.837 1335.541,329.511 1337.706,333.207 1339.835,336.925" stroke="hsl(230 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="522.660,475.389 523.432,471.100 524.247,466.820 525.104,462.548 526.003,458.284 526.944,454.030 527.926,449.785 528.950,445.549 530.015,441.324 531.122,437.110 532.270,432.907 533.460,428.715 534.690,424.535 535.961,420.367 537.274,416.212 538.626,412.070 540.020,407.942 541.454,403.827 542.928,399.727 544.443,395.641 545.997,391.571 547.591,387.516 549.225,383.476 550.899,379.453 552.612,375.447 554.364,371.457 556.156,367.485 557.986,363.531 559.855,359.595 561.762,355.677 563.708,351.779 565.692,347.899 567.714,344.040 569.774,340.200 571.871,336.381 574.006,332.582 576.177,328.805 578.386,325.049 580.632,321.315 582.914,317.603 585.232,313.913 587.587,310.247 589.977,306.604" stroke="hsl(247 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="516.078,469.628 516.907,465.275 517.780,460.931 518.695,456.596 519.652,452.269 520.652,447.953 521.694,443.646 522.779,439.350 523.905,435.065 525.074,430.791 526.284,426.529 527.537,422.278 528.831,418.041 530.166,413.816 531.543,409.604 532.961,405.406 534.420,401.223 535.920,397.053 537.461,392.899 539.043,388.760 540.665,384.637 542.328,380.530 544.030,376.439 545.773,372.365 547.556,368.309 549.379,364.270 551.241,360.250 553.142,356.248 555.083,352.264 557.062,348.300 559.081,344.356 561.138,340.431 563.233,336.527 565.367,332.644 567.539,328.782 569.748,324.941 571.995,321.122 574.280,317.326 576.601,313.552 578.960,309.801 581.355,306.073 583.787,302.369 586.255,298.689" stroke="hsl(264 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1403.110,432.274 1404.208,436.415 1405.268,440.567 1406.289,444.728 1407.271,448.899 1408.214,453.079 1409.118,457.267 1409.982,461.464 1410.808,465.668 1411.594,469.880 1412.341,474.099 1413.049,478.325 1413.717,482.558 1414.345,486.796 1414.934,491.040 1415.483,495.290 1415.993,499.544 1416.463,503.803 1416.893,508.066 1417.283,512.333 1417.633,516.603 1417.944,520.877 1418.214,525.153 1418.445,529.432 1418.636,533.712 1418.786,537.994 1418.897,542.278 1418.968,546.562 1418.999,550.846 1418.989,555.131 1418.940,559.416 1418.851,563.699 1418.722,567.982 1418.552,572.264 1418.343,576.543 1418.094,580.821 1417.805,585.096 1417.476,589.368 1417.108,593.637 1416.699,597.902 1416.251,602.163 1415.762,606.420 1415.235,610.672" stroke="hsl(281 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1362.256,649.911 1361.324,653.661 1360.358,657.403 1359.356,661.136 1358.320,664.859 1357.249,668.572 1356.144,672.276 1355.004,675.968 1353.829,679.650 1352.620,683.321 1351.377,686.980 1350.100,690.628 1348.789,694.263 1347.444,697.887 1346.066,701.497 1344.653,705.094 1343.207,708.678 1341.728,712.249 1340.216,715.805 1338.670,719.347 1337.091,722.875 1335.480,726.387 1333.835,729.885 1332.159,733.367 1330.449,736.833 1328.708,740.283 1326.934,743.717 1325.129,747.134 1323.291,750.534 1321.422,753.916 1319.521,757.281 1317.589,760.628 1315.626,763.957 1313.632,767.268 1311.607,770.560 1309.552,773.832 1307.466,777.086 1305.349,780.319 1303.203,783.533 1301.027,786.727 1298.821,789.900 1296.585,793.053 1294.320,796.184" stroke="hsl(298 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1216.243,217.333 1218.878,219.367 1221.497,221.423 1224.100,223.499 1226.687,225.595 1229.257,227.712 1231.810,229.848 1234.346,232.005 1236.865,234.182 1239.367,236.379 1241.851,238.596 1244.318,240.832 1246.767,243.087 1249.198,245.362 1251.611,247.656 1254.006,249.969 1256.382,252.300 1258.740,254.651 1261.080,257.020 1263.400,259.407 1265.702,261.813 1267.984,264.237 1270.248,266.678 1272.492,269.138 1274.716,271.615 1276.921,274.109 1279.106,276.621 1281.272,279.151 1283.417,281.697 1285.542,284.260 1287.646,286.839 1289.731,289.436 1291.794,292.048 1293.837,294.677 1295.859,297.322 1297.860,299.983 1299.841,302.660 1301.799,305.352 1303.737,308.059 1305.653,310.782 1307.548,313.520 1309.421,316.272 1311.272,319.040" stroke="hsl(193 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1199.794,196.276 1202.512,198.123 1205.215,199.991 1207.904,201.879 1210.579,203.789 1213.239,205.718 1215.884,207.668 1218.513,209.638 1221.128,211.628 1223.727,213.638 1226.311,215.668 1228.880,217.718 1231.432,219.787 1233.969,221.876 1236.489,223.984 1238.994,226.111 1241.482,228.258 1243.953,230.423 1246.408,232.607 1248.846,234.811 1251.267,237.032 1253.671,239.272 1256.057,241.531 1258.427,243.808 1260.779,246.103 1263.113,248.415 1265.429,250.746 1267.728,253.094 1270.008,255.460 1272.270,257.843 1274.514,260.244 1276.740,262.661 1278.947,265.096 1281.135,267.547 1283.304,270.015 1285.454,272.500 1287.586,275.001 1289.698,277.518 1291.790,280.051 1293.864,282.601 1295.917,285.166 1297.951,287.746 1299.965,290.343" stroke="hsl(210 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1138.876,153.835 1141.825,155.173 1144.765,156.533 1147.694,157.915 1150.613,159.318 1153.522,160.743 1156.419,162.190 1159.306,163.658 1162.182,165.148 1165.047,166.659 1167.901,168.191 1170.743,169.744 1173.574,171.318 1176.392,172.914 1179.199,174.530 1181.994,176.166 1184.777,177.824 1187.547,179.502 1190.305,181.201 1193.050,182.920 1195.782,184.659 1198.501,186.419 1201.207,188.199 1203.900,189.999 1206.579,191.818 1209.245,193.658 1211.897,195.517 1214.535,197.396 1217.160,199.294 1219.770,201.212 1222.365,203.149 1224.947,205.106 1227.513,207.081 1230.065,209.076 1232.602,211.089 1235.124,213.121 1237.631,215.172 1240.123,217.241 1242.599,219.329 1245.060,221.435 1247.505,223.559 1249.934,225.702 1252.347,227.862" stroke="hsl(227 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="516.618,528.577 516.804,525.288 517.015,522.000 517.249,518.714 517.509,515.430 517.792,512.147 518.100,508.867 518.432,505.589 518.789,502.314 519.170,499.042 519.575,495.772 520.004,492.506 520.458,489.243 520.935,485.983 521.437,482.727 521.963,479.475 522.514,476.226 523.088,472.982 523.686,469.742 524.309,466.507 524.955,463.277 525.625,460.051 526.320,456.831 527.038,453.615 527.780,450.405 528.545,447.201 529.335,444.002 530.148,440.810 530.985,437.623 531.845,434.443 532.729,431.269 533.637,428.102 534.568,424.942 535.523,421.789 536.500,418.643 537.502,415.504 538.526,412.373 539.574,409.249 540.644,406.134 541.738,403.026 542.855,399.927 543.995,396.836 545.158,393.753" stroke="hsl(244 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="509.391,523.650 509.614,520.307 509.861,516.966 510.134,513.627 510.431,510.290 510.753,506.956 511.099,503.623 511.470,500.294 511.867,496.967 512.287,493.643 512.733,490.323 513.202,487.006 513.697,483.692 514.216,480.383 514.760,477.077 515.328,473.775 515.921,470.478 516.538,467.185 517.179,463.897 517.845,460.614 518.536,457.335 519.250,454.062 519.989,450.794 520.752,447.532 521.539,444.276 522.351,441.025 523.186,437.781 524.046,434.543 524.929,431.311 525.837,428.087 526.768,424.868 527.723,421.657 528.702,418.453 529.705,415.257 530.732,412.068 531.782,408.886 532.856,405.713 533.953,402.548 535.073,399.390 536.218,396.242 537.385,393.101 538.576,389.970 539.790,386.847" stroke="hsl(261 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1418.477,573.910 1418.314,577.092 1418.128,580.273 1417.921,583.452 1417.692,586.630 1417.440,589.806 1417.167,592.980 1416.871,596.153 1416.554,599.323 1416.214,602.491 1415.853,605.656 1415.470,608.819 1415.064,611.980 1414.637,615.137 1414.188,618.291 1413.717,621.442 1413.224,624.590 1412.709,627.734 1412.172,630.875 1411.614,634.012 1411.034,637.145 1410.432,640.273 1409.808,643.398 1409.163,646.518 1408.496,649.633 1407.808,652.744 1407.097,655.850 1406.366,658.951 1405.613,662.047 1404.838,665.137 1404.042,668.222 1403.225,671.302 1402.386,674.376 1401.526,677.443 1400.644,680.505 1399.742,683.561 1398.818,686.610 1397.873,689.653 1396.907,692.689 1395.920,695.718 1394.912,698.741 1393.883,701.756 1392.833,704.764" stroke="hsl(278 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+<polyline points="1238.820,858.032 1236.689,859.960 1234.544,861.873 1232.387,863.772 1230.216,865.655 1228.032,867.523 1225.836,869.376 1223.626,871.213 1221.404,873.036 1219.169,874.842 1216.922,876.634 1214.663,878.409 1212.391,880.169 1210.107,881.913 1207.811,883.641 1205.503,885.353 1203.183,887.049 1200.851,888.729 1198.508,890.393 1196.153,892.041 1193.787,893.672 1191.410,895.286 1189.022,896.884 1186.622,898.466 1184.212,900.030 1181.790,901.578 1179.359,903.109 1176.916,904.624 1174.463,906.121 1172.000,907.601 1169.526,909.064 1167.043,910.510 1164.549,911.938 1162.046,913.349 1159.533,914.743 1157.010,916.120 1154.478,917.478 1151.936,918.819 1149.385,920.143 1146.825,921.449 1144.256,922.737 1141.679,924.007 1139.092,925.259" stroke="hsl(295 92% 72%)" stroke-width="1.100" opacity="0.160"/>
+</g>
+<g clip-path="url(#rose-clip)">
+<path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill="url(#rose-wash)" opacity="0.28"/>
+<g filter="url(#field-soften)"><circle cx="925.660" cy="139.920" r="35.000" fill="hsl(271 94% 61%)" opacity="0.268"/>
+<circle cx="960.000" cy="139.920" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
+<circle cx="994.340" cy="139.920" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="856.980" cy="174.260" r="35.000" fill="hsl(258 94% 61%)" opacity="0.251"/>
+<circle cx="891.320" cy="174.260" r="35.000" fill="hsl(266 94% 61%)" opacity="0.262"/>
+<circle cx="925.660" cy="174.260" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>
+<circle cx="960.000" cy="174.260" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="994.340" cy="174.260" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="1028.680" cy="174.260" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="1063.020" cy="174.260" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
+<circle cx="788.300" cy="208.600" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="822.640" cy="208.600" r="35.000" fill="hsl(252 94% 61%)" opacity="0.243"/>
+<circle cx="856.980" cy="208.600" r="35.000" fill="hsl(257 94% 61%)" opacity="0.250"/>
+<circle cx="891.320" cy="208.600" r="35.000" fill="hsl(265 94% 61%)" opacity="0.260"/>
+<circle cx="925.660" cy="208.600" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
+<circle cx="960.000" cy="208.600" r="35.000" fill="hsl(249 94% 61%)" opacity="0.241"/>
+<circle cx="994.340" cy="208.600" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="1028.680" cy="208.600" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>
+<circle cx="1063.020" cy="208.600" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
+<circle cx="1097.360" cy="208.600" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="1131.700" cy="208.600" r="35.000" fill="hsl(243 94% 61%)" opacity="0.233"/>
+<circle cx="719.620" cy="242.940" r="35.000" fill="hsl(238 94% 61%)" opacity="0.225"/>
+<circle cx="753.960" cy="242.940" r="35.000" fill="hsl(238 94% 61%)" opacity="0.227"/>
+<circle cx="788.300" cy="242.940" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
+<circle cx="822.640" cy="242.940" r="35.000" fill="hsl(245 94% 61%)" opacity="0.235"/>
+<circle cx="856.980" cy="242.940" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
+<circle cx="891.320" cy="242.940" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>
+<circle cx="925.660" cy="242.940" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>
+<circle cx="960.000" cy="242.940" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="994.340" cy="242.940" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
+<circle cx="1028.680" cy="242.940" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>
+<circle cx="1063.020" cy="242.940" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
+<circle cx="1097.360" cy="242.940" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
+<circle cx="1131.700" cy="242.940" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>
+<circle cx="1166.040" cy="242.940" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>
+<circle cx="1200.380" cy="242.940" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
+<circle cx="650.940" cy="277.280" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
+<circle cx="685.280" cy="277.280" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>
+<circle cx="719.620" cy="277.280" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
+<circle cx="753.960" cy="277.280" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
+<circle cx="788.300" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
+<circle cx="822.640" cy="277.280" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="856.980" cy="277.280" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
+<circle cx="891.320" cy="277.280" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
+<circle cx="925.660" cy="277.280" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
+<circle cx="960.000" cy="277.280" r="35.000" fill="hsl(249 94% 61%)" opacity="0.240"/>
+<circle cx="994.340" cy="277.280" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
+<circle cx="1028.680" cy="277.280" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="1063.020" cy="277.280" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
+<circle cx="1097.360" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
+<circle cx="1131.700" cy="277.280" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="1166.040" cy="277.280" r="35.000" fill="hsl(226 94% 61%)" opacity="0.211"/>
+<circle cx="1200.380" cy="277.280" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="1234.720" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
+<circle cx="1269.060" cy="277.280" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="616.600" cy="311.620" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
+<circle cx="650.940" cy="311.620" r="35.000" fill="hsl(229 94% 61%)" opacity="0.214"/>
+<circle cx="685.280" cy="311.620" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
+<circle cx="719.620" cy="311.620" r="35.000" fill="hsl(213 94% 61%)" opacity="0.195"/>
+<circle cx="753.960" cy="311.620" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
+<circle cx="788.300" cy="311.620" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
+<circle cx="822.640" cy="311.620" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="856.980" cy="311.620" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
+<circle cx="891.320" cy="311.620" r="34.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="925.660" cy="311.620" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="960.000" cy="311.620" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="994.340" cy="311.620" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
+<circle cx="1028.680" cy="311.620" r="35.000" fill="hsl(249 94% 61%)" opacity="0.239"/>
+<circle cx="1063.020" cy="311.620" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
+<circle cx="1097.360" cy="311.620" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>
+<circle cx="1131.700" cy="311.620" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>
+<circle cx="1166.040" cy="311.620" r="35.000" fill="hsl(224 94% 61%)" opacity="0.208"/>
+<circle cx="1200.380" cy="311.620" r="35.000" fill="hsl(225 94% 61%)" opacity="0.209"/>
+<circle cx="1234.720" cy="311.620" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
+<circle cx="1269.060" cy="311.620" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
+<circle cx="1303.400" cy="311.620" r="35.000" fill="hsl(263 94% 61%)" opacity="0.258"/>
+<circle cx="616.600" cy="345.960" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
+<circle cx="650.940" cy="345.960" r="35.000" fill="hsl(225 94% 61%)" opacity="0.209"/>
+<circle cx="685.280" cy="345.960" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
+<circle cx="719.620" cy="345.960" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
+<circle cx="753.960" cy="345.960" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>
+<circle cx="788.300" cy="345.960" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>
+<circle cx="822.640" cy="345.960" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
+<circle cx="856.980" cy="345.960" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
+<circle cx="891.320" cy="345.960" r="34.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
+<circle cx="925.660" cy="345.960" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="960.000" cy="345.960" r="35.000" fill="hsl(256 94% 61%)" opacity="0.250"/>
+<circle cx="994.340" cy="345.960" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
+<circle cx="1028.680" cy="345.960" r="34.000" fill="hsl(261 94% 61%)" opacity="0.255"/>
+<circle cx="1063.020" cy="345.960" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
+<circle cx="1097.360" cy="345.960" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
+<circle cx="1131.700" cy="345.960" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="1166.040" cy="345.960" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>
+<circle cx="1200.380" cy="345.960" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
+<circle cx="1234.720" cy="345.960" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
+<circle cx="1269.060" cy="345.960" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
+<circle cx="1303.400" cy="345.960" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>
+<circle cx="616.600" cy="380.300" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
+<circle cx="650.940" cy="380.300" r="35.000" fill="hsl(223 94% 61%)" opacity="0.206"/>
+<circle cx="685.280" cy="380.300" r="35.000" fill="hsl(212 94% 61%)" opacity="0.193"/>
+<circle cx="719.620" cy="380.300" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>
+<circle cx="753.960" cy="380.300" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>
+<circle cx="788.300" cy="380.300" r="35.000" fill="hsl(210 94% 61%)" opacity="0.190"/>
+<circle cx="822.640" cy="380.300" r="35.000" fill="hsl(215 94% 61%)" opacity="0.196"/>
+<circle cx="856.980" cy="380.300" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="891.320" cy="380.300" r="35.000" fill="hsl(242 94% 61%)" opacity="0.232"/>
+<circle cx="925.660" cy="380.300" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
+<circle cx="960.000" cy="380.300" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>
+<circle cx="994.340" cy="380.300" r="35.000" fill="hsl(270 94% 61%)" opacity="0.267"/>
+<circle cx="1028.680" cy="380.300" r="35.000" fill="hsl(274 94% 61%)" opacity="0.271"/>
+<circle cx="1063.020" cy="380.300" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
+<circle cx="1097.360" cy="380.300" r="34.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
+<circle cx="1131.700" cy="380.300" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="1166.040" cy="380.300" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
+<circle cx="1200.380" cy="380.300" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>
+<circle cx="1234.720" cy="380.300" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
+<circle cx="1269.060" cy="380.300" r="34.000" fill="hsl(245 94% 61%)" opacity="0.235"/>
+<circle cx="1303.400" cy="380.300" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="616.600" cy="414.640" r="35.000" fill="hsl(241 94% 61%)" opacity="0.229"/>
+<circle cx="650.940" cy="414.640" r="35.000" fill="hsl(228 94% 61%)" opacity="0.214"/>
+<circle cx="685.280" cy="414.640" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
+<circle cx="719.620" cy="414.640" r="35.000" fill="hsl(206 94% 61%)" opacity="0.186"/>
+<circle cx="753.960" cy="414.640" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
+<circle cx="788.300" cy="414.640" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
+<circle cx="822.640" cy="414.640" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
+<circle cx="856.980" cy="414.640" r="35.000" fill="hsl(227 94% 61%)" opacity="0.213"/>
+<circle cx="891.320" cy="414.640" r="34.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
+<circle cx="925.660" cy="414.640" r="35.000" fill="hsl(258 94% 61%)" opacity="0.251"/>
+<circle cx="960.000" cy="414.640" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
+<circle cx="994.340" cy="414.640" r="35.000" fill="hsl(280 94% 61%)" opacity="0.280"/>
+<circle cx="1028.680" cy="414.640" r="34.000" fill="hsl(285 94% 61%)" opacity="0.286"/>
+<circle cx="1063.020" cy="414.640" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>
+<circle cx="1097.360" cy="414.640" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
+<circle cx="1131.700" cy="414.640" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>
+<circle cx="1166.040" cy="414.640" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="1200.380" cy="414.640" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="1234.720" cy="414.640" r="34.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
+<circle cx="1269.060" cy="414.640" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
+<circle cx="1303.400" cy="414.640" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
+<circle cx="582.260" cy="448.980" r="35.000" fill="hsl(248 94% 61%)" opacity="0.238"/>
+<circle cx="616.600" cy="448.980" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
+<circle cx="650.940" cy="448.980" r="35.000" fill="hsl(235 94% 61%)" opacity="0.222"/>
+<circle cx="685.280" cy="448.980" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
+<circle cx="719.620" cy="448.980" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
+<circle cx="753.960" cy="448.980" r="34.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
+<circle cx="788.300" cy="448.980" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
+<circle cx="822.640" cy="448.980" r="35.000" fill="hsl(221 94% 61%)" opacity="0.204"/>
+<circle cx="856.980" cy="448.980" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
+<circle cx="891.320" cy="448.980" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
+<circle cx="925.660" cy="448.980" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>
+<circle cx="960.000" cy="448.980" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
+<circle cx="994.340" cy="448.980" r="35.000" fill="hsl(289 94% 61%)" opacity="0.291"/>
+<circle cx="1028.680" cy="448.980" r="35.000" fill="hsl(294 94% 61%)" opacity="0.297"/>
+<circle cx="1063.020" cy="448.980" r="35.000" fill="hsl(289 94% 61%)" opacity="0.291"/>
+<circle cx="1097.360" cy="448.980" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>
+<circle cx="1131.700" cy="448.980" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
+<circle cx="1166.040" cy="448.980" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
+<circle cx="1200.380" cy="448.980" r="34.000" fill="hsl(250 94% 61%)" opacity="0.242"/>
+<circle cx="1234.720" cy="448.980" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
+<circle cx="1269.060" cy="448.980" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="1303.400" cy="448.980" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
+<circle cx="1337.740" cy="448.980" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>
+<circle cx="582.260" cy="483.320" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="616.600" cy="483.320" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="650.940" cy="483.320" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
+<circle cx="685.280" cy="483.320" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="719.620" cy="483.320" r="35.000" fill="hsl(224 94% 61%)" opacity="0.209"/>
+<circle cx="753.960" cy="483.320" r="35.000" fill="hsl(216 94% 61%)" opacity="0.199"/>
+<circle cx="788.300" cy="483.320" r="35.000" fill="hsl(214 94% 61%)" opacity="0.196"/>
+<circle cx="822.640" cy="483.320" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
+<circle cx="856.980" cy="483.320" r="35.000" fill="hsl(234 94% 61%)" opacity="0.220"/>
+<circle cx="891.320" cy="483.320" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
+<circle cx="925.660" cy="483.320" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>
+<circle cx="960.000" cy="483.320" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
+<circle cx="994.340" cy="483.320" r="35.000" fill="hsl(292 94% 61%)" opacity="0.294"/>
+<circle cx="1028.680" cy="483.320" r="35.000" fill="hsl(297 94% 61%)" opacity="0.301"/>
+<circle cx="1063.020" cy="483.320" r="35.000" fill="hsl(296 94% 61%)" opacity="0.299"/>
+<circle cx="1097.360" cy="483.320" r="35.000" fill="hsl(291 94% 61%)" opacity="0.293"/>
+<circle cx="1131.700" cy="483.320" r="34.000" fill="hsl(283 94% 61%)" opacity="0.283"/>
+<circle cx="1166.040" cy="483.320" r="35.000" fill="hsl(267 94% 61%)" opacity="0.262"/>
+<circle cx="1200.380" cy="483.320" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
+<circle cx="1234.720" cy="483.320" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="1269.060" cy="483.320" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>
+<circle cx="1303.400" cy="483.320" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
+<circle cx="1337.740" cy="483.320" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="582.260" cy="517.660" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
+<circle cx="616.600" cy="517.660" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
+<circle cx="650.940" cy="517.660" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="685.280" cy="517.660" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="719.620" cy="517.660" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
+<circle cx="753.960" cy="517.660" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>
+<circle cx="788.300" cy="517.660" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
+<circle cx="822.640" cy="517.660" r="34.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="856.980" cy="517.660" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="891.320" cy="517.660" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
+<circle cx="925.660" cy="517.660" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>
+<circle cx="960.000" cy="517.660" r="35.000" fill="hsl(276 94% 61%)" opacity="0.274"/>
+<circle cx="994.340" cy="517.660" r="35.000" fill="hsl(290 94% 61%)" opacity="0.292"/>
+<circle cx="1028.680" cy="517.660" r="33.000" fill="hsl(299 94% 61%)" opacity="0.303"/>
+<circle cx="1063.020" cy="517.660" r="34.000" fill="hsl(301 94% 61%)" opacity="0.306"/>
+<circle cx="1097.360" cy="517.660" r="34.000" fill="hsl(296 94% 61%)" opacity="0.300"/>
+<circle cx="1131.700" cy="517.660" r="35.000" fill="hsl(284 94% 61%)" opacity="0.285"/>
+<circle cx="1166.040" cy="517.660" r="34.000" fill="hsl(268 94% 61%)" opacity="0.265"/>
+<circle cx="1200.380" cy="517.660" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
+<circle cx="1234.720" cy="517.660" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>
+<circle cx="1269.060" cy="517.660" r="35.000" fill="hsl(224 94% 61%)" opacity="0.209"/>
+<circle cx="1303.400" cy="517.660" r="35.000" fill="hsl(219 94% 61%)" opacity="0.201"/>
+<circle cx="1337.740" cy="517.660" r="35.000" fill="hsl(219 94% 61%)" opacity="0.201"/>
+<circle cx="582.260" cy="552.000" r="35.000" fill="hsl(259 94% 61%)" opacity="0.252"/>
+<circle cx="616.600" cy="552.000" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
+<circle cx="650.940" cy="552.000" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
+<circle cx="685.280" cy="552.000" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>
+<circle cx="719.620" cy="552.000" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
+<circle cx="753.960" cy="552.000" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
+<circle cx="788.300" cy="552.000" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="822.640" cy="552.000" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="856.980" cy="552.000" r="35.000" fill="hsl(238 94% 61%)" opacity="0.225"/>
+<circle cx="891.320" cy="552.000" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
+<circle cx="925.660" cy="552.000" r="35.000" fill="hsl(258 94% 61%)" opacity="0.252"/>
+<circle cx="960.000" cy="552.000" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
+<circle cx="994.340" cy="552.000" r="35.000" fill="hsl(283 94% 61%)" opacity="0.283"/>
+<circle cx="1028.680" cy="552.000" r="35.000" fill="hsl(293 94% 61%)" opacity="0.296"/>
+<circle cx="1063.020" cy="552.000" r="35.000" fill="hsl(295 94% 61%)" opacity="0.298"/>
+<circle cx="1097.360" cy="552.000" r="35.000" fill="hsl(290 94% 61%)" opacity="0.293"/>
+<circle cx="1131.700" cy="552.000" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>
+<circle cx="1166.040" cy="552.000" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
+<circle cx="1200.380" cy="552.000" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="1234.720" cy="552.000" r="35.000" fill="hsl(235 94% 61%)" opacity="0.222"/>
+<circle cx="1269.060" cy="552.000" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
+<circle cx="1303.400" cy="552.000" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>
+<circle cx="1337.740" cy="552.000" r="35.000" fill="hsl(209 94% 61%)" opacity="0.190"/>
+<circle cx="547.920" cy="586.340" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
+<circle cx="582.260" cy="586.340" r="34.000" fill="hsl(268 94% 61%)" opacity="0.264"/>
+<circle cx="616.600" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
+<circle cx="650.940" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
+<circle cx="685.280" cy="586.340" r="35.000" fill="hsl(271 94% 61%)" opacity="0.267"/>
+<circle cx="719.620" cy="586.340" r="35.000" fill="hsl(263 94% 61%)" opacity="0.258"/>
+<circle cx="753.960" cy="586.340" r="35.000" fill="hsl(254 94% 61%)" opacity="0.247"/>
+<circle cx="788.300" cy="586.340" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>
+<circle cx="822.640" cy="586.340" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
+<circle cx="856.980" cy="586.340" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
+<circle cx="891.320" cy="586.340" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
+<circle cx="925.660" cy="586.340" r="34.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="960.000" cy="586.340" r="35.000" fill="hsl(262 94% 61%)" opacity="0.256"/>
+<circle cx="994.340" cy="586.340" r="35.000" fill="hsl(273 94% 61%)" opacity="0.270"/>
+<circle cx="1028.680" cy="586.340" r="35.000" fill="hsl(283 94% 61%)" opacity="0.283"/>
+<circle cx="1063.020" cy="586.340" r="35.000" fill="hsl(284 94% 61%)" opacity="0.285"/>
+<circle cx="1097.360" cy="586.340" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>
+<circle cx="1131.700" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
+<circle cx="1166.040" cy="586.340" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
+<circle cx="1200.380" cy="586.340" r="35.000" fill="hsl(250 94% 61%)" opacity="0.242"/>
+<circle cx="1234.720" cy="586.340" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>
+<circle cx="1269.060" cy="586.340" r="35.000" fill="hsl(220 94% 61%)" opacity="0.204"/>
+<circle cx="1303.400" cy="586.340" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
+<circle cx="1337.740" cy="586.340" r="35.000" fill="hsl(200 94% 61%)" opacity="0.177"/>
+<circle cx="1372.080" cy="586.340" r="35.000" fill="hsl(207 94% 61%)" opacity="0.186"/>
+<circle cx="547.920" cy="620.680" r="35.000" fill="hsl(259 94% 61%)" opacity="0.252"/>
+<circle cx="582.260" cy="620.680" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
+<circle cx="616.600" cy="620.680" r="35.000" fill="hsl(280 94% 61%)" opacity="0.280"/>
+<circle cx="650.940" cy="620.680" r="35.000" fill="hsl(284 94% 61%)" opacity="0.284"/>
+<circle cx="685.280" cy="620.680" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>
+<circle cx="719.620" cy="620.680" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
+<circle cx="753.960" cy="620.680" r="35.000" fill="hsl(266 94% 61%)" opacity="0.262"/>
+<circle cx="788.300" cy="620.680" r="35.000" fill="hsl(257 94% 61%)" opacity="0.250"/>
+<circle cx="822.640" cy="620.680" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="856.980" cy="620.680" r="33.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
+<circle cx="891.320" cy="620.680" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>
+<circle cx="925.660" cy="620.680" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
+<circle cx="960.000" cy="620.680" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
+<circle cx="994.340" cy="620.680" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
+<circle cx="1028.680" cy="620.680" r="34.000" fill="hsl(268 94% 61%)" opacity="0.265"/>
+<circle cx="1063.020" cy="620.680" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>
+<circle cx="1097.360" cy="620.680" r="35.000" fill="hsl(274 94% 61%)" opacity="0.272"/>
+<circle cx="1131.700" cy="620.680" r="35.000" fill="hsl(270 94% 61%)" opacity="0.266"/>
+<circle cx="1166.040" cy="620.680" r="35.000" fill="hsl(261 94% 61%)" opacity="0.255"/>
+<circle cx="1200.380" cy="620.680" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="1234.720" cy="620.680" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="1269.060" cy="620.680" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>
+<circle cx="1303.400" cy="620.680" r="35.000" fill="hsl(212 94% 61%)" opacity="0.192"/>
+<circle cx="1337.740" cy="620.680" r="35.000" fill="hsl(203 94% 61%)" opacity="0.181"/>
+<circle cx="1372.080" cy="620.680" r="35.000" fill="hsl(198 94% 61%)" opacity="0.175"/>
+<circle cx="547.920" cy="655.020" r="35.000" fill="hsl(261 94% 61%)" opacity="0.255"/>
+<circle cx="582.260" cy="655.020" r="35.000" fill="hsl(275 94% 61%)" opacity="0.274"/>
+<circle cx="616.600" cy="655.020" r="35.000" fill="hsl(286 94% 61%)" opacity="0.287"/>
+<circle cx="650.940" cy="655.020" r="35.000" fill="hsl(293 94% 61%)" opacity="0.295"/>
+<circle cx="685.280" cy="655.020" r="35.000" fill="hsl(293 94% 61%)" opacity="0.296"/>
+<circle cx="719.620" cy="655.020" r="35.000" fill="hsl(288 94% 61%)" opacity="0.290"/>
+<circle cx="753.960" cy="655.020" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
+<circle cx="788.300" cy="655.020" r="35.000" fill="hsl(266 94% 61%)" opacity="0.261"/>
+<circle cx="822.640" cy="655.020" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="856.980" cy="655.020" r="34.000" fill="hsl(242 94% 61%)" opacity="0.232"/>
+<circle cx="891.320" cy="655.020" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
+<circle cx="925.660" cy="655.020" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="960.000" cy="655.020" r="34.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
+<circle cx="994.340" cy="655.020" r="35.000" fill="hsl(245 94% 61%)" opacity="0.234"/>
+<circle cx="1028.680" cy="655.020" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
+<circle cx="1063.020" cy="655.020" r="34.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
+<circle cx="1097.360" cy="655.020" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>
+<circle cx="1131.700" cy="655.020" r="34.000" fill="hsl(263 94% 61%)" opacity="0.258"/>
+<circle cx="1166.040" cy="655.020" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>
+<circle cx="1200.380" cy="655.020" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="1234.720" cy="655.020" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="1269.060" cy="655.020" r="35.000" fill="hsl(226 94% 61%)" opacity="0.211"/>
+<circle cx="1303.400" cy="655.020" r="35.000" fill="hsl(215 94% 61%)" opacity="0.197"/>
+<circle cx="1337.740" cy="655.020" r="35.000" fill="hsl(206 94% 61%)" opacity="0.186"/>
+<circle cx="1372.080" cy="655.020" r="35.000" fill="hsl(201 94% 61%)" opacity="0.179"/>
+<circle cx="582.260" cy="689.360" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>
+<circle cx="616.600" cy="689.360" r="35.000" fill="hsl(287 94% 61%)" opacity="0.288"/>
+<circle cx="650.940" cy="689.360" r="35.000" fill="hsl(294 94% 61%)" opacity="0.297"/>
+<circle cx="685.280" cy="689.360" r="35.000" fill="hsl(295 94% 61%)" opacity="0.298"/>
+<circle cx="719.620" cy="689.360" r="35.000" fill="hsl(290 94% 61%)" opacity="0.292"/>
+<circle cx="753.960" cy="689.360" r="35.000" fill="hsl(280 94% 61%)" opacity="0.279"/>
+<circle cx="788.300" cy="689.360" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>
+<circle cx="822.640" cy="689.360" r="34.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
+<circle cx="856.980" cy="689.360" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
+<circle cx="891.320" cy="689.360" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
+<circle cx="925.660" cy="689.360" r="35.000" fill="hsl(225 94% 61%)" opacity="0.210"/>
+<circle cx="960.000" cy="689.360" r="34.000" fill="hsl(224 94% 61%)" opacity="0.208"/>
+<circle cx="994.340" cy="689.360" r="35.000" fill="hsl(230 94% 61%)" opacity="0.215"/>
+<circle cx="1028.680" cy="689.360" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="1063.020" cy="689.360" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="1097.360" cy="689.360" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="1131.700" cy="689.360" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
+<circle cx="1166.040" cy="689.360" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
+<circle cx="1200.380" cy="689.360" r="35.000" fill="hsl(245 94% 61%)" opacity="0.235"/>
+<circle cx="1234.720" cy="689.360" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
+<circle cx="1269.060" cy="689.360" r="35.000" fill="hsl(228 94% 61%)" opacity="0.213"/>
+<circle cx="1303.400" cy="689.360" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
+<circle cx="1337.740" cy="689.360" r="35.000" fill="hsl(210 94% 61%)" opacity="0.191"/>
+<circle cx="616.600" cy="723.700" r="35.000" fill="hsl(271 94% 61%)" opacity="0.268"/>
+<circle cx="650.940" cy="723.700" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>
+<circle cx="685.280" cy="723.700" r="35.000" fill="hsl(286 94% 61%)" opacity="0.287"/>
+<circle cx="719.620" cy="723.700" r="35.000" fill="hsl(285 94% 61%)" opacity="0.285"/>
+<circle cx="753.960" cy="723.700" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
+<circle cx="788.300" cy="723.700" r="35.000" fill="hsl(268 94% 61%)" opacity="0.265"/>
+<circle cx="822.640" cy="723.700" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
+<circle cx="856.980" cy="723.700" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
+<circle cx="891.320" cy="723.700" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="925.660" cy="723.700" r="35.000" fill="hsl(216 94% 61%)" opacity="0.198"/>
+<circle cx="960.000" cy="723.700" r="35.000" fill="hsl(213 94% 61%)" opacity="0.195"/>
+<circle cx="994.340" cy="723.700" r="35.000" fill="hsl(216 94% 61%)" opacity="0.198"/>
+<circle cx="1028.680" cy="723.700" r="35.000" fill="hsl(225 94% 61%)" opacity="0.210"/>
+<circle cx="1063.020" cy="723.700" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="1097.360" cy="723.700" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
+<circle cx="1131.700" cy="723.700" r="35.000" fill="hsl(248 94% 61%)" opacity="0.238"/>
+<circle cx="1166.040" cy="723.700" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
+<circle cx="1200.380" cy="723.700" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>
+<circle cx="1234.720" cy="723.700" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>
+<circle cx="1269.060" cy="723.700" r="35.000" fill="hsl(229 94% 61%)" opacity="0.214"/>
+<circle cx="1303.400" cy="723.700" r="35.000" fill="hsl(221 94% 61%)" opacity="0.205"/>
+<circle cx="616.600" cy="758.040" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>
+<circle cx="650.940" cy="758.040" r="35.000" fill="hsl(269 94% 61%)" opacity="0.266"/>
+<circle cx="685.280" cy="758.040" r="35.000" fill="hsl(277 94% 61%)" opacity="0.275"/>
+<circle cx="719.620" cy="758.040" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>
+<circle cx="753.960" cy="758.040" r="35.000" fill="hsl(276 94% 61%)" opacity="0.274"/>
+<circle cx="788.300" cy="758.040" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>
+<circle cx="822.640" cy="758.040" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
+<circle cx="856.980" cy="758.040" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="891.320" cy="758.040" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>
+<circle cx="925.660" cy="758.040" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>
+<circle cx="960.000" cy="758.040" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
+<circle cx="994.340" cy="758.040" r="35.000" fill="hsl(205 94% 61%)" opacity="0.184"/>
+<circle cx="1028.680" cy="758.040" r="35.000" fill="hsl(214 94% 61%)" opacity="0.196"/>
+<circle cx="1063.020" cy="758.040" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>
+<circle cx="1097.360" cy="758.040" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
+<circle cx="1131.700" cy="758.040" r="34.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="1166.040" cy="758.040" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
+<circle cx="1200.380" cy="758.040" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>
+<circle cx="1234.720" cy="758.040" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="1269.060" cy="758.040" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>
+<circle cx="650.940" cy="792.380" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>
+<circle cx="685.280" cy="792.380" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>
+<circle cx="719.620" cy="792.380" r="35.000" fill="hsl(272 94% 61%)" opacity="0.270"/>
+<circle cx="753.960" cy="792.380" r="35.000" fill="hsl(273 94% 61%)" opacity="0.270"/>
+<circle cx="788.300" cy="792.380" r="34.000" fill="hsl(266 94% 61%)" opacity="0.262"/>
+<circle cx="822.640" cy="792.380" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>
+<circle cx="856.980" cy="792.380" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>
+<circle cx="891.320" cy="792.380" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
+<circle cx="925.660" cy="792.380" r="35.000" fill="hsl(211 94% 61%)" opacity="0.191"/>
+<circle cx="960.000" cy="792.380" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>
+<circle cx="994.340" cy="792.380" r="35.000" fill="hsl(203 94% 61%)" opacity="0.182"/>
+<circle cx="1028.680" cy="792.380" r="35.000" fill="hsl(207 94% 61%)" opacity="0.186"/>
+<circle cx="1063.020" cy="792.380" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>
+<circle cx="1097.360" cy="792.380" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>
+<circle cx="1131.700" cy="792.380" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="1166.040" cy="792.380" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="1200.380" cy="792.380" r="35.000" fill="hsl(247 94% 61%)" opacity="0.238"/>
+<circle cx="1234.720" cy="792.380" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>
+<circle cx="1269.060" cy="792.380" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>
+<circle cx="685.280" cy="826.720" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>
+<circle cx="719.620" cy="826.720" r="35.000" fill="hsl(265 94% 61%)" opacity="0.261"/>
+<circle cx="753.960" cy="826.720" r="35.000" fill="hsl(268 94% 61%)" opacity="0.264"/>
+<circle cx="788.300" cy="826.720" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
+<circle cx="822.640" cy="826.720" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
+<circle cx="856.980" cy="826.720" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>
+<circle cx="891.320" cy="826.720" r="34.000" fill="hsl(221 94% 61%)" opacity="0.205"/>
+<circle cx="925.660" cy="826.720" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
+<circle cx="960.000" cy="826.720" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
+<circle cx="994.340" cy="826.720" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>
+<circle cx="1028.680" cy="826.720" r="34.000" fill="hsl(203 94% 61%)" opacity="0.181"/>
+<circle cx="1063.020" cy="826.720" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
+<circle cx="1097.360" cy="826.720" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>
+<circle cx="1131.700" cy="826.720" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>
+<circle cx="1166.040" cy="826.720" r="35.000" fill="hsl(255 94% 61%)" opacity="0.248"/>
+<circle cx="1200.380" cy="826.720" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
+<circle cx="1234.720" cy="826.720" r="35.000" fill="hsl(242 94% 61%)" opacity="0.232"/>
+<circle cx="719.620" cy="861.060" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>
+<circle cx="753.960" cy="861.060" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>
+<circle cx="788.300" cy="861.060" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>
+<circle cx="822.640" cy="861.060" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>
+<circle cx="856.980" cy="861.060" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
+<circle cx="891.320" cy="861.060" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
+<circle cx="925.660" cy="861.060" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="960.000" cy="861.060" r="35.000" fill="hsl(223 94% 61%)" opacity="0.206"/>
+<circle cx="994.340" cy="861.060" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>
+<circle cx="1028.680" cy="861.060" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>
+<circle cx="1063.020" cy="861.060" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>
+<circle cx="1097.360" cy="861.060" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>
+<circle cx="1131.700" cy="861.060" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="1166.040" cy="861.060" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
+<circle cx="1200.380" cy="861.060" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>
+<circle cx="753.960" cy="895.400" r="35.000" fill="hsl(256 94% 61%)" opacity="0.248"/>
+<circle cx="788.300" cy="895.400" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>
+<circle cx="822.640" cy="895.400" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>
+<circle cx="856.980" cy="895.400" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="891.320" cy="895.400" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
+<circle cx="925.660" cy="895.400" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>
+<circle cx="960.000" cy="895.400" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="994.340" cy="895.400" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>
+<circle cx="1028.680" cy="895.400" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>
+<circle cx="1063.020" cy="895.400" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>
+<circle cx="1097.360" cy="895.400" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>
+<circle cx="1131.700" cy="895.400" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="1166.040" cy="895.400" r="35.000" fill="hsl(266 94% 61%)" opacity="0.261"/>
+<circle cx="753.960" cy="929.740" r="34.000" fill="hsl(251 94% 61%)" opacity="0.243"/>
+<circle cx="788.300" cy="929.740" r="35.000" fill="hsl(256 94% 61%)" opacity="0.248"/>
+<circle cx="822.640" cy="929.740" r="34.000" fill="hsl(257 94% 61%)" opacity="0.250"/>
+<circle cx="856.980" cy="929.740" r="35.000" fill="hsl(253 94% 61%)" opacity="0.246"/>
+<circle cx="891.320" cy="929.740" r="35.000" fill="hsl(250 94% 61%)" opacity="0.242"/>
+<circle cx="925.660" cy="929.740" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>
+<circle cx="960.000" cy="929.740" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>
+<circle cx="994.340" cy="929.740" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>
+<circle cx="1028.680" cy="929.740" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>
+<circle cx="1063.020" cy="929.740" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>
+<circle cx="1097.360" cy="929.740" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>
+<circle cx="1131.700" cy="929.740" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>
+<circle cx="1166.040" cy="929.740" r="34.000" fill="hsl(262 94% 61%)" opacity="0.257"/>
+</g>
+<g opacity="0.58"><line x1="983.332" y1="534.766" x2="973.544" y2="526.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="988.499" y1="546.596" x2="983.332" y2="534.766" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="983.332" y1="534.766" x2="1006.907" y2="517.352" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="981.994" y1="570.912" x2="988.022" y2="559.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="988.022" y1="559.496" x2="988.499" y2="546.596" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1016.824" y1="567.202" x2="988.022" y2="559.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="988.499" y1="546.596" x2="1027.405" y2="539.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="991.808" y1="491.757" x2="973.544" y2="526.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="973.544" y1="526.349" x2="961.073" y2="523.013" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="962.146" y1="494.029" x2="961.073" y2="523.013" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="961.073" y1="523.013" x2="948.390" y2="525.418" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="971.610" y1="578.582" x2="981.994" y2="570.912" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="981.994" y1="570.912" x2="1012.465" y2="597.114" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="932.876" y1="489.898" x2="948.390" y2="525.418" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="948.390" y1="525.418" x2="938.006" y2="533.088" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.927" y1="580.987" x2="971.610" y2="578.582" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="971.610" y1="578.582" x2="983.824" y2="606.545" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="913.124" y1="511.692" x2="938.006" y2="533.088" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="938.006" y1="533.088" x2="931.978" y2="544.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="946.456" y1="577.651" x2="958.927" y2="580.987" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.927" y1="580.987" x2="957.415" y2="621.827" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="890.959" y1="533.530" x2="931.978" y2="544.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="931.978" y1="544.504" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="936.668" y1="569.234" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="899.643" y1="563.445" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="936.668" y1="569.234" x2="946.456" y2="577.651" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="903.183" y1="593.968" x2="936.668" y2="569.234" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="946.456" y1="577.651" x2="931.800" y2="605.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="972.412" y1="484.686" x2="962.146" y2="494.029" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="962.146" y1="494.029" x2="947.830" y2="484.058" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1018.788" y1="518.699" x2="1006.907" y2="517.352" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1006.907" y1="517.352" x2="1004.363" y2="498.501" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1016.824" y1="567.202" x2="1029.925" y2="551.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1029.925" y1="551.208" x2="1027.405" y2="539.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1022.389" y1="575.335" x2="1016.824" y2="567.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="983.824" y1="606.545" x2="1006.276" y2="604.953" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1006.276" y1="604.953" x2="1012.465" y2="597.114" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="981.859" y1="613.815" x2="983.824" y2="606.545" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="927.316" y1="607.504" x2="931.800" y2="605.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="931.800" y1="605.409" x2="950.032" y2="621.996" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="897.765" y1="562.566" x2="899.643" y2="563.445" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="899.643" y1="563.445" x2="900.347" y2="590.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="912.015" y1="511.701" x2="913.124" y2="511.692" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="912.015" y1="511.701" x2="891.092" y2="532.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="926.036" y1="486.913" x2="932.876" y2="489.898" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="926.036" y1="486.913" x2="913.124" y2="511.692" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="879.746" y1="488.235" x2="912.015" y2="511.701" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="888.385" y1="535.872" x2="897.765" y2="562.566" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="897.765" y1="562.566" x2="861.003" y2="574.923" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="903.269" y1="597.046" x2="927.316" y2="607.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="927.316" y1="607.504" x2="916.556" y2="643.053" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="959.674" y1="623.670" x2="981.859" y2="613.815" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="981.859" y1="613.815" x2="1003.828" y2="642.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1015.388" y1="596.606" x2="1022.389" y2="575.335" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1022.389" y1="575.335" x2="1057.456" y2="573.982" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1029.059" y1="536.487" x2="1018.788" y2="518.699" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1018.788" y1="518.699" x2="1037.830" y2="489.758" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="990.734" y1="488.378" x2="991.808" y2="491.757" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="990.734" y1="488.378" x2="972.412" y2="484.686" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="452.426" x2="972.412" y2="484.686" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="452.426" x2="947.830" y2="484.058" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="947.830" y1="484.058" x2="932.876" y2="489.898" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="924.801" y1="485.323" x2="926.036" y2="486.913" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1037.830" y1="489.758" x2="1004.363" y2="498.501" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1004.363" y1="498.501" x2="991.808" y2="491.757" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="998.095" y1="470.935" x2="990.734" y2="488.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1057.456" y1="573.982" x2="1029.925" y2="551.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1027.405" y1="539.218" x2="1029.059" y2="536.487" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1044.753" y1="530.377" x2="1029.059" y2="536.487" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1003.828" y1="642.233" x2="1006.276" y2="604.953" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1012.465" y1="597.114" x2="1015.388" y2="596.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1028.621" y1="602.722" x2="1015.388" y2="596.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="950.032" y1="621.996" x2="957.415" y2="621.827" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="957.415" y1="621.827" x2="959.674" y2="623.670" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="916.556" y1="643.053" x2="950.032" y2="621.996" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="963.524" y1="635.004" x2="959.674" y2="623.670" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="900.347" y1="590.671" x2="903.183" y2="593.968" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="903.183" y1="593.968" x2="903.269" y2="597.046" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="861.003" y1="574.923" x2="900.347" y2="590.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="899.115" y1="604.826" x2="903.269" y2="597.046" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="879.746" y1="488.235" x2="891.092" y2="532.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="891.092" y1="532.785" x2="890.959" y2="533.530" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="888.385" y1="535.872" x2="890.959" y2="533.530" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="883.649" y1="537.102" x2="888.385" y2="535.872" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="924.690" y1="468.840" x2="924.801" y2="485.323" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="924.801" y1="485.323" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="858.806" y1="525.718" x2="883.649" y2="537.102" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="883.649" y1="537.102" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="877.335" y1="612.112" x2="899.115" y2="604.826" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="899.115" y1="604.826" x2="913.901" y2="646.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="954.849" y1="651.750" x2="963.524" y2="635.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="963.524" y1="635.004" x2="1005.424" y2="647.755" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1033.065" y1="616.531" x2="1028.621" y2="602.722" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1028.621" y1="602.722" x2="1064.264" y2="576.363" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1053.303" y1="534.525" x2="1044.753" y2="530.377" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1044.753" y1="530.377" x2="1044.903" y2="484.740" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="442.292" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="442.292" x2="924.690" y2="468.840" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="907.607" y1="456.774" x2="924.690" y2="468.840" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1001.349" y1="469.664" x2="998.095" y2="470.935" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="998.095" y1="470.935" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1046.205" y1="482.726" x2="1001.349" y2="469.664" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1001.349" y1="469.664" x2="1012.440" y2="438.687" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1069.001" y1="576.148" x2="1053.303" y2="534.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1053.303" y1="534.525" x2="1077.607" y2="520.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1009.761" y1="653.301" x2="1033.065" y2="616.531" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1033.065" y1="616.531" x2="1056.813" y2="621.206" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="911.243" y1="655.315" x2="954.849" y2="651.750" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="954.849" y1="651.750" x2="966.607" y2="668.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="847.298" y1="578.490" x2="877.335" y2="612.112" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="877.335" y1="612.112" x2="874.824" y2="627.402" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="867.916" y1="479.092" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="867.916" y1="479.092" x2="858.806" y2="525.718" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="850.800" y1="531.184" x2="858.806" y2="525.718" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="912.200" y1="437.122" x2="907.607" y2="456.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="907.607" y1="456.774" x2="866.518" y2="476.847" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="822.234" y1="515.708" x2="850.800" y2="531.184" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="850.800" y1="531.184" x2="841.435" y2="578.126" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="847.328" y1="633.483" x2="874.824" y2="627.402" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="874.824" y1="627.402" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1005.424" y1="647.755" x2="1009.761" y2="653.301" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1013.053" y1="664.981" x2="1009.761" y2="653.301" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="966.607" y1="668.164" x2="1013.053" y2="664.981" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="952.990" y1="687.766" x2="966.607" y2="668.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1064.264" y1="576.363" x2="1069.001" y2="576.148" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1083.326" y1="581.231" x2="1069.001" y2="576.148" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1056.813" y1="621.206" x2="1083.326" y2="581.231" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1059.998" y1="639.787" x2="1056.813" y2="621.206" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1077.607" y1="520.313" x2="1061.111" y2="472.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1061.111" y1="472.202" x2="1046.205" y2="482.726" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1088.004" y1="527.396" x2="1077.607" y2="520.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="420.997" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="420.997" x2="912.200" y2="437.122" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="890.064" y1="422.515" x2="912.200" y2="437.122" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1016.991" y1="437.643" x2="1012.440" y2="438.687" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1012.440" y1="438.687" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1063.356" y1="468.620" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1044.903" y1="484.740" x2="1037.830" y2="489.758" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1016.991" y1="437.643" x2="1063.356" y2="468.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1031.409" y1="399.015" x2="1016.991" y2="437.643" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1091.646" y1="580.750" x2="1088.004" y2="527.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1088.004" y1="527.396" x2="1119.190" y2="510.664" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1020.755" y1="674.626" x2="1059.998" y2="639.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1059.998" y1="639.787" x2="1089.392" y2="647.174" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="901.037" y1="677.977" x2="952.990" y2="687.766" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="952.990" y1="687.766" x2="966.417" y2="709.035" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="841.435" y1="578.126" x2="822.368" y2="584.702" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="822.368" y1="584.702" x2="847.328" y2="633.483" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="843.098" y1="652.260" x2="847.328" y2="633.483" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="866.518" y1="476.847" x2="867.916" y2="479.092" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="879.746" y1="488.235" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="847.026" y1="462.778" x2="866.518" y2="476.847" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="844.902" y1="459.236" x2="847.026" y2="462.778" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="822.234" y1="515.708" x2="847.026" y2="462.778" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="811.924" y1="521.940" x2="822.234" y2="515.708" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="892.538" y1="398.415" x2="890.064" y2="422.515" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="890.064" y1="422.515" x2="844.902" y2="459.236" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="817.477" y1="439.555" x2="844.902" y2="459.236" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="774.927" y1="505.831" x2="811.924" y2="521.940" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="811.924" y1="521.940" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="911.243" y1="655.315" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="901.037" y1="677.977" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="843.098" y1="652.260" x2="892.095" y2="688.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="811.028" y1="664.055" x2="843.098" y2="652.260" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="966.417" y1="709.035" x2="1025.682" y2="692.759" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1020.755" y1="674.626" x2="1013.053" y2="664.981" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="954.383" y1="734.417" x2="966.417" y2="709.035" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1089.392" y1="647.174" x2="1113.737" y2="588.747" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1091.646" y1="580.750" x2="1083.326" y2="581.231" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1096.731" y1="667.435" x2="1089.392" y2="647.174" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1063.356" y1="468.620" x2="1061.111" y2="472.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1046.205" y1="482.726" x2="1044.903" y2="484.740" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1119.190" y1="510.664" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1089.518" y1="447.313" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="420.997" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="452.426" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="388.149" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="388.149" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="892.538" y1="398.415" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="870.081" y1="377.240" x2="892.538" y2="398.415" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1031.409" y1="399.015" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="388.149" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1119.476" y1="426.455" x2="1089.518" y2="447.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1089.518" y1="447.313" x2="1036.135" y2="397.016" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1125.495" y1="587.895" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1125.495" y1="587.895" x2="1132.028" y2="516.372" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1036.713" y1="706.233" x2="1096.731" y2="667.435" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1096.731" y1="667.435" x2="1129.234" y2="681.108" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="874.114" y1="724.752" x2="885.923" y2="710.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="885.923" y1="710.844" x2="954.383" y2="734.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="964.714" y1="760.668" x2="954.383" y2="734.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="786.510" y1="593.409" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="822.368" y1="584.702" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="811.028" y1="664.055" x2="786.510" y2="593.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="814.773" y1="434.875" x2="817.477" y2="439.555" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="817.477" y1="439.555" x2="774.927" y2="505.831" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="870.081" y1="377.240" x2="814.773" y2="434.875" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="814.773" y1="434.875" x2="781.631" y2="411.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="867.820" y1="350.841" x2="870.081" y2="377.240" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="847.298" y1="578.490" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="841.435" y1="578.126" x2="847.298" y2="578.490" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="763.070" y1="509.820" x2="774.629" y2="592.324" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="802.354" y1="682.911" x2="874.114" y2="724.752" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="901.037" y1="677.977" x2="892.095" y2="688.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="964.714" y1="760.668" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1020.755" y1="674.626" x2="1025.682" y2="692.759" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1003.828" y1="642.233" x2="1005.424" y2="647.755" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1025.682" y1="692.759" x2="1036.713" y2="706.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1036.713" y1="706.233" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1056.399" y1="746.009" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1091.646" y1="580.750" x2="1113.737" y2="588.747" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1064.264" y1="576.363" x2="1057.456" y2="573.982" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1129.234" y1="681.108" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1113.737" y1="588.747" x2="1125.495" y2="587.895" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1168.319" y1="597.246" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1171.121" y1="500.189" x2="1119.476" y2="426.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1119.476" y1="426.455" x2="1123.156" y2="420.161" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="345.580" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="345.580" x2="867.820" y2="350.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1055.165" y1="351.011" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="345.580" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1157.051" y1="396.671" x2="1123.156" y2="420.161" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1123.156" y1="420.161" x2="1059.283" y2="348.098" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1200.567" y1="609.315" x2="1168.319" y2="597.246" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1168.319" y1="597.246" x2="1185.153" y2="503.235" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1063.217" y1="772.753" x2="1056.399" y2="746.009" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1056.399" y1="746.009" x2="1140.858" y2="700.809" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="866.867" y1="751.471" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="874.114" y1="724.752" x2="866.867" y2="751.471" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="956.014" y1="790.291" x2="866.867" y2="751.471" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="774.629" y1="592.324" x2="786.510" y2="593.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="742.414" y1="603.842" x2="767.921" y2="700.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="778.616" y1="405.859" x2="781.631" y2="411.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="781.631" y1="411.208" x2="719.271" y2="494.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="846.478" y1="324.667" x2="778.616" y2="405.859" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="778.616" y1="405.859" x2="743.401" y2="380.760" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="706.813" y1="496.189" x2="728.954" y2="602.485" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="742.414" y1="603.842" x2="728.954" y2="602.485" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="892.095" y1="688.774" x2="885.923" y2="710.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="916.556" y1="643.053" x2="913.901" y2="646.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="755.980" y1="718.378" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="913.901" y1="646.396" x2="911.243" y2="655.315" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="845.762" y1="795.518" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="963.309" y1="819.481" x2="1063.217" y2="772.753" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1063.217" y1="772.753" x2="1077.647" y2="789.877" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1174.640" y1="719.101" x2="1200.567" y2="609.315" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1200.567" y1="609.315" x2="1215.710" y2="607.957" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1229.348" y1="488.029" x2="1157.051" y2="396.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1157.051" y1="396.671" x2="1160.932" y2="389.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="297.480" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="297.480" x2="840.722" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1081.464" y1="297.480" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="297.480" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1194.827" y1="366.457" x2="1160.932" y2="389.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1160.932" y1="389.948" x2="1085.015" y2="294.014" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1247.958" y1="620.026" x2="1215.710" y2="607.957" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1215.710" y1="607.957" x2="1243.784" y2="489.085" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1084.465" y1="816.620" x2="1077.647" y2="789.877" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1077.647" y1="789.877" x2="1188.966" y2="737.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="832.269" y1="811.159" x2="845.762" y2="795.518" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="845.762" y1="795.518" x2="957.220" y2="851.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="742.414" y1="603.842" x2="774.629" y2="592.324" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="694.769" y1="614.787" x2="720.749" y2="739.517" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="740.385" y1="375.411" x2="743.401" y2="380.760" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="743.401" y1="380.760" x2="659.219" y2="481.477" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="820.635" y1="268.446" x2="740.385" y2="375.411" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="740.385" y1="375.411" x2="707.243" y2="351.744" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="728.954" y1="602.485" x2="694.769" y2="614.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="681.309" y1="613.430" x2="694.769" y2="614.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="646.566" y1="481.855" x2="681.309" y2="613.430" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="706.783" y1="756.515" x2="832.269" y2="811.159" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="832.269" y1="811.159" x2="825.021" y2="837.878" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="962.311" y1="881.993" x2="1084.465" y2="816.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1084.465" y1="816.620" x2="1097.977" y2="832.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1223.079" y1="758.972" x2="1247.958" y2="620.026" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1247.958" y1="620.026" x2="1262.200" y2="618.818" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1290.828" y1="474.767" x2="1194.827" y2="366.457" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1194.827" y1="366.457" x2="1198.507" y2="360.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="249.380" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="249.380" x2="812.520" y2="240.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1109.071" y1="240.873" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="249.380" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1228.466" y1="339.305" x2="1198.507" y2="360.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1198.507" y1="360.164" x2="1112.243" y2="237.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1290.782" y1="629.377" x2="1262.200" y2="618.818" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1262.200" y1="618.818" x2="1305.599" y2="474.431" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1104.151" y1="856.396" x2="1097.977" y2="832.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1097.977" y1="832.785" x2="1239.349" y2="777.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="813.213" y1="851.786" x2="825.021" y2="837.878" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="825.021" y1="837.878" x2="958.045" y2="915.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="649.094" y1="624.948" x2="681.309" y2="613.430" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="861.003" y1="574.923" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="637.213" y1="623.863" x2="649.094" y2="624.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="670.857" y1="780.283" x2="649.094" y2="624.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="704.539" y1="347.063" x2="707.243" y2="351.744" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="707.243" y1="351.744" x2="596.046" y2="467.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="792.977" y1="209.314" x2="704.539" y2="347.063" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="704.539" y1="347.063" x2="677.115" y2="327.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="582.735" y1="466.892" x2="637.213" y2="623.863" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="637.213" y1="623.863" x2="610.472" y2="633.269" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="654.802" y1="797.239" x2="813.213" y2="851.786" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="813.213" y1="851.786" x2="807.041" y2="873.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="961.660" y1="948.507" x2="1104.151" y2="856.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1104.151" y1="856.396" x2="1115.182" y2="869.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1275.085" y1="801.240" x2="1290.782" y2="629.377" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1290.782" y1="629.377" x2="1302.541" y2="628.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1356.776" y1="460.198" x2="1228.466" y2="339.305" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1228.466" y1="339.305" x2="1231.566" y2="334.170" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="206.811" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="206.811" x2="782.440" y2="179.793" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1138.787" y1="179.606" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="206.811" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1254.627" y1="317.999" x2="1231.566" y2="334.170" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1231.566" y1="334.170" x2="1141.961" y2="174.998" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1324.631" y1="636.522" x2="1302.541" y2="628.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1302.541" y1="628.525" x2="1373.328" y2="458.637" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1120.109" y1="888.003" x2="1115.182" y2="869.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1115.182" y1="869.870" x2="1294.663" y2="820.579" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="798.099" y1="884.652" x2="807.041" y2="873.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="807.041" y1="873.855" x2="958.533" y2="986.753" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="601.355" y1="632.570" x2="610.472" y2="633.269" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="610.472" y1="633.269" x2="614.586" y2="825.722" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="674.990" y1="323.841" x2="677.115" y2="327.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="677.115" y1="327.383" x2="524.370" y2="451.576" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="760.577" y1="141.117" x2="674.990" y2="323.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="674.990" y1="323.841" x2="655.498" y2="309.772" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="507.715" y1="449.567" x2="601.355" y2="632.570" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="601.355" y1="632.570" x2="582.288" y2="639.146" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="593.158" y1="846.038" x2="798.099" y2="884.652" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="798.099" y1="884.652" x2="793.570" y2="900.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="961.349" y1="1028.947" x2="1120.109" y2="888.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1120.109" y1="888.003" x2="1127.812" y2="897.649" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1339.402" y1="852.893" x2="1324.631" y2="636.522" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1324.631" y1="636.522" x2="1332.952" y2="636.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1439.513" y1="441.523" x2="1254.627" y2="317.999" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1254.627" y1="317.999" x2="1256.873" y2="314.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="173.963" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="173.963" x2="744.361" y2="101.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1176.815" y1="100.821" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="173.963" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1271.779" y1="303.892" x2="1256.873" y2="314.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1256.873" y1="314.417" x2="1181.142" y2="93.546" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1347.277" y1="641.124" x2="1332.952" y2="636.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1332.952" y1="636.041" x2="1464.468" y2="437.742" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1131.103" y1="909.328" x2="1127.812" y2="897.649" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1127.812" y1="897.649" x2="1370.608" y2="881.008" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="787.893" y1="907.314" x2="793.570" y2="900.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="793.570" y1="900.289" x2="958.625" y2="1087.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="576.425" y1="638.782" x2="582.288" y2="639.146" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="582.288" y1="639.146" x2="531.894" y2="891.743" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="654.101" y1="307.526" x2="655.498" y2="309.772" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="655.498" y1="309.772" x2="415.868" y2="426.834" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="707.944" y1="31.960" x2="654.101" y2="307.526" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="654.101" y1="307.526" x2="642.940" y2="299.432" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="381.778" y1="420.908" x2="576.425" y2="638.782" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="576.425" y1="638.782" x2="565.486" y2="642.493" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="485.594" y1="932.026" x2="787.893" y2="907.314" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="787.893" y1="907.314" x2="785.235" y2="916.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="961.588" y1="1176.797" x2="1131.103" y2="909.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1131.103" y1="909.328" x2="1135.441" y2="914.874" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1464.690" y1="952.440" x2="1347.277" y2="641.124" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1347.277" y1="641.124" x2="1352.013" y2="640.909" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1609.901" y1="402.354" x2="1271.779" y2="303.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1271.779" y1="303.892" x2="1273.081" y2="301.879" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="152.668" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="152.668" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1260.498" y1="-73.265" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="960.000" y1="152.668" x2="960.000" y2="142.534" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1280.154" y1="296.861" x2="1273.081" y2="301.879" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1273.081" y1="301.879" x2="1274.399" y2="-99.719" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1358.822" y1="643.290" x2="1352.013" y2="640.909" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1352.013" y1="640.909" x2="1698.162" y2="384.929" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1137.036" y1="920.396" x2="1135.441" y2="914.874" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1135.441" y1="914.874" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="782.580" y1="919.577" x2="785.235" y2="916.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="785.235" y1="916.233" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="562.720" y1="642.349" x2="565.486" y2="642.493" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="565.486" y1="642.493" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="642.270" y1="298.383" x2="642.940" y2="299.432" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="642.940" y1="299.432" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="428.039" y1="-542.846" x2="642.270" y2="298.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="-470.272" y1="228.865" x2="562.720" y2="642.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="-509.558" y1="1732.361" x2="782.580" y2="919.577" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="972.408" y1="3428.116" x2="1137.036" y2="920.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="7397.757" y1="5638.793" x2="1358.822" y2="643.290" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="846.478" y1="324.667" x2="867.820" y2="350.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="846.478" y1="324.667" x2="840.722" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="840.722" y1="297.480" x2="820.635" y2="268.446" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="812.520" y1="240.963" x2="820.635" y2="268.446" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="792.977" y1="209.314" x2="812.520" y2="240.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="782.440" y1="179.793" x2="792.977" y2="209.314" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="782.440" y1="179.793" x2="760.577" y2="141.117" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="744.361" y1="101.218" x2="760.577" y2="141.117" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="744.361" y1="101.218" x2="707.944" y2="31.960" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="707.944" y1="31.960" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="428.039" y1="-542.846" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="763.070" y1="509.820" x2="774.927" y2="505.831" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="763.070" y1="509.820" x2="719.271" y2="494.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="719.271" y1="494.328" x2="706.813" y2="496.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="659.219" y1="481.477" x2="706.813" y2="496.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="659.219" y1="481.477" x2="646.566" y2="481.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="596.046" y1="467.620" x2="646.566" y2="481.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="596.046" y1="467.620" x2="582.735" y2="466.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="524.370" y1="451.576" x2="582.735" y2="466.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="524.370" y1="451.576" x2="507.715" y2="449.567" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="507.715" y1="449.567" x2="415.868" y2="426.834" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="415.868" y1="426.834" x2="381.778" y2="420.908" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="381.778" y1="420.908" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="-470.272" y1="228.865" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="802.354" y1="682.911" x2="811.028" y2="664.055" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="802.354" y1="682.911" x2="767.921" y2="700.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="767.921" y1="700.439" x2="755.980" y2="718.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="720.749" y1="739.517" x2="755.980" y2="718.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="706.783" y1="756.515" x2="720.749" y2="739.517" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="670.857" y1="780.283" x2="706.783" y2="756.515" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="670.857" y1="780.283" x2="654.802" y2="797.239" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="614.586" y1="825.722" x2="654.802" y2="797.239" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="614.586" y1="825.722" x2="593.158" y2="846.038" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="593.158" y1="846.038" x2="531.894" y2="891.743" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="531.894" y1="891.743" x2="485.594" y2="932.026" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="485.594" y1="932.026" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="-509.558" y1="1732.361" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="964.714" y1="760.668" x2="956.014" y2="790.291" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="956.014" y1="790.291" x2="963.309" y2="819.481" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="957.220" y1="851.289" x2="963.309" y2="819.481" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="962.311" y1="881.993" x2="957.220" y2="851.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.045" y1="915.439" x2="962.311" y2="881.993" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="961.660" y1="948.507" x2="958.045" y2="915.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.533" y1="986.753" x2="961.660" y2="948.507" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.533" y1="986.753" x2="961.349" y2="1028.947" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.625" y1="1087.870" x2="961.349" y2="1028.947" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="958.625" y1="1087.870" x2="961.588" y2="1176.797" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="961.588" y1="1176.797" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="972.408" y1="3428.116" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1129.234" y1="681.108" x2="1140.858" y2="700.809" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1140.858" y1="700.809" x2="1174.640" y2="719.101" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1188.966" y1="737.811" x2="1174.640" y2="719.101" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1223.079" y1="758.972" x2="1188.966" y2="737.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1239.349" y1="777.041" x2="1223.079" y2="758.972" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1275.085" y1="801.240" x2="1239.349" y2="777.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1294.663" y1="820.579" x2="1275.085" y2="801.240" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1294.663" y1="820.579" x2="1339.402" y2="852.893" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1370.608" y1="881.008" x2="1339.402" y2="852.893" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1370.608" y1="881.008" x2="1464.690" y2="952.440" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1464.690" y1="952.440" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="7397.757" y1="5638.793" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1119.190" y1="510.664" x2="1132.028" y2="516.372" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1132.028" y1="516.372" x2="1171.121" y2="500.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1185.153" y1="503.235" x2="1171.121" y2="500.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1229.348" y1="488.029" x2="1185.153" y2="503.235" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1243.784" y1="489.085" x2="1229.348" y2="488.029" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1290.828" y1="474.767" x2="1243.784" y2="489.085" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1305.599" y1="474.431" x2="1290.828" y2="474.767" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1356.776" y1="460.198" x2="1305.599" y2="474.431" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1373.328" y1="458.637" x2="1356.776" y2="460.198" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1373.328" y1="458.637" x2="1439.513" y2="441.523" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1464.468" y1="437.742" x2="1439.513" y2="441.523" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1464.468" y1="437.742" x2="1609.901" y2="402.354" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1609.901" y1="402.354" x2="1698.162" y2="384.929" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1031.409" y1="399.015" x2="1036.135" y2="397.016" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1036.135" y1="397.016" x2="1055.165" y2="351.011" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1059.283" y1="348.098" x2="1055.165" y2="351.011" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1081.464" y1="297.480" x2="1059.283" y2="348.098" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1085.015" y1="294.014" x2="1081.464" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1109.071" y1="240.873" x2="1085.015" y2="294.014" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1112.243" y1="237.004" x2="1109.071" y2="240.873" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1138.787" y1="179.606" x2="1112.243" y2="237.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1141.961" y1="174.998" x2="1138.787" y2="179.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1141.961" y1="174.998" x2="1176.815" y2="100.821" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1181.142" y1="93.546" x2="1176.815" y2="100.821" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1181.142" y1="93.546" x2="1260.498" y2="-73.265" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+<line x1="1260.498" y1="-73.265" x2="1274.399" y2="-99.719" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />
+</g>
+<polygon points="960.000,552.000 996.876,509.114 1011.832,529.361" fill="hsl(232 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,552.000 1016.521,554.092 1010.016,578.409" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="1016.521,554.092 960.000,552.000 1011.832,529.361" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="996.876,509.114 960.000,552.000 974.617,497.361" fill="hsl(232 88% 60%)" opacity="0.230"/>
+<polygon points="974.617,497.361 960.000,552.000 949.463,496.430" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,552.000 1010.016,578.409 993.605,597.495" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="949.463,496.430 960.000,552.000 926.395,506.505" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,552.000 993.605,597.495 970.537,607.570" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="926.395,506.505 960.000,552.000 909.984,525.591" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,552.000 970.537,607.570 945.383,606.639" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="909.984,525.591 960.000,552.000 903.479,549.908" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,552.000 908.168,574.639 903.479,549.908" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,552.000 923.124,594.886 908.168,574.639" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="923.124,594.886 960.000,552.000 945.383,606.639" fill="hsl(233 88% 60%)" opacity="0.230"/>
+<polygon points="974.617,497.361 949.463,496.430 960.000,481.300" fill="hsl(270 88% 60%)" opacity="0.230"/>
+<polygon points="1011.832,529.361 996.876,509.114 1014.395,506.750" fill="hsl(270 88% 60%)" opacity="0.230"/>
+<polygon points="1016.521,554.092 1043.355,548.453 1029.736,564.916" fill="hsl(267 88% 60%)" opacity="0.230"/>
+<polygon points="1010.016,578.409 1016.521,554.092 1029.736,564.916" fill="hsl(269 88% 60%)" opacity="0.230"/>
+<polygon points="993.605,597.495 1016.470,615.548 994.784,614.125" fill="hsl(265 88% 60%)" opacity="0.230"/>
+<polygon points="970.537,607.570 993.605,597.495 994.784,614.125" fill="hsl(268 88% 60%)" opacity="0.230"/>
+<polygon points="923.124,594.886 945.383,606.639 934.305,618.816" fill="hsl(267 88% 60%)" opacity="0.230"/>
+<polygon points="903.479,549.908 908.168,574.639 891.708,575.064" fill="hsl(266 88% 60%)" opacity="0.230"/>
+<polygon points="909.984,525.591 909.772,497.843 926.395,506.505" fill="hsl(266 88% 60%)" opacity="0.230"/>
+<polygon points="926.395,506.505 940.647,473.854 949.463,496.430" fill="hsl(258 88% 60%)" opacity="0.230"/>
+<polygon points="909.772,497.843 909.984,525.591 898.140,513.838" fill="hsl(266 88% 60%)" opacity="0.230"/>
+<polygon points="903.479,549.908 891.708,575.064 885.389,556.265" fill="hsl(267 88% 60%)" opacity="0.230"/>
+<polygon points="923.124,594.886 934.305,618.816 915.227,613.042" fill="hsl(268 88% 60%)" opacity="0.230"/>
+<polygon points="970.537,607.570 994.784,614.125 978.903,626.402" fill="hsl(270 88% 60%)" opacity="0.230"/>
+<polygon points="1010.016,578.409 1029.736,564.916 1030.517,585.156" fill="hsl(271 88% 60%)" opacity="0.230"/>
+<polygon points="1011.832,529.361 1014.395,506.750 1031.501,518.004" fill="hsl(271 88% 60%)" opacity="0.230"/>
+<polygon points="974.617,497.361 1009.078,486.399 996.876,509.114" fill="hsl(257 88% 60%)" opacity="0.230"/>
+<polygon points="979.353,473.854 974.617,497.361 960.000,481.300" fill="hsl(270 88% 60%)" opacity="0.230"/>
+<polygon points="960.000,481.300 949.463,496.430 940.647,473.854" fill="hsl(274 88% 60%)" opacity="0.230"/>
+<polygon points="940.647,473.854 926.395,506.505 909.772,497.843" fill="hsl(252 88% 60%)" opacity="0.230"/>
+<polygon points="1014.395,506.750 996.876,509.114 1009.078,486.399" fill="hsl(270 88% 60%)" opacity="0.230"/>
+<polygon points="1009.078,486.399 974.617,497.361 979.353,473.854" fill="hsl(256 88% 60%)" opacity="0.230"/>
+<polygon points="1043.355,548.453 1016.521,554.092 1011.832,529.361" fill="hsl(256 88% 60%)" opacity="0.230"/>
+<polygon points="1043.355,548.453 1011.832,529.361 1031.501,518.004" fill="hsl(255 88% 60%)" opacity="0.230"/>
+<polygon points="1016.470,615.548 993.605,597.495 1010.016,578.409" fill="hsl(255 88% 60%)" opacity="0.230"/>
+<polygon points="1016.470,615.548 1010.016,578.409 1030.517,585.156" fill="hsl(254 88% 60%)" opacity="0.230"/>
+<polygon points="945.383,606.639 970.537,607.570 946.088,637.549" fill="hsl(253 88% 60%)" opacity="0.230"/>
+<polygon points="934.305,618.816 945.383,606.639 946.088,637.549" fill="hsl(263 88% 60%)" opacity="0.230"/>
+<polygon points="946.088,637.549 970.537,607.570 978.903,626.402" fill="hsl(253 88% 60%)" opacity="0.230"/>
+<polygon points="908.168,574.639 923.124,594.886 883.325,596.007" fill="hsl(251 88% 60%)" opacity="0.230"/>
+<polygon points="891.708,575.064 908.168,574.639 883.325,596.007" fill="hsl(258 88% 60%)" opacity="0.230"/>
+<polygon points="883.325,596.007 923.124,594.886 915.227,613.042" fill="hsl(251 88% 60%)" opacity="0.230"/>
+<polygon points="898.140,513.838 909.984,525.591 875.839,519.518" fill="hsl(251 88% 60%)" opacity="0.230"/>
+<polygon points="875.839,519.518 909.984,525.591 903.479,549.908" fill="hsl(250 88% 60%)" opacity="0.230"/>
+<polygon points="875.839,519.518 903.479,549.908 885.389,556.265" fill="hsl(250 88% 60%)" opacity="0.230"/>
+<polygon points="940.647,473.854 909.772,497.843 908.803,474.067" fill="hsl(252 88% 60%)" opacity="0.230"/>
+<polygon points="875.839,519.518 885.389,556.265 865.231,542.668" fill="hsl(254 88% 60%)" opacity="0.230"/>
+<polygon points="883.325,596.007 915.227,613.042 891.810,621.371" fill="hsl(257 88% 60%)" opacity="0.230"/>
+<polygon points="946.088,637.549 978.903,626.402 971.504,650.715" fill="hsl(259 88% 60%)" opacity="0.230"/>
+<polygon points="1016.470,615.548 1030.517,585.156 1045.972,606.054" fill="hsl(259 88% 60%)" opacity="0.230"/>
+<polygon points="1043.355,548.453 1031.501,518.004 1058.086,518.092" fill="hsl(257 88% 60%)" opacity="0.230"/>
+<polygon points="940.647,473.854 979.353,473.854 960.000,481.300" fill="hsl(235 88% 60%)" opacity="0.230"/>
+<polygon points="924.305,452.119 940.647,473.854 908.803,474.067" fill="hsl(261 88% 60%)" opacity="0.230"/>
+<polygon points="1009.078,486.399 979.353,473.854 995.695,452.119" fill="hsl(254 88% 60%)" opacity="0.230"/>
+<polygon points="1009.078,486.399 995.695,452.119 1016.855,459.695" fill="hsl(256 88% 60%)" opacity="0.230"/>
+<polygon points="1043.355,548.453 1058.086,518.092 1069.970,538.415" fill="hsl(260 88% 60%)" opacity="0.230"/>
+<polygon points="1016.470,615.548 1045.972,606.054 1041.038,631.119" fill="hsl(262 88% 60%)" opacity="0.230"/>
+<polygon points="946.088,637.549 971.504,650.715 948.510,667.186" fill="hsl(262 88% 60%)" opacity="0.230"/>
+<polygon points="883.325,596.007 891.810,621.371 860.659,616.256" fill="hsl(260 88% 60%)" opacity="0.230"/>
+<polygon points="875.839,519.518 909.772,497.843 898.140,513.838" fill="hsl(230 88% 60%)" opacity="0.230"/>
+<polygon points="845.359,513.562 875.839,519.518 865.231,542.668" fill="hsl(257 88% 60%)" opacity="0.230"/>
+<polygon points="924.305,452.119 908.803,474.067 894.702,445.202" fill="hsl(260 88% 60%)" opacity="0.230"/>
+<polygon points="845.359,513.562 865.231,542.668 833.067,536.251" fill="hsl(256 88% 60%)" opacity="0.230"/>
+<polygon points="860.659,616.256 891.810,621.371 866.681,643.482" fill="hsl(258 88% 60%)" opacity="0.230"/>
+<polygon points="971.504,650.715 1016.470,615.548 1041.038,631.119" fill="hsl(226 88% 60%)" opacity="0.230"/>
+<polygon points="971.504,650.715 1041.038,631.119 973.839,684.781" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="948.510,667.186 971.504,650.715 973.839,684.781" fill="hsl(257 88% 60%)" opacity="0.230"/>
+<polygon points="1045.972,606.054 1043.355,548.453 1069.970,538.415" fill="hsl(226 88% 60%)" opacity="0.230"/>
+<polygon points="1045.972,606.054 1069.970,538.415 1074.989,625.299" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="1041.038,631.119 1045.972,606.054 1074.989,625.299" fill="hsl(255 88% 60%)" opacity="0.230"/>
+<polygon points="1058.086,518.092 1016.855,459.695 1091.658,506.581" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="1069.970,538.415 1058.086,518.092 1091.658,506.581" fill="hsl(252 88% 60%)" opacity="0.230"/>
+<polygon points="995.695,452.119 940.647,473.854 924.305,452.119" fill="hsl(226 88% 60%)" opacity="0.230"/>
+<polygon points="912.747,417.857 924.305,452.119 894.702,445.202" fill="hsl(253 88% 60%)" opacity="0.230"/>
+<polygon points="1016.855,459.695 995.695,452.119 1007.253,417.857" fill="hsl(249 88% 60%)" opacity="0.230"/>
+<polygon points="1009.078,486.399 1058.086,518.092 1031.501,518.004" fill="hsl(227 88% 60%)" opacity="0.230"/>
+<polygon points="1009.078,486.399 1031.501,518.004 1014.395,506.750" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="1037.312,429.076 1016.855,459.695 1007.253,417.857" fill="hsl(248 88% 60%)" opacity="0.230"/>
+<polygon points="1069.970,538.415 1091.658,506.581 1107.369,535.862" fill="hsl(249 88% 60%)" opacity="0.230"/>
+<polygon points="1041.038,631.119 1074.989,625.299 1066.359,659.641" fill="hsl(250 88% 60%)" opacity="0.230"/>
+<polygon points="948.510,667.186 973.839,684.781 941.328,705.304" fill="hsl(250 88% 60%)" opacity="0.230"/>
+<polygon points="883.325,596.007 885.389,556.265 891.708,575.064" fill="hsl(231 88% 60%)" opacity="0.230"/>
+<polygon points="825.558,634.217 860.659,616.256 866.681,643.482" fill="hsl(248 88% 60%)" opacity="0.230"/>
+<polygon points="908.803,474.067 875.839,519.518 845.359,513.562" fill="hsl(224 88% 60%)" opacity="0.230"/>
+<polygon points="845.359,513.562 894.702,445.202 908.803,474.067" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="894.702,445.202 845.359,513.562 809.079,496.569" fill="hsl(218 88% 60%)" opacity="0.230"/>
+<polygon points="909.772,497.843 875.839,519.518 908.803,474.067" fill="hsl(229 88% 60%)" opacity="0.230"/>
+<polygon points="809.079,496.569 845.359,513.562 833.067,536.251" fill="hsl(245 88% 60%)" opacity="0.230"/>
+<polygon points="912.747,417.857 894.702,445.202 868.801,413.347" fill="hsl(246 88% 60%)" opacity="0.230"/>
+<polygon points="868.801,413.347 894.702,445.202 809.079,496.569" fill="hsl(216 88% 60%)" opacity="0.230"/>
+<polygon points="809.079,496.569 833.067,536.251 791.413,537.142" fill="hsl(241 88% 60%)" opacity="0.230"/>
+<polygon points="891.810,621.371 948.510,667.186 866.681,643.482" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="866.681,643.482 941.328,705.304 841.428,677.368" fill="hsl(216 88% 60%)" opacity="0.230"/>
+<polygon points="825.558,634.217 866.681,643.482 841.428,677.368" fill="hsl(242 88% 60%)" opacity="0.230"/>
+<polygon points="973.839,684.781 1066.359,659.641 985.185,726.096" fill="hsl(215 88% 60%)" opacity="0.230"/>
+<polygon points="941.328,705.304 973.839,684.781 985.185,726.096" fill="hsl(241 88% 60%)" opacity="0.230"/>
+<polygon points="1074.989,625.299 1107.369,535.862 1115.066,641.998" fill="hsl(215 88% 60%)" opacity="0.230"/>
+<polygon points="1066.359,659.641 1074.989,625.299 1115.066,641.998" fill="hsl(237 88% 60%)" opacity="0.230"/>
+<polygon points="1058.086,518.092 1009.078,486.399 1016.855,459.695" fill="hsl(226 88% 60%)" opacity="0.230"/>
+<polygon points="1091.658,506.581 1037.312,429.076 1129.979,485.008" fill="hsl(214 88% 60%)" opacity="0.230"/>
+<polygon points="1037.312,429.076 1091.658,506.581 1016.855,459.695" fill="hsl(221 88% 60%)" opacity="0.230"/>
+<polygon points="995.695,452.119 924.305,452.119 912.747,417.857" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="912.747,417.857 1007.253,417.857 995.695,452.119" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="1007.253,417.857 912.747,417.857 906.049,373.842" fill="hsl(211 88% 60%)" opacity="0.230"/>
+<polygon points="979.353,473.854 940.647,473.854 995.695,452.119" fill="hsl(227 88% 60%)" opacity="0.230"/>
+<polygon points="906.049,373.842 912.747,417.857 868.801,413.347" fill="hsl(237 88% 60%)" opacity="0.230"/>
+<polygon points="1007.253,417.857 906.049,373.842 1013.951,373.842" fill="hsl(211 88% 60%)" opacity="0.230"/>
+<polygon points="1129.979,485.008 1037.312,429.076 1068.207,396.285" fill="hsl(212 88% 60%)" opacity="0.230"/>
+<polygon points="1107.369,535.862 1074.989,625.299 1069.970,538.415" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="1066.359,659.641 1115.066,641.998 1091.369,698.330" fill="hsl(230 88% 60%)" opacity="0.230"/>
+<polygon points="946.088,637.549 915.227,613.042 934.305,618.816" fill="hsl(231 88% 60%)" opacity="0.230"/>
+<polygon points="926.172,749.320 941.328,705.304 985.185,726.096" fill="hsl(230 88% 60%)" opacity="0.230"/>
+<polygon points="825.558,634.217 833.067,536.251 860.659,616.256" fill="hsl(218 88% 60%)" opacity="0.230"/>
+<polygon points="860.659,616.256 865.231,542.668 883.325,596.007" fill="hsl(224 88% 60%)" opacity="0.230"/>
+<polygon points="868.801,413.347 809.079,496.569 769.133,470.923" fill="hsl(207 88% 60%)" opacity="0.230"/>
+<polygon points="868.801,413.347 769.133,470.923 834.058,380.006" fill="hsl(206 88% 60%)" opacity="0.230"/>
+<polygon points="906.049,373.842 868.801,413.347 834.058,380.006" fill="hsl(227 88% 60%)" opacity="0.230"/>
+<polygon points="833.067,536.251 825.558,634.217 791.413,537.142" fill="hsl(217 88% 60%)" opacity="0.230"/>
+<polygon points="731.345,662.781 692.378,554.491 743.326,543.879" fill="hsl(201 88% 60%)" opacity="0.230"/>
+<polygon points="841.428,677.368 926.172,749.320 816.777,719.646" fill="hsl(208 88% 60%)" opacity="0.230"/>
+<polygon points="985.185,726.096 1091.369,698.330 1003.272,771.965" fill="hsl(207 88% 60%)" opacity="0.230"/>
+<polygon points="1016.470,615.548 971.504,650.715 978.903,626.402" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="1016.470,615.548 978.903,626.402 994.784,614.125" fill="hsl(231 88% 60%)" opacity="0.230"/>
+<polygon points="1066.359,659.641 973.839,684.781 1041.038,631.119" fill="hsl(219 88% 60%)" opacity="0.230"/>
+<polygon points="1091.369,698.330 985.185,726.096 1066.359,659.641" fill="hsl(213 88% 60%)" opacity="0.230"/>
+<polygon points="1043.355,548.453 1045.972,606.054 1030.517,585.156" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="1115.066,641.998 1152.748,540.007 1162.616,656.290" fill="hsl(205 88% 60%)" opacity="0.230"/>
+<polygon points="1043.355,548.453 1030.517,585.156 1029.736,564.916" fill="hsl(232 88% 60%)" opacity="0.230"/>
+<polygon points="1152.748,540.007 1115.066,641.998 1107.369,535.862" fill="hsl(213 88% 60%)" opacity="0.230"/>
+<polygon points="1129.979,485.008 1068.207,396.285 1170.914,456.336" fill="hsl(203 88% 60%)" opacity="0.230"/>
+<polygon points="1170.914,456.336 1068.207,396.285 1106.050,362.743" fill="hsl(203 88% 60%)" opacity="0.230"/>
+<polygon points="1162.616,656.290 1152.748,540.007 1202.785,548.966" fill="hsl(204 88% 60%)" opacity="0.230"/>
+<polygon points="1003.272,771.965 1091.369,698.330 1115.469,743.362" fill="hsl(205 88% 60%)" opacity="0.230"/>
+<polygon points="926.172,749.320 841.428,677.368 941.328,705.304" fill="hsl(212 88% 60%)" opacity="0.230"/>
+<polygon points="941.328,705.304 866.681,643.482 948.510,667.186" fill="hsl(219 88% 60%)" opacity="0.230"/>
+<polygon points="791.413,537.142 825.558,634.217 781.137,649.633" fill="hsl(210 88% 60%)" opacity="0.230"/>
+<polygon points="692.378,554.491 731.345,662.781 680.396,673.393" fill="hsl(201 88% 60%)" opacity="0.230"/>
+<polygon points="891.810,621.371 946.088,637.549 948.510,667.186" fill="hsl(225 88% 60%)" opacity="0.230"/>
+<polygon points="816.777,719.646 905.691,796.353 793.444,766.277" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="915.227,613.042 946.088,637.549 891.810,621.371" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="905.691,796.353 816.777,719.646 926.172,749.320" fill="hsl(204 88% 60%)" opacity="0.230"/>
+<polygon points="1003.272,771.965 1115.469,743.362 1025.396,819.267" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="1213.493,668.306 1202.785,548.966 1253.661,560.982" fill="hsl(201 88% 60%)" opacity="0.170"/>
+<polygon points="1025.396,819.267 1115.469,743.362 1137.592,790.664" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="793.444,766.277 905.691,796.353 882.359,842.983" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="781.137,649.633 743.326,543.879 791.413,537.142" fill="hsl(207 88% 60%)" opacity="0.230"/>
+<polygon points="865.231,542.668 860.659,616.256 833.067,536.251" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="692.378,554.491 680.396,673.393 642.585,567.639" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="793.444,766.277 882.359,842.983 772.964,813.309" fill="hsl(204 88% 60%)" opacity="0.230"/>
+<polygon points="1025.396,819.267 1137.592,790.664 1049.496,864.300" fill="hsl(205 88% 60%)" opacity="0.230"/>
+<polygon points="1213.493,668.306 1253.661,560.982 1263.530,677.265" fill="hsl(204 88% 60%)" opacity="0.230"/>
+<polygon points="1211.934,423.876 1147.069,330.282 1249.776,390.334" fill="hsl(203 88% 60%)" opacity="0.230"/>
+<polygon points="1249.776,390.334 1147.069,330.282 1188.004,301.611" fill="hsl(203 88% 60%)" opacity="0.230"/>
+<polygon points="1263.530,677.265 1253.661,560.982 1301.211,575.274" fill="hsl(205 88% 60%)" opacity="0.230"/>
+<polygon points="1049.496,864.300 1137.592,790.664 1155.679,836.533" fill="hsl(207 88% 60%)" opacity="0.230"/>
+<polygon points="772.964,813.309 882.359,842.983 857.708,885.262" fill="hsl(208 88% 60%)" opacity="0.230"/>
+<polygon points="743.326,543.879 781.137,649.633 731.345,662.781" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="642.585,567.639 680.396,673.393 632.310,680.130" fill="hsl(207 88% 60%)" opacity="0.230"/>
+<polygon points="883.325,596.007 865.231,542.668 885.389,556.265" fill="hsl(229 88% 60%)" opacity="0.230"/>
+<polygon points="752.883,315.696 687.959,406.613 653.215,373.272" fill="hsl(206 88% 60%)" opacity="0.230"/>
+<polygon points="752.883,315.696 653.215,373.272 712.938,290.050" fill="hsl(207 88% 60%)" opacity="0.230"/>
+<polygon points="642.585,567.639 632.310,680.130 598.165,583.055" fill="hsl(210 88% 60%)" opacity="0.230"/>
+<polygon points="772.964,813.309 857.708,885.262 757.808,857.325" fill="hsl(211 88% 60%)" opacity="0.230"/>
+<polygon points="1049.496,864.300 1155.679,836.533 1074.505,902.988" fill="hsl(212 88% 60%)" opacity="0.230"/>
+<polygon points="1263.530,677.265 1301.211,575.274 1308.909,681.410" fill="hsl(213 88% 60%)" opacity="0.230"/>
+<polygon points="1249.776,390.334 1188.004,301.611 1280.671,357.543" fill="hsl(212 88% 60%)" opacity="0.230"/>
+<polygon points="1013.951,221.118 906.049,221.118 912.747,177.103" fill="hsl(211 88% 60%)" opacity="0.230"/>
+<polygon points="1013.951,221.118 912.747,177.103 1007.253,177.103" fill="hsl(211 88% 60%)" opacity="0.230"/>
+<polygon points="1280.671,357.543 1188.004,301.611 1226.325,280.038" fill="hsl(213 88% 60%)" opacity="0.230"/>
+<polygon points="1308.909,681.410 1301.211,575.274 1341.288,591.973" fill="hsl(215 88% 60%)" opacity="0.230"/>
+<polygon points="1074.505,902.988 1155.679,836.533 1167.025,877.848" fill="hsl(215 88% 60%)" opacity="0.230"/>
+<polygon points="757.808,857.325 857.708,885.262 832.455,919.147" fill="hsl(216 88% 60%)" opacity="0.230"/>
+<polygon points="598.165,583.055 632.310,680.130 590.656,681.021" fill="hsl(217 88% 60%)" opacity="0.230"/>
+<polygon points="712.938,290.050 653.215,373.272 627.315,341.417" fill="hsl(216 88% 60%)" opacity="0.230"/>
+<polygon points="712.938,290.050 627.315,341.417 676.658,273.057" fill="hsl(217 88% 60%)" opacity="0.230"/>
+<polygon points="598.165,583.055 590.656,681.021 563.063,601.016" fill="hsl(218 88% 60%)" opacity="0.230"/>
+<polygon points="757.808,857.325 832.455,919.147 750.625,895.443" fill="hsl(219 88% 60%)" opacity="0.230"/>
+<polygon points="1074.505,902.988 1167.025,877.848 1099.827,931.510" fill="hsl(219 88% 60%)" opacity="0.230"/>
+<polygon points="1308.909,681.410 1341.288,591.973 1346.307,678.857" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="1280.671,357.543 1226.325,280.038 1301.129,326.923" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="1007.253,177.103 912.747,177.103 924.305,142.841" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="1007.253,177.103 924.305,142.841 995.695,142.841" fill="hsl(220 88% 60%)" opacity="0.230"/>
+<polygon points="1301.129,326.923 1226.325,280.038 1259.898,268.527" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="1346.307,678.857 1341.288,591.973 1370.306,611.218" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="1099.827,931.510 1167.025,877.848 1169.360,911.914" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="750.625,895.443 832.455,919.147 807.326,941.258" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="563.063,601.016 590.656,681.021 558.492,674.604" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="676.658,273.057 627.315,341.417 613.214,312.551" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="676.658,273.057 613.214,312.551 646.178,267.101" fill="hsl(224 88% 60%)" opacity="0.230"/>
+<polygon points="563.063,601.016 558.492,674.604 540.398,621.265" fill="hsl(224 88% 60%)" opacity="0.230"/>
+<polygon points="750.625,895.443 807.326,941.258 753.048,925.081" fill="hsl(225 88% 60%)" opacity="0.230"/>
+<polygon points="1099.827,931.510 1169.360,911.914 1124.394,947.081" fill="hsl(225 88% 60%)" opacity="0.230"/>
+<polygon points="1346.307,678.857 1370.306,611.218 1372.922,668.819" fill="hsl(226 88% 60%)" opacity="0.230"/>
+<polygon points="1301.129,326.923 1259.898,268.527 1308.905,300.219" fill="hsl(226 88% 60%)" opacity="0.230"/>
+<polygon points="924.305,142.841 979.353,121.106 995.695,142.841" fill="hsl(227 88% 60%)" opacity="0.230"/>
+<polygon points="979.353,121.106 924.305,142.841 940.647,121.106" fill="hsl(227 88% 60%)" opacity="0.230"/>
+<polygon points="1308.905,300.219 1259.898,268.527 1286.483,268.614" fill="hsl(227 88% 60%)" opacity="0.230"/>
+<polygon points="1372.922,668.819 1370.306,611.218 1385.760,632.116" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="1124.394,947.081 1169.360,911.914 1161.961,936.228" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="753.048,925.081 807.326,941.258 783.908,949.587" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="540.398,621.265 558.492,674.604 538.334,661.008" fill="hsl(229 88% 60%)" opacity="0.230"/>
+<polygon points="646.178,267.101 613.214,312.551 612.245,288.776" fill="hsl(229 88% 60%)" opacity="0.230"/>
+<polygon points="646.178,267.101 612.245,288.776 623.876,272.780" fill="hsl(230 88% 60%)" opacity="0.230"/>
+<polygon points="540.398,621.265 538.334,661.008 532.015,642.208" fill="hsl(230 88% 60%)" opacity="0.230"/>
+<polygon points="753.048,925.081 783.908,949.587 764.831,943.813" fill="hsl(231 88% 60%)" opacity="0.230"/>
+<polygon points="1124.394,947.081 1161.961,936.228 1146.080,948.504" fill="hsl(232 88% 60%)" opacity="0.230"/>
+<polygon points="1372.922,668.819 1385.760,632.116 1386.541,652.356" fill="hsl(232 88% 60%)" opacity="0.230"/>
+<polygon points="1308.905,300.219 1286.483,268.614 1303.589,279.869" fill="hsl(234 88% 60%)" opacity="0.230"/>
+<polygon points="979.353,121.106 940.647,121.106 960.000,113.660" fill="hsl(235 88% 60%)" opacity="0.230"/>
+<polygon points="903.184,323.640 906.049,373.842 834.058,380.006" fill="hsl(222 88% 60%)" opacity="0.230"/>
+<polygon points="794.221,346.709 903.184,323.640 834.058,380.006" fill="hsl(209 88% 60%)" opacity="0.230"/>
+<polygon points="769.133,470.923 809.079,496.569 791.413,537.142" fill="hsl(228 88% 60%)" opacity="0.230"/>
+<polygon points="769.133,470.923 743.326,543.879 727.796,439.910" fill="hsl(213 88% 60%)" opacity="0.230"/>
+<polygon points="743.326,543.879 769.133,470.923 791.413,537.142" fill="hsl(225 88% 60%)" opacity="0.230"/>
+<polygon points="692.378,554.491 727.796,439.910 743.326,543.879" fill="hsl(202 88% 60%)" opacity="0.230"/>
+<polygon points="781.137,649.633 825.558,634.217 841.428,677.368" fill="hsl(229 88% 60%)" opacity="0.230"/>
+<polygon points="781.137,649.633 816.777,719.646 731.345,662.781" fill="hsl(215 88% 60%)" opacity="0.230"/>
+<polygon points="816.777,719.646 781.137,649.633 841.428,677.368" fill="hsl(225 88% 60%)" opacity="0.230"/>
+<polygon points="926.172,749.320 1003.272,771.965 905.691,796.353" fill="hsl(218 88% 60%)" opacity="0.230"/>
+<polygon points="1003.272,771.965 926.172,749.320 985.185,726.096" fill="hsl(224 88% 60%)" opacity="0.230"/>
+<polygon points="1091.369,698.330 1162.616,656.290 1115.469,743.362" fill="hsl(218 88% 60%)" opacity="0.230"/>
+<polygon points="1162.616,656.290 1091.369,698.330 1115.066,641.998" fill="hsl(224 88% 60%)" opacity="0.230"/>
+<polygon points="1107.369,535.862 1129.979,485.008 1152.748,540.007" fill="hsl(230 88% 60%)" opacity="0.230"/>
+<polygon points="1152.748,540.007 1170.914,456.336 1202.785,548.966" fill="hsl(218 88% 60%)" opacity="0.230"/>
+<polygon points="1129.979,485.008 1107.369,535.862 1091.658,506.581" fill="hsl(237 88% 60%)" opacity="0.230"/>
+<polygon points="1170.914,456.336 1152.748,540.007 1129.979,485.008" fill="hsl(223 88% 60%)" opacity="0.230"/>
+<polygon points="1037.312,429.076 1013.951,373.842 1068.207,396.285" fill="hsl(229 88% 60%)" opacity="0.230"/>
+<polygon points="1068.207,396.285 1016.816,323.640 1106.050,362.743" fill="hsl(219 88% 60%)" opacity="0.230"/>
+<polygon points="1013.951,373.842 1037.312,429.076 1007.253,417.857" fill="hsl(231 88% 60%)" opacity="0.230"/>
+<polygon points="1016.816,323.640 1068.207,396.285 1013.951,373.842" fill="hsl(221 88% 60%)" opacity="0.230"/>
+<line x1="960.000" y1="552.000" x2="996.876" y2="509.114" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="1011.832" y2="529.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="996.876" y1="509.114" x2="1011.832" y2="529.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="1010.016" y2="578.409" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1010.016" y1="578.409" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1011.832" y1="529.361" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="996.876" y1="509.114" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="974.617" y1="497.361" x2="949.463" y2="496.430" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="949.463" y2="496.430" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="993.605" y2="597.495" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1010.016" y1="578.409" x2="993.605" y2="597.495" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="949.463" y1="496.430" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="970.537" y2="607.570" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="993.605" y1="597.495" x2="970.537" y2="607.570" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="926.395" y1="506.505" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="945.383" y2="606.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="970.537" y1="607.570" x2="945.383" y2="606.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="909.984" y1="525.591" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.168" y1="574.639" x2="960.000" y2="552.000" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="903.479" y1="549.908" x2="908.168" y2="574.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="552.000" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.168" y1="574.639" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="945.383" y1="606.639" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="974.617" y1="497.361" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="949.463" y1="496.430" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1011.832" y1="529.361" x2="1014.395" y2="506.750" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="996.876" y1="509.114" x2="1014.395" y2="506.750" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.521" y1="554.092" x2="1029.736" y2="564.916" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.521" y1="554.092" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1010.016" y1="578.409" x2="1029.736" y2="564.916" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="993.605" y1="597.495" x2="994.784" y2="614.125" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="993.605" y1="597.495" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="970.537" y1="607.570" x2="994.784" y2="614.125" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="923.124" y1="594.886" x2="934.305" y2="618.816" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="945.383" y1="606.639" x2="934.305" y2="618.816" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="903.479" y1="549.908" x2="891.708" y2="575.064" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.168" y1="574.639" x2="891.708" y2="575.064" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="909.772" y1="497.843" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="909.984" y1="525.591" x2="898.140" y2="513.838" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="473.854" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="926.395" y1="506.505" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="909.772" y1="497.843" x2="898.140" y2="513.838" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="903.479" y1="549.908" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="891.708" y1="575.064" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="923.124" y1="594.886" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="934.305" y1="618.816" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="970.537" y1="607.570" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="994.784" y1="614.125" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1010.016" y1="578.409" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1029.736" y1="564.916" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1011.832" y1="529.361" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1014.395" y1="506.750" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1009.078" y1="486.399" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="974.617" y1="497.361" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="979.353" y1="473.854" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="960.000" y1="481.300" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="949.463" y1="496.430" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="473.854" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1014.395" y1="506.750" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="996.876" y1="509.114" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1009.078" y1="486.399" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1029.736" y1="564.916" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1011.832" y1="529.361" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1043.355" y1="548.453" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="994.784" y1="614.125" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1010.016" y1="578.409" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.470" y1="615.548" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="945.383" y1="606.639" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="970.537" y1="607.570" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="934.305" y1="618.816" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="946.088" y1="637.549" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.168" y1="574.639" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="923.124" y1="594.886" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="891.708" y1="575.064" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="883.325" y1="596.007" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="898.140" y1="513.838" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="909.984" y1="525.591" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="875.839" y1="519.518" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="875.839" y1="519.518" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="473.854" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="909.772" y1="497.843" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="875.839" y1="519.518" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="885.389" y1="556.265" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="883.325" y1="596.007" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="915.227" y1="613.042" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="946.088" y1="637.549" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="978.903" y1="626.402" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.470" y1="615.548" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1030.517" y1="585.156" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1043.355" y1="548.453" x2="1058.086" y2="518.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1031.501" y1="518.004" x2="1058.086" y2="518.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="452.119" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="473.854" x2="924.305" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="924.305" y1="452.119" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1009.078" y1="486.399" x2="995.695" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="979.353" y1="473.854" x2="995.695" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1009.078" y1="486.399" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="452.119" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1043.355" y1="548.453" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1058.086" y1="518.092" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.470" y1="615.548" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1045.972" y1="606.054" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="946.088" y1="637.549" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="971.504" y1="650.715" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="883.325" y1="596.007" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="891.810" y1="621.371" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.803" y1="474.067" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="875.839" y1="519.518" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="845.359" y1="513.562" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="924.305" y1="452.119" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.803" y1="474.067" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="845.359" y1="513.562" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="865.231" y1="542.668" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="860.659" y1="616.256" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="891.810" y1="621.371" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.470" y1="615.548" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="971.504" y1="650.715" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="971.504" y1="650.715" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="948.510" y1="667.186" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1043.355" y1="548.453" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1045.972" y1="606.054" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1045.972" y1="606.054" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1041.038" y1="631.119" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1058.086" y1="518.092" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1058.086" y1="518.092" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1069.970" y1="538.415" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="452.119" x2="924.305" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="924.305" y1="452.119" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="912.747" y1="417.857" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.855" y1="459.695" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="452.119" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1037.312" y1="429.076" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1031.501" y1="518.004" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1037.312" y1="429.076" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1037.312" y1="429.076" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1069.970" y1="538.415" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1091.658" y1="506.581" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1041.038" y1="631.119" x2="1066.359" y2="659.641" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1074.989" y1="625.299" x2="1066.359" y2="659.641" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="948.510" y1="667.186" x2="941.328" y2="705.304" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="973.839" y1="684.781" x2="941.328" y2="705.304" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="860.659" y1="616.256" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="860.659" y1="616.256" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="825.558" y1="634.217" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="908.803" y1="474.067" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="875.839" y1="519.518" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="894.702" y1="445.202" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="894.702" y1="445.202" x2="809.079" y2="496.569" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="809.079" y1="496.569" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="809.079" y1="496.569" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="912.747" y1="417.857" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="894.702" y1="445.202" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="868.801" y1="413.347" x2="809.079" y2="496.569" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="809.079" y1="496.569" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="833.067" y1="536.251" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="948.510" y1="667.186" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="866.681" y1="643.482" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="866.681" y1="643.482" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="825.558" y1="634.217" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="973.839" y1="684.781" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="973.839" y1="684.781" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="941.328" y1="705.304" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1074.989" y1="625.299" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1074.989" y1="625.299" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1066.359" y1="659.641" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1091.658" y1="506.581" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1058.086" y1="518.092" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1091.658" y1="506.581" x2="1129.979" y2="485.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1129.979" y1="485.008" x2="1037.312" y2="429.076" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="452.119" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="473.854" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1007.253" y1="417.857" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1007.253" y1="417.857" x2="906.049" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="373.842" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="373.842" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1007.253" y1="417.857" x2="1013.951" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="373.842" x2="1013.951" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1129.979" y1="485.008" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1037.312" y1="429.076" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1152.748" y1="540.007" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1107.369" y1="535.862" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1066.359" y1="659.641" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1115.066" y1="641.998" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="841.428" y1="677.368" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="941.328" y1="705.304" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="926.172" y1="749.320" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="791.413" y1="537.142" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="825.558" y1="634.217" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="781.137" y1="649.633" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="868.801" y1="413.347" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="809.079" y1="496.569" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="868.801" y1="413.347" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="769.133" y1="470.923" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="373.842" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="865.231" y1="542.668" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="865.231" y1="542.668" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="791.413" y1="537.142" x2="743.326" y2="543.879" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="841.428" y1="677.368" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="941.328" y1="705.304" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="985.185" y1="726.096" x2="1003.272" y2="771.965" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1066.359" y1="659.641" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.470" y1="615.548" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1066.359" y1="659.641" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1091.369" y1="698.330" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1003.272" y1="771.965" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1107.369" y1="535.862" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1030.517" y1="585.156" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1115.066" y1="641.998" x2="1162.616" y2="656.290" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1107.369" y1="535.862" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1162.616" y1="656.290" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1129.979" y1="485.008" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1068.207" y1="396.285" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1013.951" y1="373.842" x2="903.184" y2="323.640" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="373.842" x2="903.184" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1013.951" y1="373.842" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="903.184" y1="323.640" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1170.914" y1="456.336" x2="1106.050" y2="362.743" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1068.207" y1="396.285" x2="1106.050" y2="362.743" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1162.616" y1="656.290" x2="1202.785" y2="548.966" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1152.748" y1="540.007" x2="1202.785" y2="548.966" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1003.272" y1="771.965" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1091.369" y1="698.330" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="905.691" y1="796.353" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="926.172" y1="749.320" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="905.691" y1="796.353" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="791.413" y1="537.142" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="781.137" y1="649.633" x2="731.345" y2="662.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="834.058" y1="380.006" x2="727.796" y2="439.910" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="769.133" y1="470.923" x2="727.796" y2="439.910" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="834.058" y1="380.006" x2="794.221" y2="346.709" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="727.796" y1="439.910" x2="794.221" y2="346.709" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="743.326" y1="543.879" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="731.345" y1="662.781" x2="743.326" y2="543.879" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="941.328" y1="705.304" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="946.088" y1="637.549" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="816.777" y1="719.646" x2="793.444" y2="766.277" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="946.088" y1="637.549" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="793.444" y1="766.277" x2="905.691" y2="796.353" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1003.272" y1="771.965" x2="1025.396" y2="819.267" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1115.469" y1="743.362" x2="1025.396" y2="819.267" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1162.616" y1="656.290" x2="1213.493" y2="668.306" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1202.785" y1="548.966" x2="1213.493" y2="668.306" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1170.914" y1="456.336" x2="1211.934" y2="423.876" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1106.050" y1="362.743" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1016.816" y1="323.640" x2="903.184" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="903.184" y1="323.640" x2="903.184" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.816" y1="323.640" x2="1016.816" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="903.184" y1="271.320" x2="1016.816" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1211.934" y1="423.876" x2="1147.069" y2="330.282" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1106.050" y1="362.743" x2="1147.069" y2="330.282" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1213.493" y1="668.306" x2="1253.661" y2="560.982" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1202.785" y1="548.966" x2="1253.661" y2="560.982" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1025.396" y1="819.267" x2="1137.592" y2="790.664" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1115.469" y1="743.362" x2="1137.592" y2="790.664" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="793.444" y1="766.277" x2="882.359" y2="842.983" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="905.691" y1="796.353" x2="882.359" y2="842.983" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="743.326" y1="543.879" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="731.345" y1="662.781" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="794.221" y1="346.709" x2="687.959" y2="406.613" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="727.796" y1="439.910" x2="687.959" y2="406.613" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="794.221" y1="346.709" x2="752.883" y2="315.696" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="687.959" y1="406.613" x2="752.883" y2="315.696" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="731.345" y1="662.781" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="692.378" y1="554.491" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="692.378" y1="554.491" x2="642.585" y2="567.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="793.444" y1="766.277" x2="772.964" y2="813.309" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="882.359" y1="842.983" x2="772.964" y2="813.309" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1025.396" y1="819.267" x2="1049.496" y2="864.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1137.592" y1="790.664" x2="1049.496" y2="864.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1213.493" y1="668.306" x2="1263.530" y2="677.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1253.661" y1="560.982" x2="1263.530" y2="677.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1211.934" y1="423.876" x2="1249.776" y2="390.334" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1147.069" y1="330.282" x2="1249.776" y2="390.334" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.816" y1="271.320" x2="906.049" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="903.184" y1="271.320" x2="906.049" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1016.816" y1="271.320" x2="1013.951" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="221.118" x2="1013.951" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1249.776" y1="390.334" x2="1188.004" y2="301.611" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1147.069" y1="330.282" x2="1188.004" y2="301.611" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1263.530" y1="677.265" x2="1301.211" y2="575.274" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1253.661" y1="560.982" x2="1301.211" y2="575.274" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1049.496" y1="864.300" x2="1155.679" y2="836.533" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1137.592" y1="790.664" x2="1155.679" y2="836.533" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="772.964" y1="813.309" x2="857.708" y2="885.262" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="882.359" y1="842.983" x2="857.708" y2="885.262" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="642.585" y1="567.639" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="883.325" y1="596.007" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="642.585" y1="567.639" x2="632.310" y2="680.130" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="632.310" y1="680.130" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="752.883" y1="315.696" x2="653.215" y2="373.272" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="687.959" y1="406.613" x2="653.215" y2="373.272" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="752.883" y1="315.696" x2="712.938" y2="290.050" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="653.215" y1="373.272" x2="712.938" y2="290.050" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="642.585" y1="567.639" x2="598.165" y2="583.055" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="632.310" y1="680.130" x2="598.165" y2="583.055" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="772.964" y1="813.309" x2="757.808" y2="857.325" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="857.708" y1="885.262" x2="757.808" y2="857.325" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1049.496" y1="864.300" x2="1074.505" y2="902.988" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1155.679" y1="836.533" x2="1074.505" y2="902.988" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1263.530" y1="677.265" x2="1308.909" y2="681.410" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1301.211" y1="575.274" x2="1308.909" y2="681.410" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1249.776" y1="390.334" x2="1280.671" y2="357.543" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1188.004" y1="301.611" x2="1280.671" y2="357.543" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1013.951" y1="221.118" x2="912.747" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="221.118" x2="912.747" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1013.951" y1="221.118" x2="1007.253" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="912.747" y1="177.103" x2="1007.253" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1280.671" y1="357.543" x2="1226.325" y2="280.038" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1188.004" y1="301.611" x2="1226.325" y2="280.038" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1308.909" y1="681.410" x2="1341.288" y2="591.973" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1301.211" y1="575.274" x2="1341.288" y2="591.973" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1074.505" y1="902.988" x2="1167.025" y2="877.848" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1155.679" y1="836.533" x2="1167.025" y2="877.848" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="757.808" y1="857.325" x2="832.455" y2="919.147" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="857.708" y1="885.262" x2="832.455" y2="919.147" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="598.165" y1="583.055" x2="590.656" y2="681.021" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="632.310" y1="680.130" x2="590.656" y2="681.021" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="712.938" y1="290.050" x2="627.315" y2="341.417" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="653.215" y1="373.272" x2="627.315" y2="341.417" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="712.938" y1="290.050" x2="676.658" y2="273.057" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="627.315" y1="341.417" x2="676.658" y2="273.057" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="598.165" y1="583.055" x2="563.063" y2="601.016" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="590.656" y1="681.021" x2="563.063" y2="601.016" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="757.808" y1="857.325" x2="750.625" y2="895.443" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="832.455" y1="919.147" x2="750.625" y2="895.443" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1074.505" y1="902.988" x2="1099.827" y2="931.510" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1167.025" y1="877.848" x2="1099.827" y2="931.510" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1308.909" y1="681.410" x2="1346.307" y2="678.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1341.288" y1="591.973" x2="1346.307" y2="678.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1280.671" y1="357.543" x2="1301.129" y2="326.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1226.325" y1="280.038" x2="1301.129" y2="326.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1007.253" y1="177.103" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="912.747" y1="177.103" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1007.253" y1="177.103" x2="995.695" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="924.305" y1="142.841" x2="995.695" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1301.129" y1="326.923" x2="1259.898" y2="268.527" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1226.325" y1="280.038" x2="1259.898" y2="268.527" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1346.307" y1="678.857" x2="1370.306" y2="611.218" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1341.288" y1="591.973" x2="1370.306" y2="611.218" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1099.827" y1="931.510" x2="1169.360" y2="911.914" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1167.025" y1="877.848" x2="1169.360" y2="911.914" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="750.625" y1="895.443" x2="807.326" y2="941.258" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="832.455" y1="919.147" x2="807.326" y2="941.258" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="563.063" y1="601.016" x2="558.492" y2="674.604" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="590.656" y1="681.021" x2="558.492" y2="674.604" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="676.658" y1="273.057" x2="613.214" y2="312.551" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="627.315" y1="341.417" x2="613.214" y2="312.551" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="676.658" y1="273.057" x2="646.178" y2="267.101" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="613.214" y1="312.551" x2="646.178" y2="267.101" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="563.063" y1="601.016" x2="540.398" y2="621.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="558.492" y1="674.604" x2="540.398" y2="621.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="750.625" y1="895.443" x2="753.048" y2="925.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="807.326" y1="941.258" x2="753.048" y2="925.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1099.827" y1="931.510" x2="1124.394" y2="947.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1169.360" y1="911.914" x2="1124.394" y2="947.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1346.307" y1="678.857" x2="1372.922" y2="668.819" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1370.306" y1="611.218" x2="1372.922" y2="668.819" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1301.129" y1="326.923" x2="1308.905" y2="300.219" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1259.898" y1="268.527" x2="1308.905" y2="300.219" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="979.353" y1="121.106" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="924.305" y1="142.841" x2="940.647" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="142.841" x2="979.353" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="121.106" x2="979.353" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1308.905" y1="300.219" x2="1286.483" y2="268.614" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1259.898" y1="268.527" x2="1286.483" y2="268.614" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1372.922" y1="668.819" x2="1385.760" y2="632.116" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1370.306" y1="611.218" x2="1385.760" y2="632.116" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1124.394" y1="947.081" x2="1161.961" y2="936.228" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1169.360" y1="911.914" x2="1161.961" y2="936.228" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="753.048" y1="925.081" x2="783.908" y2="949.587" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="807.326" y1="941.258" x2="783.908" y2="949.587" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="540.398" y1="621.265" x2="538.334" y2="661.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="558.492" y1="674.604" x2="538.334" y2="661.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="646.178" y1="267.101" x2="612.245" y2="288.776" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="613.214" y1="312.551" x2="612.245" y2="288.776" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="646.178" y1="267.101" x2="623.876" y2="272.780" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="612.245" y1="288.776" x2="623.876" y2="272.780" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="540.398" y1="621.265" x2="532.015" y2="642.208" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="538.334" y1="661.008" x2="532.015" y2="642.208" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="753.048" y1="925.081" x2="764.831" y2="943.813" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="783.908" y1="949.587" x2="764.831" y2="943.813" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1124.394" y1="947.081" x2="1146.080" y2="948.504" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1161.961" y1="936.228" x2="1146.080" y2="948.504" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1372.922" y1="668.819" x2="1386.541" y2="652.356" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1385.760" y1="632.116" x2="1386.541" y2="652.356" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1308.905" y1="300.219" x2="1303.589" y2="279.869" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1286.483" y1="268.614" x2="1303.589" y2="279.869" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="979.353" y1="121.106" x2="960.000" y2="113.660" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="121.106" x2="960.000" y2="113.660" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="903.184" y1="323.640" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="794.221" y1="346.709" x2="903.184" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="794.221" y1="346.709" x2="903.184" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="903.184" y1="271.320" x2="752.883" y2="315.696" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="221.118" x2="752.883" y2="315.696" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="906.049" y1="221.118" x2="712.938" y2="290.050" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="712.938" y1="290.050" x2="912.747" y2="177.103" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="912.747" y1="177.103" x2="676.658" y2="273.057" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="676.658" y1="273.057" x2="924.305" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="646.178" y1="267.101" x2="924.305" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="121.106" x2="646.178" y2="267.101" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="940.647" y1="121.106" x2="623.876" y2="272.780" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="769.133" y1="470.923" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="743.326" y1="543.879" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="743.326" y1="543.879" x2="727.796" y2="439.910" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="727.796" y1="439.910" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="692.378" y1="554.491" x2="687.959" y2="406.613" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="687.959" y1="406.613" x2="642.585" y2="567.639" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="642.585" y1="567.639" x2="653.215" y2="373.272" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="653.215" y1="373.272" x2="598.165" y2="583.055" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="598.165" y1="583.055" x2="627.315" y2="341.417" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="563.063" y1="601.016" x2="627.315" y2="341.417" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="563.063" y1="601.016" x2="613.214" y2="312.551" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="540.398" y1="621.265" x2="613.214" y2="312.551" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="612.245" y1="288.776" x2="540.398" y2="621.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="612.245" y1="288.776" x2="532.015" y2="642.208" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="781.137" y1="649.633" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="816.777" y1="719.646" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="816.777" y1="719.646" x2="731.345" y2="662.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="731.345" y1="662.781" x2="793.444" y2="766.277" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="680.396" y1="673.393" x2="793.444" y2="766.277" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="680.396" y1="673.393" x2="772.964" y2="813.309" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="772.964" y1="813.309" x2="632.310" y2="680.130" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="632.310" y1="680.130" x2="757.808" y2="857.325" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="757.808" y1="857.325" x2="590.656" y2="681.021" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="750.625" y1="895.443" x2="590.656" y2="681.021" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="750.625" y1="895.443" x2="558.492" y2="674.604" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="753.048" y1="925.081" x2="558.492" y2="674.604" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="538.334" y1="661.008" x2="753.048" y2="925.081" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="538.334" y1="661.008" x2="764.831" y2="943.813" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1003.272" y1="771.965" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1003.272" y1="771.965" x2="905.691" y2="796.353" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="905.691" y1="796.353" x2="1025.396" y2="819.267" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="882.359" y1="842.983" x2="1025.396" y2="819.267" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="882.359" y1="842.983" x2="1049.496" y2="864.300" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="857.708" y1="885.262" x2="1049.496" y2="864.300" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="857.708" y1="885.262" x2="1074.505" y2="902.988" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1074.505" y1="902.988" x2="832.455" y2="919.147" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="832.455" y1="919.147" x2="1099.827" y2="931.510" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1099.827" y1="931.510" x2="807.326" y2="941.258" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1124.394" y1="947.081" x2="807.326" y2="941.258" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="783.908" y1="949.587" x2="1124.394" y2="947.081" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="783.908" y1="949.587" x2="1146.080" y2="948.504" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1162.616" y1="656.290" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1162.616" y1="656.290" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1115.469" y1="743.362" x2="1213.493" y2="668.306" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1137.592" y1="790.664" x2="1213.493" y2="668.306" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1137.592" y1="790.664" x2="1263.530" y2="677.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1155.679" y1="836.533" x2="1263.530" y2="677.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1155.679" y1="836.533" x2="1308.909" y2="681.410" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1308.909" y1="681.410" x2="1167.025" y2="877.848" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1167.025" y1="877.848" x2="1346.307" y2="678.857" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1346.307" y1="678.857" x2="1169.360" y2="911.914" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1372.922" y1="668.819" x2="1169.360" y2="911.914" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1161.961" y1="936.228" x2="1372.922" y2="668.819" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1161.961" y1="936.228" x2="1386.541" y2="652.356" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1129.979" y1="485.008" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1129.979" y1="485.008" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1152.748" y1="540.007" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1202.785" y1="548.966" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1202.785" y1="548.966" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1253.661" y1="560.982" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1253.661" y1="560.982" x2="1249.776" y2="390.334" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1301.211" y1="575.274" x2="1249.776" y2="390.334" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1301.211" y1="575.274" x2="1280.671" y2="357.543" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1280.671" y1="357.543" x2="1341.288" y2="591.973" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1341.288" y1="591.973" x2="1301.129" y2="326.923" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1301.129" y1="326.923" x2="1370.306" y2="611.218" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1308.905" y1="300.219" x2="1370.306" y2="611.218" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1385.760" y1="632.116" x2="1308.905" y2="300.219" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1013.951" y1="373.842" x2="1037.312" y2="429.076" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1013.951" y1="373.842" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1068.207" y1="396.285" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1106.050" y1="362.743" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>
+<line x1="1106.050" y1="362.743" x2="1016.816" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1147.069" y1="330.282" x2="1016.816" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1147.069" y1="330.282" x2="1013.951" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1188.004" y1="301.611" x2="1013.951" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1188.004" y1="301.611" x2="1007.253" y2="177.103" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1007.253" y1="177.103" x2="1226.325" y2="280.038" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1226.325" y1="280.038" x2="995.695" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="995.695" y1="142.841" x2="1259.898" y2="268.527" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="979.353" y1="121.106" x2="1259.898" y2="268.527" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<line x1="1286.483" y1="268.614" x2="979.353" y2="121.106" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>
+<g filter="url(#hole-glow)"><path d="M 1013.951,373.842 L 906.049,373.842 L 903.184,323.640 L 1016.816,323.640 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+<path d="M 834.058,380.006 L 769.133,470.923 L 727.796,439.910 L 794.221,346.709 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+<path d="M 1162.616,656.290 L 1202.785,548.966 L 1213.493,668.306 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+<path d="M 1170.914,456.336 L 1106.050,362.743 L 1147.069,330.282 L 1211.934,423.876 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+<path d="M 1016.816,323.640 L 903.184,323.640 L 903.184,271.320 L 1016.816,271.320 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+<path d="M 794.221,346.709 L 727.796,439.910 L 687.959,406.613 L 752.883,315.696 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+<path d="M 1016.816,271.320 L 903.184,271.320 L 906.049,221.118 L 1013.951,221.118 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>
+</g>
+<circle cx="960.000" cy="552.000" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="996.876" cy="509.114" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1011.832" cy="529.361" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1010.016" cy="578.409" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1016.521" cy="554.092" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="974.617" cy="497.361" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="949.463" cy="496.430" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="993.605" cy="597.495" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="926.395" cy="506.505" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="970.537" cy="607.570" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="909.984" cy="525.591" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="945.383" cy="606.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="903.479" cy="549.908" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="908.168" cy="574.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="923.124" cy="594.886" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="960.000" cy="481.300" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1014.395" cy="506.750" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1029.736" cy="564.916" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="994.784" cy="614.125" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="934.305" cy="618.816" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="891.708" cy="575.064" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="898.140" cy="513.838" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="909.772" cy="497.843" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="885.389" cy="556.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="915.227" cy="613.042" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="978.903" cy="626.402" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1030.517" cy="585.156" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1031.501" cy="518.004" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="979.353" cy="473.854" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="940.647" cy="473.854" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1009.078" cy="486.399" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1043.355" cy="548.453" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1016.470" cy="615.548" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="946.088" cy="637.549" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="883.325" cy="596.007" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="875.839" cy="519.518" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="908.803" cy="474.067" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="865.231" cy="542.668" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="891.810" cy="621.371" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="971.504" cy="650.715" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1045.972" cy="606.054" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1058.086" cy="518.092" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="924.305" cy="452.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="995.695" cy="452.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1016.855" cy="459.695" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1069.970" cy="538.415" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1041.038" cy="631.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="948.510" cy="667.186" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="860.659" cy="616.256" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="845.359" cy="513.562" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="894.702" cy="445.202" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="833.067" cy="536.251" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="866.681" cy="643.482" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="973.839" cy="684.781" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1074.989" cy="625.299" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1091.658" cy="506.581" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="912.747" cy="417.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1007.253" cy="417.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1037.312" cy="429.076" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1107.369" cy="535.862" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1066.359" cy="659.641" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="941.328" cy="705.304" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="825.558" cy="634.217" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="809.079" cy="496.569" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="868.801" cy="413.347" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="791.413" cy="537.142" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="841.428" cy="677.368" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="985.185" cy="726.096" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1115.066" cy="641.998" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1129.979" cy="485.008" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="906.049" cy="373.842" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1013.951" cy="373.842" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1068.207" cy="396.285" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1152.748" cy="540.007" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1091.369" cy="698.330" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="926.172" cy="749.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="781.137" cy="649.633" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="769.133" cy="470.923" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="834.058" cy="380.006" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="743.326" cy="543.879" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="816.777" cy="719.646" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1003.272" cy="771.965" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1162.616" cy="656.290" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1170.914" cy="456.336" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="903.184" cy="323.640" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1016.816" cy="323.640" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1106.050" cy="362.743" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1202.785" cy="548.966" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1115.469" cy="743.362" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="905.691" cy="796.353" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="731.345" cy="662.781" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="727.796" cy="439.910" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="794.221" cy="346.709" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="692.378" cy="554.491" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="793.444" cy="766.277" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1025.396" cy="819.267" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1213.493" cy="668.306" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1211.934" cy="423.876" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="903.184" cy="271.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1016.816" cy="271.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1147.069" cy="330.282" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1253.661" cy="560.982" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1137.592" cy="790.664" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="882.359" cy="842.983" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="680.396" cy="673.393" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="687.959" cy="406.613" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="752.883" cy="315.696" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="642.585" cy="567.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="772.964" cy="813.309" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1049.496" cy="864.300" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1263.530" cy="677.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1249.776" cy="390.334" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="906.049" cy="221.118" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1013.951" cy="221.118" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1188.004" cy="301.611" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1301.211" cy="575.274" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1155.679" cy="836.533" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="857.708" cy="885.262" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="632.310" cy="680.130" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="653.215" cy="373.272" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="712.938" cy="290.050" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="598.165" cy="583.055" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="757.808" cy="857.325" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1074.505" cy="902.988" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1308.909" cy="681.410" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1280.671" cy="357.543" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="912.747" cy="177.103" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1007.253" cy="177.103" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1226.325" cy="280.038" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1341.288" cy="591.973" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1167.025" cy="877.848" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="832.455" cy="919.147" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="590.656" cy="681.021" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="627.315" cy="341.417" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="676.658" cy="273.057" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="563.063" cy="601.016" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="750.625" cy="895.443" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1099.827" cy="931.510" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1346.307" cy="678.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1301.129" cy="326.923" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="924.305" cy="142.841" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="995.695" cy="142.841" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1259.898" cy="268.527" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1370.306" cy="611.218" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1169.360" cy="911.914" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="807.326" cy="941.258" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="558.492" cy="674.604" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="613.214" cy="312.551" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="646.178" cy="267.101" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="540.398" cy="621.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="753.048" cy="925.081" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1124.394" cy="947.081" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1372.922" cy="668.819" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1308.905" cy="300.219" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="940.647" cy="121.106" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="979.353" cy="121.106" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1286.483" cy="268.614" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1385.760" cy="632.116" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1161.961" cy="936.228" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="783.908" cy="949.587" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="538.334" cy="661.008" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="612.245" cy="288.776" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="623.876" cy="272.780" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="532.015" cy="642.208" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="764.831" cy="943.813" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1146.080" cy="948.504" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1386.541" cy="652.356" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="1303.589" cy="279.869" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+<circle cx="960.000" cy="113.660" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>
+</g>
+<path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill="none" stroke="url(#mask-stroke)" stroke-width="2.2" opacity="0.92" filter="url(#edge-glow)"/>
+<g font-family="Inter, Avenir Next, Helvetica, Arial, sans-serif" fill="#f7f3ff">
+<text x="72" y="82" font-size="24" font-weight="600" letter-spacing="8" fill="#9ff7ff">MOONLIGHT TRIANGULATION</text>
+<text x="70" y="145" font-size="58" font-weight="780" letter-spacing="-1.5">PERSISTENCE ROSE</text>
+<g transform="translate(1500 790)">
+<text x="0" y="0" font-size="18" letter-spacing="2.4" fill="#91a8d8">TOPOLOGICAL HOLES AT THIS SCALE</text>
+<text x="0" y="74" font-size="78" font-weight="720" fill="#fff5cd">β₁ = 7</text>
+<text x="2" y="116" font-size="17" fill="#c9d3ff">7 independent one-dimensional holes</text>
+<text x="2" y="144" font-size="15" fill="#9ba8d4">in the exact alpha complex</text>
+<text x="2" y="176" font-size="14" fill="#7682aa">critical rank 538 · α² = 0.358</text>
+<text x="2" y="200" font-size="12" fill="#596486">exact 94-digit / 94-digit rational</text>
+</g>
+</g>
+</svg>
diff --git a/docs/social/moonlight-triangulation-persistence-rose.gif b/docs/social/moonlight-triangulation-persistence-rose.gif
deleted file mode 100644
Binary files a/docs/social/moonlight-triangulation-persistence-rose.gif and /dev/null differ
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Boundary.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Boundary.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Boundary.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE TypeFamilies #-}
+
+module Moonlight.Triangulation.Foreign.Boundary
+  ( GeometryMesh
+  , HandleValue
+  , checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , prepareHandleOutput
+  , produceHandle
+  , publishHandle
+  , readPoints
+  , requireOutputCapacity
+  , requirePointer
+  , runBoundary
+  ) where
+
+import Control.Exception (SomeException, try)
+import Foreign.C.Types (CDouble (..), CSize, CUInt)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.StablePtr
+  ( StablePtr
+  , castPtrToStablePtr
+  , castStablePtrToPtr
+  , deRefStablePtr
+  , freeStablePtr
+  , newStablePtr
+  )
+import Foreign.Storable (peekElemOff, poke)
+import qualified Data.Vector as V
+import qualified Moonlight.Triangulation as T
+import Moonlight.Triangulation.Foreign.Contract
+  ( CMesh
+  , CObstruction
+  , CRegion
+  , CStructuringElement
+  )
+import Moonlight.Triangulation.Foreign.Obstruction
+  ( AbiFailure (..)
+  , bufferTooSmallFailure
+  , countOverflowFailure
+  , emptyObstruction
+  , nullPointerFailure
+  , runtimeFailure
+  , statusOk
+  )
+
+type GeometryMesh = T.DelaunayTriangulation ()
+
+type family HandleValue carrier where
+  HandleValue CMesh = GeometryMesh
+  HandleValue CRegion = T.PlanarRegion
+  HandleValue CStructuringElement = T.StructuringElement
+
+runBoundary :: Ptr CObstruction -> IO (Either AbiFailure ()) -> IO CUInt
+runBoundary obstructionPointer action = do
+  writeObstruction obstructionPointer emptyObstruction
+  outcome <- try action :: IO (Either SomeException (Either AbiFailure ()))
+  case outcome of
+    Left exception -> finishFailure obstructionPointer (runtimeFailure exception)
+    Right (Left failure) -> finishFailure obstructionPointer failure
+    Right (Right ()) -> pure statusOk
+
+finishFailure :: Ptr CObstruction -> AbiFailure -> IO CUInt
+finishFailure obstructionPointer (AbiFailure status obstruction) = do
+  writeObstruction obstructionPointer obstruction
+  pure status
+
+writeObstruction :: Ptr CObstruction -> CObstruction -> IO ()
+writeObstruction pointer obstruction
+  | pointer == nullPtr = pure ()
+  | otherwise = poke pointer obstruction
+
+requirePointer :: String -> Ptr value -> Either AbiFailure ()
+requirePointer label pointer
+  | pointer == nullPtr = Left (nullPointerFailure label)
+  | otherwise = Right ()
+
+checkedCount :: Int -> CSize -> Either AbiFailure Int
+checkedCount elementsPerItem rawCount
+  | toInteger rawCount * toInteger elementsPerItem > toInteger (maxBound :: Int) =
+      Left (countOverflowFailure (fromIntegral rawCount))
+  | otherwise = Right (fromIntegral rawCount)
+
+readPoints :: Ptr CDouble -> Int -> IO (Either AbiFailure (V.Vector T.Point))
+readPoints pointer count
+  | count == 0 = pure (Right V.empty)
+  | pointer == nullPtr = pure (Left (nullPointerFailure "coordinates"))
+  | otherwise =
+      Right
+        <$> V.generateM
+          count
+          ( \index -> do
+              CDouble x <- peekElemOff pointer (index * 2)
+              CDouble y <- peekElemOff pointer (index * 2 + 1)
+              pure (T.Point x y)
+          )
+
+prepareHandleOutput :: Ptr (Ptr carrier) -> IO (Either AbiFailure ())
+prepareHandleOutput pointer =
+  case requirePointer "result" pointer of
+    Left failure -> pure (Left failure)
+    Right () -> poke pointer nullPtr >> pure (Right ())
+
+publishHandle :: Ptr (Ptr carrier) -> HandleValue carrier -> IO ()
+publishHandle output value = do
+  stable <- newStablePtr value
+  poke output (castPtr (castStablePtrToPtr stable))
+
+produceHandle
+  :: Ptr (Ptr carrier)
+  -> IO (Either AbiFailure (HandleValue carrier))
+  -> IO (Either AbiFailure ())
+produceHandle output obtain = do
+  prepared <- prepareHandleOutput output
+  case prepared of
+    Left failure -> pure (Left failure)
+    Right () -> do
+      outcome <- obtain
+      case outcome of
+        Left failure -> pure (Left failure)
+        Right value -> publishHandle output value >> pure (Right ())
+
+dereferenceHandle :: Ptr carrier -> IO (HandleValue carrier)
+dereferenceHandle pointer = deRefStablePtr (castPtrToStablePtr (castPtr pointer))
+
+freeHandle :: Ptr carrier -> IO ()
+freeHandle pointer
+  | pointer == nullPtr = pure ()
+  | otherwise = freeStablePtr (castPtrToStablePtr (castPtr pointer) :: StablePtr ())
+
+requireOutputCapacity :: String -> Ptr value -> Int -> Int -> Either AbiFailure ()
+requireOutputCapacity label output required capacity
+  | capacity < required = Left (bufferTooSmallFailure required capacity)
+  | required > 0 = requirePointer label output
+  | otherwise = Right ()
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Contract.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Contract.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Contract.hs
@@ -0,0 +1,650 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Moonlight.Triangulation.Foreign.Contract
+  ( AbiField (..)
+  , AbiFieldType (..)
+  , AbiFunction
+  , AbiFunctionFamily (..)
+  , AbiFunctionImplementation (..)
+  , AbiFunctionName (..)
+  , AbiHandle (..)
+  , AbiParameter (..)
+  , AbiParameterKind (..)
+  , AbiResult (..)
+  , AbiStatus (..)
+  , AbiStruct (..)
+  , CMesh
+  , CMinkowskiReceipt (..)
+  , CObstruction (..)
+  , CRegion
+  , CStructuringElement
+  , CoordinateErrorCode (..)
+  , MinkowskiOperationCode (..)
+  , ObstructionCode (..)
+  , RegionLocationCode (..)
+  , abiFunction
+  , abiFunctionImplementation
+  , abiFunctionParameters
+  , abiFunctionResult
+  , abiFunctionSymbol
+  , abiHandleSymbol
+  , abiStatusId
+  , abiStatusSymbol
+  , abiStructFields
+  , abiStructSymbol
+  , abiVersion
+  , allAbiFunctionNames
+  , allAbiFunctions
+  , allAbiHandles
+  , allAbiStatuses
+  , allAbiStructs
+  , allCoordinateErrorCodes
+  , allMinkowskiOperationCodes
+  , allObstructionCodes
+  , allRegionLocationCodes
+  , coordinateErrorCodeId
+  , coordinateErrorCodeSymbol
+  , minkowskiOperationCodeId
+  , minkowskiOperationCodeSymbol
+  , obstructionCodeId
+  , obstructionCodeSymbol
+  , regionLocationCodeId
+  , regionLocationCodeSymbol
+  ) where
+
+import Data.Word (Word32, Word64)
+import Foreign.C.String (peekCString, withCStringLen)
+import Foreign.C.Types (CChar)
+import Foreign.Marshal.Utils (copyBytes, fillBytes)
+import Foreign.Ptr (Ptr, castPtr, plusPtr)
+import Foreign.Storable (Storable (..))
+import GHC.Generics (Generic)
+
+abiVersion :: Word32
+abiVersion = 2
+
+data AbiStatus
+  = AbiStatusOk
+  | AbiStatusNullPointer
+  | AbiStatusCountOverflow
+  | AbiStatusBufferTooSmall
+  | AbiStatusGeometryObstruction
+  | AbiStatusRuntimeFailure
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allAbiStatuses :: [AbiStatus]
+allAbiStatuses = [minBound .. maxBound]
+
+abiStatusId :: AbiStatus -> Word32
+abiStatusId status =
+  case status of
+    AbiStatusOk -> 0
+    AbiStatusNullPointer -> 1
+    AbiStatusCountOverflow -> 2
+    AbiStatusBufferTooSmall -> 3
+    AbiStatusGeometryObstruction -> 4
+    AbiStatusRuntimeFailure -> 5
+
+abiStatusSymbol :: AbiStatus -> String
+abiStatusSymbol status =
+  case status of
+    AbiStatusOk -> "ML_STATUS_OK"
+    AbiStatusNullPointer -> "ML_STATUS_NULL_POINTER"
+    AbiStatusCountOverflow -> "ML_STATUS_COUNT_OVERFLOW"
+    AbiStatusBufferTooSmall -> "ML_STATUS_BUFFER_TOO_SMALL"
+    AbiStatusGeometryObstruction -> "ML_STATUS_GEOMETRY_OBSTRUCTION"
+    AbiStatusRuntimeFailure -> "ML_STATUS_RUNTIME_FAILURE"
+
+data ObstructionCode
+  = ObstructionNone
+  | ObstructionInvalidCoordinate
+  | ObstructionPointLocationFailed
+  | ObstructionLocationWalkExhausted
+  | ObstructionRefinementInputTopologyInvalid
+  | ObstructionFreshInsertionMatchedExistingVertex
+  | ObstructionDegenerateLineEndpointMissingOutgoing
+  | ObstructionDegenerateLineEndpointTurnMissing
+  | ObstructionDegenerateLineConnectedVertexMissing
+  | ObstructionHullStartNotVisible
+  | ObstructionOuterRangeDidNotTerminate
+  | ObstructionOuterRangeContainsInnerEdge
+  | ObstructionConstrainedEdgeFlipRefused
+  | ObstructionRemovalVertexOutOfRange
+  | ObstructionRemovalEdgeOutOfRange
+  | ObstructionRemovalFaceOutOfRange
+  | ObstructionRemovalFaceCycleDidNotTerminate
+  | ObstructionRemovalEmptyTriangulation
+  | ObstructionRemovalTwoPointDegreeMismatch
+  | ObstructionRemovalCollinearDegreeMismatch
+  | ObstructionRemovalBorderTooShort
+  | ObstructionRemovalBorderArityMismatch
+  | ObstructionRemovalOutgoingCycleDidNotTerminate
+  | ObstructionCircleSweepHullEmpty
+  | ObstructionOuterCycleDidNotTerminate
+  | ObstructionHierarchyLevelPopulationMismatch
+  | ObstructionHierarchyInsertionHandleMismatch
+  | ObstructionPointIndexCapacityExhausted
+  | ObstructionRefinementMinimumAngleNotFinite
+  | ObstructionRefinementMinimumAngleOutOfRange
+  | ObstructionRefinementMinimumAngleDerivedRatioNotFinite
+  | ObstructionRefinementMaximumAdditionalVerticesNegative
+  | ObstructionRefinementMinimumAreaNotFinite
+  | ObstructionRefinementMinimumAreaNegative
+  | ObstructionRefinementMaximumAreaNotFinite
+  | ObstructionRefinementMaximumAreaNotPositive
+  | ObstructionRefinementMaximumRadiusEdgeRatioNotFinite
+  | ObstructionRefinementMaximumRadiusEdgeRatioNotPositive
+  | ObstructionRefinementMinimumAreaExceedsMaximum
+  | ObstructionRefinementSeedFaceNotActive
+  | ObstructionRefinementDomainInterfaceEdgeNotActive
+  | ObstructionRefinementDomainInterfaceMissing
+  | ObstructionRefinementDomainInterfaceExtraneous
+  | ObstructionRefinementDomainTopologyChanged
+  | ObstructionRefinementDomainRequiresConvexHullPreservation
+  | ObstructionRefinementDomainRequiresConstraintPreservation
+  | ObstructionRefinementDomainForbidsOuterFaceExclusion
+  | ObstructionRefinementDomainWouldCrossInterface
+  | ObstructionRefinementDomainWouldRewriteProtectedFace
+  | ObstructionRefinementDomainProtectedFaceChanged
+  | ObstructionCapacityExceeded
+  | ObstructionHalfEdgeCapacityExceeded
+  | ObstructionFaceCapacityExceeded
+  | ObstructionPayloadStorageFailure
+  | ObstructionCoordinatePayloadCountMismatch
+  | ObstructionCircleSweepRequiresDenseStorage
+  | ObstructionSeamFrontierUnavailable
+  | ObstructionRefinementDomainRequiresFiniteVertexBudget
+  | ObstructionSeamSourceEdgeRequiresFlip
+  | ObstructionSeamProtectedSourceFaceChanged
+  | ObstructionRefinementMaximumEdgeLengthNotFinite
+  | ObstructionRefinementMaximumEdgeLengthNotPositive
+  | ObstructionRefinementOversizedEdge
+  | ObstructionRefinementSeamBridgeBudgetExceeded
+  | ObstructionRefinementSeamBridgeMidpointCollapsed
+  | ObstructionBoundarySplitRequiresBoundaryEdge
+  | ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted
+  | ObstructionNullPointer
+  | ObstructionCountOverflow
+  | ObstructionBufferTooSmall
+  | ObstructionRuntimeFailure
+  | ObstructionRegionLayoutInvalid
+  | ObstructionRegionValidationFailed
+  | ObstructionOverlayFailed
+  | ObstructionRegionPublicationFailed
+  | ObstructionValuationFailed
+  | ObstructionMinkowskiFailed
+  | ObstructionRegionProjectionFailed
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allObstructionCodes :: [ObstructionCode]
+allObstructionCodes = [minBound .. maxBound]
+
+obstructionCodeId :: ObstructionCode -> Word32
+obstructionCodeId code = fst (obstructionCodeMetadata code)
+
+obstructionCodeSymbol :: ObstructionCode -> String
+obstructionCodeSymbol code = snd (obstructionCodeMetadata code)
+
+obstructionCodeMetadata :: ObstructionCode -> (Word32, String)
+obstructionCodeMetadata code =
+  case code of
+    ObstructionNone -> (0, "ML_OBSTRUCTION_NONE")
+    ObstructionInvalidCoordinate -> (1, "ML_OBSTRUCTION_INVALID_COORDINATE")
+    ObstructionPointLocationFailed -> (2, "ML_OBSTRUCTION_POINT_LOCATION_FAILED")
+    ObstructionLocationWalkExhausted -> (3, "ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED")
+    ObstructionRefinementInputTopologyInvalid -> (4, "ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID")
+    ObstructionFreshInsertionMatchedExistingVertex -> (5, "ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX")
+    ObstructionDegenerateLineEndpointMissingOutgoing -> (6, "ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING")
+    ObstructionDegenerateLineEndpointTurnMissing -> (7, "ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING")
+    ObstructionDegenerateLineConnectedVertexMissing -> (8, "ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING")
+    ObstructionHullStartNotVisible -> (9, "ML_OBSTRUCTION_HULL_START_NOT_VISIBLE")
+    ObstructionOuterRangeDidNotTerminate -> (10, "ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE")
+    ObstructionOuterRangeContainsInnerEdge -> (11, "ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE")
+    ObstructionConstrainedEdgeFlipRefused -> (12, "ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED")
+    ObstructionRemovalVertexOutOfRange -> (13, "ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE")
+    ObstructionRemovalEdgeOutOfRange -> (14, "ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE")
+    ObstructionRemovalFaceOutOfRange -> (15, "ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE")
+    ObstructionRemovalFaceCycleDidNotTerminate -> (16, "ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE")
+    ObstructionRemovalEmptyTriangulation -> (17, "ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION")
+    ObstructionRemovalTwoPointDegreeMismatch -> (18, "ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH")
+    ObstructionRemovalCollinearDegreeMismatch -> (19, "ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH")
+    ObstructionRemovalBorderTooShort -> (20, "ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT")
+    ObstructionRemovalBorderArityMismatch -> (21, "ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH")
+    ObstructionRemovalOutgoingCycleDidNotTerminate -> (22, "ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE")
+    ObstructionCircleSweepHullEmpty -> (23, "ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY")
+    ObstructionOuterCycleDidNotTerminate -> (24, "ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE")
+    ObstructionHierarchyLevelPopulationMismatch -> (25, "ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH")
+    ObstructionHierarchyInsertionHandleMismatch -> (26, "ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH")
+    ObstructionPointIndexCapacityExhausted -> (27, "ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED")
+    ObstructionRefinementMinimumAngleNotFinite -> (28, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE")
+    ObstructionRefinementMinimumAngleOutOfRange -> (29, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE")
+    ObstructionRefinementMinimumAngleDerivedRatioNotFinite -> (30, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE")
+    ObstructionRefinementMaximumAdditionalVerticesNegative -> (31, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE")
+    ObstructionRefinementMinimumAreaNotFinite -> (32, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE")
+    ObstructionRefinementMinimumAreaNegative -> (33, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE")
+    ObstructionRefinementMaximumAreaNotFinite -> (34, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE")
+    ObstructionRefinementMaximumAreaNotPositive -> (35, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE")
+    ObstructionRefinementMaximumRadiusEdgeRatioNotFinite -> (36, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE")
+    ObstructionRefinementMaximumRadiusEdgeRatioNotPositive -> (37, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE")
+    ObstructionRefinementMinimumAreaExceedsMaximum -> (38, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM")
+    ObstructionRefinementSeedFaceNotActive -> (39, "ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE")
+    ObstructionRefinementDomainInterfaceEdgeNotActive -> (40, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE")
+    ObstructionRefinementDomainInterfaceMissing -> (41, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING")
+    ObstructionRefinementDomainInterfaceExtraneous -> (42, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS")
+    ObstructionRefinementDomainTopologyChanged -> (43, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED")
+    ObstructionRefinementDomainRequiresConvexHullPreservation -> (44, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION")
+    ObstructionRefinementDomainRequiresConstraintPreservation -> (45, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION")
+    ObstructionRefinementDomainForbidsOuterFaceExclusion -> (46, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION")
+    ObstructionRefinementDomainWouldCrossInterface -> (47, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE")
+    ObstructionRefinementDomainWouldRewriteProtectedFace -> (48, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE")
+    ObstructionRefinementDomainProtectedFaceChanged -> (49, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED")
+    ObstructionCapacityExceeded -> (50, "ML_OBSTRUCTION_CAPACITY_EXCEEDED")
+    ObstructionHalfEdgeCapacityExceeded -> (51, "ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED")
+    ObstructionFaceCapacityExceeded -> (52, "ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED")
+    ObstructionPayloadStorageFailure -> (53, "ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE")
+    ObstructionCoordinatePayloadCountMismatch -> (54, "ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH")
+    ObstructionCircleSweepRequiresDenseStorage -> (55, "ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE")
+    ObstructionSeamFrontierUnavailable -> (56, "ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE")
+    ObstructionRefinementDomainRequiresFiniteVertexBudget -> (57, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET")
+    ObstructionSeamSourceEdgeRequiresFlip -> (58, "ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP")
+    ObstructionSeamProtectedSourceFaceChanged -> (59, "ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED")
+    ObstructionRefinementMaximumEdgeLengthNotFinite -> (60, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE")
+    ObstructionRefinementMaximumEdgeLengthNotPositive -> (61, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE")
+    ObstructionRefinementOversizedEdge -> (62, "ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE")
+    ObstructionRefinementSeamBridgeBudgetExceeded -> (63, "ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED")
+    ObstructionRefinementSeamBridgeMidpointCollapsed -> (64, "ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED")
+    ObstructionBoundarySplitRequiresBoundaryEdge -> (65, "ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE")
+    ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted -> (66, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED")
+    ObstructionNullPointer -> (100, "ML_OBSTRUCTION_NULL_POINTER")
+    ObstructionCountOverflow -> (101, "ML_OBSTRUCTION_COUNT_OVERFLOW")
+    ObstructionBufferTooSmall -> (102, "ML_OBSTRUCTION_BUFFER_TOO_SMALL")
+    ObstructionRuntimeFailure -> (103, "ML_OBSTRUCTION_RUNTIME_FAILURE")
+    ObstructionRegionLayoutInvalid -> (200, "ML_OBSTRUCTION_REGION_LAYOUT_INVALID")
+    ObstructionRegionValidationFailed -> (201, "ML_OBSTRUCTION_REGION_VALIDATION_FAILED")
+    ObstructionOverlayFailed -> (202, "ML_OBSTRUCTION_OVERLAY_FAILED")
+    ObstructionRegionPublicationFailed -> (203, "ML_OBSTRUCTION_REGION_PUBLICATION_FAILED")
+    ObstructionValuationFailed -> (204, "ML_OBSTRUCTION_VALUATION_FAILED")
+    ObstructionMinkowskiFailed -> (205, "ML_OBSTRUCTION_MINKOWSKI_FAILED")
+    ObstructionRegionProjectionFailed -> (206, "ML_OBSTRUCTION_REGION_PROJECTION_FAILED")
+
+data CoordinateErrorCode
+  = CoordinateErrorNone
+  | CoordinateErrorNaN
+  | CoordinateErrorInfinite
+  | CoordinateErrorTooSmall
+  | CoordinateErrorTooLarge
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allCoordinateErrorCodes :: [CoordinateErrorCode]
+allCoordinateErrorCodes = [minBound .. maxBound]
+
+coordinateErrorCodeId :: CoordinateErrorCode -> Word32
+coordinateErrorCodeId code =
+  case code of
+    CoordinateErrorNone -> 0
+    CoordinateErrorNaN -> 1
+    CoordinateErrorInfinite -> 2
+    CoordinateErrorTooSmall -> 3
+    CoordinateErrorTooLarge -> 4
+
+coordinateErrorCodeSymbol :: CoordinateErrorCode -> String
+coordinateErrorCodeSymbol code =
+  case code of
+    CoordinateErrorNone -> "ML_COORDINATE_ERROR_NONE"
+    CoordinateErrorNaN -> "ML_COORDINATE_ERROR_NAN"
+    CoordinateErrorInfinite -> "ML_COORDINATE_ERROR_INFINITE"
+    CoordinateErrorTooSmall -> "ML_COORDINATE_ERROR_TOO_SMALL"
+    CoordinateErrorTooLarge -> "ML_COORDINATE_ERROR_TOO_LARGE"
+
+data RegionLocationCode
+  = RegionLocationExterior
+  | RegionLocationBoundary
+  | RegionLocationInterior
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allRegionLocationCodes :: [RegionLocationCode]
+allRegionLocationCodes = [minBound .. maxBound]
+
+regionLocationCodeId :: RegionLocationCode -> Word32
+regionLocationCodeId code =
+  case code of
+    RegionLocationExterior -> 0
+    RegionLocationBoundary -> 1
+    RegionLocationInterior -> 2
+
+regionLocationCodeSymbol :: RegionLocationCode -> String
+regionLocationCodeSymbol code =
+  case code of
+    RegionLocationExterior -> "ML_REGION_EXTERIOR"
+    RegionLocationBoundary -> "ML_REGION_BOUNDARY"
+    RegionLocationInterior -> "ML_REGION_INTERIOR"
+
+data MinkowskiOperationCode
+  = MinkowskiOperationAddition
+  | MinkowskiOperationErosion
+  | MinkowskiOperationOpening
+  | MinkowskiOperationClosing
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allMinkowskiOperationCodes :: [MinkowskiOperationCode]
+allMinkowskiOperationCodes = [minBound .. maxBound]
+
+minkowskiOperationCodeId :: MinkowskiOperationCode -> Word32
+minkowskiOperationCodeId code =
+  case code of
+    MinkowskiOperationAddition -> 0
+    MinkowskiOperationErosion -> 1
+    MinkowskiOperationOpening -> 2
+    MinkowskiOperationClosing -> 3
+
+minkowskiOperationCodeSymbol :: MinkowskiOperationCode -> String
+minkowskiOperationCodeSymbol code =
+  case code of
+    MinkowskiOperationAddition -> "ML_MINKOWSKI_ADDITION"
+    MinkowskiOperationErosion -> "ML_MINKOWSKI_EROSION"
+    MinkowskiOperationOpening -> "ML_MINKOWSKI_OPENING"
+    MinkowskiOperationClosing -> "ML_MINKOWSKI_CLOSING"
+
+data CMesh
+data CRegion
+data CStructuringElement
+
+data CObstruction = CObstruction
+  { obstructionCode :: !Word32
+  , obstructionCoordinateError :: !Word32
+  , obstructionInputIndex :: !Word64
+  , obstructionFirstIndex :: !Word64
+  , obstructionSecondIndex :: !Word64
+  , obstructionFirstValue :: !Double
+  , obstructionSecondValue :: !Double
+  , obstructionPointX :: !Double
+  , obstructionPointY :: !Double
+  , obstructionMessage :: !String
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance Storable CObstruction where
+  sizeOf _ = 320
+  alignment _ = alignment (0 :: Word64)
+  peek pointer = do
+    obstructionCode <- peekByteOff pointer 0
+    obstructionCoordinateError <- peekByteOff pointer 4
+    obstructionInputIndex <- peekByteOff pointer 8
+    obstructionFirstIndex <- peekByteOff pointer 16
+    obstructionSecondIndex <- peekByteOff pointer 24
+    obstructionFirstValue <- peekByteOff pointer 32
+    obstructionSecondValue <- peekByteOff pointer 40
+    obstructionPointX <- peekByteOff pointer 48
+    obstructionPointY <- peekByteOff pointer 56
+    obstructionMessage <- peekCString (castPtr pointer `plusPtr` 64)
+    pure CObstruction {..}
+  poke pointer CObstruction {..} = do
+    pokeByteOff pointer 0 obstructionCode
+    pokeByteOff pointer 4 obstructionCoordinateError
+    pokeByteOff pointer 8 obstructionInputIndex
+    pokeByteOff pointer 16 obstructionFirstIndex
+    pokeByteOff pointer 24 obstructionSecondIndex
+    pokeByteOff pointer 32 obstructionFirstValue
+    pokeByteOff pointer 40 obstructionSecondValue
+    pokeByteOff pointer 48 obstructionPointX
+    pokeByteOff pointer 56 obstructionPointY
+    let messagePointer = castPtr pointer `plusPtr` 64 :: Ptr CChar
+    fillBytes messagePointer 0 256
+    withCStringLen obstructionMessage $ \(source, lengthInBytes) ->
+      copyBytes messagePointer source (min 255 lengthInBytes)
+
+data CMinkowskiReceipt = CMinkowskiReceipt
+  { receiptOperation :: !Word32
+  , receiptInputComponents :: !Word64
+  , receiptConvexPieces :: !Word64
+  , receiptGeneratedPieces :: !Word64
+  , receiptGeneratedConvolutionEdges :: !Word64
+  , receiptOverlayPasses :: !Word64
+  , receiptExactCrossings :: !Word64
+  , receiptOutputCells :: !Word64
+  , receiptExactCoordinateBitGrowth :: !Word64
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance Storable CMinkowskiReceipt where
+  sizeOf _ = 72
+  alignment _ = alignment (0 :: Word64)
+  peek pointer = do
+    receiptOperation <- peekByteOff pointer 0
+    receiptInputComponents <- peekByteOff pointer 8
+    receiptConvexPieces <- peekByteOff pointer 16
+    receiptGeneratedPieces <- peekByteOff pointer 24
+    receiptGeneratedConvolutionEdges <- peekByteOff pointer 32
+    receiptOverlayPasses <- peekByteOff pointer 40
+    receiptExactCrossings <- peekByteOff pointer 48
+    receiptOutputCells <- peekByteOff pointer 56
+    receiptExactCoordinateBitGrowth <- peekByteOff pointer 64
+    pure CMinkowskiReceipt {..}
+  poke pointer CMinkowskiReceipt {..} = do
+    pokeByteOff pointer 0 receiptOperation
+    pokeByteOff pointer 4 (0 :: Word32)
+    pokeByteOff pointer 8 receiptInputComponents
+    pokeByteOff pointer 16 receiptConvexPieces
+    pokeByteOff pointer 24 receiptGeneratedPieces
+    pokeByteOff pointer 32 receiptGeneratedConvolutionEdges
+    pokeByteOff pointer 40 receiptOverlayPasses
+    pokeByteOff pointer 48 receiptExactCrossings
+    pokeByteOff pointer 56 receiptOutputCells
+    pokeByteOff pointer 64 receiptExactCoordinateBitGrowth
+
+data AbiFieldType
+  = AbiFieldUInt32
+  | AbiFieldUInt64
+  | AbiFieldDouble
+  | AbiFieldCharArray !Int
+  deriving stock (Eq, Show, Generic)
+
+data AbiField = AbiField
+  { abiFieldName :: !String
+  , abiFieldType :: !AbiFieldType
+  }
+  deriving stock (Eq, Show, Generic)
+
+data AbiStruct
+  = AbiStructObstruction
+  | AbiStructMinkowskiReceipt
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allAbiStructs :: [AbiStruct]
+allAbiStructs = [minBound .. maxBound]
+
+abiStructSymbol :: AbiStruct -> String
+abiStructSymbol structure =
+  case structure of
+    AbiStructObstruction -> "ml_obstruction"
+    AbiStructMinkowskiReceipt -> "ml_minkowski_receipt"
+
+abiStructFields :: AbiStruct -> [AbiField]
+abiStructFields structure =
+  case structure of
+    AbiStructObstruction ->
+      [ AbiField "code" AbiFieldUInt32
+      , AbiField "coordinate_error" AbiFieldUInt32
+      , AbiField "input_index" AbiFieldUInt64
+      , AbiField "first_index" AbiFieldUInt64
+      , AbiField "second_index" AbiFieldUInt64
+      , AbiField "first_value" AbiFieldDouble
+      , AbiField "second_value" AbiFieldDouble
+      , AbiField "point_x" AbiFieldDouble
+      , AbiField "point_y" AbiFieldDouble
+      , AbiField "message" (AbiFieldCharArray 256)
+      ]
+    AbiStructMinkowskiReceipt ->
+      [ AbiField "operation" AbiFieldUInt32
+      , AbiField "reserved" AbiFieldUInt32
+      , AbiField "input_components" AbiFieldUInt64
+      , AbiField "convex_pieces" AbiFieldUInt64
+      , AbiField "generated_pieces" AbiFieldUInt64
+      , AbiField "generated_convolution_edges" AbiFieldUInt64
+      , AbiField "overlay_passes" AbiFieldUInt64
+      , AbiField "exact_crossings" AbiFieldUInt64
+      , AbiField "output_cells" AbiFieldUInt64
+      , AbiField "exact_coordinate_bit_growth" AbiFieldUInt64
+      ]
+
+data AbiHandle
+  = AbiMesh
+  | AbiRegion
+  | AbiStructuringElement
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allAbiHandles :: [AbiHandle]
+allAbiHandles = [minBound .. maxBound]
+
+abiHandleSymbol :: AbiHandle -> String
+abiHandleSymbol handle =
+  case handle of
+    AbiMesh -> "ml_mesh"
+    AbiRegion -> "ml_region"
+    AbiStructuringElement -> "ml_structuring_element"
+
+data AbiParameterKind
+  = AbiValueSize
+  | AbiValueDouble
+  | AbiHandleInput !AbiHandle
+  | AbiHandleRelease !AbiHandle
+  | AbiHandleOutput !AbiHandle
+  | AbiBufferInputDouble
+  | AbiBufferInputSize
+  | AbiBufferOutputChar
+  | AbiBufferOutputDouble
+  | AbiBufferOutputUInt32
+  | AbiBufferOutputSize
+  | AbiScalarOutputRegionLocation
+  | AbiScalarOutputInt64
+  | AbiScalarOutputSize
+  | AbiScalarOutputDouble
+  | AbiStructOutput !AbiStruct
+  deriving stock (Eq, Show, Generic)
+
+data AbiParameter = AbiParameter
+  { abiParameterName :: !String
+  , abiParameterKind :: !AbiParameterKind
+  }
+  deriving stock (Eq, Show, Generic)
+
+data AbiResult
+  = AbiResultVoid
+  | AbiResultUInt32
+  | AbiResultStatus
+  deriving stock (Eq, Show, Generic)
+
+data AbiFunctionFamily
+  = AbiFunctionMesh
+  | AbiFunctionRegion
+  | AbiFunctionMorphology
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+data AbiFunctionImplementation
+  = AbiFunctionRuntime
+  | AbiFunctionHaskell !AbiFunctionFamily !String
+  deriving stock (Eq, Show, Generic)
+
+data AbiFunctionName
+  = AbiVersion
+  | RuntimeInitialize
+  | DelaunayF64
+  | MeshInsertManyF64
+  | MeshSiteUnion
+  | MeshSiteIntersection
+  | MeshSiteDifference
+  | MeshSiteSymmetricDifference
+  | MeshVertexCount
+  | MeshTriangleCount
+  | MeshCopyVerticesF64
+  | MeshCopyTrianglesU32
+  | MeshFree
+  | RegionCreateF64
+  | RegionCounts
+  | RegionCopyF64
+  | RegionUnion
+  | RegionIntersection
+  | RegionDifference
+  | RegionSymmetricDifference
+  | RegionLocatePointF64
+  | RegionMeasure
+  | RegionFree
+  | StructuringElementCreateF64
+  | StructuringElementFree
+  | RegionMinkowskiSum
+  | RegionOffset
+  | RegionInset
+  | RegionOpen
+  | RegionClose
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+data AbiFunction = AbiFunction
+  { abiFunctionImplementation :: !AbiFunctionImplementation
+  , abiFunctionSymbol :: !String
+  , abiFunctionResult :: !AbiResult
+  , abiFunctionParameters :: ![AbiParameter]
+  }
+  deriving stock (Eq, Show, Generic)
+
+allAbiFunctionNames :: [AbiFunctionName]
+allAbiFunctionNames = [minBound .. maxBound]
+
+allAbiFunctions :: [AbiFunction]
+allAbiFunctions = fmap abiFunction allAbiFunctionNames
+
+abiFunction :: AbiFunctionName -> AbiFunction
+abiFunction name =
+  case name of
+    AbiVersion -> runtime "ml_abi_version" AbiResultUInt32 []
+    RuntimeInitialize -> runtime "ml_runtime_initialize" AbiResultStatus []
+    DelaunayF64 -> mesh "ml_delaunay_f64" "delaunayF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiMesh, obstruction]
+    MeshInsertManyF64 -> mesh "ml_mesh_insert_many_f64" "meshInsertManyF64" AbiResultStatus [inputHandle "mesh" AbiMesh, inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiMesh, obstruction]
+    MeshSiteUnion -> binaryMesh "ml_mesh_site_union" "meshSiteUnion"
+    MeshSiteIntersection -> binaryMesh "ml_mesh_site_intersection" "meshSiteIntersection"
+    MeshSiteDifference -> binaryMesh "ml_mesh_site_difference" "meshSiteDifference"
+    MeshSiteSymmetricDifference -> binaryMesh "ml_mesh_site_symmetric_difference" "meshSiteSymmetricDifference"
+    MeshVertexCount -> meshCount "ml_mesh_vertex_count" "meshVertexCount"
+    MeshTriangleCount -> meshCount "ml_mesh_triangle_count" "meshTriangleCount"
+    MeshCopyVerticesF64 -> mesh "ml_mesh_copy_vertices_f64" "meshCopyVerticesF64" AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "coordinates" AbiBufferOutputDouble, valueSize "point_capacity", AbiParameter "points_written" AbiScalarOutputSize, obstruction]
+    MeshCopyTrianglesU32 -> mesh "ml_mesh_copy_triangles_u32" "meshCopyTrianglesU32" AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "triangles" AbiBufferOutputUInt32, valueSize "triangle_capacity", AbiParameter "triangles_written" AbiScalarOutputSize, obstruction]
+    MeshFree -> mesh "ml_mesh_free" "meshFree" AbiResultVoid [releaseHandle "mesh" AbiMesh]
+    RegionCreateF64 -> region "ml_region_create_f64" "regionCreateF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", AbiParameter "loop_point_counts" AbiBufferInputSize, valueSize "loop_count", AbiParameter "component_loop_counts" AbiBufferInputSize, valueSize "component_count", outputHandle "result" AbiRegion, obstruction]
+    RegionCounts -> region "ml_region_counts" "regionCounts" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "component_count" AbiScalarOutputSize, AbiParameter "loop_count" AbiScalarOutputSize, AbiParameter "point_count" AbiScalarOutputSize, obstruction]
+    RegionCopyF64 -> region "ml_region_copy_f64" "regionCopyF64" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "coordinates" AbiBufferOutputDouble, valueSize "point_capacity", AbiParameter "loop_point_offsets" AbiBufferOutputSize, valueSize "loop_offset_capacity", AbiParameter "component_loop_offsets" AbiBufferOutputSize, valueSize "component_offset_capacity", obstruction]
+    RegionUnion -> binaryRegion "ml_region_union" "regionUnion"
+    RegionIntersection -> binaryRegion "ml_region_intersection" "regionIntersection"
+    RegionDifference -> binaryRegion "ml_region_difference" "regionDifference"
+    RegionSymmetricDifference -> binaryRegion "ml_region_symmetric_difference" "regionSymmetricDifference"
+    RegionLocatePointF64 -> region "ml_region_locate_point_f64" "regionLocatePointF64" AbiResultStatus [inputHandle "region" AbiRegion, valueDouble "x", valueDouble "y", AbiParameter "location" AbiScalarOutputRegionLocation, obstruction]
+    RegionMeasure -> region "ml_region_measure" "regionMeasure" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "euler_characteristic" AbiScalarOutputInt64, AbiParameter "area_ratio_utf8" AbiBufferOutputChar, valueSize "area_capacity", AbiParameter "area_bytes_written" AbiScalarOutputSize, AbiParameter "perimeter_lower" AbiScalarOutputDouble, AbiParameter "perimeter_upper" AbiScalarOutputDouble, obstruction]
+    RegionFree -> region "ml_region_free" "regionFree" AbiResultVoid [releaseHandle "region" AbiRegion]
+    StructuringElementCreateF64 -> morphology "ml_structuring_element_create_f64" "structuringElementCreateF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiStructuringElement, obstruction]
+    StructuringElementFree -> morphology "ml_structuring_element_free" "structuringElementFree" AbiResultVoid [releaseHandle "element" AbiStructuringElement]
+    RegionMinkowskiSum -> morphologyOperation "ml_region_minkowski_sum" "regionMinkowskiSum" (inputHandle "left" AbiRegion) (inputHandle "right" AbiRegion)
+    RegionOffset -> morphologyOperation "ml_region_offset" "regionOffset" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+    RegionInset -> morphologyOperation "ml_region_inset" "regionInset" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+    RegionOpen -> morphologyOperation "ml_region_open" "regionOpen" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+    RegionClose -> morphologyOperation "ml_region_close" "regionClose" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+ where
+  runtime = AbiFunction AbiFunctionRuntime
+  implemented family symbol haskellName =
+    AbiFunction (AbiFunctionHaskell family haskellName) symbol
+  mesh = implemented AbiFunctionMesh
+  region = implemented AbiFunctionRegion
+  morphology = implemented AbiFunctionMorphology
+  binaryMesh symbol haskellName =
+    mesh symbol haskellName AbiResultStatus [inputHandle "left" AbiMesh, inputHandle "right" AbiMesh, outputHandle "result" AbiMesh, obstruction]
+  meshCount symbol haskellName =
+    mesh symbol haskellName AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "count" AbiScalarOutputSize, obstruction]
+  binaryRegion symbol haskellName =
+    region symbol haskellName AbiResultStatus [inputHandle "left" AbiRegion, inputHandle "right" AbiRegion, outputHandle "result" AbiRegion, obstruction]
+  morphologyOperation symbol haskellName first second =
+    morphology symbol haskellName AbiResultStatus [first, second, outputHandle "result" AbiRegion, AbiParameter "receipt" (AbiStructOutput AbiStructMinkowskiReceipt), obstruction]
+  valueSize parameterName = AbiParameter parameterName AbiValueSize
+  valueDouble parameterName = AbiParameter parameterName AbiValueDouble
+  inputHandle parameterName handle = AbiParameter parameterName (AbiHandleInput handle)
+  releaseHandle parameterName handle = AbiParameter parameterName (AbiHandleRelease handle)
+  outputHandle parameterName handle = AbiParameter parameterName (AbiHandleOutput handle)
+  inputDoubles parameterName = AbiParameter parameterName AbiBufferInputDouble
+  obstruction = AbiParameter "obstruction" (AbiStructOutput AbiStructObstruction)
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Contract/Render.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Contract/Render.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Contract/Render.hs
@@ -0,0 +1,763 @@
+module Moonlight.Triangulation.Foreign.Contract.Render
+  ( GeneratedFile (..)
+  , generatedFiles
+  , renderCHeader
+  , renderDefinitionFile
+  , renderHaskellExports
+  , renderPythonRaw
+  , renderRustRaw
+  , renderTypeScriptWire
+  , renderTypeScriptRaw
+  ) where
+
+import Data.Char (toLower, toUpper)
+import Data.List (intercalate)
+import Data.Maybe (mapMaybe)
+import Data.Word (Word32)
+import Moonlight.Triangulation.Foreign.Contract
+
+data GeneratedFile = GeneratedFile
+  { generatedFilePath :: !FilePath
+  , generatedFileContents :: !String
+  }
+  deriving stock (Eq, Show)
+
+generatedFiles :: [GeneratedFile]
+generatedFiles =
+  [ GeneratedFile "ffi/include/moonlight_triangulation.h" renderCHeader
+  , GeneratedFile "ffi/cbits/moonlight-triangulation.def" renderDefinitionFile
+  , GeneratedFile "ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs" renderHaskellExports
+  , GeneratedFile "ffi/bindings/typescript/src/wire.generated.ts" renderTypeScriptWire
+  , GeneratedFile "ffi/bindings/typescript/src/internal/native.generated.ts" renderTypeScriptRaw
+  , GeneratedFile "ffi/bindings/python/src/moonlight_triangulation/_native_generated.py" renderPythonRaw
+  , GeneratedFile "ffi/bindings/rust/src/raw_generated.rs" renderRustRaw
+  ]
+
+generatedNotice :: String -> String
+generatedNotice comment =
+  comment
+    <> " Generated from Moonlight.Triangulation.Foreign.Contract.\n"
+    <> comment
+    <> " Run moonlight-triangulation-ffi-contract write; do not edit this file.\n\n"
+
+renderCHeader :: String
+renderCHeader =
+  generatedNotice "//"
+    <> unlines
+      [ "#ifndef MOONLIGHT_TRIANGULATION_H"
+      , "#define MOONLIGHT_TRIANGULATION_H"
+      , ""
+      , "#include <stddef.h>"
+      , "#include <stdint.h>"
+      , ""
+      , "#if defined(_WIN32)"
+      , "#define ML_API __declspec(dllexport)"
+      , "#else"
+      , "#define ML_API __attribute__((visibility(\"default\")))"
+      , "#endif"
+      , ""
+      , "#ifdef __cplusplus"
+      , "extern \"C\" {"
+      , "#endif"
+      , ""
+      , "#define ML_ABI_VERSION " <> show abiVersion <> "u"
+      , ""
+      , concatMap renderCOpaqueHandle allAbiHandles
+      , "typedef uint32_t ml_status;"
+      , "typedef uint32_t ml_obstruction_code;"
+      , "typedef uint32_t ml_coordinate_error;"
+      , "typedef uint32_t ml_region_location;"
+      , "typedef uint32_t ml_minkowski_operation;"
+      , ""
+      ]
+    <> renderCEnum (fmap (wireEntry abiStatusSymbol abiStatusId) allAbiStatuses)
+    <> renderCEnum (fmap (wireEntry obstructionCodeSymbol obstructionCodeId) allObstructionCodes)
+    <> renderCEnum (fmap (wireEntry coordinateErrorCodeSymbol coordinateErrorCodeId) allCoordinateErrorCodes)
+    <> renderCEnum (fmap (wireEntry regionLocationCodeSymbol regionLocationCodeId) allRegionLocationCodes)
+    <> renderCEnum (fmap (wireEntry minkowskiOperationCodeSymbol minkowskiOperationCodeId) allMinkowskiOperationCodes)
+    <> concatMap renderCStruct allAbiStructs
+    <> concatMap renderCFunction allAbiFunctions
+    <> unlines
+      [ ""
+      , "#ifdef __cplusplus"
+      , "}"
+      , "#endif"
+      , ""
+      , "#endif"
+      ]
+
+wireEntry :: (value -> String) -> (value -> Word32) -> value -> (String, Word32)
+wireEntry symbol identifier value = (symbol value, identifier value)
+
+renderCEnum :: [(String, Word32)] -> String
+renderCEnum entries =
+  "enum {\n"
+    <> intercalate ",\n" (fmap (\(symbol, identifier) -> "  " <> symbol <> " = " <> show identifier) entries)
+    <> "\n};\n\n"
+
+renderCOpaqueHandle :: AbiHandle -> String
+renderCOpaqueHandle handle =
+  "typedef struct " <> abiHandleSymbol handle <> " " <> abiHandleSymbol handle <> ";\n"
+
+renderCStruct :: AbiStruct -> String
+renderCStruct structure =
+  "typedef struct "
+    <> abiStructSymbol structure
+    <> " {\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "} "
+    <> abiStructSymbol structure
+    <> ";\n\n"
+ where
+  renderField (AbiField name fieldType) =
+    "  " <> cFieldType fieldType <> " " <> name <> cFieldSuffix fieldType <> ";\n"
+
+renderCFunction :: AbiFunction -> String
+renderCFunction function =
+  "ML_API "
+    <> cResultType (abiFunctionResult function)
+    <> " "
+    <> abiFunctionSymbol function
+    <> "("
+    <> renderParameters
+    <> ");\n"
+ where
+  parameters = abiFunctionParameters function
+  renderParameters =
+    if null parameters
+      then "void"
+      else intercalate ", " (fmap renderParameter parameters)
+  renderParameter (AbiParameter name kind) = cParameterType kind <> " " <> name
+
+cFieldType :: AbiFieldType -> String
+cFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "uint32_t"
+    AbiFieldUInt64 -> "uint64_t"
+    AbiFieldDouble -> "double"
+    AbiFieldCharArray _ -> "char"
+
+cFieldSuffix :: AbiFieldType -> String
+cFieldSuffix fieldType =
+  case fieldType of
+    AbiFieldCharArray lengthInBytes -> "[" <> show lengthInBytes <> "]"
+    _ -> ""
+
+cParameterType :: AbiParameterKind -> String
+cParameterType kind =
+  case kind of
+    AbiValueSize -> "size_t"
+    AbiValueDouble -> "double"
+    AbiHandleInput handle -> "const " <> abiHandleSymbol handle <> " *"
+    AbiHandleRelease handle -> abiHandleSymbol handle <> " *"
+    AbiHandleOutput handle -> abiHandleSymbol handle <> " **"
+    AbiBufferInputDouble -> "const double *"
+    AbiBufferInputSize -> "const size_t *"
+    AbiBufferOutputChar -> "char *"
+    AbiBufferOutputDouble -> "double *"
+    AbiBufferOutputUInt32 -> "uint32_t *"
+    AbiBufferOutputSize -> "size_t *"
+    AbiScalarOutputRegionLocation -> "ml_region_location *"
+    AbiScalarOutputInt64 -> "int64_t *"
+    AbiScalarOutputSize -> "size_t *"
+    AbiScalarOutputDouble -> "double *"
+    AbiStructOutput structure -> abiStructSymbol structure <> " *"
+
+cResultType :: AbiResult -> String
+cResultType result =
+  case result of
+    AbiResultVoid -> "void"
+    AbiResultUInt32 -> "uint32_t"
+    AbiResultStatus -> "ml_status"
+
+renderDefinitionFile :: String
+renderDefinitionFile =
+  generatedNotice ";"
+    <> "LIBRARY moonlight-triangulation-c\nEXPORTS\n"
+    <> concatMap (\function -> "  " <> abiFunctionSymbol function <> "\n") allAbiFunctions
+
+renderHaskellExports :: String
+renderHaskellExports =
+  generatedNotice "--"
+    <> unlines
+      [ "{-# LANGUAGE ForeignFunctionInterface #-}"
+      , "{-# OPTIONS_GHC -Wno-missing-signatures #-}"
+      , ""
+      , "module Moonlight.Triangulation.Foreign.Exports where"
+      , ""
+      , "import Data.Int (Int64)"
+      , "import Data.Word (Word32)"
+      , "import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))"
+      , "import Foreign.Ptr (Ptr)"
+      , "import Moonlight.Triangulation.Foreign.Contract"
+      , "  ( CMesh"
+      , "  , CMinkowskiReceipt"
+      , "  , CObstruction"
+      , "  , CRegion"
+      , "  , CStructuringElement"
+      , "  )"
+      , "import qualified Moonlight.Triangulation.Foreign.Mesh as Mesh"
+      , "import qualified Moonlight.Triangulation.Foreign.Morphology as Morphology"
+      , "import qualified Moonlight.Triangulation.Foreign.Region as Region"
+      , ""
+      ]
+    <> concatMap renderHaskellAlias implementedFunctions
+    <> "\n"
+    <> concatMap renderHaskellExport implementedFunctions
+ where
+  implementedFunctions :: [(AbiFunction, String, AbiFunctionFamily)]
+  implementedFunctions = mapMaybe implemented allAbiFunctions
+
+  implemented :: AbiFunction -> Maybe (AbiFunction, String, AbiFunctionFamily)
+  implemented function =
+    case abiFunctionImplementation function of
+      AbiFunctionRuntime -> Nothing
+      AbiFunctionHaskell family haskellName -> Just (function, haskellName, family)
+
+  renderHaskellAlias :: (AbiFunction, String, AbiFunctionFamily) -> String
+  renderHaskellAlias (_, haskellName, family) =
+    haskellName <> " = " <> familyQualifier family <> "." <> haskellName <> "\n"
+
+  renderHaskellExport :: (AbiFunction, String, AbiFunctionFamily) -> String
+  renderHaskellExport (function, haskellName, _) =
+    "foreign export ccall \""
+      <> abiFunctionSymbol function
+      <> "\" "
+      <> haskellName
+      <> " :: "
+      <> haskellFunctionType function
+      <> "\n"
+
+familyQualifier :: AbiFunctionFamily -> String
+familyQualifier family =
+  case family of
+    AbiFunctionMesh -> "Mesh"
+    AbiFunctionRegion -> "Region"
+    AbiFunctionMorphology -> "Morphology"
+
+renderTypeScriptWire :: String
+renderTypeScriptWire =
+  generatedNotice "//"
+    <> intercalate
+      "\n"
+      [ renderTypeScriptVocabulary "AbiStatus" "ML_STATUS_" abiStatusSymbol abiStatusId allAbiStatuses
+      , renderTypeScriptVocabulary "ObstructionCode" "ML_OBSTRUCTION_" obstructionCodeSymbol obstructionCodeId allObstructionCodes
+      , renderTypeScriptVocabulary "CoordinateError" "ML_COORDINATE_ERROR_" coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+      , renderTypeScriptVocabulary "RegionLocation" "ML_REGION_" regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+      , renderTypeScriptVocabulary "MinkowskiOperation" "ML_MINKOWSKI_" minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+      ]
+
+renderTypeScriptVocabulary
+  :: String
+  -> String
+  -> (value -> String)
+  -> (value -> Word32)
+  -> [value]
+  -> String
+renderTypeScriptVocabulary typeName prefix symbol identifier values =
+  "export const "
+    <> typeName
+    <> " = {\n"
+    <> concatMap renderValue values
+    <> "} as const;\n"
+    <> "export type "
+    <> typeName
+    <> " = (typeof "
+    <> typeName
+    <> ")[keyof typeof "
+    <> typeName
+    <> "];\n"
+    <> "const "
+    <> lowerInitial typeName
+    <> "ByWire: Readonly<Record<number, "
+    <> typeName
+    <> ">> = {\n"
+    <> concatMap renderWireEntry values
+    <> "};\n"
+    <> "export function decode"
+    <> typeName
+    <> "(value: number): "
+    <> typeName
+    <> " | undefined {\n"
+    <> "  return "
+    <> lowerInitial typeName
+    <> "ByWire[value];\n"
+    <> "}\n"
+ where
+  renderValue value =
+    "  " <> wireKey prefix (symbol value) <> ": \"" <> wireValue prefix (symbol value) <> "\",\n"
+  renderWireEntry value =
+    "  " <> show (identifier value) <> ": " <> typeName <> "." <> wireKey prefix (symbol value) <> ",\n"
+
+wireKey :: String -> String -> String
+wireKey prefix = concatMap capitalize . underscoreWords . drop (length prefix)
+
+wireValue :: String -> String -> String
+wireValue prefix = intercalate "-" . fmap (fmap toLower) . underscoreWords . drop (length prefix)
+
+underscoreWords :: String -> [String]
+underscoreWords value =
+  case break (== '_') value of
+    (word, []) -> [word]
+    (word, _ : rest) -> word : underscoreWords rest
+
+capitalize :: String -> String
+capitalize value =
+  case value of
+    [] -> []
+    firstCharacter : rest -> toUpper firstCharacter : fmap toLower rest
+
+lowerInitial :: String -> String
+lowerInitial value =
+  case value of
+    [] -> []
+    firstCharacter : rest -> toLower firstCharacter : rest
+
+haskellFunctionType :: AbiFunction -> String
+haskellFunctionType function =
+  intercalate " -> " (fmap (haskellParameterType . abiParameterKind) (abiFunctionParameters function) <> [haskellResultType (abiFunctionResult function)])
+
+haskellParameterType :: AbiParameterKind -> String
+haskellParameterType kind =
+  case kind of
+    AbiValueSize -> "CSize"
+    AbiValueDouble -> "CDouble"
+    AbiHandleInput handle -> "Ptr " <> haskellHandleName handle
+    AbiHandleRelease handle -> "Ptr " <> haskellHandleName handle
+    AbiHandleOutput handle -> "Ptr (Ptr " <> haskellHandleName handle <> ")"
+    AbiBufferInputDouble -> "Ptr CDouble"
+    AbiBufferInputSize -> "Ptr CSize"
+    AbiBufferOutputChar -> "Ptr CChar"
+    AbiBufferOutputDouble -> "Ptr CDouble"
+    AbiBufferOutputUInt32 -> "Ptr Word32"
+    AbiBufferOutputSize -> "Ptr CSize"
+    AbiScalarOutputRegionLocation -> "Ptr CUInt"
+    AbiScalarOutputInt64 -> "Ptr Int64"
+    AbiScalarOutputSize -> "Ptr CSize"
+    AbiScalarOutputDouble -> "Ptr CDouble"
+    AbiStructOutput structure -> "Ptr " <> haskellStructName structure
+
+haskellResultType :: AbiResult -> String
+haskellResultType result =
+  case result of
+    AbiResultVoid -> "IO ()"
+    AbiResultUInt32 -> "IO CUInt"
+    AbiResultStatus -> "IO CUInt"
+
+haskellHandleName :: AbiHandle -> String
+haskellHandleName handle =
+  case handle of
+    AbiMesh -> "CMesh"
+    AbiRegion -> "CRegion"
+    AbiStructuringElement -> "CStructuringElement"
+
+haskellStructName :: AbiStruct -> String
+haskellStructName structure =
+  case structure of
+    AbiStructObstruction -> "CObstruction"
+    AbiStructMinkowskiReceipt -> "CMinkowskiReceipt"
+
+renderTypeScriptRaw :: String
+renderTypeScriptRaw =
+  generatedNotice "//"
+    <> "import koffi from \"koffi\";\n\n"
+    <> "export const ABI_VERSION = " <> show abiVersion <> ";\n"
+    <> renderTypeScriptConstants abiStatusSymbol abiStatusId allAbiStatuses
+    <> renderTypeScriptConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
+    <> renderTypeScriptConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+    <> renderTypeScriptConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+    <> renderTypeScriptConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+    <> "\nexport type NativeHandle = object;\n"
+    <> "export type NativeInteger = number | bigint;\n"
+    <> "export type HandleOutput = Array<NativeHandle | null>;\n\n"
+    <> renderTypeScriptStruct "NativeObstruction" AbiStructObstruction
+    <> renderTypeScriptStruct "NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
+    <> "export interface NativeApi {\n"
+    <> concatMap renderTypeScriptFunctionType allAbiFunctions
+    <> "}\n\n"
+    <> unlines
+      [ "export function loadNativeApi(libraryPath: string): NativeApi {"
+      , "  const library = koffi.load(libraryPath);"
+      , concatMap renderTypeScriptKoffiHandleDeclarations allAbiHandles
+      , renderTypeScriptKoffiStruct "obstruction" AbiStructObstruction
+      , renderTypeScriptKoffiStruct "receipt" AbiStructMinkowskiReceipt
+      , "  const obstructionOutput = koffi.out(koffi.pointer(obstruction));"
+      , "  const receiptOutput = koffi.out(koffi.pointer(receipt));"
+      , "  const sizeOutput = koffi.out(koffi.pointer(\"size_t\"));"
+      , "  const uint32Output = koffi.out(koffi.pointer(\"uint32_t\"));"
+      , "  const int64Output = koffi.out(koffi.pointer(\"int64_t\"));"
+      , "  const doubleOutput = koffi.out(koffi.pointer(\"double\"));"
+      , "  const sizeArray = koffi.pointer(\"size_t\");"
+      , "  return {"
+      ]
+    <> concatMap renderTypeScriptFunctionBinding allAbiFunctions
+    <> "  };\n}\n"
+
+renderTypeScriptConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
+renderTypeScriptConstants symbol identifier =
+  concatMap (\value -> "export const " <> symbol value <> " = " <> show (identifier value) <> ";\n")
+
+renderTypeScriptStruct :: String -> AbiStruct -> String
+renderTypeScriptStruct name structure =
+  "export interface "
+    <> name
+    <> " {\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "}\n\n"
+ where
+  renderField (AbiField fieldName fieldType) =
+    "  " <> fieldName <> "?: " <> typeScriptFieldType fieldType <> ";\n"
+
+typeScriptFieldType :: AbiFieldType -> String
+typeScriptFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "number"
+    AbiFieldUInt64 -> "NativeInteger"
+    AbiFieldDouble -> "number"
+    AbiFieldCharArray _ -> "string | readonly number[]"
+
+renderTypeScriptFunctionType :: AbiFunction -> String
+renderTypeScriptFunctionType function =
+  "  readonly "
+    <> abiFunctionSymbol function
+    <> ": ("
+    <> intercalate ", " (fmap renderParameter (abiFunctionParameters function))
+    <> ") => "
+    <> typeScriptResultType (abiFunctionResult function)
+    <> ";\n"
+ where
+  renderParameter (AbiParameter name kind) = name <> ": " <> typeScriptParameterType kind
+
+typeScriptParameterType :: AbiParameterKind -> String
+typeScriptParameterType kind =
+  case kind of
+    AbiValueSize -> "number"
+    AbiValueDouble -> "number"
+    AbiHandleInput _ -> "NativeHandle"
+    AbiHandleRelease _ -> "NativeHandle"
+    AbiHandleOutput _ -> "HandleOutput"
+    AbiBufferInputDouble -> "Float64Array"
+    AbiBufferInputSize -> "BigUint64Array"
+    AbiBufferOutputChar -> "Buffer"
+    AbiBufferOutputDouble -> "Float64Array"
+    AbiBufferOutputUInt32 -> "Uint32Array"
+    AbiBufferOutputSize -> "BigUint64Array"
+    AbiScalarOutputRegionLocation -> "number[]"
+    AbiScalarOutputInt64 -> "NativeInteger[]"
+    AbiScalarOutputSize -> "NativeInteger[]"
+    AbiScalarOutputDouble -> "number[]"
+    AbiStructOutput AbiStructObstruction -> "NativeObstruction"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "NativeMinkowskiReceipt"
+
+typeScriptResultType :: AbiResult -> String
+typeScriptResultType result =
+  case result of
+    AbiResultVoid -> "void"
+    AbiResultUInt32 -> "number"
+    AbiResultStatus -> "number"
+
+renderTypeScriptKoffiStruct :: String -> AbiStruct -> String
+renderTypeScriptKoffiStruct variableName structure =
+  "  const "
+    <> variableName
+    <> " = koffi.struct({\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "  });"
+ where
+  renderField (AbiField name fieldType) =
+    "    " <> name <> ": " <> typeScriptKoffiFieldType fieldType <> ",\n"
+
+typeScriptKoffiFieldType :: AbiFieldType -> String
+typeScriptKoffiFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "\"uint32_t\""
+    AbiFieldUInt64 -> "\"uint64_t\""
+    AbiFieldDouble -> "\"double\""
+    AbiFieldCharArray lengthInBytes -> "koffi.array(\"char\", " <> show lengthInBytes <> ")"
+
+renderTypeScriptFunctionBinding :: AbiFunction -> String
+renderTypeScriptFunctionBinding function =
+  "    "
+    <> abiFunctionSymbol function
+    <> ": library.func(\""
+    <> abiFunctionSymbol function
+    <> "\", \""
+    <> typeScriptKoffiResultType (abiFunctionResult function)
+    <> "\", ["
+    <> intercalate ", " (fmap (typeScriptKoffiParameterType . abiParameterKind) (abiFunctionParameters function))
+    <> "]),\n"
+
+typeScriptKoffiResultType :: AbiResult -> String
+typeScriptKoffiResultType result =
+  case result of
+    AbiResultVoid -> "void"
+    AbiResultUInt32 -> "uint32_t"
+    AbiResultStatus -> "uint32_t"
+
+typeScriptKoffiParameterType :: AbiParameterKind -> String
+typeScriptKoffiParameterType kind =
+  case kind of
+    AbiValueSize -> "\"size_t\""
+    AbiValueDouble -> "\"double\""
+    AbiHandleInput handle -> typeScriptHandleVariable handle <> "Pointer"
+    AbiHandleRelease handle -> typeScriptHandleVariable handle <> "Pointer"
+    AbiHandleOutput handle -> typeScriptHandleVariable handle <> "Output"
+    AbiBufferInputDouble -> "koffi.pointer(\"double\")"
+    AbiBufferInputSize -> "sizeArray"
+    AbiBufferOutputChar -> "koffi.out(koffi.pointer(\"char\"))"
+    AbiBufferOutputDouble -> "koffi.out(koffi.pointer(\"double\"))"
+    AbiBufferOutputUInt32 -> "koffi.out(koffi.pointer(\"uint32_t\"))"
+    AbiBufferOutputSize -> "koffi.out(sizeArray)"
+    AbiScalarOutputRegionLocation -> "uint32Output"
+    AbiScalarOutputInt64 -> "int64Output"
+    AbiScalarOutputSize -> "sizeOutput"
+    AbiScalarOutputDouble -> "doubleOutput"
+    AbiStructOutput AbiStructObstruction -> "obstructionOutput"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "receiptOutput"
+
+typeScriptHandleVariable :: AbiHandle -> String
+typeScriptHandleVariable handle =
+  case handle of
+    AbiMesh -> "mesh"
+    AbiRegion -> "region"
+    AbiStructuringElement -> "structuringElement"
+
+renderTypeScriptKoffiHandleDeclarations :: AbiHandle -> String
+renderTypeScriptKoffiHandleDeclarations handle =
+  let variableName = typeScriptHandleVariable handle
+   in unlines
+        [ "  const " <> variableName <> " = koffi.opaque();"
+        , "  const " <> variableName <> "Pointer = koffi.pointer(" <> variableName <> ");"
+        , "  const " <> variableName <> "Output = koffi.out(koffi.pointer(" <> variableName <> ", 2));"
+        ]
+
+renderPythonRaw :: String
+renderPythonRaw =
+  generatedNotice "#"
+    <> unlines
+      [ "from __future__ import annotations"
+      , ""
+      , "import ctypes"
+      , "from collections.abc import Sequence"
+      , "from pathlib import Path"
+      , "from typing import Final"
+      , ""
+      , "ABI_VERSION: Final = " <> show abiVersion
+      ]
+    <> renderPythonConstants abiStatusSymbol abiStatusId allAbiStatuses
+    <> renderPythonConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
+    <> renderPythonConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+    <> renderPythonConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+    <> renderPythonConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+    <> "\n"
+    <> renderPythonStruct "_Obstruction" AbiStructObstruction
+    <> renderPythonStruct "_NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
+    <> unlines
+      [ "class _NativeApi:"
+      , "    def __init__(self, library_path: Path) -> None:"
+      , "        library = ctypes.CDLL(str(library_path))"
+      ]
+    <> concatMap renderPythonConfiguration allAbiFunctions
+    <> unlines
+      [ "        self.library = library"
+      , ""
+      , ""
+      , "def _configure("
+      , "    library: ctypes.CDLL,"
+      , "    name: str,"
+      , "    parameters: Sequence[object],"
+      , "    result: object = ctypes.c_uint32,"
+      , ") -> None:"
+      , "    function = getattr(library, name)"
+      , "    setattr(function, \"argtypes\", list(parameters))"
+      , "    setattr(function, \"restype\", result)"
+      ]
+
+renderPythonConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
+renderPythonConstants symbol identifier =
+  concatMap (\value -> symbol value <> ": Final = " <> show (identifier value) <> "\n")
+
+renderPythonStruct :: String -> AbiStruct -> String
+renderPythonStruct name structure =
+  "class "
+    <> name
+    <> "(ctypes.Structure):\n"
+    <> "    _fields_ = [\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "    ]\n\n\n"
+ where
+  renderField (AbiField fieldName fieldType) =
+    "        (\"" <> fieldName <> "\", " <> pythonFieldType fieldType <> "),\n"
+
+pythonFieldType :: AbiFieldType -> String
+pythonFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "ctypes.c_uint32"
+    AbiFieldUInt64 -> "ctypes.c_uint64"
+    AbiFieldDouble -> "ctypes.c_double"
+    AbiFieldCharArray lengthInBytes -> "ctypes.c_char * " <> show lengthInBytes
+
+renderPythonConfiguration :: AbiFunction -> String
+renderPythonConfiguration function =
+  "        _configure(library, \""
+    <> abiFunctionSymbol function
+    <> "\", "
+    <> pythonTuple (fmap (pythonParameterType . abiParameterKind) (abiFunctionParameters function))
+    <> ", "
+    <> pythonResultType (abiFunctionResult function)
+    <> ")\n"
+
+pythonTuple :: [String] -> String
+pythonTuple values =
+  case values of
+    [] -> "()"
+    [value] -> "(" <> value <> ",)"
+    _ -> "(" <> intercalate ", " values <> ")"
+
+pythonParameterType :: AbiParameterKind -> String
+pythonParameterType kind =
+  case kind of
+    AbiValueSize -> "ctypes.c_size_t"
+    AbiValueDouble -> "ctypes.c_double"
+    AbiHandleInput _ -> "ctypes.c_void_p"
+    AbiHandleRelease _ -> "ctypes.c_void_p"
+    AbiHandleOutput _ -> "ctypes.POINTER(ctypes.c_void_p)"
+    AbiBufferInputDouble -> "ctypes.POINTER(ctypes.c_double)"
+    AbiBufferInputSize -> "ctypes.POINTER(ctypes.c_size_t)"
+    AbiBufferOutputChar -> "ctypes.POINTER(ctypes.c_char)"
+    AbiBufferOutputDouble -> "ctypes.POINTER(ctypes.c_double)"
+    AbiBufferOutputUInt32 -> "ctypes.POINTER(ctypes.c_uint32)"
+    AbiBufferOutputSize -> "ctypes.POINTER(ctypes.c_size_t)"
+    AbiScalarOutputRegionLocation -> "ctypes.POINTER(ctypes.c_uint32)"
+    AbiScalarOutputInt64 -> "ctypes.POINTER(ctypes.c_int64)"
+    AbiScalarOutputSize -> "ctypes.POINTER(ctypes.c_size_t)"
+    AbiScalarOutputDouble -> "ctypes.POINTER(ctypes.c_double)"
+    AbiStructOutput AbiStructObstruction -> "ctypes.POINTER(_Obstruction)"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "ctypes.POINTER(_NativeMinkowskiReceipt)"
+
+pythonResultType :: AbiResult -> String
+pythonResultType result =
+  case result of
+    AbiResultVoid -> "None"
+    AbiResultUInt32 -> "ctypes.c_uint32"
+    AbiResultStatus -> "ctypes.c_uint32"
+
+renderRustRaw :: String
+renderRustRaw =
+  generatedNotice "//"
+    <> "// This private module is the complete wire projection; wrappers consume a subset.\n"
+    <> "#![allow(dead_code)]\n\n"
+    <> "use std::ffi::{c_char, c_double, c_uint};\n\n"
+    <> "pub(crate) const ABI_VERSION: u32 = " <> show abiVersion <> ";\n"
+    <> renderRustConstants abiStatusSymbol abiStatusId allAbiStatuses
+    <> renderRustConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
+    <> renderRustConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+    <> renderRustConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+    <> renderRustConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+    <> "\n"
+    <> concatMap renderRustHandle allAbiHandles
+    <> renderRustStruct "NativeObstruction" AbiStructObstruction
+    <> renderRustStruct "NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
+    <> "#[link(name = \"moonlight-triangulation-c\")]\nunsafe extern \"C\" {\n"
+    <> concatMap renderRustFunction allAbiFunctions
+    <> "}\n"
+
+renderRustConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
+renderRustConstants symbol identifier =
+  concatMap (\value -> "pub(crate) const " <> symbol value <> ": u32 = " <> show (identifier value) <> ";\n")
+
+renderRustHandle :: AbiHandle -> String
+renderRustHandle handle =
+  "#[repr(C)]\npub(crate) struct "
+    <> rustHandleName handle
+    <> " {\n    _private: [u8; 0],\n}\n\n"
+
+renderRustStruct :: String -> AbiStruct -> String
+renderRustStruct name structure =
+  "#[repr(C)]\npub(crate) struct "
+    <> name
+    <> " {\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "}\n\n"
+    <> "impl Default for "
+    <> name
+    <> " {\n"
+    <> "    fn default() -> Self {\n"
+    <> "        Self {\n"
+    <> concatMap renderDefaultField (abiStructFields structure)
+    <> "        }\n"
+    <> "    }\n"
+    <> "}\n\n"
+ where
+  renderField (AbiField fieldName fieldType) =
+    "    pub(crate) " <> fieldName <> ": " <> rustFieldType fieldType <> ",\n"
+  renderDefaultField (AbiField fieldName fieldType) =
+    "            " <> fieldName <> ": " <> rustFieldDefault fieldType <> ",\n"
+
+rustFieldType :: AbiFieldType -> String
+rustFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "u32"
+    AbiFieldUInt64 -> "u64"
+    AbiFieldDouble -> "f64"
+    AbiFieldCharArray lengthInBytes -> "[c_char; " <> show lengthInBytes <> "]"
+
+rustFieldDefault :: AbiFieldType -> String
+rustFieldDefault fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "0"
+    AbiFieldUInt64 -> "0"
+    AbiFieldDouble -> "0.0"
+    AbiFieldCharArray lengthInBytes -> "[0; " <> show lengthInBytes <> "]"
+
+renderRustFunction :: AbiFunction -> String
+renderRustFunction function =
+  case abiFunctionParameters function of
+    [] -> declarationPrefix <> "()" <> resultSuffix <> ";\n"
+    [parameter] -> declarationPrefix <> "(" <> renderParameter parameter <> ")" <> resultSuffix <> ";\n"
+    parameters ->
+      declarationPrefix
+        <> "(\n"
+        <> concatMap (\parameter -> "        " <> renderParameter parameter <> ",\n") parameters
+        <> "    )"
+        <> resultSuffix
+        <> ";\n"
+ where
+  declarationPrefix = "    pub(crate) fn " <> abiFunctionSymbol function
+  resultSuffix = rustResultSuffix (abiFunctionResult function)
+  renderParameter (AbiParameter name kind) = name <> ": " <> rustParameterType kind
+
+rustParameterType :: AbiParameterKind -> String
+rustParameterType kind =
+  case kind of
+    AbiValueSize -> "usize"
+    AbiValueDouble -> "c_double"
+    AbiHandleInput handle -> "*const " <> rustHandleName handle
+    AbiHandleRelease handle -> "*mut " <> rustHandleName handle
+    AbiHandleOutput handle -> "*mut *mut " <> rustHandleName handle
+    AbiBufferInputDouble -> "*const c_double"
+    AbiBufferInputSize -> "*const usize"
+    AbiBufferOutputChar -> "*mut c_char"
+    AbiBufferOutputDouble -> "*mut c_double"
+    AbiBufferOutputUInt32 -> "*mut u32"
+    AbiBufferOutputSize -> "*mut usize"
+    AbiScalarOutputRegionLocation -> "*mut u32"
+    AbiScalarOutputInt64 -> "*mut i64"
+    AbiScalarOutputSize -> "*mut usize"
+    AbiScalarOutputDouble -> "*mut c_double"
+    AbiStructOutput AbiStructObstruction -> "*mut NativeObstruction"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "*mut NativeMinkowskiReceipt"
+
+rustResultSuffix :: AbiResult -> String
+rustResultSuffix result =
+  case result of
+    AbiResultVoid -> ""
+    AbiResultUInt32 -> " -> c_uint"
+    AbiResultStatus -> " -> c_uint"
+
+rustHandleName :: AbiHandle -> String
+rustHandleName handle =
+  case handle of
+    AbiMesh -> "NativeMesh"
+    AbiRegion -> "NativeRegion"
+    AbiStructuringElement -> "NativeStructuringElement"
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Mesh.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Mesh.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Mesh.hs
@@ -0,0 +1,178 @@
+module Moonlight.Triangulation.Foreign.Mesh
+  ( delaunayF64
+  , insertGeometryBatch
+  , meshCopyTrianglesU32
+  , meshCopyVerticesF64
+  , meshFree
+  , meshInsertManyF64
+  , meshSiteDifference
+  , meshSiteIntersection
+  , meshSiteSymmetricDifference
+  , meshSiteUnion
+  , meshTriangleCount
+  , meshVertexCount
+  ) where
+
+import Control.Monad (void)
+import Data.Bifunctor (first)
+import Data.Word (Word32)
+import Foreign.C.Types (CDouble (..), CSize, CUInt)
+import Foreign.Ptr (Ptr)
+import Foreign.Storable (poke, pokeElemOff)
+import qualified Data.Vector as V
+import qualified Moonlight.Triangulation as T
+import Moonlight.Triangulation.Foreign.Boundary
+  ( GeometryMesh
+  , HandleValue
+  , checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , produceHandle
+  , readPoints
+  , requireOutputCapacity
+  , requirePointer
+  , runBoundary
+  )
+import Moonlight.Triangulation.Foreign.Contract (CMesh, CObstruction)
+import Moonlight.Triangulation.Foreign.Obstruction (buildFailure)
+import Moonlight.Triangulation.Math (validatePoint)
+import qualified Moonlight.Triangulation.Session as Session
+
+delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+delaunayF64 coordinates rawCount output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case checkedCount 2 rawCount of
+      Left failure -> pure (Left failure)
+      Right count -> do
+        points <- readPoints coordinates count
+        pure (points >>= first buildFailure . T.delaunayGeometry)
+
+meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+meshInsertManyF64 meshPointer coordinates rawCount output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case requirePointer "mesh" meshPointer >> checkedCount 2 rawCount of
+      Left failure -> pure (Left failure)
+      Right count -> do
+        points <- readPoints coordinates count
+        case points of
+          Left failure -> pure (Left failure)
+          Right admitted -> do
+            mesh <- dereferenceHandle meshPointer
+            pure (first buildFailure (insertGeometryBatch mesh admitted))
+
+-- | Admit the complete batch before opening the immutable insertion session.
+insertGeometryBatch :: GeometryMesh -> V.Vector T.Point -> Either T.BuildError GeometryMesh
+insertGeometryBatch mesh points = do
+  admittedPoints <- V.imapM (\index point -> validatePoint (Just index) point) points
+  (_, revised, _) <-
+    Session.withSession
+      mesh
+      (V.length points)
+      ( V.mapM_
+          ( \point ->
+              void
+                ( Session.insertAdmittedVertexAtCombining
+                    (\_ replacement -> replacement)
+                    point
+                    ()
+                )
+          )
+          admittedPoints
+      )
+  pure revised
+
+meshSiteUnion, meshSiteIntersection, meshSiteDifference, meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+meshSiteUnion = binaryMeshOperation T.union
+meshSiteIntersection = binaryMeshOperation T.intersection
+meshSiteDifference = binaryMeshOperation T.difference
+meshSiteSymmetricDifference = binaryMeshOperation T.symmetricDifference
+
+binaryMeshOperation
+  :: (GeometryMesh -> GeometryMesh -> Either T.BuildError GeometryMesh)
+  -> Ptr CMesh
+  -> Ptr CMesh
+  -> Ptr (Ptr CMesh)
+  -> Ptr CObstruction
+  -> IO CUInt
+binaryMeshOperation operation leftPointer rightPointer output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case requirePointer "left mesh" leftPointer >> requirePointer "right mesh" rightPointer of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        left <- dereferenceHandle leftPointer
+        right <- dereferenceHandle rightPointer
+        pure (first buildFailure (operation left right))
+
+meshVertexCount, meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+meshVertexCount = handleCount "mesh" T.numVertices
+meshTriangleCount = handleCount "mesh" (V.length . T.innerFaceVertexTriples)
+
+handleCount
+  :: String
+  -> (HandleValue carrier -> Int)
+  -> Ptr carrier
+  -> Ptr CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+handleCount handleLabel observe handlePointer output obstructionPointer =
+  runBoundary obstructionPointer $
+    case requirePointer handleLabel handlePointer >> requirePointer "count" output of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        value <- dereferenceHandle handlePointer
+        poke output (fromIntegral (observe value))
+        pure (Right ())
+
+meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+meshCopyVerticesF64 =
+  copyHandleProjection
+    "mesh"
+    "points_written"
+    "coordinates"
+    T.vertexPoints
+    ( \output index (T.Point x y) -> do
+        pokeElemOff output (index * 2) (CDouble x)
+        pokeElemOff output (index * 2 + 1) (CDouble y)
+    )
+
+meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+meshCopyTrianglesU32 =
+  copyHandleProjection
+    "mesh"
+    "triangles_written"
+    "triangles"
+    T.innerFaceVertexTriples
+    ( \output index (firstVertex, secondVertex, thirdVertex) -> do
+        pokeElemOff output (index * 3) (T.unVertexId firstVertex)
+        pokeElemOff output (index * 3 + 1) (T.unVertexId secondVertex)
+        pokeElemOff output (index * 3 + 2) (T.unVertexId thirdVertex)
+    )
+
+copyHandleProjection
+  :: String
+  -> String
+  -> String
+  -> (HandleValue carrier -> V.Vector item)
+  -> (Ptr element -> Int -> item -> IO ())
+  -> Ptr carrier
+  -> Ptr element
+  -> CSize
+  -> Ptr CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+{-# INLINE copyHandleProjection #-}
+copyHandleProjection handleLabel writtenLabel outputLabel project writeItem handlePointer output rawCapacity written obstructionPointer =
+  runBoundary obstructionPointer $
+    case requirePointer handleLabel handlePointer >> requirePointer writtenLabel written >> checkedCount 1 rawCapacity of
+      Left failure -> pure (Left failure)
+      Right capacity -> do
+        value <- dereferenceHandle handlePointer
+        let items = project value
+            required = V.length items
+        poke written (fromIntegral required)
+        case requireOutputCapacity outputLabel output required capacity of
+          Left failure -> pure (Left failure)
+          Right () -> V.imapM_ (writeItem output) items >> pure (Right ())
+
+meshFree :: Ptr CMesh -> IO ()
+meshFree = freeHandle
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Morphology.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Morphology.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Morphology.hs
@@ -0,0 +1,160 @@
+module Moonlight.Triangulation.Foreign.Morphology
+  ( regionClose
+  , regionInset
+  , regionMinkowskiSum
+  , regionOffset
+  , regionOpen
+  , structuringElementCreateF64
+  , structuringElementFree
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Word (Word32)
+import Foreign.C.Types (CDouble, CSize, CUInt)
+import Foreign.Ptr (Ptr)
+import Foreign.Storable (poke)
+import qualified Data.Vector as V
+import qualified Moonlight.Triangulation as T
+import Moonlight.Triangulation.Foreign.Boundary
+  ( checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , prepareHandleOutput
+  , produceHandle
+  , publishHandle
+  , readPoints
+  , requirePointer
+  , runBoundary
+  )
+import Moonlight.Triangulation.Foreign.Contract
+  ( CMinkowskiReceipt (..)
+  , CObstruction
+  , CRegion
+  , CStructuringElement
+  , MinkowskiOperationCode (..)
+  , minkowskiOperationCodeId
+  )
+import Moonlight.Triangulation.Foreign.Obstruction
+  ( AbiFailure
+  , minkowskiFailure
+  , pointInputFailure
+  , structuringElementEmptyFailure
+  )
+
+structuringElementCreateF64
+  :: Ptr CDouble
+  -> CSize
+  -> Ptr (Ptr CStructuringElement)
+  -> Ptr CObstruction
+  -> IO CUInt
+structuringElementCreateF64 coordinates rawPointCount output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case checkedCount 2 rawPointCount of
+      Left failure -> pure (Left failure)
+      Right pointCount -> do
+        points <- readPoints coordinates pointCount
+        pure (points >>= buildStructuringElement)
+
+buildStructuringElement
+  :: V.Vector T.Point
+  -> Either AbiFailure T.StructuringElement
+buildStructuringElement points = do
+  exactPoints <-
+    V.imapM
+      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
+      points
+  submitted <-
+    maybe
+      (Left structuringElementEmptyFailure)
+      Right
+      (NonEmpty.nonEmpty (V.toList exactPoints))
+  polygon <- first minkowskiFailure (T.convexPolygon submitted)
+  first minkowskiFailure (T.structuringElement polygon)
+
+structuringElementFree :: Ptr CStructuringElement -> IO ()
+structuringElementFree = freeHandle
+
+regionMinkowskiSum
+  :: Ptr CRegion
+  -> Ptr CRegion
+  -> Ptr (Ptr CRegion)
+  -> Ptr CMinkowskiReceipt
+  -> Ptr CObstruction
+  -> IO CUInt
+regionMinkowskiSum leftPointer rightPointer output receiptOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    produceRegionWithReceipt output receiptOutput $ do
+      case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
+        Left failure -> pure (Left failure)
+        Right () -> do
+          left <- dereferenceHandle leftPointer
+          right <- dereferenceHandle rightPointer
+          pure (first minkowskiFailure (T.minkowskiSum left right))
+
+regionOffset, regionInset, regionOpen, regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+regionOffset = structuringElementOperation T.polygonOffset
+regionInset = structuringElementOperation T.polygonInset
+regionOpen = structuringElementOperation T.openWith
+regionClose = structuringElementOperation T.closeWith
+
+structuringElementOperation
+  :: (T.StructuringElement -> T.PlanarRegion -> Either T.MinkowskiError (T.PlanarRegion, T.MinkowskiReceipt))
+  -> Ptr CStructuringElement
+  -> Ptr CRegion
+  -> Ptr (Ptr CRegion)
+  -> Ptr CMinkowskiReceipt
+  -> Ptr CObstruction
+  -> IO CUInt
+structuringElementOperation operation elementPointer regionPointer output receiptOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    produceRegionWithReceipt output receiptOutput $ do
+      case requirePointer "structuring element" elementPointer >> requirePointer "region" regionPointer of
+        Left failure -> pure (Left failure)
+        Right () -> do
+          element <- dereferenceHandle elementPointer
+          region <- dereferenceHandle regionPointer
+          pure (first minkowskiFailure (operation element region))
+
+produceRegionWithReceipt
+  :: Ptr (Ptr CRegion)
+  -> Ptr CMinkowskiReceipt
+  -> IO (Either AbiFailure (T.PlanarRegion, T.MinkowskiReceipt))
+  -> IO (Either AbiFailure ())
+produceRegionWithReceipt output receiptOutput obtain = do
+  prepared <- prepareHandleOutput output
+  case (prepared, requirePointer "receipt" receiptOutput) of
+    (Left failure, _) -> pure (Left failure)
+    (_, Left failure) -> pure (Left failure)
+    (Right (), Right ()) -> do
+      outcome <- obtain
+      case outcome of
+        Left failure -> pure (Left failure)
+        Right (region, receipt) -> do
+          poke receiptOutput (minkowskiReceiptProjection receipt)
+          publishHandle output region
+          pure (Right ())
+
+minkowskiReceiptProjection :: T.MinkowskiReceipt -> CMinkowskiReceipt
+minkowskiReceiptProjection receipt =
+  CMinkowskiReceipt
+    { receiptOperation = minkowskiOperationCode (T.minkowskiOperation receipt)
+    , receiptInputComponents = fromIntegral (T.minkowskiInputComponents receipt)
+    , receiptConvexPieces = fromIntegral (T.minkowskiConvexPieces receipt)
+    , receiptGeneratedPieces = fromIntegral (T.minkowskiGeneratedPieces receipt)
+    , receiptGeneratedConvolutionEdges = fromIntegral (T.minkowskiGeneratedConvolutionEdges receipt)
+    , receiptOverlayPasses = fromIntegral (T.minkowskiOverlayPasses receipt)
+    , receiptExactCrossings = fromIntegral (T.minkowskiExactCrossings receipt)
+    , receiptOutputCells = fromIntegral (T.minkowskiOutputCells receipt)
+    , receiptExactCoordinateBitGrowth = fromIntegral (T.minkowskiExactCoordinateBitGrowth receipt)
+    }
+
+minkowskiOperationCode :: T.MinkowskiOperation -> Word32
+minkowskiOperationCode operation =
+  minkowskiOperationCodeId
+    ( case operation of
+        T.MinkowskiAddition -> MinkowskiOperationAddition
+        T.MinkowskiErosion -> MinkowskiOperationErosion
+        T.MinkowskiOpening -> MinkowskiOperationOpening
+        T.MinkowskiClosing -> MinkowskiOperationClosing
+    )
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Obstruction.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Obstruction.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Obstruction.hs
@@ -0,0 +1,313 @@
+module Moonlight.Triangulation.Foreign.Obstruction
+  ( AbiFailure (..)
+  , RegionCountKind (..)
+  , RegionLayoutError (..)
+  , bufferTooSmallFailure
+  , buildFailure
+  , countOverflowFailure
+  , emptyObstruction
+  , minkowskiFailure
+  , nullPointerFailure
+  , overlayFailure
+  , pointInputFailure
+  , projectionFailure
+  , regionLayoutFailure
+  , regionPublicationFailure
+  , regionValidationFailure
+  , runtimeFailure
+  , statusOk
+  , structuringElementEmptyFailure
+  , valuationFailure
+  ) where
+
+import Control.Exception (SomeException, displayException)
+import Data.Word (Word32, Word64)
+import Foreign.C.Types (CUInt)
+import Moonlight.Triangulation.Foreign.Contract
+import qualified Moonlight.Triangulation as T
+
+data AbiFailure = AbiFailure !CUInt !CObstruction
+
+statusCode :: AbiStatus -> CUInt
+statusCode = fromIntegral . abiStatusId
+
+statusOk :: CUInt
+statusOk = statusCode AbiStatusOk
+
+emptyObstruction :: CObstruction
+emptyObstruction =
+  CObstruction
+    { obstructionCode = obstructionCodeId ObstructionNone
+    , obstructionCoordinateError = coordinateErrorCodeId CoordinateErrorNone
+    , obstructionInputIndex = maxBound
+    , obstructionFirstIndex = 0
+    , obstructionSecondIndex = 0
+    , obstructionFirstValue = 0
+    , obstructionSecondValue = 0
+    , obstructionPointX = 0
+    , obstructionPointY = 0
+    , obstructionMessage = ""
+    }
+
+apiFailure :: AbiStatus -> ObstructionCode -> String -> AbiFailure
+apiFailure status code message =
+  AbiFailure
+    (statusCode status)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId code
+      , obstructionMessage = message
+      }
+
+geometryFailure :: Show obstruction => ObstructionCode -> obstruction -> AbiFailure
+geometryFailure code obstruction =
+  apiFailure AbiStatusGeometryObstruction code (show obstruction)
+
+nullPointerFailure :: String -> AbiFailure
+nullPointerFailure label =
+  apiFailure AbiStatusNullPointer ObstructionNullPointer (label <> " must not be null")
+
+countOverflowFailure :: Word64 -> AbiFailure
+countOverflowFailure count =
+  AbiFailure
+    (statusCode AbiStatusCountOverflow)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionCountOverflow
+      , obstructionFirstIndex = count
+      , obstructionMessage = "count exceeds the host Int range"
+      }
+
+bufferTooSmallFailure :: Int -> Int -> AbiFailure
+bufferTooSmallFailure required capacity =
+  AbiFailure
+    (statusCode AbiStatusBufferTooSmall)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionBufferTooSmall
+      , obstructionFirstIndex = fromIntegral required
+      , obstructionSecondIndex = fromIntegral capacity
+      , obstructionMessage = "output buffer is smaller than the required element count"
+      }
+
+runtimeFailure :: SomeException -> AbiFailure
+runtimeFailure = apiFailure AbiStatusRuntimeFailure ObstructionRuntimeFailure . displayException
+
+buildFailure :: T.BuildError -> AbiFailure
+buildFailure obstruction =
+  AbiFailure (statusCode AbiStatusGeometryObstruction) (buildErrorObstruction obstruction)
+
+data RegionCountKind
+  = LoopPointCounts
+  | ComponentLoopCounts
+  deriving stock (Eq, Show)
+
+data RegionLayoutError
+  = RegionGroupEmpty !RegionCountKind !Int
+  | RegionCountTotalMismatch !RegionCountKind !Integer !Integer
+  deriving stock (Eq, Show)
+
+regionLayoutFailure :: RegionLayoutError -> AbiFailure
+regionLayoutFailure layoutError =
+  AbiFailure (statusCode AbiStatusGeometryObstruction) (layoutObstruction layoutError)
+
+layoutObstruction :: RegionLayoutError -> CObstruction
+layoutObstruction layoutError =
+  ( case layoutError of
+      RegionCountTotalMismatch _ actual expected ->
+        base
+          { obstructionFirstIndex = fromIntegral actual
+          , obstructionSecondIndex = fromIntegral expected
+          }
+      RegionGroupEmpty _ index -> base {obstructionInputIndex = fromIntegral index}
+  )
+    {obstructionMessage = show layoutError}
+ where
+  base = emptyObstruction {obstructionCode = obstructionCodeId ObstructionRegionLayoutInvalid}
+
+regionValidationFailure :: T.RegionValidationError -> AbiFailure
+regionValidationFailure = geometryFailure ObstructionRegionValidationFailed
+
+overlayFailure :: T.OverlayError Bool Bool -> AbiFailure
+overlayFailure = geometryFailure ObstructionOverlayFailed
+
+regionPublicationFailure :: T.RegionPublicationError -> AbiFailure
+regionPublicationFailure = geometryFailure ObstructionRegionPublicationFailed
+
+valuationFailure :: T.ValuationError -> AbiFailure
+valuationFailure = geometryFailure ObstructionValuationFailed
+
+minkowskiFailure :: T.MinkowskiError -> AbiFailure
+minkowskiFailure = geometryFailure ObstructionMinkowskiFailed
+
+structuringElementEmptyFailure :: AbiFailure
+structuringElementEmptyFailure =
+  apiFailure
+    AbiStatusGeometryObstruction
+    ObstructionRegionLayoutInvalid
+    "structuring element requires at least one point"
+
+pointInputFailure :: Int -> T.Point -> T.PointValidationError -> AbiFailure
+pointInputFailure index (T.Point x y) pointError =
+  AbiFailure
+    (statusCode AbiStatusGeometryObstruction)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionInvalidCoordinate
+      , obstructionCoordinateError = coordinateErrorCode reason
+      , obstructionInputIndex = fromIntegral index
+      , obstructionFirstValue = invalidValue
+      , obstructionPointX = x
+      , obstructionPointY = y
+      , obstructionMessage = show pointError
+      }
+ where
+  (invalidValue, reason) =
+    case pointError of
+      T.InvalidPointX coordinateError -> (x, coordinateError)
+      T.InvalidPointY coordinateError -> (y, coordinateError)
+
+projectionFailure :: Int -> T.PointValidationError -> AbiFailure
+projectionFailure index pointError =
+  AbiFailure
+    (statusCode AbiStatusGeometryObstruction)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionRegionProjectionFailed
+      , obstructionCoordinateError =
+          coordinateErrorCode
+            ( case pointError of
+                T.InvalidPointX coordinateError -> coordinateError
+                T.InvalidPointY coordinateError -> coordinateError
+            )
+      , obstructionInputIndex = fromIntegral index
+      , obstructionMessage = show pointError
+      }
+
+buildErrorObstruction :: T.BuildError -> CObstruction
+buildErrorObstruction failure =
+  ( case failure of
+      T.InvalidCoordinate inputIndex value reason ->
+        emptyObstruction
+          { obstructionCode = obstructionCodeId ObstructionInvalidCoordinate
+          , obstructionCoordinateError = coordinateErrorCode reason
+          , obstructionInputIndex = maybe maxBound fromIntegral inputIndex
+          , obstructionFirstValue = value
+          }
+      T.PointLocationFailed (T.Point x y) -> pointObstruction ObstructionPointLocationFailed x y
+      T.LocationWalkExhausted (T.Point x y) steps ->
+        (pointObstruction ObstructionLocationWalkExhausted x y) {obstructionFirstIndex = fromIntegral steps}
+      T.RefinementInputTopologyInvalid _ -> codeOnly ObstructionRefinementInputTopologyInvalid
+      T.FreshInsertionMatchedExistingVertex firstVertex secondVertex -> indices ObstructionFreshInsertionMatchedExistingVertex (T.unVertexId firstVertex) (T.unVertexId secondVertex)
+      T.DegenerateLineEndpointMissingOutgoing vertex -> firstIndex ObstructionDegenerateLineEndpointMissingOutgoing (T.unVertexId vertex)
+      T.DegenerateLineEndpointTurnMissing index -> firstIndex ObstructionDegenerateLineEndpointTurnMissing index
+      T.DegenerateLineConnectedVertexMissing index -> firstIndex ObstructionDegenerateLineConnectedVertexMissing index
+      T.HullStartNotVisible edge -> firstIndex ObstructionHullStartNotVisible (T.unDirectedEdgeId edge)
+      T.OuterRangeDidNotTerminate firstEdge secondEdge steps ->
+        (indices ObstructionOuterRangeDidNotTerminate (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.OuterRangeContainsInnerEdge edge face -> indices ObstructionOuterRangeContainsInnerEdge (T.unDirectedEdgeId edge) (T.unFaceId face)
+      T.ConstrainedEdgeFlipRefused edge -> firstIndex ObstructionConstrainedEdgeFlipRefused (T.unUndirectedEdgeId edge)
+      T.RemovalVertexOutOfRange vertex count -> indices ObstructionRemovalVertexOutOfRange (T.unVertexId vertex) count
+      T.RemovalEdgeOutOfRange edge count -> indices ObstructionRemovalEdgeOutOfRange (T.unUndirectedEdgeId edge) count
+      T.RemovalFaceOutOfRange face count -> indices ObstructionRemovalFaceOutOfRange (T.unFaceId face) count
+      T.RemovalFaceCycleDidNotTerminate face edge steps ->
+        (indices ObstructionRemovalFaceCycleDidNotTerminate (T.unFaceId face) (T.unDirectedEdgeId edge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.RemovalEmptyTriangulation vertex -> firstIndex ObstructionRemovalEmptyTriangulation (T.unVertexId vertex)
+      T.RemovalTwoPointDegreeMismatch vertex degree -> indices ObstructionRemovalTwoPointDegreeMismatch (T.unVertexId vertex) degree
+      T.RemovalCollinearDegreeMismatch vertex degree -> indices ObstructionRemovalCollinearDegreeMismatch (T.unVertexId vertex) degree
+      T.RemovalBorderTooShort count -> firstIndex ObstructionRemovalBorderTooShort count
+      T.RemovalBorderArityMismatch count -> firstIndex ObstructionRemovalBorderArityMismatch count
+      T.RemovalOutgoingCycleDidNotTerminate vertex edge steps ->
+        (indices ObstructionRemovalOutgoingCycleDidNotTerminate (T.unVertexId vertex) (T.unDirectedEdgeId edge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.CircleSweepHullEmpty -> codeOnly ObstructionCircleSweepHullEmpty
+      T.OuterCycleDidNotTerminate firstEdge secondEdge steps ->
+        (indices ObstructionOuterCycleDidNotTerminate (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.HierarchyLevelPopulationMismatch level expected observed ->
+        (indices ObstructionHierarchyLevelPopulationMismatch expected observed) {obstructionFirstValue = fromIntegral level}
+      T.HierarchyInsertionHandleMismatch expected observed -> indices ObstructionHierarchyInsertionHandleMismatch (T.unVertexId expected) (T.unVertexId observed)
+      T.PointIndexCapacityExhausted count -> firstIndex ObstructionPointIndexCapacityExhausted count
+      T.RefinementMinimumAngleNotFinite value -> nonFinite ObstructionRefinementMinimumAngleNotFinite value
+      T.RefinementMinimumAngleOutOfRange value -> firstValue ObstructionRefinementMinimumAngleOutOfRange value
+      T.RefinementMinimumAngleDerivedRatioNotFinite value -> nonFinite ObstructionRefinementMinimumAngleDerivedRatioNotFinite value
+      T.RefinementMaximumAdditionalVerticesNegative value -> firstValue ObstructionRefinementMaximumAdditionalVerticesNegative (fromIntegral value)
+      T.RefinementMinimumAreaNotFinite value -> nonFinite ObstructionRefinementMinimumAreaNotFinite value
+      T.RefinementMinimumAreaNegative value -> firstValue ObstructionRefinementMinimumAreaNegative value
+      T.RefinementMaximumAreaNotFinite value -> nonFinite ObstructionRefinementMaximumAreaNotFinite value
+      T.RefinementMaximumAreaNotPositive value -> firstValue ObstructionRefinementMaximumAreaNotPositive value
+      T.RefinementMaximumRadiusEdgeRatioNotFinite value -> nonFinite ObstructionRefinementMaximumRadiusEdgeRatioNotFinite value
+      T.RefinementMaximumRadiusEdgeRatioNotPositive value -> firstValue ObstructionRefinementMaximumRadiusEdgeRatioNotPositive value
+      T.RefinementMaximumEdgeLengthNotFinite value -> nonFinite ObstructionRefinementMaximumEdgeLengthNotFinite value
+      T.RefinementMaximumEdgeLengthNotPositive value -> firstValue ObstructionRefinementMaximumEdgeLengthNotPositive value
+      T.RefinementMinimumAreaExceedsMaximum minimumArea maximumArea -> values ObstructionRefinementMinimumAreaExceedsMaximum minimumArea maximumArea
+      T.RefinementSeedFaceNotActive face count -> indices ObstructionRefinementSeedFaceNotActive (T.unFaceId face) count
+      T.RefinementDomainTopologyChanged -> codeOnly ObstructionRefinementDomainTopologyChanged
+      T.RefinementDomainRequiresConvexHullPreservation -> codeOnly ObstructionRefinementDomainRequiresConvexHullPreservation
+      T.RefinementDomainRequiresConstraintPreservation -> codeOnly ObstructionRefinementDomainRequiresConstraintPreservation
+      T.RefinementDomainForbidsOuterFaceExclusion -> codeOnly ObstructionRefinementDomainForbidsOuterFaceExclusion
+      T.RefinementDomainWouldCrossInterface edge face -> indices ObstructionRefinementDomainWouldCrossInterface (T.unUndirectedEdgeId edge) (T.unFaceId face)
+      T.RefinementDomainWouldRewriteProtectedFace face -> firstIndex ObstructionRefinementDomainWouldRewriteProtectedFace (T.unFaceId face)
+      T.RefinementDomainProtectedFaceChanged face -> firstIndex ObstructionRefinementDomainProtectedFaceChanged (T.unFaceId face)
+      T.RefinementDomainInterfaceOppositeFaceNotPermitted edge face ->
+        indices ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted (T.unUndirectedEdgeId edge) (T.unFaceId face)
+      T.RefinementOversizedEdge face edge actual bound ->
+        (indices ObstructionRefinementOversizedEdge (T.unFaceId face) (T.unUndirectedEdgeId edge))
+          { obstructionFirstValue = actual
+          , obstructionSecondValue = bound
+          }
+      T.CapacityExceeded count -> firstIndex ObstructionCapacityExceeded count
+      T.HalfEdgeCapacityExceeded requested capacity -> indices ObstructionHalfEdgeCapacityExceeded requested capacity
+      T.FaceCapacityExceeded requested capacity -> indices ObstructionFaceCapacityExceeded requested capacity
+      T.PayloadStorageFailure _ -> codeOnly ObstructionPayloadStorageFailure
+      T.CoordinatePayloadCountMismatch coordinates payloads -> indices ObstructionCoordinatePayloadCountMismatch coordinates payloads
+      T.CircleSweepRequiresDenseStorage -> codeOnly ObstructionCircleSweepRequiresDenseStorage
+      T.SeamFrontierUnavailable -> codeOnly ObstructionSeamFrontierUnavailable
+      T.RefinementDomainRequiresFiniteVertexBudget -> codeOnly ObstructionRefinementDomainRequiresFiniteVertexBudget
+      T.SeamSourceEdgeRequiresFlip edge -> firstIndex ObstructionSeamSourceEdgeRequiresFlip (T.unUndirectedEdgeId edge)
+      T.RefinementSeamBridgeBudgetExceeded required available -> indices ObstructionRefinementSeamBridgeBudgetExceeded required available
+      T.RefinementSeamBridgeMidpointCollapsed edge -> firstIndex ObstructionRefinementSeamBridgeMidpointCollapsed (T.unUndirectedEdgeId edge)
+      T.BoundarySplitRequiresBoundaryEdge edge -> firstIndex ObstructionBoundarySplitRequiresBoundaryEdge (T.unUndirectedEdgeId edge)
+  )
+    {obstructionMessage = show failure}
+ where
+  codeOnly :: ObstructionCode -> CObstruction
+  codeOnly code = emptyObstruction {obstructionCode = obstructionCodeId code}
+  firstIndex :: Integral index => ObstructionCode -> index -> CObstruction
+  firstIndex code index = (codeOnly code) {obstructionFirstIndex = fromIntegral index}
+  indices :: (Integral first, Integral second) => ObstructionCode -> first -> second -> CObstruction
+  indices code firstIndexValue secondIndexValue =
+    (codeOnly code)
+      { obstructionFirstIndex = fromIntegral firstIndexValue
+      , obstructionSecondIndex = fromIntegral secondIndexValue
+      }
+  firstValue :: ObstructionCode -> Double -> CObstruction
+  firstValue code value = (codeOnly code) {obstructionFirstValue = value}
+  values :: ObstructionCode -> Double -> Double -> CObstruction
+  values code firstCoordinateValue secondCoordinateValue =
+    (codeOnly code)
+      { obstructionFirstValue = firstCoordinateValue
+      , obstructionSecondValue = secondCoordinateValue
+      }
+  pointObstruction :: ObstructionCode -> Double -> Double -> CObstruction
+  pointObstruction code x y =
+    (codeOnly code)
+      { obstructionPointX = x
+      , obstructionPointY = y
+      }
+  nonFinite :: ObstructionCode -> T.NonFiniteValue -> CObstruction
+  nonFinite code value = firstIndex code (nonFiniteCode value)
+
+coordinateErrorCode :: T.CoordinateError -> Word32
+coordinateErrorCode reason =
+  coordinateErrorCodeId
+    ( case reason of
+        T.CoordinateNaN -> CoordinateErrorNaN
+        T.CoordinateInfinite -> CoordinateErrorInfinite
+        T.CoordinateTooSmall -> CoordinateErrorTooSmall
+        T.CoordinateTooLarge -> CoordinateErrorTooLarge
+    )
+
+nonFiniteCode :: T.NonFiniteValue -> Word32
+nonFiniteCode value =
+  case value of
+    T.ValueNaN -> 1
+    T.ValuePositiveInfinity -> 2
+    T.ValueNegativeInfinity -> 3
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Region.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Region.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Triangulation/Foreign/Region.hs
@@ -0,0 +1,408 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Moonlight.Triangulation.Foreign.Region
+  ( regionCopyF64
+  , regionCounts
+  , regionCreateF64
+  , regionDifference
+  , regionFree
+  , regionIntersection
+  , regionLocatePointF64
+  , regionMeasure
+  , regionSymmetricDifference
+  , regionUnion
+  ) where
+
+import Data.Bifunctor (first)
+import Data.Int (Int64)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Foreign.C.String (withCStringLen)
+import Foreign.C.Types (CChar, CDouble (..), CSize, CUInt)
+import Foreign.Marshal.Utils (copyBytes)
+import Foreign.Ptr (Ptr, nullPtr)
+import Foreign.Storable (peekElemOff, poke, pokeElemOff)
+import qualified Data.Vector as V
+import qualified Moonlight.Triangulation as T
+import Moonlight.Triangulation.Foreign.Boundary
+  ( checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , produceHandle
+  , readPoints
+  , requireOutputCapacity
+  , requirePointer
+  , runBoundary
+  )
+import Moonlight.Triangulation.Foreign.Contract
+  ( CObstruction
+  , CRegion
+  , RegionLocationCode (..)
+  , regionLocationCodeId
+  )
+import Moonlight.Triangulation.Foreign.Obstruction
+  ( AbiFailure
+  , RegionCountKind (..)
+  , RegionLayoutError (..)
+  , nullPointerFailure
+  , overlayFailure
+  , pointInputFailure
+  , projectionFailure
+  , regionLayoutFailure
+  , regionPublicationFailure
+  , regionValidationFailure
+  , valuationFailure
+  )
+
+readCounts
+  :: String
+  -> Ptr CSize
+  -> Int
+  -> IO (Either AbiFailure (V.Vector Int))
+readCounts _ _ 0 = pure (Right V.empty)
+readCounts label pointer count
+  | pointer == nullPtr = pure (Left (nullPointerFailure label))
+  | otherwise = do
+      rawCounts <- V.generateM count (peekElemOff pointer)
+      pure (V.mapM (checkedCount 1) rawCounts)
+
+validateCounts
+  :: RegionCountKind
+  -> Int
+  -> V.Vector Int
+  -> Either RegionLayoutError ()
+validateCounts kind expectedTotal counts =
+  case V.findIndex (== 0) counts of
+    Just index -> Left (RegionGroupEmpty kind index)
+    Nothing
+      | observedTotal /= toInteger expectedTotal ->
+          Left (RegionCountTotalMismatch kind observedTotal (toInteger expectedTotal))
+      | otherwise -> Right ()
+ where
+  observedTotal = V.foldl' (\total count -> total + toInteger count) 0 counts
+
+regionCreateF64
+  :: Ptr CDouble
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr (Ptr CRegion)
+  -> Ptr CObstruction
+  -> IO CUInt
+regionCreateF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount output obstructionPointer =
+  runBoundary obstructionPointer $
+    produceHandle output $
+      readRegionF64
+        coordinates
+        rawPointCount
+        loopPointCounts
+        rawLoopCount
+        componentLoopCounts
+        rawComponentCount
+
+readRegionF64
+  :: Ptr CDouble
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> IO (Either AbiFailure T.PlanarRegion)
+readRegionF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount =
+  case checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount of
+    Left failure -> pure (Left failure)
+    Right (pointCount, loopCount, componentCount) -> do
+      points <- readPoints coordinates pointCount
+      loopCounts <- readCounts "loop_point_counts" loopPointCounts loopCount
+      componentCounts <- readCounts "component_loop_counts" componentLoopCounts componentCount
+      pure $ do
+        admittedPoints <- points
+        admittedLoopCounts <- loopCounts
+        admittedComponentCounts <- componentCounts
+        first regionLayoutFailure (validateCounts LoopPointCounts pointCount admittedLoopCounts)
+        first regionLayoutFailure (validateCounts ComponentLoopCounts loopCount admittedComponentCounts)
+        buildRegion admittedPoints admittedLoopCounts admittedComponentCounts
+
+checkedRegionInputCounts
+  :: CSize
+  -> CSize
+  -> CSize
+  -> Either AbiFailure (Int, Int, Int)
+checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount = do
+  pointCount <- checkedCount 2 rawPointCount
+  loopCount <- checkedCount 1 rawLoopCount
+  componentCount <- checkedCount 1 rawComponentCount
+  pure (pointCount, loopCount, componentCount)
+
+buildRegion
+  :: V.Vector T.Point
+  -> V.Vector Int
+  -> V.Vector Int
+  -> Either AbiFailure T.PlanarRegion
+buildRegion points loopCounts componentCounts = do
+  exactPoints <-
+    V.imapM
+      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
+      points
+  loops <- V.imapM (buildLoop exactPoints) (adjacentOffsets loopCounts)
+  components <- V.imapM (buildComponent loops) (adjacentOffsets componentCounts)
+  first regionValidationFailure (T.planarRegion (V.toList components))
+
+adjacentOffsets :: V.Vector Int -> V.Vector (Int, Int)
+adjacentOffsets counts =
+  let offsets = V.scanl' (+) 0 counts
+   in V.zip offsets (V.drop 1 offsets)
+
+buildLoop
+  :: V.Vector T.ExactPoint
+  -> Int
+  -> (Int, Int)
+  -> Either AbiFailure T.ExactLoop
+buildLoop points loopIndex (start, end) =
+  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) points)) of
+    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty LoopPointCounts loopIndex))
+    Just submitted -> first regionValidationFailure (T.exactLoop submitted)
+
+buildComponent
+  :: V.Vector T.ExactLoop
+  -> Int
+  -> (Int, Int)
+  -> Either AbiFailure T.PolygonComponent
+buildComponent loops componentIndex (start, end) =
+  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) loops)) of
+    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty ComponentLoopCounts componentIndex))
+    Just (outer :| holes) -> first regionValidationFailure (T.polygonComponent outer holes)
+
+regionShape
+  :: T.PlanarRegion
+  -> ([T.PolygonComponent], [[T.ExactLoop]], [T.ExactLoop])
+regionShape region =
+  let components = T.planarRegionComponents region
+      componentLoops component = T.polygonOuterLoop component : T.polygonHoleLoops component
+      loopsByComponent = map componentLoops components
+   in (components, loopsByComponent, concat loopsByComponent)
+
+regionCounts
+  :: Ptr CRegion
+  -> Ptr CSize
+  -> Ptr CSize
+  -> Ptr CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+regionCounts regionPointer componentCountOutput loopCountOutput pointCountOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      requirePointer "region" regionPointer
+        >> requirePointer "component_count" componentCountOutput
+        >> requirePointer "loop_count" loopCountOutput
+        >> requirePointer "point_count" pointCountOutput
+    of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        region <- dereferenceHandle regionPointer
+        let (components, _, loops) = regionShape region
+            pointCount = sum (map (NonEmpty.length . T.exactLoopPoints) loops)
+        poke componentCountOutput (fromIntegral (length components))
+        poke loopCountOutput (fromIntegral (length loops))
+        poke pointCountOutput (fromIntegral pointCount)
+        pure (Right ())
+
+data RegionProjection = RegionProjection
+  { projectionPoints :: !(V.Vector T.Point)
+  , projectionLoopPointOffsets :: !(V.Vector CSize)
+  , projectionComponentLoopOffsets :: !(V.Vector CSize)
+  }
+
+regionProjection :: T.PlanarRegion -> Either AbiFailure RegionProjection
+regionProjection region = do
+  let (_, loopsByComponent, loops) = regionShape region
+      exactPoints = concatMap (NonEmpty.toList . T.exactLoopPoints) loops
+      loopPointOffsets = scanl (+) 0 (map (NonEmpty.length . T.exactLoopPoints) loops)
+      componentLoopOffsets = scanl (+) 0 (map length loopsByComponent)
+  projectedPoints <-
+    V.fromList
+      <$> traverse
+        (uncurry projectPoint)
+        (zip [0 ..] exactPoints)
+  pure
+    RegionProjection
+      { projectionPoints = projectedPoints
+      , projectionLoopPointOffsets = V.fromList (map fromIntegral loopPointOffsets)
+      , projectionComponentLoopOffsets = V.fromList (map fromIntegral componentLoopOffsets)
+      }
+ where
+  projectPoint index point =
+    T.queryPointValue
+      <$> first (projectionFailure index) (T.exactPointToEmbeddingCandidate point)
+
+regionCopyF64
+  :: Ptr CRegion
+  -> Ptr CDouble
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+regionCopyF64 regionPointer coordinates rawPointCapacity loopPointOffsets rawLoopOffsetCapacity componentLoopOffsets rawComponentOffsetCapacity obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      (,,)
+        <$> (requirePointer "region" regionPointer >> checkedCount 2 rawPointCapacity)
+        <*> checkedCount 1 rawLoopOffsetCapacity
+        <*> checkedCount 1 rawComponentOffsetCapacity
+    of
+      Left failure -> pure (Left failure)
+      Right (pointCapacity, loopOffsetCapacity, componentOffsetCapacity) -> do
+        region <- dereferenceHandle regionPointer
+        case regionProjection region of
+          Left failure -> pure (Left failure)
+          Right RegionProjection {..} ->
+            case
+              requireOutputCapacity "coordinates" coordinates (V.length projectionPoints) pointCapacity
+                >> requireOutputCapacity "loop_point_offsets" loopPointOffsets (V.length projectionLoopPointOffsets) loopOffsetCapacity
+                >> requireOutputCapacity "component_loop_offsets" componentLoopOffsets (V.length projectionComponentLoopOffsets) componentOffsetCapacity
+            of
+              Left failure -> pure (Left failure)
+              Right () -> do
+                V.imapM_
+                  ( \index (T.Point x y) -> do
+                      pokeElemOff coordinates (index * 2) (CDouble x)
+                      pokeElemOff coordinates (index * 2 + 1) (CDouble y)
+                  )
+                  projectionPoints
+                V.imapM_ (pokeElemOff loopPointOffsets) projectionLoopPointOffsets
+                V.imapM_ (pokeElemOff componentLoopOffsets) projectionComponentLoopOffsets
+                pure (Right ())
+
+regionUnion, regionIntersection, regionDifference, regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+regionUnion = binaryRegionOperation (\(left, right) -> left || right)
+regionIntersection = binaryRegionOperation (\(left, right) -> left && right)
+regionDifference = binaryRegionOperation (\(left, right) -> left && not right)
+regionSymmetricDifference = binaryRegionOperation (uncurry (/=))
+
+binaryRegionOperation
+  :: ((Bool, Bool) -> Bool)
+  -> Ptr CRegion
+  -> Ptr CRegion
+  -> Ptr (Ptr CRegion)
+  -> Ptr CObstruction
+  -> IO CUInt
+binaryRegionOperation selected leftPointer rightPointer output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        left <- dereferenceHandle leftPointer
+        right <- dereferenceHandle rightPointer
+        pure (exactRegionBoolean selected left right)
+
+exactRegionBoolean
+  :: ((Bool, Bool) -> Bool)
+  -> T.PlanarRegion
+  -> T.PlanarRegion
+  -> Either AbiFailure T.PlanarRegion
+exactRegionBoolean selected left right = do
+  leftLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True left))
+  rightLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True right))
+  overlay <- first overlayFailure (T.overlayLayers leftLayer rightLayer)
+  first regionPublicationFailure (T.overlaySelectedRegion selected overlay)
+
+regionLocatePointF64
+  :: Ptr CRegion
+  -> CDouble
+  -> CDouble
+  -> Ptr CUInt
+  -> Ptr CObstruction
+  -> IO CUInt
+regionLocatePointF64 regionPointer (CDouble x) (CDouble y) output obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      requirePointer "region" regionPointer
+        >> requirePointer "location" output
+        >> first (pointInputFailure 0 (T.Point x y)) (T.exactPointFromPoint (T.Point x y))
+    of
+      Left failure -> pure (Left failure)
+      Right query -> do
+        region <- dereferenceHandle regionPointer
+        poke output (regionLocationCode (T.regionPointLocation region query))
+        pure (Right ())
+
+regionLocationCode :: T.RegionPointLocation -> CUInt
+regionLocationCode location =
+  fromIntegral
+    ( regionLocationCodeId
+        ( case location of
+            T.RegionExterior -> RegionLocationExterior
+            T.RegionOnBoundary -> RegionLocationBoundary
+            T.RegionInterior -> RegionLocationInterior
+        )
+    )
+
+regionMeasure
+  :: Ptr CRegion
+  -> Ptr Int64
+  -> Ptr CChar
+  -> CSize
+  -> Ptr CSize
+  -> Ptr CDouble
+  -> Ptr CDouble
+  -> Ptr CObstruction
+  -> IO CUInt
+regionMeasure regionPointer eulerOutput areaRatioOutput rawAreaCapacity areaBytesWritten perimeterLowerOutput perimeterUpperOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      requirePointer "region" regionPointer
+        >> requirePointer "euler_characteristic" eulerOutput
+        >> requirePointer "area_bytes_written" areaBytesWritten
+        >> requirePointer "perimeter_lower" perimeterLowerOutput
+        >> requirePointer "perimeter_upper" perimeterUpperOutput
+        >> checkedCount 1 rawAreaCapacity
+    of
+      Left failure -> pure (Left failure)
+      Right areaCapacity -> do
+        region <- dereferenceHandle regionPointer
+        case regionMeasurements region of
+          Left failure -> pure (Left failure)
+          Right (valuations, perimeter) -> do
+            let area = T.exactAreaValue (T.valuationArea valuations)
+                areaText =
+                  show (T.exactRationalNumerator area)
+                    <> "/"
+                    <> show (T.exactRationalDenominator area)
+                bounds = T.exactLengthBounds perimeter
+            poke eulerOutput (fromIntegral (T.eulerCharacteristicValue (T.valuationEuler valuations)))
+            poke perimeterLowerOutput (CDouble (T.intervalLower bounds))
+            poke perimeterUpperOutput (CDouble (T.intervalUpper bounds))
+            copyCStringOutput areaText areaRatioOutput areaCapacity areaBytesWritten
+
+regionMeasurements
+  :: T.PlanarRegion
+  -> Either AbiFailure (T.PlanarValuations, T.ExactLengthMeasurement)
+regionMeasurements region = do
+  valuations <- first valuationFailure (T.regionValuations region)
+  perimeter <- first valuationFailure (T.planarValuationsPerimeter valuations)
+  pure (valuations, perimeter)
+
+copyCStringOutput
+  :: String
+  -> Ptr CChar
+  -> Int
+  -> Ptr CSize
+  -> IO (Either AbiFailure ())
+copyCStringOutput value output capacity bytesWritten =
+  withCStringLen value $ \(source, byteCount) -> do
+    poke bytesWritten (fromIntegral byteCount)
+    case requireOutputCapacity "area_ratio_utf8" output (byteCount + 1) capacity of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        copyBytes output source byteCount
+        pokeElemOff output byteCount 0
+        pure (Right ())
+
+regionFree :: Ptr CRegion -> IO ()
+regionFree = freeHandle
diff --git a/ffi/bindings/README.md b/ffi/bindings/README.md
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/README.md
@@ -0,0 +1,104 @@
+# Foreign bindings
+
+`moonlight-triangulation.cabal` owns the private Haskell `ffi` sublibrary and
+`moonlight-triangulation-c` foreign library. Python, TypeScript, and Rust are
+thin lifecycle and bulk-layout leaves over that sole ABI; none implements
+triangulation, overlay, publication, valuation, or morphology.
+
+`Moonlight.Triangulation.Foreign.Contract` is the closed wire authority. Its
+pure renderer owns the installed header, Windows export list, Haskell export
+module, TypeScript wire vocabulary and raw declarations, Python ctypes
+declarations, and Rust extern block. Check or rewrite those derived sections
+from `compiler`—never edit them directly:
+
+```bash
+../scripts/safe-cabal.sh run \
+  moonlight-triangulation:exe:moonlight-triangulation-ffi-contract \
+  --project-file=cabal.project.triangulation-dev \
+  -- check foundation/moonlight-triangulation
+```
+
+From `compiler`, build and locate the shared library:
+
+```bash
+../scripts/safe-cabal.sh build \
+  moonlight-triangulation:flib:moonlight-triangulation-c \
+  --project-file=cabal.project.triangulation-dev --enable-shared -j1
+cabal list-bin moonlight-triangulation:flib:moonlight-triangulation-c \
+  --project-file=cabal.project.triangulation-dev --enable-shared
+```
+
+Set `MOONLIGHT_TRIANGULATION_LIBRARY` to that `.dylib`, `.so`, or `.dll`; Rust
+linking also reads its directory from `MOONLIGHT_TRIANGULATION_LIB_DIR`.
+
+## ABI version 2
+
+Three opaque immutable carriers cross the boundary:
+
+- `ml_mesh` retains a binary64 Delaunay mesh. Its `ml_mesh_site_*` algebra
+  combines sites, not polygon interiors.
+- `ml_region` retains the authoritative exact rational `PlanarRegion`.
+  `ml_region_create_f64` admits every input coordinate exactly as its binary64
+  dyadic value in one call, using point counts per loop and loop counts per
+  component. The inverse bulk projection returns component/loop offsets and
+  binary64 rendering points; it does not replace the exact retained geometry.
+- `ml_structuring_element` retains one admitted origin-containing convex
+  polygon for repeated offset, inset, opening, and closing calls.
+
+Region union, intersection, difference, and symmetric difference call the one
+Haskell overlay and grouped-publication path. Point location is exact. A single
+valuation call returns Euler characteristic, exact reduced area as
+`numerator/denominator`, and certified conventional-perimeter bounds. Morphology
+returns a new region plus the existing operation/work receipt.
+
+Every status-returning call accepts an optional `ml_obstruction` distinguishing
+pointer/count/buffer failures from region layout, validation, overlay,
+publication, valuation, projection, morphology, and runtime failures. Handles
+remain valid until explicitly freed; every operation publishes a fresh output
+handle. `ml_runtime_initialize` is idempotent and process-lifetime.
+
+Python and Rust owned handles are thread-affine: create, use, and release each
+handle on one thread. Python exposes context management plus `dispose`; Rust
+releases through `Drop`. This prevents release from racing a foreign call after
+the runtime has received the raw handle.
+
+Batch related mesh edits into one `insert_many` call. Author a whole region with
+one component list; there is deliberately no mutable builder or per-vertex FFI
+surface.
+
+## Python
+
+```python
+from fractions import Fraction
+from moonlight_triangulation import Moonlight, PolygonComponent
+
+moonlight = Moonlight()
+with moonlight.region([
+    PolygonComponent(((0, 0), (2, 0), (2, 2), (0, 2)))
+]) as left, moonlight.region([
+    PolygonComponent(((1, 0), (3, 0), (3, 2), (1, 2)))
+]) as right, left.intersection(right) as intersection:
+    assert intersection.valuations.area == Fraction(2, 1)
+```
+
+## TypeScript
+
+The [`@moonlight/triangulation` package guide](./typescript/README.md) owns
+installation, native-library loading, typed results, and resource lifetime.
+
+## Rust
+
+```rust
+use moonlight_triangulation::{Moonlight, PolygonComponent};
+
+let moonlight = Moonlight::initialize()?;
+let region = moonlight.region(&[PolygonComponent {
+    outer: vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]],
+    holes: vec![],
+}])?;
+println!("{:?}", region.valuations()?.area);
+```
+
+Rust reports native refusals as `MoonlightError::AbiObstruction` and keeps ABI
+version, initialization, malformed projection, and unknown-wire failures as
+distinct variants; it never forges a native obstruction for a wrapper defect.
diff --git a/ffi/bindings/python/pyproject.toml b/ffi/bindings/python/pyproject.toml
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/pyproject.toml
@@ -0,0 +1,13 @@
+[build-system]
+requires = ["hatchling>=1.27,<2"]
+build-backend = "hatchling.build"
+
+[project]
+name = "moonlight-triangulation"
+version = "2.0.0"
+description = "Python bindings for Moonlight exact regions and immutable Delaunay meshes."
+requires-python = ">=3.11"
+license = { text = "MIT" }
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/moonlight_triangulation"]
diff --git a/ffi/bindings/python/src/moonlight_triangulation/__init__.py b/ffi/bindings/python/src/moonlight_triangulation/__init__.py
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/src/moonlight_triangulation/__init__.py
@@ -0,0 +1,26 @@
+from ._binding import Mesh, Moonlight, Region, StructuringElement
+from .errors import MoonlightError
+from .values import (
+    MinkowskiOperation,
+    MinkowskiReceipt,
+    Point,
+    PolygonComponent,
+    RegionLocation,
+    RegionValuations,
+    Triangle,
+)
+
+__all__ = [
+    "Mesh",
+    "MinkowskiOperation",
+    "MinkowskiReceipt",
+    "Moonlight",
+    "MoonlightError",
+    "Point",
+    "PolygonComponent",
+    "Region",
+    "RegionLocation",
+    "RegionValuations",
+    "StructuringElement",
+    "Triangle",
+]
diff --git a/ffi/bindings/python/src/moonlight_triangulation/_binding.py b/ffi/bindings/python/src/moonlight_triangulation/_binding.py
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/src/moonlight_triangulation/_binding.py
@@ -0,0 +1,450 @@
+from __future__ import annotations
+
+import ctypes
+import os
+import weakref
+from collections.abc import Callable, Iterable, Sequence
+from fractions import Fraction
+from pathlib import Path
+from threading import get_ident
+from typing import Self
+
+from ._native_generated import (
+    ABI_VERSION,
+    ML_OBSTRUCTION_BUFFER_TOO_SMALL,
+    ML_STATUS_BUFFER_TOO_SMALL,
+    ML_STATUS_OK,
+    _NativeApi,
+    _NativeMinkowskiReceipt,
+    _Obstruction,
+)
+from .errors import MoonlightError
+from .values import (
+    MinkowskiOperation,
+    MinkowskiReceipt,
+    Point,
+    PolygonComponent,
+    RegionLocation,
+    RegionValuations,
+    Triangle,
+)
+
+_BinaryNativeOperation = Callable[[ctypes.c_void_p, ctypes.c_void_p, object, object], int]
+_MorphologyNativeOperation = Callable[[ctypes.c_void_p, ctypes.c_void_p, object, object, object], int]
+
+class Moonlight:
+    def __init__(self, library_path: str | os.PathLike[str] | None = None) -> None:
+        configured_path = (
+            os.environ.get("MOONLIGHT_TRIANGULATION_LIBRARY")
+            if library_path is None
+            else library_path
+        )
+        if configured_path is None:
+            raise ValueError("set MOONLIGHT_TRIANGULATION_LIBRARY or pass library_path")
+        if os.fspath(configured_path) == "":
+            raise ValueError("Moonlight native library path must not be empty")
+        self._native = _NativeApi(Path(configured_path))
+        initialization_status = int(self._native.library.ml_runtime_initialize())
+        if initialization_status != ML_STATUS_OK:
+            raise RuntimeError(
+                f"Moonlight runtime initialization failed with status {initialization_status}"
+            )
+        observed_version = int(self._native.library.ml_abi_version())
+        if observed_version != ABI_VERSION:
+            raise RuntimeError(f"unsupported Moonlight ABI version {observed_version}")
+
+    def delaunay(self, points: Sequence[Point]) -> Mesh:
+        coordinates, pointer = _coordinate_buffer(points)
+        handle = _produce_handle(
+            lambda output, obstruction: self._native.library.ml_delaunay_f64(
+                pointer, len(coordinates) // 2, output, obstruction
+            ),
+        )
+        return Mesh(self._native, handle)
+
+    def region(self, components: Sequence[PolygonComponent]) -> Region:
+        loops, loop_counts, component_counts = _component_layout(components)
+        coordinates, coordinate_pointer = _coordinate_buffer(
+            point for loop in loops for point in loop
+        )
+        loop_buffer = _size_buffer(loop_counts)
+        component_buffer = _size_buffer(component_counts)
+        handle = _produce_handle(
+            lambda output, obstruction: self._native.library.ml_region_create_f64(
+                coordinate_pointer,
+                len(coordinates) // 2,
+                loop_buffer,
+                len(loop_counts),
+                component_buffer,
+                len(component_counts),
+                output,
+                obstruction,
+            ),
+        )
+        return Region(self._native, handle)
+
+    def structuring_element(self, points: Sequence[Point]) -> StructuringElement:
+        coordinates, pointer = _coordinate_buffer(points)
+        handle = _produce_handle(
+            lambda output, obstruction: self._native.library.ml_structuring_element_create_f64(
+                pointer, len(coordinates) // 2, output, obstruction
+            ),
+        )
+        return StructuringElement(self._native, handle)
+
+
+class _OwnedHandle:
+    __slots__ = ("_native", "_handle", "_finalizer", "_kind", "_owner_thread", "__weakref__")
+
+    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p, free: Callable[[ctypes.c_void_p], None], kind: str) -> None:
+        self._native = native
+        self._handle = handle
+        self._kind = kind
+        self._owner_thread = get_ident()
+        self._finalizer = weakref.finalize(self, free, handle)
+
+    def dispose(self) -> None:
+        self._require_owner_thread()
+        self._finalizer()
+
+    def __enter__(self) -> Self:
+        return self
+
+    def __exit__(self, _type: object, _value: object, _traceback: object) -> None:
+        self.dispose()
+
+    def _live_handle(self) -> ctypes.c_void_p:
+        self._require_owner_thread()
+        if not self._finalizer.alive:
+            raise RuntimeError(f"{self._kind} is disposed")
+        return self._handle
+
+    def _require_owner_thread(self) -> None:
+        if get_ident() != self._owner_thread:
+            raise RuntimeError(f"{self._kind} belongs to another thread")
+
+    def _require_same_runtime(self, other: _OwnedHandle) -> None:
+        if self._native is not other._native:
+            raise ValueError("both values must belong to the same Moonlight runtime")
+
+
+class Mesh(_OwnedHandle):
+    __slots__ = ()
+
+    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:
+        super().__init__(native, handle, native.library.ml_mesh_free, "mesh")
+
+    @property
+    def vertex_count(self) -> int:
+        return self._count(self._native.library.ml_mesh_vertex_count)
+
+    @property
+    def triangle_count(self) -> int:
+        return self._count(self._native.library.ml_mesh_triangle_count)
+
+    @property
+    def vertices(self) -> tuple[Point, ...]:
+        count = self.vertex_count
+        output = (ctypes.c_double * (count * 2))()
+        written = ctypes.c_size_t()
+        obstruction = _Obstruction()
+        status = int(
+            self._native.library.ml_mesh_copy_vertices_f64(
+                self._live_handle(), output, count, ctypes.byref(written), ctypes.byref(obstruction)
+            )
+        )
+        _check_status(status, obstruction)
+        return tuple((float(output[index * 2]), float(output[index * 2 + 1])) for index in range(written.value))
+
+    @property
+    def triangles(self) -> tuple[Triangle, ...]:
+        count = self.triangle_count
+        output = (ctypes.c_uint32 * (count * 3))()
+        written = ctypes.c_size_t()
+        obstruction = _Obstruction()
+        status = int(
+            self._native.library.ml_mesh_copy_triangles_u32(
+                self._live_handle(), output, count, ctypes.byref(written), ctypes.byref(obstruction)
+            )
+        )
+        _check_status(status, obstruction)
+        return tuple(
+            (int(output[index * 3]), int(output[index * 3 + 1]), int(output[index * 3 + 2]))
+            for index in range(written.value)
+        )
+
+    def insert_many(self, points: Sequence[Point]) -> Mesh:
+        coordinates, pointer = _coordinate_buffer(points)
+        handle = _produce_handle(
+            lambda output, obstruction: self._native.library.ml_mesh_insert_many_f64(
+                self._live_handle(), pointer, len(coordinates) // 2, output, obstruction
+            ),
+        )
+        return Mesh(self._native, handle)
+
+    def site_union(self, other: Mesh) -> Mesh:
+        return self._binary(other, self._native.library.ml_mesh_site_union)
+
+    def site_intersection(self, other: Mesh) -> Mesh:
+        return self._binary(other, self._native.library.ml_mesh_site_intersection)
+
+    def site_difference(self, other: Mesh) -> Mesh:
+        return self._binary(other, self._native.library.ml_mesh_site_difference)
+
+    def site_symmetric_difference(self, other: Mesh) -> Mesh:
+        return self._binary(other, self._native.library.ml_mesh_site_symmetric_difference)
+
+    def _binary(self, other: Mesh, operation: _BinaryNativeOperation) -> Mesh:
+        self._require_same_runtime(other)
+        handle = _produce_handle(
+            lambda output, obstruction: operation(
+                self._live_handle(), other._live_handle(), output, obstruction
+            ),
+        )
+        return Mesh(self._native, handle)
+
+    def _count(self, operation: Callable[[ctypes.c_void_p, object, object], int]) -> int:
+        output = ctypes.c_size_t()
+        obstruction = _Obstruction()
+        status = int(operation(self._live_handle(), ctypes.byref(output), ctypes.byref(obstruction)))
+        _check_status(status, obstruction)
+        return int(output.value)
+
+
+class Region(_OwnedHandle):
+    __slots__ = ()
+
+    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:
+        super().__init__(native, handle, native.library.ml_region_free, "region")
+
+    @property
+    def components(self) -> tuple[PolygonComponent, ...]:
+        component_count, loop_count, point_count = self._counts()
+        coordinates = (ctypes.c_double * (point_count * 2))()
+        loop_offsets = (ctypes.c_size_t * (loop_count + 1))()
+        component_offsets = (ctypes.c_size_t * (component_count + 1))()
+        obstruction = _Obstruction()
+        status = int(
+            self._native.library.ml_region_copy_f64(
+                self._live_handle(),
+                coordinates,
+                point_count,
+                loop_offsets,
+                loop_count + 1,
+                component_offsets,
+                component_count + 1,
+                ctypes.byref(obstruction),
+            )
+        )
+        _check_status(status, obstruction)
+        points = tuple((float(coordinates[index * 2]), float(coordinates[index * 2 + 1])) for index in range(point_count))
+        loops = tuple(
+            points[int(loop_offsets[index]) : int(loop_offsets[index + 1])]
+            for index in range(loop_count)
+        )
+        component_ranges = tuple(
+            (int(component_offsets[index]), int(component_offsets[index + 1]))
+            for index in range(component_count)
+        )
+        if any(start >= end for start, end in component_ranges):
+            raise RuntimeError("Moonlight returned a component without an outer loop")
+        return tuple(
+            PolygonComponent(loops[start], loops[start + 1 : end])
+            for start, end in component_ranges
+        )
+
+    @property
+    def valuations(self) -> RegionValuations:
+        return self._measure_with_capacity(128)
+
+    def locate(self, point: Point) -> RegionLocation:
+        output = ctypes.c_uint32()
+        obstruction = _Obstruction()
+        status = int(
+            self._native.library.ml_region_locate_point_f64(
+                self._live_handle(), point[0], point[1], ctypes.byref(output), ctypes.byref(obstruction)
+            )
+        )
+        _check_status(status, obstruction)
+        return RegionLocation(output.value)
+
+    def union(self, other: Region) -> Region:
+        return self._binary(other, self._native.library.ml_region_union)
+
+    def intersection(self, other: Region) -> Region:
+        return self._binary(other, self._native.library.ml_region_intersection)
+
+    def difference(self, other: Region) -> Region:
+        return self._binary(other, self._native.library.ml_region_difference)
+
+    def symmetric_difference(self, other: Region) -> Region:
+        return self._binary(other, self._native.library.ml_region_symmetric_difference)
+
+    def minkowski_sum(self, other: Region) -> tuple[Region, MinkowskiReceipt]:
+        self._require_same_runtime(other)
+        return self._morph(
+            lambda output, receipt, obstruction: self._native.library.ml_region_minkowski_sum(
+                self._live_handle(), other._live_handle(), output, receipt, obstruction
+            )
+        )
+
+    def offset(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
+        return self._with_element(element, self._native.library.ml_region_offset)
+
+    def inset(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
+        return self._with_element(element, self._native.library.ml_region_inset)
+
+    def open(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
+        return self._with_element(element, self._native.library.ml_region_open)
+
+    def close(self, element: StructuringElement) -> tuple[Region, MinkowskiReceipt]:
+        return self._with_element(element, self._native.library.ml_region_close)
+
+    def _binary(self, other: Region, operation: _BinaryNativeOperation) -> Region:
+        self._require_same_runtime(other)
+        handle = _produce_handle(
+            lambda output, obstruction: operation(
+                self._live_handle(), other._live_handle(), output, obstruction
+            ),
+        )
+        return Region(self._native, handle)
+
+    def _with_element(
+        self, element: StructuringElement, operation: _MorphologyNativeOperation
+    ) -> tuple[Region, MinkowskiReceipt]:
+        self._require_same_runtime(element)
+        return self._morph(
+            lambda output, receipt, obstruction: operation(
+                element._live_handle(), self._live_handle(), output, receipt, obstruction
+            )
+        )
+
+    def _morph(
+        self,
+        operation: Callable[[object, object, object], int],
+    ) -> tuple[Region, MinkowskiReceipt]:
+        handle, native_receipt = _produce_morphology(operation)
+        return Region(self._native, handle), _receipt(native_receipt)
+
+    def _counts(self) -> tuple[int, int, int]:
+        component_count = ctypes.c_size_t()
+        loop_count = ctypes.c_size_t()
+        point_count = ctypes.c_size_t()
+        obstruction = _Obstruction()
+        status = int(
+            self._native.library.ml_region_counts(
+                self._live_handle(),
+                ctypes.byref(component_count),
+                ctypes.byref(loop_count),
+                ctypes.byref(point_count),
+                ctypes.byref(obstruction),
+            )
+        )
+        _check_status(status, obstruction)
+        return int(component_count.value), int(loop_count.value), int(point_count.value)
+
+    def _measure_with_capacity(self, capacity: int) -> RegionValuations:
+        euler = ctypes.c_int64()
+        area = ctypes.create_string_buffer(capacity)
+        area_bytes = ctypes.c_size_t()
+        lower = ctypes.c_double()
+        upper = ctypes.c_double()
+        obstruction = _Obstruction()
+        status = int(
+            self._native.library.ml_region_measure(
+                self._live_handle(),
+                ctypes.byref(euler),
+                area,
+                capacity,
+                ctypes.byref(area_bytes),
+                ctypes.byref(lower),
+                ctypes.byref(upper),
+                ctypes.byref(obstruction),
+            )
+        )
+        if (
+            status == ML_STATUS_BUFFER_TOO_SMALL
+            and obstruction.code == ML_OBSTRUCTION_BUFFER_TOO_SMALL
+        ):
+            return self._measure_with_capacity(int(area_bytes.value) + 1)
+        _check_status(status, obstruction)
+        numerator, separator, denominator = area.value.decode("ascii").partition("/")
+        if separator != "/":
+            raise RuntimeError("Moonlight returned a malformed exact-area ratio")
+        return RegionValuations(int(euler.value), Fraction(int(numerator), int(denominator)), (lower.value, upper.value))
+
+
+class StructuringElement(_OwnedHandle):
+    __slots__ = ()
+
+    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:
+        super().__init__(native, handle, native.library.ml_structuring_element_free, "structuring element")
+
+
+def _component_layout(
+    components: Sequence[PolygonComponent],
+) -> tuple[tuple[Sequence[Point], ...], tuple[int, ...], tuple[int, ...]]:
+    loops = tuple(
+        loop
+        for component in components
+        for loop in (component.outer, *component.holes)
+    )
+    loop_counts = tuple(len(loop) for loop in loops)
+    component_counts = tuple(len(component.holes) + 1 for component in components)
+    return loops, loop_counts, component_counts
+
+
+def _coordinate_buffer(points: Iterable[Point]) -> tuple[tuple[float, ...], object]:
+    coordinates = tuple(component for x, y in points for component in (x, y))
+    pointer = None if not coordinates else (ctypes.c_double * len(coordinates))(*coordinates)
+    return coordinates, pointer
+
+
+def _size_buffer(values: Sequence[int]) -> object:
+    return (ctypes.c_size_t * len(values))(*values)
+
+
+def _produce_handle(
+    operation: Callable[[object, object], int],
+) -> ctypes.c_void_p:
+    output = ctypes.c_void_p()
+    obstruction = _Obstruction()
+    status = int(operation(ctypes.byref(output), ctypes.byref(obstruction)))
+    _check_status(status, obstruction)
+    return _required_handle(output)
+
+
+def _produce_morphology(
+    operation: Callable[[object, object, object], int],
+) -> tuple[ctypes.c_void_p, _NativeMinkowskiReceipt]:
+    output = ctypes.c_void_p()
+    receipt = _NativeMinkowskiReceipt()
+    obstruction = _Obstruction()
+    status = int(operation(ctypes.byref(output), ctypes.byref(receipt), ctypes.byref(obstruction)))
+    _check_status(status, obstruction)
+    return _required_handle(output), receipt
+
+
+def _receipt(native: _NativeMinkowskiReceipt) -> MinkowskiReceipt:
+    return MinkowskiReceipt(
+        MinkowskiOperation(native.operation),
+        int(native.input_components),
+        int(native.convex_pieces),
+        int(native.generated_pieces),
+        int(native.generated_convolution_edges),
+        int(native.overlay_passes),
+        int(native.exact_crossings),
+        int(native.output_cells),
+        int(native.exact_coordinate_bit_growth),
+    )
+
+
+def _check_status(status: int, obstruction: _Obstruction) -> None:
+    if status != ML_STATUS_OK:
+        raise MoonlightError(status, obstruction)
+
+
+def _required_handle(handle: ctypes.c_void_p) -> ctypes.c_void_p:
+    if not handle.value:
+        raise RuntimeError("Moonlight returned success without a handle")
+    return handle
diff --git a/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py b/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
@@ -0,0 +1,183 @@
+# Generated from Moonlight.Triangulation.Foreign.Contract.
+# Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+from __future__ import annotations
+
+import ctypes
+from collections.abc import Sequence
+from pathlib import Path
+from typing import Final
+
+ABI_VERSION: Final = 2
+ML_STATUS_OK: Final = 0
+ML_STATUS_NULL_POINTER: Final = 1
+ML_STATUS_COUNT_OVERFLOW: Final = 2
+ML_STATUS_BUFFER_TOO_SMALL: Final = 3
+ML_STATUS_GEOMETRY_OBSTRUCTION: Final = 4
+ML_STATUS_RUNTIME_FAILURE: Final = 5
+ML_OBSTRUCTION_NONE: Final = 0
+ML_OBSTRUCTION_INVALID_COORDINATE: Final = 1
+ML_OBSTRUCTION_POINT_LOCATION_FAILED: Final = 2
+ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED: Final = 3
+ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID: Final = 4
+ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX: Final = 5
+ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING: Final = 6
+ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING: Final = 7
+ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING: Final = 8
+ML_OBSTRUCTION_HULL_START_NOT_VISIBLE: Final = 9
+ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE: Final = 10
+ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE: Final = 11
+ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED: Final = 12
+ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE: Final = 13
+ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE: Final = 14
+ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE: Final = 15
+ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE: Final = 16
+ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION: Final = 17
+ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH: Final = 18
+ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH: Final = 19
+ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT: Final = 20
+ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH: Final = 21
+ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE: Final = 22
+ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY: Final = 23
+ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE: Final = 24
+ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH: Final = 25
+ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH: Final = 26
+ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED: Final = 27
+ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE: Final = 28
+ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE: Final = 29
+ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE: Final = 30
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE: Final = 31
+ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE: Final = 32
+ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE: Final = 33
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE: Final = 34
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE: Final = 35
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE: Final = 36
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE: Final = 37
+ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM: Final = 38
+ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE: Final = 39
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE: Final = 40
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING: Final = 41
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS: Final = 42
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED: Final = 43
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION: Final = 44
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION: Final = 45
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION: Final = 46
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE: Final = 47
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE: Final = 48
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED: Final = 49
+ML_OBSTRUCTION_CAPACITY_EXCEEDED: Final = 50
+ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED: Final = 51
+ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED: Final = 52
+ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE: Final = 53
+ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH: Final = 54
+ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE: Final = 55
+ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE: Final = 56
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET: Final = 57
+ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP: Final = 58
+ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED: Final = 59
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE: Final = 60
+ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE: Final = 61
+ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE: Final = 62
+ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED: Final = 63
+ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED: Final = 64
+ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE: Final = 65
+ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED: Final = 66
+ML_OBSTRUCTION_NULL_POINTER: Final = 100
+ML_OBSTRUCTION_COUNT_OVERFLOW: Final = 101
+ML_OBSTRUCTION_BUFFER_TOO_SMALL: Final = 102
+ML_OBSTRUCTION_RUNTIME_FAILURE: Final = 103
+ML_OBSTRUCTION_REGION_LAYOUT_INVALID: Final = 200
+ML_OBSTRUCTION_REGION_VALIDATION_FAILED: Final = 201
+ML_OBSTRUCTION_OVERLAY_FAILED: Final = 202
+ML_OBSTRUCTION_REGION_PUBLICATION_FAILED: Final = 203
+ML_OBSTRUCTION_VALUATION_FAILED: Final = 204
+ML_OBSTRUCTION_MINKOWSKI_FAILED: Final = 205
+ML_OBSTRUCTION_REGION_PROJECTION_FAILED: Final = 206
+ML_COORDINATE_ERROR_NONE: Final = 0
+ML_COORDINATE_ERROR_NAN: Final = 1
+ML_COORDINATE_ERROR_INFINITE: Final = 2
+ML_COORDINATE_ERROR_TOO_SMALL: Final = 3
+ML_COORDINATE_ERROR_TOO_LARGE: Final = 4
+ML_REGION_EXTERIOR: Final = 0
+ML_REGION_BOUNDARY: Final = 1
+ML_REGION_INTERIOR: Final = 2
+ML_MINKOWSKI_ADDITION: Final = 0
+ML_MINKOWSKI_EROSION: Final = 1
+ML_MINKOWSKI_OPENING: Final = 2
+ML_MINKOWSKI_CLOSING: Final = 3
+
+class _Obstruction(ctypes.Structure):
+    _fields_ = [
+        ("code", ctypes.c_uint32),
+        ("coordinate_error", ctypes.c_uint32),
+        ("input_index", ctypes.c_uint64),
+        ("first_index", ctypes.c_uint64),
+        ("second_index", ctypes.c_uint64),
+        ("first_value", ctypes.c_double),
+        ("second_value", ctypes.c_double),
+        ("point_x", ctypes.c_double),
+        ("point_y", ctypes.c_double),
+        ("message", ctypes.c_char * 256),
+    ]
+
+
+class _NativeMinkowskiReceipt(ctypes.Structure):
+    _fields_ = [
+        ("operation", ctypes.c_uint32),
+        ("reserved", ctypes.c_uint32),
+        ("input_components", ctypes.c_uint64),
+        ("convex_pieces", ctypes.c_uint64),
+        ("generated_pieces", ctypes.c_uint64),
+        ("generated_convolution_edges", ctypes.c_uint64),
+        ("overlay_passes", ctypes.c_uint64),
+        ("exact_crossings", ctypes.c_uint64),
+        ("output_cells", ctypes.c_uint64),
+        ("exact_coordinate_bit_growth", ctypes.c_uint64),
+    ]
+
+
+class _NativeApi:
+    def __init__(self, library_path: Path) -> None:
+        library = ctypes.CDLL(str(library_path))
+        _configure(library, "ml_abi_version", (), ctypes.c_uint32)
+        _configure(library, "ml_runtime_initialize", (), ctypes.c_uint32)
+        _configure(library, "ml_delaunay_f64", (ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_insert_many_f64", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_site_union", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_site_intersection", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_site_difference", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_site_symmetric_difference", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_vertex_count", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_triangle_count", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_copy_vertices_f64", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_copy_triangles_u32", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_mesh_free", (ctypes.c_void_p,), None)
+        _configure(library, "ml_region_create_f64", (ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_counts", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_copy_f64", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_union", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_intersection", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_difference", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_symmetric_difference", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_locate_point_f64", (ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_measure", (ctypes.c_void_p, ctypes.POINTER(ctypes.c_int64), ctypes.POINTER(ctypes.c_char), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_free", (ctypes.c_void_p,), None)
+        _configure(library, "ml_structuring_element_create_f64", (ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_structuring_element_free", (ctypes.c_void_p,), None)
+        _configure(library, "ml_region_minkowski_sum", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_NativeMinkowskiReceipt), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_offset", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_NativeMinkowskiReceipt), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_inset", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_NativeMinkowskiReceipt), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_open", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_NativeMinkowskiReceipt), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        _configure(library, "ml_region_close", (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_NativeMinkowskiReceipt), ctypes.POINTER(_Obstruction)), ctypes.c_uint32)
+        self.library = library
+
+
+def _configure(
+    library: ctypes.CDLL,
+    name: str,
+    parameters: Sequence[object],
+    result: object = ctypes.c_uint32,
+) -> None:
+    function = getattr(library, name)
+    setattr(function, "argtypes", list(parameters))
+    setattr(function, "restype", result)
diff --git a/ffi/bindings/python/src/moonlight_triangulation/errors.py b/ffi/bindings/python/src/moonlight_triangulation/errors.py
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/src/moonlight_triangulation/errors.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+from ._native_generated import _Obstruction
+
+
+class MoonlightError(RuntimeError):
+    def __init__(self, status: int, obstruction: _Obstruction) -> None:
+        self.status = status
+        self.code = obstruction.code
+        self.coordinate_error = obstruction.coordinate_error
+        self.input_index = obstruction.input_index
+        self.first_index = obstruction.first_index
+        self.second_index = obstruction.second_index
+        self.first_value = obstruction.first_value
+        self.second_value = obstruction.second_value
+        self.point = (obstruction.point_x, obstruction.point_y)
+        message = bytes(obstruction.message).split(b"\0", 1)[0].decode("utf-8", errors="replace")
+        super().__init__(message or f"Moonlight ABI failure {status}:{self.code}")
diff --git a/ffi/bindings/python/src/moonlight_triangulation/values.py b/ffi/bindings/python/src/moonlight_triangulation/values.py
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/src/moonlight_triangulation/values.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import IntEnum
+from fractions import Fraction
+
+from ._native_generated import (
+    ML_MINKOWSKI_ADDITION,
+    ML_MINKOWSKI_CLOSING,
+    ML_MINKOWSKI_EROSION,
+    ML_MINKOWSKI_OPENING,
+    ML_REGION_BOUNDARY,
+    ML_REGION_EXTERIOR,
+    ML_REGION_INTERIOR,
+)
+
+Point = tuple[float, float]
+Triangle = tuple[int, int, int]
+
+
+@dataclass(frozen=True)
+class PolygonComponent:
+    outer: tuple[Point, ...]
+    holes: tuple[tuple[Point, ...], ...] = ()
+
+
+class RegionLocation(IntEnum):
+    EXTERIOR = ML_REGION_EXTERIOR
+    BOUNDARY = ML_REGION_BOUNDARY
+    INTERIOR = ML_REGION_INTERIOR
+
+
+class MinkowskiOperation(IntEnum):
+    ADDITION = ML_MINKOWSKI_ADDITION
+    EROSION = ML_MINKOWSKI_EROSION
+    OPENING = ML_MINKOWSKI_OPENING
+    CLOSING = ML_MINKOWSKI_CLOSING
+
+
+@dataclass(frozen=True)
+class RegionValuations:
+    euler_characteristic: int
+    area: Fraction
+    perimeter_bounds: tuple[float, float]
+
+
+@dataclass(frozen=True)
+class MinkowskiReceipt:
+    operation: MinkowskiOperation
+    input_components: int
+    convex_pieces: int
+    generated_pieces: int
+    generated_convolution_edges: int
+    overlay_passes: int
+    exact_crossings: int
+    output_cells: int
+    exact_coordinate_bit_growth: int
diff --git a/ffi/bindings/python/tests/test_binding.py b/ffi/bindings/python/tests/test_binding.py
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/python/tests/test_binding.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+import math
+import os
+import unittest
+from collections.abc import Sequence
+from concurrent.futures import ThreadPoolExecutor
+from fractions import Fraction
+from typing import ClassVar
+
+from moonlight_triangulation import (
+    MinkowskiOperation,
+    Moonlight,
+    MoonlightError,
+    PolygonComponent,
+    RegionLocation,
+)
+
+
+class LengthInflatingPoints(Sequence[tuple[float, float]]):
+    def __len__(self) -> int:
+        return 1_000
+
+    def __getitem__(self, index: int) -> tuple[float, float]:
+        if index == 0:
+            return (0.0, 0.0)
+        raise IndexError(index)
+
+
+class MoonlightBindingTest(unittest.TestCase):
+    engine: ClassVar[Moonlight]
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.engine = Moonlight(os.environ["MOONLIGHT_TRIANGULATION_LIBRARY"])
+
+    def test_immutable_site_set_algebra_and_dense_projection(self) -> None:
+        left = self.engine.delaunay([(0, 0), (2, 0), (0, 2), (2, 2)])
+        right = self.engine.delaunay([(2, 0), (4, 0), (2, 2), (4, 2)])
+        union = left.site_union(right)
+        intersection = left.site_intersection(right)
+        difference = left.site_difference(right)
+        symmetric = left.site_symmetric_difference(right)
+        extended = left.insert_many([(1, 1), (3, 1)])
+        self.addCleanup(left.dispose)
+        self.addCleanup(right.dispose)
+        self.addCleanup(union.dispose)
+        self.addCleanup(intersection.dispose)
+        self.addCleanup(difference.dispose)
+        self.addCleanup(symmetric.dispose)
+        self.addCleanup(extended.dispose)
+
+        self.assertEqual(left.vertex_count, 4)
+        self.assertEqual(union.vertex_count, 6)
+        self.assertEqual(intersection.vertex_count, 2)
+        self.assertEqual(difference.vertex_count, 2)
+        self.assertEqual(symmetric.vertex_count, 4)
+        self.assertEqual(extended.vertex_count, 6)
+        self.assertEqual(len(left.vertices), left.vertex_count)
+        self.assertEqual(len(left.triangles), left.triangle_count)
+        self.assertTrue(all(max(triangle) < left.vertex_count for triangle in left.triangles))
+
+    def test_exact_region_boolean_valuation_and_morphology(self) -> None:
+        left = self.engine.region([PolygonComponent(((0, 0), (2, 0), (2, 2), (0, 2)))])
+        right = self.engine.region([PolygonComponent(((1, 0), (3, 0), (3, 2), (1, 2)))])
+        intersection = left.intersection(right)
+        difference = left.difference(right)
+        symmetric = left.symmetric_difference(right)
+        kernel = self.engine.structuring_element(((-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)))
+        offset, receipt = left.offset(kernel)
+        self.addCleanup(left.dispose)
+        self.addCleanup(right.dispose)
+        self.addCleanup(intersection.dispose)
+        self.addCleanup(difference.dispose)
+        self.addCleanup(symmetric.dispose)
+        self.addCleanup(kernel.dispose)
+        self.addCleanup(offset.dispose)
+
+        self.assertEqual(len(left.components), 1)
+        self.assertEqual(len(left.components[0].outer), 4)
+        self.assertEqual(intersection.valuations.area, Fraction(2, 1))
+        self.assertEqual(difference.valuations.euler_characteristic, 1)
+        self.assertEqual(symmetric.valuations.euler_characteristic, 2)
+        self.assertEqual(left.locate((1, 1)), RegionLocation.INTERIOR)
+        self.assertEqual(left.locate((0, 1)), RegionLocation.BOUNDARY)
+        self.assertEqual(left.locate((3, 1)), RegionLocation.EXTERIOR)
+        self.assertEqual(offset.valuations.area, Fraction(9, 1))
+        self.assertEqual(receipt.operation, MinkowskiOperation.ADDITION)
+        self.assertGreaterEqual(receipt.generated_pieces, 1)
+
+    def test_invalid_coordinate_preserves_typed_witness(self) -> None:
+        with self.assertRaises(MoonlightError) as raised:
+            self.engine.delaunay([(0, 0), (math.nan, 1), (1, 0)])
+        self.assertEqual(raised.exception.status, 4)
+        self.assertEqual(raised.exception.code, 1)
+        self.assertEqual(raised.exception.coordinate_error, 1)
+        self.assertEqual(raised.exception.input_index, 1)
+
+    def test_native_count_descends_from_materialized_coordinates(self) -> None:
+        mesh = self.engine.delaunay(LengthInflatingPoints())
+        self.addCleanup(mesh.dispose)
+        self.assertEqual(mesh.vertex_count, 1)
+
+    def test_explicit_empty_library_path_is_not_an_environment_fallback(self) -> None:
+        with self.assertRaisesRegex(ValueError, "must not be empty"):
+            Moonlight("")
+
+    def test_owned_handles_are_thread_affine(self) -> None:
+        mesh = self.engine.delaunay([(0, 0), (1, 0), (0, 1)])
+        self.addCleanup(mesh.dispose)
+        with ThreadPoolExecutor(max_workers=1) as executor:
+            foreign_access = executor.submit(lambda: mesh.vertex_count)
+            with self.assertRaisesRegex(RuntimeError, "belongs to another thread"):
+                foreign_access.result()
+        self.assertEqual(mesh.vertex_count, 3)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/ffi/bindings/rust/Cargo.toml b/ffi/bindings/rust/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "moonlight-triangulation"
+version = "2.0.0"
+edition = "2024"
+license = "MIT"
+description = "Rust bindings for Moonlight exact regions and immutable Delaunay meshes."
+repository = "https://github.com/PaleRoses/moonlight.git"
+build = "build.rs"
+links = "moonlight-triangulation-c"
+
+[lib]
+path = "src/lib.rs"
+
+[workspace]
diff --git a/ffi/bindings/rust/build.rs b/ffi/bindings/rust/build.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/build.rs
@@ -0,0 +1,11 @@
+use std::env;
+
+fn main() {
+    println!("cargo:rerun-if-env-changed=MOONLIGHT_TRIANGULATION_LIB_DIR");
+    if let Some(directory) = env::var_os("MOONLIGHT_TRIANGULATION_LIB_DIR") {
+        println!(
+            "cargo:rustc-link-search=native={}",
+            directory.to_string_lossy()
+        );
+    }
+}
diff --git a/ffi/bindings/rust/src/binding.rs b/ffi/bindings/rust/src/binding.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/src/binding.rs
@@ -0,0 +1,646 @@
+use std::marker::PhantomData;
+use std::ptr::NonNull;
+use std::rc::Rc;
+
+use crate::error::{
+    MoonlightError, WireVocabulary, invalid_native_result, projection_shape_error, status_result,
+    success_without_handle_error,
+};
+use crate::raw_generated::{
+    ABI_VERSION, ML_MINKOWSKI_ADDITION, ML_MINKOWSKI_CLOSING, ML_MINKOWSKI_EROSION,
+    ML_MINKOWSKI_OPENING, ML_OBSTRUCTION_BUFFER_TOO_SMALL, ML_REGION_BOUNDARY, ML_REGION_EXTERIOR,
+    ML_REGION_INTERIOR, ML_STATUS_BUFFER_TOO_SMALL, ML_STATUS_OK, NativeMesh,
+    NativeMinkowskiReceipt, NativeObstruction, NativeRegion, NativeStructuringElement,
+    ml_abi_version, ml_delaunay_f64, ml_mesh_copy_triangles_u32, ml_mesh_copy_vertices_f64,
+    ml_mesh_free, ml_mesh_insert_many_f64, ml_mesh_site_difference, ml_mesh_site_intersection,
+    ml_mesh_site_symmetric_difference, ml_mesh_site_union, ml_mesh_triangle_count,
+    ml_mesh_vertex_count, ml_region_close, ml_region_copy_f64, ml_region_counts,
+    ml_region_create_f64, ml_region_difference, ml_region_free, ml_region_inset,
+    ml_region_intersection, ml_region_locate_point_f64, ml_region_measure, ml_region_minkowski_sum,
+    ml_region_offset, ml_region_open, ml_region_symmetric_difference, ml_region_union,
+    ml_runtime_initialize, ml_structuring_element_create_f64, ml_structuring_element_free,
+};
+use crate::values::{
+    ExactRational, MinkowskiOperation, MinkowskiReceipt, PolygonComponent, RegionLocation,
+    RegionValuations,
+};
+
+pub struct Moonlight;
+
+impl Moonlight {
+    pub fn initialize() -> Result<Self, MoonlightError> {
+        let status = unsafe { ml_runtime_initialize() };
+        if status != ML_STATUS_OK {
+            return Err(MoonlightError::RuntimeInitializationRefused { status });
+        }
+        let version = unsafe { ml_abi_version() };
+        if version != ABI_VERSION {
+            return Err(MoonlightError::AbiVersionMismatch {
+                expected: ABI_VERSION,
+                observed: version,
+            });
+        }
+        Ok(Self)
+    }
+
+    pub fn delaunay(&self, points: &[[f64; 2]]) -> Result<Mesh, MoonlightError> {
+        let coordinates = flatten_points(points);
+        create_handle(|output, obstruction| unsafe {
+            ml_delaunay_f64(coordinates.as_ptr(), points.len(), output, obstruction)
+        })
+        .map(Mesh::from_handle)
+    }
+
+    pub fn region(&self, components: &[PolygonComponent]) -> Result<Region, MoonlightError> {
+        let loop_point_counts = components
+            .iter()
+            .flat_map(|component| std::iter::once(&component.outer).chain(component.holes.iter()))
+            .map(Vec::len)
+            .collect::<Vec<_>>();
+        let component_loop_counts = components
+            .iter()
+            .map(|component| component.holes.len() + 1)
+            .collect::<Vec<_>>();
+        let coordinates = components
+            .iter()
+            .flat_map(|component| std::iter::once(&component.outer).chain(component.holes.iter()))
+            .flat_map(|loop_points| loop_points.iter())
+            .flat_map(|[x, y]| [*x, *y])
+            .collect::<Vec<_>>();
+        create_handle(|output, obstruction| unsafe {
+            ml_region_create_f64(
+                coordinates.as_ptr(),
+                coordinates.len() / 2,
+                loop_point_counts.as_ptr(),
+                loop_point_counts.len(),
+                component_loop_counts.as_ptr(),
+                components.len(),
+                output,
+                obstruction,
+            )
+        })
+        .map(Region::from_handle)
+    }
+
+    pub fn structuring_element(
+        &self,
+        points: &[[f64; 2]],
+    ) -> Result<StructuringElement, MoonlightError> {
+        let coordinates = flatten_points(points);
+        create_handle(|output, obstruction| unsafe {
+            ml_structuring_element_create_f64(
+                coordinates.as_ptr(),
+                points.len(),
+                output,
+                obstruction,
+            )
+        })
+        .map(StructuringElement::from_handle)
+    }
+}
+
+pub struct Mesh {
+    handle: NonNull<NativeMesh>,
+    _thread_affinity: PhantomData<Rc<()>>,
+}
+
+impl Mesh {
+    fn from_handle(handle: NonNull<NativeMesh>) -> Self {
+        Self {
+            handle,
+            _thread_affinity: PhantomData,
+        }
+    }
+
+    pub fn vertex_count(&self) -> Result<usize, MoonlightError> {
+        self.count(|count, obstruction| unsafe {
+            ml_mesh_vertex_count(self.handle.as_ptr(), count, obstruction)
+        })
+    }
+
+    pub fn triangle_count(&self) -> Result<usize, MoonlightError> {
+        self.count(|count, obstruction| unsafe {
+            ml_mesh_triangle_count(self.handle.as_ptr(), count, obstruction)
+        })
+    }
+
+    pub fn vertices(&self) -> Result<Vec<[f64; 2]>, MoonlightError> {
+        let count = self.vertex_count()?;
+        let mut coordinates = vec![0.0; count * 2];
+        let mut written = 0;
+        let mut obstruction = NativeObstruction::default();
+        let status = unsafe {
+            ml_mesh_copy_vertices_f64(
+                self.handle.as_ptr(),
+                coordinates.as_mut_ptr(),
+                count,
+                &mut written,
+                &mut obstruction,
+            )
+        };
+        status_result(status, obstruction)?;
+        coordinates.truncate(written * 2);
+        coordinate_pairs(&coordinates)
+    }
+
+    pub fn triangles(&self) -> Result<Vec<[u32; 3]>, MoonlightError> {
+        let count = self.triangle_count()?;
+        let mut triangles = vec![0; count * 3];
+        let mut written = 0;
+        let mut obstruction = NativeObstruction::default();
+        let status = unsafe {
+            ml_mesh_copy_triangles_u32(
+                self.handle.as_ptr(),
+                triangles.as_mut_ptr(),
+                count,
+                &mut written,
+                &mut obstruction,
+            )
+        };
+        status_result(status, obstruction)?;
+        triangles.truncate(written * 3);
+        triangles
+            .chunks_exact(3)
+            .map(|triangle| {
+                Ok([
+                    *triangle.first().ok_or_else(projection_shape_error)?,
+                    *triangle.get(1).ok_or_else(projection_shape_error)?,
+                    *triangle.get(2).ok_or_else(projection_shape_error)?,
+                ])
+            })
+            .collect()
+    }
+
+    pub fn insert_many(&self, points: &[[f64; 2]]) -> Result<Self, MoonlightError> {
+        let coordinates = flatten_points(points);
+        create_handle(|output, obstruction| unsafe {
+            ml_mesh_insert_many_f64(
+                self.handle.as_ptr(),
+                coordinates.as_ptr(),
+                points.len(),
+                output,
+                obstruction,
+            )
+        })
+        .map(Self::from_handle)
+    }
+
+    pub fn site_union(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_mesh_site_union(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn site_intersection(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_mesh_site_intersection(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn site_difference(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_mesh_site_difference(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn site_symmetric_difference(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_mesh_site_symmetric_difference(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    fn binary(
+        &self,
+        operation: impl FnOnce(*mut *mut NativeMesh, *mut NativeObstruction) -> u32,
+    ) -> Result<Self, MoonlightError> {
+        create_handle(operation).map(Self::from_handle)
+    }
+
+    fn count(
+        &self,
+        operation: impl FnOnce(*mut usize, *mut NativeObstruction) -> u32,
+    ) -> Result<usize, MoonlightError> {
+        let mut count = 0;
+        let mut obstruction = NativeObstruction::default();
+        let status = operation(&mut count, &mut obstruction);
+        status_result(status, obstruction)?;
+        Ok(count)
+    }
+}
+
+impl Drop for Mesh {
+    fn drop(&mut self) {
+        unsafe { ml_mesh_free(self.handle.as_ptr()) }
+    }
+}
+
+pub struct Region {
+    handle: NonNull<NativeRegion>,
+    _thread_affinity: PhantomData<Rc<()>>,
+}
+
+impl Region {
+    fn from_handle(handle: NonNull<NativeRegion>) -> Self {
+        Self {
+            handle,
+            _thread_affinity: PhantomData,
+        }
+    }
+
+    pub fn components(&self) -> Result<Vec<PolygonComponent>, MoonlightError> {
+        let (component_count, loop_count, point_count) = self.counts()?;
+        let mut coordinates = vec![0.0; point_count * 2];
+        let mut loop_point_offsets = vec![0; loop_count + 1];
+        let mut component_loop_offsets = vec![0; component_count + 1];
+        let mut obstruction = NativeObstruction::default();
+        let status = unsafe {
+            ml_region_copy_f64(
+                self.handle.as_ptr(),
+                coordinates.as_mut_ptr(),
+                point_count,
+                loop_point_offsets.as_mut_ptr(),
+                loop_point_offsets.len(),
+                component_loop_offsets.as_mut_ptr(),
+                component_loop_offsets.len(),
+                &mut obstruction,
+            )
+        };
+        status_result(status, obstruction)?;
+        let points = coordinate_pairs(&coordinates)?;
+        component_loop_offsets
+            .windows(2)
+            .map(|window| {
+                let (start, end) = offset_window(window)?;
+                if start >= end {
+                    return Err(projection_shape_error());
+                }
+                let outer = projected_loop(&points, &loop_point_offsets, start)?;
+                let holes = (start + 1..end)
+                    .map(|index| projected_loop(&points, &loop_point_offsets, index))
+                    .collect::<Result<Vec<_>, _>>()?;
+                Ok(PolygonComponent { outer, holes })
+            })
+            .collect()
+    }
+
+    pub fn valuations(&self) -> Result<RegionValuations, MoonlightError> {
+        self.measure_with_capacity(128)
+    }
+
+    pub fn locate(&self, [x, y]: [f64; 2]) -> Result<RegionLocation, MoonlightError> {
+        let mut location = 0;
+        let mut obstruction = NativeObstruction::default();
+        let status = unsafe {
+            ml_region_locate_point_f64(self.handle.as_ptr(), x, y, &mut location, &mut obstruction)
+        };
+        status_result(status, obstruction)?;
+        region_location(location)
+    }
+
+    pub fn union(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_region_union(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn intersection(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_region_intersection(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn difference(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_region_difference(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn symmetric_difference(&self, other: &Self) -> Result<Self, MoonlightError> {
+        self.binary(|output, obstruction| unsafe {
+            ml_region_symmetric_difference(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn minkowski_sum(&self, other: &Self) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
+        produce_morphology(|output, receipt, obstruction| unsafe {
+            ml_region_minkowski_sum(
+                self.handle.as_ptr(),
+                other.handle.as_ptr(),
+                output,
+                receipt,
+                obstruction,
+            )
+        })
+    }
+
+    pub fn offset(
+        &self,
+        element: &StructuringElement,
+    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
+        self.with_element(
+            element,
+            |element, region, output, receipt, obstruction| unsafe {
+                ml_region_offset(element, region, output, receipt, obstruction)
+            },
+        )
+    }
+
+    pub fn inset(
+        &self,
+        element: &StructuringElement,
+    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
+        self.with_element(
+            element,
+            |element, region, output, receipt, obstruction| unsafe {
+                ml_region_inset(element, region, output, receipt, obstruction)
+            },
+        )
+    }
+
+    pub fn open(
+        &self,
+        element: &StructuringElement,
+    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
+        self.with_element(
+            element,
+            |element, region, output, receipt, obstruction| unsafe {
+                ml_region_open(element, region, output, receipt, obstruction)
+            },
+        )
+    }
+
+    pub fn close(
+        &self,
+        element: &StructuringElement,
+    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
+        self.with_element(
+            element,
+            |element, region, output, receipt, obstruction| unsafe {
+                ml_region_close(element, region, output, receipt, obstruction)
+            },
+        )
+    }
+
+    fn binary(
+        &self,
+        operation: impl FnOnce(*mut *mut NativeRegion, *mut NativeObstruction) -> u32,
+    ) -> Result<Self, MoonlightError> {
+        create_handle(operation).map(Self::from_handle)
+    }
+
+    fn with_element(
+        &self,
+        element: &StructuringElement,
+        operation: impl FnOnce(
+            *const NativeStructuringElement,
+            *const NativeRegion,
+            *mut *mut NativeRegion,
+            *mut NativeMinkowskiReceipt,
+            *mut NativeObstruction,
+        ) -> u32,
+    ) -> Result<(Self, MinkowskiReceipt), MoonlightError> {
+        produce_morphology(|output, receipt, obstruction| {
+            operation(
+                element.handle.as_ptr(),
+                self.handle.as_ptr(),
+                output,
+                receipt,
+                obstruction,
+            )
+        })
+    }
+
+    fn counts(&self) -> Result<(usize, usize, usize), MoonlightError> {
+        let mut component_count = 0;
+        let mut loop_count = 0;
+        let mut point_count = 0;
+        let mut obstruction = NativeObstruction::default();
+        let status = unsafe {
+            ml_region_counts(
+                self.handle.as_ptr(),
+                &mut component_count,
+                &mut loop_count,
+                &mut point_count,
+                &mut obstruction,
+            )
+        };
+        status_result(status, obstruction)?;
+        Ok((component_count, loop_count, point_count))
+    }
+
+    fn measure_with_capacity(&self, capacity: usize) -> Result<RegionValuations, MoonlightError> {
+        let mut euler_characteristic = 0;
+        let mut area_ratio = vec![0; capacity];
+        let mut area_bytes_written = 0;
+        let mut perimeter_lower = 0.0;
+        let mut perimeter_upper = 0.0;
+        let mut obstruction = NativeObstruction::default();
+        let status = unsafe {
+            ml_region_measure(
+                self.handle.as_ptr(),
+                &mut euler_characteristic,
+                area_ratio.as_mut_ptr(),
+                capacity,
+                &mut area_bytes_written,
+                &mut perimeter_lower,
+                &mut perimeter_upper,
+                &mut obstruction,
+            )
+        };
+        if status == ML_STATUS_BUFFER_TOO_SMALL
+            && obstruction.code == ML_OBSTRUCTION_BUFFER_TOO_SMALL
+        {
+            return self.measure_with_capacity(area_bytes_written + 1);
+        }
+        status_result(status, obstruction)?;
+        let area_bytes = area_ratio
+            .get(..area_bytes_written)
+            .ok_or_else(projection_shape_error)?
+            .iter()
+            .map(|byte| *byte as u8)
+            .collect::<Vec<_>>();
+        let area_text = String::from_utf8(area_bytes).map_err(|failure| {
+            invalid_native_result(format!(
+                "Moonlight returned a non-UTF-8 exact-area ratio: {failure}"
+            ))
+        })?;
+        let (numerator, denominator) = area_text.split_once('/').ok_or_else(|| {
+            invalid_native_result("Moonlight returned a malformed exact-area ratio")
+        })?;
+        Ok(RegionValuations {
+            euler_characteristic,
+            area: ExactRational {
+                numerator: numerator.to_owned(),
+                denominator: denominator.to_owned(),
+            },
+            perimeter_bounds: [perimeter_lower, perimeter_upper],
+        })
+    }
+}
+
+impl Drop for Region {
+    fn drop(&mut self) {
+        unsafe { ml_region_free(self.handle.as_ptr()) }
+    }
+}
+
+pub struct StructuringElement {
+    handle: NonNull<NativeStructuringElement>,
+    _thread_affinity: PhantomData<Rc<()>>,
+}
+
+impl StructuringElement {
+    fn from_handle(handle: NonNull<NativeStructuringElement>) -> Self {
+        Self {
+            handle,
+            _thread_affinity: PhantomData,
+        }
+    }
+}
+
+impl Drop for StructuringElement {
+    fn drop(&mut self) {
+        unsafe { ml_structuring_element_free(self.handle.as_ptr()) }
+    }
+}
+
+fn flatten_points(points: &[[f64; 2]]) -> Vec<f64> {
+    points.iter().flat_map(|[x, y]| [*x, *y]).collect()
+}
+
+fn coordinate_pairs(coordinates: &[f64]) -> Result<Vec<[f64; 2]>, MoonlightError> {
+    coordinates
+        .chunks_exact(2)
+        .map(|point| {
+            Ok([
+                *point.first().ok_or_else(projection_shape_error)?,
+                *point.get(1).ok_or_else(projection_shape_error)?,
+            ])
+        })
+        .collect()
+}
+
+fn offset_window(window: &[usize]) -> Result<(usize, usize), MoonlightError> {
+    Ok((
+        *window.first().ok_or_else(projection_shape_error)?,
+        *window.get(1).ok_or_else(projection_shape_error)?,
+    ))
+}
+
+fn projected_loop(
+    points: &[[f64; 2]],
+    offsets: &[usize],
+    index: usize,
+) -> Result<Vec<[f64; 2]>, MoonlightError> {
+    let start = *offsets.get(index).ok_or_else(projection_shape_error)?;
+    let end = *offsets.get(index + 1).ok_or_else(projection_shape_error)?;
+    points
+        .get(start..end)
+        .map(<[_]>::to_vec)
+        .ok_or_else(projection_shape_error)
+}
+
+fn create_handle<Native>(
+    operation: impl FnOnce(*mut *mut Native, *mut NativeObstruction) -> u32,
+) -> Result<NonNull<Native>, MoonlightError> {
+    let mut output = std::ptr::null_mut();
+    let mut obstruction = NativeObstruction::default();
+    let status = operation(&mut output, &mut obstruction);
+    status_result(status, obstruction)?;
+    NonNull::new(output).ok_or_else(success_without_handle_error)
+}
+
+fn produce_morphology(
+    operation: impl FnOnce(
+        *mut *mut NativeRegion,
+        *mut NativeMinkowskiReceipt,
+        *mut NativeObstruction,
+    ) -> u32,
+) -> Result<(Region, MinkowskiReceipt), MoonlightError> {
+    let mut output = std::ptr::null_mut();
+    let mut receipt = NativeMinkowskiReceipt::default();
+    let mut obstruction = NativeObstruction::default();
+    let status = operation(&mut output, &mut receipt, &mut obstruction);
+    status_result(status, obstruction)?;
+    let handle = NonNull::new(output).ok_or_else(success_without_handle_error)?;
+    Ok((Region::from_handle(handle), minkowski_receipt(receipt)?))
+}
+
+fn minkowski_receipt(receipt: NativeMinkowskiReceipt) -> Result<MinkowskiReceipt, MoonlightError> {
+    Ok(MinkowskiReceipt {
+        operation: minkowski_operation(receipt.operation)?,
+        input_components: receipt.input_components,
+        convex_pieces: receipt.convex_pieces,
+        generated_pieces: receipt.generated_pieces,
+        generated_convolution_edges: receipt.generated_convolution_edges,
+        overlay_passes: receipt.overlay_passes,
+        exact_crossings: receipt.exact_crossings,
+        output_cells: receipt.output_cells,
+        exact_coordinate_bit_growth: receipt.exact_coordinate_bit_growth,
+    })
+}
+
+fn minkowski_operation(code: u32) -> Result<MinkowskiOperation, MoonlightError> {
+    match code {
+        ML_MINKOWSKI_ADDITION => Ok(MinkowskiOperation::Addition),
+        ML_MINKOWSKI_EROSION => Ok(MinkowskiOperation::Erosion),
+        ML_MINKOWSKI_OPENING => Ok(MinkowskiOperation::Opening),
+        ML_MINKOWSKI_CLOSING => Ok(MinkowskiOperation::Closing),
+        _ => Err(MoonlightError::UnknownWireValue {
+            vocabulary: WireVocabulary::MinkowskiOperation,
+            value: code,
+        }),
+    }
+}
+
+fn region_location(code: u32) -> Result<RegionLocation, MoonlightError> {
+    match code {
+        ML_REGION_EXTERIOR => Ok(RegionLocation::Exterior),
+        ML_REGION_BOUNDARY => Ok(RegionLocation::Boundary),
+        ML_REGION_INTERIOR => Ok(RegionLocation::Interior),
+        _ => Err(MoonlightError::UnknownWireValue {
+            vocabulary: WireVocabulary::RegionLocation,
+            value: code,
+        }),
+    }
+}
diff --git a/ffi/bindings/rust/src/error.rs b/ffi/bindings/rust/src/error.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/src/error.rs
@@ -0,0 +1,150 @@
+use std::error::Error;
+use std::fmt::{Display, Formatter};
+
+use crate::raw_generated::{ML_STATUS_OK, NativeObstruction};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum MoonlightError {
+    RuntimeInitializationRefused {
+        status: u32,
+    },
+    AbiVersionMismatch {
+        expected: u32,
+        observed: u32,
+    },
+    AbiObstruction(AbiObstruction),
+    InvalidNativeResult {
+        message: String,
+    },
+    UnknownWireValue {
+        vocabulary: WireVocabulary,
+        value: u32,
+    },
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct AbiObstruction {
+    pub status: u32,
+    pub code: u32,
+    pub coordinate_error: u32,
+    pub input_index: u64,
+    pub first_index: u64,
+    pub second_index: u64,
+    pub first_value: f64,
+    pub second_value: f64,
+    pub point: [f64; 2],
+    pub message: String,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum WireVocabulary {
+    RegionLocation,
+    MinkowskiOperation,
+}
+
+impl Display for MoonlightError {
+    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::RuntimeInitializationRefused { status } => {
+                write!(
+                    formatter,
+                    "Moonlight runtime initialization refused with status {status}"
+                )
+            }
+            Self::AbiVersionMismatch { expected, observed } => write!(
+                formatter,
+                "Moonlight ABI {observed} is incompatible with expected ABI {expected}"
+            ),
+            Self::AbiObstruction(obstruction) => write!(formatter, "{obstruction}"),
+            Self::InvalidNativeResult { message } => write!(formatter, "{message}"),
+            Self::UnknownWireValue { vocabulary, value } => {
+                write!(
+                    formatter,
+                    "Moonlight returned unknown {vocabulary} value {value}"
+                )
+            }
+        }
+    }
+}
+
+impl Display for AbiObstruction {
+    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
+        if self.message.is_empty() {
+            write!(
+                formatter,
+                "Moonlight ABI failure {}:{}",
+                self.status, self.code
+            )
+        } else {
+            write!(formatter, "{}", self.message)
+        }
+    }
+}
+
+impl Display for WireVocabulary {
+    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::RegionLocation => write!(formatter, "region-location"),
+            Self::MinkowskiOperation => write!(formatter, "minkowski-operation"),
+        }
+    }
+}
+
+impl Error for MoonlightError {}
+
+pub(crate) fn status_result(
+    status: u32,
+    obstruction: NativeObstruction,
+) -> Result<(), MoonlightError> {
+    if status == ML_STATUS_OK {
+        Ok(())
+    } else {
+        Err(MoonlightError::AbiObstruction(AbiObstruction::from_native(
+            status,
+            obstruction,
+        )))
+    }
+}
+
+impl AbiObstruction {
+    fn from_native(status: u32, obstruction: NativeObstruction) -> Self {
+        let message_end = obstruction
+            .message
+            .iter()
+            .position(|character| *character == 0)
+            .unwrap_or(obstruction.message.len());
+        let message_bytes = obstruction
+            .message
+            .get(..message_end)
+            .unwrap_or_default()
+            .iter()
+            .map(|character| *character as u8)
+            .collect::<Vec<_>>();
+        Self {
+            status,
+            code: obstruction.code,
+            coordinate_error: obstruction.coordinate_error,
+            input_index: obstruction.input_index,
+            first_index: obstruction.first_index,
+            second_index: obstruction.second_index,
+            first_value: obstruction.first_value,
+            second_value: obstruction.second_value,
+            point: [obstruction.point_x, obstruction.point_y],
+            message: String::from_utf8_lossy(&message_bytes).into_owned(),
+        }
+    }
+}
+
+pub(crate) fn projection_shape_error() -> MoonlightError {
+    invalid_native_result("Moonlight returned a malformed bulk projection")
+}
+
+pub(crate) fn success_without_handle_error() -> MoonlightError {
+    invalid_native_result("Moonlight returned success without a handle")
+}
+
+pub(crate) fn invalid_native_result(message: impl Into<String>) -> MoonlightError {
+    MoonlightError::InvalidNativeResult {
+        message: message.into(),
+    }
+}
diff --git a/ffi/bindings/rust/src/lib.rs b/ffi/bindings/rust/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/src/lib.rs
@@ -0,0 +1,11 @@
+mod binding;
+mod error;
+mod raw_generated;
+mod values;
+
+pub use binding::{Mesh, Moonlight, Region, StructuringElement};
+pub use error::{AbiObstruction, MoonlightError, WireVocabulary};
+pub use values::{
+    ExactRational, MinkowskiOperation, MinkowskiReceipt, PolygonComponent, RegionLocation,
+    RegionValuations,
+};
diff --git a/ffi/bindings/rust/src/raw_generated.rs b/ffi/bindings/rust/src/raw_generated.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/src/raw_generated.rs
@@ -0,0 +1,361 @@
+// Generated from Moonlight.Triangulation.Foreign.Contract.
+// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+// This private module is the complete wire projection; wrappers consume a subset.
+#![allow(dead_code)]
+
+use std::ffi::{c_char, c_double, c_uint};
+
+pub(crate) const ABI_VERSION: u32 = 2;
+pub(crate) const ML_STATUS_OK: u32 = 0;
+pub(crate) const ML_STATUS_NULL_POINTER: u32 = 1;
+pub(crate) const ML_STATUS_COUNT_OVERFLOW: u32 = 2;
+pub(crate) const ML_STATUS_BUFFER_TOO_SMALL: u32 = 3;
+pub(crate) const ML_STATUS_GEOMETRY_OBSTRUCTION: u32 = 4;
+pub(crate) const ML_STATUS_RUNTIME_FAILURE: u32 = 5;
+pub(crate) const ML_OBSTRUCTION_NONE: u32 = 0;
+pub(crate) const ML_OBSTRUCTION_INVALID_COORDINATE: u32 = 1;
+pub(crate) const ML_OBSTRUCTION_POINT_LOCATION_FAILED: u32 = 2;
+pub(crate) const ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED: u32 = 3;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID: u32 = 4;
+pub(crate) const ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX: u32 = 5;
+pub(crate) const ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING: u32 = 6;
+pub(crate) const ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING: u32 = 7;
+pub(crate) const ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING: u32 = 8;
+pub(crate) const ML_OBSTRUCTION_HULL_START_NOT_VISIBLE: u32 = 9;
+pub(crate) const ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE: u32 = 10;
+pub(crate) const ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE: u32 = 11;
+pub(crate) const ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED: u32 = 12;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE: u32 = 13;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE: u32 = 14;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE: u32 = 15;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE: u32 = 16;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION: u32 = 17;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH: u32 = 18;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH: u32 = 19;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT: u32 = 20;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH: u32 = 21;
+pub(crate) const ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE: u32 = 22;
+pub(crate) const ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY: u32 = 23;
+pub(crate) const ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE: u32 = 24;
+pub(crate) const ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH: u32 = 25;
+pub(crate) const ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH: u32 = 26;
+pub(crate) const ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED: u32 = 27;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE: u32 = 28;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE: u32 = 29;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE: u32 = 30;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE: u32 = 31;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE: u32 = 32;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE: u32 = 33;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE: u32 = 34;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE: u32 = 35;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE: u32 = 36;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE: u32 = 37;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM: u32 = 38;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE: u32 = 39;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE: u32 = 40;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING: u32 = 41;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS: u32 = 42;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED: u32 = 43;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION: u32 = 44;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION: u32 = 45;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION: u32 = 46;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE: u32 = 47;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE: u32 = 48;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED: u32 = 49;
+pub(crate) const ML_OBSTRUCTION_CAPACITY_EXCEEDED: u32 = 50;
+pub(crate) const ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED: u32 = 51;
+pub(crate) const ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED: u32 = 52;
+pub(crate) const ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE: u32 = 53;
+pub(crate) const ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH: u32 = 54;
+pub(crate) const ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE: u32 = 55;
+pub(crate) const ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE: u32 = 56;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET: u32 = 57;
+pub(crate) const ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP: u32 = 58;
+pub(crate) const ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED: u32 = 59;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE: u32 = 60;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE: u32 = 61;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE: u32 = 62;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED: u32 = 63;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED: u32 = 64;
+pub(crate) const ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE: u32 = 65;
+pub(crate) const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED: u32 = 66;
+pub(crate) const ML_OBSTRUCTION_NULL_POINTER: u32 = 100;
+pub(crate) const ML_OBSTRUCTION_COUNT_OVERFLOW: u32 = 101;
+pub(crate) const ML_OBSTRUCTION_BUFFER_TOO_SMALL: u32 = 102;
+pub(crate) const ML_OBSTRUCTION_RUNTIME_FAILURE: u32 = 103;
+pub(crate) const ML_OBSTRUCTION_REGION_LAYOUT_INVALID: u32 = 200;
+pub(crate) const ML_OBSTRUCTION_REGION_VALIDATION_FAILED: u32 = 201;
+pub(crate) const ML_OBSTRUCTION_OVERLAY_FAILED: u32 = 202;
+pub(crate) const ML_OBSTRUCTION_REGION_PUBLICATION_FAILED: u32 = 203;
+pub(crate) const ML_OBSTRUCTION_VALUATION_FAILED: u32 = 204;
+pub(crate) const ML_OBSTRUCTION_MINKOWSKI_FAILED: u32 = 205;
+pub(crate) const ML_OBSTRUCTION_REGION_PROJECTION_FAILED: u32 = 206;
+pub(crate) const ML_COORDINATE_ERROR_NONE: u32 = 0;
+pub(crate) const ML_COORDINATE_ERROR_NAN: u32 = 1;
+pub(crate) const ML_COORDINATE_ERROR_INFINITE: u32 = 2;
+pub(crate) const ML_COORDINATE_ERROR_TOO_SMALL: u32 = 3;
+pub(crate) const ML_COORDINATE_ERROR_TOO_LARGE: u32 = 4;
+pub(crate) const ML_REGION_EXTERIOR: u32 = 0;
+pub(crate) const ML_REGION_BOUNDARY: u32 = 1;
+pub(crate) const ML_REGION_INTERIOR: u32 = 2;
+pub(crate) const ML_MINKOWSKI_ADDITION: u32 = 0;
+pub(crate) const ML_MINKOWSKI_EROSION: u32 = 1;
+pub(crate) const ML_MINKOWSKI_OPENING: u32 = 2;
+pub(crate) const ML_MINKOWSKI_CLOSING: u32 = 3;
+
+#[repr(C)]
+pub(crate) struct NativeMesh {
+    _private: [u8; 0],
+}
+
+#[repr(C)]
+pub(crate) struct NativeRegion {
+    _private: [u8; 0],
+}
+
+#[repr(C)]
+pub(crate) struct NativeStructuringElement {
+    _private: [u8; 0],
+}
+
+#[repr(C)]
+pub(crate) struct NativeObstruction {
+    pub(crate) code: u32,
+    pub(crate) coordinate_error: u32,
+    pub(crate) input_index: u64,
+    pub(crate) first_index: u64,
+    pub(crate) second_index: u64,
+    pub(crate) first_value: f64,
+    pub(crate) second_value: f64,
+    pub(crate) point_x: f64,
+    pub(crate) point_y: f64,
+    pub(crate) message: [c_char; 256],
+}
+
+impl Default for NativeObstruction {
+    fn default() -> Self {
+        Self {
+            code: 0,
+            coordinate_error: 0,
+            input_index: 0,
+            first_index: 0,
+            second_index: 0,
+            first_value: 0.0,
+            second_value: 0.0,
+            point_x: 0.0,
+            point_y: 0.0,
+            message: [0; 256],
+        }
+    }
+}
+
+#[repr(C)]
+pub(crate) struct NativeMinkowskiReceipt {
+    pub(crate) operation: u32,
+    pub(crate) reserved: u32,
+    pub(crate) input_components: u64,
+    pub(crate) convex_pieces: u64,
+    pub(crate) generated_pieces: u64,
+    pub(crate) generated_convolution_edges: u64,
+    pub(crate) overlay_passes: u64,
+    pub(crate) exact_crossings: u64,
+    pub(crate) output_cells: u64,
+    pub(crate) exact_coordinate_bit_growth: u64,
+}
+
+impl Default for NativeMinkowskiReceipt {
+    fn default() -> Self {
+        Self {
+            operation: 0,
+            reserved: 0,
+            input_components: 0,
+            convex_pieces: 0,
+            generated_pieces: 0,
+            generated_convolution_edges: 0,
+            overlay_passes: 0,
+            exact_crossings: 0,
+            output_cells: 0,
+            exact_coordinate_bit_growth: 0,
+        }
+    }
+}
+
+#[link(name = "moonlight-triangulation-c")]
+unsafe extern "C" {
+    pub(crate) fn ml_abi_version() -> c_uint;
+    pub(crate) fn ml_runtime_initialize() -> c_uint;
+    pub(crate) fn ml_delaunay_f64(
+        coordinates: *const c_double,
+        point_count: usize,
+        result: *mut *mut NativeMesh,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_insert_many_f64(
+        mesh: *const NativeMesh,
+        coordinates: *const c_double,
+        point_count: usize,
+        result: *mut *mut NativeMesh,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_site_union(
+        left: *const NativeMesh,
+        right: *const NativeMesh,
+        result: *mut *mut NativeMesh,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_site_intersection(
+        left: *const NativeMesh,
+        right: *const NativeMesh,
+        result: *mut *mut NativeMesh,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_site_difference(
+        left: *const NativeMesh,
+        right: *const NativeMesh,
+        result: *mut *mut NativeMesh,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_site_symmetric_difference(
+        left: *const NativeMesh,
+        right: *const NativeMesh,
+        result: *mut *mut NativeMesh,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_vertex_count(
+        mesh: *const NativeMesh,
+        count: *mut usize,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_triangle_count(
+        mesh: *const NativeMesh,
+        count: *mut usize,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_copy_vertices_f64(
+        mesh: *const NativeMesh,
+        coordinates: *mut c_double,
+        point_capacity: usize,
+        points_written: *mut usize,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_copy_triangles_u32(
+        mesh: *const NativeMesh,
+        triangles: *mut u32,
+        triangle_capacity: usize,
+        triangles_written: *mut usize,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_mesh_free(mesh: *mut NativeMesh);
+    pub(crate) fn ml_region_create_f64(
+        coordinates: *const c_double,
+        point_count: usize,
+        loop_point_counts: *const usize,
+        loop_count: usize,
+        component_loop_counts: *const usize,
+        component_count: usize,
+        result: *mut *mut NativeRegion,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_counts(
+        region: *const NativeRegion,
+        component_count: *mut usize,
+        loop_count: *mut usize,
+        point_count: *mut usize,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_copy_f64(
+        region: *const NativeRegion,
+        coordinates: *mut c_double,
+        point_capacity: usize,
+        loop_point_offsets: *mut usize,
+        loop_offset_capacity: usize,
+        component_loop_offsets: *mut usize,
+        component_offset_capacity: usize,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_union(
+        left: *const NativeRegion,
+        right: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_intersection(
+        left: *const NativeRegion,
+        right: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_difference(
+        left: *const NativeRegion,
+        right: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_symmetric_difference(
+        left: *const NativeRegion,
+        right: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_locate_point_f64(
+        region: *const NativeRegion,
+        x: c_double,
+        y: c_double,
+        location: *mut u32,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_measure(
+        region: *const NativeRegion,
+        euler_characteristic: *mut i64,
+        area_ratio_utf8: *mut c_char,
+        area_capacity: usize,
+        area_bytes_written: *mut usize,
+        perimeter_lower: *mut c_double,
+        perimeter_upper: *mut c_double,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_free(region: *mut NativeRegion);
+    pub(crate) fn ml_structuring_element_create_f64(
+        coordinates: *const c_double,
+        point_count: usize,
+        result: *mut *mut NativeStructuringElement,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_structuring_element_free(element: *mut NativeStructuringElement);
+    pub(crate) fn ml_region_minkowski_sum(
+        left: *const NativeRegion,
+        right: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        receipt: *mut NativeMinkowskiReceipt,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_offset(
+        element: *const NativeStructuringElement,
+        region: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        receipt: *mut NativeMinkowskiReceipt,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_inset(
+        element: *const NativeStructuringElement,
+        region: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        receipt: *mut NativeMinkowskiReceipt,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_open(
+        element: *const NativeStructuringElement,
+        region: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        receipt: *mut NativeMinkowskiReceipt,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+    pub(crate) fn ml_region_close(
+        element: *const NativeStructuringElement,
+        region: *const NativeRegion,
+        result: *mut *mut NativeRegion,
+        receipt: *mut NativeMinkowskiReceipt,
+        obstruction: *mut NativeObstruction,
+    ) -> c_uint;
+}
diff --git a/ffi/bindings/rust/src/values.rs b/ffi/bindings/rust/src/values.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/src/values.rs
@@ -0,0 +1,46 @@
+#[derive(Debug, Clone, PartialEq)]
+pub struct PolygonComponent {
+    pub outer: Vec<[f64; 2]>,
+    pub holes: Vec<Vec<[f64; 2]>>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RegionLocation {
+    Exterior,
+    Boundary,
+    Interior,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ExactRational {
+    pub numerator: String,
+    pub denominator: String,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct RegionValuations {
+    pub euler_characteristic: i64,
+    pub area: ExactRational,
+    pub perimeter_bounds: [f64; 2],
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MinkowskiOperation {
+    Addition,
+    Erosion,
+    Opening,
+    Closing,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MinkowskiReceipt {
+    pub operation: MinkowskiOperation,
+    pub input_components: u64,
+    pub convex_pieces: u64,
+    pub generated_pieces: u64,
+    pub generated_convolution_edges: u64,
+    pub overlay_passes: u64,
+    pub exact_crossings: u64,
+    pub output_cells: u64,
+    pub exact_coordinate_bit_growth: u64,
+}
diff --git a/ffi/bindings/rust/tests/binding.rs b/ffi/bindings/rust/tests/binding.rs
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/rust/tests/binding.rs
@@ -0,0 +1,119 @@
+use moonlight_triangulation::{
+    ExactRational, MinkowskiOperation, Moonlight, MoonlightError, PolygonComponent, RegionLocation,
+};
+
+#[test]
+fn immutable_site_set_algebra_and_dense_projection() {
+    let engine = Moonlight::initialize().expect("runtime");
+    let left = engine
+        .delaunay(&[[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0]])
+        .expect("left mesh");
+    let right = engine
+        .delaunay(&[[2.0, 0.0], [4.0, 0.0], [2.0, 2.0], [4.0, 2.0]])
+        .expect("right mesh");
+    let union = left.site_union(&right).expect("site union");
+    let intersection = left.site_intersection(&right).expect("site intersection");
+    let difference = left.site_difference(&right).expect("site difference");
+    let symmetric = left
+        .site_symmetric_difference(&right)
+        .expect("site symmetric difference");
+    let extended = left
+        .insert_many(&[[1.0, 1.0], [3.0, 1.0]])
+        .expect("batch insertion");
+
+    assert_eq!(left.vertex_count().expect("left count"), 4);
+    assert_eq!(union.vertex_count().expect("union count"), 6);
+    assert_eq!(intersection.vertex_count().expect("intersection count"), 2);
+    assert_eq!(difference.vertex_count().expect("difference count"), 2);
+    assert_eq!(symmetric.vertex_count().expect("symmetric count"), 4);
+    assert_eq!(extended.vertex_count().expect("extended count"), 6);
+    assert_eq!(
+        left.vertices().expect("vertices").len(),
+        left.vertex_count().expect("vertex count")
+    );
+    assert_eq!(
+        left.triangles().expect("triangles").len(),
+        left.triangle_count().expect("triangle count")
+    );
+}
+
+#[test]
+fn exact_region_boolean_valuation_and_morphology() {
+    let engine = Moonlight::initialize().expect("runtime");
+    let left = engine
+        .region(&[PolygonComponent {
+            outer: vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]],
+            holes: vec![],
+        }])
+        .expect("left region");
+    let right = engine
+        .region(&[PolygonComponent {
+            outer: vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]],
+            holes: vec![],
+        }])
+        .expect("right region");
+    let intersection = left.intersection(&right).expect("intersection");
+    let symmetric = left
+        .symmetric_difference(&right)
+        .expect("symmetric difference");
+    let kernel = engine
+        .structuring_element(&[[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]])
+        .expect("structuring element");
+    let (offset, receipt) = left.offset(&kernel).expect("offset");
+
+    assert_eq!(left.components().expect("components").len(), 1);
+    assert_eq!(
+        intersection
+            .valuations()
+            .expect("intersection valuation")
+            .area,
+        ExactRational {
+            numerator: "2".to_owned(),
+            denominator: "1".to_owned(),
+        }
+    );
+    assert_eq!(
+        symmetric
+            .valuations()
+            .expect("symmetric valuation")
+            .euler_characteristic,
+        2
+    );
+    assert_eq!(
+        left.locate([1.0, 1.0]).expect("interior"),
+        RegionLocation::Interior
+    );
+    assert_eq!(
+        left.locate([0.0, 1.0]).expect("boundary"),
+        RegionLocation::Boundary
+    );
+    assert_eq!(
+        left.locate([3.0, 1.0]).expect("exterior"),
+        RegionLocation::Exterior
+    );
+    assert_eq!(
+        offset.valuations().expect("offset valuation").area,
+        ExactRational {
+            numerator: "9".to_owned(),
+            denominator: "1".to_owned(),
+        }
+    );
+    assert_eq!(receipt.operation, MinkowskiOperation::Addition);
+    assert!(receipt.generated_pieces >= 1);
+}
+
+#[test]
+fn invalid_coordinate_preserves_typed_witness() {
+    let engine = Moonlight::initialize().expect("runtime");
+    let failure = engine
+        .delaunay(&[[0.0, 0.0], [f64::NAN, 1.0], [1.0, 0.0]])
+        .err()
+        .expect("invalid coordinate refusal");
+    let MoonlightError::AbiObstruction(obstruction) = failure else {
+        panic!("expected an ABI obstruction");
+    };
+    assert_eq!(obstruction.status, 4);
+    assert_eq!(obstruction.code, 1);
+    assert_eq!(obstruction.coordinate_error, 1);
+    assert_eq!(obstruction.input_index, 1);
+}
diff --git a/ffi/bindings/typescript/README.md b/ffi/bindings/typescript/README.md
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/README.md
@@ -0,0 +1,44 @@
+# `@moonlight/triangulation`
+
+[Read the shared ABI contract.](../README.md#abi-version-2)
+
+Typed TypeScript access to Moonlight's immutable Delaunay meshes, exact planar
+regions, and polygonal morphology. The package owns JavaScript resource
+lifetime and error terms; the generated raw declarations descend from the same
+Haskell contract as the C header, Python declarations, and Rust declarations.
+
+This source package does not pretend to contain a native binary. Supply an
+explicit path to the ABI-version-2 shared library:
+
+```typescript
+import {
+  RegionLocation,
+  formatMoonlightFailure,
+  loadMoonlight,
+  type Result,
+} from "@moonlight/triangulation";
+
+const moonlight = loadMoonlight({
+  kind: "path",
+  path: "/absolute/path/to/libmoonlight-triangulation-c.so",
+});
+
+if (!moonlight.ok) {
+  throw new Error(formatMoonlightFailure(moonlight.error));
+}
+
+const region = moonlight.value.region([
+  { outer: [[0, 0], [2, 0], [2, 2], [0, 2]] },
+]);
+
+if (region.ok) {
+  const location: Result<RegionLocation> = region.value.locate([1, 1]);
+  region.value.dispose();
+}
+```
+
+Every operation that can be refused returns `Result<Value, MoonlightFailure>`.
+No raw Koffi handle, constructor capability, or numeric status code crosses the
+package entrypoint. Owned meshes, regions, and structuring elements implement
+both `dispose()` and `Symbol.dispose`; morphology therefore keeps the honest
+domain method `region.close(element)`.
diff --git a/ffi/bindings/typescript/package.json b/ffi/bindings/typescript/package.json
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "@moonlight/triangulation",
+  "version": "2.0.0",
+  "description": "TypeScript bindings for Moonlight exact regions and immutable Delaunay meshes.",
+  "type": "module",
+  "main": "./dist/src/index.js",
+  "types": "./dist/src/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/src/index.d.ts",
+      "import": "./dist/src/index.js"
+    }
+  },
+  "files": [
+    "dist/src",
+    "README.md"
+  ],
+  "scripts": {
+    "build": "tsc -p tsconfig.json",
+    "test": "node --test dist/test/binding.test.js"
+  },
+  "engines": {
+    "node": ">=20"
+  },
+  "dependencies": {
+    "koffi": "3.1.4"
+  },
+  "devDependencies": {
+    "@types/node": "26.2.0",
+    "typescript": "7.0.2"
+  },
+  "license": "MIT",
+  "packageManager": "pnpm@11.21.0"
+}
diff --git a/ffi/bindings/typescript/pnpm-workspace.yaml b/ffi/bindings/typescript/pnpm-workspace.yaml
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/pnpm-workspace.yaml
@@ -0,0 +1,5 @@
+packages:
+  - .
+
+allowBuilds:
+  koffi: true
diff --git a/ffi/bindings/typescript/src/failure.ts b/ffi/bindings/typescript/src/failure.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/failure.ts
@@ -0,0 +1,111 @@
+import type {
+  AbiStatus,
+  CoordinateError,
+  ObstructionCode,
+} from "./wire.generated.js";
+
+import type { Point } from "./values.js";
+
+export type Result<Value, Failure = MoonlightFailure> =
+  | { readonly ok: true; readonly value: Value }
+  | { readonly ok: false; readonly error: Failure };
+
+export interface Obstruction {
+  readonly status: AbiStatus;
+  readonly code: ObstructionCode;
+  readonly coordinateError: CoordinateError;
+  readonly inputIndex: bigint;
+  readonly firstIndex: bigint;
+  readonly secondIndex: bigint;
+  readonly firstValue: number;
+  readonly secondValue: number;
+  readonly point: Point;
+  readonly message: string;
+}
+
+export type ResourceKind = "mesh" | "region" | "structuring-element";
+
+export type MoonlightFailure =
+  | {
+      readonly kind: "native-library-unavailable";
+      readonly path: string;
+      readonly message: string;
+    }
+  | {
+      readonly kind: "runtime-initialization-refused";
+      readonly status: AbiStatus;
+    }
+  | {
+      readonly kind: "abi-version-mismatch";
+      readonly expected: number;
+      readonly observed: number;
+    }
+  | {
+      readonly kind: "abi-obstruction";
+      readonly obstruction: Obstruction;
+    }
+  | {
+      readonly kind: "disposed-resource";
+      readonly resource: ResourceKind;
+    }
+  | {
+      readonly kind: "foreign-runtime";
+      readonly resource: ResourceKind;
+    }
+  | {
+      readonly kind: "unsafe-integer";
+      readonly value: bigint;
+    }
+  | {
+      readonly kind: "unknown-wire-value";
+      readonly vocabulary: "status" | "obstruction" | "coordinate-error" | "region-location" | "minkowski-operation";
+      readonly value: number;
+    }
+  | {
+      readonly kind: "invalid-native-result";
+      readonly message: string;
+    }
+  | {
+      readonly kind: "native-call-failed";
+      readonly message: string;
+    };
+
+export function success<Value>(value: Value): Result<Value> {
+  return { ok: true, value };
+}
+
+export function failure<Value = never>(error: MoonlightFailure): Result<Value> {
+  return { ok: false, error };
+}
+
+export function collectResults<Value>(results: readonly Result<Value>[]): Result<readonly Value[]> {
+  const refused = results.find((result) => !result.ok);
+  return refused?.ok === false
+    ? failure(refused.error)
+    : success(results.flatMap((result) => (result.ok ? [result.value] : [])));
+}
+
+export function formatMoonlightFailure(error: MoonlightFailure): string {
+  switch (error.kind) {
+    case "native-library-unavailable":
+      return `cannot load Moonlight native library at ${error.path}: ${error.message}`;
+    case "runtime-initialization-refused":
+      return `Moonlight runtime initialization refused with ${error.status}`;
+    case "abi-version-mismatch":
+      return `Moonlight ABI ${error.observed} is incompatible with expected ABI ${error.expected}`;
+    case "abi-obstruction":
+      return error.obstruction.message || `${error.obstruction.status}: ${error.obstruction.code}`;
+    case "disposed-resource":
+      return `${error.resource} is disposed`;
+    case "foreign-runtime":
+      return `${error.resource} belongs to another Moonlight runtime`;
+    case "unsafe-integer":
+      return `Moonlight count ${error.value} exceeds JavaScript's safe integer range`;
+    case "unknown-wire-value":
+      return `Moonlight returned unknown ${error.vocabulary} value ${error.value}`;
+    case "invalid-native-result":
+      return error.message;
+    case "native-call-failed":
+      return error.message;
+  }
+}
diff --git a/ffi/bindings/typescript/src/index.ts b/ffi/bindings/typescript/src/index.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/index.ts
@@ -0,0 +1,28 @@
+export type {
+  MoonlightFailure,
+  Obstruction,
+  ResourceKind,
+  Result,
+} from "./failure.js";
+export { formatMoonlightFailure } from "./failure.js";
+export type { Mesh } from "./mesh.js";
+export type { Moonlight, NativeLibrarySource } from "./moonlight.js";
+export { loadMoonlight } from "./moonlight.js";
+export type { StructuringElement } from "./morphology.js";
+export type { MorphologyResult, Region } from "./region.js";
+export type { OwnedResource } from "./resource.js";
+export type {
+  ExactRational,
+  MinkowskiReceipt,
+  Point,
+  PolygonComponent,
+  RegionValuations,
+  Triangle,
+} from "./values.js";
+export {
+  AbiStatus,
+  CoordinateError,
+  MinkowskiOperation,
+  ObstructionCode,
+  RegionLocation,
+} from "./wire.generated.js";
diff --git a/ffi/bindings/typescript/src/internal/call.ts b/ffi/bindings/typescript/src/internal/call.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/internal/call.ts
@@ -0,0 +1,83 @@
+import { failure, success, type Result } from "../failure.js";
+import type { MinkowskiReceipt, Point } from "../values.js";
+import { decodeMinkowskiReceipt, decodeStatus, nativeCallFailure } from "./decode.js";
+import type {
+  HandleOutput,
+  NativeHandle,
+  NativeMinkowskiReceipt,
+  NativeObstruction,
+} from "./native.generated.js";
+
+type HandleOperation = (output: HandleOutput, obstruction: NativeObstruction) => number;
+type MorphologyOperation = (
+  output: HandleOutput,
+  receipt: NativeMinkowskiReceipt,
+  obstruction: NativeObstruction,
+) => number;
+
+export interface NativeMorphologyResult {
+  readonly handle: NativeHandle;
+  readonly receipt: MinkowskiReceipt;
+}
+
+export function produceHandle(operation: HandleOperation): Result<NativeHandle> {
+  return captureNativeCall(() => {
+    const output: HandleOutput = [null];
+    const obstruction: NativeObstruction = {};
+    const status = decodeStatus(operation(output, obstruction), obstruction);
+    if (!status.ok) {
+      return status;
+    }
+    return requireHandle(output, "handle");
+  });
+}
+
+export function produceMorphology(operation: MorphologyOperation): Result<NativeMorphologyResult> {
+  return captureNativeCall(() => {
+    const output: HandleOutput = [null];
+    const receipt: NativeMinkowskiReceipt = {};
+    const obstruction: NativeObstruction = {};
+    const status = decodeStatus(operation(output, receipt, obstruction), obstruction);
+    if (!status.ok) {
+      return status;
+    }
+    const handle = requireHandle(output, "region handle");
+    if (!handle.ok) {
+      return handle;
+    }
+    const decodedReceipt = decodeMinkowskiReceipt(receipt);
+    return decodedReceipt.ok
+      ? success({ handle: handle.value, receipt: decodedReceipt.value })
+      : decodedReceipt;
+  });
+}
+
+export function callStatus(operation: (obstruction: NativeObstruction) => number): Result<void> {
+  return captureNativeCall(() => {
+    const obstruction: NativeObstruction = {};
+    return decodeStatus(operation(obstruction), obstruction);
+  });
+}
+
+export function captureNativeCall<Value>(operation: () => Result<Value>): Result<Value> {
+  try {
+    return operation();
+  } catch (cause: unknown) {
+    return failure(nativeCallFailure(cause));
+  }
+}
+
+export function flattenPoints(points: readonly Point[]): Float64Array {
+  return Float64Array.from(points.flatMap(([x, y]) => [x, y]));
+}
+
+export function flattenLoops(loops: readonly (readonly Point[])[]): Float64Array {
+  return Float64Array.from(loops.flatMap((loop) => loop.flatMap(([x, y]) => [x, y])));
+}
+
+function requireHandle(output: HandleOutput, description: string): Result<NativeHandle> {
+  const handle = output[0];
+  return handle === null || handle === undefined
+    ? failure({ kind: "invalid-native-result", message: `Moonlight returned success without a ${description}` })
+    : success(handle);
+}
diff --git a/ffi/bindings/typescript/src/internal/decode.ts b/ffi/bindings/typescript/src/internal/decode.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/internal/decode.ts
@@ -0,0 +1,117 @@
+import { failure, success, type MoonlightFailure, type Obstruction, type Result } from "../failure.js";
+import type { MinkowskiReceipt, Point } from "../values.js";
+import {
+  decodeAbiStatus,
+  decodeCoordinateError,
+  decodeMinkowskiOperation,
+  decodeObstructionCode,
+  decodeRegionLocation,
+  type RegionLocation,
+} from "../wire.generated.js";
+import {
+  ML_COORDINATE_ERROR_NONE,
+  ML_OBSTRUCTION_NONE,
+  ML_STATUS_OK,
+  type NativeInteger,
+  type NativeMinkowskiReceipt,
+  type NativeObstruction,
+} from "./native.generated.js";
+
+export function decodeStatus(status: number, obstruction: NativeObstruction): Result<void> {
+  if (status === ML_STATUS_OK) {
+    return success(undefined);
+  }
+  const decodedStatus = decodeAbiStatus(status);
+  if (decodedStatus === undefined) {
+    return failure({ kind: "unknown-wire-value", vocabulary: "status", value: status });
+  }
+  const codeValue = obstruction.code ?? ML_OBSTRUCTION_NONE;
+  const code = decodeObstructionCode(codeValue);
+  if (code === undefined) {
+    return failure({ kind: "unknown-wire-value", vocabulary: "obstruction", value: codeValue });
+  }
+  const coordinateErrorValue = obstruction.coordinate_error ?? ML_COORDINATE_ERROR_NONE;
+  const coordinateError = decodeCoordinateError(coordinateErrorValue);
+  if (coordinateError === undefined) {
+    return failure({ kind: "unknown-wire-value", vocabulary: "coordinate-error", value: coordinateErrorValue });
+  }
+  const decoded: Obstruction = {
+    status: decodedStatus,
+    code,
+    coordinateError,
+    inputIndex: toBigInt(obstruction.input_index),
+    firstIndex: toBigInt(obstruction.first_index),
+    secondIndex: toBigInt(obstruction.second_index),
+    firstValue: obstruction.first_value ?? 0,
+    secondValue: obstruction.second_value ?? 0,
+    point: [obstruction.point_x ?? 0, obstruction.point_y ?? 0],
+    message: obstructionMessage(obstruction),
+  };
+  return failure({ kind: "abi-obstruction", obstruction: decoded });
+}
+
+export function decodeLocation(value: number | undefined): Result<RegionLocation> {
+  const wireValue = value ?? -1;
+  const location = decodeRegionLocation(wireValue);
+  return location === undefined
+    ? failure({ kind: "unknown-wire-value", vocabulary: "region-location", value: wireValue })
+    : success(location);
+}
+
+export function decodeMinkowskiReceipt(receipt: NativeMinkowskiReceipt): Result<MinkowskiReceipt> {
+  const operationValue = receipt.operation ?? -1;
+  const operation = decodeMinkowskiOperation(operationValue);
+  if (operation === undefined) {
+    return failure({ kind: "unknown-wire-value", vocabulary: "minkowski-operation", value: operationValue });
+  }
+  return success({
+    operation,
+    inputComponents: toBigInt(receipt.input_components),
+    convexPieces: toBigInt(receipt.convex_pieces),
+    generatedPieces: toBigInt(receipt.generated_pieces),
+    generatedConvolutionEdges: toBigInt(receipt.generated_convolution_edges),
+    overlayPasses: toBigInt(receipt.overlay_passes),
+    exactCrossings: toBigInt(receipt.exact_crossings),
+    outputCells: toBigInt(receipt.output_cells),
+    exactCoordinateBitGrowth: toBigInt(receipt.exact_coordinate_bit_growth),
+  });
+}
+
+export function toBigInt(value: NativeInteger | undefined): bigint {
+  return value === undefined ? 0n : BigInt(value);
+}
+
+export function toSafeNumber(value: NativeInteger | undefined): Result<number> {
+  const exact = toBigInt(value);
+  const projected = Number(exact);
+  return Number.isSafeInteger(projected)
+    ? success(projected)
+    : failure({ kind: "unsafe-integer", value: exact });
+}
+
+export function nativeCallFailure(cause: unknown): MoonlightFailure {
+  return {
+    kind: "native-call-failed",
+    message: cause instanceof Error ? cause.message : String(cause),
+  };
+}
+
+export function pointsFromCoordinates(coordinates: Float64Array, count: number): readonly Point[] {
+  return Array.from({ length: count }, (_unused, index): Point => [
+    coordinates[index * 2] ?? 0,
+    coordinates[index * 2 + 1] ?? 0,
+  ]);
+}
+
+function obstructionMessage(obstruction: NativeObstruction): string {
+  const message = obstruction.message;
+  if (message === undefined) {
+    return "Moonlight ABI obstruction";
+  }
+  if (typeof message === "string") {
+    return message.split("\0", 1)[0] ?? "";
+  }
+  const terminator = message.indexOf(0);
+  const length = terminator === -1 ? message.length : terminator;
+  return new TextDecoder().decode(Uint8Array.from(message.slice(0, length)));
+}
diff --git a/ffi/bindings/typescript/src/internal/native.generated.ts b/ffi/bindings/typescript/src/internal/native.generated.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/internal/native.generated.ts
@@ -0,0 +1,242 @@
+// Generated from Moonlight.Triangulation.Foreign.Contract.
+// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+import koffi from "koffi";
+
+export const ABI_VERSION = 2;
+export const ML_STATUS_OK = 0;
+export const ML_STATUS_NULL_POINTER = 1;
+export const ML_STATUS_COUNT_OVERFLOW = 2;
+export const ML_STATUS_BUFFER_TOO_SMALL = 3;
+export const ML_STATUS_GEOMETRY_OBSTRUCTION = 4;
+export const ML_STATUS_RUNTIME_FAILURE = 5;
+export const ML_OBSTRUCTION_NONE = 0;
+export const ML_OBSTRUCTION_INVALID_COORDINATE = 1;
+export const ML_OBSTRUCTION_POINT_LOCATION_FAILED = 2;
+export const ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED = 3;
+export const ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID = 4;
+export const ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX = 5;
+export const ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING = 6;
+export const ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING = 7;
+export const ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING = 8;
+export const ML_OBSTRUCTION_HULL_START_NOT_VISIBLE = 9;
+export const ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE = 10;
+export const ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE = 11;
+export const ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED = 12;
+export const ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE = 13;
+export const ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE = 14;
+export const ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE = 15;
+export const ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE = 16;
+export const ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION = 17;
+export const ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH = 18;
+export const ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH = 19;
+export const ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT = 20;
+export const ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH = 21;
+export const ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE = 22;
+export const ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY = 23;
+export const ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE = 24;
+export const ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH = 25;
+export const ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH = 26;
+export const ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED = 27;
+export const ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE = 28;
+export const ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE = 29;
+export const ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE = 30;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE = 31;
+export const ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE = 32;
+export const ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE = 33;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE = 34;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE = 35;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE = 36;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE = 37;
+export const ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM = 38;
+export const ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE = 39;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE = 40;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING = 41;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS = 42;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED = 43;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION = 44;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION = 45;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION = 46;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE = 47;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE = 48;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED = 49;
+export const ML_OBSTRUCTION_CAPACITY_EXCEEDED = 50;
+export const ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED = 51;
+export const ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED = 52;
+export const ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE = 53;
+export const ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH = 54;
+export const ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE = 55;
+export const ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE = 56;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET = 57;
+export const ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP = 58;
+export const ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED = 59;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE = 60;
+export const ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE = 61;
+export const ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE = 62;
+export const ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED = 63;
+export const ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED = 64;
+export const ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE = 65;
+export const ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED = 66;
+export const ML_OBSTRUCTION_NULL_POINTER = 100;
+export const ML_OBSTRUCTION_COUNT_OVERFLOW = 101;
+export const ML_OBSTRUCTION_BUFFER_TOO_SMALL = 102;
+export const ML_OBSTRUCTION_RUNTIME_FAILURE = 103;
+export const ML_OBSTRUCTION_REGION_LAYOUT_INVALID = 200;
+export const ML_OBSTRUCTION_REGION_VALIDATION_FAILED = 201;
+export const ML_OBSTRUCTION_OVERLAY_FAILED = 202;
+export const ML_OBSTRUCTION_REGION_PUBLICATION_FAILED = 203;
+export const ML_OBSTRUCTION_VALUATION_FAILED = 204;
+export const ML_OBSTRUCTION_MINKOWSKI_FAILED = 205;
+export const ML_OBSTRUCTION_REGION_PROJECTION_FAILED = 206;
+export const ML_COORDINATE_ERROR_NONE = 0;
+export const ML_COORDINATE_ERROR_NAN = 1;
+export const ML_COORDINATE_ERROR_INFINITE = 2;
+export const ML_COORDINATE_ERROR_TOO_SMALL = 3;
+export const ML_COORDINATE_ERROR_TOO_LARGE = 4;
+export const ML_REGION_EXTERIOR = 0;
+export const ML_REGION_BOUNDARY = 1;
+export const ML_REGION_INTERIOR = 2;
+export const ML_MINKOWSKI_ADDITION = 0;
+export const ML_MINKOWSKI_EROSION = 1;
+export const ML_MINKOWSKI_OPENING = 2;
+export const ML_MINKOWSKI_CLOSING = 3;
+
+export type NativeHandle = object;
+export type NativeInteger = number | bigint;
+export type HandleOutput = Array<NativeHandle | null>;
+
+export interface NativeObstruction {
+  code?: number;
+  coordinate_error?: number;
+  input_index?: NativeInteger;
+  first_index?: NativeInteger;
+  second_index?: NativeInteger;
+  first_value?: number;
+  second_value?: number;
+  point_x?: number;
+  point_y?: number;
+  message?: string | readonly number[];
+}
+
+export interface NativeMinkowskiReceipt {
+  operation?: number;
+  reserved?: number;
+  input_components?: NativeInteger;
+  convex_pieces?: NativeInteger;
+  generated_pieces?: NativeInteger;
+  generated_convolution_edges?: NativeInteger;
+  overlay_passes?: NativeInteger;
+  exact_crossings?: NativeInteger;
+  output_cells?: NativeInteger;
+  exact_coordinate_bit_growth?: NativeInteger;
+}
+
+export interface NativeApi {
+  readonly ml_abi_version: () => number;
+  readonly ml_runtime_initialize: () => number;
+  readonly ml_delaunay_f64: (coordinates: Float64Array, point_count: number, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_mesh_insert_many_f64: (mesh: NativeHandle, coordinates: Float64Array, point_count: number, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_mesh_site_union: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_mesh_site_intersection: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_mesh_site_difference: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_mesh_site_symmetric_difference: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_mesh_vertex_count: (mesh: NativeHandle, count: NativeInteger[], obstruction: NativeObstruction) => number;
+  readonly ml_mesh_triangle_count: (mesh: NativeHandle, count: NativeInteger[], obstruction: NativeObstruction) => number;
+  readonly ml_mesh_copy_vertices_f64: (mesh: NativeHandle, coordinates: Float64Array, point_capacity: number, points_written: NativeInteger[], obstruction: NativeObstruction) => number;
+  readonly ml_mesh_copy_triangles_u32: (mesh: NativeHandle, triangles: Uint32Array, triangle_capacity: number, triangles_written: NativeInteger[], obstruction: NativeObstruction) => number;
+  readonly ml_mesh_free: (mesh: NativeHandle) => void;
+  readonly ml_region_create_f64: (coordinates: Float64Array, point_count: number, loop_point_counts: BigUint64Array, loop_count: number, component_loop_counts: BigUint64Array, component_count: number, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_region_counts: (region: NativeHandle, component_count: NativeInteger[], loop_count: NativeInteger[], point_count: NativeInteger[], obstruction: NativeObstruction) => number;
+  readonly ml_region_copy_f64: (region: NativeHandle, coordinates: Float64Array, point_capacity: number, loop_point_offsets: BigUint64Array, loop_offset_capacity: number, component_loop_offsets: BigUint64Array, component_offset_capacity: number, obstruction: NativeObstruction) => number;
+  readonly ml_region_union: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_region_intersection: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_region_difference: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_region_symmetric_difference: (left: NativeHandle, right: NativeHandle, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_region_locate_point_f64: (region: NativeHandle, x: number, y: number, location: number[], obstruction: NativeObstruction) => number;
+  readonly ml_region_measure: (region: NativeHandle, euler_characteristic: NativeInteger[], area_ratio_utf8: Buffer, area_capacity: number, area_bytes_written: NativeInteger[], perimeter_lower: number[], perimeter_upper: number[], obstruction: NativeObstruction) => number;
+  readonly ml_region_free: (region: NativeHandle) => void;
+  readonly ml_structuring_element_create_f64: (coordinates: Float64Array, point_count: number, result: HandleOutput, obstruction: NativeObstruction) => number;
+  readonly ml_structuring_element_free: (element: NativeHandle) => void;
+  readonly ml_region_minkowski_sum: (left: NativeHandle, right: NativeHandle, result: HandleOutput, receipt: NativeMinkowskiReceipt, obstruction: NativeObstruction) => number;
+  readonly ml_region_offset: (element: NativeHandle, region: NativeHandle, result: HandleOutput, receipt: NativeMinkowskiReceipt, obstruction: NativeObstruction) => number;
+  readonly ml_region_inset: (element: NativeHandle, region: NativeHandle, result: HandleOutput, receipt: NativeMinkowskiReceipt, obstruction: NativeObstruction) => number;
+  readonly ml_region_open: (element: NativeHandle, region: NativeHandle, result: HandleOutput, receipt: NativeMinkowskiReceipt, obstruction: NativeObstruction) => number;
+  readonly ml_region_close: (element: NativeHandle, region: NativeHandle, result: HandleOutput, receipt: NativeMinkowskiReceipt, obstruction: NativeObstruction) => number;
+}
+
+export function loadNativeApi(libraryPath: string): NativeApi {
+  const library = koffi.load(libraryPath);
+  const mesh = koffi.opaque();
+  const meshPointer = koffi.pointer(mesh);
+  const meshOutput = koffi.out(koffi.pointer(mesh, 2));
+  const region = koffi.opaque();
+  const regionPointer = koffi.pointer(region);
+  const regionOutput = koffi.out(koffi.pointer(region, 2));
+  const structuringElement = koffi.opaque();
+  const structuringElementPointer = koffi.pointer(structuringElement);
+  const structuringElementOutput = koffi.out(koffi.pointer(structuringElement, 2));
+
+  const obstruction = koffi.struct({
+    code: "uint32_t",
+    coordinate_error: "uint32_t",
+    input_index: "uint64_t",
+    first_index: "uint64_t",
+    second_index: "uint64_t",
+    first_value: "double",
+    second_value: "double",
+    point_x: "double",
+    point_y: "double",
+    message: koffi.array("char", 256),
+  });
+  const receipt = koffi.struct({
+    operation: "uint32_t",
+    reserved: "uint32_t",
+    input_components: "uint64_t",
+    convex_pieces: "uint64_t",
+    generated_pieces: "uint64_t",
+    generated_convolution_edges: "uint64_t",
+    overlay_passes: "uint64_t",
+    exact_crossings: "uint64_t",
+    output_cells: "uint64_t",
+    exact_coordinate_bit_growth: "uint64_t",
+  });
+  const obstructionOutput = koffi.out(koffi.pointer(obstruction));
+  const receiptOutput = koffi.out(koffi.pointer(receipt));
+  const sizeOutput = koffi.out(koffi.pointer("size_t"));
+  const uint32Output = koffi.out(koffi.pointer("uint32_t"));
+  const int64Output = koffi.out(koffi.pointer("int64_t"));
+  const doubleOutput = koffi.out(koffi.pointer("double"));
+  const sizeArray = koffi.pointer("size_t");
+  return {
+    ml_abi_version: library.func("ml_abi_version", "uint32_t", []),
+    ml_runtime_initialize: library.func("ml_runtime_initialize", "uint32_t", []),
+    ml_delaunay_f64: library.func("ml_delaunay_f64", "uint32_t", [koffi.pointer("double"), "size_t", meshOutput, obstructionOutput]),
+    ml_mesh_insert_many_f64: library.func("ml_mesh_insert_many_f64", "uint32_t", [meshPointer, koffi.pointer("double"), "size_t", meshOutput, obstructionOutput]),
+    ml_mesh_site_union: library.func("ml_mesh_site_union", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
+    ml_mesh_site_intersection: library.func("ml_mesh_site_intersection", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
+    ml_mesh_site_difference: library.func("ml_mesh_site_difference", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
+    ml_mesh_site_symmetric_difference: library.func("ml_mesh_site_symmetric_difference", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),
+    ml_mesh_vertex_count: library.func("ml_mesh_vertex_count", "uint32_t", [meshPointer, sizeOutput, obstructionOutput]),
+    ml_mesh_triangle_count: library.func("ml_mesh_triangle_count", "uint32_t", [meshPointer, sizeOutput, obstructionOutput]),
+    ml_mesh_copy_vertices_f64: library.func("ml_mesh_copy_vertices_f64", "uint32_t", [meshPointer, koffi.out(koffi.pointer("double")), "size_t", sizeOutput, obstructionOutput]),
+    ml_mesh_copy_triangles_u32: library.func("ml_mesh_copy_triangles_u32", "uint32_t", [meshPointer, koffi.out(koffi.pointer("uint32_t")), "size_t", sizeOutput, obstructionOutput]),
+    ml_mesh_free: library.func("ml_mesh_free", "void", [meshPointer]),
+    ml_region_create_f64: library.func("ml_region_create_f64", "uint32_t", [koffi.pointer("double"), "size_t", sizeArray, "size_t", sizeArray, "size_t", regionOutput, obstructionOutput]),
+    ml_region_counts: library.func("ml_region_counts", "uint32_t", [regionPointer, sizeOutput, sizeOutput, sizeOutput, obstructionOutput]),
+    ml_region_copy_f64: library.func("ml_region_copy_f64", "uint32_t", [regionPointer, koffi.out(koffi.pointer("double")), "size_t", koffi.out(sizeArray), "size_t", koffi.out(sizeArray), "size_t", obstructionOutput]),
+    ml_region_union: library.func("ml_region_union", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
+    ml_region_intersection: library.func("ml_region_intersection", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
+    ml_region_difference: library.func("ml_region_difference", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
+    ml_region_symmetric_difference: library.func("ml_region_symmetric_difference", "uint32_t", [regionPointer, regionPointer, regionOutput, obstructionOutput]),
+    ml_region_locate_point_f64: library.func("ml_region_locate_point_f64", "uint32_t", [regionPointer, "double", "double", uint32Output, obstructionOutput]),
+    ml_region_measure: library.func("ml_region_measure", "uint32_t", [regionPointer, int64Output, koffi.out(koffi.pointer("char")), "size_t", sizeOutput, doubleOutput, doubleOutput, obstructionOutput]),
+    ml_region_free: library.func("ml_region_free", "void", [regionPointer]),
+    ml_structuring_element_create_f64: library.func("ml_structuring_element_create_f64", "uint32_t", [koffi.pointer("double"), "size_t", structuringElementOutput, obstructionOutput]),
+    ml_structuring_element_free: library.func("ml_structuring_element_free", "void", [structuringElementPointer]),
+    ml_region_minkowski_sum: library.func("ml_region_minkowski_sum", "uint32_t", [regionPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
+    ml_region_offset: library.func("ml_region_offset", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
+    ml_region_inset: library.func("ml_region_inset", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
+    ml_region_open: library.func("ml_region_open", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
+    ml_region_close: library.func("ml_region_close", "uint32_t", [structuringElementPointer, regionPointer, regionOutput, receiptOutput, obstructionOutput]),
+  };
+}
diff --git a/ffi/bindings/typescript/src/internal/resource.ts b/ffi/bindings/typescript/src/internal/resource.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/internal/resource.ts
@@ -0,0 +1,113 @@
+import { failure, success, type ResourceKind, type Result } from "../failure.js";
+import type { OwnedResource } from "../resource.js";
+import type { NativeApi, NativeHandle } from "./native.generated.js";
+
+export interface RuntimeContext {
+  readonly native: NativeApi;
+  readonly identity: symbol;
+}
+
+interface FinalizerState {
+  readonly free: (handle: NativeHandle) => void;
+  readonly handle: NativeHandle;
+}
+
+const handleFinalizer = new FinalizationRegistry<FinalizerState>(({ free, handle }) => free(handle));
+export const resourceAccess: unique symbol = Symbol("moonlight resource access");
+
+export interface InternalResource {
+  readonly [resourceAccess]: () => ResourceProjection;
+}
+
+interface OpenResource {
+  readonly kind: "open";
+  readonly handle: NativeHandle;
+}
+
+interface DisposedResource {
+  readonly kind: "disposed";
+}
+
+type ResourceState = OpenResource | DisposedResource;
+
+export interface ResourceProjection {
+  readonly context: RuntimeContext;
+  readonly resource: ResourceKind;
+  readonly state: ResourceState;
+}
+
+export abstract class OwnedNativeResource implements InternalResource, OwnedResource {
+  readonly #context: RuntimeContext;
+  readonly #free: (handle: NativeHandle) => void;
+  readonly #resource: ResourceKind;
+  #state: ResourceState;
+
+  protected constructor(
+    context: RuntimeContext,
+    handle: NativeHandle,
+    free: (handle: NativeHandle) => void,
+    resource: ResourceKind,
+  ) {
+    this.#context = context;
+    this.#free = free;
+    this.#resource = resource;
+    this.#state = { kind: "open", handle };
+    handleFinalizer.register(this, { free, handle }, this);
+  }
+
+  get disposed(): boolean {
+    return this.#state.kind === "disposed";
+  }
+
+  dispose(): void {
+    const state = this.#state;
+    if (state.kind === "open") {
+      handleFinalizer.unregister(this);
+      this.#free(state.handle);
+      this.#state = { kind: "disposed" };
+    }
+  }
+
+  [Symbol.dispose](): void {
+    this.dispose();
+  }
+
+  [resourceAccess](): ResourceProjection {
+    return {
+      context: this.#context,
+      resource: this.#resource,
+      state: this.#state,
+    };
+  }
+
+  protected context(): RuntimeContext {
+    return this.#context;
+  }
+
+  protected handle(): Result<NativeHandle> {
+    return this.#state.kind === "open"
+      ? success(this.#state.handle)
+      : failure({ kind: "disposed-resource", resource: this.#resource });
+  }
+}
+
+export function borrowResource(
+  value: unknown,
+  expectedContext: RuntimeContext,
+  expectedResource: ResourceKind,
+): Result<NativeHandle> {
+  if (!isInternalResource(value)) {
+    return failure({ kind: "foreign-runtime", resource: expectedResource });
+  }
+  const projection = value[resourceAccess]();
+  if (projection.context.identity !== expectedContext.identity || projection.resource !== expectedResource) {
+    return failure({ kind: "foreign-runtime", resource: expectedResource });
+  }
+  return projection.state.kind === "open"
+    ? success(projection.state.handle)
+    : failure({ kind: "disposed-resource", resource: expectedResource });
+}
+
+function isInternalResource(value: unknown): value is InternalResource {
+  return typeof value === "object" && value !== null && resourceAccess in value;
+}
diff --git a/ffi/bindings/typescript/src/mesh.ts b/ffi/bindings/typescript/src/mesh.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/mesh.ts
@@ -0,0 +1,161 @@
+import { success, type Result } from "./failure.js";
+import { callStatus, flattenPoints, produceHandle } from "./internal/call.js";
+import { pointsFromCoordinates, toSafeNumber } from "./internal/decode.js";
+import type { NativeApi, NativeHandle, NativeInteger } from "./internal/native.generated.js";
+import { borrowResource, OwnedNativeResource, type RuntimeContext } from "./internal/resource.js";
+import type { OwnedResource } from "./resource.js";
+import type { Point, Triangle } from "./values.js";
+
+export interface Mesh extends OwnedResource {
+  vertexCount(): Result<number>;
+  triangleCount(): Result<number>;
+  vertices(): Result<readonly Point[]>;
+  triangles(): Result<readonly Triangle[]>;
+  insertMany(points: readonly Point[]): Result<Mesh>;
+  siteUnion(other: Mesh): Result<Mesh>;
+  siteIntersection(other: Mesh): Result<Mesh>;
+  siteDifference(other: Mesh): Result<Mesh>;
+  siteSymmetricDifference(other: Mesh): Result<Mesh>;
+}
+
+/** @internal */
+export function createMesh(context: RuntimeContext, handle: NativeHandle): Mesh {
+  return new MeshResource(context, handle);
+}
+
+class MeshResource extends OwnedNativeResource implements Mesh {
+  constructor(context: RuntimeContext, handle: NativeHandle) {
+    super(context, handle, context.native.ml_mesh_free, "mesh");
+  }
+
+  vertexCount(): Result<number> {
+    return this.count(this.context().native.ml_mesh_vertex_count);
+  }
+
+  triangleCount(): Result<number> {
+    return this.count(this.context().native.ml_mesh_triangle_count);
+  }
+
+  vertices(): Result<readonly Point[]> {
+    const handle = this.handle();
+    if (!handle.ok) {
+      return handle;
+    }
+    const count = this.vertexCount();
+    if (!count.ok) {
+      return count;
+    }
+    const coordinates = new Float64Array(count.value * 2);
+    const written: NativeInteger[] = [0];
+    const copied = callStatus((obstruction) =>
+      this.context().native.ml_mesh_copy_vertices_f64(
+        handle.value,
+        coordinates,
+        count.value,
+        written,
+        obstruction,
+      ),
+    );
+    if (!copied.ok) {
+      return copied;
+    }
+    const writtenCount = toSafeNumber(written[0]);
+    return writtenCount.ok
+      ? success(pointsFromCoordinates(coordinates, writtenCount.value))
+      : writtenCount;
+  }
+
+  triangles(): Result<readonly Triangle[]> {
+    const handle = this.handle();
+    if (!handle.ok) {
+      return handle;
+    }
+    const count = this.triangleCount();
+    if (!count.ok) {
+      return count;
+    }
+    const triangles = new Uint32Array(count.value * 3);
+    const written: NativeInteger[] = [0];
+    const copied = callStatus((obstruction) =>
+      this.context().native.ml_mesh_copy_triangles_u32(
+        handle.value,
+        triangles,
+        count.value,
+        written,
+        obstruction,
+      ),
+    );
+    if (!copied.ok) {
+      return copied;
+    }
+    const writtenCount = toSafeNumber(written[0]);
+    return writtenCount.ok
+      ? success(
+          Array.from({ length: writtenCount.value }, (_unused, index): Triangle => [
+            triangles[index * 3] ?? 0,
+            triangles[index * 3 + 1] ?? 0,
+            triangles[index * 3 + 2] ?? 0,
+          ]),
+        )
+      : writtenCount;
+  }
+
+  insertMany(points: readonly Point[]): Result<Mesh> {
+    const handle = this.handle();
+    if (!handle.ok) {
+      return handle;
+    }
+    const coordinates = flattenPoints(points);
+    const produced = produceHandle((output, obstruction) =>
+      this.context().native.ml_mesh_insert_many_f64(
+        handle.value,
+        coordinates,
+        coordinates.length / 2,
+        output,
+        obstruction,
+      ),
+    );
+    return produced.ok ? success(createMesh(this.context(), produced.value)) : produced;
+  }
+
+  siteUnion(other: Mesh): Result<Mesh> {
+    return this.binary(other, this.context().native.ml_mesh_site_union);
+  }
+
+  siteIntersection(other: Mesh): Result<Mesh> {
+    return this.binary(other, this.context().native.ml_mesh_site_intersection);
+  }
+
+  siteDifference(other: Mesh): Result<Mesh> {
+    return this.binary(other, this.context().native.ml_mesh_site_difference);
+  }
+
+  siteSymmetricDifference(other: Mesh): Result<Mesh> {
+    return this.binary(other, this.context().native.ml_mesh_site_symmetric_difference);
+  }
+
+  private binary(other: Mesh, operation: NativeApi["ml_mesh_site_union"]): Result<Mesh> {
+    const left = this.handle();
+    if (!left.ok) {
+      return left;
+    }
+    const right = borrowResource(other, this.context(), "mesh");
+    if (!right.ok) {
+      return right;
+    }
+    const produced = produceHandle((output, obstruction) =>
+      operation(left.value, right.value, output, obstruction),
+    );
+    return produced.ok ? success(createMesh(this.context(), produced.value)) : produced;
+  }
+
+  private count(operation: NativeApi["ml_mesh_vertex_count"]): Result<number> {
+    const handle = this.handle();
+    if (!handle.ok) {
+      return handle;
+    }
+    const output: NativeInteger[] = [0];
+    const counted = callStatus((obstruction) => operation(handle.value, output, obstruction));
+    return counted.ok ? toSafeNumber(output[0]) : counted;
+  }
+}
diff --git a/ffi/bindings/typescript/src/moonlight.ts b/ffi/bindings/typescript/src/moonlight.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/moonlight.ts
@@ -0,0 +1,120 @@
+import { failure, success, type Result } from "./failure.js";
+import { flattenLoops, flattenPoints, produceHandle } from "./internal/call.js";
+import {
+  ABI_VERSION,
+  ML_STATUS_OK,
+  loadNativeApi,
+} from "./internal/native.generated.js";
+import type { RuntimeContext } from "./internal/resource.js";
+import { createMesh, type Mesh } from "./mesh.js";
+import { createStructuringElement, type StructuringElement } from "./morphology.js";
+import { createRegion, type Region } from "./region.js";
+import type { Point, PolygonComponent } from "./values.js";
+import { decodeAbiStatus } from "./wire.generated.js";
+
+export type NativeLibrarySource = {
+  readonly kind: "path";
+  readonly path: string;
+};
+
+export interface Moonlight {
+  readonly abiVersion: number;
+  delaunay(points: readonly Point[]): Result<Mesh>;
+  region(components: readonly PolygonComponent[]): Result<Region>;
+  structuringElement(points: readonly Point[]): Result<StructuringElement>;
+}
+
+export function loadMoonlight(source: NativeLibrarySource): Result<Moonlight> {
+  if (source.path.length === 0) {
+    return failure({
+      kind: "native-library-unavailable",
+      path: source.path,
+      message: "native library path must not be empty",
+    });
+  }
+  try {
+    const native = loadNativeApi(source.path);
+    const initializationStatus = native.ml_runtime_initialize();
+    if (initializationStatus !== ML_STATUS_OK) {
+      const status = decodeAbiStatus(initializationStatus);
+      return status === undefined
+        ? failure({ kind: "unknown-wire-value", vocabulary: "status", value: initializationStatus })
+        : failure({ kind: "runtime-initialization-refused", status });
+    }
+    const observedVersion = native.ml_abi_version();
+    if (observedVersion !== ABI_VERSION) {
+      return failure({
+        kind: "abi-version-mismatch",
+        expected: ABI_VERSION,
+        observed: observedVersion,
+      });
+    }
+    const context: RuntimeContext = { native, identity: Symbol("moonlight runtime") };
+    return success(new MoonlightRuntime(context));
+  } catch (cause: unknown) {
+    return failure({
+      kind: "native-library-unavailable",
+      path: source.path,
+      message: cause instanceof Error ? cause.message : String(cause),
+    });
+  }
+}
+
+class MoonlightRuntime implements Moonlight {
+  readonly abiVersion = ABI_VERSION;
+  readonly #context: RuntimeContext;
+
+  constructor(context: RuntimeContext) {
+    this.#context = context;
+  }
+
+  delaunay(points: readonly Point[]): Result<Mesh> {
+    const coordinates = flattenPoints(points);
+    const produced = produceHandle((output, obstruction) =>
+      this.#context.native.ml_delaunay_f64(
+        coordinates,
+        coordinates.length / 2,
+        output,
+        obstruction,
+      ),
+    );
+    return produced.ok ? success(createMesh(this.#context, produced.value)) : produced;
+  }
+
+  region(components: readonly PolygonComponent[]): Result<Region> {
+    const loops = components.flatMap(({ outer, holes = [] }) => [outer, ...holes]);
+    const loopPointCounts = BigUint64Array.from(loops.map((loop) => BigInt(loop.length)));
+    const componentLoopCounts = BigUint64Array.from(
+      components.map(({ holes = [] }) => BigInt(holes.length + 1)),
+    );
+    const coordinates = flattenLoops(loops);
+    const produced = produceHandle((output, obstruction) =>
+      this.#context.native.ml_region_create_f64(
+        coordinates,
+        coordinates.length / 2,
+        loopPointCounts,
+        loopPointCounts.length,
+        componentLoopCounts,
+        componentLoopCounts.length,
+        output,
+        obstruction,
+      ),
+    );
+    return produced.ok ? success(createRegion(this.#context, produced.value)) : produced;
+  }
+
+  structuringElement(points: readonly Point[]): Result<StructuringElement> {
+    const coordinates = flattenPoints(points);
+    const produced = produceHandle((output, obstruction) =>
+      this.#context.native.ml_structuring_element_create_f64(
+        coordinates,
+        coordinates.length / 2,
+        output,
+        obstruction,
+      ),
+    );
+    return produced.ok
+      ? success(createStructuringElement(this.#context, produced.value))
+      : produced;
+  }
+}
diff --git a/ffi/bindings/typescript/src/morphology.ts b/ffi/bindings/typescript/src/morphology.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/morphology.ts
@@ -0,0 +1,25 @@
+import type { NativeHandle } from "./internal/native.generated.js";
+import { OwnedNativeResource, type RuntimeContext } from "./internal/resource.js";
+import type { OwnedResource } from "./resource.js";
+
+declare const structuringElementBrand: unique symbol;
+
+export interface StructuringElement extends OwnedResource {
+  readonly [structuringElementBrand]: "structuring-element";
+}
+
+/** @internal */
+export function createStructuringElement(
+  context: RuntimeContext,
+  handle: NativeHandle,
+): StructuringElement {
+  return new StructuringElementResource(context, handle);
+}
+
+class StructuringElementResource extends OwnedNativeResource implements StructuringElement {
+  declare readonly [structuringElementBrand]: "structuring-element";
+
+  constructor(context: RuntimeContext, handle: NativeHandle) {
+    super(context, handle, context.native.ml_structuring_element_free, "structuring-element");
+  }
+}
diff --git a/ffi/bindings/typescript/src/region.ts b/ffi/bindings/typescript/src/region.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/region.ts
@@ -0,0 +1,281 @@
+import { Buffer } from "node:buffer";
+
+import { collectResults, failure, success, type Result } from "./failure.js";
+import { captureNativeCall, callStatus, produceHandle, produceMorphology } from "./internal/call.js";
+import { decodeLocation, decodeStatus, pointsFromCoordinates, toBigInt, toSafeNumber } from "./internal/decode.js";
+import {
+  ML_OBSTRUCTION_BUFFER_TOO_SMALL,
+  ML_STATUS_BUFFER_TOO_SMALL,
+  type NativeApi,
+  type NativeHandle,
+  type NativeInteger,
+  type NativeObstruction,
+} from "./internal/native.generated.js";
+import { borrowResource, OwnedNativeResource, type RuntimeContext } from "./internal/resource.js";
+import type { StructuringElement } from "./morphology.js";
+import type { OwnedResource } from "./resource.js";
+import type { MinkowskiReceipt, Point, PolygonComponent, RegionValuations } from "./values.js";
+import type { RegionLocation } from "./wire.generated.js";
+
+export interface MorphologyResult {
+  readonly region: Region;
+  readonly receipt: MinkowskiReceipt;
+}
+
+export interface Region extends OwnedResource {
+  components(): Result<readonly PolygonComponent[]>;
+  valuations(): Result<RegionValuations>;
+  locate(point: Point): Result<RegionLocation>;
+  union(other: Region): Result<Region>;
+  intersection(other: Region): Result<Region>;
+  difference(other: Region): Result<Region>;
+  symmetricDifference(other: Region): Result<Region>;
+  minkowskiSum(other: Region): Result<MorphologyResult>;
+  offset(element: StructuringElement): Result<MorphologyResult>;
+  inset(element: StructuringElement): Result<MorphologyResult>;
+  open(element: StructuringElement): Result<MorphologyResult>;
+  close(element: StructuringElement): Result<MorphologyResult>;
+}
+
+/** @internal */
+export function createRegion(context: RuntimeContext, handle: NativeHandle): Region {
+  return new RegionResource(context, handle);
+}
+
+class RegionResource extends OwnedNativeResource implements Region {
+  constructor(context: RuntimeContext, handle: NativeHandle) {
+    super(context, handle, context.native.ml_region_free, "region");
+  }
+
+  components(): Result<readonly PolygonComponent[]> {
+    const handle = this.handle();
+    if (!handle.ok) {
+      return handle;
+    }
+    const counts = this.counts(handle.value);
+    if (!counts.ok) {
+      return counts;
+    }
+    const [componentCount, loopCount, pointCount] = counts.value;
+    const coordinates = new Float64Array(pointCount * 2);
+    const loopPointOffsets = new BigUint64Array(loopCount + 1);
+    const componentLoopOffsets = new BigUint64Array(componentCount + 1);
+    const copied = callStatus((obstruction) =>
+      this.context().native.ml_region_copy_f64(
+        handle.value,
+        coordinates,
+        pointCount,
+        loopPointOffsets,
+        loopCount + 1,
+        componentLoopOffsets,
+        componentCount + 1,
+        obstruction,
+      ),
+    );
+    if (!copied.ok) {
+      return copied;
+    }
+    const points = pointsFromCoordinates(coordinates, pointCount);
+    const loopOffsets = collectResults(
+      Array.from({ length: loopCount + 1 }, (_unused, index) => toSafeNumber(loopPointOffsets[index])),
+    );
+    if (!loopOffsets.ok) {
+      return loopOffsets;
+    }
+    const componentOffsets = collectResults(
+      Array.from({ length: componentCount + 1 }, (_unused, index) => toSafeNumber(componentLoopOffsets[index])),
+    );
+    if (!componentOffsets.ok) {
+      return componentOffsets;
+    }
+    const loops = Array.from({ length: loopCount }, (_unused, index) =>
+      points.slice(loopOffsets.value[index] ?? 0, loopOffsets.value[index + 1] ?? 0),
+    );
+    return collectResults(
+      Array.from({ length: componentCount }, (_unused, index): Result<PolygonComponent> => {
+        const start = componentOffsets.value[index] ?? 0;
+        const end = componentOffsets.value[index + 1] ?? 0;
+        const outer = loops[start];
+        return outer === undefined || start >= end
+          ? failure({ kind: "invalid-native-result", message: "Moonlight returned a component without an outer loop" })
+          : success({ outer, holes: loops.slice(start + 1, end) });
+      }),
+    );
+  }
+
+  valuations(): Result<RegionValuations> {
+    const handle = this.handle();
+    return handle.ok ? this.measureWithCapacity(handle.value, 128) : handle;
+  }
+
+  locate([x, y]: Point): Result<RegionLocation> {
+    const handle = this.handle();
+    if (!handle.ok) {
+      return handle;
+    }
+    const location = [0];
+    const located = callStatus((obstruction) =>
+      this.context().native.ml_region_locate_point_f64(handle.value, x, y, location, obstruction),
+    );
+    return located.ok ? decodeLocation(location[0]) : located;
+  }
+
+  union(other: Region): Result<Region> {
+    return this.binary(other, this.context().native.ml_region_union);
+  }
+
+  intersection(other: Region): Result<Region> {
+    return this.binary(other, this.context().native.ml_region_intersection);
+  }
+
+  difference(other: Region): Result<Region> {
+    return this.binary(other, this.context().native.ml_region_difference);
+  }
+
+  symmetricDifference(other: Region): Result<Region> {
+    return this.binary(other, this.context().native.ml_region_symmetric_difference);
+  }
+
+  minkowskiSum(other: Region): Result<MorphologyResult> {
+    const left = this.handle();
+    if (!left.ok) {
+      return left;
+    }
+    const right = borrowResource(other, this.context(), "region");
+    return right.ok
+      ? this.morphology(this.context().native.ml_region_minkowski_sum, left.value, right.value)
+      : right;
+  }
+
+  offset(element: StructuringElement): Result<MorphologyResult> {
+    return this.withElement(element, this.context().native.ml_region_offset);
+  }
+
+  inset(element: StructuringElement): Result<MorphologyResult> {
+    return this.withElement(element, this.context().native.ml_region_inset);
+  }
+
+  open(element: StructuringElement): Result<MorphologyResult> {
+    return this.withElement(element, this.context().native.ml_region_open);
+  }
+
+  close(element: StructuringElement): Result<MorphologyResult> {
+    return this.withElement(element, this.context().native.ml_region_close);
+  }
+
+  private binary(other: Region, operation: NativeApi["ml_region_union"]): Result<Region> {
+    const left = this.handle();
+    if (!left.ok) {
+      return left;
+    }
+    const right = borrowResource(other, this.context(), "region");
+    if (!right.ok) {
+      return right;
+    }
+    const produced = produceHandle((output, obstruction) =>
+      operation(left.value, right.value, output, obstruction),
+    );
+    return produced.ok ? success(createRegion(this.context(), produced.value)) : produced;
+  }
+
+  private withElement(
+    element: StructuringElement,
+    operation: NativeApi["ml_region_offset"],
+  ): Result<MorphologyResult> {
+    const region = this.handle();
+    if (!region.ok) {
+      return region;
+    }
+    const structuringElement = borrowResource(element, this.context(), "structuring-element");
+    return structuringElement.ok
+      ? this.morphology(operation, structuringElement.value, region.value)
+      : structuringElement;
+  }
+
+  private morphology(
+    operation: NativeApi["ml_region_minkowski_sum"],
+    first: NativeHandle,
+    second: NativeHandle,
+  ): Result<MorphologyResult> {
+    const produced = produceMorphology((output, receipt, obstruction) =>
+      operation(first, second, output, receipt, obstruction),
+    );
+    return produced.ok
+      ? success({ region: createRegion(this.context(), produced.value.handle), receipt: produced.value.receipt })
+      : produced;
+  }
+
+  private counts(handle: NativeHandle): Result<readonly [number, number, number]> {
+    const componentCount: NativeInteger[] = [0];
+    const loopCount: NativeInteger[] = [0];
+    const pointCount: NativeInteger[] = [0];
+    const counted = callStatus((obstruction) =>
+      this.context().native.ml_region_counts(
+        handle,
+        componentCount,
+        loopCount,
+        pointCount,
+        obstruction,
+      ),
+    );
+    if (!counted.ok) {
+      return counted;
+    }
+    const components = toSafeNumber(componentCount[0]);
+    const loops = toSafeNumber(loopCount[0]);
+    const points = toSafeNumber(pointCount[0]);
+    if (!components.ok) {
+      return failure(components.error);
+    }
+    if (!loops.ok) {
+      return failure(loops.error);
+    }
+    if (!points.ok) {
+      return failure(points.error);
+    }
+    return success([components.value, loops.value, points.value]);
+  }
+
+  private measureWithCapacity(handle: NativeHandle, capacity: number): Result<RegionValuations> {
+    return captureNativeCall(() => {
+      const euler: NativeInteger[] = [0];
+      const areaRatio = Buffer.alloc(capacity);
+      const areaBytes: NativeInteger[] = [0];
+      const perimeterLower = [0];
+      const perimeterUpper = [0];
+      const obstruction: NativeObstruction = {};
+      const status = this.context().native.ml_region_measure(
+        handle,
+        euler,
+        areaRatio,
+        capacity,
+        areaBytes,
+        perimeterLower,
+        perimeterUpper,
+        obstruction,
+      );
+      if (status === ML_STATUS_BUFFER_TOO_SMALL && obstruction.code === ML_OBSTRUCTION_BUFFER_TOO_SMALL) {
+        const required = toSafeNumber(areaBytes[0]);
+        return required.ok ? this.measureWithCapacity(handle, required.value + 1) : required;
+      }
+      const measured = decodeStatus(status, obstruction);
+      if (!measured.ok) {
+        return measured;
+      }
+      const byteCount = toSafeNumber(areaBytes[0]);
+      if (!byteCount.ok) {
+        return byteCount;
+      }
+      const ratio = areaRatio.subarray(0, byteCount.value).toString("ascii");
+      const [numerator, denominator, remainder] = ratio.split("/");
+      if (numerator === undefined || denominator === undefined || remainder !== undefined) {
+        return failure({ kind: "invalid-native-result", message: "Moonlight returned a malformed exact-area ratio" });
+      }
+      return success({
+        eulerCharacteristic: toBigInt(euler[0]),
+        area: { numerator: BigInt(numerator), denominator: BigInt(denominator) },
+        perimeterBounds: [perimeterLower[0] ?? 0, perimeterUpper[0] ?? 0],
+      });
+    });
+  }
+}
diff --git a/ffi/bindings/typescript/src/resource.ts b/ffi/bindings/typescript/src/resource.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/resource.ts
@@ -0,0 +1,4 @@
+export interface OwnedResource extends Disposable {
+  readonly disposed: boolean;
+  dispose(): void;
+}
diff --git a/ffi/bindings/typescript/src/values.ts b/ffi/bindings/typescript/src/values.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/values.ts
@@ -0,0 +1,32 @@
+import type { MinkowskiOperation } from "./wire.generated.js";
+
+export type Point = readonly [x: number, y: number];
+export type Triangle = readonly [first: number, second: number, third: number];
+
+export interface PolygonComponent {
+  readonly outer: readonly Point[];
+  readonly holes?: readonly (readonly Point[])[];
+}
+
+export interface ExactRational {
+  readonly numerator: bigint;
+  readonly denominator: bigint;
+}
+
+export interface RegionValuations {
+  readonly eulerCharacteristic: bigint;
+  readonly area: ExactRational;
+  readonly perimeterBounds: readonly [lower: number, upper: number];
+}
+
+export interface MinkowskiReceipt {
+  readonly operation: MinkowskiOperation;
+  readonly inputComponents: bigint;
+  readonly convexPieces: bigint;
+  readonly generatedPieces: bigint;
+  readonly generatedConvolutionEdges: bigint;
+  readonly overlayPasses: bigint;
+  readonly exactCrossings: bigint;
+  readonly outputCells: bigint;
+  readonly exactCoordinateBitGrowth: bigint;
+}
diff --git a/ffi/bindings/typescript/src/wire.generated.ts b/ffi/bindings/typescript/src/wire.generated.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/src/wire.generated.ts
@@ -0,0 +1,239 @@
+// Generated from Moonlight.Triangulation.Foreign.Contract.
+// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+export const AbiStatus = {
+  Ok: "ok",
+  NullPointer: "null-pointer",
+  CountOverflow: "count-overflow",
+  BufferTooSmall: "buffer-too-small",
+  GeometryObstruction: "geometry-obstruction",
+  RuntimeFailure: "runtime-failure",
+} as const;
+export type AbiStatus = (typeof AbiStatus)[keyof typeof AbiStatus];
+const abiStatusByWire: Readonly<Record<number, AbiStatus>> = {
+  0: AbiStatus.Ok,
+  1: AbiStatus.NullPointer,
+  2: AbiStatus.CountOverflow,
+  3: AbiStatus.BufferTooSmall,
+  4: AbiStatus.GeometryObstruction,
+  5: AbiStatus.RuntimeFailure,
+};
+export function decodeAbiStatus(value: number): AbiStatus | undefined {
+  return abiStatusByWire[value];
+}
+
+export const ObstructionCode = {
+  None: "none",
+  InvalidCoordinate: "invalid-coordinate",
+  PointLocationFailed: "point-location-failed",
+  LocationWalkExhausted: "location-walk-exhausted",
+  RefinementInputTopologyInvalid: "refinement-input-topology-invalid",
+  FreshInsertionMatchedExistingVertex: "fresh-insertion-matched-existing-vertex",
+  DegenerateLineEndpointMissingOutgoing: "degenerate-line-endpoint-missing-outgoing",
+  DegenerateLineEndpointTurnMissing: "degenerate-line-endpoint-turn-missing",
+  DegenerateLineConnectedVertexMissing: "degenerate-line-connected-vertex-missing",
+  HullStartNotVisible: "hull-start-not-visible",
+  OuterRangeDidNotTerminate: "outer-range-did-not-terminate",
+  OuterRangeContainsInnerEdge: "outer-range-contains-inner-edge",
+  ConstrainedEdgeFlipRefused: "constrained-edge-flip-refused",
+  RemovalVertexOutOfRange: "removal-vertex-out-of-range",
+  RemovalEdgeOutOfRange: "removal-edge-out-of-range",
+  RemovalFaceOutOfRange: "removal-face-out-of-range",
+  RemovalFaceCycleDidNotTerminate: "removal-face-cycle-did-not-terminate",
+  RemovalEmptyTriangulation: "removal-empty-triangulation",
+  RemovalTwoPointDegreeMismatch: "removal-two-point-degree-mismatch",
+  RemovalCollinearDegreeMismatch: "removal-collinear-degree-mismatch",
+  RemovalBorderTooShort: "removal-border-too-short",
+  RemovalBorderArityMismatch: "removal-border-arity-mismatch",
+  RemovalOutgoingCycleDidNotTerminate: "removal-outgoing-cycle-did-not-terminate",
+  CircleSweepHullEmpty: "circle-sweep-hull-empty",
+  OuterCycleDidNotTerminate: "outer-cycle-did-not-terminate",
+  HierarchyLevelPopulationMismatch: "hierarchy-level-population-mismatch",
+  HierarchyInsertionHandleMismatch: "hierarchy-insertion-handle-mismatch",
+  PointIndexCapacityExhausted: "point-index-capacity-exhausted",
+  RefinementMinimumAngleNotFinite: "refinement-minimum-angle-not-finite",
+  RefinementMinimumAngleOutOfRange: "refinement-minimum-angle-out-of-range",
+  RefinementMinimumAngleDerivedRatioNotFinite: "refinement-minimum-angle-derived-ratio-not-finite",
+  RefinementMaximumAdditionalVerticesNegative: "refinement-maximum-additional-vertices-negative",
+  RefinementMinimumAreaNotFinite: "refinement-minimum-area-not-finite",
+  RefinementMinimumAreaNegative: "refinement-minimum-area-negative",
+  RefinementMaximumAreaNotFinite: "refinement-maximum-area-not-finite",
+  RefinementMaximumAreaNotPositive: "refinement-maximum-area-not-positive",
+  RefinementMaximumRadiusEdgeRatioNotFinite: "refinement-maximum-radius-edge-ratio-not-finite",
+  RefinementMaximumRadiusEdgeRatioNotPositive: "refinement-maximum-radius-edge-ratio-not-positive",
+  RefinementMinimumAreaExceedsMaximum: "refinement-minimum-area-exceeds-maximum",
+  RefinementSeedFaceNotActive: "refinement-seed-face-not-active",
+  RefinementDomainInterfaceEdgeNotActive: "refinement-domain-interface-edge-not-active",
+  RefinementDomainInterfaceMissing: "refinement-domain-interface-missing",
+  RefinementDomainInterfaceExtraneous: "refinement-domain-interface-extraneous",
+  RefinementDomainTopologyChanged: "refinement-domain-topology-changed",
+  RefinementDomainRequiresConvexHullPreservation: "refinement-domain-requires-convex-hull-preservation",
+  RefinementDomainRequiresConstraintPreservation: "refinement-domain-requires-constraint-preservation",
+  RefinementDomainForbidsOuterFaceExclusion: "refinement-domain-forbids-outer-face-exclusion",
+  RefinementDomainWouldCrossInterface: "refinement-domain-would-cross-interface",
+  RefinementDomainWouldRewriteProtectedFace: "refinement-domain-would-rewrite-protected-face",
+  RefinementDomainProtectedFaceChanged: "refinement-domain-protected-face-changed",
+  CapacityExceeded: "capacity-exceeded",
+  HalfEdgeCapacityExceeded: "half-edge-capacity-exceeded",
+  FaceCapacityExceeded: "face-capacity-exceeded",
+  PayloadStorageFailure: "payload-storage-failure",
+  CoordinatePayloadCountMismatch: "coordinate-payload-count-mismatch",
+  CircleSweepRequiresDenseStorage: "circle-sweep-requires-dense-storage",
+  SeamFrontierUnavailable: "seam-frontier-unavailable",
+  RefinementDomainRequiresFiniteVertexBudget: "refinement-domain-requires-finite-vertex-budget",
+  SeamSourceEdgeRequiresFlip: "seam-source-edge-requires-flip",
+  SeamProtectedSourceFaceChanged: "seam-protected-source-face-changed",
+  RefinementMaximumEdgeLengthNotFinite: "refinement-maximum-edge-length-not-finite",
+  RefinementMaximumEdgeLengthNotPositive: "refinement-maximum-edge-length-not-positive",
+  RefinementOversizedEdge: "refinement-oversized-edge",
+  RefinementSeamBridgeBudgetExceeded: "refinement-seam-bridge-budget-exceeded",
+  RefinementSeamBridgeMidpointCollapsed: "refinement-seam-bridge-midpoint-collapsed",
+  BoundarySplitRequiresBoundaryEdge: "boundary-split-requires-boundary-edge",
+  RefinementDomainInterfaceOppositeFaceNotPermitted: "refinement-domain-interface-opposite-face-not-permitted",
+  NullPointer: "null-pointer",
+  CountOverflow: "count-overflow",
+  BufferTooSmall: "buffer-too-small",
+  RuntimeFailure: "runtime-failure",
+  RegionLayoutInvalid: "region-layout-invalid",
+  RegionValidationFailed: "region-validation-failed",
+  OverlayFailed: "overlay-failed",
+  RegionPublicationFailed: "region-publication-failed",
+  ValuationFailed: "valuation-failed",
+  MinkowskiFailed: "minkowski-failed",
+  RegionProjectionFailed: "region-projection-failed",
+} as const;
+export type ObstructionCode = (typeof ObstructionCode)[keyof typeof ObstructionCode];
+const obstructionCodeByWire: Readonly<Record<number, ObstructionCode>> = {
+  0: ObstructionCode.None,
+  1: ObstructionCode.InvalidCoordinate,
+  2: ObstructionCode.PointLocationFailed,
+  3: ObstructionCode.LocationWalkExhausted,
+  4: ObstructionCode.RefinementInputTopologyInvalid,
+  5: ObstructionCode.FreshInsertionMatchedExistingVertex,
+  6: ObstructionCode.DegenerateLineEndpointMissingOutgoing,
+  7: ObstructionCode.DegenerateLineEndpointTurnMissing,
+  8: ObstructionCode.DegenerateLineConnectedVertexMissing,
+  9: ObstructionCode.HullStartNotVisible,
+  10: ObstructionCode.OuterRangeDidNotTerminate,
+  11: ObstructionCode.OuterRangeContainsInnerEdge,
+  12: ObstructionCode.ConstrainedEdgeFlipRefused,
+  13: ObstructionCode.RemovalVertexOutOfRange,
+  14: ObstructionCode.RemovalEdgeOutOfRange,
+  15: ObstructionCode.RemovalFaceOutOfRange,
+  16: ObstructionCode.RemovalFaceCycleDidNotTerminate,
+  17: ObstructionCode.RemovalEmptyTriangulation,
+  18: ObstructionCode.RemovalTwoPointDegreeMismatch,
+  19: ObstructionCode.RemovalCollinearDegreeMismatch,
+  20: ObstructionCode.RemovalBorderTooShort,
+  21: ObstructionCode.RemovalBorderArityMismatch,
+  22: ObstructionCode.RemovalOutgoingCycleDidNotTerminate,
+  23: ObstructionCode.CircleSweepHullEmpty,
+  24: ObstructionCode.OuterCycleDidNotTerminate,
+  25: ObstructionCode.HierarchyLevelPopulationMismatch,
+  26: ObstructionCode.HierarchyInsertionHandleMismatch,
+  27: ObstructionCode.PointIndexCapacityExhausted,
+  28: ObstructionCode.RefinementMinimumAngleNotFinite,
+  29: ObstructionCode.RefinementMinimumAngleOutOfRange,
+  30: ObstructionCode.RefinementMinimumAngleDerivedRatioNotFinite,
+  31: ObstructionCode.RefinementMaximumAdditionalVerticesNegative,
+  32: ObstructionCode.RefinementMinimumAreaNotFinite,
+  33: ObstructionCode.RefinementMinimumAreaNegative,
+  34: ObstructionCode.RefinementMaximumAreaNotFinite,
+  35: ObstructionCode.RefinementMaximumAreaNotPositive,
+  36: ObstructionCode.RefinementMaximumRadiusEdgeRatioNotFinite,
+  37: ObstructionCode.RefinementMaximumRadiusEdgeRatioNotPositive,
+  38: ObstructionCode.RefinementMinimumAreaExceedsMaximum,
+  39: ObstructionCode.RefinementSeedFaceNotActive,
+  40: ObstructionCode.RefinementDomainInterfaceEdgeNotActive,
+  41: ObstructionCode.RefinementDomainInterfaceMissing,
+  42: ObstructionCode.RefinementDomainInterfaceExtraneous,
+  43: ObstructionCode.RefinementDomainTopologyChanged,
+  44: ObstructionCode.RefinementDomainRequiresConvexHullPreservation,
+  45: ObstructionCode.RefinementDomainRequiresConstraintPreservation,
+  46: ObstructionCode.RefinementDomainForbidsOuterFaceExclusion,
+  47: ObstructionCode.RefinementDomainWouldCrossInterface,
+  48: ObstructionCode.RefinementDomainWouldRewriteProtectedFace,
+  49: ObstructionCode.RefinementDomainProtectedFaceChanged,
+  50: ObstructionCode.CapacityExceeded,
+  51: ObstructionCode.HalfEdgeCapacityExceeded,
+  52: ObstructionCode.FaceCapacityExceeded,
+  53: ObstructionCode.PayloadStorageFailure,
+  54: ObstructionCode.CoordinatePayloadCountMismatch,
+  55: ObstructionCode.CircleSweepRequiresDenseStorage,
+  56: ObstructionCode.SeamFrontierUnavailable,
+  57: ObstructionCode.RefinementDomainRequiresFiniteVertexBudget,
+  58: ObstructionCode.SeamSourceEdgeRequiresFlip,
+  59: ObstructionCode.SeamProtectedSourceFaceChanged,
+  60: ObstructionCode.RefinementMaximumEdgeLengthNotFinite,
+  61: ObstructionCode.RefinementMaximumEdgeLengthNotPositive,
+  62: ObstructionCode.RefinementOversizedEdge,
+  63: ObstructionCode.RefinementSeamBridgeBudgetExceeded,
+  64: ObstructionCode.RefinementSeamBridgeMidpointCollapsed,
+  65: ObstructionCode.BoundarySplitRequiresBoundaryEdge,
+  66: ObstructionCode.RefinementDomainInterfaceOppositeFaceNotPermitted,
+  100: ObstructionCode.NullPointer,
+  101: ObstructionCode.CountOverflow,
+  102: ObstructionCode.BufferTooSmall,
+  103: ObstructionCode.RuntimeFailure,
+  200: ObstructionCode.RegionLayoutInvalid,
+  201: ObstructionCode.RegionValidationFailed,
+  202: ObstructionCode.OverlayFailed,
+  203: ObstructionCode.RegionPublicationFailed,
+  204: ObstructionCode.ValuationFailed,
+  205: ObstructionCode.MinkowskiFailed,
+  206: ObstructionCode.RegionProjectionFailed,
+};
+export function decodeObstructionCode(value: number): ObstructionCode | undefined {
+  return obstructionCodeByWire[value];
+}
+
+export const CoordinateError = {
+  None: "none",
+  Nan: "nan",
+  Infinite: "infinite",
+  TooSmall: "too-small",
+  TooLarge: "too-large",
+} as const;
+export type CoordinateError = (typeof CoordinateError)[keyof typeof CoordinateError];
+const coordinateErrorByWire: Readonly<Record<number, CoordinateError>> = {
+  0: CoordinateError.None,
+  1: CoordinateError.Nan,
+  2: CoordinateError.Infinite,
+  3: CoordinateError.TooSmall,
+  4: CoordinateError.TooLarge,
+};
+export function decodeCoordinateError(value: number): CoordinateError | undefined {
+  return coordinateErrorByWire[value];
+}
+
+export const RegionLocation = {
+  Exterior: "exterior",
+  Boundary: "boundary",
+  Interior: "interior",
+} as const;
+export type RegionLocation = (typeof RegionLocation)[keyof typeof RegionLocation];
+const regionLocationByWire: Readonly<Record<number, RegionLocation>> = {
+  0: RegionLocation.Exterior,
+  1: RegionLocation.Boundary,
+  2: RegionLocation.Interior,
+};
+export function decodeRegionLocation(value: number): RegionLocation | undefined {
+  return regionLocationByWire[value];
+}
+
+export const MinkowskiOperation = {
+  Addition: "addition",
+  Erosion: "erosion",
+  Opening: "opening",
+  Closing: "closing",
+} as const;
+export type MinkowskiOperation = (typeof MinkowskiOperation)[keyof typeof MinkowskiOperation];
+const minkowskiOperationByWire: Readonly<Record<number, MinkowskiOperation>> = {
+  0: MinkowskiOperation.Addition,
+  1: MinkowskiOperation.Erosion,
+  2: MinkowskiOperation.Opening,
+  3: MinkowskiOperation.Closing,
+};
+export function decodeMinkowskiOperation(value: number): MinkowskiOperation | undefined {
+  return minkowskiOperationByWire[value];
+}
diff --git a/ffi/bindings/typescript/test/binding.test.ts b/ffi/bindings/typescript/test/binding.test.ts
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/test/binding.test.ts
@@ -0,0 +1,136 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  AbiStatus,
+  CoordinateError,
+  MinkowskiOperation,
+  ObstructionCode,
+  RegionLocation,
+  formatMoonlightFailure,
+  loadMoonlight,
+  type Moonlight,
+  type Result,
+} from "../src/index.js";
+
+function expectSuccess<Value>(result: Result<Value>): Value {
+  if (!result.ok) {
+    assert.fail(formatMoonlightFailure(result.error));
+  }
+  return result.value;
+}
+
+function libraryPath(): string {
+  const path = process.env.MOONLIGHT_TRIANGULATION_LIBRARY;
+  assert.notEqual(path, undefined, "MOONLIGHT_TRIANGULATION_LIBRARY is required");
+  return path ?? "";
+}
+
+function runtime(): Moonlight {
+  return expectSuccess(loadMoonlight({ kind: "path", path: libraryPath() }));
+}
+
+test("immutable site-set algebra and dense projection", () => {
+  const engine = runtime();
+  using left = expectSuccess(engine.delaunay([[0, 0], [2, 0], [0, 2], [2, 2]]));
+  using right = expectSuccess(engine.delaunay([[2, 0], [4, 0], [2, 2], [4, 2]]));
+  using union = expectSuccess(left.siteUnion(right));
+  using intersection = expectSuccess(left.siteIntersection(right));
+  using difference = expectSuccess(left.siteDifference(right));
+  using symmetric = expectSuccess(left.siteSymmetricDifference(right));
+  using extended = expectSuccess(left.insertMany([[1, 1], [3, 1]]));
+
+  assert.equal(expectSuccess(left.vertexCount()), 4);
+  assert.equal(expectSuccess(union.vertexCount()), 6);
+  assert.equal(expectSuccess(intersection.vertexCount()), 2);
+  assert.equal(expectSuccess(difference.vertexCount()), 2);
+  assert.equal(expectSuccess(symmetric.vertexCount()), 4);
+  assert.equal(expectSuccess(extended.vertexCount()), 6);
+  assert.equal(expectSuccess(left.vertices()).length, expectSuccess(left.vertexCount()));
+  assert.equal(expectSuccess(left.triangles()).length, expectSuccess(left.triangleCount()));
+
+});
+
+test("exact regions expose Boolean, valuation, location, and all morphology operations", () => {
+  const engine = runtime();
+  using left = expectSuccess(engine.region([{ outer: [[0, 0], [2, 0], [2, 2], [0, 2]] }]));
+  using right = expectSuccess(engine.region([{ outer: [[1, 0], [3, 0], [3, 2], [1, 2]] }]));
+  using intersection = expectSuccess(left.intersection(right));
+  using symmetric = expectSuccess(left.symmetricDifference(right));
+  using kernel = expectSuccess(
+    engine.structuringElement([[-0.25, -0.25], [0.25, -0.25], [0.25, 0.25], [-0.25, 0.25]]),
+  );
+  const sum = expectSuccess(left.minkowskiSum(right));
+  const offset = expectSuccess(left.offset(kernel));
+  const inset = expectSuccess(left.inset(kernel));
+  const opened = expectSuccess(left.open(kernel));
+  const closed = expectSuccess(left.close(kernel));
+  using sumRegion = sum.region;
+  using offsetRegion = offset.region;
+  using insetRegion = inset.region;
+  using openedRegion = opened.region;
+  using closedRegion = closed.region;
+
+  assert.equal(expectSuccess(left.components()).length, 1);
+  assert.equal(expectSuccess(left.components())[0]?.outer.length, 4);
+  assert.deepEqual(expectSuccess(intersection.valuations()).area, { numerator: 2n, denominator: 1n });
+  assert.equal(expectSuccess(symmetric.valuations()).eulerCharacteristic, 2n);
+  assert.equal(expectSuccess(left.locate([1, 1])), RegionLocation.Interior);
+  assert.equal(expectSuccess(left.locate([0, 1])), RegionLocation.Boundary);
+  assert.equal(expectSuccess(left.locate([3, 1])), RegionLocation.Exterior);
+  assert.equal(offset.receipt.operation, MinkowskiOperation.Addition);
+  assert.equal(inset.receipt.operation, MinkowskiOperation.Erosion);
+  assert.equal(opened.receipt.operation, MinkowskiOperation.Opening);
+  assert.equal(closed.receipt.operation, MinkowskiOperation.Closing);
+  assert(sum.receipt.generatedPieces >= 1n);
+
+});
+
+test("invalid coordinates return a typed obstruction", () => {
+  const result = runtime().delaunay([[0, 0], [Number.NaN, 1], [1, 0]]);
+  assert.equal(result.ok, false);
+  if (result.ok || result.error.kind !== "abi-obstruction") {
+    throw new Error("expected an ABI obstruction");
+  }
+  assert.equal(result.error.obstruction.status, AbiStatus.GeometryObstruction);
+  assert.equal(result.error.obstruction.code, ObstructionCode.InvalidCoordinate);
+  assert.equal(result.error.obstruction.coordinateError, CoordinateError.Nan);
+  assert.equal(result.error.obstruction.inputIndex, 1n);
+});
+
+test("materialized coordinates own native counts", () => {
+  const points = new Proxy<readonly (readonly [number, number])[]>([[0, 0]], {
+    get(target, property, receiver) {
+      return property === "length" ? 1_000 : Reflect.get(target, property, receiver);
+    },
+  });
+  using mesh = expectSuccess(runtime().delaunay(points));
+  assert.equal(expectSuccess(mesh.vertexCount()), 1);
+});
+
+test("loading, runtime descent, and disposal are explicit", () => {
+  const unavailable = loadMoonlight({ kind: "path", path: "" });
+  assert.equal(unavailable.ok, false);
+  if (!unavailable.ok) {
+    assert.equal(unavailable.error.kind, "native-library-unavailable");
+  }
+
+  const firstRuntime = runtime();
+  const secondRuntime = runtime();
+  const first = expectSuccess(firstRuntime.delaunay([[0, 0]]));
+  const second = expectSuccess(secondRuntime.delaunay([[1, 1]]));
+  const mixed = first.siteUnion(second);
+  assert.equal(mixed.ok, false);
+  if (!mixed.ok) {
+    assert.equal(mixed.error.kind, "foreign-runtime");
+  }
+
+  first[Symbol.dispose]();
+  assert.equal(first.disposed, true);
+  const afterDispose = first.vertexCount();
+  assert.equal(afterDispose.ok, false);
+  if (!afterDispose.ok) {
+    assert.equal(afterDispose.error.kind, "disposed-resource");
+  }
+  second.dispose();
+});
diff --git a/ffi/bindings/typescript/tsconfig.json b/ffi/bindings/typescript/tsconfig.json
new file mode 100644
--- /dev/null
+++ b/ffi/bindings/typescript/tsconfig.json
@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "target": "ES2023",
+    "lib": [
+      "ES2023",
+      "ESNext.Disposable"
+    ],
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "strict": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "noUncheckedIndexedAccess": true,
+    "exactOptionalPropertyTypes": true,
+    "types": [
+      "node"
+    ],
+    "declaration": true,
+    "stripInternal": true,
+    "rootDir": ".",
+    "outDir": "dist",
+    "skipLibCheck": false
+  },
+  "include": [
+    "src/**/*.ts",
+    "test/**/*.ts"
+  ]
+}
diff --git a/ffi/cbits/moonlight-triangulation.def b/ffi/cbits/moonlight-triangulation.def
new file mode 100644
--- /dev/null
+++ b/ffi/cbits/moonlight-triangulation.def
@@ -0,0 +1,35 @@
+; Generated from Moonlight.Triangulation.Foreign.Contract.
+; Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+LIBRARY moonlight-triangulation-c
+EXPORTS
+  ml_abi_version
+  ml_runtime_initialize
+  ml_delaunay_f64
+  ml_mesh_insert_many_f64
+  ml_mesh_site_union
+  ml_mesh_site_intersection
+  ml_mesh_site_difference
+  ml_mesh_site_symmetric_difference
+  ml_mesh_vertex_count
+  ml_mesh_triangle_count
+  ml_mesh_copy_vertices_f64
+  ml_mesh_copy_triangles_u32
+  ml_mesh_free
+  ml_region_create_f64
+  ml_region_counts
+  ml_region_copy_f64
+  ml_region_union
+  ml_region_intersection
+  ml_region_difference
+  ml_region_symmetric_difference
+  ml_region_locate_point_f64
+  ml_region_measure
+  ml_region_free
+  ml_structuring_element_create_f64
+  ml_structuring_element_free
+  ml_region_minkowski_sum
+  ml_region_offset
+  ml_region_inset
+  ml_region_open
+  ml_region_close
diff --git a/ffi/cbits/moonlight_runtime.c b/ffi/cbits/moonlight_runtime.c
new file mode 100644
--- /dev/null
+++ b/ffi/cbits/moonlight_runtime.c
@@ -0,0 +1,48 @@
+#include "moonlight_triangulation.h"
+#include "HsFFI.h"
+
+#if defined(_WIN32)
+#include <windows.h>
+
+static INIT_ONCE moonlight_runtime_once = INIT_ONCE_STATIC_INIT;
+
+static BOOL CALLBACK moonlight_initialize_runtime(PINIT_ONCE once, PVOID parameter, PVOID *context) {
+  int argc = 1;
+  char program_name[] = "moonlight-triangulation";
+  char *argv[] = {program_name, NULL};
+  char **argv_pointer = argv;
+  (void)once;
+  (void)parameter;
+  (void)context;
+  hs_init(&argc, &argv_pointer);
+  return TRUE;
+}
+
+ML_API ml_status ml_runtime_initialize(void) {
+  return InitOnceExecuteOnce(&moonlight_runtime_once, moonlight_initialize_runtime, NULL, NULL)
+    ? ML_STATUS_OK
+    : ML_STATUS_RUNTIME_FAILURE;
+}
+#else
+#include <pthread.h>
+
+static pthread_once_t moonlight_runtime_once = PTHREAD_ONCE_INIT;
+
+static void moonlight_initialize_runtime(void) {
+  int argc = 1;
+  char program_name[] = "moonlight-triangulation";
+  char *argv[] = {program_name, NULL};
+  char **argv_pointer = argv;
+  hs_init(&argc, &argv_pointer);
+}
+
+ML_API ml_status ml_runtime_initialize(void) {
+  return pthread_once(&moonlight_runtime_once, moonlight_initialize_runtime) == 0
+    ? ML_STATUS_OK
+    : ML_STATUS_RUNTIME_FAILURE;
+}
+#endif
+
+ML_API uint32_t ml_abi_version(void) {
+  return ML_ABI_VERSION;
+}
diff --git a/ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs b/ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs
new file mode 100644
--- /dev/null
+++ b/ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs
@@ -0,0 +1,80 @@
+-- Generated from Moonlight.Triangulation.Foreign.Contract.
+-- Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
+
+module Moonlight.Triangulation.Foreign.Exports where
+
+import Data.Int (Int64)
+import Data.Word (Word32)
+import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))
+import Foreign.Ptr (Ptr)
+import Moonlight.Triangulation.Foreign.Contract
+  ( CMesh
+  , CMinkowskiReceipt
+  , CObstruction
+  , CRegion
+  , CStructuringElement
+  )
+import qualified Moonlight.Triangulation.Foreign.Mesh as Mesh
+import qualified Moonlight.Triangulation.Foreign.Morphology as Morphology
+import qualified Moonlight.Triangulation.Foreign.Region as Region
+
+delaunayF64 = Mesh.delaunayF64
+meshInsertManyF64 = Mesh.meshInsertManyF64
+meshSiteUnion = Mesh.meshSiteUnion
+meshSiteIntersection = Mesh.meshSiteIntersection
+meshSiteDifference = Mesh.meshSiteDifference
+meshSiteSymmetricDifference = Mesh.meshSiteSymmetricDifference
+meshVertexCount = Mesh.meshVertexCount
+meshTriangleCount = Mesh.meshTriangleCount
+meshCopyVerticesF64 = Mesh.meshCopyVerticesF64
+meshCopyTrianglesU32 = Mesh.meshCopyTrianglesU32
+meshFree = Mesh.meshFree
+regionCreateF64 = Region.regionCreateF64
+regionCounts = Region.regionCounts
+regionCopyF64 = Region.regionCopyF64
+regionUnion = Region.regionUnion
+regionIntersection = Region.regionIntersection
+regionDifference = Region.regionDifference
+regionSymmetricDifference = Region.regionSymmetricDifference
+regionLocatePointF64 = Region.regionLocatePointF64
+regionMeasure = Region.regionMeasure
+regionFree = Region.regionFree
+structuringElementCreateF64 = Morphology.structuringElementCreateF64
+structuringElementFree = Morphology.structuringElementFree
+regionMinkowskiSum = Morphology.regionMinkowskiSum
+regionOffset = Morphology.regionOffset
+regionInset = Morphology.regionInset
+regionOpen = Morphology.regionOpen
+regionClose = Morphology.regionClose
+
+foreign export ccall "ml_delaunay_f64" delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_insert_many_f64" meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_union" meshSiteUnion :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_intersection" meshSiteIntersection :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_difference" meshSiteDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_symmetric_difference" meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_vertex_count" meshVertexCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_triangle_count" meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_copy_vertices_f64" meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_copy_triangles_u32" meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_free" meshFree :: Ptr CMesh -> IO ()
+foreign export ccall "ml_region_create_f64" regionCreateF64 :: Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_counts" regionCounts :: Ptr CRegion -> Ptr CSize -> Ptr CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_copy_f64" regionCopyF64 :: Ptr CRegion -> Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_union" regionUnion :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_intersection" regionIntersection :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_difference" regionDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_symmetric_difference" regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_locate_point_f64" regionLocatePointF64 :: Ptr CRegion -> CDouble -> CDouble -> Ptr CUInt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_measure" regionMeasure :: Ptr CRegion -> Ptr Int64 -> Ptr CChar -> CSize -> Ptr CSize -> Ptr CDouble -> Ptr CDouble -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_free" regionFree :: Ptr CRegion -> IO ()
+foreign export ccall "ml_structuring_element_create_f64" structuringElementCreateF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CStructuringElement) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_structuring_element_free" structuringElementFree :: Ptr CStructuringElement -> IO ()
+foreign export ccall "ml_region_minkowski_sum" regionMinkowskiSum :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_offset" regionOffset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_inset" regionInset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_open" regionOpen :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_close" regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
diff --git a/ffi/generator/Main.hs b/ffi/generator/Main.hs
new file mode 100644
--- /dev/null
+++ b/ffi/generator/Main.hs
@@ -0,0 +1,116 @@
+module Main (main) where
+
+import Data.Either (partitionEithers)
+import Data.Foldable (traverse_)
+import Data.Bifunctor (first)
+import Moonlight.Triangulation.Foreign.Contract.Render
+  ( GeneratedFile (..)
+  , generatedFiles
+  )
+import System.Directory (createDirectoryIfMissing, doesFileExist)
+import System.Environment (getArgs)
+import System.Exit (exitFailure)
+import System.FilePath ((</>), takeDirectory)
+import System.IO (readFile')
+import System.IO.Error (ioeGetErrorString, tryIOError)
+
+data Command
+  = WriteGenerated !FilePath
+  | CheckGenerated !FilePath
+  deriving stock (Eq, Show)
+
+data GeneratorObstruction
+  = InvalidArguments
+  | GeneratedFileMissing !FilePath
+  | GeneratedFileStale !FilePath
+  | GeneratedFileReadFailed !FilePath !String
+  | GeneratedFileWriteFailed !FilePath !String
+  deriving stock (Eq, Show)
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  case parseCommand arguments of
+    Left obstruction -> reportObstructions [obstruction]
+    Right command -> runCommand command >>= either reportObstructions (const (pure ()))
+
+parseCommand :: [String] -> Either GeneratorObstruction Command
+parseCommand arguments =
+  case arguments of
+    ["write", packageRoot] -> Right (WriteGenerated packageRoot)
+    ["check", packageRoot] -> Right (CheckGenerated packageRoot)
+    _ -> Left InvalidArguments
+
+runCommand :: Command -> IO (Either [GeneratorObstruction] ())
+runCommand command =
+  case command of
+    WriteGenerated packageRoot -> do
+      outcomes <- traverse (writeGeneratedFile packageRoot) generatedFiles
+      pure (collectObstructions outcomes)
+    CheckGenerated packageRoot -> do
+      comparisons <- traverse (checkGeneratedFile packageRoot) generatedFiles
+      pure (collectObstructions comparisons)
+
+collectObstructions :: [Either GeneratorObstruction ()] -> Either [GeneratorObstruction] ()
+collectObstructions outcomes =
+  case partitionEithers outcomes of
+    ([], _) -> Right ()
+    (obstructions, _) -> Left obstructions
+
+writeGeneratedFile :: FilePath -> GeneratedFile -> IO (Either GeneratorObstruction ())
+writeGeneratedFile packageRoot generated@GeneratedFile {generatedFilePath, generatedFileContents} = do
+  let destination = packageRoot </> generatedFilePath
+  comparison <- checkGeneratedFile packageRoot generated
+  case comparison of
+    Right () -> putStrLn ("current " <> destination) >> pure (Right ())
+    Left (GeneratedFileMissing _) -> replaceGeneratedFile destination generatedFileContents
+    Left (GeneratedFileStale _) -> replaceGeneratedFile destination generatedFileContents
+    Left obstruction -> pure (Left obstruction)
+
+replaceGeneratedFile :: FilePath -> String -> IO (Either GeneratorObstruction ())
+replaceGeneratedFile destination contents = do
+  outcome <-
+    captureFileFailure GeneratedFileWriteFailed destination $ do
+      createDirectoryIfMissing True (takeDirectory destination)
+      writeFile destination contents
+  case outcome of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> putStrLn ("wrote " <> destination) >> pure (Right ())
+
+checkGeneratedFile :: FilePath -> GeneratedFile -> IO (Either GeneratorObstruction ())
+checkGeneratedFile packageRoot GeneratedFile {generatedFilePath, generatedFileContents} = do
+  let destination = packageRoot </> generatedFilePath
+  existence <- captureFileFailure GeneratedFileReadFailed destination (doesFileExist destination)
+  case existence of
+    Left obstruction -> pure (Left obstruction)
+    Right False -> pure (Left (GeneratedFileMissing destination))
+    Right True -> do
+      observed <- captureFileFailure GeneratedFileReadFailed destination (readFile' destination)
+      pure $ do
+        contents <- observed
+        if contents == generatedFileContents
+          then Right ()
+          else Left (GeneratedFileStale destination)
+
+captureFileFailure
+  :: (FilePath -> String -> GeneratorObstruction)
+  -> FilePath
+  -> IO value
+  -> IO (Either GeneratorObstruction value)
+captureFileFailure obstruction path action =
+  first (obstruction path . ioeGetErrorString) <$> tryIOError action
+
+reportObstructions :: [GeneratorObstruction] -> IO ()
+reportObstructions obstructions = do
+  traverse_ (putStrLn . renderObstruction) obstructions
+  exitFailure
+
+renderObstruction :: GeneratorObstruction -> String
+renderObstruction obstruction =
+  case obstruction of
+    InvalidArguments ->
+      "usage: moonlight-triangulation-ffi-contract (write|check) PACKAGE_ROOT"
+    GeneratedFileMissing path -> "missing generated file: " <> path
+    GeneratedFileStale path -> "stale generated file: " <> path
+    GeneratedFileReadFailed path message -> "cannot read generated file " <> path <> ": " <> message
+    GeneratedFileWriteFailed path message -> "cannot write generated file " <> path <> ": " <> message
diff --git a/ffi/include/moonlight_triangulation.h b/ffi/include/moonlight_triangulation.h
new file mode 100644
--- /dev/null
+++ b/ffi/include/moonlight_triangulation.h
@@ -0,0 +1,204 @@
+// Generated from Moonlight.Triangulation.Foreign.Contract.
+// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+
+#ifndef MOONLIGHT_TRIANGULATION_H
+#define MOONLIGHT_TRIANGULATION_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#if defined(_WIN32)
+#define ML_API __declspec(dllexport)
+#else
+#define ML_API __attribute__((visibility("default")))
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define ML_ABI_VERSION 2u
+
+typedef struct ml_mesh ml_mesh;
+typedef struct ml_region ml_region;
+typedef struct ml_structuring_element ml_structuring_element;
+
+typedef uint32_t ml_status;
+typedef uint32_t ml_obstruction_code;
+typedef uint32_t ml_coordinate_error;
+typedef uint32_t ml_region_location;
+typedef uint32_t ml_minkowski_operation;
+
+enum {
+  ML_STATUS_OK = 0,
+  ML_STATUS_NULL_POINTER = 1,
+  ML_STATUS_COUNT_OVERFLOW = 2,
+  ML_STATUS_BUFFER_TOO_SMALL = 3,
+  ML_STATUS_GEOMETRY_OBSTRUCTION = 4,
+  ML_STATUS_RUNTIME_FAILURE = 5
+};
+
+enum {
+  ML_OBSTRUCTION_NONE = 0,
+  ML_OBSTRUCTION_INVALID_COORDINATE = 1,
+  ML_OBSTRUCTION_POINT_LOCATION_FAILED = 2,
+  ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED = 3,
+  ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID = 4,
+  ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX = 5,
+  ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING = 6,
+  ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING = 7,
+  ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING = 8,
+  ML_OBSTRUCTION_HULL_START_NOT_VISIBLE = 9,
+  ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE = 10,
+  ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE = 11,
+  ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED = 12,
+  ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE = 13,
+  ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE = 14,
+  ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE = 15,
+  ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE = 16,
+  ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION = 17,
+  ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH = 18,
+  ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH = 19,
+  ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT = 20,
+  ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH = 21,
+  ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE = 22,
+  ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY = 23,
+  ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE = 24,
+  ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH = 25,
+  ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH = 26,
+  ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED = 27,
+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE = 28,
+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE = 29,
+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE = 30,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE = 31,
+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE = 32,
+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE = 33,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE = 34,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE = 35,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE = 36,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE = 37,
+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM = 38,
+  ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE = 39,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE = 40,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING = 41,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS = 42,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED = 43,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION = 44,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION = 45,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION = 46,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE = 47,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE = 48,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED = 49,
+  ML_OBSTRUCTION_CAPACITY_EXCEEDED = 50,
+  ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED = 51,
+  ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED = 52,
+  ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE = 53,
+  ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH = 54,
+  ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE = 55,
+  ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE = 56,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET = 57,
+  ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP = 58,
+  ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED = 59,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE = 60,
+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE = 61,
+  ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE = 62,
+  ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED = 63,
+  ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED = 64,
+  ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE = 65,
+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED = 66,
+  ML_OBSTRUCTION_NULL_POINTER = 100,
+  ML_OBSTRUCTION_COUNT_OVERFLOW = 101,
+  ML_OBSTRUCTION_BUFFER_TOO_SMALL = 102,
+  ML_OBSTRUCTION_RUNTIME_FAILURE = 103,
+  ML_OBSTRUCTION_REGION_LAYOUT_INVALID = 200,
+  ML_OBSTRUCTION_REGION_VALIDATION_FAILED = 201,
+  ML_OBSTRUCTION_OVERLAY_FAILED = 202,
+  ML_OBSTRUCTION_REGION_PUBLICATION_FAILED = 203,
+  ML_OBSTRUCTION_VALUATION_FAILED = 204,
+  ML_OBSTRUCTION_MINKOWSKI_FAILED = 205,
+  ML_OBSTRUCTION_REGION_PROJECTION_FAILED = 206
+};
+
+enum {
+  ML_COORDINATE_ERROR_NONE = 0,
+  ML_COORDINATE_ERROR_NAN = 1,
+  ML_COORDINATE_ERROR_INFINITE = 2,
+  ML_COORDINATE_ERROR_TOO_SMALL = 3,
+  ML_COORDINATE_ERROR_TOO_LARGE = 4
+};
+
+enum {
+  ML_REGION_EXTERIOR = 0,
+  ML_REGION_BOUNDARY = 1,
+  ML_REGION_INTERIOR = 2
+};
+
+enum {
+  ML_MINKOWSKI_ADDITION = 0,
+  ML_MINKOWSKI_EROSION = 1,
+  ML_MINKOWSKI_OPENING = 2,
+  ML_MINKOWSKI_CLOSING = 3
+};
+
+typedef struct ml_obstruction {
+  uint32_t code;
+  uint32_t coordinate_error;
+  uint64_t input_index;
+  uint64_t first_index;
+  uint64_t second_index;
+  double first_value;
+  double second_value;
+  double point_x;
+  double point_y;
+  char message[256];
+} ml_obstruction;
+
+typedef struct ml_minkowski_receipt {
+  uint32_t operation;
+  uint32_t reserved;
+  uint64_t input_components;
+  uint64_t convex_pieces;
+  uint64_t generated_pieces;
+  uint64_t generated_convolution_edges;
+  uint64_t overlay_passes;
+  uint64_t exact_crossings;
+  uint64_t output_cells;
+  uint64_t exact_coordinate_bit_growth;
+} ml_minkowski_receipt;
+
+ML_API uint32_t ml_abi_version(void);
+ML_API ml_status ml_runtime_initialize(void);
+ML_API ml_status ml_delaunay_f64(const double * coordinates, size_t point_count, ml_mesh ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_insert_many_f64(const ml_mesh * mesh, const double * coordinates, size_t point_count, ml_mesh ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_site_union(const ml_mesh * left, const ml_mesh * right, ml_mesh ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_site_intersection(const ml_mesh * left, const ml_mesh * right, ml_mesh ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_site_difference(const ml_mesh * left, const ml_mesh * right, ml_mesh ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_site_symmetric_difference(const ml_mesh * left, const ml_mesh * right, ml_mesh ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_vertex_count(const ml_mesh * mesh, size_t * count, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_triangle_count(const ml_mesh * mesh, size_t * count, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_copy_vertices_f64(const ml_mesh * mesh, double * coordinates, size_t point_capacity, size_t * points_written, ml_obstruction * obstruction);
+ML_API ml_status ml_mesh_copy_triangles_u32(const ml_mesh * mesh, uint32_t * triangles, size_t triangle_capacity, size_t * triangles_written, ml_obstruction * obstruction);
+ML_API void ml_mesh_free(ml_mesh * mesh);
+ML_API ml_status ml_region_create_f64(const double * coordinates, size_t point_count, const size_t * loop_point_counts, size_t loop_count, const size_t * component_loop_counts, size_t component_count, ml_region ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_region_counts(const ml_region * region, size_t * component_count, size_t * loop_count, size_t * point_count, ml_obstruction * obstruction);
+ML_API ml_status ml_region_copy_f64(const ml_region * region, double * coordinates, size_t point_capacity, size_t * loop_point_offsets, size_t loop_offset_capacity, size_t * component_loop_offsets, size_t component_offset_capacity, ml_obstruction * obstruction);
+ML_API ml_status ml_region_union(const ml_region * left, const ml_region * right, ml_region ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_region_intersection(const ml_region * left, const ml_region * right, ml_region ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_region_difference(const ml_region * left, const ml_region * right, ml_region ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_region_symmetric_difference(const ml_region * left, const ml_region * right, ml_region ** result, ml_obstruction * obstruction);
+ML_API ml_status ml_region_locate_point_f64(const ml_region * region, double x, double y, ml_region_location * location, ml_obstruction * obstruction);
+ML_API ml_status ml_region_measure(const ml_region * region, int64_t * euler_characteristic, char * area_ratio_utf8, size_t area_capacity, size_t * area_bytes_written, double * perimeter_lower, double * perimeter_upper, ml_obstruction * obstruction);
+ML_API void ml_region_free(ml_region * region);
+ML_API ml_status ml_structuring_element_create_f64(const double * coordinates, size_t point_count, ml_structuring_element ** result, ml_obstruction * obstruction);
+ML_API void ml_structuring_element_free(ml_structuring_element * element);
+ML_API ml_status ml_region_minkowski_sum(const ml_region * left, const ml_region * right, ml_region ** result, ml_minkowski_receipt * receipt, ml_obstruction * obstruction);
+ML_API ml_status ml_region_offset(const ml_structuring_element * element, const ml_region * region, ml_region ** result, ml_minkowski_receipt * receipt, ml_obstruction * obstruction);
+ML_API ml_status ml_region_inset(const ml_structuring_element * element, const ml_region * region, ml_region ** result, ml_minkowski_receipt * receipt, ml_obstruction * obstruction);
+ML_API ml_status ml_region_open(const ml_structuring_element * element, const ml_region * region, ml_region ** result, ml_minkowski_receipt * receipt, ml_obstruction * obstruction);
+ML_API ml_status ml_region_close(const ml_structuring_element * element, const ml_region * region, ml_region ** result, ml_minkowski_receipt * receipt, ml_obstruction * obstruction);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/moonlight_triangulation.h b/include/moonlight_triangulation.h
deleted file mode 100644
--- a/include/moonlight_triangulation.h
+++ /dev/null
@@ -1,236 +0,0 @@
-#ifndef MOONLIGHT_TRIANGULATION_H
-#define MOONLIGHT_TRIANGULATION_H
-
-#include <stddef.h>
-#include <stdint.h>
-
-#if defined(_WIN32)
-#define ML_API __declspec(dllexport)
-#else
-#define ML_API __attribute__((visibility("default")))
-#endif
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct ml_mesh ml_mesh;
-typedef struct ml_region ml_region;
-typedef struct ml_structuring_element ml_structuring_element;
-typedef uint32_t ml_status;
-typedef uint32_t ml_obstruction_code;
-typedef uint32_t ml_coordinate_error;
-typedef uint32_t ml_region_location;
-typedef uint32_t ml_minkowski_operation;
-
-enum {
-  ML_STATUS_OK = 0,
-  ML_STATUS_NULL_POINTER = 1,
-  ML_STATUS_COUNT_OVERFLOW = 2,
-  ML_STATUS_BUFFER_TOO_SMALL = 3,
-  ML_STATUS_GEOMETRY_OBSTRUCTION = 4,
-  ML_STATUS_RUNTIME_FAILURE = 5
-};
-
-enum {
-  ML_OBSTRUCTION_NONE = 0,
-  ML_OBSTRUCTION_INVALID_COORDINATE = 1,
-  ML_OBSTRUCTION_POINT_LOCATION_FAILED = 2,
-  ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED = 3,
-  ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID = 4,
-  ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX = 5,
-  ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING = 6,
-  ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING = 7,
-  ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING = 8,
-  ML_OBSTRUCTION_HULL_START_NOT_VISIBLE = 9,
-  ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE = 10,
-  ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE = 11,
-  ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED = 12,
-  ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE = 13,
-  ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE = 14,
-  ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE = 15,
-  ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE = 16,
-  ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION = 17,
-  ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH = 18,
-  ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH = 19,
-  ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT = 20,
-  ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH = 21,
-  ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE = 22,
-  ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY = 23,
-  ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE = 24,
-  ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH = 25,
-  ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH = 26,
-  ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED = 27,
-  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE = 28,
-  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE = 29,
-  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE = 30,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE = 31,
-  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE = 32,
-  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE = 33,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE = 34,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE = 35,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE = 36,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE = 37,
-  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM = 38,
-  ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE = 39,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE = 40,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING = 41,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS = 42,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED = 43,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION = 44,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION = 45,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION = 46,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE = 47,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE = 48,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED = 49,
-  ML_OBSTRUCTION_CAPACITY_EXCEEDED = 50,
-  ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED = 51,
-  ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED = 52,
-  ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE = 53,
-  ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH = 54,
-  ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE = 55,
-  ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE = 56,
-  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET = 57,
-  ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP = 58,
-  ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED = 59,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE = 60,
-  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE = 61,
-  ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE = 62,
-  ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED = 63,
-  ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED = 64,
-  ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE = 65,
-  ML_OBSTRUCTION_NULL_POINTER = 100,
-  ML_OBSTRUCTION_COUNT_OVERFLOW = 101,
-  ML_OBSTRUCTION_BUFFER_TOO_SMALL = 102,
-  ML_OBSTRUCTION_RUNTIME_FAILURE = 103,
-  ML_OBSTRUCTION_REGION_LAYOUT_INVALID = 200,
-  ML_OBSTRUCTION_REGION_VALIDATION_FAILED = 201,
-  ML_OBSTRUCTION_OVERLAY_FAILED = 202,
-  ML_OBSTRUCTION_REGION_PUBLICATION_FAILED = 203,
-  ML_OBSTRUCTION_VALUATION_FAILED = 204,
-  ML_OBSTRUCTION_MINKOWSKI_FAILED = 205,
-  ML_OBSTRUCTION_REGION_PROJECTION_FAILED = 206
-};
-
-enum {
-  ML_COORDINATE_ERROR_NONE = 0,
-  ML_COORDINATE_ERROR_NAN = 1,
-  ML_COORDINATE_ERROR_INFINITE = 2,
-  ML_COORDINATE_ERROR_TOO_SMALL = 3,
-  ML_COORDINATE_ERROR_TOO_LARGE = 4
-};
-
-enum {
-  ML_REGION_EXTERIOR = 0,
-  ML_REGION_BOUNDARY = 1,
-  ML_REGION_INTERIOR = 2
-};
-
-enum {
-  ML_MINKOWSKI_ADDITION = 0,
-  ML_MINKOWSKI_EROSION = 1,
-  ML_MINKOWSKI_OPENING = 2,
-  ML_MINKOWSKI_CLOSING = 3
-};
-
-typedef struct ml_obstruction {
-  uint32_t code;
-  uint32_t coordinate_error;
-  uint64_t input_index;
-  uint64_t first_index;
-  uint64_t second_index;
-  double first_value;
-  double second_value;
-  double point_x;
-  double point_y;
-  char message[256];
-} ml_obstruction;
-
-typedef struct ml_minkowski_receipt {
-  uint32_t operation;
-  uint32_t reserved;
-  uint64_t input_components;
-  uint64_t convex_pieces;
-  uint64_t generated_pieces;
-  uint64_t generated_convolution_edges;
-  uint64_t overlay_passes;
-  uint64_t exact_crossings;
-  uint64_t output_cells;
-  uint64_t exact_coordinate_bit_growth;
-} ml_minkowski_receipt;
-
-ML_API uint32_t ml_abi_version(void);
-ML_API ml_status ml_runtime_initialize(void);
-
-/* Immutable Delaunay meshes; these operations combine site sets, not regions. */
-ML_API ml_status ml_delaunay_f64(const double *coordinates, size_t point_count, ml_mesh **result, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_insert_many_f64(const ml_mesh *mesh, const double *coordinates, size_t point_count, ml_mesh **result, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_site_union(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_site_intersection(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_site_difference(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_site_symmetric_difference(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_vertex_count(const ml_mesh *mesh, size_t *count, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_triangle_count(const ml_mesh *mesh, size_t *count, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_copy_vertices_f64(const ml_mesh *mesh, double *coordinates, size_t point_capacity, size_t *points_written, ml_obstruction *obstruction);
-ML_API ml_status ml_mesh_copy_triangles_u32(const ml_mesh *mesh, uint32_t *triangles, size_t triangle_capacity, size_t *triangles_written, ml_obstruction *obstruction);
-ML_API void ml_mesh_free(ml_mesh *mesh);
-
-/*
- * Bulk exact-region authoring. loop_point_counts gives the number of points in
- * each loop; component_loop_counts gives the number of loops in each component.
- * Within each component, the first loop is a CCW outer loop and subsequent
- * loops are CW holes.
- */
-ML_API ml_status ml_region_create_f64(
-  const double *coordinates,
-  size_t point_count,
-  const size_t *loop_point_counts,
-  size_t loop_count,
-  const size_t *component_loop_counts,
-  size_t component_count,
-  ml_region **result,
-  ml_obstruction *obstruction
-);
-ML_API ml_status ml_region_counts(const ml_region *region, size_t *component_count, size_t *loop_count, size_t *point_count, ml_obstruction *obstruction);
-/* Capacities count points or offset entries; copied coordinates are a derived binary64 rendering projection. */
-ML_API ml_status ml_region_copy_f64(
-  const ml_region *region,
-  double *coordinates,
-  size_t point_capacity,
-  size_t *loop_point_offsets,
-  size_t loop_offset_capacity,
-  size_t *component_loop_offsets,
-  size_t component_offset_capacity,
-  ml_obstruction *obstruction
-);
-ML_API ml_status ml_region_union(const ml_region *left, const ml_region *right, ml_region **result, ml_obstruction *obstruction);
-ML_API ml_status ml_region_intersection(const ml_region *left, const ml_region *right, ml_region **result, ml_obstruction *obstruction);
-ML_API ml_status ml_region_difference(const ml_region *left, const ml_region *right, ml_region **result, ml_obstruction *obstruction);
-ML_API ml_status ml_region_symmetric_difference(const ml_region *left, const ml_region *right, ml_region **result, ml_obstruction *obstruction);
-ML_API ml_status ml_region_locate_point_f64(const ml_region *region, double x, double y, ml_region_location *location, ml_obstruction *obstruction);
-/* area_bytes_written excludes the trailing NUL; area_capacity includes it. */
-ML_API ml_status ml_region_measure(
-  const ml_region *region,
-  int64_t *euler_characteristic,
-  char *area_ratio_utf8,
-  size_t area_capacity,
-  size_t *area_bytes_written,
-  double *perimeter_lower,
-  double *perimeter_upper,
-  ml_obstruction *obstruction
-);
-ML_API void ml_region_free(ml_region *region);
-
-ML_API ml_status ml_structuring_element_create_f64(const double *coordinates, size_t point_count, ml_structuring_element **result, ml_obstruction *obstruction);
-ML_API void ml_structuring_element_free(ml_structuring_element *element);
-ML_API ml_status ml_region_minkowski_sum(const ml_region *left, const ml_region *right, ml_region **result, ml_minkowski_receipt *receipt, ml_obstruction *obstruction);
-ML_API ml_status ml_region_offset(const ml_structuring_element *element, const ml_region *region, ml_region **result, ml_minkowski_receipt *receipt, ml_obstruction *obstruction);
-ML_API ml_status ml_region_inset(const ml_structuring_element *element, const ml_region *region, ml_region **result, ml_minkowski_receipt *receipt, ml_obstruction *obstruction);
-ML_API ml_status ml_region_open(const ml_structuring_element *element, const ml_region *region, ml_region **result, ml_minkowski_receipt *receipt, ml_obstruction *obstruction);
-ML_API ml_status ml_region_close(const ml_structuring_element *element, const ml_region *region, ml_region **result, ml_minkowski_receipt *receipt, ml_obstruction *obstruction);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/moonlight-triangulation.cabal b/moonlight-triangulation.cabal
--- a/moonlight-triangulation.cabal
+++ b/moonlight-triangulation.cabal
@@ -1,6 +1,6 @@
 cabal-version:       3.4
 name:                moonlight-triangulation
-version:             1.4.0.1
+version:             1.4.0.2
 synopsis:            Delaunay meshes and exact planar-region algebra.
 description:         Delaunay and constrained Delaunay triangulation as a lawful
                      finite-set algebra, together with exact rational planar
@@ -10,15 +10,13 @@
                      union, intersection and difference return triangulations
                      and refinement composes after them rather than replacing
                      them. One structure-of-arrays half-edge mesh carries the
-                     constrained and unconstrained layers, split into public
-                     sublibraries by role: exact binary64 predicates and paged
-                     storage; the finite DCEL with its handle,
-                     iterator, location and validation surface; circle-sweep bulk
-                     load, incremental insertion, removal, conflict-strip
-                     constraint recovery and Ruppert refinement; the Voronoi dual
-                     with natural-neighbour interpolation and Delaunay hierarchy
-                     hints; a bounded-concurrency interpreter for the join
-                     tournament; and a versioned binary serialization surface.
+                     constrained and unconstrained layers. A private finite-DCEL
+                     and construction tower owns its handles, iterators,
+                     circle-sweep build, incremental edits, constraint recovery,
+                     and refinement. Public opt-in sublibraries expose the exact
+                     scalar core, Voronoi and natural-neighbour dual, bounded
+                     concurrent join interpreter, and versioned serialization
+                     surface beside the main construction facade.
                      On GHC 9.14, a public cell-complex component interprets
                      admitted exact cell selections for Homology and Category
                      and lowers exact Delaunay alpha filtrations into persistent
@@ -41,33 +39,61 @@
   README.md
   CHANGELOG.md
   docs/README.md
-  docs/moonlight-triangulation-alpha-eclipse.svg
-  docs/moonlight-triangulation-persistence-rose.svg
-  docs/social/moonlight-triangulation-persistence-rose.gif
-  bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg
+  docs/development.md
+  docs/examples/README.md
+  docs/mesh-publication.md
+  docs/category-observatory/README.md
+  docs/category-observatory/moonlight-triangulation-category-observatory.svg
+  docs/delaunay-compare/README.md
+  docs/delaunay-compare/results/moonlight-delaunay-compare-small.svg
+  docs/persistence-rose/alpha-eclipse.md
+  docs/persistence-rose/README.md
+  docs/persistence-rose/moonlight-triangulation-alpha-eclipse.svg
+  docs/persistence-rose/moonlight-triangulation-persistence-rose.svg
+  docs/persistence-rose/moonlight-triangulation-persistence-rose.gif
 extra-source-files:
   weeder.toml
-  bench/delaunay-compare/README.md
   -- The retained CSV, source receipt, and big-point SVG remain beside the
-  -- benchmark but are not package inputs. The small README projection is the
-  -- sole published chart and is declared as package documentation above.
-  bench/delaunay-compare/rust/Cargo.lock
-  bench/delaunay-compare/rust/Cargo.toml
-  bench/delaunay-compare/rust/src/lib.rs
-  include/moonlight_triangulation.h
-  bindings/README.md
-  bindings/python/pyproject.toml
-  bindings/python/src/moonlight_triangulation/__init__.py
-  bindings/python/tests/test_binding.py
-  bindings/rust/Cargo.toml
-  bindings/rust/build.rs
-  bindings/rust/src/lib.rs
-  bindings/rust/tests/binding.rs
-  bindings/typescript/package.json
-  bindings/typescript/pnpm-workspace.yaml
-  bindings/typescript/tsconfig.json
-  bindings/typescript/src/index.ts
-  bindings/typescript/test/binding.test.ts
+  -- benchmark but are not package inputs. The small documentation projection
+  -- is the sole published chart and is declared above.
+  docs/delaunay-compare/rust/Cargo.lock
+  docs/delaunay-compare/rust/Cargo.toml
+  docs/delaunay-compare/rust/src/lib.rs
+  ffi/include/moonlight_triangulation.h
+  ffi/bindings/README.md
+  ffi/bindings/python/pyproject.toml
+  ffi/bindings/python/src/moonlight_triangulation/__init__.py
+  ffi/bindings/python/src/moonlight_triangulation/_binding.py
+  ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
+  ffi/bindings/python/src/moonlight_triangulation/errors.py
+  ffi/bindings/python/src/moonlight_triangulation/values.py
+  ffi/bindings/python/tests/test_binding.py
+  ffi/bindings/rust/Cargo.toml
+  ffi/bindings/rust/build.rs
+  ffi/bindings/rust/src/binding.rs
+  ffi/bindings/rust/src/error.rs
+  ffi/bindings/rust/src/lib.rs
+  ffi/bindings/rust/src/raw_generated.rs
+  ffi/bindings/rust/src/values.rs
+  ffi/bindings/rust/tests/binding.rs
+  ffi/bindings/typescript/README.md
+  ffi/bindings/typescript/package.json
+  ffi/bindings/typescript/pnpm-workspace.yaml
+  ffi/bindings/typescript/tsconfig.json
+  ffi/bindings/typescript/src/failure.ts
+  ffi/bindings/typescript/src/index.ts
+  ffi/bindings/typescript/src/internal/call.ts
+  ffi/bindings/typescript/src/internal/decode.ts
+  ffi/bindings/typescript/src/internal/native.generated.ts
+  ffi/bindings/typescript/src/internal/resource.ts
+  ffi/bindings/typescript/src/mesh.ts
+  ffi/bindings/typescript/src/moonlight.ts
+  ffi/bindings/typescript/src/morphology.ts
+  ffi/bindings/typescript/src/region.ts
+  ffi/bindings/typescript/src/resource.ts
+  ffi/bindings/typescript/src/values.ts
+  ffi/bindings/typescript/src/wire.generated.ts
+  ffi/bindings/typescript/test/binding.test.ts
 
 source-repository head
   type:     git
@@ -77,7 +103,7 @@
 source-repository this
   type:     git
   location: https://github.com/PaleRoses/moonlight.git
-  tag:      moonlight-triangulation-1.4.0.1
+  tag:      moonlight-triangulation-1.4.0.2
   subdir:   moonlight-triangulation
 
 flag warnings-as-errors
@@ -138,7 +164,7 @@
 
 library dcel
   import: shared-properties
-  visibility: public
+  visibility: private
   hs-source-dirs: src-dcel
   exposed-modules:
     Moonlight.Triangulation.Alpha
@@ -223,7 +249,7 @@
 
 library build
   import: shared-properties
-  visibility: public
+  visibility: private
   hs-source-dirs: src-build
   exposed-modules:
     Moonlight.Triangulation.BulkLoad
@@ -257,7 +283,6 @@
     Moonlight.Triangulation.Internal.Refinement
     Moonlight.Triangulation.Internal.Transaction
     Moonlight.Triangulation.Internal.Join.Plan
-    Moonlight.Triangulation.Internal.Join.Rebuild
     Moonlight.Triangulation.Internal.Join.SiteSet
   build-depends:
     base >= 4.19 && < 5
@@ -333,7 +358,6 @@
   import: shared-properties
   hs-source-dirs:
     src-public
-    src-planar
   exposed-modules:
     Moonlight.Triangulation
     Moonlight.Triangulation.HintGenerator
@@ -360,9 +384,15 @@
 library ffi
   import: shared-properties
   visibility: private
-  hs-source-dirs: src-ffi
+  hs-source-dirs: ffi/abi
   exposed-modules:
-    Moonlight.Triangulation.Foreign.ABI
+    Moonlight.Triangulation.Foreign.Boundary
+    Moonlight.Triangulation.Foreign.Contract
+    Moonlight.Triangulation.Foreign.Contract.Render
+    Moonlight.Triangulation.Foreign.Mesh
+    Moonlight.Triangulation.Foreign.Morphology
+    Moonlight.Triangulation.Foreign.Obstruction
+    Moonlight.Triangulation.Foreign.Region
   build-depends:
     base >= 4.19 && < 5
     , containers >= 0.8 && < 0.9
@@ -374,13 +404,13 @@
 foreign-library moonlight-triangulation-c
   import: shared-properties
   type: native-shared
-  hs-source-dirs: src-capi
+  hs-source-dirs: ffi/exports
   other-modules:
     Moonlight.Triangulation.Foreign.Exports
   c-sources:
-    cbits/moonlight_runtime.c
+    ffi/cbits/moonlight_runtime.c
   include-dirs:
-    include
+    ffi/include
   install-includes:
     moonlight_triangulation.h
   build-depends:
@@ -389,12 +419,22 @@
   ghc-options: -threaded
   if os(windows)
     options: standalone
-    mod-def-file: cbits/moonlight-triangulation.def
+    mod-def-file: ffi/cbits/moonlight-triangulation.def
   else
     extra-libraries: pthread
   if os(linux)
     lib-version-info: 2:0:0
 
+executable moonlight-triangulation-ffi-contract
+  import: shared-properties
+  hs-source-dirs: ffi/generator
+  main-is: Main.hs
+  build-depends:
+    base >= 4.19 && < 5
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , moonlight-triangulation:ffi >= 1.4 && < 1.5
+
 -- ── test slices ──────────────────────────────────────────────────────────────
 
 -- Each slice is an ATOM: a @common@ stanza binding a spec module to the
@@ -513,6 +553,22 @@
     , tasty-hunit >= 0.10 && < 0.11
     , vector >= 0.13 && < 0.14
 
+-- Canonical documentation examples compile exclusively against the public
+-- facade and retain their expected semantic summaries as executable checks.
+test-suite moonlight-triangulation-examples-test
+  import: triangulation-test-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/examples
+    docs/examples
+  other-modules:
+    Moonlight.Triangulation.Example.AlphaBoundary
+    Moonlight.Triangulation.Example.ConstrainedRegion
+    Moonlight.Triangulation.Example.PlanarRegion
+    Moonlight.Triangulation.Example.SetAlgebra
+  build-depends: containers >= 0.8 && < 0.9
+
 -- One pure, source-grounded projection of the alpha, dual, interpolation,
 -- planar-algebra, and persistence owners. The test shares the internal exhibit
 -- module; neither component creates a public visualization API.
@@ -522,7 +578,7 @@
   main-is: Main.hs
   hs-source-dirs:
     test/persistence-rose
-    app/persistence-rose
+    docs/persistence-rose
   other-modules:
     Moonlight.Triangulation.Exhibit.PersistenceRose
   if impl(ghc < 9.14)
@@ -714,7 +770,7 @@
   import: triangulation-delaunay-compare-properties
   ghc-options: -threaded -rtsopts
   main-is: Main.hs
-  hs-source-dirs: bench/delaunay-compare
+  hs-source-dirs: docs/delaunay-compare
   other-modules:
     Moonlight.Triangulation.Bench.DelaunayCompare.Domain
     Moonlight.Triangulation.Bench.DelaunayCompare.Native
@@ -731,7 +787,7 @@
 executable moonlight-triangulation-delaunay-pictures
   import: triangulation-delaunay-compare-properties
   main-is: PictureMain.hs
-  hs-source-dirs: bench/delaunay-compare
+  hs-source-dirs: docs/delaunay-compare
   other-modules:
     Moonlight.Triangulation.Bench.DelaunayCompare.Domain
     Moonlight.Triangulation.Bench.DelaunayCompare.Picture
@@ -744,7 +800,15 @@
 executable moonlight-triangulation-category-observatory-export
   import: shared-properties
   main-is: Main.hs
-  hs-source-dirs: app/category-observatory
+  hs-source-dirs: docs/category-observatory
+  other-modules:
+    Moonlight.Triangulation.Exhibit.CategoryObservatory
+    Moonlight.Triangulation.Exhibit.CategoryObservatory.Category
+    Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry
+    Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
+    Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
+    Moonlight.Triangulation.Exhibit.CategoryObservatory.Nerve
+    Moonlight.Triangulation.Exhibit.CategoryObservatory.Picture
   ghc-options: -Wall -Wcompat
   default-extensions:
     TypeFamilies
@@ -755,6 +819,8 @@
     , base >= 4.22 && < 5
     , bytestring >= 0.12 && < 0.13
     , containers >= 0.6 && < 0.9
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
     , moonlight-category >= 1.1.0.0 && < 1.2
     , moonlight-category:simplicial >= 1.1.0.0 && < 1.2
     , moonlight-homology:cell-category >= 0.1.0.2 && < 0.2
@@ -762,12 +828,13 @@
     , moonlight-triangulation:build
     , moonlight-triangulation:cell-complex
     , moonlight-triangulation:dcel
+    , transformers >= 0.5 && < 0.7
     , vector >= 0.13 && < 0.14
 
 executable moonlight-triangulation-persistence-rose
   import: shared-properties
   main-is: Main.hs
-  hs-source-dirs: app/persistence-rose
+  hs-source-dirs: docs/persistence-rose
   other-modules:
     Moonlight.Triangulation.Exhibit.PersistenceRose
   if impl(ghc < 9.14)
@@ -786,6 +853,93 @@
     , text >= 2.0 && < 2.2
     , transformers >= 0.5 && < 0.7
     , vector >= 0.13 && < 0.14
+
+-- These referent instruments inspect package-internal handles and thaw/freeze
+-- lanes. They belong to the triangulation package rather than reopening its
+-- private construction boundary through a second package.
+executable moonlight-triangulation-spade-referent
+  default-language: GHC2024
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+    -Wpartial-fields
+    -O2
+    -rtsopts
+  default-extensions:
+    BangPatterns
+    LambdaCase
+    NumericUnderscores
+  hs-source-dirs: bench/spade-compare/hs
+  main-is: Main.hs
+  -- Main is the entry and the lane dispatch; every workload, gate and
+  -- diagnostic sits in the domain module that owns it. The domains depend on
+  -- SpadeCompare.Support and on nothing else of each other, so they compile in
+  -- parallel and a lane edit rebuilds one slice.
+  other-modules:
+    Moonlight.Triangulation.Bench.SpadeCompare.Lane
+    Moonlight.Triangulation.Bench.SpadeCompare.Input
+    Moonlight.Triangulation.Bench.SpadeCompare.Timing
+    Moonlight.Triangulation.Bench.SpadeCompare.Support
+    Moonlight.Triangulation.Bench.SpadeCompare.Delaunay
+    Moonlight.Triangulation.Bench.SpadeCompare.Removal
+    Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy
+    Moonlight.Triangulation.Bench.SpadeCompare.Constraint
+    Moonlight.Triangulation.Bench.SpadeCompare.Interpolation
+    Moonlight.Triangulation.Bench.SpadeCompare.Voronoi
+    Moonlight.Triangulation.Bench.SpadeCompare.Dcel
+    Moonlight.Triangulation.Bench.SpadeCompare.Intersection
+    Moonlight.Triangulation.Bench.SpadeCompare.Gates
+  build-depends:
+    base >= 4.22 && < 5
+    , deepseq >= 1.4 && < 1.6
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , primitive >= 0.9 && < 0.10
+    , moonlight-triangulation
+    , moonlight-triangulation:dcel
+    , moonlight-triangulation:build
+    , moonlight-triangulation:dual
+
+-- The alpha comparison is deliberately opt-in: ordinary scorecard listing and
+-- replay do not inherit Homology merely because this diagnostic exists beside
+-- the canonical board.
+executable moonlight-triangulation-alpha-spade-referent
+  default-language: GHC2024
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+    -Wpartial-fields
+    -O2
+    -rtsopts
+  default-extensions:
+    BangPatterns
+    LambdaCase
+    NumericUnderscores
+  hs-source-dirs:
+    bench/spade-compare/hs-alpha
+    bench/spade-compare/hs
+  main-is: Main.hs
+  other-modules:
+    Moonlight.Triangulation.Bench.SpadeCompare.Alpha
+    Moonlight.Triangulation.Bench.SpadeCompare.Input
+    Moonlight.Triangulation.Bench.SpadeCompare.Lane
+    Moonlight.Triangulation.Bench.SpadeCompare.Timing
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , deepseq >= 1.4 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , moonlight-homology >= 0.1 && < 0.2
+    , moonlight-triangulation:build
+    , moonlight-triangulation:cell-complex
+    , moonlight-triangulation:dcel
 
 -- One exact Delaunay alpha filtration versus independently restricting and
 -- reducing the same chain complex at every critical radius. Construction of
diff --git a/src-build/Moonlight/Triangulation/BulkLoad.hs b/src-build/Moonlight/Triangulation/BulkLoad.hs
--- a/src-build/Moonlight/Triangulation/BulkLoad.hs
+++ b/src-build/Moonlight/Triangulation/BulkLoad.hs
@@ -14,6 +14,7 @@
   , delaunayGeometry
   , DuplicatePayloadPolicy (..)
   , delaunayFromCoordinates
+  , delaunayFromAdmittedDistinctAscending
   , insert
   , insertAt
   , insertMany
@@ -49,7 +50,8 @@
   , setCounter
   )
 import Moonlight.Triangulation.Internal.CircleSweep
-  ( circleSweepInsert
+  ( RadiallyOrderedArena
+  , circleSweepInsert
   , radiallyOrderArena
   , radiallyOrderGeometry
   )
@@ -107,6 +109,17 @@
   = KeepFirstPayload
   | CombineDuplicatePayload !(vertex -> vertex -> vertex)
 
+-- | The two lawful ingress sections for the one bulk construction engine.
+-- Raw callers need coordinate admission and duplicate classification; an exact
+-- SiteSet has already established canonical, distinct, lexicographically
+-- ascending 'QueryPoint's. The latter remains package-internal through the
+-- private build component, rather than becoming a second constructor.
+data BulkInput vertex
+  = RawPossiblyDuplicate
+      !(Int -> Point)
+      !(DuplicatePayloadPolicy vertex)
+  | AdmittedDistinctAscending !(Int -> QueryPoint)
+
 -- | The local identity verdict for one input position. A fresh verdict carries
 -- the canonical coordinates already admitted to the mutable DCEL, so ingress
 -- never rereads its own writes merely to accumulate the sweep centre.
@@ -127,9 +140,8 @@
   buildDelaunayFromSource
     defaults
     (V.length input)
-    (position . (input V.!))
+    (RawPossiblyDuplicate (position . (input V.!)) KeepFirstPayload)
     (input V.!)
-    KeepFirstPayload
 
 -- | Build the geometry-only Delaunay triangulation of a coordinate vector.
 -- Exact duplicate positions collapse to one site. Use 'delaunay' or
@@ -291,60 +303,65 @@
       buildDelaunayFromSource
         defaults
         coordinateCount
-        (coordinates V.!)
+        (RawPossiblyDuplicate (coordinates V.!) duplicatePolicy)
         (payloads V.!)
-        duplicatePolicy
  where
   !coordinateCount = V.length coordinates
   !payloadCount = V.length payloads
 
+-- | Construction from an already-admitted exact site section. The caller
+-- supplies Map-ascending, distinct 'QueryPoint's; this skips only raw
+-- admission, duplicate classification, and unique-site recentering. It still
+-- derives the radial execution order and runs the one circle sweep.
+delaunayFromAdmittedDistinctAscending
+  :: forall vertex directed undirected face
+   . ElementDefaults directed undirected face
+  -> V.Vector (QueryPoint, vertex)
+  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
+delaunayFromAdmittedDistinctAscending defaults input =
+  buildDelaunayFromSource
+    defaults
+    (V.length input)
+    (AdmittedDistinctAscending (\index -> fst (input V.! index)))
+    (\index -> snd (input V.! index))
+{-# INLINE delaunayFromAdmittedDistinctAscending #-}
+
 buildDelaunayFromSource
   :: forall vertex directed undirected face
    . ElementDefaults directed undirected face
   -> Int
-  -> (Int -> Point)
+  -> BulkInput vertex
   -> (Int -> vertex)
-  -> DuplicatePayloadPolicy vertex
   -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
-buildDelaunayFromSource defaults inputCount pointAtInput payloadAtInput duplicatePolicy = do
+buildDelaunayFromSource defaults inputCount input payloadAtInput = do
   ensureCapacity inputCount
   runST $ do
     inputArena <- MUV.new inputCount
-    admitted <- initializeInputArena inputArena 0 0 0
+    admitted <- initializeInputArena inputArena
     case admitted of
       Left failure -> pure (Left failure)
       Right (inputSumX, inputSumY) -> do
-        let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
-            !inputCenterX = inputSumX * inputScale
-            !inputCenterY = inputSumY * inputScale
-        assignRadialDistances inputArena inputCenterX inputCenterY 0
-        orderedInputArena <- radiallyOrderArena inputArena
         mutable <- newMutableDcel defaults (planarDcelCapacity inputCount)
         operation <- newOperationState (halfEdgeCapacity mutable)
-        mapping <- newPrimArray inputCount
-        unique <- classifySortedInputs mapping inputArena
-        (sumX, sumY) <- appendClassifiedInputs mutable mapping 0 0 0
+        (mapping, unique, orderedArena) <-
+          case input of
+            RawPossiblyDuplicate pointAtInput duplicatePolicy ->
+              prepareRawInput
+                inputArena
+                mutable
+                pointAtInput
+                duplicatePolicy
+                inputSumX
+                inputSumY
+            AdmittedDistinctAscending _ ->
+              prepareAdmittedDistinctInput inputArena mutable inputSumX inputSumY
         setCounter operation CounterInputPoints inputCount
         setCounter operation CounterUniquePoints unique
         setCounter operation CounterDuplicatePoints (inputCount - unique)
         inserted <-
           if unique == 0
             then pure (Right 0)
-            else do
-              orderedArena <-
-                if unique == inputCount
-                  then pure orderedInputArena
-                  else do
-                    let !scale = recip (fromIntegral unique)
-                        !uniqueArena = MUV.unsafeSlice 0 unique inputArena
-                    rewriteCompactedArena
-                      mapping
-                      (sumX * scale)
-                      (sumY * scale)
-                      uniqueArena
-                      0
-                    radiallyOrderArena uniqueArena
-              circleSweepInsert mutable operation orderedArena
+            else circleSweepInsert mutable operation orderedArena
         case inserted of
           Left failure -> pure (Left failure)
           Right seedCount -> do
@@ -364,18 +381,29 @@
                         }
                   )
  where
-  -- Validate, canonicalize, and materialize each source position in one local
-  -- section. The former validation pass and centre fold both traversed the
-  -- boxed source before arena generation traversed it a third time; this
-  -- section returns their only global invariant, the coordinate sum.
+  -- Raw sources validate and canonicalize at this one ingress. Admitted
+  -- sources merely project their retained QueryPoint evidence into the sweep
+  -- arena; neither route reconstructs coordinate identity downstream.
   initializeInputArena
     :: forall s
      . MUV.MVector s (Double, Double, Double, Word32)
+    -> ST s (Either BuildError (Double, Double))
+  initializeInputArena arena =
+    case input of
+      RawPossiblyDuplicate pointAtInput _ ->
+        initializeRawInputArena arena pointAtInput 0 0 0
+      AdmittedDistinctAscending pointAtInput ->
+        initializeAdmittedInputArena arena pointAtInput
+
+  initializeRawInputArena
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> (Int -> Point)
     -> Int
     -> Double
     -> Double
     -> ST s (Either BuildError (Double, Double))
-  initializeInputArena arena !index !sumX !sumY
+  initializeRawInputArena arena pointAtInput !index !sumX !sumY
     | index >= inputCount = pure (Right (sumX, sumY))
     | otherwise =
         case validatePoint (Just index) (pointAtInput index) of
@@ -384,8 +412,99 @@
             case queryPointValue admitted of
               Point x y -> do
                 MUV.unsafeWrite arena index (0, x, y, fromIntegral index)
-                initializeInputArena arena (index + 1) (sumX + x) (sumY + y)
+                initializeRawInputArena
+                  arena
+                  pointAtInput
+                  (index + 1)
+                  (sumX + x)
+                  (sumY + y)
 
+  initializeAdmittedInputArena
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> (Int -> QueryPoint)
+    -> ST s (Either BuildError (Double, Double))
+  initializeAdmittedInputArena arena pointAtInput =
+    Right
+      <$> MUV.ifoldM'
+        (\(!sumX, !sumY) index _ ->
+           case queryPointValue (pointAtInput index) of
+             Point x y -> do
+               MUV.unsafeWrite arena index (0, x, y, fromIntegral index)
+               pure (sumX + x, sumY + y)
+        )
+        (0, 0)
+        arena
+
+  prepareRawInput
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> MutableDcel s vertex directed undirected face
+    -> (Int -> Point)
+    -> DuplicatePayloadPolicy vertex
+    -> Double
+    -> Double
+    -> ST s (MutablePrimArray s Word32, Int, RadiallyOrderedArena s)
+  prepareRawInput inputArena mutable pointAtInput duplicatePolicy inputSumX inputSumY = do
+    let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
+        !inputCenterX = inputSumX * inputScale
+        !inputCenterY = inputSumY * inputScale
+    assignRadialDistances inputArena inputCenterX inputCenterY 0
+    orderedInputArena <- radiallyOrderArena inputArena
+    mapping <- newPrimArray inputCount
+    unique <- classifySortedInputs mapping inputArena
+    (sumX, sumY) <-
+      appendClassifiedInputs mutable mapping pointAtInput duplicatePolicy 0 0 0
+    orderedArena <-
+      if unique == inputCount
+        then pure orderedInputArena
+        else do
+          let !scale = recip (fromIntegral unique)
+              !uniqueArena = MUV.unsafeSlice 0 unique inputArena
+          rewriteCompactedArena
+            mapping
+            (sumX * scale)
+            (sumY * scale)
+            uniqueArena
+            0
+          radiallyOrderArena uniqueArena
+    pure (mapping, unique, orderedArena)
+
+  prepareAdmittedDistinctInput
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> MutableDcel s vertex directed undirected face
+    -> Double
+    -> Double
+    -> ST s (MutablePrimArray s Word32, Int, RadiallyOrderedArena s)
+  prepareAdmittedDistinctInput inputArena mutable inputSumX inputSumY = do
+    mapping <- newPrimArray inputCount
+    appendAdmittedDistinctInputs mutable mapping inputArena
+    let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
+        !inputCenterX = inputSumX * inputScale
+        !inputCenterY = inputSumY * inputScale
+    assignRadialDistances inputArena inputCenterX inputCenterY 0
+    orderedArena <- radiallyOrderArena inputArena
+    pure (mapping, inputCount, orderedArena)
+
+  -- An admitted SiteSet is already distinct. Sequential append therefore
+  -- transports the identity class map directly into authoritative vertex ids,
+  -- without reopening duplicate policy or recentering a compacted arena.
+  appendAdmittedDistinctInputs
+    :: forall s
+     . MutableDcel s vertex directed undirected face
+    -> MutablePrimArray s Word32
+    -> MUV.MVector s (Double, Double, Double, Word32)
+    -> ST s ()
+  appendAdmittedDistinctInputs mutable mapping arena =
+    MUV.imapM_
+      (\index (_, x, y, _) -> do
+         vertex <- appendVertexCoordinates mutable x y (payloadAtInput index)
+         writePrimArray mapping index (fromIntegral vertex)
+         MUV.unsafeWrite arena index (0, x, y, fromIntegral vertex)
+      )
+      arena
+
   -- The all-input centre seeds the first radial ordering. Duplicate
   -- equivalence classes become adjacent in that total order; after descent,
   -- the compacted section is recentered over unique sites and ordered once
@@ -400,10 +519,10 @@
   assignRadialDistances arena centerX centerY !index
     | index >= inputCount = pure ()
     | otherwise = do
-        (_, x, y, input) <- MUV.unsafeRead arena index
+        (_, x, y, inputOrdinal) <- MUV.unsafeRead arena index
         let !deltaX = centerX - x
             !deltaY = centerY - y
-        MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, x, y, input)
+        MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, x, y, inputOrdinal)
         assignRadialDistances arena centerX centerY (index + 1)
 
   -- Classify the sorted local sections by exact canonical position. The
@@ -421,20 +540,28 @@
         writePrimArray mapping (fromIntegral firstInput) firstInput
         classifyFrom first firstX firstY firstInput 1 1
    where
+    classifyFrom
+      :: (Double, Double, Double, Word32)
+      -> Double
+      -> Double
+      -> Word32
+      -> Int
+      -> Int
+      -> ST s Int
     classifyFrom !_ !previousX !previousY !classInput !readIndex !uniqueCount
       | readIndex >= inputCount = pure uniqueCount
       | otherwise = do
-          record@(_, x, y, input) <- MUV.unsafeRead arena readIndex
+          record@(_, x, y, inputOrdinal) <- MUV.unsafeRead arena readIndex
           if x == previousX && y == previousY
             then do
-              writePrimArray mapping (fromIntegral input) classInput
+              writePrimArray mapping (fromIntegral inputOrdinal) classInput
               classifyFrom record previousX previousY classInput (readIndex + 1) uniqueCount
             else do
-              writePrimArray mapping (fromIntegral input) input
+              writePrimArray mapping (fromIntegral inputOrdinal) inputOrdinal
               if uniqueCount == readIndex
                 then pure ()
                 else MUV.unsafeWrite arena uniqueCount record
-              classifyFrom record x y input (readIndex + 1) (uniqueCount + 1)
+              classifyFrom record x y inputOrdinal (readIndex + 1) (uniqueCount + 1)
 
   -- Materialize in original order, preserving the established handle
   -- assignment and duplicate-payload law. The equivalence mapping for a
@@ -443,11 +570,13 @@
     :: forall s
      . MutableDcel s vertex directed undirected face
     -> MutablePrimArray s Word32
+    -> (Int -> Point)
+    -> DuplicatePayloadPolicy vertex
     -> Int
     -> Double
     -> Double
     -> ST s (Double, Double)
-  appendClassifiedInputs mutable mapping !index !sumX !sumY
+  appendClassifiedInputs mutable mapping pointAtInput duplicatePolicy !index !sumX !sumY
     | index >= inputCount = pure (sumX, sumY)
     | otherwise = do
         classInput <- readPrimArray mapping index
@@ -462,6 +591,8 @@
               appendClassifiedInputs
                 mutable
                 mapping
+                pointAtInput
+                duplicatePolicy
                 (index + 1)
                 (sumX + canonicalX)
                 (sumY + canonicalY)
@@ -473,7 +604,14 @@
               CombineDuplicatePayload combine -> do
                 residentData <- vertexDataAt mutable resident
                 writeVertexData mutable resident (combine residentData vertexData)
-            appendClassifiedInputs mutable mapping (index + 1) sumX sumY
+            appendClassifiedInputs
+              mutable
+              mapping
+              pointAtInput
+              duplicatePolicy
+              (index + 1)
+              sumX
+              sumY
 
   -- Once duplicates have shortened the vertex arena, translate each compacted
   -- representative from its input-class name to its authoritative vertex and
diff --git a/src-build/Moonlight/Triangulation/Cdt.hs b/src-build/Moonlight/Triangulation/Cdt.hs
--- a/src-build/Moonlight/Triangulation/Cdt.hs
+++ b/src-build/Moonlight/Triangulation/Cdt.hs
@@ -17,16 +17,12 @@
   , constrainedSeamFaceFirstPoint
   , constrainedSeamFaceSecondPoint
   , constrainedSeamFaceThirdPoint
-  , ConstrainedSeamConstraintEvidence
-  , constrainedSeamConstraintSegment
-  , constrainedSeamConstraintRecovery
   , ConstrainedSeamResult
   , constrainedSeamResultTriangulation
   , constrainedSeamLeftFaceCount
   , constrainedSeamRightFaceEvidence
   , constrainedSeamJoinFaces
   , constrainedSeamLeftConstraintCount
-  , constrainedSeamRightConstraintEvidence
   , constrainedSeamConstraintStats
   , constrainedSeamBuildStats
   , constrainedSeamPublicationStats
@@ -102,7 +98,6 @@
   , ConstraintBatchStats (..)
   , ConstrainedExtensionResult (..)
   , ConstrainedSeamSide (..)
-  , ConstrainedSeamConstraintEvidence (..)
   , ConstrainedSeamFaceEvidence (..)
   , ConstrainedSeamResult (..)
   , ConstraintConflict (..)
diff --git a/src-build/Moonlight/Triangulation/Insertion.hs b/src-build/Moonlight/Triangulation/Insertion.hs
--- a/src-build/Moonlight/Triangulation/Insertion.hs
+++ b/src-build/Moonlight/Triangulation/Insertion.hs
@@ -8,6 +8,7 @@
   ( insertExistingVertex
   , insertExistingVertexWithHint
   , insertPointCombining
+  , insertAdmittedPointCombining
   , insertVertexAtPoint
   , insertExistingVertexAtLocation
   ) where
@@ -37,11 +38,12 @@
 import Moonlight.Triangulation.Math
 import Moonlight.Triangulation.Types
 
--- | The shared exact-site insertion interpreter. Geometry decides whether a
--- site is new; callers choose only the annotation law for an occupied site.
--- Keeping the validation, location, placement, and counters here prevents
--- sessions and constrained extension from drifting into two insertion
--- semantics merely because they own different enclosing transactions.
+-- | Admit a raw site, then delegate to the shared admitted-site interpreter.
+-- Geometry decides whether a site is new; callers choose only the annotation
+-- law for an occupied site. Keeping raw admission and the common location,
+-- placement, and counters in this one owner prevents sessions and constrained
+-- extension from drifting into two insertion semantics merely because they own
+-- different enclosing transactions.
 insertPointCombining
   :: (vertex -> vertex -> vertex)
   -> Maybe Int
@@ -53,21 +55,43 @@
 insertPointCombining combine seed mutable operation point payload =
   case validatePoint Nothing point of
     Left failure -> pure (Left failure)
-    Right _ -> do
-      addCounter operation CounterInputPoints 1
-      outcome <- insertVertexAtPoint @'ProbeOff mutable operation seed point payload
-      case outcome of
-        Left failure -> pure (Left failure)
-        Right resolved@(vertex, disposition) -> do
-          case disposition of
-            AlreadyPresent -> do
-              resident <- vertexDataAt mutable vertex
-              writeVertexData mutable vertex (combine resident payload)
-              addCounter operation CounterExistingPoints 1
-              addCounter operation CounterDuplicatePoints 1
-            Inserted -> addCounter operation CounterUniquePoints 1
-          pure (Right resolved)
+    Right admitted ->
+      insertAdmittedPointCombining combine seed mutable operation admitted payload
 {-# INLINE insertPointCombining #-}
+
+-- | Insert an already admitted site. The caller has retained the canonical
+-- coordinate section, so this interpreter only locates and glues the payload;
+-- it must not reopen coordinate admission.
+insertAdmittedPointCombining
+  :: (vertex -> vertex -> vertex)
+  -> Maybe Int
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> QueryPoint
+  -> vertex
+  -> ST s (Either BuildError (Int, InsertionDisposition))
+insertAdmittedPointCombining combine seed mutable operation queryPoint payload = do
+  addCounter operation CounterInputPoints 1
+  outcome <-
+    insertVertexAtPoint
+      @'ProbeOff
+      mutable
+      operation
+      seed
+      (queryPointValue queryPoint)
+      payload
+  case outcome of
+    Left failure -> pure (Left failure)
+    Right resolved@(vertex, disposition) -> do
+      case disposition of
+        AlreadyPresent -> do
+          resident <- vertexDataAt mutable vertex
+          writeVertexData mutable vertex (combine resident payload)
+          addCounter operation CounterExistingPoints 1
+          addCounter operation CounterDuplicatePoints 1
+        Inserted -> addCounter operation CounterUniquePoints 1
+      pure (Right resolved)
+{-# INLINE insertAdmittedPointCombining #-}
 
 insertExistingVertex
   :: forall p s vertex directed undirected face
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
 -- | The singleton segment verbs: glue one segment or a polyline, and retire a
 -- constraint, each inside one sealed transaction.
@@ -43,25 +44,20 @@
 import Moonlight.Triangulation.Internal.Representation
 import Moonlight.Triangulation.Internal.Transaction (runUnmeasuredTransaction)
 import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Math
 
 -- | Site both endpoints and glue the segment between them, in one transaction.
 -- A refused request publishes nothing, so the caller's triangulation still
 -- stands and the endpoints it would have sited are not among its vertices.
 addConstraintEdge
-  :: HasPosition vertex
+  :: forall vertex directed undirected face
+   . HasPosition vertex
   => Triangulation 'Constrained vertex directed undirected face
   -> vertex
   -> vertex
   -> Either (CdtError) (ConstraintResult vertex directed undirected face)
 addConstraintEdge triangulation fromVertex toVertex = do
-  _ <- first CdtBuildError (validatePoint Nothing fromPoint)
-  _ <- first CdtBuildError (validatePoint Nothing toPoint)
   result <-
-    case
-        ( lookupExistingConstraintEndpoint triangulation fromPoint
-        , lookupExistingConstraintEndpoint triangulation toPoint
-        ) of
+    case (fromExisting, toExisting) of
       (Just from, Just to) ->
         case constraintAdmission triangulation from to of
           ConstraintBlocked blocking -> Left (ConstraintIntersection blocking)
@@ -72,22 +68,48 @@
               fromVertex
               to
               toVertex
-      _ -> addConstraintWithEndpointPlacement
+      _ -> addConstraintWithEndpointPlacement fromExisting toExisting
   pure result
  where
   !fromPoint = position fromVertex
   !toPoint = position toVertex
+  !fromExisting = lookupExistingConstraintEndpoint triangulation fromPoint
+  !toExisting = lookupExistingConstraintEndpoint triangulation toPoint
 
-  addConstraintWithEndpointPlacement = do
+  addConstraintWithEndpointPlacement existingFrom existingTo = do
     (request, frozen) <-
-      runUnmeasuredTransaction CdtBuildError LocalTransaction triangulation 2 $ \mutable operation -> do
-        programWords <- newGrowableWord32 256
-        asConstraintStep (placeConstraintEndpoint mutable operation Nothing fromPoint fromVertex)
-          `bindMutable` \from ->
-            asConstraintStep (placeConstraintEndpoint mutable operation Nothing toPoint toVertex)
-              `bindMutable` \to ->
-                recoverConstraintRequest programWords mutable operation from to
+      runUnmeasuredTransaction
+        CdtBuildError
+        LocalTransaction
+        triangulation
+        (missingEndpointCount existingFrom + missingEndpointCount existingTo)
+        $ \mutable operation -> do
+            programWords <- newGrowableWord32 256
+            asConstraintStep
+              (placeOrReuseConstraintEndpoint mutable operation existingFrom fromPoint fromVertex)
+              `bindMutable` \from ->
+                asConstraintStep
+                  (placeOrReuseConstraintEndpoint mutable operation existingTo toPoint toVertex)
+                  `bindMutable` \to ->
+                    recoverConstraintRequest programWords mutable operation from to
     pure (publishConstraintResult frozen request)
+
+  missingEndpointCount :: Maybe VertexId -> Int
+  missingEndpointCount = maybe 1 (const 0)
+
+  placeOrReuseConstraintEndpoint
+    :: MutableDcel s vertex directed undirected face
+    -> OperationState s
+    -> Maybe VertexId
+    -> Point
+    -> vertex
+    -> ST s (Either BuildError VertexId)
+  placeOrReuseConstraintEndpoint mutable operation existing point payload =
+    case existing of
+      Just vertex@(VertexId rawVertex) -> do
+        writeVertexData mutable (fromIntegral rawVertex) payload
+        pure (Right vertex)
+      Nothing -> placeConstraintEndpoint mutable operation Nothing point payload
 
 -- | The common singleton case already owns both sites. Resolve them before
 -- opening topology, then retain the original payload semantics in the
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs
@@ -97,22 +97,18 @@
   -> VertexId
   -> Either (CdtError) (ConstraintResult vertex directed undirected face)
 addConstraintAndSplit makeVertex triangulation from to = do
-  validateEndpoints triangulation from to
-  if from == to
-    then pure (ConstraintRecoveryResult triangulation V.empty 0)
-    else do
-      (published, receipts) <-
-        driveConstraintSplits LocalTransaction makeVertex triangulation (V.singleton (from, to))
-      pure
-        ( case receipts V.!? 0 of
-            Just receipt ->
-              ConstraintRecoveryResult
-                { constraintRecoveryTriangulation = published
-                , constraintRecoveryPathReceipt = splitReceiptPath receipt
-                , constraintRecoveryAddedEdges = splitReceiptAdded receipt
-                }
-            Nothing -> ConstraintRecoveryResult published V.empty 0
-        )
+  (published, receipts) <-
+    driveConstraintSplits LocalTransaction makeVertex triangulation (V.singleton (from, to))
+  pure
+    ( case receipts V.!? 0 of
+        Just receipt ->
+          ConstraintRecoveryResult
+            { constraintRecoveryTriangulation = published
+            , constraintRecoveryPathReceipt = splitReceiptPath receipt
+            , constraintRecoveryAddedEdges = splitReceiptAdded receipt
+            }
+        Nothing -> ConstraintRecoveryResult published V.empty 0
+    )
 
 -- | Divide every requested segment inside as few transactions as the vertex
 -- reservation allows: usually one, which is the referent semantics for a
@@ -179,8 +175,12 @@
       )
 driveConstraintSplits shape makeVertex base requests = do
   V.mapM_ (uncurry (validateEndpoints base)) requests
-  advance base 0 [] Nothing
+  if V.all (uncurry (==)) requests
+    then Right (base, V.replicate (V.length requests) emptySplitReceipt)
+    else advance base 0 [] Nothing
  where
+  emptySplitReceipt = SplitReceipt V.empty 0
+
   advance
     :: Triangulation 'Constrained vertex directed undirected face
     -> Int
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs
@@ -13,7 +13,6 @@
   , ConstrainedUnionError (..)
   , ConstrainedSeamSide (..)
   , ConstrainedSeamFaceEvidence (..)
-  , ConstrainedSeamConstraintEvidence (..)
   , ConstrainedSeamResult (..)
   , CorridorObstruction (..)
   , ConstraintRecoveryResult (..)
@@ -152,18 +151,6 @@
   deriving stock (Eq, Show, Generic)
   deriving anyclass (NFData)
 
--- | Exact transport of one source constraint segment. 'Nothing' means its
--- copied constraint edge already represented the segment after zippering;
--- 'Just' records the corridor outcome when recovery was required.
-data ConstrainedSeamConstraintEvidence = ConstrainedSeamConstraintEvidence
-  { constrainedSeamConstraintSegment :: !(CanonicalSegment)
-    -- ^ Canonical source segment preserved by the publication.
-  , constrainedSeamConstraintRecovery :: !(Maybe ConstraintOutcome)
-    -- ^ Recovery work when copying alone did not preserve the segment.
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
 -- | Atomic publication of a source-preserving constrained seam and the proof
 -- needed to transport solved face-indexed data. The receipt is derived during
 -- the ordered append/seam interpretation; canonical numbering remains an
@@ -183,9 +170,6 @@
     -- ^ Exact final J component, including rewritten exterior source filler.
   , constrainedSeamLeftConstraintCount :: {-# UNPACK #-} !Int
     -- ^ Stable number of left-source constraints.
-  , constrainedSeamRightConstraintEvidence
-      :: !(V.Vector (ConstrainedSeamConstraintEvidence))
-    -- ^ Preservation evidence for right-source constraints.
   , constrainedSeamConstraintStats :: !ConstraintBatchStats
     -- ^ Constraint recovery work performed by the seam.
   , constrainedSeamBuildStats :: !BuildStats
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs
@@ -29,7 +29,7 @@
 import qualified Moonlight.Triangulation.Dcel as Dcel
 import Moonlight.Triangulation.Handles.HandleDefs
 import Moonlight.Triangulation.Internal.Canonical (canonicalize)
-import Moonlight.Triangulation.Insertion (insertPointCombining)
+import Moonlight.Triangulation.Insertion (insertAdmittedPointCombining)
 import Moonlight.Triangulation.Refinement (validateRefinementParameters)
 import Moonlight.Triangulation.Internal.Cdt.Batch
   ( finalizeConstraintBatch
@@ -41,7 +41,6 @@
 import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)
 import Moonlight.Triangulation.IntersectionIterator (foldCorridorBetweenPoints)
 import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.Join.Rebuild (rebuildCanonicalSiteSet)
 import Moonlight.Triangulation.Internal.Join.Seam
   ( executeConstrainedSeam
   , planSeam
@@ -55,6 +54,7 @@
   )
 import Moonlight.Triangulation.Internal.Join.SiteSet
   ( SiteSet
+  , delaunayFromSiteSet
   , siteSetAssocs
   , siteSetFromTriangulation
   , siteSetPoints
@@ -111,7 +111,7 @@
       unconstrained <-
         Bifunctor.first
           (ConstraintUnionConstructionFailed . CdtBuildError)
-          (rebuildCanonicalSiteSet unionSites)
+          (delaunayFromSiteSet unionSites)
       requests <- traverse (segmentRequest unconstrained) (V.toList segments)
       recovered <-
         Bifunctor.first ConstraintUnionConstructionFailed
@@ -183,11 +183,11 @@
       (ConstrainedUnionError)
       (ConstrainedSeamResult annotation)
 joinSeparatedConstrained sourceFacePreserved parameters left right = do
-  vertexDefault <- ensureDefaultedPayloads left right
   Bifunctor.first
     (ConstraintUnionConstructionFailed . CdtBuildError)
     (validateRefinementParameters parameters)
   seamPlan <- maybe (Left ConstraintUnionNotSeparated) Right (planSeam left right)
+  vertexDefault <- ensureDefaultedPayloads left right
   seamExecution <-
     Bifunctor.first
       (ConstraintUnionConstructionFailed . CdtBuildError)
@@ -200,8 +200,6 @@
           right
       )
   let published = seamExecutionTriangulation seamExecution
-      rightSegments = constraintSegments right
-  rightConstraintEvidence <- sourceConstraintEvidence rightSegments Map.empty
   pure
     ConstrainedSeamResult
       { constrainedSeamResultTriangulation = published
@@ -209,7 +207,6 @@
       , constrainedSeamRightFaceEvidence = seamExecutionRightFaceEvidence seamExecution
       , constrainedSeamJoinFaces = seamExecutionJoinFaces seamExecution
       , constrainedSeamLeftConstraintCount = Dcel.numConstraints left
-      , constrainedSeamRightConstraintEvidence = rightConstraintEvidence
       , constrainedSeamConstraintStats = ConstraintBatchStats 0 0 0 0 0 0
       , constrainedSeamBuildStats = seamExecutionBuildStats seamExecution
       , constrainedSeamPublicationStats = seamExecutionPublicationStats seamExecution
@@ -222,9 +219,7 @@
   -> Either ConstrainedUnionError annotation
 ensureDefaultedPayloads left right =
   case
-      ( triSeamFrontier left
-      , triSeamFrontier right
-      , boxedFill (triVertexData left)
+      ( boxedFill (triVertexData left)
       , boxedFill (triDirectedData left)
       , boxedFill (triUndirectedData left)
       , boxedFill (triFaceData left)
@@ -233,9 +228,7 @@
       , boxedFill (triUndirectedData right)
       , boxedFill (triFaceData right)
       ) of
-    ( Just _
-      , Just _
-      , Just vertexDefault
+    ( Just vertexDefault
       , Just _
       , Just _
       , Just _
@@ -246,21 +239,6 @@
       ) -> Right vertexDefault
     _ -> Left ConstraintUnionRequiresGeometryOnlyPublication
 
-sourceConstraintEvidence
-  :: V.Vector (CanonicalSegment)
-  -> Map.Map (CanonicalSegment) ConstraintOutcome
-  -> Either (ConstrainedUnionError) (V.Vector (ConstrainedSeamConstraintEvidence))
-sourceConstraintEvidence segments recovered =
-  traverse
-    (\segment ->
-       Right
-         ConstrainedSeamConstraintEvidence
-           { constrainedSeamConstraintSegment = segment
-           , constrainedSeamConstraintRecovery = Map.lookup segment recovered
-           }
-    )
-    segments
-
 -- | Extend one already-resident constrained triangulation with one new
 -- constrained section. This is intentionally asymmetric: the base mesh is
 -- thawed once, extension sites are inserted into it, and only the extension's
@@ -306,10 +284,13 @@
         , residentCorridorIsEmpty segment -> LocalTransaction
       _ -> DenseTransaction
   residentCorridorIsEmpty segment =
-    case (mkQueryPoint (segmentStart segment), mkQueryPoint (segmentEnd segment)) of
-      (Right from, Right to) ->
-        foldCorridorBetweenPoints base from to (\_ _ -> Left ()) () == Just (Right ())
-      _ -> False
+    foldCorridorBetweenPoints
+      base
+      (QueryPoint (segmentStart segment))
+      (QueryPoint (segmentEnd segment))
+      (\_ _ -> Left ())
+      ()
+      == Just (Right ())
 {-# INLINE extendConstrainedWith #-}
 
 insertAndRecoverExtension
@@ -363,16 +344,20 @@
  where
   insertOne
     :: Either (ConstrainedUnionError) [(Point, VertexId)]
-    -> (Point, annotation)
+    -> (QueryPoint, annotation)
     -> ST s (Either (ConstrainedUnionError) [(Point, VertexId)])
   insertOne rejected@(Left _) _ = pure rejected
-  insertOne (Right accumulated) (point, annotation) =
+  insertOne (Right accumulated) (queryPoint, annotation) =
     fmap
       ( Bifunctor.first (ConstraintUnionConstructionFailed . CdtBuildError)
           . fmap
-            (\(vertex, _) -> (point, VertexId (fromIntegral vertex)) : accumulated)
+            ( \(vertex, _) ->
+                ( queryPointValue queryPoint
+                , VertexId (fromIntegral vertex)
+                ) : accumulated
+            )
       )
-      (insertPointCombining combine Nothing mutable operation point annotation)
+      (insertAdmittedPointCombining combine Nothing mutable operation queryPoint annotation)
 
 accumulatorOutcomes :: ConstraintBatchAccumulator -> V.Vector ConstraintOutcome
 accumulatorOutcomes = V.fromList . reverse . accumulatedConstraintOutcomes
diff --git a/src-build/Moonlight/Triangulation/Internal/Join.hs b/src-build/Moonlight/Triangulation/Internal/Join.hs
--- a/src-build/Moonlight/Triangulation/Internal/Join.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Join.hs
@@ -22,9 +22,9 @@
   , planTournament
   )
 import Moonlight.Triangulation.Internal.Join.Seam (executeSeam)
-import Moonlight.Triangulation.Internal.Join.Rebuild (rebuildCanonicalSiteSet)
 import Moonlight.Triangulation.Internal.Join.SiteSet
   ( SiteSet
+  , rebuildCanonicalSiteSet
   , siteSetAssocs
   , siteSetSize
   )
@@ -32,7 +32,7 @@
   ( JoinSemilattice (joinAnnotations)
   )
 import Moonlight.Triangulation.Session
-  ( insertVertexAtCombining
+  ( insertAdmittedVertexAtCombining
   , withLocalSession
   )
 import Moonlight.Triangulation.Types
@@ -85,7 +85,7 @@
       (siteSetSize sites)
       ( traverse_
           (\(point, annotation) ->
-             () <$ insertVertexAtCombining joinAnnotations point annotation
+             () <$ insertAdmittedVertexAtCombining joinAnnotations point annotation
           )
           (siteSetAssocs sites)
       )
diff --git a/src-build/Moonlight/Triangulation/Internal/Join/Rebuild.hs b/src-build/Moonlight/Triangulation/Internal/Join/Rebuild.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Join/Rebuild.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Canonical publication of an exact site section through the existing bulk
--- loader. Join, finite-set algebra, and constrained union share this boundary;
--- none owns a private point/payload carrier or a second constructor.
-module Moonlight.Triangulation.Internal.Join.Rebuild
-  ( rebuildCanonicalSiteSet
-  ) where
-
-import qualified Data.Vector as V
-import Moonlight.Triangulation.BulkLoad
-  ( DuplicatePayloadPolicy (KeepFirstPayload)
-  , delaunayFromCoordinates
-  )
-import Moonlight.Triangulation.Internal.Canonical (canonicalize)
-import Moonlight.Triangulation.Internal.Join.SiteSet (SiteSet, siteSetAssocs)
-import Moonlight.Triangulation.Internal.Representation
-  ( BuildResult (buildTriangulation)
-  , Triangulation
-  )
-import Moonlight.Triangulation.Internal.Types
-  ( BuildError
-  , ConstraintMode (Unconstrained)
-  , unitElementDefaults
-  )
-
-rebuildCanonicalSiteSet
-  :: SiteSet annotation
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-rebuildCanonicalSiteSet sites = do
-  built <-
-    delaunayFromCoordinates
-      unitElementDefaults
-      (V.fromList (fmap fst associations))
-      (V.fromList (fmap snd associations))
-      KeepFirstPayload
-  canonicalize (buildTriangulation built)
- where
-  associations = siteSetAssocs sites
diff --git a/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs b/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs
--- a/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs
@@ -44,13 +44,12 @@
   , vertexOutEdge
   , vertexData
   )
-import Moonlight.Triangulation.FloodFillIterator (floodFillFaces)
+import Moonlight.Triangulation.FloodFillIterator (floodFillFacesWithRejectedEdges)
 import Moonlight.Triangulation.Handles.HandleDefs
   ( DirectedEdgeId (..)
   , FaceId (..)
   , UndirectedEdgeId (..)
   , VertexId (..)
-  , asUndirected
   , directedPair
   )
 import Moonlight.Triangulation.Internal.DcelOperations.Hull (closeOuterTurn)
@@ -105,8 +104,12 @@
 -- kernel. Constructors stay private so an unseparated pair cannot be handed to
 -- the executor by accident.
 data SeamPlan
-  = SeamLeftBeforeRight !SeamTangents
-  | SeamRightBeforeLeft !SeamTangents
+  = -- | The operands are already in geometric left/right order.  The two
+    -- retained frontiers are therefore the exact ones consumed by the zipper.
+    SeamLeftBeforeRight !SeamTangents !SeamFrontierIndex !SeamFrontierIndex
+  | -- | The input operands are reversed, but the retained frontiers remain in
+    -- geometric left/right order rather than input-operand order.
+    SeamRightBeforeLeft !SeamTangents !SeamFrontierIndex !SeamFrontierIndex
 
 -- | Coordinate chart used by the separated seam.  The second chart is the
 -- orientation-preserving quarter turn @(u,v) = (y,-x)@; exact predicates and
@@ -130,10 +133,10 @@
   planAxis axis leftFrontier rightFrontier =
     case separatedOrder axis left right leftFrontier rightFrontier of
       Just LeftBeforeRight ->
-        SeamLeftBeforeRight
+        (\tangents -> SeamLeftBeforeRight tangents leftFrontier rightFrontier)
           <$> seamTangents axis left leftFrontier right rightFrontier
       Just RightBeforeLeft ->
-        SeamRightBeforeLeft
+        (\tangents -> SeamRightBeforeLeft tangents rightFrontier leftFrontier)
           <$> seamTangents axis right rightFrontier left leftFrontier
       Nothing -> Nothing
 
@@ -148,9 +151,9 @@
   -> Either BuildError (Triangulation 'Unconstrained vertex () () ())
 executeSeam plan left right =
   case plan of
-    SeamLeftBeforeRight tangents ->
+    SeamLeftBeforeRight tangents _ _ ->
       fmap fst (mergeSeparated left right tangents)
-    SeamRightBeforeLeft tangents ->
+    SeamRightBeforeLeft tangents _ _ ->
       fmap fst (mergeSeparated right left tangents)
 
 -- | Result of the constrained seam kernel. Constraint flags are copied before
@@ -182,12 +185,12 @@
   -> Triangulation 'Constrained vertex () () ()
   -> Either BuildError (ConstrainedSeamExecution vertex)
 executeConstrainedSeam sourceFacePreserved parameters vertexDefault plan left right = do
-  let (order, tangents) =
+  let (order, tangents, leftFrontier, rightFrontier) =
         case plan of
-          SeamLeftBeforeRight selectedTangents ->
-            (GeometricBaseLeft, selectedTangents)
-          SeamRightBeforeLeft selectedTangents ->
-            (GeometricBaseRight, selectedTangents)
+          SeamLeftBeforeRight selectedTangents selectedLeftFrontier selectedRightFrontier ->
+            (GeometricBaseLeft, selectedTangents, selectedLeftFrontier, selectedRightFrontier)
+          SeamRightBeforeLeft selectedTangents selectedLeftFrontier selectedRightFrontier ->
+            (GeometricBaseRight, selectedTangents, selectedLeftFrontier, selectedRightFrontier)
   (triangulation, statistics, publicationStats, joinFaces) <-
     executeResidentConstrained
       targetFaceProtected
@@ -196,11 +199,12 @@
       order
       left
       right
+      leftFrontier
+      rightFrontier
       tangents
   rightEvidence <-
     seamFaceEvidenceForSource
       right
-      triangulation
       incomingFaceOffset
       rightEvidenceFaces
   pure
@@ -244,14 +248,14 @@
   :: Triangulation 'Constrained vertex () () ()
   -> (Int -> Bool)
   -> V.Vector FaceId
-  -> V.Vector FaceId
+  -> (V.Vector FaceId, [UndirectedEdgeId])
 exactJoinFaces triangulation targetFaceProtected seeds =
-  V.fromList
-    ( floodFillFaces
-        triangulation
-        (V.toList seeds)
-        canCross
-    )
+  let (faces, rejectedPairs) =
+        floodFillFacesWithRejectedEdges
+          triangulation
+          (V.toList seeds)
+          canCross
+   in (V.fromList faces, rejectedPairs)
  where
   canCross edge =
     not (isConstraintEdge triangulation edge)
@@ -270,51 +274,16 @@
 -- rewrite the solved section.
 certifyJoinBoundary
   :: Triangulation 'Constrained vertex () () ()
-  -> (Int -> Bool)
-  -> V.Vector FaceId
+  -> [UndirectedEdgeId]
   -> Either BuildError ()
-certifyJoinBoundary triangulation targetFaceProtected joinFaces =
-  traverse_ certifyPair (IntSet.toAscList candidatePairs)
+certifyJoinBoundary triangulation = traverse_ certifyPair
  where
-  joinFaceSet =
-    IntSet.fromList
-      [ fromIntegral raw
-      | FaceId raw <- V.toList joinFaces
-      ]
-
-  candidatePairs =
-    List.foldl'
-      (\pairs faceHandle ->
-         List.foldl'
-           (\acc edge ->
-              let UndirectedEdgeId raw = asUndirected edge
-               in IntSet.insert (fromIntegral raw) acc
-           )
-           pairs
-           (faceDirectedEdges triangulation faceHandle)
-      )
-      IntSet.empty
-      (V.toList joinFaces)
-
-  certifyPair rawPair =
-    let edge = UndirectedEdgeId (fromIntegral rawPair)
-     in if not (selectedJoinOverlap edge)
-          || isConstraintEdge triangulation edge
+  certifyPair edge =
+    if isConstraintEdge triangulation edge
           || not (immutableEdgeRequiresFlip triangulation edge)
           then Right ()
           else Left (SeamSourceEdgeRequiresFlip edge)
 
-  selectedJoinOverlap edge =
-    case directedPair edge of
-      (forward, backward) ->
-        let forwardFace = incidentFace triangulation forward
-            backwardFace = incidentFace triangulation backward
-         in (joinFace forwardFace && protectedFace backwardFace)
-              || (joinFace backwardFace && protectedFace forwardFace)
-
-  joinFace (FaceId raw) = IntSet.member (fromIntegral raw) joinFaceSet
-  protectedFace (FaceId raw) = targetFaceProtected (fromIntegral raw)
-
 incidentInnerFaces
   :: Triangulation mode vertex directed undirected face
   -> UndirectedEdgeId
@@ -498,6 +467,8 @@
   -> GeometricBaseOrder
   -> Triangulation 'Constrained vertex () () ()
   -> Triangulation 'Constrained vertex () () ()
+  -> SeamFrontierIndex
+  -> SeamFrontierIndex
   -> SeamTangents
   -> Either BuildError
       ( Triangulation 'Constrained vertex () () ()
@@ -505,7 +476,7 @@
       , PublicationStats
       , V.Vector FaceId
       )
-executeResidentConstrained targetFaceProtected parameters vertexDefault order base extension tangents = do
+executeResidentConstrained targetFaceProtected parameters vertexDefault order base extension leftFrontier rightFrontier tangents = do
   let !baseDirected = numDirectedEdges base
       !extensionDirected = numDirectedEdges extension
       !baseVertices = numVertices base
@@ -521,8 +492,6 @@
         , seamUpperLeft = SeamTangent _ upperLeft
         , seamUpperRight = SeamTangent _ upperRight
         } = tangents
-  leftFrontier <- maybe (Left SeamFrontierUnavailable) Right (triSeamFrontier leftSource)
-  rightFrontier <- maybe (Left SeamFrontierUnavailable) Right (triSeamFrontier rightSource)
   bridgePlan <-
     planSeamBridgeSubdivision
       parameters
@@ -569,9 +538,9 @@
            mutable
            operation
            (Seq.length (seamFrontierEdges leftFrontier))
-           (seamTangentIndex (seamLowerLeft tangents))
+           (tangentIndex (seamLowerLeft tangents))
            (Seq.length (seamFrontierEdges rightFrontier))
-           (seamTangentIndex (seamLowerRight tangents))
+           (tangentIndex (seamLowerRight tangents))
            seamBase
            (leftVertexOffset + topologyAt leftSource (4 * upperLeft))
            (rightVertexOffset + topologyAt rightSource (4 * upperRight))
@@ -605,8 +574,9 @@
                            )
       )
   let newFaceSeeds = V.fromList (fmap (FaceId . fromIntegral) newFaces)
-      joinFaces = exactJoinFaces triangulation targetFaceProtected newFaceSeeds
-  certifyJoinBoundary triangulation targetFaceProtected joinFaces
+      (joinFaces, rejectedBoundaryPairs) =
+        exactJoinFaces triangulation targetFaceProtected newFaceSeeds
+  certifyJoinBoundary triangulation rejectedBoundaryPairs
   frontier <-
     combineSeamFrontier
       triangulation
@@ -628,38 +598,38 @@
 
 seamFaceEvidenceForSource
   :: Triangulation 'Constrained vertex () () ()
-  -> Triangulation 'Constrained vertex () () ()
   -> Int
   -> [FaceId]
   -> Either BuildError (V.Vector ConstrainedSeamFaceEvidence)
-seamFaceEvidenceForSource source target targetOffset sourceFaces =
+seamFaceEvidenceForSource source targetOffset sourceFaces =
   V.fromList <$> traverse evidenceFor sourceFaces
  where
   evidenceFor sourceFace = do
-    sourcePoints <- facePoints source sourceFace
+    (first, second, third) <- facePoints source sourceFace
     let targetFace = FaceId (unFaceId sourceFace + fromIntegral targetOffset)
-    targetPoints <- facePoints target targetFace
-    if sourcePoints == targetPoints
-      then
-        case sourcePoints of
-          [first, second, third] ->
-            Right
-              ConstrainedSeamFaceEvidence
-                { constrainedSeamSourceFace = sourceFace
-                , constrainedSeamTargetFace = targetFace
-                , constrainedSeamFaceFirstPoint = first
-                , constrainedSeamFaceSecondPoint = second
-                , constrainedSeamFaceThirdPoint = third
-                }
-          points -> Left (faceCardinalityFailure source sourceFace points)
-      else Left (SeamProtectedSourceFaceChanged sourceFace)
+    Right
+      ConstrainedSeamFaceEvidence
+        { constrainedSeamSourceFace = sourceFace
+        , constrainedSeamTargetFace = targetFace
+        , constrainedSeamFaceFirstPoint = first
+        , constrainedSeamFaceSecondPoint = second
+        , constrainedSeamFaceThirdPoint = third
+        }
 
+  facePoints
+    :: Triangulation mode sourceVertex directed undirected face
+    -> FaceId
+    -> Either BuildError (Point, Point, Point)
   facePoints triangulation faceHandle =
     case List.sort (fmap (vertexPointAt triangulation) (faceVertices triangulation faceHandle)) of
-      [first, second, third] -> Right [first, second, third]
-      points -> Left (faceCardinalityFailure triangulation faceHandle points)
+      [first, second, third] -> Right (first, second, third)
+      _ -> Left (faceCardinalityFailure triangulation faceHandle)
 
-  faceCardinalityFailure triangulation faceHandle _points =
+  faceCardinalityFailure
+    :: Triangulation mode sourceVertex directed undirected face
+    -> FaceId
+    -> BuildError
+  faceCardinalityFailure triangulation faceHandle =
     case faceDirectedEdges triangulation faceHandle of
       edge : _ -> RefinementInputTopologyInvalid (InnerFaceNotTriangularAtEdge edge)
       [] -> RefinementInputTopologyInvalid (FaceMissingAdjacentEdge faceHandle)
@@ -698,10 +668,13 @@
 -- The opposite-sign branch cannot overflow in its sum. The same-sign branch
 -- cannot overflow in its difference.
 data SeamTangent = SeamTangent
-  { seamTangentIndex :: {-# UNPACK #-} !Int
-  , seamTangentEdge :: {-# UNPACK #-} !Int
-  }
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
 
+tangentIndex :: SeamTangent -> Int
+tangentIndex (SeamTangent index _) = index
+{-# INLINE tangentIndex #-}
+
 data SeamTangents = SeamTangents
   { seamLowerLeft :: !SeamTangent
   , seamLowerRight :: !SeamTangent
@@ -760,12 +733,12 @@
   leftSize = Seq.length (seamFrontierEdges leftFrontier)
   rightSize = Seq.length (seamFrontierEdges rightFrontier)
 
-  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !reads = do
+  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !frontierReads = do
     nextLeftIndex <- pure (nextIndex leftSize leftIndex)
     previousRightIndex <- pure (previousIndex rightSize rightIndex)
     (nextLeftX, nextLeftY) <- frontierPointAt axis left leftFrontier nextLeftIndex
     (previousRightX, previousRightY) <- frontierPointAt axis right rightFrontier previousRightIndex
-    let !nextReads = reads + 2
+    let !nextReads = frontierReads + 2
         !leftBelow =
           orient2dCoordinates leftX leftY rightX rightY nextLeftX nextLeftY == LT
         !rightBelow =
@@ -812,12 +785,12 @@
   leftSize = Seq.length (seamFrontierEdges leftFrontier)
   rightSize = Seq.length (seamFrontierEdges rightFrontier)
 
-  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !reads = do
+  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !frontierReads = do
     previousLeftIndex <- pure (previousIndex leftSize leftIndex)
     nextRightIndex <- pure (nextIndex rightSize rightIndex)
     (previousLeftX, previousLeftY) <- frontierPointAt axis left leftFrontier previousLeftIndex
     (nextRightX, nextRightY) <- frontierPointAt axis right rightFrontier nextRightIndex
-    let !nextReads = reads + 2
+    let !nextReads = frontierReads + 2
         !leftAbove =
           orient2dCoordinates leftX leftY rightX rightY previousLeftX previousLeftY == GT
         !rightAbove =
diff --git a/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs b/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs
--- a/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 
 -- | Exact transient site sections derived from the authoritative coordinate
@@ -18,6 +19,8 @@
   , siteSetSymmetricDifferenceFromTriangulations
   , siteSetAssocs
   , siteSetPoints
+  , delaunayFromSiteSet
+  , rebuildCanonicalSiteSet
   ) where
 
 import Control.Monad.ST (ST, runST)
@@ -28,8 +31,10 @@
 import qualified Data.Vector as V
 import qualified Data.Vector.Unboxed as U
 import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Triangulation.BulkLoad (delaunayFromAdmittedDistinctAscending)
 import Moonlight.Triangulation.Dcel (numVertices, vertexData, vertexPoint)
 import Moonlight.Triangulation.Handles.HandleDefs (VertexId (..))
+import Moonlight.Triangulation.Internal.Canonical (canonicalizeKnownAscending)
 import Moonlight.Triangulation.Handles.Iterators.FixedIterators
   ( foldVertices'
   )
@@ -44,14 +49,20 @@
   , newMutablePointIndex
   , seedMutablePointIndex
   )
-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
+import Moonlight.Triangulation.Internal.Representation
+  ( BuildResult (buildTriangulation)
+  , Triangulation (..)
+  )
 import Moonlight.Triangulation.Internal.Types
   ( BuildError
+  , ConstraintMode (Unconstrained)
   , Point (..)
+  , QueryPoint (..)
   , SiteRelation (..)
+  , unitElementDefaults
   )
 
-newtype SiteSet annotation = SiteSet (Map.Map (Point) annotation)
+newtype SiteSet annotation = SiteSet (Map.Map QueryPoint annotation)
 
 -- Strict sufficient statistics for the one-pass ordered-map descent used by
 -- 'siteSetRelation'. Keeping the census strict prevents a relation query from
@@ -74,9 +85,11 @@
 siteSetFromTriangulation triangulation =
   SiteSet
     ( Map.fromList
-        [ ( Point
-              (triPointX triangulation `pagedUnsafeIndex` vertex)
-              (triPointY triangulation `pagedUnsafeIndex` vertex)
+        [ ( QueryPoint
+              ( Point
+                  (triPointX triangulation `pagedUnsafeIndex` vertex)
+                  (triPointY triangulation `pagedUnsafeIndex` vertex)
+              )
           , vertexData triangulation (VertexId (fromIntegral vertex))
           )
         | vertex <- [0 .. numVertices triangulation - 1]
@@ -93,9 +106,11 @@
 siteSupportFromTriangulation triangulation =
   SiteSet
     ( Map.fromList
-        [ ( Point
-              (triPointX triangulation `pagedUnsafeIndex` vertex)
-              (triPointY triangulation `pagedUnsafeIndex` vertex)
+        [ ( QueryPoint
+              ( Point
+                  (triPointX triangulation `pagedUnsafeIndex` vertex)
+                  (triPointY triangulation `pagedUnsafeIndex` vertex)
+              )
           , ()
           )
         | vertex <- [0 .. numVertices triangulation - 1]
@@ -254,7 +269,7 @@
   fmap (SiteSet . Map.fromList) (runST collectExclusiveAssociations)
  where
   collectExclusiveAssociations
-    :: forall state. ST state (Either BuildError [(Point, annotation)])
+    :: forall state. ST state (Either BuildError [(QueryPoint, annotation)])
   collectExclusiveAssociations = do
     pointIndex <- newMutablePointIndex (numVertices indexed)
     seeded <-
@@ -282,14 +297,14 @@
     collectSourceExclusive
       :: MutablePointIndex state
       -> MUV.MVector state Bool
-      -> [(Point, annotation)]
+      -> [(QueryPoint, annotation)]
       -> Int
       -> Double
-      -> ST state [(Point, annotation)]
+      -> ST state [(QueryPoint, annotation)]
     collectSourceExclusive pointIndex matchedIndexedVertices associations rawVertex x = do
       let vertex = VertexId (fromIntegral rawVertex)
           y = triPointY source `pagedUnsafeIndex` rawVertex
-          point = Point x y
+          point = QueryPoint (Point x y)
       match <-
         lookupMutablePoint
           pointIndex
@@ -305,13 +320,13 @@
 
     collectIndexedExclusive
       :: MUV.MVector state Bool
-      -> [(Point, annotation)]
+      -> [(QueryPoint, annotation)]
       -> Int
       -> Double
-      -> ST state [(Point, annotation)]
+      -> ST state [(QueryPoint, annotation)]
     collectIndexedExclusive matchedIndexedVertices associations rawVertex x = do
       let vertex = VertexId (fromIntegral rawVertex)
-          point = Point x (triPointY indexed `pagedUnsafeIndex` rawVertex)
+          point = QueryPoint (Point x (triPointY indexed `pagedUnsafeIndex` rawVertex))
       matched <- MUV.unsafeRead matchedIndexedVertices rawVertex
       pure
         ( if matched
@@ -354,10 +369,32 @@
     (triPointIndex triangulation)
 {-# INLINE lookupPointIn #-}
 
-siteSetAssocs :: SiteSet annotation -> [(Point, annotation)]
+siteSetAssocs :: SiteSet annotation -> [(QueryPoint, annotation)]
 siteSetAssocs (SiteSet sites) = Map.toAscList sites
 {-# INLINE siteSetAssocs #-}
 
 siteSetPoints :: SiteSet annotation -> V.Vector (Point)
-siteSetPoints (SiteSet sites) = V.fromList (Map.keys sites)
+siteSetPoints (SiteSet sites) = V.fromList (fmap queryPointValue (Map.keys sites))
 {-# INLINE siteSetPoints #-}
+
+-- | Publish an admitted exact site section through the one bulk-construction
+-- engine. Its Map order is the trusted strict lexicographic vertex order; the
+-- circle sweep still establishes the algorithm's radial execution order.
+delaunayFromSiteSet
+  :: SiteSet annotation
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+delaunayFromSiteSet sites =
+  buildTriangulation
+    <$> delaunayFromAdmittedDistinctAscending
+      unitElementDefaults
+      (V.fromList (siteSetAssocs sites))
+{-# INLINE delaunayFromSiteSet #-}
+
+-- | Canonical finite-set publication. The preceding SiteSet build preserves
+-- lexicographic vertex order, so Canonical need not rediscover that fact.
+rebuildCanonicalSiteSet
+  :: SiteSet annotation
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+rebuildCanonicalSiteSet sites =
+  delaunayFromSiteSet sites >>= canonicalizeKnownAscending
+{-# INLINE rebuildCanonicalSiteSet #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Refinement.hs b/src-build/Moonlight/Triangulation/Internal/Refinement.hs
--- a/src-build/Moonlight/Triangulation/Internal/Refinement.hs
+++ b/src-build/Moonlight/Triangulation/Internal/Refinement.hs
@@ -23,7 +23,7 @@
 import Data.List (find)
 import qualified Data.List as List
 import qualified Data.IntMap.Strict as IntMap
-import Data.Maybe (fromMaybe)
+import Data.Maybe (catMaybes, fromMaybe)
 import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)
 import qualified Data.Vector.Unboxed.Mutable as MUV
 import Data.Word (Word32)
@@ -258,7 +258,9 @@
 
 data FaceHint
   = FaceAcceptable
-  | FaceMustSplitLongestEdge {-# UNPACK #-} !Int
+  | FaceMustSplitLongestEdge
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Double
   | FaceMustRefine
   | FaceShouldRefine
 
@@ -344,7 +346,7 @@
   wsCavityWork <- newGrowableWord32 16
   let workspace = Workspace{wsQueue, wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts, wsForcedSplits, wsEncroachment, wsEncroachmentSlot, wsSegmentOrigin, wsCavityEpoch, wsCavityFaceMarks, wsCavityPairMarks, wsCavityVertexMarks, wsCavityFaces, wsCavityInternal, wsCavityBoundary, wsCavityCocircular, wsCavityChain, wsCavityWork}
   seedInitialWork workspace initialSeed
-  loop workspace 0
+  loop workspace IntMap.empty 0
  where
   !limit = max 0 budget
 
@@ -412,9 +414,13 @@
         writeSTRef wsCavityEpoch next
         pure next
 
-  loop :: Workspace s -> Int -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
-  loop workspace@Workspace{wsQueue, wsForcedSplits} !added
-    | added >= limit = finish workspace False added
+  loop
+    :: Workspace s
+    -> IntMap.IntMap Double
+    -> Int
+    -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
+  loop workspace@Workspace{wsQueue, wsForcedSplits} pendingOversizedEdges !added
+    | added >= limit = finish workspace pendingOversizedEdges False added
     | otherwise = do
         forced <- popGrowableOr noIndex wsForcedSplits
         if forced /= noIndex
@@ -424,14 +430,14 @@
               Left obstruction -> pure (Left obstruction)
               Right split ->
                 if split
-                  then loop workspace (added + 1)
+                  then loop workspace pendingOversizedEdges (added + 1)
                   else do
                     -- The split refused (degenerate position or a kept
                     -- constraint). Retrying the face that forced it would loop,
                     -- so one queued face is sacrificed, exactly the face whose
                     -- requeue sits on top.
                     _ <- popFace wsQueue
-                    loop workspace added
+                    loop workspace pendingOversizedEdges added
           else do
             candidate <- popEncroachment workspace
             if candidate /= noIndex
@@ -439,26 +445,126 @@
                 splitOutcome <- checkEncroachment workspace (fromIntegral candidate)
                 case splitOutcome of
                   Left obstruction -> pure (Left obstruction)
-                  Right split -> loop workspace (if split then added + 1 else added)
+                  Right split ->
+                    loop
+                      workspace
+                      pendingOversizedEdges
+                      (if split then added + 1 else added)
               else do
                 next <- popFace wsQueue
                 case next of
-                  Nothing -> finish workspace True added
+                  Nothing -> finish workspace pendingOversizedEdges True added
                   Just face -> do
                     outcome <- handleFace workspace face
                     case outcome of
                       Left failure -> pure (Left failure)
-                      Right gained -> loop workspace (added + gained)
+                      Right (gained, pendingEdge) ->
+                        loop
+                          workspace
+                          ( maybe
+                              pendingOversizedEdges
+                              (\(pair, actualSquaredLength) ->
+                                 IntMap.insertWith
+                                   max
+                                   pair
+                                   actualSquaredLength
+                                   pendingOversizedEdges
+                              )
+                              pendingEdge
+                          )
+                          (added + gained)
 
-  finish :: Workspace s -> Bool -> Int -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
-  finish Workspace{wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts} complete added = do
-    excludedFaces <- markedIndices wsExcluded
-    visitedFaces <- maybe (pure []) markedIndices wsVisited
-    permittedFaces <- maybe (pure []) markedIndices wsPermitted
-    interfaceBoundaryReads <- readSTRef wsInterfaceBoundaryReads
-    boundaryCrossingAttempts <- readSTRef wsBoundaryCrossingAttempts
-    pure (Right (complete, added, excludedFaces, visitedFaces, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts))
+  finish
+    :: Workspace s
+    -> IntMap.IntMap Double
+    -> Bool
+    -> Int
+    -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
+  finish workspace@Workspace{wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts} pendingOversizedEdges complete added = do
+    pendingObstruction <-
+      if complete
+        then completedMaximumEdgeObstruction workspace pendingOversizedEdges
+        else pure (Right Nothing)
+    case pendingObstruction of
+      Left failure -> pure (Left failure)
+      Right (Just failure) -> pure (Left failure)
+      Right Nothing -> do
+        excludedFaces <- markedIndices wsExcluded
+        visitedFaces <- maybe (pure []) markedIndices wsVisited
+        permittedFaces <- maybe (pure []) markedIndices wsPermitted
+        interfaceBoundaryReads <- readSTRef wsInterfaceBoundaryReads
+        boundaryCrossingAttempts <- readSTRef wsBoundaryCrossingAttempts
+        pure (Right (complete, added, excludedFaces, visitedFaces, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts))
 
+  -- A quality edge that cannot be split is remembered at the point where the
+  -- worklist discovers it.  Only a drained run discharges the pending section;
+  -- a budget-limited run remains an ordinary resumable publication.  The
+  -- final face is read from the constant-degree live edge neighbourhood so the
+  -- reported obstruction retains the former face/edge ordering without an
+  -- all-face audit.
+  completedMaximumEdgeObstruction
+    :: Workspace s
+    -> IntMap.IntMap Double
+    -> ST s (Either BuildError (Maybe BuildError))
+  completedMaximumEdgeObstruction workspace pendingOversizedEdges =
+    case refineMaxEdgeLength parameters of
+      Nothing -> pure (Right Nothing)
+      Just maximumLength -> do
+        candidateResults <-
+          traverse
+            (liveOversizedEdgeCandidate workspace)
+            (IntMap.toAscList pendingOversizedEdges)
+        pure $ do
+          candidates <- catMaybes <$> sequence candidateResults
+          Right
+            ( case List.sortOn (\(face, pair, _) -> (face, pair)) candidates of
+                [] -> Nothing
+                (face, pair, actualSquaredLength) : _ ->
+                  Just
+                    ( RefinementOversizedEdge
+                        (FaceId (fromIntegral face))
+                        (UndirectedEdgeId (fromIntegral pair))
+                        (sqrt actualSquaredLength)
+                        maximumLength
+                    )
+            )
+
+  liveOversizedEdgeCandidate
+    :: Workspace s
+    -> (Int, Double)
+    -> ST s (Either BuildError (Maybe (Int, Int, Double)))
+  liveOversizedEdgeCandidate workspace (pair, actualSquaredLength) = do
+    directedTotal <- directedEdgeCount mutable
+    let directed = 2 * pair
+    if directed < 0 || directed + 1 >= directedTotal
+      then pure (Left RefinementDomainTopologyChanged)
+      else do
+        forwardFace <- readFace mutable directed
+        backwardFace <- readFace mutable (reverseIndex directed)
+        eligibleFaces <-
+          filterM
+            (maximumEdgeFaceEligible workspace)
+            [forwardFace, backwardFace]
+        pure
+          ( Right
+              ( case eligibleFaces of
+                  [] -> Nothing
+                  _ -> Just (minimum eligibleFaces, pair, actualSquaredLength)
+              )
+          )
+
+  maximumEdgeFaceEligible :: Workspace s -> Int -> ST s Bool
+  maximumEdgeFaceEligible Workspace{wsExcluded, wsPermitted} face
+    | face <= 0 = pure False
+    | otherwise = do
+        excluded <- readBoolMark wsExcluded face
+        permitted <-
+          maybe
+            (pure True)
+            (\membership -> readBoolMark membership face)
+            wsPermitted
+        pure (not excluded && permitted)
+
   markedIndices :: BoolMarks s -> ST s [Int]
   markedIndices marks =
     case marks of
@@ -576,7 +682,10 @@
         let (!longestEdge, !longestSquaredLength, !side01, !side12, !side20) =
               faceEdgeMetrics e0 e1 e2 p0 p1 p2
          in if exceeds longestSquaredLength maximumSquaredLength
-              then FaceMustSplitLongestEdge (longestEdge `quot` 2)
+              then
+                FaceMustSplitLongestEdge
+                  (longestEdge `quot` 2)
+                  longestSquaredLength
               else
                 nonEdgeHint
                   ( ratioBad
@@ -604,7 +713,10 @@
       | angleBad = FaceShouldRefine
       | otherwise = FaceAcceptable
 
-  handleFace :: Workspace s -> Int -> ST s (Either BuildError Int)
+  handleFace
+    :: Workspace s
+    -> Int
+    -> ST s (Either BuildError (Int, Maybe (Int, Double)))
   handleFace workspace@Workspace{wsExcluded, wsPermitted, wsVisited} face = do
     addCounter operation CounterRefinementQueuePops 1
     permitted <-
@@ -618,7 +730,7 @@
         traverse_ (\visited -> writeBoolMark visited face True) wsVisited
         skip <- readBoolMark wsExcluded face
         if skip
-          then pure (Right 0)
+          then pure (Right (0, Nothing))
           else do
             addCounter operation CounterRefinementFaceChecks 1
             (e0, e1, e2) <- faceEdges mutable face
@@ -626,14 +738,21 @@
             p1 <- edgeOriginPoint mutable e1
             p2 <- edgeOriginPoint mutable e2
             case faceHint e0 e1 e2 p0 p1 p2 of
-              FaceAcceptable -> pure (Right 0)
-              FaceMustSplitLongestEdge pair -> qualitySplit workspace pair
-              FaceMustRefine -> attemptCandidate workspace face p0 p1 p2
+              FaceAcceptable -> pure (Right (0, Nothing))
+              FaceMustSplitLongestEdge pair actualSquaredLength ->
+                qualitySplit workspace pair actualSquaredLength
+              FaceMustRefine ->
+                fmap
+                  (fmap (\gained -> (gained, Nothing)))
+                  (attemptCandidate workspace face p0 p1 p2)
               FaceShouldRefine -> do
                 blocked <- inputAngleBlocks workspace e0 e1 e2 p0 p1 p2
                 if blocked
-                  then pure (Right 0)
-                  else attemptCandidate workspace face p0 p1 p2
+                  then pure (Right (0, Nothing))
+                  else
+                    fmap
+                      (fmap (\gained -> (gained, Nothing)))
+                      (attemptCandidate workspace face p0 p1 p2)
 
   faceEdgeMetrics
     :: Int
@@ -654,17 +773,21 @@
     result :: Int -> Double -> (Int, Double, Double, Double, Double)
     result edge longest = (edge, longest, side01, side12, side20)
 
-  -- An overlong edge on a local interface is a publish-time quality
-  -- obstruction, not a permission to cross the frozen section. Let the
-  -- completed-domain audit report that exact edge; a budget-limited run stays
-  -- resumable and returns its ordinary incomplete result.
-  qualitySplit :: Workspace s -> Int -> ST s (Either BuildError Int)
-  qualitySplit workspace pair = do
+  -- An unsplittable quality edge is a pending law obstruction, not permission
+  -- to cross a frozen interface and not immediate failure of a resumable run.
+  qualitySplit
+    :: Workspace s
+    -> Int
+    -> Double
+    -> ST s (Either BuildError (Int, Maybe (Int, Double)))
+  qualitySplit workspace pair actualSquaredLength = do
     outcome <- resolveSplit SplitQualityEdge workspace pair
     case outcome of
-      Left (RefinementDomainWouldCrossInterface _ _) -> pure (Right 0)
+      Left (RefinementDomainWouldCrossInterface _ _) ->
+        pure (Right (0, Just (pair, actualSquaredLength)))
       Left failure -> pure (Left failure)
-      Right split -> pure (Right (if split then 1 else 0))
+      Right True -> pure (Right (1, Nothing))
+      Right False -> pure (Right (0, Just (pair, actualSquaredLength)))
 
   -- Two subsegments of one original segment meeting at a small input angle
   -- cannot be refined apart: the angle is fixed by the input geometry. When
diff --git a/src-build/Moonlight/Triangulation/Refinement.hs b/src-build/Moonlight/Triangulation/Refinement.hs
--- a/src-build/Moonlight/Triangulation/Refinement.hs
+++ b/src-build/Moonlight/Triangulation/Refinement.hs
@@ -34,13 +34,11 @@
 import Moonlight.Triangulation.FloodFillIterator (facesAtEvenBarrierDepth)
 import Moonlight.Triangulation.Handles.HandleDefs
   ( FaceId (..)
-  , DirectedEdgeId
   , UndirectedEdgeId (..)
   , asUndirected
   , directedPair
   , reverseEdge
   )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
 import Moonlight.Triangulation.Internal.Mutable
 import Moonlight.Triangulation.Internal.Paged
   ( PublicationStats
@@ -49,7 +47,6 @@
 import Moonlight.Triangulation.Internal.Refinement
 import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
 import Moonlight.Triangulation.Internal.Transaction (runTransactionWithPublication)
-import Moonlight.Triangulation.Math (squaredDistanceWide)
 import Moonlight.Triangulation.Types
 import Moonlight.Triangulation.Validation
   ( validateTopology
@@ -100,38 +97,22 @@
   fmap refinementExecutionResult
     . refineWithInitialSeed RefineEveryFace Nothing makeVertex parameters
 
--- | Prove that a set of active faces is separated from every protected inner
--- face by exactly the supplied immutable edge section. The witness is tied to
--- the input arena cardinalities and is revalidated before interpretation.
+-- | Derive the exact immutable interface separating a checked active face
+-- section from every protected inner face.  A permitted face set determines
+-- this cut uniquely, so callers cannot supply a second claim about Gamma.
 mkRefinementDomain
   :: Set.Set FaceId
-  -> Set.Set UndirectedEdgeId
   -> Triangulation mode vertex directed undirected face
   -> Either BuildError RefinementDomain
-mkRefinementDomain permittedFaces interfaceEdges triangulation = do
+mkRefinementDomain permittedFaces triangulation = do
   permitted <- validateSeedFaces triangulation permittedFaces
-  interface <- IntSet.fromList <$> traverse validateInterfaceEdge (Set.toAscList interfaceEdges)
-  let expected = expectedInterface permitted
-  case IntSet.lookupMin (expected IntSet.\\ interface) of
-    Just pair -> Left (RefinementDomainInterfaceMissing (UndirectedEdgeId (fromIntegral pair)))
-    Nothing ->
-      case IntSet.lookupMin (interface IntSet.\\ expected) of
-        Just pair -> Left (RefinementDomainInterfaceExtraneous (UndirectedEdgeId (fromIntegral pair)))
-        Nothing ->
-          Right
-            RefinementDomain
-              { refinementDomainPermittedFaces = permitted
-              , refinementDomainInterfacePairs = interface
-              , refinementDomainInputFaceCount = numFaces triangulation
-              }
+  Right
+    RefinementDomain
+      { refinementDomainPermittedFaces = permitted
+      , refinementDomainInterfacePairs = expectedInterface permitted
+      , refinementDomainInputFaceCount = numFaces triangulation
+      }
  where
-  totalEdges = numUndirectedEdges triangulation
-  validateInterfaceEdge :: UndirectedEdgeId -> Either BuildError Int
-  validateInterfaceEdge edge@(UndirectedEdgeId raw)
-    | toInteger raw >= toInteger totalEdges =
-        Left (RefinementDomainInterfaceEdgeNotActive edge totalEdges)
-    | otherwise = Right (fromIntegral raw)
-
   expectedInterface permitted =
     IntSet.fromList
       [ pair
@@ -154,12 +135,11 @@
      (Point -> vertex)
   -> RefinementParameters
   -> Set.Set FaceId
-  -> Set.Set UndirectedEdgeId
   -> Triangulation mode vertex directed undirected face
   -> Either BuildError (RefinementDomainResult mode vertex directed undirected face)
-refineWithinDomain makeVertex parameters permittedFaces interfaceEdges triangulation = do
+refineWithinDomain makeVertex parameters permittedFaces triangulation = do
   validateDomainParameters parameters
-  domain <- mkRefinementDomain permittedFaces interfaceEdges triangulation
+  domain <- mkRefinementDomain permittedFaces triangulation
   execution <-
     refineWithInitialSeed
       (RefineSeededFaces (refinementDomainPermittedFaces domain))
@@ -168,7 +148,7 @@
       parameters
       triangulation
   let result = refinementExecutionResult execution
-  validationStats <- validateDomainClosure
+  (validationStats, finalInterfaceIncidence) <- validateDomainClosure
     domain
     triangulation
     (refinedTriangulation result)
@@ -180,6 +160,7 @@
           (refinedTriangulation result)
           (refinementExecutionVisitedFaces execution)
           (refinementExecutionPermittedFaces execution)
+          finalInterfaceIncidence
           (refinementExecutionInterfaceBoundaryReads execution)
           (refinementExecutionBoundaryCrossingAttempts execution)
           (refinementExecutionPublicationStats execution)
@@ -226,11 +207,6 @@
 refineWithInitialSeed initialSeed domain makeVertex parameters triangulation = do
   validateRefinementParameters parameters
   case domain of
-    Just _
-      | refineMaxAdditionalVertices parameters == Nothing ->
-          Left RefinementDomainRequiresFiniteVertexBudget
-    _ -> Right ()
-  case domain of
     Just _ -> pure ()
     Nothing ->
       case validateTopology triangulation of
@@ -272,18 +248,6 @@
       removeInterfaceBarriers mutable triangulation domain
       pure refinement
   let (complete, added, excluded, visited, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts) = outcome
-      excludedFaceSet = IntSet.fromList excluded
-      finalAuditFaces =
-        case domain of
-          Nothing ->
-            [ face
-            | face@(FaceId raw) <- innerFaces frozen
-            , IntSet.notMember (fromIntegral raw) excludedFaceSet
-            ]
-          Just _ -> fmap (FaceId . fromIntegral) permittedFaces
-  if complete
-    then validateMaximumEdgeLength parameters frozen finalAuditFaces
-    else Right ()
   pure
     RefinementExecution
       { refinementExecutionResult =
@@ -334,17 +298,19 @@
   -> Triangulation mode vertex directed undirected face
   -> [Int]
   -> [Int]
+  -> V.Vector (UndirectedEdgeId, FaceId, FaceId)
   -> Int
   -> Int
   -> PublicationStats
   -> ValidationClosureStats
   -> RefinementReceipt
-buildRefinementReceipt domain before after visited permittedFaces interfaceBoundaryReads boundaryCrossingAttempts publicationStats validationStats =
+buildRefinementReceipt domain before after visited permittedFaces finalInterfaceIncidence interfaceBoundaryReads boundaryCrossingAttempts publicationStats validationStats =
   RefinementReceipt
     { refinementVisitedJoinFaces = V.fromList (fmap toFace visitedJoin)
     , refinementVisitedProtectedFaces = V.fromList (fmap toFace visitedProtected)
     , refinementCreatedFaces = V.fromList (fmap toFace createdFaces)
     , refinementFinalPermittedFaces = V.fromList (fmap toFace permittedFaces)
+    , refinementFinalInterfaceIncidence = finalInterfaceIncidence
     , refinementTouchedEdges = V.fromList (fmap toEdge touchedEdges)
     , refinementRemovedEdges = V.fromList (fmap toEdge removedEdges)
     , refinementInterfaceBoundaryReads = interfaceBoundaryReads
@@ -438,13 +404,14 @@
   -> Triangulation mode vertex directed undirected face
   -> Triangulation mode vertex directed undirected face
   -> [Int]
-  -> Either BuildError ValidationClosureStats
+  -> Either BuildError (ValidationClosureStats, V.Vector (UndirectedEdgeId, FaceId, FaceId))
 validateDomainClosure domain before after finalPermittedFaces = do
-  let (closureStats, closureViolations) =
+  let finalPermitted = IntSet.fromList finalPermittedFaces
+      (closureStats, closureViolations) =
         validateTopologyClosureWithStats
           ( IntSet.union
               (refinementDomainPermittedFaces domain)
-              (IntSet.fromList finalPermittedFaces)
+              finalPermitted
           )
           (refinementDomainInterfacePairs domain)
           after
@@ -452,8 +419,12 @@
     violation : _ -> Left (RefinementInputTopologyInvalid violation)
     [] -> pure ()
   traverse_ validateProtectedFace (IntSet.toAscList protectedFaces)
-  traverse_ validateInterfaceEdge (IntSet.toAscList (refinementDomainInterfacePairs domain))
-  pure closureStats
+  finalInterfaceIncidence <-
+    V.fromList
+      <$> traverse
+        (validateInterfaceEdge finalPermitted)
+        (IntSet.toAscList (refinementDomainInterfacePairs domain))
+  pure (closureStats, finalInterfaceIncidence)
  where
   permitted = refinementDomainPermittedFaces domain
   protectedFaces =
@@ -475,67 +446,38 @@
       then Right ()
       else Left (RefinementDomainProtectedFaceChanged (FaceId (fromIntegral rawFace)))
 
-  validateInterfaceEdge rawPair =
-    if interfaceEdgePreserved rawPair
-      then Right ()
-      else Left RefinementDomainTopologyChanged
-
-  interfaceEdgePreserved rawPair =
-    case (localEdgeSignature before rawPair, localEdgeSignature after rawPair) of
+  validateInterfaceEdge finalPermitted rawPair =
+    let edge = UndirectedEdgeId (fromIntegral rawPair)
+     in case (localEdgeSignature before rawPair, localEdgeSignature after rawPair) of
       ( Just (beforeFrom, beforeTo, beforeFaces)
         , Just (afterFrom, afterTo, afterFaces)
-        ) ->
-          beforeFrom == afterFrom
-            && beforeTo == afterTo
-            && all (`elem` afterFaces) (filter protected beforeFaces)
-      _ -> False
+        )
+          | beforeFrom == afterFrom && beforeTo == afterTo ->
+              validateInterfaceFaces finalPermitted edge beforeFaces afterFaces
+      _ -> Left RefinementDomainTopologyChanged
 
+  validateInterfaceFaces finalPermitted edge beforeFaces afterFaces =
+    case filter protected beforeFaces of
+      [protectedFace]
+        | protectedFace `elem` afterFaces ->
+            case filter (/= protectedFace) afterFaces of
+              [oppositeFace]
+                | IntSet.member (fromIntegral (unFace oppositeFace)) finalPermitted ->
+                    Right (edge, protectedFace, oppositeFace)
+                | otherwise ->
+                    Left
+                      ( RefinementDomainInterfaceOppositeFaceNotPermitted
+                          edge
+                          oppositeFace
+                      )
+              _ -> Left RefinementDomainTopologyChanged
+      _ -> Left RefinementDomainTopologyChanged
+
   protected face =
     let raw = fromIntegral (unFace face)
      in raw > 0 && IntSet.notMember raw permitted
 
   unFace (FaceId raw) = raw
-
--- | A drained worklist is publishable only when its final active face section
--- satisfies the optional edge-length law. Global refinement checks every
--- active inner face; local refinement checks only its exact dynamic permitted
--- witness, never the retired handles from the input cover.
-validateMaximumEdgeLength
-  :: RefinementParameters
-  -> Triangulation mode vertex directed undirected face
-  -> [FaceId]
-  -> Either BuildError ()
-validateMaximumEdgeLength parameters triangulation finalPermittedFaces =
-  case refineMaxEdgeLength parameters of
-    Nothing -> Right ()
-    Just maximumLength ->
-      traverse_
-        (validateFace maximumLength (maximumLength * maximumLength))
-        finalPermittedFaces
- where
-  validateFace :: Double -> Double -> FaceId -> Either BuildError ()
-  validateFace maximumLength maximumSquaredLength face =
-    traverse_ (validateEdge maximumLength maximumSquaredLength face)
-      (faceDirectedEdges triangulation face)
-
-  validateEdge :: Double -> Double -> FaceId -> DirectedEdgeId -> Either BuildError ()
-  validateEdge maximumLength maximumSquaredLength face edge =
-    let undirected = asUndirected edge
-        (fromVertex, toVertex) = undirectedEndpoints triangulation undirected
-        actualSquaredLength =
-          squaredDistanceWide
-            (vertexPoint triangulation fromVertex)
-            (vertexPoint triangulation toVertex)
-     in if actualSquaredLength > maximumSquaredLength
-          then
-            Left
-              ( RefinementOversizedEdge
-                  face
-                  undirected
-                  (sqrt actualSquaredLength)
-                  maximumLength
-              )
-          else Right ()
 
 -- | Refuse outer or absent faces rather than silently treating an invalid
 -- topology witness as an empty repair. Face zero is the outer face and has no
diff --git a/src-build/Moonlight/Triangulation/Session.hs b/src-build/Moonlight/Triangulation/Session.hs
--- a/src-build/Moonlight/Triangulation/Session.hs
+++ b/src-build/Moonlight/Triangulation/Session.hs
@@ -12,7 +12,7 @@
   , insertVertex
   , insertVertexAt
   , insertVertexAtNear
-  , insertVertexAtCombining
+  , insertAdmittedVertexAtCombining
   , removeAt
   , removeAtNear
   , removeManyAt
@@ -26,7 +26,10 @@
 import Data.Bits (xor)
 import qualified Data.Vector as V
 import Moonlight.Triangulation.Handles.HandleDefs
-import Moonlight.Triangulation.Insertion (insertPointCombining)
+import Moonlight.Triangulation.Insertion
+  ( insertAdmittedPointCombining
+  , insertPointCombining
+  )
 import Moonlight.Triangulation.Internal.Excision (removeMutable)
 import Moonlight.Triangulation.Internal.Location (MutableLocation (..), locateMutable)
 import Moonlight.Triangulation.Internal.Mutable
@@ -147,18 +150,18 @@
        payload
     )
 
--- | Insert at a site while combining an occupied site's resident annotation
--- with the incoming one. Geometry still owns identity; this supplies only the
--- overlap law used by annotated joins.
-insertVertexAtCombining
+-- | Insert an admitted site while combining an occupied annotation. This is
+-- package-internal descent from a boundary that already owns coordinate
+-- admission; only location and annotation gluing remain here.
+insertAdmittedVertexAtCombining
   :: (vertex -> vertex -> vertex)
-  -> Point
+  -> QueryPoint
   -> vertex
   -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
-insertVertexAtCombining combine point payload = Session $ \mesh operation ->
+insertAdmittedVertexAtCombining combine queryPoint payload = Session $ \mesh operation ->
   fmap
     (fmap (\(vertex, disposition) -> (VertexId (fromIntegral vertex), disposition)))
-    (insertPointCombining combine Nothing mesh operation point payload)
+    (insertAdmittedPointCombining combine Nothing mesh operation queryPoint payload)
 
 -- | Remove the vertex standing at a point, answering 'Nothing' when no vertex
 -- stands there.
diff --git a/src-build/Moonlight/Triangulation/SetAlgebra.hs b/src-build/Moonlight/Triangulation/SetAlgebra.hs
--- a/src-build/Moonlight/Triangulation/SetAlgebra.hs
+++ b/src-build/Moonlight/Triangulation/SetAlgebra.hs
@@ -20,9 +20,9 @@
 import Moonlight.Triangulation.Dcel (numVertices, vertexData, vertexPoint, vertexPoints)
 import Moonlight.Triangulation.Handles.Iterators.FixedIterators (vertices)
 import Moonlight.Triangulation.Internal.Join (joinBalanced, joinNormalForm)
-import Moonlight.Triangulation.Internal.Join.Rebuild (rebuildCanonicalSiteSet)
 import Moonlight.Triangulation.Internal.Join.SiteSet
-  ( siteSetDifference
+  ( rebuildCanonicalSiteSet
+  , siteSetDifference
   , siteSetFromTriangulation
   , siteSetIntersectionWith
   , siteSetPoints
diff --git a/src-capi/Moonlight/Triangulation/Foreign/Exports.hs b/src-capi/Moonlight/Triangulation/Foreign/Exports.hs
deleted file mode 100644
--- a/src-capi/Moonlight/Triangulation/Foreign/Exports.hs
+++ /dev/null
@@ -1,75 +0,0 @@
-{-# LANGUAGE ForeignFunctionInterface #-}
-{-# OPTIONS_GHC -Wno-missing-signatures #-}
-
-module Moonlight.Triangulation.Foreign.Exports where
-
-import Data.Int (Int64)
-import Data.Word (Word32)
-import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))
-import Foreign.Ptr (Ptr)
-import Moonlight.Triangulation.Foreign.ABI
-  ( CMesh
-  , CMinkowskiReceipt
-  , CObstruction
-  , CRegion
-  , CStructuringElement
-  )
-import qualified Moonlight.Triangulation.Foreign.ABI as ABI
-
-delaunayF64 = ABI.delaunayF64
-meshInsertManyF64 = ABI.meshInsertManyF64
-meshSiteUnion = ABI.meshSiteUnion
-meshSiteIntersection = ABI.meshSiteIntersection
-meshSiteDifference = ABI.meshSiteDifference
-meshSiteSymmetricDifference = ABI.meshSiteSymmetricDifference
-meshVertexCount = ABI.meshVertexCount
-meshTriangleCount = ABI.meshTriangleCount
-meshCopyVerticesF64 = ABI.meshCopyVerticesF64
-meshCopyTrianglesU32 = ABI.meshCopyTrianglesU32
-meshFree = ABI.meshFree
-regionCreateF64 = ABI.regionCreateF64
-regionCounts = ABI.regionCounts
-regionCopyF64 = ABI.regionCopyF64
-regionUnion = ABI.regionUnion
-regionIntersection = ABI.regionIntersection
-regionDifference = ABI.regionDifference
-regionSymmetricDifference = ABI.regionSymmetricDifference
-regionLocatePointF64 = ABI.regionLocatePointF64
-regionMeasure = ABI.regionMeasure
-regionFree = ABI.regionFree
-structuringElementCreateF64 = ABI.structuringElementCreateF64
-structuringElementFree = ABI.structuringElementFree
-regionMinkowskiSum = ABI.regionMinkowskiSum
-regionOffset = ABI.regionOffset
-regionInset = ABI.regionInset
-regionOpen = ABI.regionOpen
-regionClose = ABI.regionClose
-
-foreign export ccall "ml_delaunay_f64" delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_insert_many_f64" meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_union" meshSiteUnion :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_intersection" meshSiteIntersection :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_difference" meshSiteDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_symmetric_difference" meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_vertex_count" meshVertexCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_triangle_count" meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_copy_vertices_f64" meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_copy_triangles_u32" meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_free" meshFree :: Ptr CMesh -> IO ()
-foreign export ccall "ml_region_create_f64" regionCreateF64 :: Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_counts" regionCounts :: Ptr CRegion -> Ptr CSize -> Ptr CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_copy_f64" regionCopyF64 :: Ptr CRegion -> Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_union" regionUnion :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_intersection" regionIntersection :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_difference" regionDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_symmetric_difference" regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_locate_point_f64" regionLocatePointF64 :: Ptr CRegion -> CDouble -> CDouble -> Ptr CUInt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_measure" regionMeasure :: Ptr CRegion -> Ptr Int64 -> Ptr CChar -> CSize -> Ptr CSize -> Ptr CDouble -> Ptr CDouble -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_free" regionFree :: Ptr CRegion -> IO ()
-foreign export ccall "ml_structuring_element_create_f64" structuringElementCreateF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CStructuringElement) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_structuring_element_free" structuringElementFree :: Ptr CStructuringElement -> IO ()
-foreign export ccall "ml_region_minkowski_sum" regionMinkowskiSum :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_offset" regionOffset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_inset" regionInset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_open" regionOpen :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_close" regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
diff --git a/src-dcel/Moonlight/Triangulation/Exact.hs b/src-dcel/Moonlight/Triangulation/Exact.hs
--- a/src-dcel/Moonlight/Triangulation/Exact.hs
+++ b/src-dcel/Moonlight/Triangulation/Exact.hs
@@ -131,14 +131,13 @@
 -- callers must certify the candidate projection before relying on it.
 exactPointToEmbeddingCandidate
   :: ExactPoint
-  -> Either PointValidationError Point
+  -> Either PointValidationError QueryPoint
 exactPointToEmbeddingCandidate (ExactPoint x y) =
-  queryPointValue
-    <$> mkQueryPoint
-      ( Point
-          (integerRatioToDouble (exactRationalNumerator x) (exactRationalDenominator x))
-          (integerRatioToDouble (exactRationalNumerator y) (exactRationalDenominator y))
-      )
+  mkQueryPoint
+    ( Point
+        (integerRatioToDouble (exactRationalNumerator x) (exactRationalDenominator x))
+        (integerRatioToDouble (exactRationalNumerator y) (exactRationalDenominator y))
+    )
 
 -- | Exact orientation of an ordered triple. 'GT' is a positive determinant
 -- and counter-clockwise turn, 'EQ' is collinear, and 'LT' is clockwise.
diff --git a/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs b/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs
--- a/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs
+++ b/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs
@@ -19,6 +19,7 @@
   , edgesInRectangle
   , verticesInRectangle
   , floodFillFaces
+  , floodFillFacesWithRejectedEdges
   , outerFaceFloodFill
   , facesAtEvenBarrierDepth
   , FaceComponent
@@ -214,7 +215,7 @@
       [edge | edge <- undirectedEdges triangulation, edgeInside edge]
   | otherwise =
       let starts = shapeStartFaces triangulation metric
-          (_, accepted) = floodFillFacesWithEdges triangulation starts edgeInside
+          (_, accepted, _) = floodFillFacesWithEdges triangulation starts edgeInside
        in map (UndirectedEdgeId . fromIntegral) (IntSet.toAscList accepted)
  where
   edgeInside edge =
@@ -244,15 +245,27 @@
   -> (UndirectedEdgeId -> Bool)
   -> [FaceId]
 floodFillFaces triangulation starts canCross =
-  fst (floodFillFacesWithEdges triangulation starts canCross)
+  let (faces, _, _) = floodFillFacesWithEdges triangulation starts canCross
+   in faces
 
+-- | The reached section and the edge pairs whose crossing was rejected while
+-- descending it. This package-internal observation retains the boundary
+-- witness instead of requiring a later face-wide rediscovery.
+floodFillFacesWithRejectedEdges
+  :: Triangulation mode vertex directed undirected face -> [FaceId]
+  -> (UndirectedEdgeId -> Bool)
+  -> ([FaceId], [UndirectedEdgeId])
+floodFillFacesWithRejectedEdges triangulation starts canCross =
+  let (faces, _, rejected) = floodFillFacesWithEdges triangulation starts canCross
+   in (faces, fmap (UndirectedEdgeId . fromIntegral) (IntSet.toAscList rejected))
+
 floodFillFacesWithEdges
   :: Triangulation mode vertex directed undirected face -> [FaceId]
   -> (UndirectedEdgeId -> Bool)
-  -> ([FaceId], IntSet.IntSet)
+  -> ([FaceId], IntSet.IntSet, IntSet.IntSet)
 floodFillFacesWithEdges triangulation starts canCross =
-  let (faces, accepted, _) = go initialStack initialVisited IntSet.empty IntSet.empty []
-   in (reverse faces, accepted)
+  let (faces, accepted, rejected) = go initialStack initialVisited IntSet.empty IntSet.empty []
+   in (reverse faces, accepted, rejected)
  where
   valid face@(FaceId value) = face /= outerFace && fromIntegral value < numFaces triangulation
   (initialStack, initialVisited) = List.foldl' enqueueStart ([], IntSet.empty) starts
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs b/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs
--- a/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs
+++ b/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs
@@ -6,6 +6,7 @@
 -- representation is a function of its geometry alone.
 module Moonlight.Triangulation.Internal.Canonical
   ( canonicalize
+  , canonicalizeKnownAscending
   ) where
 
 import Control.Monad (when)
@@ -20,6 +21,14 @@
 import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
 import Moonlight.Triangulation.Internal.Types (BuildError)
 
+-- | Whether the caller has already established the authoritative vertex-order
+-- section. The canonicalizer still owns every edge, directed-edge, face,
+-- anchor, and constraint normalization; this witness affects only its vertex
+-- rank entrance.
+data VertexOrderAdmission
+  = VertexOrderUnknown
+  | VertexOrderKnownAscending
+
 -- | Renumber a triangulation into its canonical representation.
 --
 -- Two triangulations of the same sites are the same triangulation — Delaunay
@@ -56,7 +65,21 @@
 canonicalize
   :: Triangulation mode vertex () () ()
   -> Either BuildError (Triangulation mode vertex () () ())
-canonicalize source = runST $ do
+canonicalize = canonicalizeWithVertexOrder VertexOrderUnknown
+
+-- | Canonicalize a triangulation whose vertices were appended in strict
+-- lexicographic coordinate order by the admitted SiteSet bulk entrance.
+-- This is an execution witness, not a second numbering implementation.
+canonicalizeKnownAscending
+  :: Triangulation mode vertex () () ()
+  -> Either BuildError (Triangulation mode vertex () () ())
+canonicalizeKnownAscending = canonicalizeWithVertexOrder VertexOrderKnownAscending
+
+canonicalizeWithVertexOrder
+  :: VertexOrderAdmission
+  -> Triangulation mode vertex () () ()
+  -> Either BuildError (Triangulation mode vertex () () ())
+canonicalizeWithVertexOrder vertexOrderAdmission source = runST $ do
   mutable <-
     newMutableDcel
       (triElementDefaults source)
@@ -118,7 +141,10 @@
   -- the schedule is assumed here — the coordinates are simply read and
   -- believed, so a mesh that arrives sorted for any other reason is served just
   -- as well.
-  !alreadyRanked = coordinatesAscend vertexTotal coordinateX coordinateY
+  !alreadyRanked =
+    case vertexOrderAdmission of
+      VertexOrderUnknown -> coordinatesAscend vertexTotal coordinateX coordinateY
+      VertexOrderKnownAscending -> True
   !vertexOrder
     | alreadyRanked = U.enumFromN 0 vertexTotal
     | otherwise =
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Representation.hs b/src-dcel/Moonlight/Triangulation/Internal/Representation.hs
--- a/src-dcel/Moonlight/Triangulation/Internal/Representation.hs
+++ b/src-dcel/Moonlight/Triangulation/Internal/Representation.hs
@@ -602,6 +602,7 @@
     -- ^ Face slots whose published triangle changed or was appended.
   , refinementFinalPermittedFaces :: !(V.Vector FaceId)
     -- ^ Exact final face lineage admitted by a checked local domain.
+  , refinementFinalInterfaceIncidence :: !(V.Vector (UndirectedEdgeId, FaceId, FaceId))
   , refinementTouchedEdges :: !(V.Vector UndirectedEdgeId)
   , refinementRemovedEdges :: !(V.Vector UndirectedEdgeId)
   , refinementInterfaceBoundaryReads :: {-# UNPACK #-} !Int
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Types.hs b/src-dcel/Moonlight/Triangulation/Internal/Types.hs
--- a/src-dcel/Moonlight/Triangulation/Internal/Types.hs
+++ b/src-dcel/Moonlight/Triangulation/Internal/Types.hs
@@ -283,7 +283,6 @@
   | RefinementInputTopologyInvalid !InvariantViolation
   | SeamFrontierUnavailable
   | SeamSourceEdgeRequiresFlip !UndirectedEdgeId
-  | SeamProtectedSourceFaceChanged !FaceId
   | FreshInsertionMatchedExistingVertex !VertexId !VertexId
   | DegenerateLineEndpointMissingOutgoing !VertexId
   | DegenerateLineEndpointTurnMissing {-# UNPACK #-} !Int
@@ -336,9 +335,6 @@
       {-# UNPACK #-} !Double
       {-# UNPACK #-} !Double
   | RefinementSeedFaceNotActive !FaceId {-# UNPACK #-} !Int
-  | RefinementDomainInterfaceEdgeNotActive !UndirectedEdgeId {-# UNPACK #-} !Int
-  | RefinementDomainInterfaceMissing !UndirectedEdgeId
-  | RefinementDomainInterfaceExtraneous !UndirectedEdgeId
   | RefinementDomainTopologyChanged
   | RefinementDomainRequiresConvexHullPreservation
   | RefinementDomainRequiresConstraintPreservation
@@ -352,6 +348,7 @@
   | RefinementDomainWouldCrossInterface !UndirectedEdgeId !FaceId
   | RefinementDomainWouldRewriteProtectedFace !FaceId
   | RefinementDomainProtectedFaceChanged !FaceId
+  | RefinementDomainInterfaceOppositeFaceNotPermitted !UndirectedEdgeId !FaceId
   | RefinementOversizedEdge
       !FaceId
       !UndirectedEdgeId
diff --git a/src-ffi/Moonlight/Triangulation/Foreign/ABI.hs b/src-ffi/Moonlight/Triangulation/Foreign/ABI.hs
deleted file mode 100644
--- a/src-ffi/Moonlight/Triangulation/Foreign/ABI.hs
+++ /dev/null
@@ -1,1117 +0,0 @@
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE TypeFamilies #-}
-
-module Moonlight.Triangulation.Foreign.ABI
-  ( CObstruction (..)
-  , CMinkowskiReceipt (..)
-  , CMesh
-  , CRegion
-  , CStructuringElement
-  , delaunayF64
-  , meshInsertManyF64
-  , -- | Exported for measurement. @meshInsertManyF64@ is a pointer boundary and
-    -- cannot be timed against a Haskell arm without dragging marshalling into
-    -- one side only; this is the same route with the pointers already resolved.
-    insertGeometryBatch
-  , meshSiteUnion
-  , meshSiteIntersection
-  , meshSiteDifference
-  , meshSiteSymmetricDifference
-  , meshVertexCount
-  , meshTriangleCount
-  , meshCopyVerticesF64
-  , meshCopyTrianglesU32
-  , meshFree
-  , regionCreateF64
-  , regionCounts
-  , regionCopyF64
-  , regionUnion
-  , regionIntersection
-  , regionDifference
-  , regionSymmetricDifference
-  , regionLocatePointF64
-  , regionMeasure
-  , regionFree
-  , structuringElementCreateF64
-  , structuringElementFree
-  , regionMinkowskiSum
-  , regionOffset
-  , regionInset
-  , regionOpen
-  , regionClose
-  ) where
-
-import Control.Exception (SomeException, displayException, try)
-import Control.Monad (void)
-import Data.Bifunctor (first)
-import Data.Int (Int64)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Word (Word32, Word64)
-import Foreign.C.String (peekCString, withCStringLen)
-import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))
-import Foreign.Marshal.Utils (copyBytes, fillBytes)
-import Foreign.Ptr (Ptr, castPtr, nullPtr, plusPtr)
-import Foreign.StablePtr
-  ( StablePtr
-  , castPtrToStablePtr
-  , castStablePtrToPtr
-  , deRefStablePtr
-  , freeStablePtr
-  , newStablePtr
-  )
-import Foreign.Storable (Storable (..))
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation as T
-import Moonlight.Triangulation.Math (canonicalPoint, validatePoint)
-import qualified Moonlight.Triangulation.Session as Session
-
-type GeometryMesh = T.DelaunayTriangulation ()
-
-data CMesh
-data CRegion
-data CStructuringElement
-
-type family HandleValue carrier where
-  HandleValue CMesh = GeometryMesh
-  HandleValue CRegion = T.PlanarRegion
-  HandleValue CStructuringElement = T.StructuringElement
-
-data CObstruction = CObstruction
-  { obstructionCode :: !Word32
-  , obstructionCoordinateError :: !Word32
-  , obstructionInputIndex :: !Word64
-  , obstructionFirstIndex :: !Word64
-  , obstructionSecondIndex :: !Word64
-  , obstructionFirstValue :: !Double
-  , obstructionSecondValue :: !Double
-  , obstructionPointX :: !Double
-  , obstructionPointY :: !Double
-  , obstructionMessage :: !String
-  }
-  deriving stock (Eq, Show)
-
-instance Storable CObstruction where
-  sizeOf _ = 320
-  alignment _ = alignment (0 :: Word64)
-  peek pointer = do
-    obstructionCode <- peekByteOff pointer 0
-    obstructionCoordinateError <- peekByteOff pointer 4
-    obstructionInputIndex <- peekByteOff pointer 8
-    obstructionFirstIndex <- peekByteOff pointer 16
-    obstructionSecondIndex <- peekByteOff pointer 24
-    obstructionFirstValue <- peekByteOff pointer 32
-    obstructionSecondValue <- peekByteOff pointer 40
-    obstructionPointX <- peekByteOff pointer 48
-    obstructionPointY <- peekByteOff pointer 56
-    obstructionMessage <- peekCString (castPtr pointer `plusPtr` 64)
-    pure CObstruction {..}
-  poke pointer CObstruction {..} = do
-    pokeByteOff pointer 0 obstructionCode
-    pokeByteOff pointer 4 obstructionCoordinateError
-    pokeByteOff pointer 8 obstructionInputIndex
-    pokeByteOff pointer 16 obstructionFirstIndex
-    pokeByteOff pointer 24 obstructionSecondIndex
-    pokeByteOff pointer 32 obstructionFirstValue
-    pokeByteOff pointer 40 obstructionSecondValue
-    pokeByteOff pointer 48 obstructionPointX
-    pokeByteOff pointer 56 obstructionPointY
-    let messagePointer = castPtr pointer `plusPtr` 64 :: Ptr CChar
-    fillBytes messagePointer 0 256
-    withCStringLen obstructionMessage $ \(source, lengthInBytes) ->
-      copyBytes messagePointer source (min 255 lengthInBytes)
-
--- | Fixed-width projection of the existing Haskell morphology receipt.
-data CMinkowskiReceipt = CMinkowskiReceipt
-  { receiptOperation :: !Word32
-  , receiptInputComponents :: !Word64
-  , receiptConvexPieces :: !Word64
-  , receiptGeneratedPieces :: !Word64
-  , receiptGeneratedConvolutionEdges :: !Word64
-  , receiptOverlayPasses :: !Word64
-  , receiptExactCrossings :: !Word64
-  , receiptOutputCells :: !Word64
-  , receiptExactCoordinateBitGrowth :: !Word64
-  }
-  deriving stock (Eq, Show)
-
-instance Storable CMinkowskiReceipt where
-  sizeOf _ = 72
-  alignment _ = alignment (0 :: Word64)
-  peek pointer = do
-    receiptOperation <- peekByteOff pointer 0
-    receiptInputComponents <- peekByteOff pointer 8
-    receiptConvexPieces <- peekByteOff pointer 16
-    receiptGeneratedPieces <- peekByteOff pointer 24
-    receiptGeneratedConvolutionEdges <- peekByteOff pointer 32
-    receiptOverlayPasses <- peekByteOff pointer 40
-    receiptExactCrossings <- peekByteOff pointer 48
-    receiptOutputCells <- peekByteOff pointer 56
-    receiptExactCoordinateBitGrowth <- peekByteOff pointer 64
-    pure CMinkowskiReceipt {..}
-  poke pointer CMinkowskiReceipt {..} = do
-    pokeByteOff pointer 0 receiptOperation
-    pokeByteOff pointer 4 (0 :: Word32)
-    pokeByteOff pointer 8 receiptInputComponents
-    pokeByteOff pointer 16 receiptConvexPieces
-    pokeByteOff pointer 24 receiptGeneratedPieces
-    pokeByteOff pointer 32 receiptGeneratedConvolutionEdges
-    pokeByteOff pointer 40 receiptOverlayPasses
-    pokeByteOff pointer 48 receiptExactCrossings
-    pokeByteOff pointer 56 receiptOutputCells
-    pokeByteOff pointer 64 receiptExactCoordinateBitGrowth
-
-data AbiFailure = AbiFailure !CUInt !CObstruction
-
-statusOk, statusNullPointer, statusCountOverflow, statusBufferTooSmall, statusGeometryObstruction, statusRuntimeFailure :: CUInt
-statusOk = 0
-statusNullPointer = 1
-statusCountOverflow = 2
-statusBufferTooSmall = 3
-statusGeometryObstruction = 4
-statusRuntimeFailure = 5
-
-emptyObstruction :: CObstruction
-emptyObstruction =
-  CObstruction
-    { obstructionCode = 0
-    , obstructionCoordinateError = 0
-    , obstructionInputIndex = maxBound
-    , obstructionFirstIndex = 0
-    , obstructionSecondIndex = 0
-    , obstructionFirstValue = 0
-    , obstructionSecondValue = 0
-    , obstructionPointX = 0
-    , obstructionPointY = 0
-    , obstructionMessage = ""
-    }
-
-apiFailure :: CUInt -> Word32 -> String -> AbiFailure
-apiFailure status code message =
-  AbiFailure status emptyObstruction {obstructionCode = code, obstructionMessage = message}
-
-geometryFailure :: Show obstruction => Word32 -> obstruction -> AbiFailure
-geometryFailure code obstruction =
-  apiFailure statusGeometryObstruction code (show obstruction)
-
-nullPointerFailure :: String -> AbiFailure
-nullPointerFailure label = apiFailure statusNullPointer 100 (label <> " must not be null")
-
-countOverflowFailure :: Word64 -> AbiFailure
-countOverflowFailure count =
-  AbiFailure statusCountOverflow emptyObstruction
-    { obstructionCode = 101
-    , obstructionFirstIndex = count
-    , obstructionMessage = "count exceeds the host Int range"
-    }
-
-bufferTooSmallFailure :: Int -> Int -> AbiFailure
-bufferTooSmallFailure required capacity =
-  AbiFailure statusBufferTooSmall emptyObstruction
-    { obstructionCode = 102
-    , obstructionFirstIndex = fromIntegral required
-    , obstructionSecondIndex = fromIntegral capacity
-    , obstructionMessage = "output buffer is smaller than the required element count"
-    }
-
-runtimeFailure :: SomeException -> AbiFailure
-runtimeFailure = apiFailure statusRuntimeFailure 103 . displayException
-
-runBoundary :: Ptr CObstruction -> IO (Either AbiFailure ()) -> IO CUInt
-runBoundary obstructionPointer action = do
-  writeObstruction obstructionPointer emptyObstruction
-  outcome <- try action :: IO (Either SomeException (Either AbiFailure ()))
-  case outcome of
-    Left exception -> finishFailure obstructionPointer (runtimeFailure exception)
-    Right (Left failure) -> finishFailure obstructionPointer failure
-    Right (Right ()) -> pure statusOk
-
-finishFailure :: Ptr CObstruction -> AbiFailure -> IO CUInt
-finishFailure obstructionPointer (AbiFailure status obstruction) = do
-  writeObstruction obstructionPointer obstruction
-  pure status
-
-writeObstruction :: Ptr CObstruction -> CObstruction -> IO ()
-writeObstruction pointer obstruction
-  | pointer == nullPtr = pure ()
-  | otherwise = poke pointer obstruction
-
-requirePointer :: String -> Ptr value -> Either AbiFailure ()
-requirePointer label pointer
-  | pointer == nullPtr = Left (nullPointerFailure label)
-  | otherwise = Right ()
-
-checkedCount :: Int -> CSize -> Either AbiFailure Int
-checkedCount elementsPerItem rawCount
-  | toInteger rawCount * toInteger elementsPerItem > toInteger (maxBound :: Int) =
-      Left (countOverflowFailure (fromIntegral rawCount))
-  | otherwise = Right (fromIntegral rawCount)
-
-readPoints :: Ptr CDouble -> Int -> IO (Either AbiFailure (V.Vector T.Point))
-readPoints pointer count
-  | count == 0 = pure (Right V.empty)
-  | pointer == nullPtr = pure (Left (nullPointerFailure "coordinates"))
-  | otherwise =
-      Right
-        <$> V.generateM
-              count
-              ( \index -> do
-                  CDouble x <- peekElemOff pointer (index * 2)
-                  CDouble y <- peekElemOff pointer (index * 2 + 1)
-                  pure (T.Point x y)
-              )
-
-prepareHandleOutput :: Ptr (Ptr carrier) -> IO (Either AbiFailure ())
-prepareHandleOutput pointer =
-  case requirePointer "result" pointer of
-    Left failure -> pure (Left failure)
-    Right () -> poke pointer nullPtr >> pure (Right ())
-
-publishHandle :: Ptr (Ptr carrier) -> HandleValue carrier -> IO ()
-publishHandle output value = do
-  stable <- newStablePtr value
-  poke output (castPtr (castStablePtrToPtr stable))
-
-produceHandle
-  :: Ptr (Ptr carrier)
-  -> IO (Either AbiFailure (HandleValue carrier))
-  -> IO (Either AbiFailure ())
-produceHandle output obtain = do
-  prepared <- prepareHandleOutput output
-  case prepared of
-    Left failure -> pure (Left failure)
-    Right () -> do
-      outcome <- obtain
-      case outcome of
-        Left failure -> pure (Left failure)
-        Right value -> publishHandle output value >> pure (Right ())
-
-dereferenceHandle :: Ptr carrier -> IO (HandleValue carrier)
-dereferenceHandle pointer = deRefStablePtr (castPtrToStablePtr (castPtr pointer))
-
-freeHandle :: Ptr carrier -> IO ()
-freeHandle pointer
-  | pointer == nullPtr = pure ()
-  | otherwise = freeStablePtr (castPtrToStablePtr (castPtr pointer) :: StablePtr ())
-
-buildFailure :: T.BuildError -> AbiFailure
-buildFailure obstruction =
-  AbiFailure statusGeometryObstruction (buildErrorObstruction obstruction)
-
-delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-delaunayF64 coordinates rawCount output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case checkedCount 2 rawCount of
-      Left failure -> pure (Left failure)
-      Right count -> do
-        points <- readPoints coordinates count
-        pure (points >>= first buildFailure . T.delaunayGeometry)
-
-meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-meshInsertManyF64 meshPointer coordinates rawCount output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case requirePointer "mesh" meshPointer >> checkedCount 2 rawCount of
-      Left failure -> pure (Left failure)
-      Right count -> do
-        points <- readPoints coordinates count
-        case points of
-          Left failure -> pure (Left failure)
-          Right admitted -> do
-            mesh <- dereferenceHandle meshPointer
-            pure (first buildFailure (insertGeometryBatch mesh admitted))
-
--- | Admission stays where it was — every point is validated before any point
--- is inserted, so a malformed input at the end still refuses the whole batch —
--- but it no longer materializes a second vector to carry the canonical
--- coordinates. @imapM@ over 'Either' cannot fill in place, so the discarded
--- form is the one that costs nothing; canonicalization is exactly what
--- @queryPointValue . validatePoint@ returned, applied where the point is used.
-insertGeometryBatch :: GeometryMesh -> V.Vector T.Point -> Either T.BuildError GeometryMesh
-insertGeometryBatch mesh points = do
-  V.imapM_ (\index point -> () <$ validatePoint (Just index) point) points
-  (_, revised, _) <-
-    Session.withSession
-      mesh
-      (V.length points)
-      (V.mapM_ (\point -> void (Session.insertVertexAt (canonicalPoint point) ())) points)
-  pure revised
-
-meshSiteUnion, meshSiteIntersection, meshSiteDifference, meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-meshSiteUnion = binaryMeshOperation T.union
-meshSiteIntersection = binaryMeshOperation T.intersection
-meshSiteDifference = binaryMeshOperation T.difference
-meshSiteSymmetricDifference = binaryMeshOperation T.symmetricDifference
-
-binaryMeshOperation
-  :: (GeometryMesh -> GeometryMesh -> Either T.BuildError GeometryMesh)
-  -> Ptr CMesh
-  -> Ptr CMesh
-  -> Ptr (Ptr CMesh)
-  -> Ptr CObstruction
-  -> IO CUInt
-binaryMeshOperation operation leftPointer rightPointer output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case requirePointer "left mesh" leftPointer >> requirePointer "right mesh" rightPointer of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        left <- dereferenceHandle leftPointer
-        right <- dereferenceHandle rightPointer
-        pure (first buildFailure (operation left right))
-
-meshVertexCount, meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-meshVertexCount = handleCount "mesh" T.numVertices
-meshTriangleCount = handleCount "mesh" (V.length . T.innerFaceVertexTriples)
-
-handleCount
-  :: String
-  -> (HandleValue carrier -> Int)
-  -> Ptr carrier
-  -> Ptr CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-handleCount handleLabel observe handlePointer output obstructionPointer =
-  runBoundary obstructionPointer $
-    case requirePointer handleLabel handlePointer >> requirePointer "count" output of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        value <- dereferenceHandle handlePointer
-        poke output (fromIntegral (observe value))
-        pure (Right ())
-
-meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-meshCopyVerticesF64 =
-  copyHandleProjection
-    "mesh"
-    "points_written"
-    "coordinates"
-    T.vertexPoints
-    ( \output index (T.Point x y) -> do
-        pokeElemOff output (index * 2) (CDouble x)
-        pokeElemOff output (index * 2 + 1) (CDouble y)
-    )
-
-meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-meshCopyTrianglesU32 =
-  copyHandleProjection
-    "mesh"
-    "triangles_written"
-    "triangles"
-    T.innerFaceVertexTriples
-    ( \output index (firstVertex, secondVertex, thirdVertex) -> do
-        pokeElemOff output (index * 3) (T.unVertexId firstVertex)
-        pokeElemOff output (index * 3 + 1) (T.unVertexId secondVertex)
-        pokeElemOff output (index * 3 + 2) (T.unVertexId thirdVertex)
-    )
-
-copyHandleProjection
-  :: String
-  -> String
-  -> String
-  -> (HandleValue carrier -> V.Vector item)
-  -> (Ptr element -> Int -> item -> IO ())
-  -> Ptr carrier
-  -> Ptr element
-  -> CSize
-  -> Ptr CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-{-# INLINE copyHandleProjection #-}
-copyHandleProjection handleLabel writtenLabel outputLabel project writeItem handlePointer output rawCapacity written obstructionPointer =
-  runBoundary obstructionPointer $
-    case requirePointer handleLabel handlePointer >> requirePointer writtenLabel written >> checkedCount 1 rawCapacity of
-      Left failure -> pure (Left failure)
-      Right capacity -> do
-        value <- dereferenceHandle handlePointer
-        let items = project value
-            required = V.length items
-        poke written (fromIntegral required)
-        case requireOutputCapacity outputLabel output required capacity of
-          Left failure -> pure (Left failure)
-          Right () -> V.imapM_ (writeItem output) items >> pure (Right ())
-
-requireOutputCapacity :: String -> Ptr value -> Int -> Int -> Either AbiFailure ()
-requireOutputCapacity label output required capacity
-  | capacity < required = Left (bufferTooSmallFailure required capacity)
-  | required > 0 = requirePointer label output
-  | otherwise = Right ()
-
-meshFree :: Ptr CMesh -> IO ()
-meshFree = freeHandle
-
-data RegionCountKind
-  = LoopPointCounts
-  | ComponentLoopCounts
-  deriving stock (Eq, Show)
-
-data RegionLayoutError
-  = RegionGroupEmpty !RegionCountKind !Int
-  | RegionCountTotalMismatch !RegionCountKind !Integer !Integer
-  | RegionStructuringElementEmpty
-  deriving stock (Eq, Show)
-
-regionLayoutFailure :: RegionLayoutError -> AbiFailure
-regionLayoutFailure layoutError =
-  AbiFailure statusGeometryObstruction (layoutObstruction layoutError)
-
-layoutObstruction :: RegionLayoutError -> CObstruction
-layoutObstruction layoutError =
-  (case layoutError of
-    RegionCountTotalMismatch _ actual expected ->
-      base
-        { obstructionFirstIndex = fromIntegral actual
-        , obstructionSecondIndex = fromIntegral expected
-        }
-    RegionGroupEmpty _ index -> base {obstructionInputIndex = fromIntegral index}
-    RegionStructuringElementEmpty -> base
-  )
-    {obstructionMessage = show layoutError}
- where
-  base = emptyObstruction {obstructionCode = 200}
-
-regionValidationFailure :: T.RegionValidationError -> AbiFailure
-regionValidationFailure = geometryFailure 201
-
-overlayFailure :: T.OverlayError Bool Bool -> AbiFailure
-overlayFailure = geometryFailure 202
-
-regionPublicationFailure :: T.RegionPublicationError -> AbiFailure
-regionPublicationFailure = geometryFailure 203
-
-valuationFailure :: T.ValuationError -> AbiFailure
-valuationFailure = geometryFailure 204
-
-minkowskiFailure :: T.MinkowskiError -> AbiFailure
-minkowskiFailure = geometryFailure 205
-
-pointInputFailure :: Int -> T.Point -> T.PointValidationError -> AbiFailure
-pointInputFailure index (T.Point x y) pointError =
-  AbiFailure statusGeometryObstruction emptyObstruction
-    { obstructionCode = 1
-    , obstructionCoordinateError = coordinateErrorCode reason
-    , obstructionInputIndex = fromIntegral index
-    , obstructionFirstValue = invalidValue
-    , obstructionPointX = x
-    , obstructionPointY = y
-    , obstructionMessage = show pointError
-    }
- where
-  (invalidValue, reason) =
-    case pointError of
-      T.InvalidPointX coordinateError -> (x, coordinateError)
-      T.InvalidPointY coordinateError -> (y, coordinateError)
-
-projectionFailure :: Int -> T.PointValidationError -> AbiFailure
-projectionFailure index pointError =
-  AbiFailure statusGeometryObstruction emptyObstruction
-    { obstructionCode = 206
-    , obstructionCoordinateError =
-        coordinateErrorCode
-          (case pointError of
-             T.InvalidPointX coordinateError -> coordinateError
-             T.InvalidPointY coordinateError -> coordinateError)
-    , obstructionInputIndex = fromIntegral index
-    , obstructionMessage = show pointError
-    }
-
-readCounts
-  :: String
-  -> Ptr CSize
-  -> Int
-  -> IO (Either AbiFailure (V.Vector Int))
-readCounts _ _ 0 = pure (Right V.empty)
-readCounts label pointer count
-  | pointer == nullPtr = pure (Left (nullPointerFailure label))
-  | otherwise = do
-      rawCounts <- V.generateM count (peekElemOff pointer)
-      pure (V.mapM (checkedCount 1) rawCounts)
-
-validateCounts
-  :: RegionCountKind
-  -> Int
-  -> V.Vector Int
-  -> Either RegionLayoutError ()
-validateCounts kind expectedTotal counts =
-  case V.findIndex (== 0) counts of
-    Just index -> Left (RegionGroupEmpty kind index)
-    Nothing
-      | observedTotal /= toInteger expectedTotal ->
-          Left (RegionCountTotalMismatch kind observedTotal (toInteger expectedTotal))
-      | otherwise -> Right ()
- where
-  observedTotal = V.foldl' (\total count -> total + toInteger count) 0 counts
-
-regionCreateF64
-  :: Ptr CDouble
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr (Ptr CRegion)
-  -> Ptr CObstruction
-  -> IO CUInt
-regionCreateF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount output obstructionPointer =
-  runBoundary obstructionPointer $
-    produceHandle output $
-      readRegionF64
-        coordinates
-        rawPointCount
-        loopPointCounts
-        rawLoopCount
-        componentLoopCounts
-        rawComponentCount
-
-readRegionF64
-  :: Ptr CDouble
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> IO (Either AbiFailure T.PlanarRegion)
-readRegionF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount =
-  case checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount of
-    Left failure -> pure (Left failure)
-    Right (pointCount, loopCount, componentCount) -> do
-      points <- readPoints coordinates pointCount
-      loopCounts <- readCounts "loop_point_counts" loopPointCounts loopCount
-      componentCounts <- readCounts "component_loop_counts" componentLoopCounts componentCount
-      pure $ do
-        admittedPoints <- points
-        admittedLoopCounts <- loopCounts
-        admittedComponentCounts <- componentCounts
-        first regionLayoutFailure (validateCounts LoopPointCounts pointCount admittedLoopCounts)
-        first regionLayoutFailure (validateCounts ComponentLoopCounts loopCount admittedComponentCounts)
-        buildRegion admittedPoints admittedLoopCounts admittedComponentCounts
-
-checkedRegionInputCounts
-  :: CSize
-  -> CSize
-  -> CSize
-  -> Either AbiFailure (Int, Int, Int)
-checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount = do
-  pointCount <- checkedCount 2 rawPointCount
-  loopCount <- checkedCount 1 rawLoopCount
-  componentCount <- checkedCount 1 rawComponentCount
-  pure (pointCount, loopCount, componentCount)
-
-buildRegion
-  :: V.Vector T.Point
-  -> V.Vector Int
-  -> V.Vector Int
-  -> Either AbiFailure T.PlanarRegion
-buildRegion points loopCounts componentCounts = do
-  exactPoints <-
-    V.imapM
-      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
-      points
-  loops <-
-    V.imapM
-      (buildLoop exactPoints)
-      (adjacentOffsets loopCounts)
-  components <-
-    V.imapM
-      (buildComponent loops)
-      (adjacentOffsets componentCounts)
-  first regionValidationFailure (T.planarRegion (V.toList components))
-
-adjacentOffsets :: V.Vector Int -> V.Vector (Int, Int)
-adjacentOffsets counts =
-  let offsets = V.scanl' (+) 0 counts
-   in V.zip offsets (V.drop 1 offsets)
-
-buildLoop
-  :: V.Vector T.ExactPoint
-  -> Int
-  -> (Int, Int)
-  -> Either AbiFailure T.ExactLoop
-buildLoop points loopIndex (start, end) =
-  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) points)) of
-    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty LoopPointCounts loopIndex))
-    Just submitted -> first regionValidationFailure (T.exactLoop submitted)
-
-buildComponent
-  :: V.Vector T.ExactLoop
-  -> Int
-  -> (Int, Int)
-  -> Either AbiFailure T.PolygonComponent
-buildComponent loops componentIndex (start, end) =
-  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) loops)) of
-    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty ComponentLoopCounts componentIndex))
-    Just (outer :| holes) -> first regionValidationFailure (T.polygonComponent outer holes)
-
-regionShape
-  :: T.PlanarRegion
-  -> ([T.PolygonComponent], [[T.ExactLoop]], [T.ExactLoop])
-regionShape region =
-  let components = T.planarRegionComponents region
-      componentLoops component = T.polygonOuterLoop component : T.polygonHoleLoops component
-      loopsByComponent = map componentLoops components
-   in (components, loopsByComponent, concat loopsByComponent)
-
-regionCounts
-  :: Ptr CRegion
-  -> Ptr CSize
-  -> Ptr CSize
-  -> Ptr CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-regionCounts regionPointer componentCountOutput loopCountOutput pointCountOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      requirePointer "region" regionPointer
-        >> requirePointer "component_count" componentCountOutput
-        >> requirePointer "loop_count" loopCountOutput
-        >> requirePointer "point_count" pointCountOutput
-    of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        region <- dereferenceHandle regionPointer
-        let (components, _, loops) = regionShape region
-            pointCount = sum (map (NonEmpty.length . T.exactLoopPoints) loops)
-        poke componentCountOutput (fromIntegral (length components))
-        poke loopCountOutput (fromIntegral (length loops))
-        poke pointCountOutput (fromIntegral pointCount)
-        pure (Right ())
-
-data RegionProjection = RegionProjection
-  { projectionPoints :: !(V.Vector T.Point)
-  , projectionLoopPointOffsets :: !(V.Vector CSize)
-  , projectionComponentLoopOffsets :: !(V.Vector CSize)
-  }
-
-regionProjection :: T.PlanarRegion -> Either AbiFailure RegionProjection
-regionProjection region = do
-  let (_, loopsByComponent, loops) = regionShape region
-      exactPoints = concatMap (NonEmpty.toList . T.exactLoopPoints) loops
-      loopPointOffsets = scanl (+) 0 (map (NonEmpty.length . T.exactLoopPoints) loops)
-      componentLoopOffsets = scanl (+) 0 (map length loopsByComponent)
-  projectedPoints <-
-    V.fromList
-      <$> traverse
-            (uncurry projectPoint)
-            (zip [0 ..] exactPoints)
-  pure
-    RegionProjection
-      { projectionPoints = projectedPoints
-      , projectionLoopPointOffsets = V.fromList (map fromIntegral loopPointOffsets)
-      , projectionComponentLoopOffsets = V.fromList (map fromIntegral componentLoopOffsets)
-      }
- where
-  projectPoint index point =
-    first (projectionFailure index) (T.exactPointToEmbeddingCandidate point)
-
-regionCopyF64
-  :: Ptr CRegion
-  -> Ptr CDouble
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-regionCopyF64 regionPointer coordinates rawPointCapacity loopPointOffsets rawLoopOffsetCapacity componentLoopOffsets rawComponentOffsetCapacity obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      ( (,,)
-          <$> (requirePointer "region" regionPointer >> checkedCount 2 rawPointCapacity)
-          <*> checkedCount 1 rawLoopOffsetCapacity
-          <*> checkedCount 1 rawComponentOffsetCapacity
-      )
-    of
-      Left failure -> pure (Left failure)
-      Right (pointCapacity, loopOffsetCapacity, componentOffsetCapacity) -> do
-        region <- dereferenceHandle regionPointer
-        case regionProjection region of
-          Left failure -> pure (Left failure)
-          Right RegionProjection {..} ->
-            case
-              requireOutputCapacity "coordinates" coordinates (V.length projectionPoints) pointCapacity
-                >> requireOutputCapacity "loop_point_offsets" loopPointOffsets (V.length projectionLoopPointOffsets) loopOffsetCapacity
-                >> requireOutputCapacity "component_loop_offsets" componentLoopOffsets (V.length projectionComponentLoopOffsets) componentOffsetCapacity
-            of
-              Left failure -> pure (Left failure)
-              Right () -> do
-                V.imapM_
-                  (\index (T.Point x y) -> do
-                     pokeElemOff coordinates (index * 2) (CDouble x)
-                     pokeElemOff coordinates (index * 2 + 1) (CDouble y))
-                  projectionPoints
-                V.imapM_ (pokeElemOff loopPointOffsets) projectionLoopPointOffsets
-                V.imapM_ (pokeElemOff componentLoopOffsets) projectionComponentLoopOffsets
-                pure (Right ())
-
-regionUnion, regionIntersection, regionDifference, regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-regionUnion = binaryRegionOperation (\(left, right) -> left || right)
-regionIntersection = binaryRegionOperation (\(left, right) -> left && right)
-regionDifference = binaryRegionOperation (\(left, right) -> left && not right)
-regionSymmetricDifference = binaryRegionOperation (uncurry (/=))
-
-binaryRegionOperation
-  :: ((Bool, Bool) -> Bool)
-  -> Ptr CRegion
-  -> Ptr CRegion
-  -> Ptr (Ptr CRegion)
-  -> Ptr CObstruction
-  -> IO CUInt
-binaryRegionOperation selected leftPointer rightPointer output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        left <- dereferenceHandle leftPointer
-        right <- dereferenceHandle rightPointer
-        pure (exactRegionBoolean selected left right)
-
-exactRegionBoolean
-  :: ((Bool, Bool) -> Bool)
-  -> T.PlanarRegion
-  -> T.PlanarRegion
-  -> Either AbiFailure T.PlanarRegion
-exactRegionBoolean selected left right = do
-  leftLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True left))
-  rightLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True right))
-  overlay <- first overlayFailure (T.overlayLayers leftLayer rightLayer)
-  first regionPublicationFailure (T.overlaySelectedRegion selected overlay)
-
-regionLocatePointF64
-  :: Ptr CRegion
-  -> CDouble
-  -> CDouble
-  -> Ptr CUInt
-  -> Ptr CObstruction
-  -> IO CUInt
-regionLocatePointF64 regionPointer (CDouble x) (CDouble y) output obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      requirePointer "region" regionPointer
-        >> requirePointer "location" output
-        >> first (pointInputFailure 0 (T.Point x y)) (T.exactPointFromPoint (T.Point x y))
-    of
-      Left failure -> pure (Left failure)
-      Right query -> do
-        region <- dereferenceHandle regionPointer
-        poke output (regionLocationCode (T.regionPointLocation region query))
-        pure (Right ())
-
-regionLocationCode :: T.RegionPointLocation -> CUInt
-regionLocationCode location =
-  case location of
-    T.RegionExterior -> 0
-    T.RegionOnBoundary -> 1
-    T.RegionInterior -> 2
-
-regionMeasure
-  :: Ptr CRegion
-  -> Ptr Int64
-  -> Ptr CChar
-  -> CSize
-  -> Ptr CSize
-  -> Ptr CDouble
-  -> Ptr CDouble
-  -> Ptr CObstruction
-  -> IO CUInt
-regionMeasure regionPointer eulerOutput areaRatioOutput rawAreaCapacity areaBytesWritten perimeterLowerOutput perimeterUpperOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      requirePointer "region" regionPointer
-        >> requirePointer "euler_characteristic" eulerOutput
-        >> requirePointer "area_bytes_written" areaBytesWritten
-        >> requirePointer "perimeter_lower" perimeterLowerOutput
-        >> requirePointer "perimeter_upper" perimeterUpperOutput
-        >> checkedCount 1 rawAreaCapacity
-    of
-      Left failure -> pure (Left failure)
-      Right areaCapacity -> do
-        region <- dereferenceHandle regionPointer
-        case regionMeasurements region of
-          Left failure -> pure (Left failure)
-          Right (valuations, perimeter) -> do
-            let area = T.exactAreaValue (T.valuationArea valuations)
-                areaText =
-                  show (T.exactRationalNumerator area)
-                    <> "/"
-                    <> show (T.exactRationalDenominator area)
-                bounds = T.exactLengthBounds perimeter
-            poke eulerOutput (fromIntegral (T.eulerCharacteristicValue (T.valuationEuler valuations)))
-            poke perimeterLowerOutput (CDouble (T.intervalLower bounds))
-            poke perimeterUpperOutput (CDouble (T.intervalUpper bounds))
-            copyCStringOutput areaText areaRatioOutput areaCapacity areaBytesWritten
-
-regionMeasurements
-  :: T.PlanarRegion
-  -> Either AbiFailure (T.PlanarValuations, T.ExactLengthMeasurement)
-regionMeasurements region = do
-  valuations <- first valuationFailure (T.regionValuations region)
-  perimeter <- first valuationFailure (T.planarValuationsPerimeter valuations)
-  pure (valuations, perimeter)
-
-copyCStringOutput
-  :: String
-  -> Ptr CChar
-  -> Int
-  -> Ptr CSize
-  -> IO (Either AbiFailure ())
-copyCStringOutput value output capacity bytesWritten =
-  withCStringLen value $ \(source, byteCount) -> do
-    poke bytesWritten (fromIntegral byteCount)
-    case requireOutputCapacity "area_ratio_utf8" output (byteCount + 1) capacity of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        copyBytes output source byteCount
-        pokeElemOff output byteCount 0
-        pure (Right ())
-
-regionFree :: Ptr CRegion -> IO ()
-regionFree = freeHandle
-
-structuringElementCreateF64
-  :: Ptr CDouble
-  -> CSize
-  -> Ptr (Ptr CStructuringElement)
-  -> Ptr CObstruction
-  -> IO CUInt
-structuringElementCreateF64 coordinates rawPointCount output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case checkedCount 2 rawPointCount of
-      Left failure -> pure (Left failure)
-      Right pointCount -> do
-        points <- readPoints coordinates pointCount
-        pure (points >>= buildStructuringElement)
-
-buildStructuringElement
-  :: V.Vector T.Point
-  -> Either AbiFailure T.StructuringElement
-buildStructuringElement points = do
-  exactPoints <-
-    V.imapM
-      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
-      points
-  submitted <-
-    maybe
-      (Left (regionLayoutFailure RegionStructuringElementEmpty))
-      Right
-      (NonEmpty.nonEmpty (V.toList exactPoints))
-  polygon <- first minkowskiFailure (T.convexPolygon submitted)
-  first minkowskiFailure (T.structuringElement polygon)
-
-structuringElementFree :: Ptr CStructuringElement -> IO ()
-structuringElementFree = freeHandle
-
-regionMinkowskiSum
-  :: Ptr CRegion
-  -> Ptr CRegion
-  -> Ptr (Ptr CRegion)
-  -> Ptr CMinkowskiReceipt
-  -> Ptr CObstruction
-  -> IO CUInt
-regionMinkowskiSum leftPointer rightPointer output receiptOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    produceRegionWithReceipt output receiptOutput $ do
-      case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
-        Left failure -> pure (Left failure)
-        Right () -> do
-          left <- dereferenceHandle leftPointer
-          right <- dereferenceHandle rightPointer
-          pure (first minkowskiFailure (T.minkowskiSum left right))
-
-regionOffset, regionInset, regionOpen, regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-regionOffset = structuringElementOperation T.polygonOffset
-regionInset = structuringElementOperation T.polygonInset
-regionOpen = structuringElementOperation T.openWith
-regionClose = structuringElementOperation T.closeWith
-
-structuringElementOperation
-  :: (T.StructuringElement -> T.PlanarRegion -> Either T.MinkowskiError (T.PlanarRegion, T.MinkowskiReceipt))
-  -> Ptr CStructuringElement
-  -> Ptr CRegion
-  -> Ptr (Ptr CRegion)
-  -> Ptr CMinkowskiReceipt
-  -> Ptr CObstruction
-  -> IO CUInt
-structuringElementOperation operation elementPointer regionPointer output receiptOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    produceRegionWithReceipt output receiptOutput $ do
-      case requirePointer "structuring element" elementPointer >> requirePointer "region" regionPointer of
-        Left failure -> pure (Left failure)
-        Right () -> do
-          element <- dereferenceHandle elementPointer
-          region <- dereferenceHandle regionPointer
-          pure (first minkowskiFailure (operation element region))
-
-produceRegionWithReceipt
-  :: Ptr (Ptr CRegion)
-  -> Ptr CMinkowskiReceipt
-  -> IO (Either AbiFailure (T.PlanarRegion, T.MinkowskiReceipt))
-  -> IO (Either AbiFailure ())
-produceRegionWithReceipt output receiptOutput obtain = do
-  prepared <- prepareHandleOutput output
-  case (prepared, requirePointer "receipt" receiptOutput) of
-    (Left failure, _) -> pure (Left failure)
-    (_, Left failure) -> pure (Left failure)
-    (Right (), Right ()) -> do
-      outcome <- obtain
-      case outcome of
-        Left failure -> pure (Left failure)
-        Right (region, receipt) -> do
-          poke receiptOutput (minkowskiReceiptProjection receipt)
-          publishHandle output region
-          pure (Right ())
-
-minkowskiReceiptProjection :: T.MinkowskiReceipt -> CMinkowskiReceipt
-minkowskiReceiptProjection receipt =
-  CMinkowskiReceipt
-    { receiptOperation = minkowskiOperationCode (T.minkowskiOperation receipt)
-    , receiptInputComponents = fromIntegral (T.minkowskiInputComponents receipt)
-    , receiptConvexPieces = fromIntegral (T.minkowskiConvexPieces receipt)
-    , receiptGeneratedPieces = fromIntegral (T.minkowskiGeneratedPieces receipt)
-    , receiptGeneratedConvolutionEdges = fromIntegral (T.minkowskiGeneratedConvolutionEdges receipt)
-    , receiptOverlayPasses = fromIntegral (T.minkowskiOverlayPasses receipt)
-    , receiptExactCrossings = fromIntegral (T.minkowskiExactCrossings receipt)
-    , receiptOutputCells = fromIntegral (T.minkowskiOutputCells receipt)
-    , receiptExactCoordinateBitGrowth = fromIntegral (T.minkowskiExactCoordinateBitGrowth receipt)
-    }
-
-minkowskiOperationCode :: T.MinkowskiOperation -> Word32
-minkowskiOperationCode operation =
-  case operation of
-    T.MinkowskiAddition -> 0
-    T.MinkowskiErosion -> 1
-    T.MinkowskiOpening -> 2
-    T.MinkowskiClosing -> 3
-
-buildErrorObstruction :: T.BuildError -> CObstruction
-buildErrorObstruction failure =
-  (case failure of
-    T.InvalidCoordinate inputIndex value reason ->
-      emptyObstruction
-        { obstructionCode = 1
-        , obstructionCoordinateError = coordinateErrorCode reason
-        , obstructionInputIndex = maybe maxBound fromIntegral inputIndex
-        , obstructionFirstValue = value
-        }
-    T.PointLocationFailed (T.Point x y) -> pointObstruction 2 x y
-    T.LocationWalkExhausted (T.Point x y) steps ->
-      (pointObstruction 3 x y) {obstructionFirstIndex = fromIntegral steps}
-    T.RefinementInputTopologyInvalid _ -> codeOnly 4
-    T.FreshInsertionMatchedExistingVertex firstVertex secondVertex -> indices 5 (T.unVertexId firstVertex) (T.unVertexId secondVertex)
-    T.DegenerateLineEndpointMissingOutgoing vertex -> firstIndex 6 (T.unVertexId vertex)
-    T.DegenerateLineEndpointTurnMissing index -> firstIndex 7 index
-    T.DegenerateLineConnectedVertexMissing index -> firstIndex 8 index
-    T.HullStartNotVisible edge -> firstIndex 9 (T.unDirectedEdgeId edge)
-    T.OuterRangeDidNotTerminate firstEdge secondEdge steps ->
-      (indices 10 (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
-        {obstructionFirstValue = fromIntegral steps}
-    T.OuterRangeContainsInnerEdge edge face -> indices 11 (T.unDirectedEdgeId edge) (T.unFaceId face)
-    T.ConstrainedEdgeFlipRefused edge -> firstIndex 12 (T.unUndirectedEdgeId edge)
-    T.RemovalVertexOutOfRange vertex count -> indices 13 (T.unVertexId vertex) count
-    T.RemovalEdgeOutOfRange edge count -> indices 14 (T.unUndirectedEdgeId edge) count
-    T.RemovalFaceOutOfRange face count -> indices 15 (T.unFaceId face) count
-    T.RemovalFaceCycleDidNotTerminate face edge steps ->
-      (indices 16 (T.unFaceId face) (T.unDirectedEdgeId edge))
-        {obstructionFirstValue = fromIntegral steps}
-    T.RemovalEmptyTriangulation vertex -> firstIndex 17 (T.unVertexId vertex)
-    T.RemovalTwoPointDegreeMismatch vertex degree -> indices 18 (T.unVertexId vertex) degree
-    T.RemovalCollinearDegreeMismatch vertex degree -> indices 19 (T.unVertexId vertex) degree
-    T.RemovalBorderTooShort count -> firstIndex 20 count
-    T.RemovalBorderArityMismatch count -> firstIndex 21 count
-    T.RemovalOutgoingCycleDidNotTerminate vertex edge steps ->
-      (indices 22 (T.unVertexId vertex) (T.unDirectedEdgeId edge))
-        {obstructionFirstValue = fromIntegral steps}
-    T.CircleSweepHullEmpty -> codeOnly 23
-    T.OuterCycleDidNotTerminate firstEdge secondEdge steps ->
-      (indices 24 (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
-        {obstructionFirstValue = fromIntegral steps}
-    T.HierarchyLevelPopulationMismatch level expected observed ->
-      (indices 25 expected observed) {obstructionFirstValue = fromIntegral level}
-    T.HierarchyInsertionHandleMismatch expected observed -> indices 26 (T.unVertexId expected) (T.unVertexId observed)
-    T.PointIndexCapacityExhausted count -> firstIndex 27 count
-    T.RefinementMinimumAngleNotFinite value -> nonFinite 28 value
-    T.RefinementMinimumAngleOutOfRange value -> firstValue 29 value
-    T.RefinementMinimumAngleDerivedRatioNotFinite value -> nonFinite 30 value
-    T.RefinementMaximumAdditionalVerticesNegative value -> firstValue 31 (fromIntegral value)
-    T.RefinementMinimumAreaNotFinite value -> nonFinite 32 value
-    T.RefinementMinimumAreaNegative value -> firstValue 33 value
-    T.RefinementMaximumAreaNotFinite value -> nonFinite 34 value
-    T.RefinementMaximumAreaNotPositive value -> firstValue 35 value
-    T.RefinementMaximumRadiusEdgeRatioNotFinite value -> nonFinite 36 value
-    T.RefinementMaximumRadiusEdgeRatioNotPositive value -> firstValue 37 value
-    T.RefinementMaximumEdgeLengthNotFinite value -> nonFinite 60 value
-    T.RefinementMaximumEdgeLengthNotPositive value -> firstValue 61 value
-    T.RefinementMinimumAreaExceedsMaximum minimumArea maximumArea -> values 38 minimumArea maximumArea
-    T.RefinementSeedFaceNotActive face count -> indices 39 (T.unFaceId face) count
-    T.RefinementDomainInterfaceEdgeNotActive edge count -> indices 40 (T.unUndirectedEdgeId edge) count
-    T.RefinementDomainInterfaceMissing edge -> firstIndex 41 (T.unUndirectedEdgeId edge)
-    T.RefinementDomainInterfaceExtraneous edge -> firstIndex 42 (T.unUndirectedEdgeId edge)
-    T.RefinementDomainTopologyChanged -> codeOnly 43
-    T.RefinementDomainRequiresConvexHullPreservation -> codeOnly 44
-    T.RefinementDomainRequiresConstraintPreservation -> codeOnly 45
-    T.RefinementDomainForbidsOuterFaceExclusion -> codeOnly 46
-    T.RefinementDomainWouldCrossInterface edge face -> indices 47 (T.unUndirectedEdgeId edge) (T.unFaceId face)
-    T.RefinementDomainWouldRewriteProtectedFace face -> firstIndex 48 (T.unFaceId face)
-    T.RefinementDomainProtectedFaceChanged face -> firstIndex 49 (T.unFaceId face)
-    T.RefinementOversizedEdge face edge actual bound ->
-      (indices 62 (T.unFaceId face) (T.unUndirectedEdgeId edge))
-        { obstructionFirstValue = actual
-        , obstructionSecondValue = bound
-        }
-    T.CapacityExceeded count -> firstIndex 50 count
-    T.HalfEdgeCapacityExceeded requested capacity -> indices 51 requested capacity
-    T.FaceCapacityExceeded requested capacity -> indices 52 requested capacity
-    T.PayloadStorageFailure _ -> codeOnly 53
-    T.CoordinatePayloadCountMismatch coordinates payloads -> indices 54 coordinates payloads
-    T.CircleSweepRequiresDenseStorage -> codeOnly 55
-    T.SeamFrontierUnavailable -> codeOnly 56
-    T.RefinementDomainRequiresFiniteVertexBudget -> codeOnly 57
-    T.SeamSourceEdgeRequiresFlip edge -> firstIndex 58 (T.unUndirectedEdgeId edge)
-    T.SeamProtectedSourceFaceChanged face -> firstIndex 59 (T.unFaceId face)
-    T.RefinementSeamBridgeBudgetExceeded required available -> indices 63 required available
-    T.RefinementSeamBridgeMidpointCollapsed edge -> firstIndex 64 (T.unUndirectedEdgeId edge)
-    T.BoundarySplitRequiresBoundaryEdge edge -> firstIndex 65 (T.unUndirectedEdgeId edge)
-  )
-    {obstructionMessage = show failure}
- where
-  codeOnly :: Word32 -> CObstruction
-  codeOnly code = emptyObstruction {obstructionCode = code}
-  firstIndex :: Integral index => Word32 -> index -> CObstruction
-  firstIndex code index = (codeOnly code) {obstructionFirstIndex = fromIntegral index}
-  indices :: (Integral first, Integral second) => Word32 -> first -> second -> CObstruction
-  indices code firstIndexValue secondIndexValue =
-    (codeOnly code)
-      { obstructionFirstIndex = fromIntegral firstIndexValue
-      , obstructionSecondIndex = fromIntegral secondIndexValue
-      }
-  firstValue :: Word32 -> Double -> CObstruction
-  firstValue code value = (codeOnly code) {obstructionFirstValue = value}
-  values :: Word32 -> Double -> Double -> CObstruction
-  values code firstCoordinateValue secondCoordinateValue =
-    (codeOnly code)
-      { obstructionFirstValue = firstCoordinateValue
-      , obstructionSecondValue = secondCoordinateValue
-      }
-  pointObstruction :: Word32 -> Double -> Double -> CObstruction
-  pointObstruction code x y =
-    (codeOnly code)
-      { obstructionPointX = x
-      , obstructionPointY = y
-      }
-  nonFinite :: Word32 -> T.NonFiniteValue -> CObstruction
-  nonFinite code value = firstIndex code (nonFiniteCode value)
-
-coordinateErrorCode :: T.CoordinateError -> Word32
-coordinateErrorCode reason =
-  case reason of
-    T.CoordinateNaN -> 1
-    T.CoordinateInfinite -> 2
-    T.CoordinateTooSmall -> 3
-    T.CoordinateTooLarge -> 4
-
-nonFiniteCode :: T.NonFiniteValue -> Word32
-nonFiniteCode value =
-  case value of
-    T.ValueNaN -> 1
-    T.ValuePositiveInfinity -> 2
-    T.ValueNegativeInfinity -> 3
diff --git a/src-planar/Moonlight/Triangulation/Internal/Minkowski/Convex.hs b/src-planar/Moonlight/Triangulation/Internal/Minkowski/Convex.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Internal/Minkowski/Convex.hs
+++ /dev/null
@@ -1,326 +0,0 @@
--- | Pure exact convex-polygon algebra: admission, linear edge-angle
--- convolution, reflection, hull construction, and support-half-plane erosion.
-module Moonlight.Triangulation.Internal.Minkowski.Convex
-  ( convexPolygon
-  , convexPolygonPoints
-  , convexPolygonRegion
-  , admittedConvexLoop
-  , structuringElement
-  , structuringElementPolygon
-  , convexMinkowskiSum
-  , convexMinkowskiPolygon
-  , convexHullPolygon
-  , reflectConvexPolygon
-  , convexPolygonCentroid
-  , erodeConvexBy
-  , addExactPoints
-  , subtractExactPoints
-  ) where
-
-import Control.Monad (foldM)
-import Data.Bifunctor (first)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Set as Set
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , ExactVector (..)
-  , addExactVectors
-  , compareExactVectorAngle
-  , exactVectorFromPoints
-  , exactSegment
-  , exactSupportingLineIntersection
-  , exactOrient2d
-  , exactPoint
-  , exactPointCoordinates
-  , translateExactPoint
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( cyclePairs
-  , cyclePairsNonEmpty
-  , cyclicTriples
-  , rotateCycleLeast
-  , rotateCycleLeastBy
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactDivide
-  )
-import Moonlight.Triangulation.Internal.Minkowski.Types
-  ( ConvexPolygon (..)
-  , MinkowskiError (..)
-  , StructuringElement (..)
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PlanarRegion (..)
-  , PolygonComponent (..)
-  , RegionPointLocation (..)
-  )
-import Moonlight.Triangulation.Region
-  ( exactLoop
-  , exactLoopPoints
-  , regionPointLocation
-  )
-
-convexPolygon
-  :: NonEmpty ExactPoint
-  -> Either MinkowskiError ConvexPolygon
-convexPolygon submitted = do
-  loop <- first MinkowskiInvalidConvexLoop (exactLoop submitted)
-  let points = exactLoopPoints loop
-  case
-    [ (index, turn)
-    | (index, (previous, current, next)) <-
-        zip [0 :: Int ..] (cyclicTriples (NonEmpty.toList points))
-    , let turn = exactOrient2d previous current next
-    , turn /= GT
-    ] of
-    (index, turn) : _ -> Left (MinkowskiNonConvexTurn index turn)
-    [] -> Right (ConvexPolygon loop)
-
-convexPolygonPoints :: ConvexPolygon -> NonEmpty ExactPoint
-convexPolygonPoints (ConvexPolygon loop) = exactLoopPoints loop
-
-convexPolygonRegion :: ConvexPolygon -> PlanarRegion
-convexPolygonRegion (ConvexPolygon loop) =
-  PlanarRegion [PolygonComponent loop []]
-
-admittedConvexLoop :: ExactLoop -> Maybe ConvexPolygon
-admittedConvexLoop loop
-  | all ((== GT) . orderedTurn) (cyclicTriples (NonEmpty.toList (exactLoopPoints loop))) =
-      Just (ConvexPolygon loop)
-  | otherwise = Nothing
- where
-  orderedTurn (previous, current, next) =
-    exactOrient2d previous current next
-
-structuringElement
-  :: ConvexPolygon
-  -> Either MinkowskiError StructuringElement
-structuringElement polygon =
-  let origin = exactPoint 0 0
-      location = regionPointLocation (convexPolygonRegion polygon) origin
-   in case location of
-        RegionExterior -> Left (MinkowskiOriginOutside location)
-        _ -> Right (StructuringElement polygon)
-
-structuringElementPolygon :: StructuringElement -> ConvexPolygon
-structuringElementPolygon (StructuringElement polygon) = polygon
-
-convexMinkowskiSum
-  :: ConvexPolygon
-  -> ConvexPolygon
-  -> PlanarRegion
-convexMinkowskiSum left right =
-  convexPolygonRegion (convexMinkowskiPolygon left right)
-
-convexMinkowskiPolygon
-  :: ConvexPolygon
-  -> ConvexPolygon
-  -> ConvexPolygon
-convexMinkowskiPolygon left right =
-  let leftPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints left)
-      rightPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints right)
-      start = addExactPoints (NonEmpty.head leftPoints) (NonEmpty.head rightPoints)
-      directions =
-        mergeDirections
-          (edgeDirections leftPoints)
-          (edgeDirections rightPoints)
-      directionList = NonEmpty.toList directions
-      scanned = scanl translateExactPoint start directionList
-      resultPoints = start :| take (length directionList - 1) (drop 1 scanned)
-   in ConvexPolygon (ExactLoop (rotateCycleLeast resultPoints))
-
-convexHullPolygon
-  :: NonEmpty ExactPoint
-  -> Either MinkowskiError ConvexPolygon
-convexHullPolygon submitted =
-  let points = NonEmpty.toList submitted
-   in case convexHullPoints points of
-    Nothing -> Left (MinkowskiConvexHullDegenerate points)
-    Just hullPoints -> Right (ConvexPolygon (ExactLoop (rotateCycleLeast hullPoints)))
-
-reflectConvexPolygon :: ConvexPolygon -> ConvexPolygon
-reflectConvexPolygon polygon =
-  ConvexPolygon
-    ( ExactLoop
-        ( rotateCycleLeast
-            (NonEmpty.reverse (fmap negateExactPoint (convexPolygonPoints polygon)))
-        )
-    )
-
-convexPolygonCentroid
-  :: ConvexPolygon
-  -> Either MinkowskiError ExactPoint
-convexPolygonCentroid polygon = do
-  let points = convexPolygonPoints polygon
-      count = fromIntegral (NonEmpty.length points)
-      (sumX, sumY) =
-        List.foldl'
-          (\(accumulatedX, accumulatedY) point ->
-             let (x, y) = exactPointCoordinates point
-              in (accumulatedX + x, accumulatedY + y))
-          (0, 0)
-          points
-  x <- first MinkowskiExactArithmetic (exactDivide sumX count)
-  y <- first MinkowskiExactArithmetic (exactDivide sumY count)
-  pure (exactPoint x y)
-
--- | Erode one convex polygon by another through the strongest translated
--- support half-plane for each source edge. Sutherland--Hodgman descent keeps
--- the construction exact; a lower-dimensional residual is represented by the
--- empty polygonal region at this two-dimensional publication boundary.
-erodeConvexBy
-  :: ConvexPolygon
-  -> ConvexPolygon
-  -> Either MinkowskiError (Maybe ConvexPolygon)
-erodeConvexBy source kernel = do
-  let sourcePoints = convexPolygonPoints source
-      kernelPoints = convexPolygonPoints kernel
-      firstKernel = NonEmpty.head kernelPoints
-      initial =
-        map
-          (`subtractExactPoints` firstKernel)
-          (NonEmpty.toList sourcePoints)
-      halfPlanes =
-        [ strongestHalfPlane kernelPoints from to
-        | (from, to) <- cyclePairs sourcePoints
-        ]
-  clipped <- foldM clipPolygon initial halfPlanes
-  pure (ConvexPolygon . ExactLoop . rotateCycleLeast <$> convexHullPoints clipped)
-
-edgeDirections :: NonEmpty ExactPoint -> NonEmpty ExactVector
-edgeDirections = fmap (uncurry exactVectorFromPoints) . cyclePairsNonEmpty
-
-mergeDirections
-  :: NonEmpty ExactVector
-  -> NonEmpty ExactVector
-  -> NonEmpty ExactVector
-mergeDirections (left :| leftTail) (right :| rightTail) =
-  case compareExactVectorAngle left right of
-    LT -> left :| mergeRemaining leftTail (right : rightTail)
-    GT -> right :| mergeRemaining (left : leftTail) rightTail
-    EQ -> addExactVectors left right :| mergeRemaining leftTail rightTail
-
-mergeRemaining :: [ExactVector] -> [ExactVector] -> [ExactVector]
-mergeRemaining [] right = right
-mergeRemaining left [] = left
-mergeRemaining left@(leftHead : leftTail) right@(rightHead : rightTail) =
-  case compareExactVectorAngle leftHead rightHead of
-    LT -> leftHead : mergeRemaining leftTail right
-    GT -> rightHead : mergeRemaining left rightTail
-    EQ -> addExactVectors leftHead rightHead : mergeRemaining leftTail rightTail
-
-pointSweepKey :: ExactPoint -> (ExactRational, ExactRational)
-pointSweepKey point =
-  let (x, y) = exactPointCoordinates point
-   in (y, x)
-
-convexHullPoints :: [ExactPoint] -> Maybe (NonEmpty ExactPoint)
-convexHullPoints submitted =
-  case Set.toAscList (Set.fromList submitted) of
-    firstPoint : secondPoint : thirdPoint : remaining ->
-      let ordered = firstPoint : secondPoint : thirdPoint : remaining
-          lower = dropFinal (reverse (List.foldl' hullStep [] ordered))
-          upper = dropFinal (reverse (List.foldl' hullStep [] (reverse ordered)))
-       in case lower <> upper of
-            firstHullPoint : secondHullPoint : thirdHullPoint : hullTail ->
-              Just (firstHullPoint :| (secondHullPoint : thirdHullPoint : hullTail))
-            _ -> Nothing
-    _ -> Nothing
-
-hullStep :: [ExactPoint] -> ExactPoint -> [ExactPoint]
-hullStep (current : previous : remaining) candidate
-  | exactOrient2d previous current candidate /= GT =
-      hullStep (previous : remaining) candidate
-hullStep hull candidate = candidate : hull
-
-dropFinal :: [value] -> [value]
-dropFinal values =
-  case reverse values of
-    _ : remaining -> reverse remaining
-    [] -> []
-
-strongestHalfPlane
-  :: NonEmpty ExactPoint
-  -> ExactPoint
-  -> ExactPoint
-  -> (ExactPoint, ExactPoint)
-strongestHalfPlane kernelPoints from to =
-  let direction = exactVectorFromPoints from to
-      supportPoint =
-        case kernelPoints of
-          initial :| remaining ->
-            List.foldl'
-              (\selected candidate ->
-                 if directionPointCross direction candidate
-                      < directionPointCross direction selected
-                   then candidate
-                   else selected)
-              initial
-              remaining
-   in ( subtractExactPoints from supportPoint
-      , subtractExactPoints to supportPoint
-      )
-
-directionPointCross :: ExactVector -> ExactPoint -> ExactRational
-directionPointCross (ExactVector directionX directionY) point =
-  let (x, y) = exactPointCoordinates point
-   in directionX * y - directionY * x
-
-clipPolygon
-  :: [ExactPoint]
-  -> (ExactPoint, ExactPoint)
-  -> Either MinkowskiError [ExactPoint]
-clipPolygon [] _ = Right []
-clipPolygon polygon halfPlane =
-  concat <$> traverse (clipEdge halfPlane) (cyclePairsList polygon)
-
-clipEdge
-  :: (ExactPoint, ExactPoint)
-  -> (ExactPoint, ExactPoint)
-  -> Either MinkowskiError [ExactPoint]
-clipEdge (boundaryFrom, boundaryTo) (from, to) =
-  case (inside from, inside to) of
-    (True, True) -> Right [to]
-    (True, False) -> (: []) <$> supportingLineIntersection from to boundaryFrom boundaryTo
-    (False, True) -> do
-      crossing <- supportingLineIntersection from to boundaryFrom boundaryTo
-      pure [crossing, to]
-    (False, False) -> Right []
- where
-  inside point = exactOrient2d boundaryFrom boundaryTo point /= LT
-
-supportingLineIntersection
-  :: ExactPoint
-  -> ExactPoint
-  -> ExactPoint
-  -> ExactPoint
-  -> Either MinkowskiError ExactPoint
-supportingLineIntersection lineFrom lineTo boundaryFrom boundaryTo = do
-  clippedSegment <- first MinkowskiInvalidSegment (exactSegment lineFrom lineTo)
-  boundarySegment <- first MinkowskiInvalidSegment (exactSegment boundaryFrom boundaryTo)
-  first MinkowskiLineIntersection
-    (exactSupportingLineIntersection clippedSegment boundarySegment)
-
-addExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
-addExactPoints left right =
-  let (leftX, leftY) = exactPointCoordinates left
-      (rightX, rightY) = exactPointCoordinates right
-   in exactPoint (leftX + rightX) (leftY + rightY)
-
-subtractExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
-subtractExactPoints left right =
-  let (leftX, leftY) = exactPointCoordinates left
-      (rightX, rightY) = exactPointCoordinates right
-   in exactPoint (leftX - rightX) (leftY - rightY)
-
-negateExactPoint :: ExactPoint -> ExactPoint
-negateExactPoint point =
-  let (x, y) = exactPointCoordinates point
-   in exactPoint (negate x) (negate y)
-
-cyclePairsList :: [value] -> [(value, value)]
-cyclePairsList = maybe [] cyclePairs . NonEmpty.nonEmpty
diff --git a/src-planar/Moonlight/Triangulation/Internal/Minkowski/Types.hs b/src-planar/Moonlight/Triangulation/Internal/Minkowski/Types.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Internal/Minkowski/Types.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Closed vocabulary and invariant carriers for exact polygonal morphology.
-module Moonlight.Triangulation.Internal.Minkowski.Types
-  ( ConvexPolygon (..)
-  , StructuringElement (..)
-  , MinkowskiOperation (..)
-  , MinkowskiError (..)
-  , MinkowskiReceipt (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactGeometryError
-  , ExactIntersectionError
-  , ExactPoint
-  )
-import Moonlight.Triangulation.Handles.HandleDefs (FaceId)
-import Moonlight.Triangulation.Internal.ExactRational (ExactArithmeticError)
-import Moonlight.Triangulation.Internal.Overlay.Types
-  ( OverlayCellId
-  , OverlayCellWitness
-  , OverlayError
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop
-  , RegionPublicationError
-  , RegionPointLocation
-  , RegionValidationError
-  )
-
-newtype ConvexPolygon = ConvexPolygon ExactLoop
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-newtype StructuringElement = StructuringElement ConvexPolygon
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data MinkowskiOperation
-  = MinkowskiAddition
-  | MinkowskiErosion
-  | MinkowskiOpening
-  | MinkowskiClosing
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data MinkowskiError
-  = MinkowskiInvalidConvexLoop !RegionValidationError
-  | MinkowskiInvalidSegment !ExactGeometryError
-  | MinkowskiNonConvexTurn !Int !Ordering
-  | MinkowskiOriginOutside !RegionPointLocation
-  | MinkowskiExactArithmetic !ExactArithmeticError
-  | MinkowskiLineIntersection !ExactIntersectionError
-  | MinkowskiOverlayFailed !(OverlayError Bool Bool)
-  | MinkowskiPublicationFailed !RegionPublicationError
-  | MinkowskiOverlayCellWitness !OverlayCellWitness
-  | MinkowskiFaceArity !FaceId !Int
-  | MinkowskiCandidateCellMissing !OverlayCellId
-  | MinkowskiInclusionAmbiguous !OverlayCellId !ExactPoint
-  | MinkowskiConvexHullDegenerate ![ExactPoint]
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data MinkowskiReceipt = MinkowskiReceipt
-  { minkowskiOperation :: !MinkowskiOperation
-  , minkowskiInputComponents :: !Int
-  , minkowskiConvexPieces :: !Int
-  , minkowskiGeneratedPieces :: !Int
-  , minkowskiGeneratedConvolutionEdges :: !Int
-  , minkowskiOverlayPasses :: !Int
-  , minkowskiExactCrossings :: !Int
-  , minkowskiOutputCells :: !Int
-  , minkowskiExactCoordinateBitGrowth :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
diff --git a/src-planar/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs b/src-planar/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs
+++ /dev/null
@@ -1,814 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact source normalization and binary64 embedding certification. This
--- module ends at the arrangement/resident seam: no DCEL face state crosses it.
-module Moonlight.Triangulation.Internal.Overlay.Arrangement
-  ( ExactEdgeKey
-  , AtomicEdge (..)
-  , atomicEdgeFrom
-  , atomicEdgeTo
-  , OverlayVertexSeed (..)
-  , ArrangementMetrics (..)
-  , CertifiedArrangement (..)
-  , certifyArrangement
-  , canonicalEdgeKey
-  , atomicKey
-  , compareAround
-  ) where
-
-import Data.Bifunctor (first)
-import Data.List (sortBy)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as V
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactGeometryError
-  , ExactPoint
-  , ExactSegment
-  , SegmentRelation (SegmentsShareEndpoint)
-  , compareExactVectorAngle
-  , exactPointCoordinates
-  , exactPointFromPoint
-  , exactSegment
-  , exactSegmentEndpoints
-  , exactVectorFromPoints
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( consecutivePairs
-  , orderedPair
-  , unorderedPairs
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactDivide
-  , exactRationalIsZero
-  )
-import Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSegmentEvent (..)
-  , ExactSegmentEventPlan
-  , ExactSweepSegmentId (..)
-  , exactSegmentEventPlan
-  , exactSegmentEvents
-  , exactSegmentPairChecks
-  , exactSegmentRelationMap
-  , exactSegmentSplitPoints
-  , exactSegmentSweepMaximumHeight
-  )
-import Moonlight.Triangulation.Internal.Overlay.Embedding
-  ( DraftId (..)
-  , DraftIncidence (..)
-  , DraftNeighborhood (..)
-  , DraftReference (..)
-  , DraftVertexId
-  , ExactArrangementDraft (..)
-  , LocalEmbeddingCertificate (..)
-  , OverlayEmbeddingObstruction (..)
-  , certifyLocalEmbedding
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-import Moonlight.Triangulation.Internal.Types (HasPosition (..), Point)
-import Moonlight.Triangulation.Region
-  ( ExactLoop
-  , PlanarLayer
-  , PolygonComponent
-  , exactLoopPoints
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarRegionComponents
-  , polygonHoleLoops
-  , polygonOuterLoop
-  )
-
-type ExactEdgeKey = (ExactPoint, ExactPoint)
-
-data SourceBoundary leftLabel rightLabel
-  = LeftSourceBoundary
-      !ExactSegment
-      !leftLabel
-      !(BoundaryVertexRef 'LeftOverlayOperand)
-      !(BoundaryVertexRef 'LeftOverlayOperand)
-      !(BoundaryEdgeRef 'LeftOverlayOperand)
-  | RightSourceBoundary
-      !ExactSegment
-      !rightLabel
-      !(BoundaryVertexRef 'RightOverlayOperand)
-      !(BoundaryVertexRef 'RightOverlayOperand)
-      !(BoundaryEdgeRef 'RightOverlayOperand)
-
-type SourceBoundaryConstructor operand label leftLabel rightLabel =
-  ExactSegment
-  -> label
-  -> BoundaryVertexRef operand
-  -> BoundaryVertexRef operand
-  -> BoundaryEdgeRef operand
-  -> SourceBoundary leftLabel rightLabel
-
-data AtomicContribution leftLabel rightLabel =
-  AtomicContribution !(SourceBoundary leftLabel rightLabel) !Bool
-
-data AtomicEdge leftLabel rightLabel = AtomicEdge
-  { atomicEdgeSegment :: !ExactSegment
-  , atomicEdgeOrigin :: !OverlayEdgeOrigin
-  , atomicEdgeLeftTransition :: !(Maybe (leftLabel, leftLabel))
-  , atomicEdgeRightTransition :: !(Maybe (rightLabel, rightLabel))
-  }
-
-data OriginAccumulation = OriginAccumulation
-  { accumulatedLeftVertices :: !(Set (BoundaryVertexRef 'LeftOverlayOperand))
-  , accumulatedRightVertices :: !(Set (BoundaryVertexRef 'RightOverlayOperand))
-  , accumulatedLeftEdges :: !(Set (BoundaryEdgeRef 'LeftOverlayOperand))
-  , accumulatedRightEdges :: !(Set (BoundaryEdgeRef 'RightOverlayOperand))
-  }
-
-data OverlayVertexSeed = OverlayVertexSeed
-  { seedExactPoint :: !ExactPoint
-  , seedEmbeddedPoint :: !Point
-  , seedOrigin :: !OverlayVertexOrigin
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-
-instance HasPosition OverlayVertexSeed where
-  position = seedEmbeddedPoint
-
-data ArrangementMetrics = ArrangementMetrics
-  { arrangementInputSegments :: !Int
-  , arrangementRelationEvents :: !Int
-  , arrangementExactCrossings :: !Int
-  , arrangementOverlapIntervals :: !Int
-  , arrangementEmbeddingCandidates :: !Int
-  , arrangementTotalRelationChecks :: !Int
-  , arrangementSweepMaximumHeight :: !Int
-  }
-
-data CertifiedArrangement leftLabel rightLabel = CertifiedArrangement
-  { certifiedAtomicEdges :: !(V.Vector (AtomicEdge leftLabel rightLabel))
-  , certifiedVertexSeeds :: !(V.Vector OverlayVertexSeed)
-  , certifiedConstraints :: !(V.Vector (Int, Int))
-  , certifiedInexactEmbeddingPoints :: !(Set ExactPoint)
-  , certifiedMetrics :: !ArrangementMetrics
-  }
-
-certifyArrangement
-  :: (Ord leftLabel, Ord rightLabel)
-  => PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (CertifiedArrangement leftLabel rightLabel)
-certifyArrangement leftLayer rightLayer = do
-  sources <- flattenLayers leftLayer rightLayer
-  sourcePlan <-
-    first OverlaySegmentEventsInvalid
-      (exactSegmentEventPlan (V.map sourceExactSegment sources))
-  atomicEdges <- normalizeAtomicEdges leftLayer rightLayer sources sourcePlan
-  let atomicVector = V.fromList atomicEdges
-      pointIds = exactPointIds atomicVector
-      origins = vertexOrigins sources atomicVector
-      atomicRelations = atomicEndpointRelations atomicVector
-  draft <- exactArrangementDraft sources sourcePlan atomicVector pointIds atomicRelations
-  localCertificate <- first OverlayEmbeddingRefused (certifyLocalEmbedding draft)
-  projectedPoints <- projectedExactPoints localCertificate
-  let inexactEmbeddingPoints =
-        Set.fromList
-          [ exactPoint
-          | (exactPoint, vertexId) <- Map.toAscList pointIds
-          , Map.lookup vertexId projectedPoints /= Just exactPoint
-          ]
-  projectedSegments <- projectedAtomicSegments atomicVector pointIds projectedPoints
-  projectedPlan <-
-    first OverlaySegmentEventsInvalid
-      (exactSegmentEventPlan projectedSegments)
-  dischargeGlobalRelations atomicRelations projectedPlan
-  seeds <- overlayVertexSeeds pointIds origins localCertificate
-  constraints <- atomicConstraints pointIds atomicVector
-  let events = exactSegmentEvents sourcePlan
-  pure
-    CertifiedArrangement
-      { certifiedAtomicEdges = atomicVector
-      , certifiedVertexSeeds = seeds
-      , certifiedConstraints = constraints
-      , certifiedInexactEmbeddingPoints = inexactEmbeddingPoints
-      , certifiedMetrics =
-          ArrangementMetrics
-            { arrangementInputSegments = V.length sources
-            , arrangementRelationEvents = length events
-            , arrangementExactCrossings =
-                length [() | ExactProperCrossing {} <- events]
-            , arrangementOverlapIntervals =
-                length [() | ExactCollinearOverlap {} <- events]
-            , arrangementEmbeddingCandidates = Map.size pointIds
-            , arrangementTotalRelationChecks =
-                exactSegmentPairChecks sourcePlan
-                  + exactSegmentPairChecks projectedPlan
-            , arrangementSweepMaximumHeight =
-                max
-                  (exactSegmentSweepMaximumHeight sourcePlan)
-                  (exactSegmentSweepMaximumHeight projectedPlan)
-            }
-      }
-flattenLayers
-  :: PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector (SourceBoundary leftLabel rightLabel))
-flattenLayers leftLayer rightLayer = do
-  leftSources <- flattenLayer LeftSourceBoundary leftLayer
-  rightSources <- flattenLayer RightSourceBoundary rightLayer
-  pure (V.fromList (leftSources <> rightSources))
-
-flattenLayer
-  :: SourceBoundaryConstructor operand label leftLabel rightLabel
-  -> PlanarLayer label
-  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
-flattenLayer makeBoundary layer =
-  fmap concat
-    ( traverse
-        (\(componentIndex, (label, component)) ->
-           flattenComponent makeBoundary componentIndex label component)
-        (zip [0 ..] (labelledComponents layer))
-    )
-{-# INLINE flattenLayer #-}
-
-labelledComponents :: PlanarLayer label -> [(label, PolygonComponent)]
-labelledComponents layer =
-  [ (label, component)
-  | (label, region) <- Map.toAscList (planarLayerRegions layer)
-  , component <- planarRegionComponents region
-  ]
-
-flattenComponent
-  :: SourceBoundaryConstructor operand label leftLabel rightLabel
-  -> Int
-  -> label
-  -> PolygonComponent
-  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
-flattenComponent makeBoundary componentIndex label component = do
-  outer <-
-    flattenLoop
-      makeBoundary
-      componentIndex
-      BoundaryOuterLoop
-      label
-      (polygonOuterLoop component)
-  holes <-
-    fmap concat
-      ( traverse
-          (\(holeIndex, loop) ->
-             flattenLoop
-               makeBoundary
-               componentIndex
-               (BoundaryHoleLoop holeIndex)
-               label
-               loop)
-          (zip [0 ..] (polygonHoleLoops component))
-      )
-  pure (outer <> holes)
-{-# INLINE flattenComponent #-}
-
-flattenLoop
-  :: SourceBoundaryConstructor operand label leftLabel rightLabel
-  -> Int
-  -> BoundaryLoopRef
-  -> label
-  -> ExactLoop
-  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
-flattenLoop makeBoundary componentIndex loopRef label loop =
-  traverse constructSource (indexedCycle (exactLoopPoints loop))
- where
-  constructSource (edgeIndex, fromIndex, from, toIndex, to) = do
-    segment <- first (sourceGeometryError from) (exactSegment from to)
-    pure
-      ( makeBoundary
-          segment
-          label
-          (BoundaryRef componentIndex loopRef fromIndex)
-          (BoundaryRef componentIndex loopRef toIndex)
-          (BoundaryRef componentIndex loopRef edgeIndex)
-      )
-{-# INLINE flattenLoop #-}
-
-sourceGeometryError
-  :: ExactPoint
-  -> ExactGeometryError
-  -> OverlayError leftLabel rightLabel
-sourceGeometryError point _ = OverlayArrangementInvalid (OverlayRotationDegenerate point)
-
-indexedCycle :: NonEmpty value -> [(Int, Int, value, Int, value)]
-indexedCycle (firstValue :| remaining) =
-  let values = firstValue : remaining
-      count = length values
-   in [ (index, index, from, (index + 1) `mod` count, to)
-      | (index, (from, to)) <- zip [0 ..] (zip values (remaining <> [firstValue]))
-      ]
-
-sourceExactSegment :: SourceBoundary leftLabel rightLabel -> ExactSegment
-sourceExactSegment (LeftSourceBoundary segment _ _ _ _) = segment
-sourceExactSegment (RightSourceBoundary segment _ _ _ _) = segment
-
-normalizeAtomicEdges
-  :: (Ord leftLabel, Ord rightLabel)
-  => PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> V.Vector (SourceBoundary leftLabel rightLabel)
-  -> ExactSegmentEventPlan
-  -> Either (OverlayError leftLabel rightLabel) [AtomicEdge leftLabel rightLabel]
-normalizeAtomicEdges leftLayer rightLayer sources plan =
-  fmap concat
-    ( traverse
-        (resolveAtomicContributions leftOutside rightOutside)
-        (Map.toAscList grouped)
-    )
- where
-  leftOutside = planarLayerOutsideLabel leftLayer
-  rightOutside = planarLayerOutsideLabel rightLayer
-  grouped =
-    V.ifoldl'
-      (\groups sourceIndex source ->
-         List.foldl'
-           (insertAtomic source)
-           groups
-           (consecutivePairs (exactSegmentSplitPoints plan (ExactSweepSegmentId sourceIndex))))
-      Map.empty
-      sources
-  insertAtomic
-    :: SourceBoundary leftLabel' rightLabel'
-    -> Map ExactEdgeKey [AtomicContribution leftLabel' rightLabel']
-    -> (ExactPoint, ExactPoint)
-    -> Map ExactEdgeKey [AtomicContribution leftLabel' rightLabel']
-  insertAtomic source groups (from, to)
-    | from == to = groups
-    | otherwise =
-        let key@(canonicalFrom, _) = canonicalEdgeKey from to
-            contribution = AtomicContribution source (from == canonicalFrom)
-         in Map.insertWith (<>) key [contribution] groups
-
-resolveAtomicContributions
-  :: (Ord leftLabel, Ord rightLabel)
-  => leftLabel
-  -> rightLabel
-  -> (ExactEdgeKey, [AtomicContribution leftLabel rightLabel])
-  -> Either (OverlayError leftLabel rightLabel) [AtomicEdge leftLabel rightLabel]
-resolveAtomicContributions leftOutside rightOutside ((from, to), contributions) = do
-  let
-      ( leftLabelsOnLeft
-        , leftLabelsOnRight
-        , rightLabelsOnLeft
-        , rightLabelsOnRight
-        , leftSources
-        , rightSources
-        ) =
-          List.foldl'
-            collectContribution
-            (Set.empty, Set.empty, Set.empty, Set.empty, Set.empty, Set.empty)
-            contributions
-      origin =
-        OverlayEdgeOrigin
-          (Set.toAscList leftSources)
-          (Set.toAscList rightSources)
-  leftTransition <-
-    resolveTransition
-      OverlayLeftSourceSideConflict
-      origin
-      leftOutside
-      leftLabelsOnLeft
-      leftLabelsOnRight
-  rightTransition <-
-    resolveTransition
-      OverlayRightSourceSideConflict
-      origin
-      rightOutside
-      rightLabelsOnLeft
-      rightLabelsOnRight
-  if transitionIsIdentity leftTransition && transitionIsIdentity rightTransition
-    then Right []
-    else do
-      segment <- first (sourceGeometryError from) (exactSegment from to)
-      Right
-        [ AtomicEdge
-            { atomicEdgeSegment = segment
-            , atomicEdgeOrigin = origin
-            , atomicEdgeLeftTransition = leftTransition
-            , atomicEdgeRightTransition = rightTransition
-            }
-        ]
- where
-  collectContribution
-    :: (Ord leftLabel', Ord rightLabel')
-    => ( Set leftLabel'
-       , Set leftLabel'
-       , Set rightLabel'
-       , Set rightLabel'
-       , Set (BoundaryEdgeRef 'LeftOverlayOperand)
-       , Set (BoundaryEdgeRef 'RightOverlayOperand)
-       )
-    -> AtomicContribution leftLabel' rightLabel'
-    -> ( Set leftLabel'
-       , Set leftLabel'
-       , Set rightLabel'
-       , Set rightLabel'
-       , Set (BoundaryEdgeRef 'LeftOverlayOperand)
-       , Set (BoundaryEdgeRef 'RightOverlayOperand)
-       )
-  collectContribution
-    ( !leftLeft
-      , !leftRight
-      , !rightLeft
-      , !rightRight
-      , !leftSources
-      , !rightSources
-      )
-    contribution =
-    case contribution of
-      AtomicContribution (LeftSourceBoundary _ label _ _ reference) follows ->
-        ( if follows then Set.insert label leftLeft else leftLeft
-        , if follows then leftRight else Set.insert label leftRight
-        , rightLeft
-        , rightRight
-        , Set.insert reference leftSources
-        , rightSources
-        )
-      AtomicContribution (RightSourceBoundary _ label _ _ reference) follows ->
-        ( leftLeft
-        , leftRight
-        , if follows then Set.insert label rightLeft else rightLeft
-        , if follows then rightRight else Set.insert label rightRight
-        , leftSources
-        , Set.insert reference rightSources
-        )
-
-resolveTransition
-  :: (OverlayEdgeOrigin -> NonEmpty label -> OverlayArrangementObstruction leftLabel rightLabel)
-  -> OverlayEdgeOrigin
-  -> label
-  -> Set label
-  -> Set label
-  -> Either (OverlayError leftLabel rightLabel) (Maybe (label, label))
-resolveTransition sideConflict origin outside labelsOnLeft labelsOnRight
-  | Set.null labelsOnLeft && Set.null labelsOnRight = Right Nothing
-  | otherwise =
-      Just
-        <$> ((,)
-               <$> resolveSide sideConflict origin outside labelsOnLeft
-               <*> resolveSide sideConflict origin outside labelsOnRight)
-{-# INLINE resolveTransition #-}
-
-transitionIsIdentity :: Eq label => Maybe (label, label) -> Bool
-transitionIsIdentity Nothing = True
-transitionIsIdentity (Just (leftLabel, rightLabel)) = leftLabel == rightLabel
-
-resolveSide
-  :: (OverlayEdgeOrigin -> NonEmpty label -> OverlayArrangementObstruction leftLabel rightLabel)
-  -> OverlayEdgeOrigin
-  -> label
-  -> Set label
-  -> Either (OverlayError leftLabel rightLabel) label
-resolveSide sideConflict origin outside labels =
-  case Set.toAscList labels of
-    [] -> Right outside
-    [label] -> Right label
-    firstLabel : remaining ->
-      Left
-        ( OverlayArrangementInvalid
-            (sideConflict origin (firstLabel :| remaining))
-        )
-{-# INLINE resolveSide #-}
-
-exactPointIds
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-exactPointIds edges =
-  Map.fromAscList
-    ( zip
-        (Set.toAscList (V.foldl' collect Set.empty edges))
-      (map DraftId [0 ..])
-    )
- where
-  collect
-    :: Set ExactPoint
-    -> AtomicEdge leftLabel rightLabel
-    -> Set ExactPoint
-  collect points edge = Set.insert (atomicEdgeFrom edge) (Set.insert (atomicEdgeTo edge) points)
-
-vertexOrigins
-  :: V.Vector (SourceBoundary leftLabel rightLabel)
-  -> V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint OverlayVertexOrigin
-vertexOrigins sources atomicEdges =
-  Map.map finalizeOrigin
-    ( V.foldl'
-        addAtomicOrigin
-        (V.foldl' addSourceEndpoints Map.empty sources)
-        atomicEdges
-    )
-
-emptyOrigin :: OriginAccumulation
-emptyOrigin = OriginAccumulation Set.empty Set.empty Set.empty Set.empty
-
-mergeOrigin :: OriginAccumulation -> OriginAccumulation -> OriginAccumulation
-mergeOrigin left right =
-  OriginAccumulation
-    { accumulatedLeftVertices = accumulatedLeftVertices left <> accumulatedLeftVertices right
-    , accumulatedRightVertices = accumulatedRightVertices left <> accumulatedRightVertices right
-    , accumulatedLeftEdges = accumulatedLeftEdges left <> accumulatedLeftEdges right
-    , accumulatedRightEdges = accumulatedRightEdges left <> accumulatedRightEdges right
-    }
-
-addSourceEndpoints
-  :: Map ExactPoint OriginAccumulation
-  -> SourceBoundary leftLabel rightLabel
-  -> Map ExactPoint OriginAccumulation
-addSourceEndpoints origins source =
-  case source of
-    LeftSourceBoundary segment _ fromReference toReference _ ->
-      let (from, to) = exactSegmentEndpoints segment
-       in insertOrigin to (emptyOrigin{accumulatedLeftVertices = Set.singleton toReference})
-            (insertOrigin from (emptyOrigin{accumulatedLeftVertices = Set.singleton fromReference}) origins)
-    RightSourceBoundary segment _ fromReference toReference _ ->
-      let (from, to) = exactSegmentEndpoints segment
-       in insertOrigin to (emptyOrigin{accumulatedRightVertices = Set.singleton toReference})
-            (insertOrigin from (emptyOrigin{accumulatedRightVertices = Set.singleton fromReference}) origins)
-
-addAtomicOrigin
-  :: Map ExactPoint OriginAccumulation
-  -> AtomicEdge leftLabel rightLabel
-  -> Map ExactPoint OriginAccumulation
-addAtomicOrigin origins edge =
-  let origin = atomicEdgeOrigin edge
-      accumulation =
-        emptyOrigin
-          { accumulatedLeftEdges = Set.fromList (overlayEdgeLeftSources origin)
-          , accumulatedRightEdges = Set.fromList (overlayEdgeRightSources origin)
-          }
-   in insertOrigin (atomicEdgeTo edge) accumulation
-        (insertOrigin (atomicEdgeFrom edge) accumulation origins)
-
-insertOrigin
-  :: ExactPoint
-  -> OriginAccumulation
-  -> Map ExactPoint OriginAccumulation
-  -> Map ExactPoint OriginAccumulation
-insertOrigin = Map.insertWith mergeOrigin
-
-finalizeOrigin :: OriginAccumulation -> OverlayVertexOrigin
-finalizeOrigin accumulated =
-  OverlayVertexOrigin
-    { overlayOriginLeftVertices = Set.toAscList (accumulatedLeftVertices accumulated)
-    , overlayOriginRightVertices = Set.toAscList (accumulatedRightVertices accumulated)
-    , overlayOriginLeftEdges = Set.toAscList (accumulatedLeftEdges accumulated)
-    , overlayOriginRightEdges = Set.toAscList (accumulatedRightEdges accumulated)
-    }
-
-exactArrangementDraft
-  :: V.Vector (SourceBoundary leftLabel rightLabel)
-  -> ExactSegmentEventPlan
-  -> V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-  -> Either (OverlayError leftLabel rightLabel) ExactArrangementDraft
-exactArrangementDraft sources sourcePlan atomicEdges pointIds atomicRelations = do
-  draftSegmentsMap <-
-    Map.fromList
-      <$> traverse
-        (\(segmentIndex, edge) -> do
-           from <- requireDraftVertex pointIds (atomicEdgeFrom edge)
-           to <- requireDraftVertex pointIds (atomicEdgeTo edge)
-           pure (DraftId segmentIndex, (from, to)))
-        (V.toList (V.indexed atomicEdges))
-  memberships <-
-    Map.fromList
-      <$> traverse
-        (\(sourceIndex, source) -> do
-           values <-
-             traverse
-               (\point -> do
-                  parameter <- exactSourceParameter source point
-                  vertex <- requireDraftVertex pointIds point
-                  pure (parameter, vertex))
-               (exactSegmentSplitPoints sourcePlan (ExactSweepSegmentId sourceIndex))
-           pure (DraftId sourceIndex, values))
-        (V.toList (V.indexed sources))
-  neighborhoods <- buildDraftNeighborhoods atomicEdges pointIds
-  let incidences =
-        [ DraftIncidence
-            (DraftId leftIndex)
-            (DraftId rightIndex)
-            relation
-        | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
-            Map.toAscList atomicRelations
-        ]
-  pure
-    ExactArrangementDraft
-      { draftVertices = Map.fromList [(vertexId, point) | (point, vertexId) <- Map.toAscList pointIds]
-      , draftSegments = draftSegmentsMap
-      , draftSourceMemberships = memberships
-      , draftIncidences = incidences
-      , draftNeighborhoods = neighborhoods
-      }
-
-requireDraftVertex
-  :: Map ExactPoint DraftVertexId
-  -> ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) DraftVertexId
-requireDraftVertex pointIds point =
-  case Map.lookup point pointIds of
-    Just vertex -> Right vertex
-    Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
-
-exactSourceParameter
-  :: SourceBoundary leftLabel rightLabel
-  -> ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) ExactRational
-exactSourceParameter source point =
-  let (from, to) = exactSegmentEndpoints (sourceExactSegment source)
-      (fromX, fromY) = exactPointCoordinates from
-      (toX, toY) = exactPointCoordinates to
-      (pointX, pointY) = exactPointCoordinates point
-      (numerator, denominator) =
-        if exactRationalIsZero (toX - fromX)
-          then (pointY - fromY, toY - fromY)
-          else (pointX - fromX, toX - fromX)
-   in first OverlayExactArithmetic (exactDivide numerator denominator)
-
-buildDraftNeighborhoods
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-  -> Either (OverlayError leftLabel rightLabel) [DraftNeighborhood]
-buildDraftNeighborhoods edges pointIds =
-  traverse neighborhood (Map.toAscList pointIds)
- where
-  neighborhood (centerPoint, centerId) = do
-    let neighbors = Map.findWithDefault Set.empty centerPoint adjacency
-    orderedPoints <-
-      case NonEmpty.nonEmpty (sortBy (compareAround centerPoint) (Set.toList neighbors)) of
-        Just points -> Right points
-        Nothing -> Left (OverlayArrangementInvalid (OverlayRotationDegenerate centerPoint))
-    orderedIds <- traverse (requireDraftVertex pointIds) orderedPoints
-    pure (DraftNeighborhood centerId orderedIds)
-  adjacency =
-    V.foldl'
-      (\graph edge ->
-         Map.insertWith Set.union (atomicEdgeTo edge) (Set.singleton (atomicEdgeFrom edge))
-           (Map.insertWith Set.union (atomicEdgeFrom edge) (Set.singleton (atomicEdgeTo edge)) graph))
-      Map.empty
-      edges
-
-compareAround :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
-compareAround center left right =
-  case
-    compareExactVectorAngle
-      (exactVectorFromPoints center left)
-      (exactVectorFromPoints center right) of
-    EQ -> compare left right
-    ordering -> ordering
-
--- | Normalization splits every source at every exact event and coalesces
--- duplicate intervals. Distinct atomics can therefore meet only at a stored
--- endpoint; their entire relation section is the endpoint-incidence index.
-atomicEndpointRelations
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-atomicEndpointRelations edges =
-  Map.fromList
-    [ ( orderedPair
-          (ExactSweepSegmentId leftIndex)
-          (ExactSweepSegmentId rightIndex)
-      , SegmentsShareEndpoint
-      )
-    | incident <- Map.elems incidenceByPoint
-    , (leftIndex, rightIndex) <- unorderedPairs (Set.toAscList incident)
-    ]
- where
-  incidenceByPoint =
-    V.ifoldl'
-      (\incidence index edge ->
-         Map.insertWith Set.union (atomicEdgeTo edge) (Set.singleton index)
-           ( Map.insertWith Set.union
-               (atomicEdgeFrom edge)
-               (Set.singleton index)
-               incidence
-           ))
-      Map.empty
-      edges
-projectedExactPoints
-  :: LocalEmbeddingCertificate
-  -> Either (OverlayError leftLabel rightLabel) (Map DraftVertexId ExactPoint)
-projectedExactPoints certificate =
-  Map.traverseWithKey
-    (\vertex point ->
-       first
-         (\projectionError ->
-            OverlayEmbeddingRefused
-              (VertexProjectionRefused vertex projectionError :| []))
-         (exactPointFromPoint point))
-    (certificateRoundedVertices certificate)
-
-projectedAtomicSegments
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-  -> Map DraftVertexId ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector ExactSegment)
-projectedAtomicSegments edges pointIds projected =
-  V.imapM project edges
- where
-  project segmentIndex edge = do
-    fromId <- requireDraftVertex pointIds (atomicEdgeFrom edge)
-    toId <- requireDraftVertex pointIds (atomicEdgeTo edge)
-    from <- requireProjected fromId
-    to <- requireProjected toId
-    first
-      (\_ ->
-         OverlayEmbeddingRefused
-           ( ProjectedSegmentCollapsed
-               (DraftId segmentIndex)
-               fromId
-               toId
-               :| []
-           ))
-      (exactSegment from to)
-  requireProjected vertex =
-    case Map.lookup vertex projected of
-      Just point -> Right point
-      Nothing ->
-        Left
-          ( OverlayEmbeddingRefused
-              (DraftReferenceMissing (DraftVertexReference vertex) :| [])
-          )
-
-dischargeGlobalRelations
-  :: Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-  -> ExactSegmentEventPlan
-  -> Either (OverlayError leftLabel rightLabel) ()
-dischargeGlobalRelations exactRelations projectedPlan =
-  case NonEmpty.nonEmpty obstructions of
-    Nothing -> Right ()
-    Just failures -> Left (OverlayEmbeddingRefused failures)
- where
-  projectedRelations = exactSegmentRelationMap projectedPlan
-  keys = Set.toAscList (Map.keysSet exactRelations <> Map.keysSet projectedRelations)
-  obstructions = concatMap compareRelation keys
-  compareRelation key@(ExactSweepSegmentId leftId, ExactSweepSegmentId rightId) =
-    case (Map.lookup key exactRelations, Map.lookup key projectedRelations) of
-      (Nothing, Just projected) ->
-        [GlobalRelationAdded (DraftId leftId) (DraftId rightId) projected]
-      (Just exact, Nothing) ->
-        [GlobalRelationRemoved (DraftId leftId) (DraftId rightId) exact]
-      (Just exact, Just projected)
-        | exact /= projected ->
-            [GlobalRelationChanged (DraftId leftId) (DraftId rightId) exact projected]
-      _ -> []
-
-overlayVertexSeeds
-  :: Map ExactPoint DraftVertexId
-  -> Map ExactPoint OverlayVertexOrigin
-  -> LocalEmbeddingCertificate
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector OverlayVertexSeed)
-overlayVertexSeeds pointIds origins certificate =
-  V.fromList
-    <$> traverse
-      (\(point, vertexId) -> do
-         embedded <-
-           case Map.lookup vertexId (certificateRoundedVertices certificate) of
-             Just value -> Right value
-             Nothing ->
-               Left
-                 ( OverlayEmbeddingRefused
-                     (DraftReferenceMissing (DraftVertexReference vertexId) :| [])
-                 )
-         origin <-
-           case Map.lookup point origins of
-             Just value -> Right value
-             Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
-         pure (OverlayVertexSeed point embedded origin))
-      (Map.toAscList pointIds)
-
-atomicConstraints
-  :: Map ExactPoint DraftVertexId
-  -> V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector (Int, Int))
-atomicConstraints pointIds =
-  V.mapM
-    (\edge -> do
-       DraftId from <- requireDraftVertex pointIds (atomicEdgeFrom edge)
-       DraftId to <- requireDraftVertex pointIds (atomicEdgeTo edge)
-       pure (from, to))
-
-
-canonicalEdgeKey :: ExactPoint -> ExactPoint -> ExactEdgeKey
-canonicalEdgeKey = orderedPair
-
-atomicEdgeFrom :: AtomicEdge leftLabel rightLabel -> ExactPoint
-atomicEdgeFrom = fst . exactSegmentEndpoints . atomicEdgeSegment
-
-atomicEdgeTo :: AtomicEdge leftLabel rightLabel -> ExactPoint
-atomicEdgeTo = snd . exactSegmentEndpoints . atomicEdgeSegment
-
-atomicKey :: AtomicEdge leftLabel rightLabel -> ExactEdgeKey
-atomicKey edge = (atomicEdgeFrom edge, atomicEdgeTo edge)
diff --git a/src-planar/Moonlight/Triangulation/Internal/Overlay/Embedding.hs b/src-planar/Moonlight/Triangulation/Internal/Overlay/Embedding.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Internal/Overlay/Embedding.hs
+++ /dev/null
@@ -1,533 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Bounded certification of local binary64 embedding obligations for a
--- declared exact arrangement draft.
-module Moonlight.Triangulation.Internal.Overlay.Embedding
-  ( DraftId (..)
-  , DraftVertexId
-  , DraftSegmentId
-  , DraftSourceId
-  , DraftIncidence (..)
-  , DraftNeighborhood (..)
-  , ExactArrangementDraft (..)
-  , DraftReference (..)
-  , OverlayEmbeddingObstruction (..)
-  , EmbeddingObligation (..)
-  , EmbeddingResidual
-  , residualUndischargedObligations
-  , milestoneOneResidual
-  , LocalEmbeddingCertificate (..)
-  , certifyLocalEmbedding
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List (sort, tails)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , SegmentRelation
-  , exactPointToEmbeddingCandidate
-  , exactSegmentRelation
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (cyclePairs)
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import qualified Moonlight.Triangulation.Math as Math
-  ( orient2d
-  , segmentRelation
-  )
-import Moonlight.Triangulation.Types
-  ( Point (..)
-  , PointValidationError
-  )
-
--- | Phantom draft kind; three incompatible identifiers share one scalar owner.
-data DraftEntity = DraftVertexEntity | DraftSegmentEntity | DraftSourceEntity
-
-newtype DraftId (entity :: DraftEntity) = DraftId Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-type DraftVertexId = DraftId 'DraftVertexEntity
-type DraftSegmentId = DraftId 'DraftSegmentEntity
-type DraftSourceId = DraftId 'DraftSourceEntity
-
--- | A declared relation between two atomic draft segments.
-data DraftIncidence = DraftIncidence
-  { -- | First declared atomic segment.
-    draftIncidenceFirstSegment :: !DraftSegmentId
-  , -- | Second declared atomic segment.
-    draftIncidenceSecondSegment :: !DraftSegmentId
-  , -- | Relation declared to hold in both exact and rounded geometry.
-    draftIncidenceRelation :: !SegmentRelation
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One exact vertex and the cyclic neighbor order declared around it.
-data DraftNeighborhood = DraftNeighborhood
-  { -- | Center of the declared local rotation.
-    draftNeighborhoodCenter :: !DraftVertexId
-  , -- | Neighbors in cyclic rotation order.
-    draftNeighborhoodNeighbors :: !(NonEmpty DraftVertexId)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A hand-built exact arrangement draft. It contains only declared local
--- structure; no arrangement or global crossing search is derived here.
-data ExactArrangementDraft = ExactArrangementDraft
-  { -- | Exact coordinates keyed by draft-local vertex identity.
-    draftVertices :: !(Map DraftVertexId ExactPoint)
-  , -- | Atomic segment endpoint identities.
-    draftSegments :: !(Map DraftSegmentId (DraftVertexId, DraftVertexId))
-  , -- | Exact split parameters and vertices in source-segment order.
-    draftSourceMemberships :: !(Map DraftSourceId [(ExactRational, DraftVertexId)])
-  , -- | Segment relations declared to remain invariant under projection.
-    draftIncidences :: ![DraftIncidence]
-  , -- | Cyclic local rotations declared to retain their neighbor order.
-    draftNeighborhoods :: ![DraftNeighborhood]
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The typed identity of a missing draft reference.
-data DraftReference
-  = DraftVertexReference !DraftVertexId
-  | DraftSegmentReference !DraftSegmentId
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A witness that prevents local embedding certification.
-data OverlayEmbeddingObstruction
-  = DraftReferenceMissing !DraftReference
-  | VertexProjectionRefused !DraftVertexId !PointValidationError
-  | RoundedVerticesCollide !DraftVertexId !DraftVertexId !Point
-  | SplitOrderNotPreserved !DraftSourceId !DraftVertexId !DraftVertexId
-  | IncidenceRelationChanged
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-      !SegmentRelation
-      !SegmentRelation
-  | NeighborhoodRotationChanged
-      !DraftVertexId
-      !(NonEmpty DraftVertexId)
-      !(NonEmpty DraftVertexId)
-  | GlobalRelationAdded
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-  | GlobalRelationRemoved
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-  | GlobalRelationChanged
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-      !SegmentRelation
-  | ProjectedSegmentCollapsed
-      !DraftSegmentId
-      !DraftVertexId
-      !DraftVertexId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | An embedding obligation not discharged by the bounded local certifier.
-data EmbeddingObligation
-  = GlobalNoNewCrossing
-  deriving stock (Bounded, Enum, Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A nonempty collection of obligations deferred to a later owner.
-newtype EmbeddingResidual = EmbeddingResidual (NonEmpty EmbeddingObligation)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Read the obligations that remain explicitly undischarged.
-residualUndischargedObligations
-  :: EmbeddingResidual
-  -> NonEmpty EmbeddingObligation
-residualUndischargedObligations (EmbeddingResidual obligations) = obligations
-
--- | The Milestone 1 residual: the arrangement sweep has not yet proved global
--- absence of new crossings.
-milestoneOneResidual :: EmbeddingResidual
-milestoneOneResidual = EmbeddingResidual (GlobalNoNewCrossing :| [])
-
--- | A certificate for exactly four local obligations on a declared draft:
--- vertex distinctness, source split order, declared incidences, and local
--- neighborhood rotation. Global absence of new crossings is unproved until
--- the Milestone 2 arrangement sweep supplies the complete obligation set.
-data LocalEmbeddingCertificate = LocalEmbeddingCertificate
-  { -- | Number of rounded vertex-pair distinctness checks discharged.
-    certificateRoundedVertexDistinctnessCount :: !Int
-  , -- | Number of adjacent source split-order checks discharged.
-    certificateSplitOrderPreservationCount :: !Int
-  , -- | Number of declared incidence checks discharged.
-    certificateIncidenceRelationPreservationCount :: !Int
-  , -- | Number of neighborhood rotation entries certified.
-    certificateNeighborhoodRotationPreservationCount :: !Int
-  , -- | The candidate projection certified by the four local obligations.
-    certificateRoundedVertices :: !(Map DraftVertexId Point)
-  , -- | The necessarily nonempty global obligation residual.
-    certificateResidual :: !EmbeddingResidual
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Certify the four bounded local embedding obligations, collecting every
--- witness within each obligation and preserving obligation order.
-certifyLocalEmbedding
-  :: ExactArrangementDraft
-  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
-certifyLocalEmbedding draft =
-  case NonEmpty.nonEmpty (structuralObstructions draft) of
-    Just obstructions -> Left obstructions
-    Nothing ->
-      case projectVertices (draftVertices draft) of
-        Invalid obstructions -> Left obstructions
-        Valid projectedVertices ->
-          certifyProjectedDraft draft projectedVertices
-
-data Validation value
-  = Invalid !(NonEmpty OverlayEmbeddingObstruction)
-  | Valid value
-
-instance Functor Validation where
-  fmap _ (Invalid obstructions) = Invalid obstructions
-  fmap transform (Valid value) = Valid (transform value)
-
-instance Applicative Validation where
-  pure = Valid
-  Invalid left <*> Invalid right = Invalid (left <> right)
-  Invalid obstructions <*> Valid _ = Invalid obstructions
-  Valid _ <*> Invalid obstructions = Invalid obstructions
-  Valid transform <*> Valid value = Valid (transform value)
-
-invalid :: OverlayEmbeddingObstruction -> Validation value
-invalid obstruction = Invalid (obstruction :| [])
-
-data ProjectedVertex = ProjectedVertex
-  { projectedExactPoint :: !ExactPoint
-  , projectedRoundedPoint :: !Point
-  }
-
-data ResolvedSegment = ResolvedSegment
-  { resolvedSegmentId :: !DraftSegmentId
-  , resolvedSegmentFrom :: !ResolvedVertex
-  , resolvedSegmentTo :: !ResolvedVertex
-  }
-
-data ResolvedVertex = ResolvedVertex
-  { resolvedVertexId :: !DraftVertexId
-  , resolvedProjectedVertex :: !ProjectedVertex
-  }
-
-data ResolvedMembership = ResolvedMembership
-  { resolvedMembershipParameter :: !ExactRational
-  , resolvedMembershipVertex :: !ResolvedVertex
-  }
-
-structuralObstructions
-  :: ExactArrangementDraft
-  -> [OverlayEmbeddingObstruction]
-structuralObstructions draft =
-  segmentEndpointObstructions
-    <> sourceMembershipObstructions
-    <> incidenceObstructions
-    <> neighborhoodObstructions
- where
-  vertices = draftVertices draft
-  segments = draftSegments draft
-  missingVertex vertexId =
-    [ DraftReferenceMissing (DraftVertexReference vertexId)
-    | Map.notMember vertexId vertices
-    ]
-  missingSegment segmentId =
-    [ DraftReferenceMissing (DraftSegmentReference segmentId)
-    | Map.notMember segmentId segments
-    ]
-  segmentEndpointObstructions =
-    concatMap
-      (\(_, (from, to)) -> missingVertex from <> missingVertex to)
-      (Map.toAscList segments)
-  sourceMembershipObstructions =
-    concatMap
-      (concatMap (missingVertex . snd) . snd)
-      (Map.toAscList (draftSourceMemberships draft))
-  incidenceObstructions =
-    concatMap
-      ( \incidence ->
-          missingSegment (draftIncidenceFirstSegment incidence)
-            <> missingSegment (draftIncidenceSecondSegment incidence)
-      )
-      (draftIncidences draft)
-  neighborhoodObstructions =
-    concatMap
-      ( \neighborhood ->
-          missingVertex (draftNeighborhoodCenter neighborhood)
-            <> concatMap missingVertex (draftNeighborhoodNeighbors neighborhood)
-      )
-      (draftNeighborhoods draft)
-
-projectVertices
-  :: Map DraftVertexId ExactPoint
-  -> Validation (Map DraftVertexId ProjectedVertex)
-projectVertices =
-  Map.traverseWithKey
-    ( \vertexId point ->
-        case exactPointToEmbeddingCandidate point of
-          Left projectionError ->
-            invalid (VertexProjectionRefused vertexId projectionError)
-          Right roundedPoint -> Valid (ProjectedVertex point roundedPoint)
-    )
-
-certifyProjectedDraft
-  :: ExactArrangementDraft
-  -> Map DraftVertexId ProjectedVertex
-  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
-certifyProjectedDraft draft projectedVertices =
-  case resolvedFailures of
-    Left obstruction -> Left (obstruction :| [])
-    Right localFailures ->
-      case NonEmpty.nonEmpty (collisionFailures <> localFailures) of
-        Just obstructions -> Left obstructions
-        Nothing ->
-          Right
-            LocalEmbeddingCertificate
-              { certificateRoundedVertexDistinctnessCount = distinctnessCount
-              , certificateSplitOrderPreservationCount = splitOrderCount
-              , certificateIncidenceRelationPreservationCount = incidenceCount
-              , certificateNeighborhoodRotationPreservationCount = neighborhoodCount
-              , certificateRoundedVertices = Map.map projectedRoundedPoint projectedVertices
-              , certificateResidual = milestoneOneResidual
-              }
- where
-  collisionFailures = roundedVertexCollisionObstructions projectedVertices
-  resolvedFailures = do
-    splitFailures <- traverse (uncurry resolveSource) (Map.toAscList (draftSourceMemberships draft))
-    incidenceFailures <- traverse resolveIncidence (draftIncidences draft)
-    neighborhoodFailures <- traverse resolveNeighborhood (draftNeighborhoods draft)
-    pure (concat splitFailures <> concat incidenceFailures <> concat neighborhoodFailures)
-  resolveVertex vertexId =
-    case Map.lookup vertexId projectedVertices of
-      Nothing -> Left (DraftReferenceMissing (DraftVertexReference vertexId))
-      Just projectedVertex -> Right (ResolvedVertex vertexId projectedVertex)
-  resolveSegment segmentId =
-    case Map.lookup segmentId (draftSegments draft) of
-      Nothing -> Left (DraftReferenceMissing (DraftSegmentReference segmentId))
-      Just (from, to) ->
-        ResolvedSegment segmentId
-          <$> resolveVertex from
-          <*> resolveVertex to
-  resolveSource sourceId memberships =
-    splitOrderObstructions sourceId
-      <$> traverse
-        ( \(parameter, vertexId) ->
-            ResolvedMembership parameter <$> resolveVertex vertexId
-        )
-        memberships
-  resolveIncidence incidence =
-    incidenceRelationObstructions
-      (draftIncidenceRelation incidence)
-      <$> resolveSegment (draftIncidenceFirstSegment incidence)
-      <*> resolveSegment (draftIncidenceSecondSegment incidence)
-  resolveNeighborhood neighborhood =
-    neighborhoodRotationObstructions
-      <$> resolveVertex (draftNeighborhoodCenter neighborhood)
-      <*> traverse resolveVertex (draftNeighborhoodNeighbors neighborhood)
-  vertexCount = Map.size projectedVertices
-  distinctnessCount = vertexCount * (vertexCount - 1) `quot` 2
-  splitOrderCount =
-    sum
-      ( map
-          (max 0 . subtract 1 . length)
-          (Map.elems (draftSourceMemberships draft))
-      )
-  incidenceCount = length (draftIncidences draft)
-  neighborhoodCount =
-    sum
-      ( map
-          (length . draftNeighborhoodNeighbors)
-          (draftNeighborhoods draft)
-      )
-
-roundedVertexCollisionObstructions
-  :: Map DraftVertexId ProjectedVertex
-  -> [OverlayEmbeddingObstruction]
-roundedVertexCollisionObstructions projectedVertices =
-  [ RoundedVerticesCollide leftId rightId roundedPoint
-  | (roundedPoint, vertexIds) <- Map.toAscList verticesByRoundedPoint
-  , (leftId : remainingIds) <- tails (sort vertexIds)
-  , rightId <- remainingIds
-  ]
- where
-  verticesByRoundedPoint =
-    Map.fromListWith (<>)
-      [ (projectedRoundedPoint projectedVertex, [vertexId])
-      | (vertexId, projectedVertex) <- Map.toAscList projectedVertices
-      ]
-
-splitOrderObstructions
-  :: DraftSourceId
-  -> [ResolvedMembership]
-  -> [OverlayEmbeddingObstruction]
-splitOrderObstructions sourceId memberships =
-  case memberships of
-    firstMembership : secondMembership : remainingMemberships ->
-      let finalMembership =
-            List.foldl' (\_ current -> current) secondMembership remainingMemberships
-          sourceFrom = roundedMembershipPoint firstMembership
-          sourceTo = roundedMembershipPoint finalMembership
-       in [ SplitOrderNotPreserved
-              sourceId
-              (resolvedVertexId (resolvedMembershipVertex leftMembership))
-              (resolvedVertexId (resolvedMembershipVertex rightMembership))
-          | (leftMembership, rightMembership) <-
-              zip
-                memberships
-                (drop 1 memberships)
-          , compare
-              (resolvedMembershipParameter leftMembership)
-              (resolvedMembershipParameter rightMembership)
-              /= roundedOrderAlong
-                sourceFrom
-                sourceTo
-                (roundedMembershipPoint leftMembership)
-                (roundedMembershipPoint rightMembership)
-          ]
-    _ -> []
-
-roundedMembershipPoint :: ResolvedMembership -> Point
-roundedMembershipPoint =
-  projectedRoundedPoint
-    . resolvedProjectedVertex
-    . resolvedMembershipVertex
-
-roundedOrderAlong :: Point -> Point -> Point -> Point -> Ordering
-roundedOrderAlong
-  (Point sourceFromX sourceFromY)
-  (Point sourceToX sourceToY)
-  (Point leftX leftY)
-  (Point rightX rightY) =
-    let directionX = sourceToX - sourceFromX
-        directionY = sourceToY - sourceFromY
-     in if abs directionX >= abs directionY
-          then
-            if directionX >= 0
-              then compare leftX rightX
-              else compare rightX leftX
-          else
-            if directionY >= 0
-              then compare leftY rightY
-              else compare rightY leftY
-
-incidenceRelationObstructions
-  :: SegmentRelation
-  -> ResolvedSegment
-  -> ResolvedSegment
-  -> [OverlayEmbeddingObstruction]
-incidenceRelationObstructions declaredRelation firstSegment secondSegment =
-  [ IncidenceRelationChanged
-      (resolvedSegmentId firstSegment)
-      (resolvedSegmentId secondSegment)
-      declaredRelation
-      exactRelation
-      roundedRelation
- | exactRelation /= declaredRelation || roundedRelation /= declaredRelation
-  ]
- where
-  exactRelation =
-    relationFor exactSegmentRelation projectedExactPoint firstSegment secondSegment
-  roundedRelation =
-    relationFor Math.segmentRelation projectedRoundedPoint firstSegment secondSegment
-
-relationFor
-  :: (point -> point -> point -> point -> SegmentRelation)
-  -> (ProjectedVertex -> point)
-  -> ResolvedSegment
-  -> ResolvedSegment
-  -> SegmentRelation
-relationFor relation project firstSegment secondSegment =
-  relation
-    (project (resolvedProjectedVertex (resolvedSegmentFrom firstSegment)))
-    (project (resolvedProjectedVertex (resolvedSegmentTo firstSegment)))
-    (project (resolvedProjectedVertex (resolvedSegmentFrom secondSegment)))
-    (project (resolvedProjectedVertex (resolvedSegmentTo secondSegment)))
-
-neighborhoodRotationObstructions
-  :: ResolvedVertex
-  -> NonEmpty ResolvedVertex
-  -> [OverlayEmbeddingObstruction]
-neighborhoodRotationObstructions center exactRotation =
-  [ NeighborhoodRotationChanged
-      (resolvedVertexId center)
-      exactVertexRotation
-      roundedVertexRotation
-  | roundedRotationDescents > 1
-  ]
- where
-  exactVertexRotation = fmap resolvedVertexId exactRotation
-  roundedRotationDescents =
-    List.foldl'
-      (\descentCount (left, right) ->
-         descentCount
-           + if compareRoundedAround center left right == GT
-               then 1
-               else 0)
-      (0 :: Int)
-      (cyclePairs exactRotation)
-  roundedVertexRotation =
-    fmap resolvedVertexId
-      (NonEmpty.sortBy (compareRoundedAround center) exactRotation)
-
-compareRoundedAround
-  :: ResolvedVertex
-  -> ResolvedVertex
-  -> ResolvedVertex
-  -> Ordering
-compareRoundedAround center left right =
-  compareRoundedAroundWith
-    ( Math.orient2d
-        (roundedVertexPoint center)
-        (roundedVertexPoint left)
-        (roundedVertexPoint right)
-    )
-    center
-    left
-    right
-
-compareRoundedAroundWith
-  :: Ordering
-  -> ResolvedVertex
-  -> ResolvedVertex
-  -> ResolvedVertex
-  -> Ordering
-compareRoundedAroundWith roundedOrientation center left right =
-  case compare (roundedVectorHalf center left) (roundedVectorHalf center right) of
-    EQ ->
-      case roundedOrientation of
-        GT -> LT
-        LT -> GT
-        EQ -> compare (roundedVertexPoint left) (roundedVertexPoint right)
-    ordering -> ordering
-
-roundedVectorHalf :: ResolvedVertex -> ResolvedVertex -> Bool
-roundedVectorHalf center neighbor =
-  let Point centerX centerY = roundedVertexPoint center
-      Point neighborX neighborY = roundedVertexPoint neighbor
-      deltaX = neighborX - centerX
-      deltaY = neighborY - centerY
-   in deltaY < 0 || (deltaY == 0 && deltaX < 0)
-
-roundedVertexPoint :: ResolvedVertex -> Point
-roundedVertexPoint = projectedRoundedPoint . resolvedProjectedVertex
diff --git a/src-planar/Moonlight/Triangulation/Internal/Overlay/Resident.hs b/src-planar/Moonlight/Triangulation/Internal/Overlay/Resident.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Internal/Overlay/Resident.hs
+++ /dev/null
@@ -1,923 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Resident DCEL descent and exact-cell gluing. The certified arrangement is
--- authoritative at this seam; diagonal schedules may change only its resident
--- triangulation, never its exact cell descriptors.
-module Moonlight.Triangulation.Internal.Overlay.Resident
-  ( OverlayDiagonalSchedule (..)
-  , residentOverlay
-  , faceLabels
-  , faceCarriesExactArea
-  , regionFaceLabels
-  , vertexSupport
-  , edgeSupport
-  ) where
-
-import Control.Monad (foldM)
-import Data.Bifunctor (first)
-import Data.Foldable (traverse_)
-import Data.List (partition, sort, sortBy)
-import qualified Data.List as List
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Maybe (listToMaybe, mapMaybe)
-import qualified Data.Sequence as Seq
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Dcel
-  ( faceData
-  , faceDirectedEdges
-  , faceVertices
-  , imapUndirectedEdges
-  , incidentFace
-  , isConstraintEdge
-  , numInnerFaces
-  , numVertices
-  , outerFace
-  , undirectedEndpoints
-  , vertexData
-  , vertexOutgoingEdges
-  )
-import Moonlight.Triangulation.Exact (ExactPoint, exactOrient2d)
-import Moonlight.Triangulation.FloodFillIterator
-  ( FaceComponent
-  , componentBoundary
-  , faceComponentFaces
-  , faceComponentsBy
-  )
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( DirectedEdgeId
-  , FaceId
-  , UndirectedEdgeId
-  , VertexId
-  , asUndirected
-  , directedPair
-  , reverseEdge
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( allFaces
-  , innerFaces
-  , undirectedEdges
-  )
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedFromVector)
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( consecutivePairs
-  , traceOrientedBoundaryCircuits
-  )
-import Moonlight.Triangulation.Internal.Canonical (canonicalize)
-import Moonlight.Triangulation.Internal.Cdt.Build (constrainedDelaunay)
-import Moonlight.Triangulation.Internal.Cdt.Types (CdtError (..))
-import Moonlight.Triangulation.Internal.Overlay.Arrangement
-  ( ArrangementMetrics (..)
-  , AtomicEdge (..)
-  , atomicEdgeFrom
-  , atomicEdgeTo
-  , CertifiedArrangement (..)
-  , ExactEdgeKey
-  , OverlayVertexSeed (..)
-  , atomicKey
-  , canonicalEdgeKey
-  , compareAround
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( polygonComponentFromBoundaryCoordinates
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop
-  , PolygonComponent
-  , RegionPublicationError (..)
-  )
-import Moonlight.Triangulation.Internal.Representation
-  ( BuildResult (..)
-  , Triangulation (..)
-  )
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Constrained)
-  , ElementDefaults (..)
-  )
-import Moonlight.Triangulation.Region (exactLoop)
-
-data OverlayDiagonalSchedule
-  = CanonicalOverlayDiagonals
-  | FlipFirstAdmissibleDiagonal
-  deriving stock (Eq, Ord, Show)
-
-data ComponentDraft leftLabel rightLabel = ComponentDraft
-  { componentDraftLabels :: !(leftLabel, rightLabel)
-  , componentDraftFaces :: !FaceComponent
-  , componentDraftPolygon :: !PolygonComponent
-  , componentDraftTouchesOuter :: !Bool
-  }
-
-residentOverlay
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayDiagonalSchedule
-  -> (leftLabel, rightLabel)
-  -> CertifiedArrangement leftLabel rightLabel
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (OverlayResult leftLabel rightLabel)
-residentOverlay diagonalSchedule outsidePair certified = do
-  let atomicVector = certifiedAtomicEdges certified
-      seeds = certifiedVertexSeeds certified
-      constraints = certifiedConstraints certified
-      metrics = certifiedMetrics certified
-      defaults = ElementDefaults () () ()
-  built <- first OverlayBuildFailed (constrainedDelaunay defaults seeds constraints)
-  let resident = buildTriangulation built
-  if numVertices resident == V.length seeds
-    then Right ()
-    else
-      Left
-        ( OverlayProvenanceIncomplete
-            (OverlayEmbeddedVertexCountMismatch (V.length seeds) (numVertices resident))
-        )
-  canonicalResident <-
-    first (OverlayBuildFailed . CdtBuildError) (canonicalize resident)
-  let atomicByKey =
-        Map.fromList
-          [ (atomicKey edge, edge)
-          | edge <- V.toList atomicVector
-          ]
-  validateAtomicConstraints canonicalResident atomicByKey
-  scheduledResident <-
-    applyDiagonalSchedule
-      diagonalSchedule
-      defaults
-      seeds
-      constraints
-      canonicalResident
-  labelledFaces <- labelResidentFaces outsidePair scheduledResident atomicByKey
-  (fullDimensionalFaces, collapsedFaces) <-
-    if Set.null (certifiedInexactEmbeddingPoints certified)
-      then
-        Right
-          ( Set.fromList (innerFaces scheduledResident)
-          , Set.empty
-          )
-      else
-        exactFaceDimensions
-          (certifiedInexactEmbeddingPoints certified)
-          scheduledResident
-  componentDrafts <-
-    residentComponentDrafts
-      scheduledResident
-      labelledFaces
-      fullDimensionalFaces
-  let (unboundedDrafts, boundedDrafts) =
-        partitionDrafts outsidePair componentDrafts
-  numberedBounded <- numberBoundedComponents boundedDrafts
-  let fullCellIdByFace =
-        Map.fromList
-          ( [ (face, OverlayCellId 0)
-            | draftComponent <- unboundedDrafts
-            , face <- faceComponentFaces (componentDraftFaces draftComponent)
-            ]
-              <> [ (face, cellId)
-                 | (cellId, draftComponent) <- numberedBounded
-                 , face <- faceComponentFaces (componentDraftFaces draftComponent)
-                 ]
-          )
-  collapsedCellIds <-
-    descendCollapsedFaces
-      scheduledResident
-      atomicByKey
-      fullCellIdByFace
-      collapsedFaces
-  let cellIdByFace = fullCellIdByFace <> collapsedCellIds
-  unboundedLoops <-
-    unboundedCellBoundaryLoops scheduledResident atomicByKey cellIdByFace
-  let cells =
-        V.fromList
-          ( OverlayCell
-              { overlayCellLeft = fst outsidePair
-              , overlayCellRight = snd outsidePair
-              , overlayCellGeometry = UnboundedOverlayCell unboundedLoops
-              }
-              : [ OverlayCell
-                    { overlayCellLeft = fst (componentDraftLabels draftComponent)
-                    , overlayCellRight = snd (componentDraftLabels draftComponent)
-                    , overlayCellGeometry =
-                        BoundedOverlayCell (componentDraftPolygon draftComponent)
-                    }
-                | (_, draftComponent) <- numberedBounded
-                ]
-          )
-  withFaces <- attachFaceCells collapsedFaces cellIdByFace scheduledResident
-  let withVertices = attachOverlayVertices withFaces
-      withEdges =
-        imapUndirectedEdges
-          OverlayDiagonal
-          (\edge _ ->
-             case Map.lookup (residentEdgeKey withVertices edge) atomicByKey of
-               Nothing -> OverlayDiagonal
-               Just atomic -> OverlayBoundary (atomicEdgeOrigin atomic))
-          withVertices
-      receipt =
-        OverlayReceipt
-          { overlayInputSegments = arrangementInputSegments metrics
-          , overlayRelationEvents = arrangementRelationEvents metrics
-          , overlayExactCrossings = arrangementExactCrossings metrics
-          , overlayOverlapIntervals = arrangementOverlapIntervals metrics
-          , overlayAtomicEdges = V.length atomicVector
-          , overlayOutputVertices = numVertices withEdges
-          , overlayArrangementCells = V.length cells
-          , overlayResidentFaces = numInnerFaces withEdges
-          , overlayEmbeddingCandidates = arrangementEmbeddingCandidates metrics
-          , overlayTotalRelationChecks = arrangementTotalRelationChecks metrics
-          , overlaySweepMaximumHeight = arrangementSweepMaximumHeight metrics
-          }
-  pure
-    OverlayResult
-      { overlayResultTriangulation = withEdges
-      , overlayResultCells = cells
-      , overlayResultOutsideLabels = outsidePair
-      , overlayResultReceipt = receipt
-      }
-
-applyDiagonalSchedule
-  :: OverlayDiagonalSchedule
-  -> ElementDefaults () () ()
-  -> V.Vector OverlayVertexSeed
-  -> V.Vector (Int, Int)
-  -> Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Triangulation 'Constrained OverlayVertexSeed () () ())
-applyDiagonalSchedule CanonicalOverlayDiagonals _ _ _ triangulation =
-  Right triangulation
-applyDiagonalSchedule
-  FlipFirstAdmissibleDiagonal
-  defaults
-  seeds
-  constraints
-  triangulation =
-    case firstAlternativeDiagonal triangulation of
-      Nothing -> Right triangulation
-      Just (from, to) -> do
-        let indexByPoint =
-              Map.fromList
-                [ (seedExactPoint seed, index)
-                | (index, seed) <- V.toList (V.indexed seeds)
-                ]
-        fromIndex <- requireSeedIndex indexByPoint from
-        toIndex <- requireSeedIndex indexByPoint to
-        rebuilt <-
-          first OverlayBuildFailed
-            (constrainedDelaunay defaults seeds (V.snoc constraints (fromIndex, toIndex)))
-        first (OverlayBuildFailed . CdtBuildError)
-          (canonicalize (buildTriangulation rebuilt))
-
-requireSeedIndex
-  :: Map ExactPoint Int
-  -> ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) Int
-requireSeedIndex indexByPoint point =
-  case Map.lookup point indexByPoint of
-    Just index -> Right index
-    Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
-
-firstAlternativeDiagonal
-  :: Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Maybe ExactEdgeKey
-firstAlternativeDiagonal triangulation =
-  listToMaybe
-    (mapMaybe (alternativeDiagonal triangulation) (undirectedEdges triangulation))
-
-alternativeDiagonal
-  :: Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> UndirectedEdgeId
-  -> Maybe ExactEdgeKey
-alternativeDiagonal triangulation edge
-  | isConstraintEdge triangulation edge = Nothing
-  | leftFace == outerFace || rightFace == outerFace = Nothing
-  | exactOrient2d c d b == GT && exactOrient2d d c a == GT =
-      Just (canonicalEdgeKey c d)
-  | otherwise = Nothing
- where
-  (forward, backward) = directedPair edge
-  leftFace = incidentFace triangulation forward
-  rightFace = incidentFace triangulation backward
-  a = seedExactPoint (vertexData triangulation (Dcel.origin triangulation forward))
-  b = seedExactPoint (vertexData triangulation (Dcel.origin triangulation backward))
-  c =
-    seedExactPoint
-      (vertexData triangulation (Dcel.origin triangulation (Dcel.previous triangulation forward)))
-  d =
-    seedExactPoint
-      (vertexData triangulation (Dcel.origin triangulation (Dcel.previous triangulation backward)))
-labelResidentFaces
-  :: (Ord leftLabel, Ord rightLabel)
-  => (leftLabel, rightLabel)
-  -> Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId (leftLabel, rightLabel))
-labelResidentFaces outsideLabels triangulation atomicByKey = do
-  labelled <-
-    descendFaceTransitions
-      triangulation
-      atomicByKey
-      (Map.singleton outerFace outsideLabels)
-      (Seq.singleton outerFace)
-  case
-    [ face
-    | face <- innerFaces triangulation
-    , Map.notMember face labelled
-    ] of
-    missing : _ ->
-      Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned missing))
-    [] -> Right (Map.delete outerFace labelled)
-
-seedDirectedEndpoints
-  :: Triangulation mode OverlayVertexSeed directed undirected face
-  -> DirectedEdgeId
-  -> ExactEdgeKey
-seedDirectedEndpoints triangulation edge =
-  ( seedExactPoint (vertexData triangulation (Dcel.origin triangulation edge))
-  , seedExactPoint (vertexData triangulation (Dcel.destination triangulation edge))
-  )
-
-swapTransition :: (label, label) -> (label, label)
-swapTransition (fromLabel, toLabel) = (toLabel, fromLabel)
-
-descendFaceTransitions
-  :: (Eq leftLabel, Eq rightLabel)
-  => Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Map FaceId (leftLabel, rightLabel)
-  -> Seq.Seq FaceId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId (leftLabel, rightLabel))
-descendFaceTransitions triangulation atomicByKey labelled queued =
-  case Seq.viewl queued of
-    Seq.EmptyL -> Right labelled
-    face Seq.:< remaining ->
-      case Map.lookup face labelled of
-        Nothing ->
-          Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned face))
-        Just current -> do
-          (nextLabels, nextQueue) <-
-            foldM
-              (descendFaceTransition triangulation atomicByKey face current)
-              (labelled, remaining)
-              (faceDirectedEdges triangulation face)
-          descendFaceTransitions triangulation atomicByKey nextLabels nextQueue
-
-descendFaceTransition
-  :: (Eq leftLabel, Eq rightLabel)
-  => Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> FaceId
-  -> (leftLabel, rightLabel)
-  -> (Map FaceId (leftLabel, rightLabel), Seq.Seq FaceId)
-  -> DirectedEdgeId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId (leftLabel, rightLabel), Seq.Seq FaceId)
-descendFaceTransition triangulation atomicByKey source current (labelled, queued) directed = do
-  let edge = asUndirected directed
-      destinationFace = incidentFace triangulation (reverseEdge directed)
-  (leftTransition, rightTransition) <-
-    case Map.lookup (residentSeedEdgeKey triangulation edge) atomicByKey of
-      Nothing -> Right (Nothing, Nothing)
-      Just atomic -> orientedTransitions edge directed atomic
-  let
-      expected =
-        ( maybe (fst current) fst leftTransition
-        , maybe (snd current) fst rightTransition
-        )
-      derived =
-        ( maybe (fst current) snd leftTransition
-        , maybe (snd current) snd rightTransition
-        )
-  if current == expected
-    then
-      case Map.lookup destinationFace labelled of
-        Nothing ->
-          Right
-            ( Map.insert destinationFace derived labelled
-            , queued Seq.|> destinationFace
-            )
-        Just existing
-          | existing == derived -> Right (labelled, queued)
-          | otherwise ->
-              Left
-                ( OverlayArrangementInvalid
-                    ( OverlayResidentFaceLabelConflict
-                        destinationFace
-                        edge
-                        existing
-                        derived
-                    )
-                )
-    else
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayTransitionSourceMismatch source edge current expected)
-        )
- where
-  orientedTransitions edge candidateDirection atomic
-    | directedEndpoints == (atomicEdgeFrom atomic, atomicEdgeTo atomic) =
-        Right
-          ( atomicEdgeLeftTransition atomic
-          , atomicEdgeRightTransition atomic
-          )
-    | directedEndpoints == (atomicEdgeTo atomic, atomicEdgeFrom atomic) =
-        Right
-          ( swapTransition <$> atomicEdgeLeftTransition atomic
-          , swapTransition <$> atomicEdgeRightTransition atomic
-          )
-    | otherwise =
-        Left
-          ( OverlayProvenanceIncomplete
-              ( OverlayAtomicConstraintOrientationMismatch
-                  edge
-                  (atomicEdgeFrom atomic)
-                  (atomicEdgeTo atomic)
-              )
-          )
-   where
-    directedEndpoints = seedDirectedEndpoints triangulation candidateDirection
-
-exactFaceDimensions
-  :: Set ExactPoint
-  -> Triangulation 'Constrained OverlayVertexSeed directed undirected face
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Set FaceId, Set FaceId)
-exactFaceDimensions inexactEmbeddingPoints triangulation = do
-  orientations <-
-    traverse
-      ( \face ->
-          (face,)
-            <$> exactFaceOrientation
-              inexactEmbeddingPoints
-              triangulation
-              face
-      )
-      (innerFaces triangulation)
-  case [face | (face, LT) <- orientations] of
-    reversed : _ ->
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayResidentFaceOrientationReversed reversed)
-        )
-    [] ->
-      Right
-        ( Set.fromList [face | (face, GT) <- orientations]
-        , Set.fromList [face | (face, EQ) <- orientations]
-        )
-
-exactFaceOrientation
-  :: Set ExactPoint
-  -> Triangulation mode OverlayVertexSeed directed undirected face
-  -> FaceId
-  -> Either (OverlayError leftLabel rightLabel) Ordering
-exactFaceOrientation inexactEmbeddingPoints triangulation face =
-  case
-    map
-      (seedExactPoint . vertexData triangulation)
-      (faceVertices triangulation face) of
-    points@[firstPoint, secondPoint, thirdPoint]
-      | any (`Set.member` inexactEmbeddingPoints) points ->
-          Right (exactOrient2d firstPoint secondPoint thirdPoint)
-      | otherwise -> Right GT
-    vertices ->
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayResidentFaceArity face (length vertices))
-        )
-
-residentComponentDrafts
-  :: forall leftLabel rightLabel.
-     (Ord leftLabel, Ord rightLabel)
-  => Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map FaceId (leftLabel, rightLabel)
-  -> Set FaceId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      [ComponentDraft leftLabel rightLabel]
-residentComponentDrafts triangulation labels fullDimensionalFaces =
-  traverse
-    convert
-    (faceComponentsBy triangulation labelFullDimensionalFace (const True))
- where
-  exactPointAt :: VertexId -> Either RegionPublicationError ExactPoint
-  exactPointAt vertex =
-    Right (seedExactPoint (vertexData triangulation vertex))
-  labelFullDimensionalFace :: FaceId -> Maybe (leftLabel, rightLabel)
-  labelFullDimensionalFace face
-    | Set.member face fullDimensionalFaces = Map.lookup face labels
-    | otherwise = Nothing
-  convert
-    :: ((leftLabel, rightLabel), FaceComponent)
-    -> Either
-        (OverlayError leftLabel rightLabel)
-        (ComponentDraft leftLabel rightLabel)
-  convert (componentLabels, component) = do
-    boundary <-
-      first
-        (OverlayRegionPublicationFailed . RegionBoundaryObstruction)
-        (componentBoundary triangulation component)
-    polygon <-
-      first OverlayRegionPublicationFailed
-        (polygonComponentFromBoundaryCoordinates exactPointAt boundary)
-    pure
-      ComponentDraft
-        { componentDraftLabels = componentLabels
-        , componentDraftFaces = component
-        , componentDraftPolygon = polygon
-        , componentDraftTouchesOuter = touchesOuter triangulation component
-        }
-
-touchesOuter
-  :: Triangulation mode vertex directed undirected face
-  -> FaceComponent
-  -> Bool
-touchesOuter triangulation component =
-  any
-    ( any ((== outerFace) . incidentFace triangulation . reverseEdge)
-        . faceDirectedEdges triangulation
-    )
-    (faceComponentFaces component)
-
-partitionDrafts
-  :: (Eq leftLabel, Eq rightLabel)
-  => (leftLabel, rightLabel)
-  -> [ComponentDraft leftLabel rightLabel]
-  -> ([ComponentDraft leftLabel rightLabel], [ComponentDraft leftLabel rightLabel])
-partitionDrafts outsidePair =
-  partition
-    (\draftComponent ->
-       componentDraftTouchesOuter draftComponent
-         && componentDraftLabels draftComponent == outsidePair)
-
-sortComponentDrafts
-  :: (Ord leftLabel, Ord rightLabel)
-  => [ComponentDraft leftLabel rightLabel]
-  -> [ComponentDraft leftLabel rightLabel]
-sortComponentDrafts =
-  sortBy
-    (\left right ->
-       compare
-         (componentDraftPolygon left, componentDraftLabels left)
-         (componentDraftPolygon right, componentDraftLabels right))
-
-numberBoundedComponents
-  :: (Ord leftLabel, Ord rightLabel)
-  => [ComponentDraft leftLabel rightLabel]
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      [(OverlayCellId, ComponentDraft leftLabel rightLabel)]
-numberBoundedComponents drafts =
-  case
-    [ componentDraftPolygon left
-    | (left, right) <- consecutivePairs ordered
-    , componentSignature left == componentSignature right
-    ] of
-    duplicate : _ ->
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayDuplicateCellSignature duplicate)
-        )
-    [] ->
-      Right
-        ( zipWith
-            (\index component -> (OverlayCellId index, component))
-            [1 ..]
-            ordered
-        )
- where
-  ordered = sortComponentDrafts drafts
-  componentSignature
-    :: ComponentDraft leftLabel' rightLabel'
-    -> (PolygonComponent, (leftLabel', rightLabel'))
-  componentSignature component =
-    (componentDraftPolygon component, componentDraftLabels component)
-
--- | A binary64 triangle whose authoritative vertices are collinear is not an
--- exact two-cell. It may remain in the resident triangulation only when its
--- representation diagonals descend uniquely to one full-dimensional exact
--- cell; this prevents the artifact from gluing exact components through a
--- zero-area wedge.
-descendCollapsedFaces
-  :: forall leftLabel rightLabel.
-     Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Map FaceId OverlayCellId
-  -> Set FaceId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId OverlayCellId)
-descendCollapsedFaces triangulation atomicByKey fullCellIds collapsedFaces =
-  Map.fromList . concat <$> traverse resolveComponent collapsedComponents
- where
-  exactCellIds = Map.insert outerFace (OverlayCellId 0) fullCellIds
-  collapsedComponents =
-    fmap snd
-      ( faceComponentsBy
-          triangulation
-          labelCollapsedFace
-          isRepresentationDiagonal
-      )
-
-  labelCollapsedFace face
-    | Set.member face collapsedFaces = Just ()
-    | otherwise = Nothing
-
-  isRepresentationDiagonal edge =
-    Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey
-
-  resolveComponent component =
-    case faceComponentFaces component of
-      [] ->
-        Left
-          ( OverlayArrangementInvalid
-              OverlayFaceComponentEmpty
-          )
-      componentFaces@(firstFace : remainingFaces) -> do
-        let componentWitness = firstFace NonEmpty.:| remainingFaces
-            adjacentCellIds =
-              Set.fromList
-                [ cellId
-                | face <- componentFaces
-                , directed <- faceDirectedEdges triangulation face
-                , isRepresentationDiagonal (asUndirected directed)
-                , Just cellId <-
-                    [Map.lookup (incidentFace triangulation (reverseEdge directed)) exactCellIds]
-                ]
-        cellId <-
-          case Set.toAscList adjacentCellIds of
-            [singleCellId] -> Right singleCellId
-            [] ->
-              Left
-                ( OverlayArrangementInvalid
-                    (OverlayCollapsedFacesUnowned componentWitness)
-                )
-            firstCellId : remainingCellIds ->
-              Left
-                ( OverlayArrangementInvalid
-                    ( OverlayCollapsedFacesAmbiguous
-                        componentWitness
-                        (firstCellId NonEmpty.:| remainingCellIds)
-                    )
-                )
-        pure (fmap (,cellId) componentFaces)
-
--- | Trace the finite boundary cycles of the unbounded exact cell from atomic
--- edges only. Resident hull edges and Delaunay diagonals are absent by
--- construction; the DCEL contributes only the already-proved incident-cell
--- gluing needed to orient each atomic edge with cell zero on its left.
-unboundedCellBoundaryLoops
-  :: Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Map FaceId OverlayCellId
-  -> Either (OverlayError leftLabel rightLabel) [ExactLoop]
-unboundedCellBoundaryLoops triangulation atomicByKey cellIdByFace = do
-  orientedEdges <-
-    Set.fromList . concat
-      <$> traverse orientAtomicEdge (undirectedEdges triangulation)
-  traceBoundaryCycles orientedEdges
- where
-  orientAtomicEdge edge
-    | Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey = Right []
-    | otherwise = do
-        let (forward, backward) = directedPair edge
-        forwardCell <- faceCellId (incidentFace triangulation forward)
-        backwardCell <- faceCellId (incidentFace triangulation backward)
-        pure
-          ( case (forwardCell == OverlayCellId 0, backwardCell == OverlayCellId 0) of
-              (True, False) -> [seedDirectedEndpoints triangulation forward]
-              (False, True) -> [seedDirectedEndpoints triangulation backward]
-              _ -> []
-          )
-  faceCellId face
-    | face == outerFace = Right (OverlayCellId 0)
-    | otherwise =
-        case Map.lookup face cellIdByFace of
-          Just cellId -> Right cellId
-          Nothing -> Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned face))
-
-traceBoundaryCycles
-  :: Set ExactEdgeKey
-  -> Either (OverlayError leftLabel rightLabel) [ExactLoop]
-traceBoundaryCycles orientedEdges = do
-  pointCycles <-
-    traceOrientedBoundaryCircuits
-      fst
-      snd
-      (\seed _ ->
-         OverlayArrangementInvalid
-           (uncurry OverlayCellCycleDidNotClose seed))
-      outgoing
-      orientedEdges
-  sort
-    <$> traverse
-      ( first
-          (OverlayRegionPublicationFailed . RegionValidationObstruction)
-          . exactLoop
-      )
-      pointCycles
- where
-  outgoing =
-    Map.mapWithKey orderAroundOrigin
-      ( Set.foldl'
-          (\byOrigin edge@(from, _) -> Map.insertWith (<>) from [edge] byOrigin)
-          Map.empty
-          orientedEdges
-      )
-  orderAroundOrigin :: ExactPoint -> [ExactEdgeKey] -> [ExactEdgeKey]
-  orderAroundOrigin origin =
-    sortBy (\(_, left) (_, right) -> compareAround origin left right)
-
-attachFaceCells
-  :: forall leftLabel rightLabel.
-     Set FaceId
-  -> Map FaceId OverlayCellId
-  -> Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Triangulation 'Constrained OverlayVertexSeed () () OverlayFace)
-attachFaceCells collapsedFaces cellIdByFace triangulation = do
-  payloads <- V.fromList <$> traverse facePayload (allFaces triangulation)
-  let outerPayload = OverlayCellFace (OverlayCellId 0)
-      defaults = triElementDefaults triangulation
-  pure
-    triangulation
-      { triFaceData = boxedFromVector (Just outerPayload) payloads
-      , triElementDefaults = defaults{defaultFaceData = outerPayload}
-      }
- where
-  facePayload
-    :: FaceId
-    -> Either (OverlayError leftLabel rightLabel) OverlayFace
-  facePayload face
-    | face == outerFace = Right (OverlayCellFace (OverlayCellId 0))
-    | otherwise =
-        case Map.lookup face cellIdByFace of
-          Just cellId
-            | Set.member face collapsedFaces ->
-                Right (OverlayCollapsedFace cellId)
-            | otherwise -> Right (OverlayCellFace cellId)
-          Nothing ->
-            Left
-              ( OverlayProvenanceIncomplete
-                  (OverlayResidentFaceUnassigned face)
-              )
-
-attachOverlayVertices
-  :: Triangulation 'Constrained OverlayVertexSeed () () face
-  -> Triangulation 'Constrained OverlayVertex () () face
-attachOverlayVertices =
-  Dcel.mapVertices
-    (\seed -> OverlayVertex (seedExactPoint seed) (seedOrigin seed))
-
-cellPayload
-  :: V.Vector (OverlayCell leftLabel rightLabel)
-  -> OverlayCellId
-  -> Either OverlayCellWitness (OverlayCell leftLabel rightLabel)
-cellPayload cells cellId@(OverlayCellId index) =
-  case cells V.!? index of
-    Just cell -> Right cell
-    Nothing -> Left (OverlayCellPayloadMissing cellId)
-
-faceLabels
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Either OverlayCellWitness (leftLabel, rightLabel)
-faceLabels result face = do
-  cell <-
-    cellPayload
-      (overlayResultCells result)
-      (overlayFaceCellId (faceData (overlayResultTriangulation result) face))
-  pure (overlayCellLeft cell, overlayCellRight cell)
-
--- | Whether a resident triangle has nonzero area under the authoritative
--- exact coordinate section. Binary64-only wedges are representation charts,
--- never exact two-cells.
-faceCarriesExactArea
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Bool
-faceCarriesExactArea result face =
-  case faceData (overlayResultTriangulation result) face of
-    OverlayCellFace {} -> True
-    OverlayCollapsedFace {} -> False
-
-regionFaceLabels
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Either RegionPublicationError (leftLabel, rightLabel)
-regionFaceLabels result face =
-  case faceLabels result face of
-    Right labels -> Right labels
-    Left _ -> Left (RegionFaceLabelMissing face)
-
-vertexSupport
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> VertexId
-  -> Either OverlayCellWitness (OverlayCellSupport leftLabel rightLabel)
-vertexSupport result vertex = do
-  labels <-
-    traverse
-      (faceLabels result . incidentFace triangulation)
-      (vertexOutgoingEdges triangulation vertex)
-  case supportFromPairs labels of
-    Just support -> Right support
-    Nothing -> Left (OverlayVertexSupportMissing vertex)
- where
-  triangulation = overlayResultTriangulation result
-
-edgeSupport
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> UndirectedEdgeId
-  -> Either OverlayCellWitness (OverlayCellSupport leftLabel rightLabel)
-edgeSupport result edge = do
-  pairs <- traverse (faceLabels result . incidentFace triangulation) [forward, backward]
-  case supportFromPairs pairs of
-    Just support -> Right support
-    Nothing -> Left (OverlayEdgeSupportMissing edge)
- where
-  triangulation = overlayResultTriangulation result
-  (forward, backward) = directedPair edge
-
-supportFromPairs
-  :: (Ord leftLabel, Ord rightLabel)
-  => [(leftLabel, rightLabel)]
-  -> Maybe (OverlayCellSupport leftLabel rightLabel)
-supportFromPairs pairs = do
-  let (leftLabels, rightLabels) =
-        List.foldl'
-          (\(left, right) (leftLabel, rightLabel) ->
-             (Set.insert leftLabel left, Set.insert rightLabel right))
-          (Set.empty, Set.empty)
-          pairs
-  leftSupport <- nonEmptySupport leftLabels
-  rightSupport <- nonEmptySupport rightLabels
-  pure (OverlayCellSupport leftSupport rightSupport)
-
-nonEmptySupport :: Set label -> Maybe (OverlaySupport label)
-nonEmptySupport labels = OverlaySupport <$> NonEmpty.nonEmpty (Set.toAscList labels)
-
-validateAtomicConstraints
-  :: Triangulation
-      'Constrained
-      OverlayVertexSeed
-      ()
-      ()
-      ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Either (OverlayError leftLabel rightLabel) ()
-validateAtomicConstraints triangulation atomicByKey = do
-  traverse_ requireAtomic (Map.keys atomicByKey)
-  traverse_ requireExpectedConstraint (undirectedEdges triangulation)
- where
-  residentByKey =
-    Map.fromList
-      [ (residentSeedEdgeKey triangulation edge, edge)
-      | edge <- undirectedEdges triangulation
-      ]
-  requireAtomic key@(from, to) =
-    case Map.lookup key residentByKey of
-      Nothing -> Left (OverlayProvenanceIncomplete (OverlayAtomicConstraintMissing from to))
-      Just edge
-        | isConstraintEdge triangulation edge -> Right ()
-        | otherwise -> Left (OverlayProvenanceIncomplete (OverlayBoundaryEdgeNotConstrained edge))
-  requireExpectedConstraint edge
-    | isConstraintEdge triangulation edge
-        && Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey =
-        Left (OverlayProvenanceIncomplete (OverlayUnexpectedConstraint edge))
-    | otherwise = Right ()
-
-residentSeedEdgeKey
-  :: Triangulation mode OverlayVertexSeed directed undirected face
-  -> UndirectedEdgeId
-  -> ExactEdgeKey
-residentSeedEdgeKey = residentEdgeKeyBy seedExactPoint
-
-residentEdgeKey
-  :: Triangulation mode OverlayVertex directed undirected face
-  -> UndirectedEdgeId
-  -> ExactEdgeKey
-residentEdgeKey = residentEdgeKeyBy overlayExactPoint
-
-residentEdgeKeyBy
-  :: (vertex -> ExactPoint)
-  -> Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> ExactEdgeKey
-residentEdgeKeyBy exactPointAt triangulation edge =
-  let (from, to) = undirectedEndpoints triangulation edge
-   in canonicalEdgeKey
-        (exactPointAt (vertexData triangulation from))
-        (exactPointAt (vertexData triangulation to))
diff --git a/src-planar/Moonlight/Triangulation/Internal/Overlay/Types.hs b/src-planar/Moonlight/Triangulation/Internal/Overlay/Types.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Internal/Overlay/Types.hs
+++ /dev/null
@@ -1,274 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE KindSignatures #-}
-
--- | Closed vocabulary and opaque carrier representation for exact labelled
--- overlay. Construction lives in the public build-tier module; this module
--- exists so every invariant-bearing payload shares one owner.
-module Moonlight.Triangulation.Internal.Overlay.Types
-  ( BoundaryLoopRef (..)
-  , OverlayOperand (..)
-  , BoundaryRef (..)
-  , BoundaryVertexRef
-  , BoundaryEdgeRef
-  , OverlayVertexOrigin (..)
-  , OverlayEdgeOrigin (..)
-  , OverlaySupport (..)
-  , overlaySupportLabels
-  , OverlayCellSupport (..)
-  , OverlayCellId (..)
-  , OverlayCellGeometry (..)
-  , OverlayCell (..)
-  , OverlayFace (..)
-  , OverlayVertex (..)
-  , OverlayEdge (..)
-  , OverlayReceipt (..)
-  , OverlayArrangementObstruction (..)
-  , OverlayCellWitness (..)
-  , OverlayError (..)
-  , OverlayResult (..)
-  , OverlaySelectionKind (..)
-  , OverlaySelectionError (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List.NonEmpty (NonEmpty)
-import Data.Vector (Vector)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.CellSet (CellSelectionError)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  )
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( FaceId
-  , UndirectedEdgeId
-  , VertexId
-  )
-import Moonlight.Triangulation.Internal.Cdt.Types (CdtError)
-import Moonlight.Triangulation.Internal.ExactRational (ExactArithmeticError)
-import Moonlight.Triangulation.Internal.ExactSegmentEvents (ExactSegmentEventObstruction)
-import Moonlight.Triangulation.Internal.Overlay.Embedding (OverlayEmbeddingObstruction)
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop
-  , PolygonComponent
-  , RegionPublicationError
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Constrained)
-  )
-
--- | Which cycle inside a polygon component supplied a source reference.
-data BoundaryLoopRef
-  = BoundaryOuterLoop
-  | BoundaryHoleLoop !Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayOperand
-  = LeftOverlayOperand
-  | RightOverlayOperand
-
--- | Phantom source cell kind; both provenance axes share one physical payload.
-data BoundaryFeature = BoundaryVertexFeature | BoundaryEdgeFeature
-
-data BoundaryRef (feature :: BoundaryFeature) (operand :: OverlayOperand) = BoundaryRef
-  { boundaryRefComponent :: !Int
-  , boundaryRefLoop :: !BoundaryLoopRef
-  , boundaryRefLocalIndex :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-type BoundaryVertexRef operand = BoundaryRef 'BoundaryVertexFeature operand
-type BoundaryEdgeRef operand = BoundaryRef 'BoundaryEdgeFeature operand
-
--- | Complete typed source closure of one arrangement vertex.
-data OverlayVertexOrigin = OverlayVertexOrigin
-  { overlayOriginLeftVertices :: ![BoundaryVertexRef 'LeftOverlayOperand]
-  , overlayOriginRightVertices :: ![BoundaryVertexRef 'RightOverlayOperand]
-  , overlayOriginLeftEdges :: ![BoundaryEdgeRef 'LeftOverlayOperand]
-  , overlayOriginRightEdges :: ![BoundaryEdgeRef 'RightOverlayOperand]
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Nonempty typed source-edge provenance of one exact atomic interval.
-data OverlayEdgeOrigin = OverlayEdgeOrigin
-  { overlayEdgeLeftSources :: ![BoundaryEdgeRef 'LeftOverlayOperand]
-  , overlayEdgeRightSources :: ![BoundaryEdgeRef 'RightOverlayOperand]
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Sorted nonempty labels whose closures contain one relatively open cell.
-newtype OverlaySupport label = OverlaySupport (NonEmpty label)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-overlaySupportLabels :: OverlaySupport label -> NonEmpty label
-overlaySupportLabels (OverlaySupport labels) = labels
-
-data OverlayCellSupport leftLabel rightLabel = OverlayCellSupport
-  { overlaySupportLeft :: !(OverlaySupport leftLabel)
-  , overlaySupportRight :: !(OverlaySupport rightLabel)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-newtype OverlayCellId = OverlayCellId Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayCellGeometry
-  = BoundedOverlayCell !PolygonComponent
-  | UnboundedOverlayCell ![ExactLoop]
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayCell leftLabel rightLabel = OverlayCell
-  { overlayCellLeft :: !leftLabel
-  , overlayCellRight :: !rightLabel
-  , overlayCellGeometry :: !OverlayCellGeometry
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A resident triangle either realizes an exact two-cell or collapses under
--- the authoritative exact coordinates. A collapsed triangle descends to the
--- unique adjacent exact cell through representation diagonals, but exact
--- region operations must not mistake its binary64 area for exact area.
-data OverlayFace
-  = OverlayCellFace
-      { overlayFaceCellId :: !OverlayCellId
-      }
-  | OverlayCollapsedFace
-      { overlayFaceCellId :: !OverlayCellId
-      }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact geometry and provenance are intrinsic to a vertex. Label support is
--- the finite union of incident cell descriptors and is therefore a view.
-data OverlayVertex = OverlayVertex
-  { overlayExactPoint :: !ExactPoint
-  , overlayVertexOrigin :: !OverlayVertexOrigin
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Boundary provenance is intrinsic; label support is derived from the two
--- incident face-cell references.
-data OverlayEdge
-  = OverlayBoundary !OverlayEdgeOrigin
-  | OverlayDiagonal
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayReceipt = OverlayReceipt
-  { overlayInputSegments :: !Int
-  , overlayRelationEvents :: !Int
-  , overlayExactCrossings :: !Int
-  , overlayOverlapIntervals :: !Int
-  , overlayAtomicEdges :: !Int
-  , overlayOutputVertices :: !Int
-  , overlayArrangementCells :: !Int
-  , overlayResidentFaces :: !Int
-  , overlayEmbeddingCandidates :: !Int
-  , overlayTotalRelationChecks :: !Int
-  , overlaySweepMaximumHeight :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayArrangementObstruction leftLabel rightLabel
-  = OverlayLeftSourceSideConflict
-      !OverlayEdgeOrigin
-      !(NonEmpty leftLabel)
-  | OverlayRightSourceSideConflict
-      !OverlayEdgeOrigin
-      !(NonEmpty rightLabel)
-  | OverlayRotationDegenerate !ExactPoint
-  | OverlayFaceComponentEmpty
-  | OverlayCellCycleDidNotClose !ExactPoint !ExactPoint
-  | OverlayTransitionSourceMismatch
-      !FaceId
-      !UndirectedEdgeId
-      !(leftLabel, rightLabel)
-      !(leftLabel, rightLabel)
-  | OverlayResidentFaceLabelConflict
-      !FaceId
-      !UndirectedEdgeId
-      !(leftLabel, rightLabel)
-      !(leftLabel, rightLabel)
-  | OverlayResidentFaceArity !FaceId !Int
-  | OverlayResidentFaceOrientationReversed !FaceId
-  | OverlayCollapsedFacesUnowned !(NonEmpty FaceId)
-  | OverlayCollapsedFacesAmbiguous
-      !(NonEmpty FaceId)
-      !(NonEmpty OverlayCellId)
-  | OverlayDuplicateCellSignature !PolygonComponent
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayCellWitness
-  = OverlayAtomicConstraintMissing !ExactPoint !ExactPoint
-  | OverlayAtomicConstraintOrientationMismatch
-      !UndirectedEdgeId
-      !ExactPoint
-      !ExactPoint
-  | OverlayUnexpectedConstraint !UndirectedEdgeId
-  | OverlayBoundaryEdgeNotConstrained !UndirectedEdgeId
-  | OverlayResidentFaceUnassigned !FaceId
-  | OverlayCellPayloadMissing !OverlayCellId
-  | OverlayVertexSupportMissing !VertexId
-  | OverlayEdgeSupportMissing !UndirectedEdgeId
-  | OverlayEmbeddedVertexCountMismatch !Int !Int
-  | OverlayExactVertexMissing !ExactPoint
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayError leftLabel rightLabel
-  = OverlayExactArithmetic !ExactArithmeticError
-  | OverlaySegmentEventsInvalid !ExactSegmentEventObstruction
-  | OverlayArrangementInvalid !(OverlayArrangementObstruction leftLabel rightLabel)
-  | OverlayEmbeddingRefused !(NonEmpty OverlayEmbeddingObstruction)
-  | OverlayBuildFailed !CdtError
-  | OverlayRegionPublicationFailed !RegionPublicationError
-  | OverlayProvenanceIncomplete !OverlayCellWitness
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The singular exact subdivision carrier. Its embedded DCEL is a derived
--- binary64 realization; exact coordinates remain in the vertex payload plane.
-data OverlayResult leftLabel rightLabel = OverlayResult
-  { overlayResultTriangulation
-      :: !( Triangulation
-              'Constrained
-              OverlayVertex
-              ()
-              OverlayEdge
-              OverlayFace
-          )
-  , overlayResultCells :: !(Vector (OverlayCell leftLabel rightLabel))
-  , overlayResultOutsideLabels :: !(leftLabel, rightLabel)
-  , overlayResultReceipt :: !OverlayReceipt
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-data OverlaySelectionKind
-  = ClosedUnionSelection
-  | ClosedIntersectionSelection
-  | RegularizedDifferenceSelection
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlaySelectionError
-  = OverlaySelectionContainsUnboundedCell !OverlaySelectionKind
-  | OverlaySelectionProvenance !OverlayCellWitness
-  | OverlaySelectionInvalid !CellSelectionError
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
diff --git a/src-planar/Moonlight/Triangulation/Minkowski.hs b/src-planar/Moonlight/Triangulation/Minkowski.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Minkowski.hs
+++ /dev/null
@@ -1,595 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Exact polygonal Minkowski addition and regularized two-dimensional
--- morphology. Convex convolution is direct; nonconvex construction descends
--- through the existing exact overlay, resident CDT, and grouped publication
--- owners. Lower-dimensional erosion residuals cannot inhabit 'PlanarRegion'
--- and therefore publish as empty rather than being forged as polygons.
-module Moonlight.Triangulation.Minkowski
-  ( ConvexPolygon
-  , convexPolygon
-  , convexPolygonPoints
-  , StructuringElement
-  , structuringElement
-  , MinkowskiOperation (..)
-  , MinkowskiError (..)
-  , MinkowskiReceipt (..)
-  , convexMinkowskiSum
-  , minkowskiSum
-  , erodeBy
-  , openWith
-  , closeWith
-  , polygonOffset
-  , polygonInset
-  ) where
-
-import Control.Applicative ((<|>))
-import Control.Monad (filterM)
-import Data.Bifunctor (first)
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.List as List
-import qualified Data.Map.Strict as Map
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Maybe (fromMaybe)
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Dcel
-  ( faceData
-  , faceVertices
-  , vertexData
-  )
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , exactPointCoordinates
-  )
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( FaceId
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRationalDenominator
-  , exactRationalNumerator
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (cyclePairs)
-import Moonlight.Triangulation.Internal.Dyadic (integerBitLength)
-import Moonlight.Triangulation.Internal.Minkowski.Convex
-  ( addExactPoints
-  , admittedConvexLoop
-  , convexHullPolygon
-  , convexMinkowskiPolygon
-  , convexMinkowskiSum
-  , convexPolygon
-  , convexPolygonCentroid
-  , convexPolygonPoints
-  , convexPolygonRegion
-  , erodeConvexBy
-  , reflectConvexPolygon
-  , structuringElement
-  , structuringElementPolygon
-  )
-import Moonlight.Triangulation.Internal.Minkowski.Types
-import Moonlight.Triangulation.Internal.Overlay.Resident
-  ( faceCarriesExactArea
-  , faceLabels
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-  ( OverlayCell (..)
-  , OverlayCellGeometry (..)
-  , OverlayCellId (..)
-  , OverlayFace (..)
-  , OverlayResult (..)
-  , OverlayVertex (..)
-  )
-import Moonlight.Triangulation.Overlay
-  ( OverlayReceipt (..)
-  , overlayLayers
-  , overlayReceipt
-  , overlaySelectedRegion
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarLayer
-  , PlanarRegion
-  , PolygonComponent
-  , RegionPointLocation (..)
-  , emptyPlanarRegion
-  , exactLoopPoints
-  , planarLayerRegions
-  , planarRegionComponents
-  , polygonHoleLoops
-  , polygonOuterLoop
-  , regionPointLocation
-  )
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( labelledPlanarLayerFromExactCoordinates
-  , planarLayerFromAdmittedComponents
-  )
-
-data MorphologyMetrics = MorphologyMetrics
-  { metricOverlayPasses :: !Int
-  , metricExactCrossings :: !Int
-  , metricOutputCells :: !(Maybe Int)
-  }
-
-emptyMetrics :: MorphologyMetrics
-emptyMetrics = MorphologyMetrics 0 0 Nothing
-
-appendMetrics :: MorphologyMetrics -> MorphologyMetrics -> MorphologyMetrics
-appendMetrics left right =
-  MorphologyMetrics
-    { metricOverlayPasses = metricOverlayPasses left + metricOverlayPasses right
-    , metricExactCrossings = metricExactCrossings left + metricExactCrossings right
-    , metricOutputCells = metricOutputCells right <|> metricOutputCells left
-    }
-
-minkowskiSum
-  :: PlanarRegion
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-minkowskiSum left right = do
-  (leftPieces, leftMetrics) <- decomposeRegion left
-  (rightPieces, rightMetrics) <- decomposeRegion right
-  let generated =
-        [ convexMinkowskiPolygon leftPiece rightPiece
-        | leftPiece <- leftPieces
-        , rightPiece <- rightPieces
-        ]
-      generatedRegions = map convexPolygonRegion generated
-      convolutionEdges = sum (map (NonEmpty.length . convexPolygonPoints) generated)
-  (result, unionMetrics) <- unionRegions generatedRegions
-  let metrics = leftMetrics `appendMetrics` rightMetrics `appendMetrics` unionMetrics
-  pure
-    ( result
-    , MinkowskiReceipt
-        { minkowskiOperation = MinkowskiAddition
-        , minkowskiInputComponents =
-            length (planarRegionComponents left)
-              + length (planarRegionComponents right)
-        , minkowskiConvexPieces = length leftPieces + length rightPieces
-        , minkowskiGeneratedPieces = length generated
-        , minkowskiGeneratedConvolutionEdges = convolutionEdges
-        , minkowskiOverlayPasses = metricOverlayPasses metrics
-        , minkowskiExactCrossings = metricExactCrossings metrics
-        , minkowskiOutputCells = fromMaybe 0 (metricOutputCells metrics)
-        , minkowskiExactCoordinateBitGrowth =
-            coordinateBitGrowth [left, right] result
-        }
-    )
-
--- | Erode a polygonal region by an origin-anchored convex kernel and publish
--- the regularized full-dimensional result. A residual consisting only of
--- points or segments is represented by 'emptyPlanarRegion'.
-erodeBy
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-erodeBy element source =
-  case singleConvexRegion source of
-    Just sourcePolygon -> convexErosion element source sourcePolygon
-    Nothing -> generalErosion element source
-
-openWith
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-openWith element source = do
-  (eroded, erosionReceipt) <- erodeBy element source
-  (opened, additionReceipt) <- polygonOffset element eroded
-  pure
-    ( opened
-    , composeReceipts
-        MinkowskiOpening
-        (length (planarRegionComponents source))
-        erosionReceipt
-        additionReceipt
-    )
-
-closeWith
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-closeWith element source = do
-  (expanded, additionReceipt) <- polygonOffset element source
-  (closed, erosionReceipt) <- erodeBy element expanded
-  pure
-    ( closed
-    , composeReceipts
-        MinkowskiClosing
-        (length (planarRegionComponents source))
-        additionReceipt
-        erosionReceipt
-    )
-
-polygonOffset
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-polygonOffset element source =
-  minkowskiSum source (convexPolygonRegion (structuringElementPolygon element))
-
-polygonInset
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-polygonInset = erodeBy
-
-decomposeRegion
-  :: PlanarRegion
-  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
-decomposeRegion region =
-  case traverse convexComponent (planarRegionComponents region) of
-    Just convexPieces -> Right (convexPieces, emptyMetrics)
-    Nothing -> triangulatedPieces region
-
-convexComponent :: PolygonComponent -> Maybe ConvexPolygon
-convexComponent component =
-  case polygonHoleLoops component of
-    [] -> admittedConvexLoop (polygonOuterLoop component)
-    _ -> Nothing
-
-singleConvexRegion :: PlanarRegion -> Maybe ConvexPolygon
-singleConvexRegion region =
-  case planarRegionComponents region of
-    [component] -> convexComponent component
-    _ -> Nothing
-
-triangulatedPieces
-  :: PlanarRegion
-  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
-triangulatedPieces region = do
-  let sourceLayer = morphologyLayer region
-  result <- first MinkowskiOverlayFailed (overlayLayers sourceLayer emptyMorphologyLayer)
-  selectedFaces <-
-    filterM
-      (\face ->
-         if faceCarriesExactArea result face
-           then
-             fmap fst
-               ( first MinkowskiOverlayCellWitness
-                   (faceLabels result face)
-               )
-           else Right False)
-      (innerFaces (overlayResultTriangulation result))
-  pieces <- traverse (faceConvexPolygon result) selectedFaces
-  pure (pieces, metricsFromOverlay result (length pieces))
-
-faceConvexPolygon
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Either MinkowskiError ConvexPolygon
-faceConvexPolygon result face =
-  case
-    map
-      (overlayExactPoint . vertexData triangulation)
-      (faceVertices triangulation face) of
-    [firstPoint, secondPoint, thirdPoint] ->
-      convexHullPolygon (firstPoint :| [secondPoint, thirdPoint])
-    vertices -> Left (MinkowskiFaceArity face (length vertices))
- where
-  triangulation = overlayResultTriangulation result
-
-unionRegions
-  :: [PlanarRegion]
-  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
-unionRegions [] = Right (emptyPlanarRegion, emptyMetrics)
-unionRegions [region] =
-  Right
-    ( region
-    , emptyMetrics{metricOutputCells = Just (length (planarRegionComponents region))}
-    )
-unionRegions regions = do
-  let (leftRegions, rightRegions) = splitAt (length regions `div` 2) regions
-  left <- unionRegions leftRegions
-  right <- unionRegions rightRegions
-  glueRegionUnion left right
-
-glueRegionUnion
-  :: (PlanarRegion, MorphologyMetrics)
-  -> (PlanarRegion, MorphologyMetrics)
-  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
-glueRegionUnion (left, leftMetrics) (right, rightMetrics)
-  | null (planarRegionComponents left) = Right (right, leftMetrics `appendMetrics` rightMetrics)
-  | null (planarRegionComponents right) = Right (left, leftMetrics `appendMetrics` rightMetrics)
-  | otherwise = do
-      result <-
-        first MinkowskiOverlayFailed
-          (overlayLayers (morphologyLayer left) (morphologyLayer right))
-      published <-
-        first MinkowskiPublicationFailed
-          ( overlaySelectedRegion
-              (uncurry (||))
-              result
-          )
-      let selectedCells =
-            V.foldl'
-              (\count cell ->
-                 case overlayCellGeometry cell of
-                   BoundedOverlayCell _
-                     | overlayCellLeft cell || overlayCellRight cell ->
-                         count + 1
-                   _ -> count)
-              0
-              (overlayResultCells result)
-      pure
-        ( published
-        , leftMetrics
-            `appendMetrics` rightMetrics
-            `appendMetrics` metricsFromOverlay result selectedCells
-        )
-
-convexErosion
-  :: StructuringElement
-  -> PlanarRegion
-  -> ConvexPolygon
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-convexErosion element source sourcePolygon = do
-  eroded <- erodeConvexBy sourcePolygon (structuringElementPolygon element)
-  let result = maybe emptyPlanarRegion convexPolygonRegion eroded
-      outputCells = maybe 0 (const 1) eroded
-      generatedEdges = maybe 0 (NonEmpty.length . convexPolygonPoints) eroded
-  pure
-    ( result
-    , MinkowskiReceipt
-        { minkowskiOperation = MinkowskiErosion
-        , minkowskiInputComponents = 1
-        , minkowskiConvexPieces = 2
-        , minkowskiGeneratedPieces = outputCells
-        , minkowskiGeneratedConvolutionEdges = generatedEdges
-        , minkowskiOverlayPasses = 0
-        , minkowskiExactCrossings = 0
-        , minkowskiOutputCells = outputCells
-        , minkowskiExactCoordinateBitGrowth =
-            coordinateBitGrowth
-              [ source
-              , convexPolygonRegion (structuringElementPolygon element)
-              ]
-              result
-        }
-    )
-
-generalErosion
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-generalErosion element source
-  | null sourceEdges = Right (emptyPlanarRegion, emptyErosionReceipt)
-  | otherwise = do
-      sweptPolygons <- traverse (sweepBoundaryEdge reflectedKernel) sourceEdges
-      let sweptRegions = map convexPolygonRegion sweptPolygons
-          generatedEdges =
-            sum (map (NonEmpty.length . convexPolygonPoints) sweptPolygons)
-      (contactRegion, unionMetrics) <- unionRegions sweptRegions
-      candidateOverlay <-
-        first MinkowskiOverlayFailed
-          (overlayLayers (morphologyLayer contactRegion) emptyMorphologyLayer)
-      kernelWitness <- convexPolygonCentroid kernel
-      let representativeFaceByCell = representativeFaces candidateOverlay
-      selectedCellIds <-
-        Set.fromList
-          <$> filterM
-            ( classifyCandidateCell
-                source
-                kernelWitness
-                representativeFaceByCell
-                candidateOverlay
-            )
-            (boundedOutsideCellIds candidateOverlay)
-      published <- publishCellSelection selectedCellIds candidateOverlay
-      let candidateMetrics =
-            metricsFromOverlay candidateOverlay (Set.size selectedCellIds)
-          metrics = unionMetrics `appendMetrics` candidateMetrics
-      pure
-        ( published
-        , MinkowskiReceipt
-            { minkowskiOperation = MinkowskiErosion
-            , minkowskiInputComponents = length (planarRegionComponents source)
-            , minkowskiConvexPieces = 1
-            , minkowskiGeneratedPieces = length sweptPolygons
-            , minkowskiGeneratedConvolutionEdges = generatedEdges
-            , minkowskiOverlayPasses = metricOverlayPasses metrics
-            , minkowskiExactCrossings = metricExactCrossings metrics
-            , minkowskiOutputCells = Set.size selectedCellIds
-            , minkowskiExactCoordinateBitGrowth =
-                coordinateBitGrowth
-                  [source, convexPolygonRegion kernel]
-                  published
-            }
-        )
- where
-  kernel = structuringElementPolygon element
-  reflectedKernel = reflectConvexPolygon kernel
-  sourceEdges = regionBoundaryEdges source
-  emptyErosionReceipt =
-    MinkowskiReceipt
-      { minkowskiOperation = MinkowskiErosion
-      , minkowskiInputComponents = 0
-      , minkowskiConvexPieces = 1
-      , minkowskiGeneratedPieces = 0
-      , minkowskiGeneratedConvolutionEdges = 0
-      , minkowskiOverlayPasses = 0
-      , minkowskiExactCrossings = 0
-      , minkowskiOutputCells = 0
-      , minkowskiExactCoordinateBitGrowth = 0
-      }
-
-sweepBoundaryEdge
-  :: ConvexPolygon
-  -> (ExactPoint, ExactPoint)
-  -> Either MinkowskiError ConvexPolygon
-sweepBoundaryEdge reflectedKernel (from, to) =
-  case convexPolygonPoints reflectedKernel of
-    firstKernelPoint :| remainingKernelPoints ->
-      convexHullPolygon
-        ( addExactPoints from firstKernelPoint
-            :| ( map (addExactPoints from) remainingKernelPoints
-                   <> map (addExactPoints to) kernelPoints
-               )
-        )
- where
-  kernelPoints = NonEmpty.toList (convexPolygonPoints reflectedKernel)
-
-boundedOutsideCellIds
-  :: OverlayResult Bool Bool
-  -> [OverlayCellId]
-boundedOutsideCellIds result =
-  V.ifoldr
-    (\index cell selected ->
-       case overlayCellGeometry cell of
-         BoundedOverlayCell _
-           | not (overlayCellLeft cell)
-               && not (overlayCellRight cell) ->
-               OverlayCellId index : selected
-         _ -> selected)
-    []
-    (overlayResultCells result)
-
-classifyCandidateCell
-  :: PlanarRegion
-  -> ExactPoint
-  -> IntMap.IntMap FaceId
-  -> OverlayResult Bool Bool
-  -> OverlayCellId
-  -> Either MinkowskiError Bool
-classifyCandidateCell source kernelWitness representativeFaceByCell result cellId = do
-  face <-
-    maybe
-      (Left (MinkowskiCandidateCellMissing cellId))
-      Right
-      (IntMap.lookup (overlayCellIndex cellId) representativeFaceByCell)
-  candidate <- convexPolygonCentroid =<< faceConvexPolygon result face
-  let inclusionWitness = addExactPoints candidate kernelWitness
-  case regionPointLocation source inclusionWitness of
-    RegionInterior -> Right True
-    RegionExterior -> Right False
-    RegionOnBoundary -> Left (MinkowskiInclusionAmbiguous cellId inclusionWitness)
-
-representativeFaces
-  :: OverlayResult leftLabel rightLabel
-  -> IntMap.IntMap FaceId
-representativeFaces result =
-  IntMap.fromListWith min
-    [ (overlayCellIndex (overlayFaceCellId (faceData triangulation face)), face)
-    | face <- innerFaces triangulation
-    , faceCarriesExactArea result face
-    ]
- where
-  triangulation = overlayResultTriangulation result
-
-overlayCellIndex :: OverlayCellId -> Int
-overlayCellIndex (OverlayCellId index) = index
-
-publishCellSelection
-  :: Set.Set OverlayCellId
-  -> OverlayResult leftLabel rightLabel
-  -> Either MinkowskiError PlanarRegion
-publishCellSelection selected result = do
-  layer <-
-    first MinkowskiPublicationFailed
-      ( labelledPlanarLayerFromExactCoordinates
-          False
-          triangulation
-          (\vertex -> Right (overlayExactPoint (vertexData triangulation vertex)))
-          (\face ->
-             if faceCarriesExactArea result face
-               then
-                 Right
-                   ( Set.member
-                       (overlayFaceCellId (faceData triangulation face))
-                       selected
-                   )
-               else Right False)
-      )
-  pure (Map.findWithDefault emptyPlanarRegion True (planarLayerRegions layer))
- where
-  triangulation = overlayResultTriangulation result
-
-morphologyLayer
-  :: PlanarRegion
-  -> PlanarLayer Bool
-morphologyLayer region =
-  planarLayerFromAdmittedComponents
-    False
-    [(True, component) | component <- planarRegionComponents region]
-
-emptyMorphologyLayer :: PlanarLayer Bool
-emptyMorphologyLayer = morphologyLayer emptyPlanarRegion
-
-regionBoundaryEdges :: PlanarRegion -> [(ExactPoint, ExactPoint)]
-regionBoundaryEdges region =
-  concatMap
-    (\component ->
-       concatMap
-         (cyclePairs . exactLoopPoints)
-         (polygonOuterLoop component : polygonHoleLoops component))
-    (planarRegionComponents region)
-
-metricsFromOverlay
-  :: OverlayResult leftLabel rightLabel
-  -> Int
-  -> MorphologyMetrics
-metricsFromOverlay result outputCells =
-  MorphologyMetrics
-    { metricOverlayPasses = 1
-    , metricExactCrossings = overlayExactCrossings (overlayReceipt result)
-    , metricOutputCells = Just outputCells
-    }
-
-composeReceipts
-  :: MinkowskiOperation
-  -> Int
-  -> MinkowskiReceipt
-  -> MinkowskiReceipt
-  -> MinkowskiReceipt
-composeReceipts operation inputComponents firstReceipt secondReceipt =
-  MinkowskiReceipt
-    { minkowskiOperation = operation
-    , minkowskiInputComponents = inputComponents
-    , minkowskiConvexPieces =
-        minkowskiConvexPieces firstReceipt
-          + minkowskiConvexPieces secondReceipt
-    , minkowskiGeneratedPieces =
-        minkowskiGeneratedPieces firstReceipt
-          + minkowskiGeneratedPieces secondReceipt
-    , minkowskiGeneratedConvolutionEdges =
-        minkowskiGeneratedConvolutionEdges firstReceipt
-          + minkowskiGeneratedConvolutionEdges secondReceipt
-    , minkowskiOverlayPasses =
-        minkowskiOverlayPasses firstReceipt
-          + minkowskiOverlayPasses secondReceipt
-    , minkowskiExactCrossings =
-        minkowskiExactCrossings firstReceipt
-          + minkowskiExactCrossings secondReceipt
-    , minkowskiOutputCells = minkowskiOutputCells secondReceipt
-    , minkowskiExactCoordinateBitGrowth =
-        max
-          (minkowskiExactCoordinateBitGrowth firstReceipt)
-          (minkowskiExactCoordinateBitGrowth secondReceipt)
-    }
-
-coordinateBitGrowth :: [PlanarRegion] -> PlanarRegion -> Int
-coordinateBitGrowth inputs output =
-  max 0
-    ( regionCoordinateBits output
-        - List.foldl' (\maximumBits -> max maximumBits . regionCoordinateBits) 0 inputs
-    )
-
-regionCoordinateBits :: PlanarRegion -> Int
-regionCoordinateBits = List.foldl' componentBits 0 . planarRegionComponents
- where
-  componentBits maximumBits component =
-    List.foldl'
-      loopBits
-      maximumBits
-      (polygonOuterLoop component : polygonHoleLoops component)
-  loopBits maximumBits =
-    List.foldl' pointBits maximumBits . exactLoopPoints
-  pointBits maximumBits point =
-    let (x, y) = exactPointCoordinates point
-     in max maximumBits (max (rationalBits x) (rationalBits y))
-
-rationalBits :: ExactRational -> Int
-rationalBits value =
-  max
-    (integerBitLength (abs (exactRationalNumerator value)))
-    (integerBitLength (exactRationalDenominator value))
diff --git a/src-planar/Moonlight/Triangulation/Overlay.hs b/src-planar/Moonlight/Triangulation/Overlay.hs
deleted file mode 100644
--- a/src-planar/Moonlight/Triangulation/Overlay.hs
+++ /dev/null
@@ -1,296 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact labelled common refinement. Source boundaries descend through one
--- exact segment-event plan, glue into canonical atomic constraints, and are
--- admitted only when their binary64 DCEL realization preserves every exact
--- relation.
-module Moonlight.Triangulation.Overlay
-  ( BoundaryLoopRef (..)
-  , OverlayOperand
-  , BoundaryRef
-  , BoundaryVertexRef
-  , BoundaryEdgeRef
-  , boundaryRefComponent
-  , boundaryRefLoop
-  , boundaryRefLocalIndex
-  , OverlayVertexOrigin
-  , overlayOriginLeftVertices
-  , overlayOriginRightVertices
-  , overlayOriginLeftEdges
-  , overlayOriginRightEdges
-  , OverlayEdgeOrigin
-  , overlayEdgeLeftSources
-  , overlayEdgeRightSources
-  , OverlaySupport
-  , overlaySupportLabels
-  , OverlayCellSupport (..)
-  , OverlayCellId (..)
-  , OverlayCellGeometry (..)
-  , OverlayCell (..)
-  , OverlayFace (..)
-  , OverlayVertex (..)
-  , OverlayEdge (..)
-  , OverlayReceipt (..)
-  , OverlayArrangementObstruction (..)
-  , OverlayCellWitness (..)
-  , OverlayError (..)
-  , OverlayResult
-  , OverlaySelectionKind (..)
-  , OverlaySelectionError (..)
-  , overlayLayers
-  , overlayEmbeddedTriangulation
-  , overlayReceipt
-  , overlayCells
-  , overlayArrangementVertices
-  , overlayArrangementEdges
-  , overlayPlanarLayer
-  , overlaySelectedRegion
-  , overlayClosedUnion
-  , overlayClosedIntersection
-  , overlayRegularizedDifference
-  ) where
-
-import Data.Bifunctor (first)
-import Control.Monad (filterM)
-import qualified Data.Map.Strict as Map
-import qualified Data.Vector as V
-import Moonlight.Triangulation.CellSet (ExactCellSet)
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Dcel (vertexData)
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( UndirectedEdgeId
-  , VertexId
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( innerFaces
-  , undirectedEdges
-  , vertices
-  )
-import Moonlight.Triangulation.Internal.CellSet (closeExactCellSetWith)
-import Moonlight.Triangulation.Internal.Overlay.Arrangement
-  ( certifyArrangement
-  )
-import Moonlight.Triangulation.Internal.Overlay.Resident
-  ( OverlayDiagonalSchedule (CanonicalOverlayDiagonals)
-  , edgeSupport
-  , faceCarriesExactArea
-  , faceLabels
-  , regionFaceLabels
-  , residentOverlay
-  , vertexSupport
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Constrained)
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarLayer
-  , PlanarRegion
-  , RegionPublicationError (..)
-  , emptyPlanarRegion
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  )
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( labelledPlanarLayerFromExactCoordinates
-  , planarLayerFromAdmittedComponents
-  )
-
--- | Construct the exact common refinement and its one faithful resident DCEL.
-overlayLayers
-  :: (Ord leftLabel, Ord rightLabel)
-  => PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (OverlayResult leftLabel rightLabel)
-overlayLayers leftLayer rightLayer = do
-  certified <- certifyArrangement leftLayer rightLayer
-  residentOverlay
-    CanonicalOverlayDiagonals
-    (planarLayerOutsideLabel leftLayer, planarLayerOutsideLabel rightLayer)
-    certified
-
--- | The binary64 realization used by existing DCEL observations. Exact overlay
--- operations accept 'OverlayResult', never this projection.
-overlayEmbeddedTriangulation
-  :: OverlayResult leftLabel rightLabel
-  -> Triangulation
-      'Constrained
-      OverlayVertex
-      ()
-      OverlayEdge
-      OverlayFace
-overlayEmbeddedTriangulation = overlayResultTriangulation
-
-overlayReceipt :: OverlayResult leftLabel rightLabel -> OverlayReceipt
-overlayReceipt = overlayResultReceipt
-
-overlayCells
-  :: OverlayResult leftLabel rightLabel
-  -> [(OverlayCellId, OverlayCell leftLabel rightLabel)]
-overlayCells result =
-  V.toList
-    (V.imap (\index cell -> (OverlayCellId index, cell)) (overlayResultCells result))
-
-overlayArrangementVertices
-  :: OverlayResult leftLabel rightLabel
-  -> [(VertexId, OverlayVertex)]
-overlayArrangementVertices result =
-  let triangulation = overlayResultTriangulation result
-   in [(vertex, vertexData triangulation vertex) | vertex <- vertices triangulation]
-
-overlayArrangementEdges
-  :: OverlayResult leftLabel rightLabel
-  -> [(UndirectedEdgeId, OverlayEdgeOrigin)]
-overlayArrangementEdges result =
-  let triangulation = overlayResultTriangulation result
-   in [ (edge, origin)
-      | edge <- undirectedEdges triangulation
-      , OverlayBoundary origin <- [Dcel.undirectedEdgeData triangulation edge]
-      ]
-
--- | Publish the already-admitted bounded cell geometry. The resident DCEL is
--- a realization of these exact cells, not a second authoring source.
-overlayPlanarLayer
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> PlanarLayer (leftLabel, rightLabel)
-overlayPlanarLayer result =
-  planarLayerFromAdmittedComponents
-    (overlayResultOutsideLabels result)
-    [ ( (overlayCellLeft cell, overlayCellRight cell)
-      , component
-      )
-    | cell <- V.toList (overlayResultCells result)
-    , (overlayCellLeft cell, overlayCellRight cell)
-        /= overlayResultOutsideLabels result
-    , BoundedOverlayCell component <- [overlayCellGeometry cell]
-    ]
-
--- | Publish the selected two-dimensional cells. Internal arrangement edges
--- between differently labelled but jointly selected cells dissolve because
--- selection precedes component descent.
-overlaySelectedRegion
-  :: ((leftLabel, rightLabel) -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either RegionPublicationError PlanarRegion
-overlaySelectedRegion selected result
-  | selected (overlayResultOutsideLabels result) = Left RegionUnboundedSelection
-  | otherwise = do
-      published <-
-        labelledPlanarLayerFromExactCoordinates
-          False
-          triangulation
-          exactPointAt
-          labelFace
-      pure (Map.findWithDefault emptyPlanarRegion True (planarLayerRegions published))
- where
-  triangulation = overlayResultTriangulation result
-  exactPointAt vertex = Right (overlayExactPoint (vertexData triangulation vertex))
-  labelFace face
-    | faceCarriesExactArea result face =
-        selected <$> regionFaceLabels result face
-    | otherwise = Right False
-
-overlayClosedUnion
-  :: (Ord leftLabel, Ord rightLabel)
-  => (leftLabel -> Bool)
-  -> (rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-overlayClosedUnion selectLeft selectRight =
-  selectClosedCells
-    ClosedUnionSelection
-    (\support -> supportAny selectLeft (overlaySupportLeft support) || supportAny selectRight (overlaySupportRight support))
-    (\leftLabel rightLabel -> selectLeft leftLabel || selectRight rightLabel)
-
-overlayClosedIntersection
-  :: (Ord leftLabel, Ord rightLabel)
-  => (leftLabel -> Bool)
-  -> (rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-overlayClosedIntersection selectLeft selectRight =
-  selectClosedCells
-    ClosedIntersectionSelection
-    (\support -> supportAny selectLeft (overlaySupportLeft support) && supportAny selectRight (overlaySupportRight support))
-    (\leftLabel rightLabel -> selectLeft leftLabel && selectRight rightLabel)
-
-overlayRegularizedDifference
-  :: (leftLabel -> Bool)
-  -> (rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-overlayRegularizedDifference selectLeft selectRight result =
-  let outsidePair = overlayResultOutsideLabels result
-      selectFace leftLabel rightLabel = selectLeft leftLabel && not (selectRight rightLabel)
-   in if uncurry selectFace outsidePair
-        then Left (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
-        else closeSelectedCells [] [] selectFace result
-
-selectClosedCells
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlaySelectionKind
-  -> (OverlayCellSupport leftLabel rightLabel -> Bool)
-  -> (leftLabel -> rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-selectClosedCells selectionKind selectSupport selectFace result =
-  let outsidePair = overlayResultOutsideLabels result
-      triangulation = overlayResultTriangulation result
-   in if uncurry selectFace outsidePair
-        then Left (OverlaySelectionContainsUnboundedCell selectionKind)
-        else do
-          selectedVertices <-
-            filterM
-              ( fmap selectSupport
-                  . first OverlaySelectionProvenance
-                  . vertexSupport result
-              )
-              (vertices triangulation)
-          selectedEdges <-
-            filterM
-              (\edge ->
-                 case Dcel.undirectedEdgeData triangulation edge of
-                   OverlayDiagonal -> Right False
-                   OverlayBoundary _ ->
-                     selectSupport
-                       <$> first OverlaySelectionProvenance (edgeSupport result edge))
-              (undirectedEdges triangulation)
-          closeSelectedCells selectedVertices selectedEdges selectFace result
-
-closeSelectedCells
-  :: [VertexId]
-  -> [UndirectedEdgeId]
-  -> (leftLabel -> rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-closeSelectedCells selectedVertices selectedEdges selectFace result =
-  let triangulation = overlayResultTriangulation result
-      exactPointAt vertex = Right (overlayExactPoint (vertexData triangulation vertex))
-   in do
-        selectedFaces <-
-          filterM
-            (\face ->
-               if faceCarriesExactArea result face
-                 then
-                   fmap (uncurry selectFace)
-                     ( first OverlaySelectionProvenance
-                         (faceLabels result face)
-                     )
-                 else Right False)
-            (innerFaces triangulation)
-        first OverlaySelectionInvalid
-          ( closeExactCellSetWith
-              exactPointAt
-              triangulation
-              selectedVertices
-              selectedEdges
-              selectedFaces
-          )
-
-supportAny :: (label -> Bool) -> OverlaySupport label -> Bool
-supportAny predicate = any predicate . overlaySupportLabels
diff --git a/src-public/Moonlight/Triangulation.hs b/src-public/Moonlight/Triangulation.hs
--- a/src-public/Moonlight/Triangulation.hs
+++ b/src-public/Moonlight/Triangulation.hs
@@ -116,16 +116,12 @@
   , constrainedSeamFaceFirstPoint
   , constrainedSeamFaceSecondPoint
   , constrainedSeamFaceThirdPoint
-  , ConstrainedSeamConstraintEvidence
-  , constrainedSeamConstraintSegment
-  , constrainedSeamConstraintRecovery
   , ConstrainedSeamResult
   , constrainedSeamResultTriangulation
   , constrainedSeamLeftFaceCount
   , constrainedSeamRightFaceEvidence
   , constrainedSeamJoinFaces
   , constrainedSeamLeftConstraintCount
-  , constrainedSeamRightConstraintEvidence
   , constrainedSeamConstraintStats
   , constrainedSeamBuildStats
   , constrainedSeamPublicationStats
@@ -154,6 +150,7 @@
   , refinementVisitedProtectedFaces
   , refinementCreatedFaces
   , refinementFinalPermittedFaces
+  , refinementFinalInterfaceIncidence
   , refinementTouchedEdges
   , refinementRemovedEdges
   , refinementInterfaceBoundaryReads
@@ -471,7 +468,6 @@
   , ConstrainedUnionError (..)
   , ConstrainedExtensionResult
   , ConstrainedSeamSide (..)
-  , ConstrainedSeamConstraintEvidence
   , ConstrainedSeamFaceEvidence
   , ConstrainedSeamResult
   , ConstraintBatchResult
@@ -483,8 +479,6 @@
   , constraintBatchStats
   , constraintBatchTriangulation
   , constrainedSeamBuildStats
-  , constrainedSeamConstraintRecovery
-  , constrainedSeamConstraintSegment
   , constrainedSeamConstraintStats
   , constrainedSeamCachedFrontierPointReads
   , constrainedSeamFaceFirstPoint
@@ -495,7 +489,6 @@
   , constrainedSeamJoinFaces
   , constrainedSeamPublicationStats
   , constrainedSeamResultTriangulation
-  , constrainedSeamRightConstraintEvidence
   , constrainedSeamRightFaceEvidence
   , constrainedSeamSourceFace
   , constrainedSeamTargetFace
@@ -541,6 +534,7 @@
   , refinementComplete
   , refinementCreatedFaces
   , refinementFinalPermittedFaces
+  , refinementFinalInterfaceIncidence
   , refinementExcludedFaces
   , refinementRemovedEdges
   , refinementTouchedEdges
diff --git a/src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs b/src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs
@@ -0,0 +1,326 @@
+-- | Pure exact convex-polygon algebra: admission, linear edge-angle
+-- convolution, reflection, hull construction, and support-half-plane erosion.
+module Moonlight.Triangulation.Internal.Minkowski.Convex
+  ( convexPolygon
+  , convexPolygonPoints
+  , convexPolygonRegion
+  , admittedConvexLoop
+  , structuringElement
+  , structuringElementPolygon
+  , convexMinkowskiSum
+  , convexMinkowskiPolygon
+  , convexHullPolygon
+  , reflectConvexPolygon
+  , convexPolygonCentroid
+  , erodeConvexBy
+  , addExactPoints
+  , subtractExactPoints
+  ) where
+
+import Control.Monad (foldM)
+import Data.Bifunctor (first)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+import Moonlight.Triangulation.Exact
+  ( ExactPoint
+  , ExactVector (..)
+  , addExactVectors
+  , compareExactVectorAngle
+  , exactVectorFromPoints
+  , exactSegment
+  , exactSupportingLineIntersection
+  , exactOrient2d
+  , exactPoint
+  , exactPointCoordinates
+  , translateExactPoint
+  )
+import Moonlight.Triangulation.Internal.BoundaryCycle
+  ( cyclePairs
+  , cyclePairsNonEmpty
+  , cyclicTriples
+  , rotateCycleLeast
+  , rotateCycleLeastBy
+  )
+import Moonlight.Triangulation.Internal.ExactRational
+  ( ExactRational
+  , exactDivide
+  )
+import Moonlight.Triangulation.Internal.Minkowski.Types
+  ( ConvexPolygon (..)
+  , MinkowskiError (..)
+  , StructuringElement (..)
+  )
+import Moonlight.Triangulation.Internal.Region.Types
+  ( ExactLoop (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  , RegionPointLocation (..)
+  )
+import Moonlight.Triangulation.Region
+  ( exactLoop
+  , exactLoopPoints
+  , regionPointLocation
+  )
+
+convexPolygon
+  :: NonEmpty ExactPoint
+  -> Either MinkowskiError ConvexPolygon
+convexPolygon submitted = do
+  loop <- first MinkowskiInvalidConvexLoop (exactLoop submitted)
+  let points = exactLoopPoints loop
+  case
+    [ (index, turn)
+    | (index, (previous, current, next)) <-
+        zip [0 :: Int ..] (cyclicTriples (NonEmpty.toList points))
+    , let turn = exactOrient2d previous current next
+    , turn /= GT
+    ] of
+    (index, turn) : _ -> Left (MinkowskiNonConvexTurn index turn)
+    [] -> Right (ConvexPolygon loop)
+
+convexPolygonPoints :: ConvexPolygon -> NonEmpty ExactPoint
+convexPolygonPoints (ConvexPolygon loop) = exactLoopPoints loop
+
+convexPolygonRegion :: ConvexPolygon -> PlanarRegion
+convexPolygonRegion (ConvexPolygon loop) =
+  PlanarRegion [PolygonComponent loop []]
+
+admittedConvexLoop :: ExactLoop -> Maybe ConvexPolygon
+admittedConvexLoop loop
+  | all ((== GT) . orderedTurn) (cyclicTriples (NonEmpty.toList (exactLoopPoints loop))) =
+      Just (ConvexPolygon loop)
+  | otherwise = Nothing
+ where
+  orderedTurn (previous, current, next) =
+    exactOrient2d previous current next
+
+structuringElement
+  :: ConvexPolygon
+  -> Either MinkowskiError StructuringElement
+structuringElement polygon =
+  let origin = exactPoint 0 0
+      location = regionPointLocation (convexPolygonRegion polygon) origin
+   in case location of
+        RegionExterior -> Left (MinkowskiOriginOutside location)
+        _ -> Right (StructuringElement polygon)
+
+structuringElementPolygon :: StructuringElement -> ConvexPolygon
+structuringElementPolygon (StructuringElement polygon) = polygon
+
+convexMinkowskiSum
+  :: ConvexPolygon
+  -> ConvexPolygon
+  -> PlanarRegion
+convexMinkowskiSum left right =
+  convexPolygonRegion (convexMinkowskiPolygon left right)
+
+convexMinkowskiPolygon
+  :: ConvexPolygon
+  -> ConvexPolygon
+  -> ConvexPolygon
+convexMinkowskiPolygon left right =
+  let leftPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints left)
+      rightPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints right)
+      start = addExactPoints (NonEmpty.head leftPoints) (NonEmpty.head rightPoints)
+      directions =
+        mergeDirections
+          (edgeDirections leftPoints)
+          (edgeDirections rightPoints)
+      directionList = NonEmpty.toList directions
+      scanned = scanl translateExactPoint start directionList
+      resultPoints = start :| take (length directionList - 1) (drop 1 scanned)
+   in ConvexPolygon (ExactLoop (rotateCycleLeast resultPoints))
+
+convexHullPolygon
+  :: NonEmpty ExactPoint
+  -> Either MinkowskiError ConvexPolygon
+convexHullPolygon submitted =
+  let points = NonEmpty.toList submitted
+   in case convexHullPoints points of
+    Nothing -> Left (MinkowskiConvexHullDegenerate points)
+    Just hullPoints -> Right (ConvexPolygon (ExactLoop (rotateCycleLeast hullPoints)))
+
+reflectConvexPolygon :: ConvexPolygon -> ConvexPolygon
+reflectConvexPolygon polygon =
+  ConvexPolygon
+    ( ExactLoop
+        ( rotateCycleLeast
+            (NonEmpty.reverse (fmap negateExactPoint (convexPolygonPoints polygon)))
+        )
+    )
+
+convexPolygonCentroid
+  :: ConvexPolygon
+  -> Either MinkowskiError ExactPoint
+convexPolygonCentroid polygon = do
+  let points = convexPolygonPoints polygon
+      count = fromIntegral (NonEmpty.length points)
+      (sumX, sumY) =
+        List.foldl'
+          (\(accumulatedX, accumulatedY) point ->
+             let (x, y) = exactPointCoordinates point
+              in (accumulatedX + x, accumulatedY + y))
+          (0, 0)
+          points
+  x <- first MinkowskiExactArithmetic (exactDivide sumX count)
+  y <- first MinkowskiExactArithmetic (exactDivide sumY count)
+  pure (exactPoint x y)
+
+-- | Erode one convex polygon by another through the strongest translated
+-- support half-plane for each source edge. Sutherland--Hodgman descent keeps
+-- the construction exact; a lower-dimensional residual is represented by the
+-- empty polygonal region at this two-dimensional publication boundary.
+erodeConvexBy
+  :: ConvexPolygon
+  -> ConvexPolygon
+  -> Either MinkowskiError (Maybe ConvexPolygon)
+erodeConvexBy source kernel = do
+  let sourcePoints = convexPolygonPoints source
+      kernelPoints = convexPolygonPoints kernel
+      firstKernel = NonEmpty.head kernelPoints
+      initial =
+        map
+          (`subtractExactPoints` firstKernel)
+          (NonEmpty.toList sourcePoints)
+      halfPlanes =
+        [ strongestHalfPlane kernelPoints from to
+        | (from, to) <- cyclePairs sourcePoints
+        ]
+  clipped <- foldM clipPolygon initial halfPlanes
+  pure (ConvexPolygon . ExactLoop . rotateCycleLeast <$> convexHullPoints clipped)
+
+edgeDirections :: NonEmpty ExactPoint -> NonEmpty ExactVector
+edgeDirections = fmap (uncurry exactVectorFromPoints) . cyclePairsNonEmpty
+
+mergeDirections
+  :: NonEmpty ExactVector
+  -> NonEmpty ExactVector
+  -> NonEmpty ExactVector
+mergeDirections (left :| leftTail) (right :| rightTail) =
+  case compareExactVectorAngle left right of
+    LT -> left :| mergeRemaining leftTail (right : rightTail)
+    GT -> right :| mergeRemaining (left : leftTail) rightTail
+    EQ -> addExactVectors left right :| mergeRemaining leftTail rightTail
+
+mergeRemaining :: [ExactVector] -> [ExactVector] -> [ExactVector]
+mergeRemaining [] right = right
+mergeRemaining left [] = left
+mergeRemaining left@(leftHead : leftTail) right@(rightHead : rightTail) =
+  case compareExactVectorAngle leftHead rightHead of
+    LT -> leftHead : mergeRemaining leftTail right
+    GT -> rightHead : mergeRemaining left rightTail
+    EQ -> addExactVectors leftHead rightHead : mergeRemaining leftTail rightTail
+
+pointSweepKey :: ExactPoint -> (ExactRational, ExactRational)
+pointSweepKey point =
+  let (x, y) = exactPointCoordinates point
+   in (y, x)
+
+convexHullPoints :: [ExactPoint] -> Maybe (NonEmpty ExactPoint)
+convexHullPoints submitted =
+  case Set.toAscList (Set.fromList submitted) of
+    firstPoint : secondPoint : thirdPoint : remaining ->
+      let ordered = firstPoint : secondPoint : thirdPoint : remaining
+          lower = dropFinal (reverse (List.foldl' hullStep [] ordered))
+          upper = dropFinal (reverse (List.foldl' hullStep [] (reverse ordered)))
+       in case lower <> upper of
+            firstHullPoint : secondHullPoint : thirdHullPoint : hullTail ->
+              Just (firstHullPoint :| (secondHullPoint : thirdHullPoint : hullTail))
+            _ -> Nothing
+    _ -> Nothing
+
+hullStep :: [ExactPoint] -> ExactPoint -> [ExactPoint]
+hullStep (current : previous : remaining) candidate
+  | exactOrient2d previous current candidate /= GT =
+      hullStep (previous : remaining) candidate
+hullStep hull candidate = candidate : hull
+
+dropFinal :: [value] -> [value]
+dropFinal values =
+  case reverse values of
+    _ : remaining -> reverse remaining
+    [] -> []
+
+strongestHalfPlane
+  :: NonEmpty ExactPoint
+  -> ExactPoint
+  -> ExactPoint
+  -> (ExactPoint, ExactPoint)
+strongestHalfPlane kernelPoints from to =
+  let direction = exactVectorFromPoints from to
+      supportPoint =
+        case kernelPoints of
+          initial :| remaining ->
+            List.foldl'
+              (\selected candidate ->
+                 if directionPointCross direction candidate
+                      < directionPointCross direction selected
+                   then candidate
+                   else selected)
+              initial
+              remaining
+   in ( subtractExactPoints from supportPoint
+      , subtractExactPoints to supportPoint
+      )
+
+directionPointCross :: ExactVector -> ExactPoint -> ExactRational
+directionPointCross (ExactVector directionX directionY) point =
+  let (x, y) = exactPointCoordinates point
+   in directionX * y - directionY * x
+
+clipPolygon
+  :: [ExactPoint]
+  -> (ExactPoint, ExactPoint)
+  -> Either MinkowskiError [ExactPoint]
+clipPolygon [] _ = Right []
+clipPolygon polygon halfPlane =
+  concat <$> traverse (clipEdge halfPlane) (cyclePairsList polygon)
+
+clipEdge
+  :: (ExactPoint, ExactPoint)
+  -> (ExactPoint, ExactPoint)
+  -> Either MinkowskiError [ExactPoint]
+clipEdge (boundaryFrom, boundaryTo) (from, to) =
+  case (inside from, inside to) of
+    (True, True) -> Right [to]
+    (True, False) -> (: []) <$> supportingLineIntersection from to boundaryFrom boundaryTo
+    (False, True) -> do
+      crossing <- supportingLineIntersection from to boundaryFrom boundaryTo
+      pure [crossing, to]
+    (False, False) -> Right []
+ where
+  inside point = exactOrient2d boundaryFrom boundaryTo point /= LT
+
+supportingLineIntersection
+  :: ExactPoint
+  -> ExactPoint
+  -> ExactPoint
+  -> ExactPoint
+  -> Either MinkowskiError ExactPoint
+supportingLineIntersection lineFrom lineTo boundaryFrom boundaryTo = do
+  clippedSegment <- first MinkowskiInvalidSegment (exactSegment lineFrom lineTo)
+  boundarySegment <- first MinkowskiInvalidSegment (exactSegment boundaryFrom boundaryTo)
+  first MinkowskiLineIntersection
+    (exactSupportingLineIntersection clippedSegment boundarySegment)
+
+addExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
+addExactPoints left right =
+  let (leftX, leftY) = exactPointCoordinates left
+      (rightX, rightY) = exactPointCoordinates right
+   in exactPoint (leftX + rightX) (leftY + rightY)
+
+subtractExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
+subtractExactPoints left right =
+  let (leftX, leftY) = exactPointCoordinates left
+      (rightX, rightY) = exactPointCoordinates right
+   in exactPoint (leftX - rightX) (leftY - rightY)
+
+negateExactPoint :: ExactPoint -> ExactPoint
+negateExactPoint point =
+  let (x, y) = exactPointCoordinates point
+   in exactPoint (negate x) (negate y)
+
+cyclePairsList :: [value] -> [(value, value)]
+cyclePairsList = maybe [] cyclePairs . NonEmpty.nonEmpty
diff --git a/src-public/Moonlight/Triangulation/Internal/Minkowski/Types.hs b/src-public/Moonlight/Triangulation/Internal/Minkowski/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Internal/Minkowski/Types.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Closed vocabulary and invariant carriers for exact polygonal morphology.
+module Moonlight.Triangulation.Internal.Minkowski.Types
+  ( ConvexPolygon (..)
+  , StructuringElement (..)
+  , MinkowskiOperation (..)
+  , MinkowskiError (..)
+  , MinkowskiReceipt (..)
+  ) where
+
+import Control.DeepSeq (NFData)
+import GHC.Generics (Generic)
+import Moonlight.Triangulation.Exact
+  ( ExactGeometryError
+  , ExactIntersectionError
+  , ExactPoint
+  )
+import Moonlight.Triangulation.Handles.HandleDefs (FaceId)
+import Moonlight.Triangulation.Internal.ExactRational (ExactArithmeticError)
+import Moonlight.Triangulation.Internal.Overlay.Types
+  ( OverlayCellId
+  , OverlayCellWitness
+  , OverlayError
+  )
+import Moonlight.Triangulation.Internal.Region.Types
+  ( ExactLoop
+  , RegionPublicationError
+  , RegionPointLocation
+  , RegionValidationError
+  )
+
+newtype ConvexPolygon = ConvexPolygon ExactLoop
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+newtype StructuringElement = StructuringElement ConvexPolygon
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data MinkowskiOperation
+  = MinkowskiAddition
+  | MinkowskiErosion
+  | MinkowskiOpening
+  | MinkowskiClosing
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data MinkowskiError
+  = MinkowskiInvalidConvexLoop !RegionValidationError
+  | MinkowskiInvalidSegment !ExactGeometryError
+  | MinkowskiNonConvexTurn !Int !Ordering
+  | MinkowskiOriginOutside !RegionPointLocation
+  | MinkowskiExactArithmetic !ExactArithmeticError
+  | MinkowskiLineIntersection !ExactIntersectionError
+  | MinkowskiOverlayFailed !(OverlayError Bool Bool)
+  | MinkowskiPublicationFailed !RegionPublicationError
+  | MinkowskiOverlayCellWitness !OverlayCellWitness
+  | MinkowskiFaceArity !FaceId !Int
+  | MinkowskiCandidateCellMissing !OverlayCellId
+  | MinkowskiInclusionAmbiguous !OverlayCellId !ExactPoint
+  | MinkowskiConvexHullDegenerate ![ExactPoint]
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data MinkowskiReceipt = MinkowskiReceipt
+  { minkowskiOperation :: !MinkowskiOperation
+  , minkowskiInputComponents :: !Int
+  , minkowskiConvexPieces :: !Int
+  , minkowskiGeneratedPieces :: !Int
+  , minkowskiGeneratedConvolutionEdges :: !Int
+  , minkowskiOverlayPasses :: !Int
+  , minkowskiExactCrossings :: !Int
+  , minkowskiOutputCells :: !Int
+  , minkowskiExactCoordinateBitGrowth :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
diff --git a/src-public/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs b/src-public/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs
@@ -0,0 +1,811 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact source normalization and binary64 embedding certification. This
+-- module ends at the arrangement/resident seam: no DCEL face state crosses it.
+module Moonlight.Triangulation.Internal.Overlay.Arrangement
+  ( ExactEdgeKey
+  , AtomicEdge (..)
+  , atomicEdgeFrom
+  , atomicEdgeTo
+  , OverlayVertexSeed (..)
+  , ArrangementMetrics (..)
+  , CertifiedArrangement (..)
+  , certifyArrangement
+  , canonicalEdgeKey
+  , atomicKey
+  , compareAround
+  ) where
+
+import Data.Bifunctor (first)
+import Data.List (sortBy)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as V
+import GHC.Generics (Generic)
+import Moonlight.Triangulation.Exact
+  ( ExactGeometryError
+  , ExactPoint
+  , ExactSegment
+  , SegmentRelation (SegmentsShareEndpoint)
+  , compareExactVectorAngle
+  , exactPointCoordinates
+  , exactPointFromQueryPoint
+  , exactSegment
+  , exactSegmentEndpoints
+  , exactVectorFromPoints
+  )
+import Moonlight.Triangulation.Internal.BoundaryCycle
+  ( consecutivePairs
+  , orderedPair
+  , unorderedPairs
+  )
+import Moonlight.Triangulation.Internal.ExactRational
+  ( ExactRational
+  , exactDivide
+  , exactRationalIsZero
+  )
+import Moonlight.Triangulation.Internal.ExactSegmentEvents
+  ( ExactSegmentEvent (..)
+  , ExactSegmentEventPlan
+  , ExactSweepSegmentId (..)
+  , exactSegmentEventPlan
+  , exactSegmentEvents
+  , exactSegmentPairChecks
+  , exactSegmentRelationMap
+  , exactSegmentSplitPoints
+  , exactSegmentSweepMaximumHeight
+  )
+import Moonlight.Triangulation.Internal.Overlay.Embedding
+  ( DraftId (..)
+  , DraftIncidence (..)
+  , DraftNeighborhood (..)
+  , DraftReference (..)
+  , DraftVertexId
+  , ExactArrangementDraft (..)
+  , LocalEmbeddingCertificate (..)
+  , OverlayEmbeddingObstruction (..)
+  , certifyLocalEmbedding
+  )
+import Moonlight.Triangulation.Internal.Overlay.Types
+import Moonlight.Triangulation.Internal.Types
+  ( HasPosition (..)
+  , Point
+  , queryPointValue
+  )
+import Moonlight.Triangulation.Region
+  ( ExactLoop
+  , PlanarLayer
+  , PolygonComponent
+  , exactLoopPoints
+  , planarLayerOutsideLabel
+  , planarLayerRegions
+  , planarRegionComponents
+  , polygonHoleLoops
+  , polygonOuterLoop
+  )
+
+type ExactEdgeKey = (ExactPoint, ExactPoint)
+
+data SourceBoundary leftLabel rightLabel
+  = LeftSourceBoundary
+      !ExactSegment
+      !leftLabel
+      !(BoundaryVertexRef 'LeftOverlayOperand)
+      !(BoundaryVertexRef 'LeftOverlayOperand)
+      !(BoundaryEdgeRef 'LeftOverlayOperand)
+  | RightSourceBoundary
+      !ExactSegment
+      !rightLabel
+      !(BoundaryVertexRef 'RightOverlayOperand)
+      !(BoundaryVertexRef 'RightOverlayOperand)
+      !(BoundaryEdgeRef 'RightOverlayOperand)
+
+type SourceBoundaryConstructor operand label leftLabel rightLabel =
+  ExactSegment
+  -> label
+  -> BoundaryVertexRef operand
+  -> BoundaryVertexRef operand
+  -> BoundaryEdgeRef operand
+  -> SourceBoundary leftLabel rightLabel
+
+data AtomicContribution leftLabel rightLabel =
+  AtomicContribution !(SourceBoundary leftLabel rightLabel) !Bool
+
+data AtomicEdge leftLabel rightLabel = AtomicEdge
+  { atomicEdgeSegment :: !ExactSegment
+  , atomicEdgeOrigin :: !OverlayEdgeOrigin
+  , atomicEdgeLeftTransition :: !(Maybe (leftLabel, leftLabel))
+  , atomicEdgeRightTransition :: !(Maybe (rightLabel, rightLabel))
+  }
+
+data OriginAccumulation = OriginAccumulation
+  { accumulatedLeftVertices :: !(Set (BoundaryVertexRef 'LeftOverlayOperand))
+  , accumulatedRightVertices :: !(Set (BoundaryVertexRef 'RightOverlayOperand))
+  , accumulatedLeftEdges :: !(Set (BoundaryEdgeRef 'LeftOverlayOperand))
+  , accumulatedRightEdges :: !(Set (BoundaryEdgeRef 'RightOverlayOperand))
+  }
+
+data OverlayVertexSeed = OverlayVertexSeed
+  { seedExactPoint :: !ExactPoint
+  , seedEmbeddedPoint :: !Point
+  , seedOrigin :: !OverlayVertexOrigin
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+
+instance HasPosition OverlayVertexSeed where
+  position = seedEmbeddedPoint
+
+data ArrangementMetrics = ArrangementMetrics
+  { arrangementInputSegments :: !Int
+  , arrangementRelationEvents :: !Int
+  , arrangementExactCrossings :: !Int
+  , arrangementOverlapIntervals :: !Int
+  , arrangementEmbeddingCandidates :: !Int
+  , arrangementTotalRelationChecks :: !Int
+  , arrangementSweepMaximumHeight :: !Int
+  }
+
+data CertifiedArrangement leftLabel rightLabel = CertifiedArrangement
+  { certifiedAtomicEdges :: !(V.Vector (AtomicEdge leftLabel rightLabel))
+  , certifiedVertexSeeds :: !(V.Vector OverlayVertexSeed)
+  , certifiedConstraints :: !(V.Vector (Int, Int))
+  , certifiedInexactEmbeddingPoints :: !(Set ExactPoint)
+  , certifiedMetrics :: !ArrangementMetrics
+  }
+
+certifyArrangement
+  :: (Ord leftLabel, Ord rightLabel)
+  => PlanarLayer leftLabel
+  -> PlanarLayer rightLabel
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (CertifiedArrangement leftLabel rightLabel)
+certifyArrangement leftLayer rightLayer = do
+  sources <- flattenLayers leftLayer rightLayer
+  sourcePlan <-
+    first OverlaySegmentEventsInvalid
+      (exactSegmentEventPlan (V.map sourceExactSegment sources))
+  atomicEdges <- normalizeAtomicEdges leftLayer rightLayer sources sourcePlan
+  let atomicVector = V.fromList atomicEdges
+      pointIds = exactPointIds atomicVector
+      origins = vertexOrigins sources atomicVector
+      atomicRelations = atomicEndpointRelations atomicVector
+  draft <- exactArrangementDraft sources sourcePlan atomicVector pointIds atomicRelations
+  localCertificate <- first OverlayEmbeddingRefused (certifyLocalEmbedding draft)
+  let projectedPoints = projectedExactPoints localCertificate
+      inexactEmbeddingPoints =
+        Set.fromList
+          [ exactPoint
+          | (exactPoint, vertexId) <- Map.toAscList pointIds
+          , Map.lookup vertexId projectedPoints /= Just exactPoint
+          ]
+  projectedSegments <- projectedAtomicSegments atomicVector pointIds projectedPoints
+  projectedPlan <-
+    first OverlaySegmentEventsInvalid
+      (exactSegmentEventPlan projectedSegments)
+  dischargeGlobalRelations atomicRelations projectedPlan
+  seeds <- overlayVertexSeeds pointIds origins localCertificate
+  constraints <- atomicConstraints pointIds atomicVector
+  let events = exactSegmentEvents sourcePlan
+  pure
+    CertifiedArrangement
+      { certifiedAtomicEdges = atomicVector
+      , certifiedVertexSeeds = seeds
+      , certifiedConstraints = constraints
+      , certifiedInexactEmbeddingPoints = inexactEmbeddingPoints
+      , certifiedMetrics =
+          ArrangementMetrics
+            { arrangementInputSegments = V.length sources
+            , arrangementRelationEvents = length events
+            , arrangementExactCrossings =
+                length [() | ExactProperCrossing {} <- events]
+            , arrangementOverlapIntervals =
+                length [() | ExactCollinearOverlap {} <- events]
+            , arrangementEmbeddingCandidates = Map.size pointIds
+            , arrangementTotalRelationChecks =
+                exactSegmentPairChecks sourcePlan
+                  + exactSegmentPairChecks projectedPlan
+            , arrangementSweepMaximumHeight =
+                max
+                  (exactSegmentSweepMaximumHeight sourcePlan)
+                  (exactSegmentSweepMaximumHeight projectedPlan)
+            }
+      }
+flattenLayers
+  :: PlanarLayer leftLabel
+  -> PlanarLayer rightLabel
+  -> Either (OverlayError leftLabel rightLabel) (V.Vector (SourceBoundary leftLabel rightLabel))
+flattenLayers leftLayer rightLayer = do
+  leftSources <- flattenLayer LeftSourceBoundary leftLayer
+  rightSources <- flattenLayer RightSourceBoundary rightLayer
+  pure (V.fromList (leftSources <> rightSources))
+
+flattenLayer
+  :: SourceBoundaryConstructor operand label leftLabel rightLabel
+  -> PlanarLayer label
+  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
+flattenLayer makeBoundary layer =
+  fmap concat
+    ( traverse
+        (\(componentIndex, (label, component)) ->
+           flattenComponent makeBoundary componentIndex label component)
+        (zip [0 ..] (labelledComponents layer))
+    )
+{-# INLINE flattenLayer #-}
+
+labelledComponents :: PlanarLayer label -> [(label, PolygonComponent)]
+labelledComponents layer =
+  [ (label, component)
+  | (label, region) <- Map.toAscList (planarLayerRegions layer)
+  , component <- planarRegionComponents region
+  ]
+
+flattenComponent
+  :: SourceBoundaryConstructor operand label leftLabel rightLabel
+  -> Int
+  -> label
+  -> PolygonComponent
+  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
+flattenComponent makeBoundary componentIndex label component = do
+  outer <-
+    flattenLoop
+      makeBoundary
+      componentIndex
+      BoundaryOuterLoop
+      label
+      (polygonOuterLoop component)
+  holes <-
+    fmap concat
+      ( traverse
+          (\(holeIndex, loop) ->
+             flattenLoop
+               makeBoundary
+               componentIndex
+               (BoundaryHoleLoop holeIndex)
+               label
+               loop)
+          (zip [0 ..] (polygonHoleLoops component))
+      )
+  pure (outer <> holes)
+{-# INLINE flattenComponent #-}
+
+flattenLoop
+  :: SourceBoundaryConstructor operand label leftLabel rightLabel
+  -> Int
+  -> BoundaryLoopRef
+  -> label
+  -> ExactLoop
+  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
+flattenLoop makeBoundary componentIndex loopRef label loop =
+  traverse constructSource (indexedCycle (exactLoopPoints loop))
+ where
+  constructSource (edgeIndex, fromIndex, from, toIndex, to) = do
+    segment <- first (sourceGeometryError from) (exactSegment from to)
+    pure
+      ( makeBoundary
+          segment
+          label
+          (BoundaryRef componentIndex loopRef fromIndex)
+          (BoundaryRef componentIndex loopRef toIndex)
+          (BoundaryRef componentIndex loopRef edgeIndex)
+      )
+{-# INLINE flattenLoop #-}
+
+sourceGeometryError
+  :: ExactPoint
+  -> ExactGeometryError
+  -> OverlayError leftLabel rightLabel
+sourceGeometryError point _ = OverlayArrangementInvalid (OverlayRotationDegenerate point)
+
+indexedCycle :: NonEmpty value -> [(Int, Int, value, Int, value)]
+indexedCycle (firstValue :| remaining) =
+  let values = firstValue : remaining
+      count = length values
+   in [ (index, index, from, (index + 1) `mod` count, to)
+      | (index, (from, to)) <- zip [0 ..] (zip values (remaining <> [firstValue]))
+      ]
+
+sourceExactSegment :: SourceBoundary leftLabel rightLabel -> ExactSegment
+sourceExactSegment (LeftSourceBoundary segment _ _ _ _) = segment
+sourceExactSegment (RightSourceBoundary segment _ _ _ _) = segment
+
+normalizeAtomicEdges
+  :: (Ord leftLabel, Ord rightLabel)
+  => PlanarLayer leftLabel
+  -> PlanarLayer rightLabel
+  -> V.Vector (SourceBoundary leftLabel rightLabel)
+  -> ExactSegmentEventPlan
+  -> Either (OverlayError leftLabel rightLabel) [AtomicEdge leftLabel rightLabel]
+normalizeAtomicEdges leftLayer rightLayer sources plan =
+  fmap concat
+    ( traverse
+        (resolveAtomicContributions leftOutside rightOutside)
+        (Map.toAscList grouped)
+    )
+ where
+  leftOutside = planarLayerOutsideLabel leftLayer
+  rightOutside = planarLayerOutsideLabel rightLayer
+  grouped =
+    V.ifoldl'
+      (\groups sourceIndex source ->
+         List.foldl'
+           (insertAtomic source)
+           groups
+           (consecutivePairs (exactSegmentSplitPoints plan (ExactSweepSegmentId sourceIndex))))
+      Map.empty
+      sources
+  insertAtomic
+    :: SourceBoundary leftLabel' rightLabel'
+    -> Map ExactEdgeKey [AtomicContribution leftLabel' rightLabel']
+    -> (ExactPoint, ExactPoint)
+    -> Map ExactEdgeKey [AtomicContribution leftLabel' rightLabel']
+  insertAtomic source groups (from, to)
+    | from == to = groups
+    | otherwise =
+        let key@(canonicalFrom, _) = canonicalEdgeKey from to
+            contribution = AtomicContribution source (from == canonicalFrom)
+         in Map.insertWith (<>) key [contribution] groups
+
+resolveAtomicContributions
+  :: (Ord leftLabel, Ord rightLabel)
+  => leftLabel
+  -> rightLabel
+  -> (ExactEdgeKey, [AtomicContribution leftLabel rightLabel])
+  -> Either (OverlayError leftLabel rightLabel) [AtomicEdge leftLabel rightLabel]
+resolveAtomicContributions leftOutside rightOutside ((from, to), contributions) = do
+  let
+      ( leftLabelsOnLeft
+        , leftLabelsOnRight
+        , rightLabelsOnLeft
+        , rightLabelsOnRight
+        , leftSources
+        , rightSources
+        ) =
+          List.foldl'
+            collectContribution
+            (Set.empty, Set.empty, Set.empty, Set.empty, Set.empty, Set.empty)
+            contributions
+      origin =
+        OverlayEdgeOrigin
+          (Set.toAscList leftSources)
+          (Set.toAscList rightSources)
+  leftTransition <-
+    resolveTransition
+      OverlayLeftSourceSideConflict
+      origin
+      leftOutside
+      leftLabelsOnLeft
+      leftLabelsOnRight
+  rightTransition <-
+    resolveTransition
+      OverlayRightSourceSideConflict
+      origin
+      rightOutside
+      rightLabelsOnLeft
+      rightLabelsOnRight
+  if transitionIsIdentity leftTransition && transitionIsIdentity rightTransition
+    then Right []
+    else do
+      segment <- first (sourceGeometryError from) (exactSegment from to)
+      Right
+        [ AtomicEdge
+            { atomicEdgeSegment = segment
+            , atomicEdgeOrigin = origin
+            , atomicEdgeLeftTransition = leftTransition
+            , atomicEdgeRightTransition = rightTransition
+            }
+        ]
+ where
+  collectContribution
+    :: (Ord leftLabel', Ord rightLabel')
+    => ( Set leftLabel'
+       , Set leftLabel'
+       , Set rightLabel'
+       , Set rightLabel'
+       , Set (BoundaryEdgeRef 'LeftOverlayOperand)
+       , Set (BoundaryEdgeRef 'RightOverlayOperand)
+       )
+    -> AtomicContribution leftLabel' rightLabel'
+    -> ( Set leftLabel'
+       , Set leftLabel'
+       , Set rightLabel'
+       , Set rightLabel'
+       , Set (BoundaryEdgeRef 'LeftOverlayOperand)
+       , Set (BoundaryEdgeRef 'RightOverlayOperand)
+       )
+  collectContribution
+    ( !leftLeft
+      , !leftRight
+      , !rightLeft
+      , !rightRight
+      , !leftSources
+      , !rightSources
+      )
+    contribution =
+    case contribution of
+      AtomicContribution (LeftSourceBoundary _ label _ _ reference) follows ->
+        ( if follows then Set.insert label leftLeft else leftLeft
+        , if follows then leftRight else Set.insert label leftRight
+        , rightLeft
+        , rightRight
+        , Set.insert reference leftSources
+        , rightSources
+        )
+      AtomicContribution (RightSourceBoundary _ label _ _ reference) follows ->
+        ( leftLeft
+        , leftRight
+        , if follows then Set.insert label rightLeft else rightLeft
+        , if follows then rightRight else Set.insert label rightRight
+        , leftSources
+        , Set.insert reference rightSources
+        )
+
+resolveTransition
+  :: (OverlayEdgeOrigin -> NonEmpty label -> OverlayArrangementObstruction leftLabel rightLabel)
+  -> OverlayEdgeOrigin
+  -> label
+  -> Set label
+  -> Set label
+  -> Either (OverlayError leftLabel rightLabel) (Maybe (label, label))
+resolveTransition sideConflict origin outside labelsOnLeft labelsOnRight
+  | Set.null labelsOnLeft && Set.null labelsOnRight = Right Nothing
+  | otherwise =
+      Just
+        <$> ((,)
+               <$> resolveSide sideConflict origin outside labelsOnLeft
+               <*> resolveSide sideConflict origin outside labelsOnRight)
+{-# INLINE resolveTransition #-}
+
+transitionIsIdentity :: Eq label => Maybe (label, label) -> Bool
+transitionIsIdentity Nothing = True
+transitionIsIdentity (Just (leftLabel, rightLabel)) = leftLabel == rightLabel
+
+resolveSide
+  :: (OverlayEdgeOrigin -> NonEmpty label -> OverlayArrangementObstruction leftLabel rightLabel)
+  -> OverlayEdgeOrigin
+  -> label
+  -> Set label
+  -> Either (OverlayError leftLabel rightLabel) label
+resolveSide sideConflict origin outside labels =
+  case Set.toAscList labels of
+    [] -> Right outside
+    [label] -> Right label
+    firstLabel : remaining ->
+      Left
+        ( OverlayArrangementInvalid
+            (sideConflict origin (firstLabel :| remaining))
+        )
+{-# INLINE resolveSide #-}
+
+exactPointIds
+  :: V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Map ExactPoint DraftVertexId
+exactPointIds edges =
+  Map.fromAscList
+    ( zip
+        (Set.toAscList (V.foldl' collect Set.empty edges))
+      (map DraftId [0 ..])
+    )
+ where
+  collect
+    :: Set ExactPoint
+    -> AtomicEdge leftLabel rightLabel
+    -> Set ExactPoint
+  collect points edge = Set.insert (atomicEdgeFrom edge) (Set.insert (atomicEdgeTo edge) points)
+
+vertexOrigins
+  :: V.Vector (SourceBoundary leftLabel rightLabel)
+  -> V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Map ExactPoint OverlayVertexOrigin
+vertexOrigins sources atomicEdges =
+  Map.map finalizeOrigin
+    ( V.foldl'
+        addAtomicOrigin
+        (V.foldl' addSourceEndpoints Map.empty sources)
+        atomicEdges
+    )
+
+emptyOrigin :: OriginAccumulation
+emptyOrigin = OriginAccumulation Set.empty Set.empty Set.empty Set.empty
+
+mergeOrigin :: OriginAccumulation -> OriginAccumulation -> OriginAccumulation
+mergeOrigin left right =
+  OriginAccumulation
+    { accumulatedLeftVertices = accumulatedLeftVertices left <> accumulatedLeftVertices right
+    , accumulatedRightVertices = accumulatedRightVertices left <> accumulatedRightVertices right
+    , accumulatedLeftEdges = accumulatedLeftEdges left <> accumulatedLeftEdges right
+    , accumulatedRightEdges = accumulatedRightEdges left <> accumulatedRightEdges right
+    }
+
+addSourceEndpoints
+  :: Map ExactPoint OriginAccumulation
+  -> SourceBoundary leftLabel rightLabel
+  -> Map ExactPoint OriginAccumulation
+addSourceEndpoints origins source =
+  case source of
+    LeftSourceBoundary segment _ fromReference toReference _ ->
+      let (from, to) = exactSegmentEndpoints segment
+       in insertOrigin to (emptyOrigin{accumulatedLeftVertices = Set.singleton toReference})
+            (insertOrigin from (emptyOrigin{accumulatedLeftVertices = Set.singleton fromReference}) origins)
+    RightSourceBoundary segment _ fromReference toReference _ ->
+      let (from, to) = exactSegmentEndpoints segment
+       in insertOrigin to (emptyOrigin{accumulatedRightVertices = Set.singleton toReference})
+            (insertOrigin from (emptyOrigin{accumulatedRightVertices = Set.singleton fromReference}) origins)
+
+addAtomicOrigin
+  :: Map ExactPoint OriginAccumulation
+  -> AtomicEdge leftLabel rightLabel
+  -> Map ExactPoint OriginAccumulation
+addAtomicOrigin origins edge =
+  let origin = atomicEdgeOrigin edge
+      accumulation =
+        emptyOrigin
+          { accumulatedLeftEdges = Set.fromList (overlayEdgeLeftSources origin)
+          , accumulatedRightEdges = Set.fromList (overlayEdgeRightSources origin)
+          }
+   in insertOrigin (atomicEdgeTo edge) accumulation
+        (insertOrigin (atomicEdgeFrom edge) accumulation origins)
+
+insertOrigin
+  :: ExactPoint
+  -> OriginAccumulation
+  -> Map ExactPoint OriginAccumulation
+  -> Map ExactPoint OriginAccumulation
+insertOrigin = Map.insertWith mergeOrigin
+
+finalizeOrigin :: OriginAccumulation -> OverlayVertexOrigin
+finalizeOrigin accumulated =
+  OverlayVertexOrigin
+    { overlayOriginLeftVertices = Set.toAscList (accumulatedLeftVertices accumulated)
+    , overlayOriginRightVertices = Set.toAscList (accumulatedRightVertices accumulated)
+    , overlayOriginLeftEdges = Set.toAscList (accumulatedLeftEdges accumulated)
+    , overlayOriginRightEdges = Set.toAscList (accumulatedRightEdges accumulated)
+    }
+
+exactArrangementDraft
+  :: V.Vector (SourceBoundary leftLabel rightLabel)
+  -> ExactSegmentEventPlan
+  -> V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Map ExactPoint DraftVertexId
+  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
+  -> Either (OverlayError leftLabel rightLabel) ExactArrangementDraft
+exactArrangementDraft sources sourcePlan atomicEdges pointIds atomicRelations = do
+  draftSegmentsMap <-
+    Map.fromList
+      <$> traverse
+        (\(segmentIndex, edge) -> do
+           from <- requireDraftVertex pointIds (atomicEdgeFrom edge)
+           to <- requireDraftVertex pointIds (atomicEdgeTo edge)
+           pure (DraftId segmentIndex, (from, to)))
+        (V.toList (V.indexed atomicEdges))
+  memberships <-
+    Map.fromList
+      <$> traverse
+        (\(sourceIndex, source) -> do
+           values <-
+             traverse
+               (\point -> do
+                  parameter <- exactSourceParameter source point
+                  vertex <- requireDraftVertex pointIds point
+                  pure (parameter, vertex))
+               (exactSegmentSplitPoints sourcePlan (ExactSweepSegmentId sourceIndex))
+           pure (DraftId sourceIndex, values))
+        (V.toList (V.indexed sources))
+  neighborhoods <- buildDraftNeighborhoods atomicEdges pointIds
+  let incidences =
+        [ DraftIncidence
+            (DraftId leftIndex)
+            (DraftId rightIndex)
+            relation
+        | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
+            Map.toAscList atomicRelations
+        ]
+  pure
+    ExactArrangementDraft
+      { draftVertices = Map.fromList [(vertexId, point) | (point, vertexId) <- Map.toAscList pointIds]
+      , draftSegments = draftSegmentsMap
+      , draftSourceMemberships = memberships
+      , draftIncidences = incidences
+      , draftNeighborhoods = neighborhoods
+      }
+
+requireDraftVertex
+  :: Map ExactPoint DraftVertexId
+  -> ExactPoint
+  -> Either (OverlayError leftLabel rightLabel) DraftVertexId
+requireDraftVertex pointIds point =
+  case Map.lookup point pointIds of
+    Just vertex -> Right vertex
+    Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
+
+exactSourceParameter
+  :: SourceBoundary leftLabel rightLabel
+  -> ExactPoint
+  -> Either (OverlayError leftLabel rightLabel) ExactRational
+exactSourceParameter source point =
+  let (from, to) = exactSegmentEndpoints (sourceExactSegment source)
+      (fromX, fromY) = exactPointCoordinates from
+      (toX, toY) = exactPointCoordinates to
+      (pointX, pointY) = exactPointCoordinates point
+      (numerator, denominator) =
+        if exactRationalIsZero (toX - fromX)
+          then (pointY - fromY, toY - fromY)
+          else (pointX - fromX, toX - fromX)
+   in first OverlayExactArithmetic (exactDivide numerator denominator)
+
+buildDraftNeighborhoods
+  :: V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Map ExactPoint DraftVertexId
+  -> Either (OverlayError leftLabel rightLabel) [DraftNeighborhood]
+buildDraftNeighborhoods edges pointIds =
+  traverse neighborhood (Map.toAscList pointIds)
+ where
+  neighborhood (centerPoint, centerId) = do
+    let neighbors = Map.findWithDefault Set.empty centerPoint adjacency
+    orderedPoints <-
+      case NonEmpty.nonEmpty (sortBy (compareAround centerPoint) (Set.toList neighbors)) of
+        Just points -> Right points
+        Nothing -> Left (OverlayArrangementInvalid (OverlayRotationDegenerate centerPoint))
+    orderedIds <- traverse (requireDraftVertex pointIds) orderedPoints
+    pure (DraftNeighborhood centerId orderedIds)
+  adjacency =
+    V.foldl'
+      (\graph edge ->
+         Map.insertWith Set.union (atomicEdgeTo edge) (Set.singleton (atomicEdgeFrom edge))
+           (Map.insertWith Set.union (atomicEdgeFrom edge) (Set.singleton (atomicEdgeTo edge)) graph))
+      Map.empty
+      edges
+
+compareAround :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
+compareAround center left right =
+  case
+    compareExactVectorAngle
+      (exactVectorFromPoints center left)
+      (exactVectorFromPoints center right) of
+    EQ -> compare left right
+    ordering -> ordering
+
+-- | Normalization splits every source at every exact event and coalesces
+-- duplicate intervals. Distinct atomics can therefore meet only at a stored
+-- endpoint; their entire relation section is the endpoint-incidence index.
+atomicEndpointRelations
+  :: V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
+atomicEndpointRelations edges =
+  Map.fromList
+    [ ( orderedPair
+          (ExactSweepSegmentId leftIndex)
+          (ExactSweepSegmentId rightIndex)
+      , SegmentsShareEndpoint
+      )
+    | incident <- Map.elems incidenceByPoint
+    , (leftIndex, rightIndex) <- unorderedPairs (Set.toAscList incident)
+    ]
+ where
+  incidenceByPoint =
+    V.ifoldl'
+      (\incidence index edge ->
+         Map.insertWith Set.union (atomicEdgeTo edge) (Set.singleton index)
+           ( Map.insertWith Set.union
+               (atomicEdgeFrom edge)
+               (Set.singleton index)
+               incidence
+           ))
+      Map.empty
+      edges
+projectedExactPoints
+  :: LocalEmbeddingCertificate
+  -> Map DraftVertexId ExactPoint
+projectedExactPoints =
+  Map.map exactPointFromQueryPoint . certificateRoundedVertices
+
+projectedAtomicSegments
+  :: V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Map ExactPoint DraftVertexId
+  -> Map DraftVertexId ExactPoint
+  -> Either (OverlayError leftLabel rightLabel) (V.Vector ExactSegment)
+projectedAtomicSegments edges pointIds projected =
+  V.imapM project edges
+ where
+  project segmentIndex edge = do
+    fromId <- requireDraftVertex pointIds (atomicEdgeFrom edge)
+    toId <- requireDraftVertex pointIds (atomicEdgeTo edge)
+    from <- requireProjected fromId
+    to <- requireProjected toId
+    first
+      (\_ ->
+         OverlayEmbeddingRefused
+           ( ProjectedSegmentCollapsed
+               (DraftId segmentIndex)
+               fromId
+               toId
+               :| []
+           ))
+      (exactSegment from to)
+  requireProjected vertex =
+    case Map.lookup vertex projected of
+      Just point -> Right point
+      Nothing ->
+        Left
+          ( OverlayEmbeddingRefused
+              (DraftReferenceMissing (DraftVertexReference vertex) :| [])
+          )
+
+dischargeGlobalRelations
+  :: Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
+  -> ExactSegmentEventPlan
+  -> Either (OverlayError leftLabel rightLabel) ()
+dischargeGlobalRelations exactRelations projectedPlan =
+  case NonEmpty.nonEmpty obstructions of
+    Nothing -> Right ()
+    Just failures -> Left (OverlayEmbeddingRefused failures)
+ where
+  projectedRelations = exactSegmentRelationMap projectedPlan
+  keys = Set.toAscList (Map.keysSet exactRelations <> Map.keysSet projectedRelations)
+  obstructions = concatMap compareRelation keys
+  compareRelation key@(ExactSweepSegmentId leftId, ExactSweepSegmentId rightId) =
+    case (Map.lookup key exactRelations, Map.lookup key projectedRelations) of
+      (Nothing, Just projected) ->
+        [GlobalRelationAdded (DraftId leftId) (DraftId rightId) projected]
+      (Just exact, Nothing) ->
+        [GlobalRelationRemoved (DraftId leftId) (DraftId rightId) exact]
+      (Just exact, Just projected)
+        | exact /= projected ->
+            [GlobalRelationChanged (DraftId leftId) (DraftId rightId) exact projected]
+      _ -> []
+
+overlayVertexSeeds
+  :: Map ExactPoint DraftVertexId
+  -> Map ExactPoint OverlayVertexOrigin
+  -> LocalEmbeddingCertificate
+  -> Either (OverlayError leftLabel rightLabel) (V.Vector OverlayVertexSeed)
+overlayVertexSeeds pointIds origins certificate =
+  V.fromList
+    <$> traverse
+      (\(point, vertexId) -> do
+         embedded <-
+           case Map.lookup vertexId (certificateRoundedVertices certificate) of
+             Just value -> Right value
+             Nothing ->
+               Left
+                 ( OverlayEmbeddingRefused
+                     (DraftReferenceMissing (DraftVertexReference vertexId) :| [])
+                 )
+         origin <-
+           case Map.lookup point origins of
+             Just value -> Right value
+             Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
+         pure (OverlayVertexSeed point (queryPointValue embedded) origin))
+      (Map.toAscList pointIds)
+
+atomicConstraints
+  :: Map ExactPoint DraftVertexId
+  -> V.Vector (AtomicEdge leftLabel rightLabel)
+  -> Either (OverlayError leftLabel rightLabel) (V.Vector (Int, Int))
+atomicConstraints pointIds =
+  V.mapM
+    (\edge -> do
+       DraftId from <- requireDraftVertex pointIds (atomicEdgeFrom edge)
+       DraftId to <- requireDraftVertex pointIds (atomicEdgeTo edge)
+       pure (from, to))
+
+
+canonicalEdgeKey :: ExactPoint -> ExactPoint -> ExactEdgeKey
+canonicalEdgeKey = orderedPair
+
+atomicEdgeFrom :: AtomicEdge leftLabel rightLabel -> ExactPoint
+atomicEdgeFrom = fst . exactSegmentEndpoints . atomicEdgeSegment
+
+atomicEdgeTo :: AtomicEdge leftLabel rightLabel -> ExactPoint
+atomicEdgeTo = snd . exactSegmentEndpoints . atomicEdgeSegment
+
+atomicKey :: AtomicEdge leftLabel rightLabel -> ExactEdgeKey
+atomicKey edge = (atomicEdgeFrom edge, atomicEdgeTo edge)
diff --git a/src-public/Moonlight/Triangulation/Internal/Overlay/Embedding.hs b/src-public/Moonlight/Triangulation/Internal/Overlay/Embedding.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Internal/Overlay/Embedding.hs
@@ -0,0 +1,539 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Bounded certification of local binary64 embedding obligations for a
+-- declared exact arrangement draft.
+module Moonlight.Triangulation.Internal.Overlay.Embedding
+  ( DraftId (..)
+  , DraftVertexId
+  , DraftSegmentId
+  , DraftSourceId
+  , DraftIncidence (..)
+  , DraftNeighborhood (..)
+  , ExactArrangementDraft (..)
+  , DraftReference (..)
+  , OverlayEmbeddingObstruction (..)
+  , EmbeddingObligation (..)
+  , EmbeddingResidual
+  , residualUndischargedObligations
+  , milestoneOneResidual
+  , LocalEmbeddingCertificate (..)
+  , certifyLocalEmbedding
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.List (sort, tails)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import GHC.Generics (Generic)
+import Moonlight.Triangulation.Exact
+  ( ExactPoint
+  , SegmentRelation
+  , exactPointToEmbeddingCandidate
+  , exactSegmentRelation
+  )
+import Moonlight.Triangulation.Internal.BoundaryCycle (cyclePairs)
+import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
+import qualified Moonlight.Triangulation.Math as Math
+  ( orient2d
+  , segmentRelation
+  )
+import Moonlight.Triangulation.Types
+  ( Point (..)
+  , PointValidationError
+  , QueryPoint
+  , queryPointValue
+  )
+
+-- | Phantom draft kind; three incompatible identifiers share one scalar owner.
+data DraftEntity = DraftVertexEntity | DraftSegmentEntity | DraftSourceEntity
+
+newtype DraftId (entity :: DraftEntity) = DraftId Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+type DraftVertexId = DraftId 'DraftVertexEntity
+type DraftSegmentId = DraftId 'DraftSegmentEntity
+type DraftSourceId = DraftId 'DraftSourceEntity
+
+-- | A declared relation between two atomic draft segments.
+data DraftIncidence = DraftIncidence
+  { -- | First declared atomic segment.
+    draftIncidenceFirstSegment :: !DraftSegmentId
+  , -- | Second declared atomic segment.
+    draftIncidenceSecondSegment :: !DraftSegmentId
+  , -- | Relation declared to hold in both exact and rounded geometry.
+    draftIncidenceRelation :: !SegmentRelation
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One exact vertex and the cyclic neighbor order declared around it.
+data DraftNeighborhood = DraftNeighborhood
+  { -- | Center of the declared local rotation.
+    draftNeighborhoodCenter :: !DraftVertexId
+  , -- | Neighbors in cyclic rotation order.
+    draftNeighborhoodNeighbors :: !(NonEmpty DraftVertexId)
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A hand-built exact arrangement draft. It contains only declared local
+-- structure; no arrangement or global crossing search is derived here.
+data ExactArrangementDraft = ExactArrangementDraft
+  { -- | Exact coordinates keyed by draft-local vertex identity.
+    draftVertices :: !(Map DraftVertexId ExactPoint)
+  , -- | Atomic segment endpoint identities.
+    draftSegments :: !(Map DraftSegmentId (DraftVertexId, DraftVertexId))
+  , -- | Exact split parameters and vertices in source-segment order.
+    draftSourceMemberships :: !(Map DraftSourceId [(ExactRational, DraftVertexId)])
+  , -- | Segment relations declared to remain invariant under projection.
+    draftIncidences :: ![DraftIncidence]
+  , -- | Cyclic local rotations declared to retain their neighbor order.
+    draftNeighborhoods :: ![DraftNeighborhood]
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The typed identity of a missing draft reference.
+data DraftReference
+  = DraftVertexReference !DraftVertexId
+  | DraftSegmentReference !DraftSegmentId
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A witness that prevents local embedding certification.
+data OverlayEmbeddingObstruction
+  = DraftReferenceMissing !DraftReference
+  | VertexProjectionRefused !DraftVertexId !PointValidationError
+  | RoundedVerticesCollide !DraftVertexId !DraftVertexId !Point
+  | SplitOrderNotPreserved !DraftSourceId !DraftVertexId !DraftVertexId
+  | IncidenceRelationChanged
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+      !SegmentRelation
+      !SegmentRelation
+  | NeighborhoodRotationChanged
+      !DraftVertexId
+      !(NonEmpty DraftVertexId)
+      !(NonEmpty DraftVertexId)
+  | GlobalRelationAdded
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+  | GlobalRelationRemoved
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+  | GlobalRelationChanged
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+      !SegmentRelation
+  | ProjectedSegmentCollapsed
+      !DraftSegmentId
+      !DraftVertexId
+      !DraftVertexId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | An embedding obligation not discharged by the bounded local certifier.
+data EmbeddingObligation
+  = GlobalNoNewCrossing
+  deriving stock (Bounded, Enum, Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A nonempty collection of obligations deferred to a later owner.
+newtype EmbeddingResidual = EmbeddingResidual (NonEmpty EmbeddingObligation)
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Read the obligations that remain explicitly undischarged.
+residualUndischargedObligations
+  :: EmbeddingResidual
+  -> NonEmpty EmbeddingObligation
+residualUndischargedObligations (EmbeddingResidual obligations) = obligations
+
+-- | The Milestone 1 residual: the arrangement sweep has not yet proved global
+-- absence of new crossings.
+milestoneOneResidual :: EmbeddingResidual
+milestoneOneResidual = EmbeddingResidual (GlobalNoNewCrossing :| [])
+
+-- | A certificate for exactly four local obligations on a declared draft:
+-- vertex distinctness, source split order, declared incidences, and local
+-- neighborhood rotation. Global absence of new crossings is unproved until
+-- the Milestone 2 arrangement sweep supplies the complete obligation set.
+data LocalEmbeddingCertificate = LocalEmbeddingCertificate
+  { -- | Number of rounded vertex-pair distinctness checks discharged.
+    certificateRoundedVertexDistinctnessCount :: !Int
+  , -- | Number of adjacent source split-order checks discharged.
+    certificateSplitOrderPreservationCount :: !Int
+  , -- | Number of declared incidence checks discharged.
+    certificateIncidenceRelationPreservationCount :: !Int
+  , -- | Number of neighborhood rotation entries certified.
+    certificateNeighborhoodRotationPreservationCount :: !Int
+  , -- | The admitted candidate projection certified by the four local obligations.
+    certificateRoundedVertices :: !(Map DraftVertexId QueryPoint)
+  , -- | The necessarily nonempty global obligation residual.
+    certificateResidual :: !EmbeddingResidual
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Certify the four bounded local embedding obligations, collecting every
+-- witness within each obligation and preserving obligation order.
+certifyLocalEmbedding
+  :: ExactArrangementDraft
+  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
+certifyLocalEmbedding draft =
+  case NonEmpty.nonEmpty (structuralObstructions draft) of
+    Just obstructions -> Left obstructions
+    Nothing ->
+      case projectVertices (draftVertices draft) of
+        Invalid obstructions -> Left obstructions
+        Valid projectedVertices ->
+          certifyProjectedDraft draft projectedVertices
+
+data Validation value
+  = Invalid !(NonEmpty OverlayEmbeddingObstruction)
+  | Valid value
+
+instance Functor Validation where
+  fmap _ (Invalid obstructions) = Invalid obstructions
+  fmap transform (Valid value) = Valid (transform value)
+
+instance Applicative Validation where
+  pure = Valid
+  Invalid left <*> Invalid right = Invalid (left <> right)
+  Invalid obstructions <*> Valid _ = Invalid obstructions
+  Valid _ <*> Invalid obstructions = Invalid obstructions
+  Valid transform <*> Valid value = Valid (transform value)
+
+invalid :: OverlayEmbeddingObstruction -> Validation value
+invalid obstruction = Invalid (obstruction :| [])
+
+data ProjectedVertex = ProjectedVertex
+  { projectedExactPoint :: !ExactPoint
+  , projectedRoundedPoint :: !QueryPoint
+  }
+
+data ResolvedSegment = ResolvedSegment
+  { resolvedSegmentId :: !DraftSegmentId
+  , resolvedSegmentFrom :: !ResolvedVertex
+  , resolvedSegmentTo :: !ResolvedVertex
+  }
+
+data ResolvedVertex = ResolvedVertex
+  { resolvedVertexId :: !DraftVertexId
+  , resolvedProjectedVertex :: !ProjectedVertex
+  }
+
+data ResolvedMembership = ResolvedMembership
+  { resolvedMembershipParameter :: !ExactRational
+  , resolvedMembershipVertex :: !ResolvedVertex
+  }
+
+structuralObstructions
+  :: ExactArrangementDraft
+  -> [OverlayEmbeddingObstruction]
+structuralObstructions draft =
+  segmentEndpointObstructions
+    <> sourceMembershipObstructions
+    <> incidenceObstructions
+    <> neighborhoodObstructions
+ where
+  vertices = draftVertices draft
+  segments = draftSegments draft
+  missingVertex vertexId =
+    [ DraftReferenceMissing (DraftVertexReference vertexId)
+    | Map.notMember vertexId vertices
+    ]
+  missingSegment segmentId =
+    [ DraftReferenceMissing (DraftSegmentReference segmentId)
+    | Map.notMember segmentId segments
+    ]
+  segmentEndpointObstructions =
+    concatMap
+      (\(_, (from, to)) -> missingVertex from <> missingVertex to)
+      (Map.toAscList segments)
+  sourceMembershipObstructions =
+    concatMap
+      (concatMap (missingVertex . snd) . snd)
+      (Map.toAscList (draftSourceMemberships draft))
+  incidenceObstructions =
+    concatMap
+      ( \incidence ->
+          missingSegment (draftIncidenceFirstSegment incidence)
+            <> missingSegment (draftIncidenceSecondSegment incidence)
+      )
+      (draftIncidences draft)
+  neighborhoodObstructions =
+    concatMap
+      ( \neighborhood ->
+          missingVertex (draftNeighborhoodCenter neighborhood)
+            <> concatMap missingVertex (draftNeighborhoodNeighbors neighborhood)
+      )
+      (draftNeighborhoods draft)
+
+projectVertices
+  :: Map DraftVertexId ExactPoint
+  -> Validation (Map DraftVertexId ProjectedVertex)
+projectVertices =
+  Map.traverseWithKey
+    ( \vertexId point ->
+        case exactPointToEmbeddingCandidate point of
+          Left projectionError ->
+            invalid (VertexProjectionRefused vertexId projectionError)
+          Right roundedPoint -> Valid (ProjectedVertex point roundedPoint)
+    )
+
+certifyProjectedDraft
+  :: ExactArrangementDraft
+  -> Map DraftVertexId ProjectedVertex
+  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
+certifyProjectedDraft draft projectedVertices =
+  case resolvedFailures of
+    Left obstruction -> Left (obstruction :| [])
+    Right localFailures ->
+      case NonEmpty.nonEmpty (collisionFailures <> localFailures) of
+        Just obstructions -> Left obstructions
+        Nothing ->
+          Right
+            LocalEmbeddingCertificate
+              { certificateRoundedVertexDistinctnessCount = distinctnessCount
+              , certificateSplitOrderPreservationCount = splitOrderCount
+              , certificateIncidenceRelationPreservationCount = incidenceCount
+              , certificateNeighborhoodRotationPreservationCount = neighborhoodCount
+              , certificateRoundedVertices = Map.map projectedRoundedPoint projectedVertices
+              , certificateResidual = milestoneOneResidual
+              }
+ where
+  collisionFailures = roundedVertexCollisionObstructions projectedVertices
+  resolvedFailures = do
+    splitFailures <- traverse (uncurry resolveSource) (Map.toAscList (draftSourceMemberships draft))
+    incidenceFailures <- traverse resolveIncidence (draftIncidences draft)
+    neighborhoodFailures <- traverse resolveNeighborhood (draftNeighborhoods draft)
+    pure (concat splitFailures <> concat incidenceFailures <> concat neighborhoodFailures)
+  resolveVertex vertexId =
+    case Map.lookup vertexId projectedVertices of
+      Nothing -> Left (DraftReferenceMissing (DraftVertexReference vertexId))
+      Just projectedVertex -> Right (ResolvedVertex vertexId projectedVertex)
+  resolveSegment segmentId =
+    case Map.lookup segmentId (draftSegments draft) of
+      Nothing -> Left (DraftReferenceMissing (DraftSegmentReference segmentId))
+      Just (from, to) ->
+        ResolvedSegment segmentId
+          <$> resolveVertex from
+          <*> resolveVertex to
+  resolveSource sourceId memberships =
+    splitOrderObstructions sourceId
+      <$> traverse
+        ( \(parameter, vertexId) ->
+            ResolvedMembership parameter <$> resolveVertex vertexId
+        )
+        memberships
+  resolveIncidence incidence =
+    incidenceRelationObstructions
+      (draftIncidenceRelation incidence)
+      <$> resolveSegment (draftIncidenceFirstSegment incidence)
+      <*> resolveSegment (draftIncidenceSecondSegment incidence)
+  resolveNeighborhood neighborhood =
+    neighborhoodRotationObstructions
+      <$> resolveVertex (draftNeighborhoodCenter neighborhood)
+      <*> traverse resolveVertex (draftNeighborhoodNeighbors neighborhood)
+  vertexCount = Map.size projectedVertices
+  distinctnessCount = vertexCount * (vertexCount - 1) `quot` 2
+  splitOrderCount =
+    sum
+      ( map
+          (max 0 . subtract 1 . length)
+          (Map.elems (draftSourceMemberships draft))
+      )
+  incidenceCount = length (draftIncidences draft)
+  neighborhoodCount =
+    sum
+      ( map
+          (length . draftNeighborhoodNeighbors)
+          (draftNeighborhoods draft)
+      )
+
+roundedVertexCollisionObstructions
+  :: Map DraftVertexId ProjectedVertex
+  -> [OverlayEmbeddingObstruction]
+roundedVertexCollisionObstructions projectedVertices =
+  [ RoundedVerticesCollide leftId rightId (queryPointValue roundedPoint)
+  | (roundedPoint, vertexIds) <- Map.toAscList verticesByRoundedPoint
+  , (leftId : remainingIds) <- tails (sort vertexIds)
+  , rightId <- remainingIds
+  ]
+ where
+  verticesByRoundedPoint =
+    Map.fromListWith (<>)
+      [ (projectedRoundedPoint projectedVertex, [vertexId])
+      | (vertexId, projectedVertex) <- Map.toAscList projectedVertices
+      ]
+
+splitOrderObstructions
+  :: DraftSourceId
+  -> [ResolvedMembership]
+  -> [OverlayEmbeddingObstruction]
+splitOrderObstructions sourceId memberships =
+  case memberships of
+    firstMembership : secondMembership : remainingMemberships ->
+      let finalMembership =
+            List.foldl' (\_ current -> current) secondMembership remainingMemberships
+          sourceFrom = roundedMembershipPoint firstMembership
+          sourceTo = roundedMembershipPoint finalMembership
+       in [ SplitOrderNotPreserved
+              sourceId
+              (resolvedVertexId (resolvedMembershipVertex leftMembership))
+              (resolvedVertexId (resolvedMembershipVertex rightMembership))
+          | (leftMembership, rightMembership) <-
+              zip
+                memberships
+                (drop 1 memberships)
+          , compare
+              (resolvedMembershipParameter leftMembership)
+              (resolvedMembershipParameter rightMembership)
+              /= roundedOrderAlong
+                sourceFrom
+                sourceTo
+                (roundedMembershipPoint leftMembership)
+                (roundedMembershipPoint rightMembership)
+          ]
+    _ -> []
+
+roundedMembershipPoint :: ResolvedMembership -> Point
+roundedMembershipPoint =
+  queryPointValue
+    . projectedRoundedPoint
+    . resolvedProjectedVertex
+    . resolvedMembershipVertex
+
+roundedOrderAlong :: Point -> Point -> Point -> Point -> Ordering
+roundedOrderAlong
+  (Point sourceFromX sourceFromY)
+  (Point sourceToX sourceToY)
+  (Point leftX leftY)
+  (Point rightX rightY) =
+    let directionX = sourceToX - sourceFromX
+        directionY = sourceToY - sourceFromY
+     in if abs directionX >= abs directionY
+          then
+            if directionX >= 0
+              then compare leftX rightX
+              else compare rightX leftX
+          else
+            if directionY >= 0
+              then compare leftY rightY
+              else compare rightY leftY
+
+incidenceRelationObstructions
+  :: SegmentRelation
+  -> ResolvedSegment
+  -> ResolvedSegment
+  -> [OverlayEmbeddingObstruction]
+incidenceRelationObstructions declaredRelation firstSegment secondSegment =
+  [ IncidenceRelationChanged
+      (resolvedSegmentId firstSegment)
+      (resolvedSegmentId secondSegment)
+      declaredRelation
+      exactRelation
+      roundedRelation
+ | exactRelation /= declaredRelation || roundedRelation /= declaredRelation
+  ]
+ where
+  exactRelation =
+    relationFor exactSegmentRelation projectedExactPoint firstSegment secondSegment
+  roundedRelation =
+    relationFor Math.segmentRelation roundedProjectedPoint firstSegment secondSegment
+
+relationFor
+  :: (point -> point -> point -> point -> SegmentRelation)
+  -> (ProjectedVertex -> point)
+  -> ResolvedSegment
+  -> ResolvedSegment
+  -> SegmentRelation
+relationFor relation project firstSegment secondSegment =
+  relation
+    (project (resolvedProjectedVertex (resolvedSegmentFrom firstSegment)))
+    (project (resolvedProjectedVertex (resolvedSegmentTo firstSegment)))
+    (project (resolvedProjectedVertex (resolvedSegmentFrom secondSegment)))
+    (project (resolvedProjectedVertex (resolvedSegmentTo secondSegment)))
+
+neighborhoodRotationObstructions
+  :: ResolvedVertex
+  -> NonEmpty ResolvedVertex
+  -> [OverlayEmbeddingObstruction]
+neighborhoodRotationObstructions center exactRotation =
+  [ NeighborhoodRotationChanged
+      (resolvedVertexId center)
+      exactVertexRotation
+      roundedVertexRotation
+  | roundedRotationDescents > 1
+  ]
+ where
+  exactVertexRotation = fmap resolvedVertexId exactRotation
+  roundedRotationDescents =
+    List.foldl'
+      (\descentCount (left, right) ->
+         descentCount
+           + if compareRoundedAround center left right == GT
+               then 1
+               else 0)
+      (0 :: Int)
+      (cyclePairs exactRotation)
+  roundedVertexRotation =
+    fmap resolvedVertexId
+      (NonEmpty.sortBy (compareRoundedAround center) exactRotation)
+
+compareRoundedAround
+  :: ResolvedVertex
+  -> ResolvedVertex
+  -> ResolvedVertex
+  -> Ordering
+compareRoundedAround center left right =
+  compareRoundedAroundWith
+    ( Math.orient2d
+        (roundedVertexPoint center)
+        (roundedVertexPoint left)
+        (roundedVertexPoint right)
+    )
+    center
+    left
+    right
+
+compareRoundedAroundWith
+  :: Ordering
+  -> ResolvedVertex
+  -> ResolvedVertex
+  -> ResolvedVertex
+  -> Ordering
+compareRoundedAroundWith roundedOrientation center left right =
+  case compare (roundedVectorHalf center left) (roundedVectorHalf center right) of
+    EQ ->
+      case roundedOrientation of
+        GT -> LT
+        LT -> GT
+        EQ -> compare (roundedVertexPoint left) (roundedVertexPoint right)
+    ordering -> ordering
+
+roundedVectorHalf :: ResolvedVertex -> ResolvedVertex -> Bool
+roundedVectorHalf center neighbor =
+  let Point centerX centerY = roundedVertexPoint center
+      Point neighborX neighborY = roundedVertexPoint neighbor
+      deltaX = neighborX - centerX
+      deltaY = neighborY - centerY
+   in deltaY < 0 || (deltaY == 0 && deltaX < 0)
+
+roundedVertexPoint :: ResolvedVertex -> Point
+roundedVertexPoint = roundedProjectedPoint . resolvedProjectedVertex
+
+roundedProjectedPoint :: ProjectedVertex -> Point
+roundedProjectedPoint = queryPointValue . projectedRoundedPoint
diff --git a/src-public/Moonlight/Triangulation/Internal/Overlay/Resident.hs b/src-public/Moonlight/Triangulation/Internal/Overlay/Resident.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Internal/Overlay/Resident.hs
@@ -0,0 +1,923 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Resident DCEL descent and exact-cell gluing. The certified arrangement is
+-- authoritative at this seam; diagonal schedules may change only its resident
+-- triangulation, never its exact cell descriptors.
+module Moonlight.Triangulation.Internal.Overlay.Resident
+  ( OverlayDiagonalSchedule (..)
+  , residentOverlay
+  , faceLabels
+  , faceCarriesExactArea
+  , regionFaceLabels
+  , vertexSupport
+  , edgeSupport
+  ) where
+
+import Control.Monad (foldM)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import Data.List (partition, sort, sortBy)
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import Data.Maybe (listToMaybe, mapMaybe)
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as V
+import qualified Moonlight.Triangulation.Dcel as Dcel
+import Moonlight.Triangulation.Dcel
+  ( faceData
+  , faceDirectedEdges
+  , faceVertices
+  , imapUndirectedEdges
+  , incidentFace
+  , isConstraintEdge
+  , numInnerFaces
+  , numVertices
+  , outerFace
+  , undirectedEndpoints
+  , vertexData
+  , vertexOutgoingEdges
+  )
+import Moonlight.Triangulation.Exact (ExactPoint, exactOrient2d)
+import Moonlight.Triangulation.FloodFillIterator
+  ( FaceComponent
+  , componentBoundary
+  , faceComponentFaces
+  , faceComponentsBy
+  )
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( DirectedEdgeId
+  , FaceId
+  , UndirectedEdgeId
+  , VertexId
+  , asUndirected
+  , directedPair
+  , reverseEdge
+  )
+import Moonlight.Triangulation.Handles.Iterators.FixedIterators
+  ( allFaces
+  , innerFaces
+  , undirectedEdges
+  )
+import Moonlight.Triangulation.Internal.BoxedPaged (boxedFromVector)
+import Moonlight.Triangulation.Internal.BoundaryCycle
+  ( consecutivePairs
+  , traceOrientedBoundaryCircuits
+  )
+import Moonlight.Triangulation.Internal.Canonical (canonicalize)
+import Moonlight.Triangulation.Internal.Cdt.Build (constrainedDelaunay)
+import Moonlight.Triangulation.Internal.Cdt.Types (CdtError (..))
+import Moonlight.Triangulation.Internal.Overlay.Arrangement
+  ( ArrangementMetrics (..)
+  , AtomicEdge (..)
+  , atomicEdgeFrom
+  , atomicEdgeTo
+  , CertifiedArrangement (..)
+  , ExactEdgeKey
+  , OverlayVertexSeed (..)
+  , atomicKey
+  , canonicalEdgeKey
+  , compareAround
+  )
+import Moonlight.Triangulation.Internal.Overlay.Types
+import Moonlight.Triangulation.Internal.Region.Publication
+  ( polygonComponentFromBoundaryCoordinates
+  )
+import Moonlight.Triangulation.Internal.Region.Types
+  ( ExactLoop
+  , PolygonComponent
+  , RegionPublicationError (..)
+  )
+import Moonlight.Triangulation.Internal.Representation
+  ( BuildResult (..)
+  , Triangulation (..)
+  )
+import Moonlight.Triangulation.Internal.Types
+  ( ConstraintMode (Constrained)
+  , ElementDefaults (..)
+  )
+import Moonlight.Triangulation.Region (exactLoop)
+
+data OverlayDiagonalSchedule
+  = CanonicalOverlayDiagonals
+  | FlipFirstAdmissibleDiagonal
+  deriving stock (Eq, Ord, Show)
+
+data ComponentDraft leftLabel rightLabel = ComponentDraft
+  { componentDraftLabels :: !(leftLabel, rightLabel)
+  , componentDraftFaces :: !FaceComponent
+  , componentDraftPolygon :: !PolygonComponent
+  , componentDraftTouchesOuter :: !Bool
+  }
+
+residentOverlay
+  :: (Ord leftLabel, Ord rightLabel)
+  => OverlayDiagonalSchedule
+  -> (leftLabel, rightLabel)
+  -> CertifiedArrangement leftLabel rightLabel
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (OverlayResult leftLabel rightLabel)
+residentOverlay diagonalSchedule outsidePair certified = do
+  let atomicVector = certifiedAtomicEdges certified
+      seeds = certifiedVertexSeeds certified
+      constraints = certifiedConstraints certified
+      metrics = certifiedMetrics certified
+      defaults = ElementDefaults () () ()
+  built <- first OverlayBuildFailed (constrainedDelaunay defaults seeds constraints)
+  let resident = buildTriangulation built
+  if numVertices resident == V.length seeds
+    then Right ()
+    else
+      Left
+        ( OverlayProvenanceIncomplete
+            (OverlayEmbeddedVertexCountMismatch (V.length seeds) (numVertices resident))
+        )
+  canonicalResident <-
+    first (OverlayBuildFailed . CdtBuildError) (canonicalize resident)
+  let atomicByKey =
+        Map.fromList
+          [ (atomicKey edge, edge)
+          | edge <- V.toList atomicVector
+          ]
+  validateAtomicConstraints canonicalResident atomicByKey
+  scheduledResident <-
+    applyDiagonalSchedule
+      diagonalSchedule
+      defaults
+      seeds
+      constraints
+      canonicalResident
+  labelledFaces <- labelResidentFaces outsidePair scheduledResident atomicByKey
+  (fullDimensionalFaces, collapsedFaces) <-
+    if Set.null (certifiedInexactEmbeddingPoints certified)
+      then
+        Right
+          ( Set.fromList (innerFaces scheduledResident)
+          , Set.empty
+          )
+      else
+        exactFaceDimensions
+          (certifiedInexactEmbeddingPoints certified)
+          scheduledResident
+  componentDrafts <-
+    residentComponentDrafts
+      scheduledResident
+      labelledFaces
+      fullDimensionalFaces
+  let (unboundedDrafts, boundedDrafts) =
+        partitionDrafts outsidePair componentDrafts
+  numberedBounded <- numberBoundedComponents boundedDrafts
+  let fullCellIdByFace =
+        Map.fromList
+          ( [ (face, OverlayCellId 0)
+            | draftComponent <- unboundedDrafts
+            , face <- faceComponentFaces (componentDraftFaces draftComponent)
+            ]
+              <> [ (face, cellId)
+                 | (cellId, draftComponent) <- numberedBounded
+                 , face <- faceComponentFaces (componentDraftFaces draftComponent)
+                 ]
+          )
+  collapsedCellIds <-
+    descendCollapsedFaces
+      scheduledResident
+      atomicByKey
+      fullCellIdByFace
+      collapsedFaces
+  let cellIdByFace = fullCellIdByFace <> collapsedCellIds
+  unboundedLoops <-
+    unboundedCellBoundaryLoops scheduledResident atomicByKey cellIdByFace
+  let cells =
+        V.fromList
+          ( OverlayCell
+              { overlayCellLeft = fst outsidePair
+              , overlayCellRight = snd outsidePair
+              , overlayCellGeometry = UnboundedOverlayCell unboundedLoops
+              }
+              : [ OverlayCell
+                    { overlayCellLeft = fst (componentDraftLabels draftComponent)
+                    , overlayCellRight = snd (componentDraftLabels draftComponent)
+                    , overlayCellGeometry =
+                        BoundedOverlayCell (componentDraftPolygon draftComponent)
+                    }
+                | (_, draftComponent) <- numberedBounded
+                ]
+          )
+  withFaces <- attachFaceCells collapsedFaces cellIdByFace scheduledResident
+  let withVertices = attachOverlayVertices withFaces
+      withEdges =
+        imapUndirectedEdges
+          OverlayDiagonal
+          (\edge _ ->
+             case Map.lookup (residentEdgeKey withVertices edge) atomicByKey of
+               Nothing -> OverlayDiagonal
+               Just atomic -> OverlayBoundary (atomicEdgeOrigin atomic))
+          withVertices
+      receipt =
+        OverlayReceipt
+          { overlayInputSegments = arrangementInputSegments metrics
+          , overlayRelationEvents = arrangementRelationEvents metrics
+          , overlayExactCrossings = arrangementExactCrossings metrics
+          , overlayOverlapIntervals = arrangementOverlapIntervals metrics
+          , overlayAtomicEdges = V.length atomicVector
+          , overlayOutputVertices = numVertices withEdges
+          , overlayArrangementCells = V.length cells
+          , overlayResidentFaces = numInnerFaces withEdges
+          , overlayEmbeddingCandidates = arrangementEmbeddingCandidates metrics
+          , overlayTotalRelationChecks = arrangementTotalRelationChecks metrics
+          , overlaySweepMaximumHeight = arrangementSweepMaximumHeight metrics
+          }
+  pure
+    OverlayResult
+      { overlayResultTriangulation = withEdges
+      , overlayResultCells = cells
+      , overlayResultOutsideLabels = outsidePair
+      , overlayResultReceipt = receipt
+      }
+
+applyDiagonalSchedule
+  :: OverlayDiagonalSchedule
+  -> ElementDefaults () () ()
+  -> V.Vector OverlayVertexSeed
+  -> V.Vector (Int, Int)
+  -> Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Triangulation 'Constrained OverlayVertexSeed () () ())
+applyDiagonalSchedule CanonicalOverlayDiagonals _ _ _ triangulation =
+  Right triangulation
+applyDiagonalSchedule
+  FlipFirstAdmissibleDiagonal
+  defaults
+  seeds
+  constraints
+  triangulation =
+    case firstAlternativeDiagonal triangulation of
+      Nothing -> Right triangulation
+      Just (from, to) -> do
+        let indexByPoint =
+              Map.fromList
+                [ (seedExactPoint seed, index)
+                | (index, seed) <- V.toList (V.indexed seeds)
+                ]
+        fromIndex <- requireSeedIndex indexByPoint from
+        toIndex <- requireSeedIndex indexByPoint to
+        rebuilt <-
+          first OverlayBuildFailed
+            (constrainedDelaunay defaults seeds (V.snoc constraints (fromIndex, toIndex)))
+        first (OverlayBuildFailed . CdtBuildError)
+          (canonicalize (buildTriangulation rebuilt))
+
+requireSeedIndex
+  :: Map ExactPoint Int
+  -> ExactPoint
+  -> Either (OverlayError leftLabel rightLabel) Int
+requireSeedIndex indexByPoint point =
+  case Map.lookup point indexByPoint of
+    Just index -> Right index
+    Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
+
+firstAlternativeDiagonal
+  :: Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Maybe ExactEdgeKey
+firstAlternativeDiagonal triangulation =
+  listToMaybe
+    (mapMaybe (alternativeDiagonal triangulation) (undirectedEdges triangulation))
+
+alternativeDiagonal
+  :: Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> UndirectedEdgeId
+  -> Maybe ExactEdgeKey
+alternativeDiagonal triangulation edge
+  | isConstraintEdge triangulation edge = Nothing
+  | leftFace == outerFace || rightFace == outerFace = Nothing
+  | exactOrient2d c d b == GT && exactOrient2d d c a == GT =
+      Just (canonicalEdgeKey c d)
+  | otherwise = Nothing
+ where
+  (forward, backward) = directedPair edge
+  leftFace = incidentFace triangulation forward
+  rightFace = incidentFace triangulation backward
+  a = seedExactPoint (vertexData triangulation (Dcel.origin triangulation forward))
+  b = seedExactPoint (vertexData triangulation (Dcel.origin triangulation backward))
+  c =
+    seedExactPoint
+      (vertexData triangulation (Dcel.origin triangulation (Dcel.previous triangulation forward)))
+  d =
+    seedExactPoint
+      (vertexData triangulation (Dcel.origin triangulation (Dcel.previous triangulation backward)))
+labelResidentFaces
+  :: (Ord leftLabel, Ord rightLabel)
+  => (leftLabel, rightLabel)
+  -> Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Map FaceId (leftLabel, rightLabel))
+labelResidentFaces outsideLabels triangulation atomicByKey = do
+  labelled <-
+    descendFaceTransitions
+      triangulation
+      atomicByKey
+      (Map.singleton outerFace outsideLabels)
+      (Seq.singleton outerFace)
+  case
+    [ face
+    | face <- innerFaces triangulation
+    , Map.notMember face labelled
+    ] of
+    missing : _ ->
+      Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned missing))
+    [] -> Right (Map.delete outerFace labelled)
+
+seedDirectedEndpoints
+  :: Triangulation mode OverlayVertexSeed directed undirected face
+  -> DirectedEdgeId
+  -> ExactEdgeKey
+seedDirectedEndpoints triangulation edge =
+  ( seedExactPoint (vertexData triangulation (Dcel.origin triangulation edge))
+  , seedExactPoint (vertexData triangulation (Dcel.destination triangulation edge))
+  )
+
+swapTransition :: (label, label) -> (label, label)
+swapTransition (fromLabel, toLabel) = (toLabel, fromLabel)
+
+descendFaceTransitions
+  :: (Eq leftLabel, Eq rightLabel)
+  => Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
+  -> Map FaceId (leftLabel, rightLabel)
+  -> Seq.Seq FaceId
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Map FaceId (leftLabel, rightLabel))
+descendFaceTransitions triangulation atomicByKey labelled queued =
+  case Seq.viewl queued of
+    Seq.EmptyL -> Right labelled
+    face Seq.:< remaining ->
+      case Map.lookup face labelled of
+        Nothing ->
+          Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned face))
+        Just current -> do
+          (nextLabels, nextQueue) <-
+            foldM
+              (descendFaceTransition triangulation atomicByKey face current)
+              (labelled, remaining)
+              (faceDirectedEdges triangulation face)
+          descendFaceTransitions triangulation atomicByKey nextLabels nextQueue
+
+descendFaceTransition
+  :: (Eq leftLabel, Eq rightLabel)
+  => Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
+  -> FaceId
+  -> (leftLabel, rightLabel)
+  -> (Map FaceId (leftLabel, rightLabel), Seq.Seq FaceId)
+  -> DirectedEdgeId
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Map FaceId (leftLabel, rightLabel), Seq.Seq FaceId)
+descendFaceTransition triangulation atomicByKey source current (labelled, queued) directed = do
+  let edge = asUndirected directed
+      destinationFace = incidentFace triangulation (reverseEdge directed)
+  (leftTransition, rightTransition) <-
+    case Map.lookup (residentSeedEdgeKey triangulation edge) atomicByKey of
+      Nothing -> Right (Nothing, Nothing)
+      Just atomic -> orientedTransitions edge directed atomic
+  let
+      expected =
+        ( maybe (fst current) fst leftTransition
+        , maybe (snd current) fst rightTransition
+        )
+      derived =
+        ( maybe (fst current) snd leftTransition
+        , maybe (snd current) snd rightTransition
+        )
+  if current == expected
+    then
+      case Map.lookup destinationFace labelled of
+        Nothing ->
+          Right
+            ( Map.insert destinationFace derived labelled
+            , queued Seq.|> destinationFace
+            )
+        Just existing
+          | existing == derived -> Right (labelled, queued)
+          | otherwise ->
+              Left
+                ( OverlayArrangementInvalid
+                    ( OverlayResidentFaceLabelConflict
+                        destinationFace
+                        edge
+                        existing
+                        derived
+                    )
+                )
+    else
+      Left
+        ( OverlayArrangementInvalid
+            (OverlayTransitionSourceMismatch source edge current expected)
+        )
+ where
+  orientedTransitions edge candidateDirection atomic
+    | directedEndpoints == (atomicEdgeFrom atomic, atomicEdgeTo atomic) =
+        Right
+          ( atomicEdgeLeftTransition atomic
+          , atomicEdgeRightTransition atomic
+          )
+    | directedEndpoints == (atomicEdgeTo atomic, atomicEdgeFrom atomic) =
+        Right
+          ( swapTransition <$> atomicEdgeLeftTransition atomic
+          , swapTransition <$> atomicEdgeRightTransition atomic
+          )
+    | otherwise =
+        Left
+          ( OverlayProvenanceIncomplete
+              ( OverlayAtomicConstraintOrientationMismatch
+                  edge
+                  (atomicEdgeFrom atomic)
+                  (atomicEdgeTo atomic)
+              )
+          )
+   where
+    directedEndpoints = seedDirectedEndpoints triangulation candidateDirection
+
+exactFaceDimensions
+  :: Set ExactPoint
+  -> Triangulation 'Constrained OverlayVertexSeed directed undirected face
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Set FaceId, Set FaceId)
+exactFaceDimensions inexactEmbeddingPoints triangulation = do
+  orientations <-
+    traverse
+      ( \face ->
+          (face,)
+            <$> exactFaceOrientation
+              inexactEmbeddingPoints
+              triangulation
+              face
+      )
+      (innerFaces triangulation)
+  case [face | (face, LT) <- orientations] of
+    reversed : _ ->
+      Left
+        ( OverlayArrangementInvalid
+            (OverlayResidentFaceOrientationReversed reversed)
+        )
+    [] ->
+      Right
+        ( Set.fromList [face | (face, GT) <- orientations]
+        , Set.fromList [face | (face, EQ) <- orientations]
+        )
+
+exactFaceOrientation
+  :: Set ExactPoint
+  -> Triangulation mode OverlayVertexSeed directed undirected face
+  -> FaceId
+  -> Either (OverlayError leftLabel rightLabel) Ordering
+exactFaceOrientation inexactEmbeddingPoints triangulation face =
+  case
+    map
+      (seedExactPoint . vertexData triangulation)
+      (faceVertices triangulation face) of
+    points@[firstPoint, secondPoint, thirdPoint]
+      | any (`Set.member` inexactEmbeddingPoints) points ->
+          Right (exactOrient2d firstPoint secondPoint thirdPoint)
+      | otherwise -> Right GT
+    vertices ->
+      Left
+        ( OverlayArrangementInvalid
+            (OverlayResidentFaceArity face (length vertices))
+        )
+
+residentComponentDrafts
+  :: forall leftLabel rightLabel.
+     (Ord leftLabel, Ord rightLabel)
+  => Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Map FaceId (leftLabel, rightLabel)
+  -> Set FaceId
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      [ComponentDraft leftLabel rightLabel]
+residentComponentDrafts triangulation labels fullDimensionalFaces =
+  traverse
+    convert
+    (faceComponentsBy triangulation labelFullDimensionalFace (const True))
+ where
+  exactPointAt :: VertexId -> Either RegionPublicationError ExactPoint
+  exactPointAt vertex =
+    Right (seedExactPoint (vertexData triangulation vertex))
+  labelFullDimensionalFace :: FaceId -> Maybe (leftLabel, rightLabel)
+  labelFullDimensionalFace face
+    | Set.member face fullDimensionalFaces = Map.lookup face labels
+    | otherwise = Nothing
+  convert
+    :: ((leftLabel, rightLabel), FaceComponent)
+    -> Either
+        (OverlayError leftLabel rightLabel)
+        (ComponentDraft leftLabel rightLabel)
+  convert (componentLabels, component) = do
+    boundary <-
+      first
+        (OverlayRegionPublicationFailed . RegionBoundaryObstruction)
+        (componentBoundary triangulation component)
+    polygon <-
+      first OverlayRegionPublicationFailed
+        (polygonComponentFromBoundaryCoordinates exactPointAt boundary)
+    pure
+      ComponentDraft
+        { componentDraftLabels = componentLabels
+        , componentDraftFaces = component
+        , componentDraftPolygon = polygon
+        , componentDraftTouchesOuter = touchesOuter triangulation component
+        }
+
+touchesOuter
+  :: Triangulation mode vertex directed undirected face
+  -> FaceComponent
+  -> Bool
+touchesOuter triangulation component =
+  any
+    ( any ((== outerFace) . incidentFace triangulation . reverseEdge)
+        . faceDirectedEdges triangulation
+    )
+    (faceComponentFaces component)
+
+partitionDrafts
+  :: (Eq leftLabel, Eq rightLabel)
+  => (leftLabel, rightLabel)
+  -> [ComponentDraft leftLabel rightLabel]
+  -> ([ComponentDraft leftLabel rightLabel], [ComponentDraft leftLabel rightLabel])
+partitionDrafts outsidePair =
+  partition
+    (\draftComponent ->
+       componentDraftTouchesOuter draftComponent
+         && componentDraftLabels draftComponent == outsidePair)
+
+sortComponentDrafts
+  :: (Ord leftLabel, Ord rightLabel)
+  => [ComponentDraft leftLabel rightLabel]
+  -> [ComponentDraft leftLabel rightLabel]
+sortComponentDrafts =
+  sortBy
+    (\left right ->
+       compare
+         (componentDraftPolygon left, componentDraftLabels left)
+         (componentDraftPolygon right, componentDraftLabels right))
+
+numberBoundedComponents
+  :: (Ord leftLabel, Ord rightLabel)
+  => [ComponentDraft leftLabel rightLabel]
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      [(OverlayCellId, ComponentDraft leftLabel rightLabel)]
+numberBoundedComponents drafts =
+  case
+    [ componentDraftPolygon left
+    | (left, right) <- consecutivePairs ordered
+    , componentSignature left == componentSignature right
+    ] of
+    duplicate : _ ->
+      Left
+        ( OverlayArrangementInvalid
+            (OverlayDuplicateCellSignature duplicate)
+        )
+    [] ->
+      Right
+        ( zipWith
+            (\index component -> (OverlayCellId index, component))
+            [1 ..]
+            ordered
+        )
+ where
+  ordered = sortComponentDrafts drafts
+  componentSignature
+    :: ComponentDraft leftLabel' rightLabel'
+    -> (PolygonComponent, (leftLabel', rightLabel'))
+  componentSignature component =
+    (componentDraftPolygon component, componentDraftLabels component)
+
+-- | A binary64 triangle whose authoritative vertices are collinear is not an
+-- exact two-cell. It may remain in the resident triangulation only when its
+-- representation diagonals descend uniquely to one full-dimensional exact
+-- cell; this prevents the artifact from gluing exact components through a
+-- zero-area wedge.
+descendCollapsedFaces
+  :: forall leftLabel rightLabel.
+     Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
+  -> Map FaceId OverlayCellId
+  -> Set FaceId
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Map FaceId OverlayCellId)
+descendCollapsedFaces triangulation atomicByKey fullCellIds collapsedFaces =
+  Map.fromList . concat <$> traverse resolveComponent collapsedComponents
+ where
+  exactCellIds = Map.insert outerFace (OverlayCellId 0) fullCellIds
+  collapsedComponents =
+    fmap snd
+      ( faceComponentsBy
+          triangulation
+          labelCollapsedFace
+          isRepresentationDiagonal
+      )
+
+  labelCollapsedFace face
+    | Set.member face collapsedFaces = Just ()
+    | otherwise = Nothing
+
+  isRepresentationDiagonal edge =
+    Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey
+
+  resolveComponent component =
+    case faceComponentFaces component of
+      [] ->
+        Left
+          ( OverlayArrangementInvalid
+              OverlayFaceComponentEmpty
+          )
+      componentFaces@(firstFace : remainingFaces) -> do
+        let componentWitness = firstFace NonEmpty.:| remainingFaces
+            adjacentCellIds =
+              Set.fromList
+                [ cellId
+                | face <- componentFaces
+                , directed <- faceDirectedEdges triangulation face
+                , isRepresentationDiagonal (asUndirected directed)
+                , Just cellId <-
+                    [Map.lookup (incidentFace triangulation (reverseEdge directed)) exactCellIds]
+                ]
+        cellId <-
+          case Set.toAscList adjacentCellIds of
+            [singleCellId] -> Right singleCellId
+            [] ->
+              Left
+                ( OverlayArrangementInvalid
+                    (OverlayCollapsedFacesUnowned componentWitness)
+                )
+            firstCellId : remainingCellIds ->
+              Left
+                ( OverlayArrangementInvalid
+                    ( OverlayCollapsedFacesAmbiguous
+                        componentWitness
+                        (firstCellId NonEmpty.:| remainingCellIds)
+                    )
+                )
+        pure (fmap (,cellId) componentFaces)
+
+-- | Trace the finite boundary cycles of the unbounded exact cell from atomic
+-- edges only. Resident hull edges and Delaunay diagonals are absent by
+-- construction; the DCEL contributes only the already-proved incident-cell
+-- gluing needed to orient each atomic edge with cell zero on its left.
+unboundedCellBoundaryLoops
+  :: Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
+  -> Map FaceId OverlayCellId
+  -> Either (OverlayError leftLabel rightLabel) [ExactLoop]
+unboundedCellBoundaryLoops triangulation atomicByKey cellIdByFace = do
+  orientedEdges <-
+    Set.fromList . concat
+      <$> traverse orientAtomicEdge (undirectedEdges triangulation)
+  traceBoundaryCycles orientedEdges
+ where
+  orientAtomicEdge edge
+    | Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey = Right []
+    | otherwise = do
+        let (forward, backward) = directedPair edge
+        forwardCell <- faceCellId (incidentFace triangulation forward)
+        backwardCell <- faceCellId (incidentFace triangulation backward)
+        pure
+          ( case (forwardCell == OverlayCellId 0, backwardCell == OverlayCellId 0) of
+              (True, False) -> [seedDirectedEndpoints triangulation forward]
+              (False, True) -> [seedDirectedEndpoints triangulation backward]
+              _ -> []
+          )
+  faceCellId face
+    | face == outerFace = Right (OverlayCellId 0)
+    | otherwise =
+        case Map.lookup face cellIdByFace of
+          Just cellId -> Right cellId
+          Nothing -> Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned face))
+
+traceBoundaryCycles
+  :: Set ExactEdgeKey
+  -> Either (OverlayError leftLabel rightLabel) [ExactLoop]
+traceBoundaryCycles orientedEdges = do
+  pointCycles <-
+    traceOrientedBoundaryCircuits
+      fst
+      snd
+      (\seed _ ->
+         OverlayArrangementInvalid
+           (uncurry OverlayCellCycleDidNotClose seed))
+      outgoing
+      orientedEdges
+  sort
+    <$> traverse
+      ( first
+          (OverlayRegionPublicationFailed . RegionValidationObstruction)
+          . exactLoop
+      )
+      pointCycles
+ where
+  outgoing =
+    Map.mapWithKey orderAroundOrigin
+      ( Set.foldl'
+          (\byOrigin edge@(from, _) -> Map.insertWith (<>) from [edge] byOrigin)
+          Map.empty
+          orientedEdges
+      )
+  orderAroundOrigin :: ExactPoint -> [ExactEdgeKey] -> [ExactEdgeKey]
+  orderAroundOrigin origin =
+    sortBy (\(_, left) (_, right) -> compareAround origin left right)
+
+attachFaceCells
+  :: forall leftLabel rightLabel.
+     Set FaceId
+  -> Map FaceId OverlayCellId
+  -> Triangulation 'Constrained OverlayVertexSeed () () ()
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (Triangulation 'Constrained OverlayVertexSeed () () OverlayFace)
+attachFaceCells collapsedFaces cellIdByFace triangulation = do
+  payloads <- V.fromList <$> traverse facePayload (allFaces triangulation)
+  let outerPayload = OverlayCellFace (OverlayCellId 0)
+      defaults = triElementDefaults triangulation
+  pure
+    triangulation
+      { triFaceData = boxedFromVector (Just outerPayload) payloads
+      , triElementDefaults = defaults{defaultFaceData = outerPayload}
+      }
+ where
+  facePayload
+    :: FaceId
+    -> Either (OverlayError leftLabel rightLabel) OverlayFace
+  facePayload face
+    | face == outerFace = Right (OverlayCellFace (OverlayCellId 0))
+    | otherwise =
+        case Map.lookup face cellIdByFace of
+          Just cellId
+            | Set.member face collapsedFaces ->
+                Right (OverlayCollapsedFace cellId)
+            | otherwise -> Right (OverlayCellFace cellId)
+          Nothing ->
+            Left
+              ( OverlayProvenanceIncomplete
+                  (OverlayResidentFaceUnassigned face)
+              )
+
+attachOverlayVertices
+  :: Triangulation 'Constrained OverlayVertexSeed () () face
+  -> Triangulation 'Constrained OverlayVertex () () face
+attachOverlayVertices =
+  Dcel.mapVertices
+    (\seed -> OverlayVertex (seedExactPoint seed) (seedOrigin seed))
+
+cellPayload
+  :: V.Vector (OverlayCell leftLabel rightLabel)
+  -> OverlayCellId
+  -> Either OverlayCellWitness (OverlayCell leftLabel rightLabel)
+cellPayload cells cellId@(OverlayCellId index) =
+  case cells V.!? index of
+    Just cell -> Right cell
+    Nothing -> Left (OverlayCellPayloadMissing cellId)
+
+faceLabels
+  :: OverlayResult leftLabel rightLabel
+  -> FaceId
+  -> Either OverlayCellWitness (leftLabel, rightLabel)
+faceLabels result face = do
+  cell <-
+    cellPayload
+      (overlayResultCells result)
+      (overlayFaceCellId (faceData (overlayResultTriangulation result) face))
+  pure (overlayCellLeft cell, overlayCellRight cell)
+
+-- | Whether a resident triangle has nonzero area under the authoritative
+-- exact coordinate section. Binary64-only wedges are representation charts,
+-- never exact two-cells.
+faceCarriesExactArea
+  :: OverlayResult leftLabel rightLabel
+  -> FaceId
+  -> Bool
+faceCarriesExactArea result face =
+  case faceData (overlayResultTriangulation result) face of
+    OverlayCellFace {} -> True
+    OverlayCollapsedFace {} -> False
+
+regionFaceLabels
+  :: OverlayResult leftLabel rightLabel
+  -> FaceId
+  -> Either RegionPublicationError (leftLabel, rightLabel)
+regionFaceLabels result face =
+  case faceLabels result face of
+    Right labels -> Right labels
+    Left _ -> Left (RegionFaceLabelMissing face)
+
+vertexSupport
+  :: (Ord leftLabel, Ord rightLabel)
+  => OverlayResult leftLabel rightLabel
+  -> VertexId
+  -> Either OverlayCellWitness (OverlayCellSupport leftLabel rightLabel)
+vertexSupport result vertex = do
+  labels <-
+    traverse
+      (faceLabels result . incidentFace triangulation)
+      (vertexOutgoingEdges triangulation vertex)
+  case supportFromPairs labels of
+    Just support -> Right support
+    Nothing -> Left (OverlayVertexSupportMissing vertex)
+ where
+  triangulation = overlayResultTriangulation result
+
+edgeSupport
+  :: (Ord leftLabel, Ord rightLabel)
+  => OverlayResult leftLabel rightLabel
+  -> UndirectedEdgeId
+  -> Either OverlayCellWitness (OverlayCellSupport leftLabel rightLabel)
+edgeSupport result edge = do
+  pairs <- traverse (faceLabels result . incidentFace triangulation) [forward, backward]
+  case supportFromPairs pairs of
+    Just support -> Right support
+    Nothing -> Left (OverlayEdgeSupportMissing edge)
+ where
+  triangulation = overlayResultTriangulation result
+  (forward, backward) = directedPair edge
+
+supportFromPairs
+  :: (Ord leftLabel, Ord rightLabel)
+  => [(leftLabel, rightLabel)]
+  -> Maybe (OverlayCellSupport leftLabel rightLabel)
+supportFromPairs pairs = do
+  let (leftLabels, rightLabels) =
+        List.foldl'
+          (\(left, right) (leftLabel, rightLabel) ->
+             (Set.insert leftLabel left, Set.insert rightLabel right))
+          (Set.empty, Set.empty)
+          pairs
+  leftSupport <- nonEmptySupport leftLabels
+  rightSupport <- nonEmptySupport rightLabels
+  pure (OverlayCellSupport leftSupport rightSupport)
+
+nonEmptySupport :: Set label -> Maybe (OverlaySupport label)
+nonEmptySupport labels = OverlaySupport <$> NonEmpty.nonEmpty (Set.toAscList labels)
+
+validateAtomicConstraints
+  :: Triangulation
+      'Constrained
+      OverlayVertexSeed
+      ()
+      ()
+      ()
+  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
+  -> Either (OverlayError leftLabel rightLabel) ()
+validateAtomicConstraints triangulation atomicByKey = do
+  traverse_ requireAtomic (Map.keys atomicByKey)
+  traverse_ requireExpectedConstraint (undirectedEdges triangulation)
+ where
+  residentByKey =
+    Map.fromList
+      [ (residentSeedEdgeKey triangulation edge, edge)
+      | edge <- undirectedEdges triangulation
+      ]
+  requireAtomic key@(from, to) =
+    case Map.lookup key residentByKey of
+      Nothing -> Left (OverlayProvenanceIncomplete (OverlayAtomicConstraintMissing from to))
+      Just edge
+        | isConstraintEdge triangulation edge -> Right ()
+        | otherwise -> Left (OverlayProvenanceIncomplete (OverlayBoundaryEdgeNotConstrained edge))
+  requireExpectedConstraint edge
+    | isConstraintEdge triangulation edge
+        && Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey =
+        Left (OverlayProvenanceIncomplete (OverlayUnexpectedConstraint edge))
+    | otherwise = Right ()
+
+residentSeedEdgeKey
+  :: Triangulation mode OverlayVertexSeed directed undirected face
+  -> UndirectedEdgeId
+  -> ExactEdgeKey
+residentSeedEdgeKey = residentEdgeKeyBy seedExactPoint
+
+residentEdgeKey
+  :: Triangulation mode OverlayVertex directed undirected face
+  -> UndirectedEdgeId
+  -> ExactEdgeKey
+residentEdgeKey = residentEdgeKeyBy overlayExactPoint
+
+residentEdgeKeyBy
+  :: (vertex -> ExactPoint)
+  -> Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> ExactEdgeKey
+residentEdgeKeyBy exactPointAt triangulation edge =
+  let (from, to) = undirectedEndpoints triangulation edge
+   in canonicalEdgeKey
+        (exactPointAt (vertexData triangulation from))
+        (exactPointAt (vertexData triangulation to))
diff --git a/src-public/Moonlight/Triangulation/Internal/Overlay/Types.hs b/src-public/Moonlight/Triangulation/Internal/Overlay/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Internal/Overlay/Types.hs
@@ -0,0 +1,274 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE KindSignatures #-}
+
+-- | Closed vocabulary and opaque carrier representation for exact labelled
+-- overlay. Construction lives in the public build-tier module; this module
+-- exists so every invariant-bearing payload shares one owner.
+module Moonlight.Triangulation.Internal.Overlay.Types
+  ( BoundaryLoopRef (..)
+  , OverlayOperand (..)
+  , BoundaryRef (..)
+  , BoundaryVertexRef
+  , BoundaryEdgeRef
+  , OverlayVertexOrigin (..)
+  , OverlayEdgeOrigin (..)
+  , OverlaySupport (..)
+  , overlaySupportLabels
+  , OverlayCellSupport (..)
+  , OverlayCellId (..)
+  , OverlayCellGeometry (..)
+  , OverlayCell (..)
+  , OverlayFace (..)
+  , OverlayVertex (..)
+  , OverlayEdge (..)
+  , OverlayReceipt (..)
+  , OverlayArrangementObstruction (..)
+  , OverlayCellWitness (..)
+  , OverlayError (..)
+  , OverlayResult (..)
+  , OverlaySelectionKind (..)
+  , OverlaySelectionError (..)
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Vector (Vector)
+import GHC.Generics (Generic)
+import Moonlight.Triangulation.CellSet (CellSelectionError)
+import Moonlight.Triangulation.Exact
+  ( ExactPoint
+  )
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( FaceId
+  , UndirectedEdgeId
+  , VertexId
+  )
+import Moonlight.Triangulation.Internal.Cdt.Types (CdtError)
+import Moonlight.Triangulation.Internal.ExactRational (ExactArithmeticError)
+import Moonlight.Triangulation.Internal.ExactSegmentEvents (ExactSegmentEventObstruction)
+import Moonlight.Triangulation.Internal.Overlay.Embedding (OverlayEmbeddingObstruction)
+import Moonlight.Triangulation.Internal.Region.Types
+  ( ExactLoop
+  , PolygonComponent
+  , RegionPublicationError
+  )
+import Moonlight.Triangulation.Internal.Representation (Triangulation)
+import Moonlight.Triangulation.Internal.Types
+  ( ConstraintMode (Constrained)
+  )
+
+-- | Which cycle inside a polygon component supplied a source reference.
+data BoundaryLoopRef
+  = BoundaryOuterLoop
+  | BoundaryHoleLoop !Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayOperand
+  = LeftOverlayOperand
+  | RightOverlayOperand
+
+-- | Phantom source cell kind; both provenance axes share one physical payload.
+data BoundaryFeature = BoundaryVertexFeature | BoundaryEdgeFeature
+
+data BoundaryRef (feature :: BoundaryFeature) (operand :: OverlayOperand) = BoundaryRef
+  { boundaryRefComponent :: !Int
+  , boundaryRefLoop :: !BoundaryLoopRef
+  , boundaryRefLocalIndex :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+type BoundaryVertexRef operand = BoundaryRef 'BoundaryVertexFeature operand
+type BoundaryEdgeRef operand = BoundaryRef 'BoundaryEdgeFeature operand
+
+-- | Complete typed source closure of one arrangement vertex.
+data OverlayVertexOrigin = OverlayVertexOrigin
+  { overlayOriginLeftVertices :: ![BoundaryVertexRef 'LeftOverlayOperand]
+  , overlayOriginRightVertices :: ![BoundaryVertexRef 'RightOverlayOperand]
+  , overlayOriginLeftEdges :: ![BoundaryEdgeRef 'LeftOverlayOperand]
+  , overlayOriginRightEdges :: ![BoundaryEdgeRef 'RightOverlayOperand]
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Nonempty typed source-edge provenance of one exact atomic interval.
+data OverlayEdgeOrigin = OverlayEdgeOrigin
+  { overlayEdgeLeftSources :: ![BoundaryEdgeRef 'LeftOverlayOperand]
+  , overlayEdgeRightSources :: ![BoundaryEdgeRef 'RightOverlayOperand]
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Sorted nonempty labels whose closures contain one relatively open cell.
+newtype OverlaySupport label = OverlaySupport (NonEmpty label)
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+overlaySupportLabels :: OverlaySupport label -> NonEmpty label
+overlaySupportLabels (OverlaySupport labels) = labels
+
+data OverlayCellSupport leftLabel rightLabel = OverlayCellSupport
+  { overlaySupportLeft :: !(OverlaySupport leftLabel)
+  , overlaySupportRight :: !(OverlaySupport rightLabel)
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+newtype OverlayCellId = OverlayCellId Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayCellGeometry
+  = BoundedOverlayCell !PolygonComponent
+  | UnboundedOverlayCell ![ExactLoop]
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayCell leftLabel rightLabel = OverlayCell
+  { overlayCellLeft :: !leftLabel
+  , overlayCellRight :: !rightLabel
+  , overlayCellGeometry :: !OverlayCellGeometry
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A resident triangle either realizes an exact two-cell or collapses under
+-- the authoritative exact coordinates. A collapsed triangle descends to the
+-- unique adjacent exact cell through representation diagonals, but exact
+-- region operations must not mistake its binary64 area for exact area.
+data OverlayFace
+  = OverlayCellFace
+      { overlayFaceCellId :: !OverlayCellId
+      }
+  | OverlayCollapsedFace
+      { overlayFaceCellId :: !OverlayCellId
+      }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact geometry and provenance are intrinsic to a vertex. Label support is
+-- the finite union of incident cell descriptors and is therefore a view.
+data OverlayVertex = OverlayVertex
+  { overlayExactPoint :: !ExactPoint
+  , overlayVertexOrigin :: !OverlayVertexOrigin
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Boundary provenance is intrinsic; label support is derived from the two
+-- incident face-cell references.
+data OverlayEdge
+  = OverlayBoundary !OverlayEdgeOrigin
+  | OverlayDiagonal
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayReceipt = OverlayReceipt
+  { overlayInputSegments :: !Int
+  , overlayRelationEvents :: !Int
+  , overlayExactCrossings :: !Int
+  , overlayOverlapIntervals :: !Int
+  , overlayAtomicEdges :: !Int
+  , overlayOutputVertices :: !Int
+  , overlayArrangementCells :: !Int
+  , overlayResidentFaces :: !Int
+  , overlayEmbeddingCandidates :: !Int
+  , overlayTotalRelationChecks :: !Int
+  , overlaySweepMaximumHeight :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayArrangementObstruction leftLabel rightLabel
+  = OverlayLeftSourceSideConflict
+      !OverlayEdgeOrigin
+      !(NonEmpty leftLabel)
+  | OverlayRightSourceSideConflict
+      !OverlayEdgeOrigin
+      !(NonEmpty rightLabel)
+  | OverlayRotationDegenerate !ExactPoint
+  | OverlayFaceComponentEmpty
+  | OverlayCellCycleDidNotClose !ExactPoint !ExactPoint
+  | OverlayTransitionSourceMismatch
+      !FaceId
+      !UndirectedEdgeId
+      !(leftLabel, rightLabel)
+      !(leftLabel, rightLabel)
+  | OverlayResidentFaceLabelConflict
+      !FaceId
+      !UndirectedEdgeId
+      !(leftLabel, rightLabel)
+      !(leftLabel, rightLabel)
+  | OverlayResidentFaceArity !FaceId !Int
+  | OverlayResidentFaceOrientationReversed !FaceId
+  | OverlayCollapsedFacesUnowned !(NonEmpty FaceId)
+  | OverlayCollapsedFacesAmbiguous
+      !(NonEmpty FaceId)
+      !(NonEmpty OverlayCellId)
+  | OverlayDuplicateCellSignature !PolygonComponent
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayCellWitness
+  = OverlayAtomicConstraintMissing !ExactPoint !ExactPoint
+  | OverlayAtomicConstraintOrientationMismatch
+      !UndirectedEdgeId
+      !ExactPoint
+      !ExactPoint
+  | OverlayUnexpectedConstraint !UndirectedEdgeId
+  | OverlayBoundaryEdgeNotConstrained !UndirectedEdgeId
+  | OverlayResidentFaceUnassigned !FaceId
+  | OverlayCellPayloadMissing !OverlayCellId
+  | OverlayVertexSupportMissing !VertexId
+  | OverlayEdgeSupportMissing !UndirectedEdgeId
+  | OverlayEmbeddedVertexCountMismatch !Int !Int
+  | OverlayExactVertexMissing !ExactPoint
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayError leftLabel rightLabel
+  = OverlayExactArithmetic !ExactArithmeticError
+  | OverlaySegmentEventsInvalid !ExactSegmentEventObstruction
+  | OverlayArrangementInvalid !(OverlayArrangementObstruction leftLabel rightLabel)
+  | OverlayEmbeddingRefused !(NonEmpty OverlayEmbeddingObstruction)
+  | OverlayBuildFailed !CdtError
+  | OverlayRegionPublicationFailed !RegionPublicationError
+  | OverlayProvenanceIncomplete !OverlayCellWitness
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The singular exact subdivision carrier. Its embedded DCEL is a derived
+-- binary64 realization; exact coordinates remain in the vertex payload plane.
+data OverlayResult leftLabel rightLabel = OverlayResult
+  { overlayResultTriangulation
+      :: !( Triangulation
+              'Constrained
+              OverlayVertex
+              ()
+              OverlayEdge
+              OverlayFace
+          )
+  , overlayResultCells :: !(Vector (OverlayCell leftLabel rightLabel))
+  , overlayResultOutsideLabels :: !(leftLabel, rightLabel)
+  , overlayResultReceipt :: !OverlayReceipt
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+data OverlaySelectionKind
+  = ClosedUnionSelection
+  | ClosedIntersectionSelection
+  | RegularizedDifferenceSelection
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlaySelectionError
+  = OverlaySelectionContainsUnboundedCell !OverlaySelectionKind
+  | OverlaySelectionProvenance !OverlayCellWitness
+  | OverlaySelectionInvalid !CellSelectionError
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
diff --git a/src-public/Moonlight/Triangulation/Minkowski.hs b/src-public/Moonlight/Triangulation/Minkowski.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Minkowski.hs
@@ -0,0 +1,595 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Exact polygonal Minkowski addition and regularized two-dimensional
+-- morphology. Convex convolution is direct; nonconvex construction descends
+-- through the existing exact overlay, resident CDT, and grouped publication
+-- owners. Lower-dimensional erosion residuals cannot inhabit 'PlanarRegion'
+-- and therefore publish as empty rather than being forged as polygons.
+module Moonlight.Triangulation.Minkowski
+  ( ConvexPolygon
+  , convexPolygon
+  , convexPolygonPoints
+  , StructuringElement
+  , structuringElement
+  , MinkowskiOperation (..)
+  , MinkowskiError (..)
+  , MinkowskiReceipt (..)
+  , convexMinkowskiSum
+  , minkowskiSum
+  , erodeBy
+  , openWith
+  , closeWith
+  , polygonOffset
+  , polygonInset
+  ) where
+
+import Control.Applicative ((<|>))
+import Control.Monad (filterM)
+import Data.Bifunctor (first)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Maybe (fromMaybe)
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Moonlight.Triangulation.Dcel
+  ( faceData
+  , faceVertices
+  , vertexData
+  )
+import Moonlight.Triangulation.Exact
+  ( ExactPoint
+  , exactPointCoordinates
+  )
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( FaceId
+  )
+import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
+import Moonlight.Triangulation.Internal.ExactRational
+  ( ExactRational
+  , exactRationalDenominator
+  , exactRationalNumerator
+  )
+import Moonlight.Triangulation.Internal.BoundaryCycle (cyclePairs)
+import Moonlight.Triangulation.Internal.Dyadic (integerBitLength)
+import Moonlight.Triangulation.Internal.Minkowski.Convex
+  ( addExactPoints
+  , admittedConvexLoop
+  , convexHullPolygon
+  , convexMinkowskiPolygon
+  , convexMinkowskiSum
+  , convexPolygon
+  , convexPolygonCentroid
+  , convexPolygonPoints
+  , convexPolygonRegion
+  , erodeConvexBy
+  , reflectConvexPolygon
+  , structuringElement
+  , structuringElementPolygon
+  )
+import Moonlight.Triangulation.Internal.Minkowski.Types
+import Moonlight.Triangulation.Internal.Overlay.Resident
+  ( faceCarriesExactArea
+  , faceLabels
+  )
+import Moonlight.Triangulation.Internal.Overlay.Types
+  ( OverlayCell (..)
+  , OverlayCellGeometry (..)
+  , OverlayCellId (..)
+  , OverlayFace (..)
+  , OverlayResult (..)
+  , OverlayVertex (..)
+  )
+import Moonlight.Triangulation.Overlay
+  ( OverlayReceipt (..)
+  , overlayLayers
+  , overlayReceipt
+  , overlaySelectedRegion
+  )
+import Moonlight.Triangulation.Region
+  ( PlanarLayer
+  , PlanarRegion
+  , PolygonComponent
+  , RegionPointLocation (..)
+  , emptyPlanarRegion
+  , exactLoopPoints
+  , planarLayerRegions
+  , planarRegionComponents
+  , polygonHoleLoops
+  , polygonOuterLoop
+  , regionPointLocation
+  )
+import Moonlight.Triangulation.Internal.Region.Publication
+  ( labelledPlanarLayerFromExactCoordinates
+  , planarLayerFromAdmittedComponents
+  )
+
+data MorphologyMetrics = MorphologyMetrics
+  { metricOverlayPasses :: !Int
+  , metricExactCrossings :: !Int
+  , metricOutputCells :: !(Maybe Int)
+  }
+
+emptyMetrics :: MorphologyMetrics
+emptyMetrics = MorphologyMetrics 0 0 Nothing
+
+appendMetrics :: MorphologyMetrics -> MorphologyMetrics -> MorphologyMetrics
+appendMetrics left right =
+  MorphologyMetrics
+    { metricOverlayPasses = metricOverlayPasses left + metricOverlayPasses right
+    , metricExactCrossings = metricExactCrossings left + metricExactCrossings right
+    , metricOutputCells = metricOutputCells right <|> metricOutputCells left
+    }
+
+minkowskiSum
+  :: PlanarRegion
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+minkowskiSum left right = do
+  (leftPieces, leftMetrics) <- decomposeRegion left
+  (rightPieces, rightMetrics) <- decomposeRegion right
+  let generated =
+        [ convexMinkowskiPolygon leftPiece rightPiece
+        | leftPiece <- leftPieces
+        , rightPiece <- rightPieces
+        ]
+      generatedRegions = map convexPolygonRegion generated
+      convolutionEdges = sum (map (NonEmpty.length . convexPolygonPoints) generated)
+  (result, unionMetrics) <- unionRegions generatedRegions
+  let metrics = leftMetrics `appendMetrics` rightMetrics `appendMetrics` unionMetrics
+  pure
+    ( result
+    , MinkowskiReceipt
+        { minkowskiOperation = MinkowskiAddition
+        , minkowskiInputComponents =
+            length (planarRegionComponents left)
+              + length (planarRegionComponents right)
+        , minkowskiConvexPieces = length leftPieces + length rightPieces
+        , minkowskiGeneratedPieces = length generated
+        , minkowskiGeneratedConvolutionEdges = convolutionEdges
+        , minkowskiOverlayPasses = metricOverlayPasses metrics
+        , minkowskiExactCrossings = metricExactCrossings metrics
+        , minkowskiOutputCells = fromMaybe 0 (metricOutputCells metrics)
+        , minkowskiExactCoordinateBitGrowth =
+            coordinateBitGrowth [left, right] result
+        }
+    )
+
+-- | Erode a polygonal region by an origin-anchored convex kernel and publish
+-- the regularized full-dimensional result. A residual consisting only of
+-- points or segments is represented by 'emptyPlanarRegion'.
+erodeBy
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+erodeBy element source =
+  case singleConvexRegion source of
+    Just sourcePolygon -> convexErosion element source sourcePolygon
+    Nothing -> generalErosion element source
+
+openWith
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+openWith element source = do
+  (eroded, erosionReceipt) <- erodeBy element source
+  (opened, additionReceipt) <- polygonOffset element eroded
+  pure
+    ( opened
+    , composeReceipts
+        MinkowskiOpening
+        (length (planarRegionComponents source))
+        erosionReceipt
+        additionReceipt
+    )
+
+closeWith
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+closeWith element source = do
+  (expanded, additionReceipt) <- polygonOffset element source
+  (closed, erosionReceipt) <- erodeBy element expanded
+  pure
+    ( closed
+    , composeReceipts
+        MinkowskiClosing
+        (length (planarRegionComponents source))
+        additionReceipt
+        erosionReceipt
+    )
+
+polygonOffset
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+polygonOffset element source =
+  minkowskiSum source (convexPolygonRegion (structuringElementPolygon element))
+
+polygonInset
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+polygonInset = erodeBy
+
+decomposeRegion
+  :: PlanarRegion
+  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
+decomposeRegion region =
+  case traverse convexComponent (planarRegionComponents region) of
+    Just convexPieces -> Right (convexPieces, emptyMetrics)
+    Nothing -> triangulatedPieces region
+
+convexComponent :: PolygonComponent -> Maybe ConvexPolygon
+convexComponent component =
+  case polygonHoleLoops component of
+    [] -> admittedConvexLoop (polygonOuterLoop component)
+    _ -> Nothing
+
+singleConvexRegion :: PlanarRegion -> Maybe ConvexPolygon
+singleConvexRegion region =
+  case planarRegionComponents region of
+    [component] -> convexComponent component
+    _ -> Nothing
+
+triangulatedPieces
+  :: PlanarRegion
+  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
+triangulatedPieces region = do
+  let sourceLayer = morphologyLayer region
+  result <- first MinkowskiOverlayFailed (overlayLayers sourceLayer emptyMorphologyLayer)
+  selectedFaces <-
+    filterM
+      (\face ->
+         if faceCarriesExactArea result face
+           then
+             fmap fst
+               ( first MinkowskiOverlayCellWitness
+                   (faceLabels result face)
+               )
+           else Right False)
+      (innerFaces (overlayResultTriangulation result))
+  pieces <- traverse (faceConvexPolygon result) selectedFaces
+  pure (pieces, metricsFromOverlay result (length pieces))
+
+faceConvexPolygon
+  :: OverlayResult leftLabel rightLabel
+  -> FaceId
+  -> Either MinkowskiError ConvexPolygon
+faceConvexPolygon result face =
+  case
+    map
+      (overlayExactPoint . vertexData triangulation)
+      (faceVertices triangulation face) of
+    [firstPoint, secondPoint, thirdPoint] ->
+      convexHullPolygon (firstPoint :| [secondPoint, thirdPoint])
+    vertices -> Left (MinkowskiFaceArity face (length vertices))
+ where
+  triangulation = overlayResultTriangulation result
+
+unionRegions
+  :: [PlanarRegion]
+  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
+unionRegions [] = Right (emptyPlanarRegion, emptyMetrics)
+unionRegions [region] =
+  Right
+    ( region
+    , emptyMetrics{metricOutputCells = Just (length (planarRegionComponents region))}
+    )
+unionRegions regions = do
+  let (leftRegions, rightRegions) = splitAt (length regions `div` 2) regions
+  left <- unionRegions leftRegions
+  right <- unionRegions rightRegions
+  glueRegionUnion left right
+
+glueRegionUnion
+  :: (PlanarRegion, MorphologyMetrics)
+  -> (PlanarRegion, MorphologyMetrics)
+  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
+glueRegionUnion (left, leftMetrics) (right, rightMetrics)
+  | null (planarRegionComponents left) = Right (right, leftMetrics `appendMetrics` rightMetrics)
+  | null (planarRegionComponents right) = Right (left, leftMetrics `appendMetrics` rightMetrics)
+  | otherwise = do
+      result <-
+        first MinkowskiOverlayFailed
+          (overlayLayers (morphologyLayer left) (morphologyLayer right))
+      published <-
+        first MinkowskiPublicationFailed
+          ( overlaySelectedRegion
+              (uncurry (||))
+              result
+          )
+      let selectedCells =
+            V.foldl'
+              (\count cell ->
+                 case overlayCellGeometry cell of
+                   BoundedOverlayCell _
+                     | overlayCellLeft cell || overlayCellRight cell ->
+                         count + 1
+                   _ -> count)
+              0
+              (overlayResultCells result)
+      pure
+        ( published
+        , leftMetrics
+            `appendMetrics` rightMetrics
+            `appendMetrics` metricsFromOverlay result selectedCells
+        )
+
+convexErosion
+  :: StructuringElement
+  -> PlanarRegion
+  -> ConvexPolygon
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+convexErosion element source sourcePolygon = do
+  eroded <- erodeConvexBy sourcePolygon (structuringElementPolygon element)
+  let result = maybe emptyPlanarRegion convexPolygonRegion eroded
+      outputCells = maybe 0 (const 1) eroded
+      generatedEdges = maybe 0 (NonEmpty.length . convexPolygonPoints) eroded
+  pure
+    ( result
+    , MinkowskiReceipt
+        { minkowskiOperation = MinkowskiErosion
+        , minkowskiInputComponents = 1
+        , minkowskiConvexPieces = 2
+        , minkowskiGeneratedPieces = outputCells
+        , minkowskiGeneratedConvolutionEdges = generatedEdges
+        , minkowskiOverlayPasses = 0
+        , minkowskiExactCrossings = 0
+        , minkowskiOutputCells = outputCells
+        , minkowskiExactCoordinateBitGrowth =
+            coordinateBitGrowth
+              [ source
+              , convexPolygonRegion (structuringElementPolygon element)
+              ]
+              result
+        }
+    )
+
+generalErosion
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+generalErosion element source
+  | null sourceEdges = Right (emptyPlanarRegion, emptyErosionReceipt)
+  | otherwise = do
+      sweptPolygons <- traverse (sweepBoundaryEdge reflectedKernel) sourceEdges
+      let sweptRegions = map convexPolygonRegion sweptPolygons
+          generatedEdges =
+            sum (map (NonEmpty.length . convexPolygonPoints) sweptPolygons)
+      (contactRegion, unionMetrics) <- unionRegions sweptRegions
+      candidateOverlay <-
+        first MinkowskiOverlayFailed
+          (overlayLayers (morphologyLayer contactRegion) emptyMorphologyLayer)
+      kernelWitness <- convexPolygonCentroid kernel
+      let representativeFaceByCell = representativeFaces candidateOverlay
+      selectedCellIds <-
+        Set.fromList
+          <$> filterM
+            ( classifyCandidateCell
+                source
+                kernelWitness
+                representativeFaceByCell
+                candidateOverlay
+            )
+            (boundedOutsideCellIds candidateOverlay)
+      published <- publishCellSelection selectedCellIds candidateOverlay
+      let candidateMetrics =
+            metricsFromOverlay candidateOverlay (Set.size selectedCellIds)
+          metrics = unionMetrics `appendMetrics` candidateMetrics
+      pure
+        ( published
+        , MinkowskiReceipt
+            { minkowskiOperation = MinkowskiErosion
+            , minkowskiInputComponents = length (planarRegionComponents source)
+            , minkowskiConvexPieces = 1
+            , minkowskiGeneratedPieces = length sweptPolygons
+            , minkowskiGeneratedConvolutionEdges = generatedEdges
+            , minkowskiOverlayPasses = metricOverlayPasses metrics
+            , minkowskiExactCrossings = metricExactCrossings metrics
+            , minkowskiOutputCells = Set.size selectedCellIds
+            , minkowskiExactCoordinateBitGrowth =
+                coordinateBitGrowth
+                  [source, convexPolygonRegion kernel]
+                  published
+            }
+        )
+ where
+  kernel = structuringElementPolygon element
+  reflectedKernel = reflectConvexPolygon kernel
+  sourceEdges = regionBoundaryEdges source
+  emptyErosionReceipt =
+    MinkowskiReceipt
+      { minkowskiOperation = MinkowskiErosion
+      , minkowskiInputComponents = 0
+      , minkowskiConvexPieces = 1
+      , minkowskiGeneratedPieces = 0
+      , minkowskiGeneratedConvolutionEdges = 0
+      , minkowskiOverlayPasses = 0
+      , minkowskiExactCrossings = 0
+      , minkowskiOutputCells = 0
+      , minkowskiExactCoordinateBitGrowth = 0
+      }
+
+sweepBoundaryEdge
+  :: ConvexPolygon
+  -> (ExactPoint, ExactPoint)
+  -> Either MinkowskiError ConvexPolygon
+sweepBoundaryEdge reflectedKernel (from, to) =
+  case convexPolygonPoints reflectedKernel of
+    firstKernelPoint :| remainingKernelPoints ->
+      convexHullPolygon
+        ( addExactPoints from firstKernelPoint
+            :| ( map (addExactPoints from) remainingKernelPoints
+                   <> map (addExactPoints to) kernelPoints
+               )
+        )
+ where
+  kernelPoints = NonEmpty.toList (convexPolygonPoints reflectedKernel)
+
+boundedOutsideCellIds
+  :: OverlayResult Bool Bool
+  -> [OverlayCellId]
+boundedOutsideCellIds result =
+  V.ifoldr
+    (\index cell selected ->
+       case overlayCellGeometry cell of
+         BoundedOverlayCell _
+           | not (overlayCellLeft cell)
+               && not (overlayCellRight cell) ->
+               OverlayCellId index : selected
+         _ -> selected)
+    []
+    (overlayResultCells result)
+
+classifyCandidateCell
+  :: PlanarRegion
+  -> ExactPoint
+  -> IntMap.IntMap FaceId
+  -> OverlayResult Bool Bool
+  -> OverlayCellId
+  -> Either MinkowskiError Bool
+classifyCandidateCell source kernelWitness representativeFaceByCell result cellId = do
+  face <-
+    maybe
+      (Left (MinkowskiCandidateCellMissing cellId))
+      Right
+      (IntMap.lookup (overlayCellIndex cellId) representativeFaceByCell)
+  candidate <- convexPolygonCentroid =<< faceConvexPolygon result face
+  let inclusionWitness = addExactPoints candidate kernelWitness
+  case regionPointLocation source inclusionWitness of
+    RegionInterior -> Right True
+    RegionExterior -> Right False
+    RegionOnBoundary -> Left (MinkowskiInclusionAmbiguous cellId inclusionWitness)
+
+representativeFaces
+  :: OverlayResult leftLabel rightLabel
+  -> IntMap.IntMap FaceId
+representativeFaces result =
+  IntMap.fromListWith min
+    [ (overlayCellIndex (overlayFaceCellId (faceData triangulation face)), face)
+    | face <- innerFaces triangulation
+    , faceCarriesExactArea result face
+    ]
+ where
+  triangulation = overlayResultTriangulation result
+
+overlayCellIndex :: OverlayCellId -> Int
+overlayCellIndex (OverlayCellId index) = index
+
+publishCellSelection
+  :: Set.Set OverlayCellId
+  -> OverlayResult leftLabel rightLabel
+  -> Either MinkowskiError PlanarRegion
+publishCellSelection selected result = do
+  layer <-
+    first MinkowskiPublicationFailed
+      ( labelledPlanarLayerFromExactCoordinates
+          False
+          triangulation
+          (\vertex -> Right (overlayExactPoint (vertexData triangulation vertex)))
+          (\face ->
+             if faceCarriesExactArea result face
+               then
+                 Right
+                   ( Set.member
+                       (overlayFaceCellId (faceData triangulation face))
+                       selected
+                   )
+               else Right False)
+      )
+  pure (Map.findWithDefault emptyPlanarRegion True (planarLayerRegions layer))
+ where
+  triangulation = overlayResultTriangulation result
+
+morphologyLayer
+  :: PlanarRegion
+  -> PlanarLayer Bool
+morphologyLayer region =
+  planarLayerFromAdmittedComponents
+    False
+    [(True, component) | component <- planarRegionComponents region]
+
+emptyMorphologyLayer :: PlanarLayer Bool
+emptyMorphologyLayer = morphologyLayer emptyPlanarRegion
+
+regionBoundaryEdges :: PlanarRegion -> [(ExactPoint, ExactPoint)]
+regionBoundaryEdges region =
+  concatMap
+    (\component ->
+       concatMap
+         (cyclePairs . exactLoopPoints)
+         (polygonOuterLoop component : polygonHoleLoops component))
+    (planarRegionComponents region)
+
+metricsFromOverlay
+  :: OverlayResult leftLabel rightLabel
+  -> Int
+  -> MorphologyMetrics
+metricsFromOverlay result outputCells =
+  MorphologyMetrics
+    { metricOverlayPasses = 1
+    , metricExactCrossings = overlayExactCrossings (overlayReceipt result)
+    , metricOutputCells = Just outputCells
+    }
+
+composeReceipts
+  :: MinkowskiOperation
+  -> Int
+  -> MinkowskiReceipt
+  -> MinkowskiReceipt
+  -> MinkowskiReceipt
+composeReceipts operation inputComponents firstReceipt secondReceipt =
+  MinkowskiReceipt
+    { minkowskiOperation = operation
+    , minkowskiInputComponents = inputComponents
+    , minkowskiConvexPieces =
+        minkowskiConvexPieces firstReceipt
+          + minkowskiConvexPieces secondReceipt
+    , minkowskiGeneratedPieces =
+        minkowskiGeneratedPieces firstReceipt
+          + minkowskiGeneratedPieces secondReceipt
+    , minkowskiGeneratedConvolutionEdges =
+        minkowskiGeneratedConvolutionEdges firstReceipt
+          + minkowskiGeneratedConvolutionEdges secondReceipt
+    , minkowskiOverlayPasses =
+        minkowskiOverlayPasses firstReceipt
+          + minkowskiOverlayPasses secondReceipt
+    , minkowskiExactCrossings =
+        minkowskiExactCrossings firstReceipt
+          + minkowskiExactCrossings secondReceipt
+    , minkowskiOutputCells = minkowskiOutputCells secondReceipt
+    , minkowskiExactCoordinateBitGrowth =
+        max
+          (minkowskiExactCoordinateBitGrowth firstReceipt)
+          (minkowskiExactCoordinateBitGrowth secondReceipt)
+    }
+
+coordinateBitGrowth :: [PlanarRegion] -> PlanarRegion -> Int
+coordinateBitGrowth inputs output =
+  max 0
+    ( regionCoordinateBits output
+        - List.foldl' (\maximumBits -> max maximumBits . regionCoordinateBits) 0 inputs
+    )
+
+regionCoordinateBits :: PlanarRegion -> Int
+regionCoordinateBits = List.foldl' componentBits 0 . planarRegionComponents
+ where
+  componentBits maximumBits component =
+    List.foldl'
+      loopBits
+      maximumBits
+      (polygonOuterLoop component : polygonHoleLoops component)
+  loopBits maximumBits =
+    List.foldl' pointBits maximumBits . exactLoopPoints
+  pointBits maximumBits point =
+    let (x, y) = exactPointCoordinates point
+     in max maximumBits (max (rationalBits x) (rationalBits y))
+
+rationalBits :: ExactRational -> Int
+rationalBits value =
+  max
+    (integerBitLength (abs (exactRationalNumerator value)))
+    (integerBitLength (exactRationalDenominator value))
diff --git a/src-public/Moonlight/Triangulation/Overlay.hs b/src-public/Moonlight/Triangulation/Overlay.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Triangulation/Overlay.hs
@@ -0,0 +1,296 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact labelled common refinement. Source boundaries descend through one
+-- exact segment-event plan, glue into canonical atomic constraints, and are
+-- admitted only when their binary64 DCEL realization preserves every exact
+-- relation.
+module Moonlight.Triangulation.Overlay
+  ( BoundaryLoopRef (..)
+  , OverlayOperand
+  , BoundaryRef
+  , BoundaryVertexRef
+  , BoundaryEdgeRef
+  , boundaryRefComponent
+  , boundaryRefLoop
+  , boundaryRefLocalIndex
+  , OverlayVertexOrigin
+  , overlayOriginLeftVertices
+  , overlayOriginRightVertices
+  , overlayOriginLeftEdges
+  , overlayOriginRightEdges
+  , OverlayEdgeOrigin
+  , overlayEdgeLeftSources
+  , overlayEdgeRightSources
+  , OverlaySupport
+  , overlaySupportLabels
+  , OverlayCellSupport (..)
+  , OverlayCellId (..)
+  , OverlayCellGeometry (..)
+  , OverlayCell (..)
+  , OverlayFace (..)
+  , OverlayVertex (..)
+  , OverlayEdge (..)
+  , OverlayReceipt (..)
+  , OverlayArrangementObstruction (..)
+  , OverlayCellWitness (..)
+  , OverlayError (..)
+  , OverlayResult
+  , OverlaySelectionKind (..)
+  , OverlaySelectionError (..)
+  , overlayLayers
+  , overlayEmbeddedTriangulation
+  , overlayReceipt
+  , overlayCells
+  , overlayArrangementVertices
+  , overlayArrangementEdges
+  , overlayPlanarLayer
+  , overlaySelectedRegion
+  , overlayClosedUnion
+  , overlayClosedIntersection
+  , overlayRegularizedDifference
+  ) where
+
+import Data.Bifunctor (first)
+import Control.Monad (filterM)
+import qualified Data.Map.Strict as Map
+import qualified Data.Vector as V
+import Moonlight.Triangulation.CellSet (ExactCellSet)
+import qualified Moonlight.Triangulation.Dcel as Dcel
+import Moonlight.Triangulation.Dcel (vertexData)
+import Moonlight.Triangulation.Handles.HandleDefs
+  ( UndirectedEdgeId
+  , VertexId
+  )
+import Moonlight.Triangulation.Handles.Iterators.FixedIterators
+  ( innerFaces
+  , undirectedEdges
+  , vertices
+  )
+import Moonlight.Triangulation.Internal.CellSet (closeExactCellSetWith)
+import Moonlight.Triangulation.Internal.Overlay.Arrangement
+  ( certifyArrangement
+  )
+import Moonlight.Triangulation.Internal.Overlay.Resident
+  ( OverlayDiagonalSchedule (CanonicalOverlayDiagonals)
+  , edgeSupport
+  , faceCarriesExactArea
+  , faceLabels
+  , regionFaceLabels
+  , residentOverlay
+  , vertexSupport
+  )
+import Moonlight.Triangulation.Internal.Overlay.Types
+import Moonlight.Triangulation.Internal.Representation (Triangulation)
+import Moonlight.Triangulation.Internal.Types
+  ( ConstraintMode (Constrained)
+  )
+import Moonlight.Triangulation.Region
+  ( PlanarLayer
+  , PlanarRegion
+  , RegionPublicationError (..)
+  , emptyPlanarRegion
+  , planarLayerOutsideLabel
+  , planarLayerRegions
+  )
+import Moonlight.Triangulation.Internal.Region.Publication
+  ( labelledPlanarLayerFromExactCoordinates
+  , planarLayerFromAdmittedComponents
+  )
+
+-- | Construct the exact common refinement and its one faithful resident DCEL.
+overlayLayers
+  :: (Ord leftLabel, Ord rightLabel)
+  => PlanarLayer leftLabel
+  -> PlanarLayer rightLabel
+  -> Either
+      (OverlayError leftLabel rightLabel)
+      (OverlayResult leftLabel rightLabel)
+overlayLayers leftLayer rightLayer = do
+  certified <- certifyArrangement leftLayer rightLayer
+  residentOverlay
+    CanonicalOverlayDiagonals
+    (planarLayerOutsideLabel leftLayer, planarLayerOutsideLabel rightLayer)
+    certified
+
+-- | The binary64 realization used by existing DCEL observations. Exact overlay
+-- operations accept 'OverlayResult', never this projection.
+overlayEmbeddedTriangulation
+  :: OverlayResult leftLabel rightLabel
+  -> Triangulation
+      'Constrained
+      OverlayVertex
+      ()
+      OverlayEdge
+      OverlayFace
+overlayEmbeddedTriangulation = overlayResultTriangulation
+
+overlayReceipt :: OverlayResult leftLabel rightLabel -> OverlayReceipt
+overlayReceipt = overlayResultReceipt
+
+overlayCells
+  :: OverlayResult leftLabel rightLabel
+  -> [(OverlayCellId, OverlayCell leftLabel rightLabel)]
+overlayCells result =
+  V.toList
+    (V.imap (\index cell -> (OverlayCellId index, cell)) (overlayResultCells result))
+
+overlayArrangementVertices
+  :: OverlayResult leftLabel rightLabel
+  -> [(VertexId, OverlayVertex)]
+overlayArrangementVertices result =
+  let triangulation = overlayResultTriangulation result
+   in [(vertex, vertexData triangulation vertex) | vertex <- vertices triangulation]
+
+overlayArrangementEdges
+  :: OverlayResult leftLabel rightLabel
+  -> [(UndirectedEdgeId, OverlayEdgeOrigin)]
+overlayArrangementEdges result =
+  let triangulation = overlayResultTriangulation result
+   in [ (edge, origin)
+      | edge <- undirectedEdges triangulation
+      , OverlayBoundary origin <- [Dcel.undirectedEdgeData triangulation edge]
+      ]
+
+-- | Publish the already-admitted bounded cell geometry. The resident DCEL is
+-- a realization of these exact cells, not a second authoring source.
+overlayPlanarLayer
+  :: (Ord leftLabel, Ord rightLabel)
+  => OverlayResult leftLabel rightLabel
+  -> PlanarLayer (leftLabel, rightLabel)
+overlayPlanarLayer result =
+  planarLayerFromAdmittedComponents
+    (overlayResultOutsideLabels result)
+    [ ( (overlayCellLeft cell, overlayCellRight cell)
+      , component
+      )
+    | cell <- V.toList (overlayResultCells result)
+    , (overlayCellLeft cell, overlayCellRight cell)
+        /= overlayResultOutsideLabels result
+    , BoundedOverlayCell component <- [overlayCellGeometry cell]
+    ]
+
+-- | Publish the selected two-dimensional cells. Internal arrangement edges
+-- between differently labelled but jointly selected cells dissolve because
+-- selection precedes component descent.
+overlaySelectedRegion
+  :: ((leftLabel, rightLabel) -> Bool)
+  -> OverlayResult leftLabel rightLabel
+  -> Either RegionPublicationError PlanarRegion
+overlaySelectedRegion selected result
+  | selected (overlayResultOutsideLabels result) = Left RegionUnboundedSelection
+  | otherwise = do
+      published <-
+        labelledPlanarLayerFromExactCoordinates
+          False
+          triangulation
+          exactPointAt
+          labelFace
+      pure (Map.findWithDefault emptyPlanarRegion True (planarLayerRegions published))
+ where
+  triangulation = overlayResultTriangulation result
+  exactPointAt vertex = Right (overlayExactPoint (vertexData triangulation vertex))
+  labelFace face
+    | faceCarriesExactArea result face =
+        selected <$> regionFaceLabels result face
+    | otherwise = Right False
+
+overlayClosedUnion
+  :: (Ord leftLabel, Ord rightLabel)
+  => (leftLabel -> Bool)
+  -> (rightLabel -> Bool)
+  -> OverlayResult leftLabel rightLabel
+  -> Either OverlaySelectionError ExactCellSet
+overlayClosedUnion selectLeft selectRight =
+  selectClosedCells
+    ClosedUnionSelection
+    (\support -> supportAny selectLeft (overlaySupportLeft support) || supportAny selectRight (overlaySupportRight support))
+    (\leftLabel rightLabel -> selectLeft leftLabel || selectRight rightLabel)
+
+overlayClosedIntersection
+  :: (Ord leftLabel, Ord rightLabel)
+  => (leftLabel -> Bool)
+  -> (rightLabel -> Bool)
+  -> OverlayResult leftLabel rightLabel
+  -> Either OverlaySelectionError ExactCellSet
+overlayClosedIntersection selectLeft selectRight =
+  selectClosedCells
+    ClosedIntersectionSelection
+    (\support -> supportAny selectLeft (overlaySupportLeft support) && supportAny selectRight (overlaySupportRight support))
+    (\leftLabel rightLabel -> selectLeft leftLabel && selectRight rightLabel)
+
+overlayRegularizedDifference
+  :: (leftLabel -> Bool)
+  -> (rightLabel -> Bool)
+  -> OverlayResult leftLabel rightLabel
+  -> Either OverlaySelectionError ExactCellSet
+overlayRegularizedDifference selectLeft selectRight result =
+  let outsidePair = overlayResultOutsideLabels result
+      selectFace leftLabel rightLabel = selectLeft leftLabel && not (selectRight rightLabel)
+   in if uncurry selectFace outsidePair
+        then Left (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
+        else closeSelectedCells [] [] selectFace result
+
+selectClosedCells
+  :: (Ord leftLabel, Ord rightLabel)
+  => OverlaySelectionKind
+  -> (OverlayCellSupport leftLabel rightLabel -> Bool)
+  -> (leftLabel -> rightLabel -> Bool)
+  -> OverlayResult leftLabel rightLabel
+  -> Either OverlaySelectionError ExactCellSet
+selectClosedCells selectionKind selectSupport selectFace result =
+  let outsidePair = overlayResultOutsideLabels result
+      triangulation = overlayResultTriangulation result
+   in if uncurry selectFace outsidePair
+        then Left (OverlaySelectionContainsUnboundedCell selectionKind)
+        else do
+          selectedVertices <-
+            filterM
+              ( fmap selectSupport
+                  . first OverlaySelectionProvenance
+                  . vertexSupport result
+              )
+              (vertices triangulation)
+          selectedEdges <-
+            filterM
+              (\edge ->
+                 case Dcel.undirectedEdgeData triangulation edge of
+                   OverlayDiagonal -> Right False
+                   OverlayBoundary _ ->
+                     selectSupport
+                       <$> first OverlaySelectionProvenance (edgeSupport result edge))
+              (undirectedEdges triangulation)
+          closeSelectedCells selectedVertices selectedEdges selectFace result
+
+closeSelectedCells
+  :: [VertexId]
+  -> [UndirectedEdgeId]
+  -> (leftLabel -> rightLabel -> Bool)
+  -> OverlayResult leftLabel rightLabel
+  -> Either OverlaySelectionError ExactCellSet
+closeSelectedCells selectedVertices selectedEdges selectFace result =
+  let triangulation = overlayResultTriangulation result
+      exactPointAt vertex = Right (overlayExactPoint (vertexData triangulation vertex))
+   in do
+        selectedFaces <-
+          filterM
+            (\face ->
+               if faceCarriesExactArea result face
+                 then
+                   fmap (uncurry selectFace)
+                     ( first OverlaySelectionProvenance
+                         (faceLabels result face)
+                     )
+                 else Right False)
+            (innerFaces triangulation)
+        first OverlaySelectionInvalid
+          ( closeExactCellSetWith
+              exactPointAt
+              triangulation
+              selectedVertices
+              selectedEdges
+              selectedFaces
+          )
+
+supportAny :: (label -> Bool) -> OverlaySupport label -> Bool
+supportAny predicate = any predicate . overlaySupportLabels
diff --git a/test/examples/Main.hs b/test/examples/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/examples/Main.hs
@@ -0,0 +1,53 @@
+module Main (main) where
+
+import Moonlight.Triangulation.Example.AlphaBoundary
+import Moonlight.Triangulation.Example.ConstrainedRegion
+import Moonlight.Triangulation.Example.PlanarRegion
+import Moonlight.Triangulation.Example.SetAlgebra
+import System.Exit (die)
+
+main :: IO ()
+main = do
+  assertRight "set algebra" (6, 2) siteCounts
+  assertRight "constrained region" (11, 8) constrainedRegionCounts
+  assertRight "alpha boundary" [BoundaryShape 4 0] alphaBoundaryShapes
+  assertRight
+    "planar region"
+    expectedPlanarSummary
+    planarExample
+  putStrLn "examples: ok"
+
+expectedPlanarSummary :: PlanarExampleSummary
+expectedPlanarSummary =
+  PlanarExampleSummary
+    { booleanValuations =
+        [ CellValuationSummary 1 (ExactFractionSummary 20 1)
+        , CellValuationSummary 1 (ExactFractionSummary 4 1)
+        , CellValuationSummary 1 (ExactFractionSummary 12 1)
+        ]
+    , morphologyAreas =
+        fmap
+          (`ExactFractionSummary` 1)
+          [48, 36, 4, 16, 16]
+    }
+
+assertRight
+  :: (Eq value, Show value, Show obstruction)
+  => String
+  -> value
+  -> Either obstruction value
+  -> IO ()
+assertRight label expected result =
+  case result of
+    Left obstruction ->
+      die (label <> " failed with " <> show obstruction)
+    Right actual
+      | actual == expected -> pure ()
+      | otherwise ->
+          die
+            ( label
+                <> " expected "
+                <> show expected
+                <> ", got "
+                <> show actual
+            )
diff --git a/test/ffi/Main.hs b/test/ffi/Main.hs
--- a/test/ffi/Main.hs
+++ b/test/ffi/Main.hs
@@ -3,6 +3,8 @@
 import Control.Exception (bracket)
 import Control.Monad (unless)
 import Data.Int (Int64)
+import Data.List (nub)
+import Data.Maybe (mapMaybe)
 import Data.Word (Word32)
 import Foreign.C.String (peekCString)
 import Foreign.C.Types (CDouble (..), CSize (..), CUInt)
@@ -10,7 +12,10 @@
 import Foreign.Marshal.Array (allocaArray, peekArray, withArray)
 import Foreign.Ptr (Ptr, nullPtr)
 import Foreign.Storable (Storable (sizeOf), peek)
-import Moonlight.Triangulation.Foreign.ABI
+import Moonlight.Triangulation.Foreign.Contract
+import Moonlight.Triangulation.Foreign.Mesh
+import Moonlight.Triangulation.Foreign.Morphology
+import Moonlight.Triangulation.Foreign.Region
 
 type MeshPointer = Ptr CMesh
 type RegionPointer = Ptr CRegion
@@ -27,6 +32,7 @@
 
 main :: IO ()
 main = do
+  testStableWireContract
   unless (sizeOf obstructionLayoutWitness == 320) $
     fail "C obstruction layout changed"
   unless (sizeOf receiptLayoutWitness == 72) $
@@ -37,6 +43,7 @@
       requireMeshCount "left triangle count" meshTriangleCount left 2
       testDenseCopies left
       testImmutableBatch left
+      testBatchAdmission left
       testSiteSetAlgebra left right
   testExactRegionSurface
   testTypedCoordinateRefusal
@@ -45,6 +52,47 @@
   testRegionLayoutRefusal
   putStrLn "ffi: ok"
 
+testStableWireContract :: IO ()
+testStableWireContract = do
+  unless
+    ( allDistinct (fmap abiStatusId allAbiStatuses)
+        && allDistinct (fmap obstructionCodeId allObstructionCodes)
+        && allDistinct (fmap coordinateErrorCodeId allCoordinateErrorCodes)
+        && allDistinct (fmap regionLocationCodeId allRegionLocationCodes)
+        && allDistinct (fmap minkowskiOperationCodeId allMinkowskiOperationCodes)
+        && allDistinct wireSymbols
+        && allDistinct (fmap abiFunctionSymbol allAbiFunctions)
+        && allDistinct (mapMaybe haskellImplementationName allAbiFunctions)
+        && allDistinct (fmap abiHandleSymbol allAbiHandles <> fmap abiStructSymbol allAbiStructs)
+    )
+    (fail "FFI contract contains duplicate identifiers")
+  unless
+    ( abiVersion == 2
+        && fmap abiStatusId allAbiStatuses == [0, 1, 2, 3, 4, 5]
+        && obstructionCodeId ObstructionInvalidCoordinate == 1
+        && obstructionCodeId ObstructionNullPointer == 100
+        && obstructionCodeId ObstructionCountOverflow == 101
+        && obstructionCodeId ObstructionBufferTooSmall == 102
+        && obstructionCodeId ObstructionRegionLayoutInvalid == 200
+        && fmap regionLocationCodeId allRegionLocationCodes == [0, 1, 2]
+        && fmap minkowskiOperationCodeId allMinkowskiOperationCodes == [0, 1, 2, 3]
+    )
+    (fail "stable FFI wire identifiers changed")
+ where
+  wireSymbols =
+    fmap abiStatusSymbol allAbiStatuses
+      <> fmap obstructionCodeSymbol allObstructionCodes
+      <> fmap coordinateErrorCodeSymbol allCoordinateErrorCodes
+      <> fmap regionLocationCodeSymbol allRegionLocationCodes
+      <> fmap minkowskiOperationCodeSymbol allMinkowskiOperationCodes
+  haskellImplementationName function =
+    case abiFunctionImplementation function of
+      AbiFunctionRuntime -> Nothing
+      AbiFunctionHaskell _ name -> Just name
+
+allDistinct :: Eq value => [value] -> Bool
+allDistinct values = length values == length (nub values)
+
 obstructionLayoutWitness :: CObstruction
 obstructionLayoutWitness = CObstruction 0 0 0 0 0 0 0 0 0 ""
 
@@ -61,23 +109,23 @@
   alloca $ \written ->
     alloca $ \obstruction -> do
       status <- meshCopyVerticesF64 mesh nullPtr 0 written obstruction
-      requireStatus "undersized vertex copy" 3 status obstruction
+      requireStatus "undersized vertex copy" AbiStatusBufferTooSmall status obstruction
       required <- peek written
       refusal <- peek obstruction
-      unless (required == 4 && obstructionCode refusal == 102) $
+      unless (required == 4 && obstructionCode refusal == obstructionCodeId ObstructionBufferTooSmall) $
         fail "undersized vertex copy lost its required-capacity witness"
   allocaArray 8 $ \coordinates ->
     alloca $ \written ->
       alloca $ \obstruction -> do
         status <- meshCopyVerticesF64 mesh coordinates 4 written obstruction
-        requireStatus "vertex copy" 0 status obstruction
+        requireStatus "vertex copy" AbiStatusOk status obstruction
         values <- peekArray 8 coordinates
         unless (length values == 8) (fail "vertex copy wrote the wrong coordinate extent")
   allocaArray 6 $ \triangles ->
     alloca $ \written ->
       alloca $ \obstruction -> do
         status <- meshCopyTrianglesU32 mesh triangles 2 written obstruction
-        requireStatus "triangle copy" 0 status obstruction
+        requireStatus "triangle copy" AbiStatusOk status obstruction
         indices <- peekArray 6 triangles :: IO [Word32]
         unless (all (< 4) indices) (fail "triangle copy produced an out-of-range vertex")
 
@@ -91,6 +139,43 @@
         requireMeshCount "original after batch" meshVertexCount original 4
         requireMeshCount "revised after batch" meshVertexCount revised 6)
 
+testBatchAdmission :: MeshPointer -> IO ()
+testBatchAdmission original = do
+  testLateInvalidPoint
+  testCanonicalZeros
+ where
+  testLateInvalidPoint =
+    withPointArray [(1, 1), (3, 1), (0 / 0, 1)] $ \coordinates ->
+      alloca $ \output ->
+        alloca $ \obstruction -> do
+          status <- meshInsertManyF64 original coordinates 3 output obstruction
+          requireStatus "late invalid batch point" AbiStatusGeometryObstruction status obstruction
+          refusal <- peek obstruction
+          unless
+            ( obstructionCode refusal == obstructionCodeId ObstructionInvalidCoordinate
+                && obstructionCoordinateError refusal == coordinateErrorCodeId CoordinateErrorNaN
+                && obstructionInputIndex refusal == 2
+            )
+            (fail "late invalid batch point lost its raw input-index obstruction")
+          requireMeshCount "original after refused batch" meshVertexCount original 4
+  testCanonicalZeros =
+    let negativeZero = -0.0 :: Double
+     in withPointArray [(negativeZero, 1), (1, negativeZero)] $ \coordinates ->
+          bracket
+            (produceHandle "canonical-zero batch" (meshInsertManyF64 original coordinates 2))
+            meshFree
+            (\revised -> do
+               requireMeshCount "canonical-zero batch vertex count" meshVertexCount revised 6
+               allocaArray 12 $ \copiedCoordinates ->
+                 alloca $ \_written ->
+                   alloca $ \obstruction -> do
+                     status <- meshCopyVerticesF64 revised copiedCoordinates 6 _written obstruction
+                     requireStatus "canonical-zero batch copy" AbiStatusOk status obstruction
+                     copied <- map unCDouble <$> peekArray 12 copiedCoordinates
+                     unless (not (any isNegativeZero copied)) $
+                       fail "canonical-zero batch published a negative zero")
+  unCDouble (CDouble value) = value
+
 testSiteSetAlgebra :: MeshPointer -> MeshPointer -> IO ()
 testSiteSetAlgebra left right = do
   test "site union" meshSiteUnion 6
@@ -121,7 +206,7 @@
       alloca $ \pointCountOutput ->
         alloca $ \obstruction -> do
           status <- regionCounts region componentCountOutput loopCountOutput pointCountOutput obstruction
-          requireStatus "region counts" 0 status obstruction
+          requireStatus "region counts" AbiStatusOk status obstruction
           componentCount <- fromIntegral <$> peek componentCountOutput
           loopCount <- fromIntegral <$> peek loopCountOutput
           pointCount <- fromIntegral <$> peek pointCountOutput
@@ -140,7 +225,7 @@
                     componentOffsets
                     (fromIntegral (componentCount + 1))
                     obstruction
-                requireStatus "region copy" 0 copyStatus obstruction
+                requireStatus "region copy" AbiStatusOk copyStatus obstruction
                 copiedLoopOffsets <- peekArray (loopCount + 1) loopOffsets
                 copiedComponentOffsets <- peekArray (componentCount + 1) componentOffsets
                 unless (copiedLoopOffsets == [0, 4] && copiedComponentOffsets == [0, 1]) $
@@ -166,9 +251,9 @@
 
 testRegionPointLocation :: RegionPointer -> IO ()
 testRegionPointLocation region = do
-  requireRegionLocation "interior location" region (1, 1) 2
-  requireRegionLocation "boundary location" region (0, 1) 1
-  requireRegionLocation "exterior location" region (3, 1) 0
+  requireRegionLocation "interior location" region (1, 1) RegionLocationInterior
+  requireRegionLocation "boundary location" region (0, 1) RegionLocationBoundary
+  requireRegionLocation "exterior location" region (3, 1) RegionLocationExterior
 
 testRegionMorphology :: RegionPointer -> RegionPointer -> IO ()
 testRegionMorphology left right =
@@ -177,18 +262,18 @@
       (produceHandle "structuring element" (structuringElementCreateF64 coordinates 4))
       structuringElementFree
       (\element -> do
-        test "Minkowski sum" regionMinkowskiSum left right 0 "16/1"
-        test "offset" regionOffset element left 0 "9/1"
-        test "inset" regionInset element left 1 "1/1"
-        test "open" regionOpen element left 2 "4/1"
-        test "close" regionClose element left 3 "4/1")
+        test "Minkowski sum" regionMinkowskiSum left right MinkowskiOperationAddition "16/1"
+        test "offset" regionOffset element left MinkowskiOperationAddition "9/1"
+        test "inset" regionInset element left MinkowskiOperationErosion "1/1"
+        test "open" regionOpen element left MinkowskiOperationOpening "4/1"
+        test "close" regionClose element left MinkowskiOperationClosing "4/1")
  where
   test
     :: String
     -> (first -> RegionPointer -> Ptr RegionPointer -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt)
     -> first
     -> RegionPointer
-    -> Word32
+    -> MinkowskiOperationCode
     -> String
     -> IO ()
   test label operation firstInput secondInput expectedOperation expectedArea =
@@ -197,7 +282,7 @@
       (regionFree . fst)
       (\(result, receipt) -> do
         measurement <- measureRegion result
-        unless (receiptOperation receipt == expectedOperation && measuredArea measurement == expectedArea) $
+        unless (receiptOperation receipt == minkowskiOperationCodeId expectedOperation && measuredArea measurement == expectedArea) $
           fail (label <> " lost its result or morphology receipt"))
 
 measureRegion :: RegionPointer -> IO RegionMeasurement
@@ -218,7 +303,7 @@
                   perimeterLowerOutput
                   perimeterUpperOutput
                   obstruction
-              requireStatus "region measure" 0 status obstruction
+              requireStatus "region measure" AbiStatusOk status obstruction
               measuredEuler <- peek eulerOutput
               measuredArea <- peekCString areaOutput
               CDouble measuredPerimeterLower <- peek perimeterLowerOutput
@@ -230,14 +315,14 @@
   unless (lower <= expected && expected <= upper) $
     fail (label <> " perimeter certificate excludes the exact perimeter")
 
-requireRegionLocation :: String -> RegionPointer -> Point -> CUInt -> IO ()
+requireRegionLocation :: String -> RegionPointer -> Point -> RegionLocationCode -> IO ()
 requireRegionLocation label region (x, y) expected =
   alloca $ \output ->
     alloca $ \obstruction -> do
       status <- regionLocatePointF64 region (CDouble x) (CDouble y) output obstruction
-      requireStatus label 0 status obstruction
+      requireStatus label AbiStatusOk status obstruction
       observed <- peek output
-      unless (observed == expected) $
+      unless (observed == fromIntegral (regionLocationCodeId expected)) $
         fail (label <> " returned location " <> show observed)
 
 buildRegion :: [ComponentInput] -> IO RegionPointer
@@ -269,9 +354,9 @@
         alloca $ \output ->
           alloca $ \obstruction -> do
             status <- regionCreateF64 coordinates 4 loopCounts 1 componentCounts 1 output obstruction
-            requireStatus "invalid region layout" 4 status obstruction
+            requireStatus "invalid region layout" AbiStatusGeometryObstruction status obstruction
             refusal <- peek obstruction
-            unless (obstructionCode refusal == 200) $
+            unless (obstructionCode refusal == obstructionCodeId ObstructionRegionLayoutInvalid) $
               fail "invalid region layout lost its typed obstruction"
 
 testTypedCoordinateRefusal :: IO ()
@@ -280,9 +365,13 @@
     alloca $ \output ->
       alloca $ \obstruction -> do
         status <- delaunayF64 coordinates 3 output obstruction
-        requireStatus "invalid coordinate" 4 status obstruction
+        requireStatus "invalid coordinate" AbiStatusGeometryObstruction status obstruction
         refusal <- peek obstruction
-        unless (obstructionCode refusal == 1 && obstructionCoordinateError refusal == 1 && obstructionInputIndex refusal == 1) $
+        unless
+          ( obstructionCode refusal == obstructionCodeId ObstructionInvalidCoordinate
+              && obstructionCoordinateError refusal == coordinateErrorCodeId CoordinateErrorNaN
+              && obstructionInputIndex refusal == 1
+          ) $
           fail "invalid coordinate lost its typed witness"
 
 testNullPointerRefusal :: IO ()
@@ -290,9 +379,9 @@
   alloca $ \count ->
     alloca $ \obstruction -> do
       status <- meshVertexCount nullPtr count obstruction
-      requireStatus "null mesh" 1 status obstruction
+      requireStatus "null mesh" AbiStatusNullPointer status obstruction
       refusal <- peek obstruction
-      unless (obstructionCode refusal == 100) $
+      unless (obstructionCode refusal == obstructionCodeId ObstructionNullPointer) $
         fail "null pointer refusal lost its typed witness"
 
 testCoordinateCountOverflow :: IO ()
@@ -301,9 +390,9 @@
     alloca $ \obstruction -> do
       let overflowingCount = fromIntegral (maxBound `div` 2 + 1 :: Int)
       status <- delaunayF64 nullPtr overflowingCount output obstruction
-      requireStatus "coordinate count overflow" 2 status obstruction
+      requireStatus "coordinate count overflow" AbiStatusCountOverflow status obstruction
       refusal <- peek obstruction
-      unless (obstructionCode refusal == 101) $
+      unless (obstructionCode refusal == obstructionCodeId ObstructionCountOverflow) $
         fail "coordinate count overflow lost its typed witness"
 
 requireMeshCount :: String -> (MeshPointer -> Ptr CSize -> Ptr CObstruction -> IO CUInt) -> MeshPointer -> Int -> IO ()
@@ -311,7 +400,7 @@
   alloca $ \count ->
     alloca $ \obstruction -> do
       status <- operation mesh count obstruction
-      requireStatus label 0 status obstruction
+      requireStatus label AbiStatusOk status obstruction
       observed <- peek count
       unless (observed == fromIntegral expected) $
         fail (label <> " produced " <> show observed <> ", expected " <> show expected)
@@ -321,7 +410,7 @@
   alloca $ \output ->
     alloca $ \obstruction -> do
       status <- operation output obstruction
-      requireStatus label 0 status obstruction
+      requireStatus label AbiStatusOk status obstruction
       handle <- peek output
       unless (handle /= nullPtr) (fail (label <> " returned a null handle"))
       pure handle
@@ -335,15 +424,15 @@
     alloca $ \receiptOutput ->
       alloca $ \obstruction -> do
         status <- operation output receiptOutput obstruction
-        requireStatus label 0 status obstruction
+        requireStatus label AbiStatusOk status obstruction
         handle <- peek output
         receipt <- peek receiptOutput
         unless (handle /= nullPtr) (fail (label <> " returned a null region"))
         pure (handle, receipt)
 
-requireStatus :: String -> CUInt -> CUInt -> Ptr CObstruction -> IO ()
+requireStatus :: String -> AbiStatus -> CUInt -> Ptr CObstruction -> IO ()
 requireStatus label expected observed obstruction
-  | observed == expected = pure ()
+  | observed == fromIntegral (abiStatusId expected) = pure ()
   | otherwise = do
       refusal <- peek obstruction
       fail (label <> " returned status " <> show observed <> ": " <> obstructionMessage refusal)
diff --git a/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs b/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs
--- a/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs
+++ b/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs
@@ -58,6 +58,7 @@
   ( CoordinateError (..)
   , Point (..)
   , PointValidationError (..)
+  , queryPointValue
   )
 import Support (assertEqual, integerPoint, requireRight)
 import System.Mem (performGC)
@@ -233,7 +234,7 @@
     requireRight
       "finite point candidate projection"
       (exactPointToEmbeddingCandidate exactFinite)
-  assertEqual "finite exact point roundtrip" finitePoint projectedFinite
+  assertEqual "finite exact point roundtrip" finitePoint (queryPointValue projectedFinite)
   assertEqual
     "coincident exact segment endpoints"
     (Left (ExactSegmentEndpointsCoincide exactFinite))
@@ -301,6 +302,15 @@
               ]
           }
   certificate <- requireRight "ordinary local embedding" (certifyLocalEmbedding draft)
+  westEmbedding <-
+    maybe
+      (fail "ordinary local embedding omitted its admitted west candidate")
+      pure
+      (Map.lookup west (certificateRoundedVertices certificate))
+  assertEqual
+    "ordinary certificate retains admitted projection"
+    (Point (-1) 0)
+    (queryPointValue westEmbedding)
   assertEqual
     "ordinary distinctness obligations"
     10
diff --git a/test/native/Moonlight/Triangulation/NativeSpec.hs b/test/native/Moonlight/Triangulation/NativeSpec.hs
--- a/test/native/Moonlight/Triangulation/NativeSpec.hs
+++ b/test/native/Moonlight/Triangulation/NativeSpec.hs
@@ -1668,6 +1668,27 @@
 testConstrainedDelaunay = do
   base <- requirePointBuild "CDT base" [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 1 1, Point 3 3, Point 1 3, Point 3 1]
   let cdt0 = fromDelaunay (buildTriangulation base)
+      invalidPoint = Point (0 / 0) 0
+  case addConstraintEdge cdt0 invalidPoint (Point 4 0) of
+    Left (CdtBuildError (InvalidCoordinate Nothing _ CoordinateNaN)) -> pure ()
+    outcome -> fail ("invalid first constraint endpoint was admitted: " <> either show (const "success") outcome)
+  case addConstraintEdge cdt0 (Point 0 0) invalidPoint of
+    Left (CdtBuildError (InvalidCoordinate Nothing _ CoordinateNaN)) -> pure ()
+    outcome -> fail ("invalid second constraint endpoint was admitted: " <> either show (const "success") outcome)
+  assertEqual
+    "invalid endpoint refusal leaves the immutable source unpublished"
+    (8, 0)
+    (numVertices cdt0, numConstraints cdt0)
+  partlyResident <-
+    requireRight
+      "one resident one new constraint endpoint"
+      (addConstraintEdge cdt0 (Point 0 0) (Point 2 2))
+  let partlyResidentCdt = constraintRecoveryTriangulation partlyResident
+  assertEqual
+    "one resident endpoint reserves only the missing site"
+    (numVertices cdt0 + 1)
+    (numVertices partlyResidentCdt)
+  assertCdtValid "one resident one new constraint endpoint" partlyResidentCdt
   diagonalBatch <-
     requireRight
       "constraint recovery"
@@ -2370,22 +2391,28 @@
     (numInnerFaces right)
     (V.length (constrainedSeamRightFaceEvidence seam))
   forM_ (V.toList (constrainedSeamRightFaceEvidence seam)) $ \evidence ->
-    assertEqual
-      "right face witness names the exact target triangle"
-      (faceKeyOf right (constrainedSeamSourceFace evidence))
-      (faceKeyOf joined (constrainedSeamTargetFace evidence))
+    let sourceFaceKey = faceKeyOf right (constrainedSeamSourceFace evidence)
+        targetFaceKey = faceKeyOf joined (constrainedSeamTargetFace evidence)
+        evidenceFaceKey =
+          [ constrainedSeamFaceFirstPoint evidence
+          , constrainedSeamFaceSecondPoint evidence
+          , constrainedSeamFaceThirdPoint evidence
+          ]
+     in do
+          assertEqual
+            "right face witness names the exact target triangle"
+            sourceFaceKey
+            targetFaceKey
+          assertEqual
+            "right face witness carries the exact target triangle"
+            targetFaceKey
+            evidenceFaceKey
   when (V.null (constrainedSeamJoinFaces seam)) $
     fail "separated seam did not identify any new corridor face"
   assertEqual
     "copied source constraints require no corridor recovery"
     0
     (constraintBatchRequests (constrainedSeamConstraintStats seam))
-  unless
-    ( all
-        ((== Nothing) . constrainedSeamConstraintRecovery)
-        (V.toList (constrainedSeamRightConstraintEvidence seam))
-    ) $
-    fail "separated seam recovered a constraint already represented by its copied source"
   let seamPublication = constrainedSeamPublicationStats seam
   assertEqual
     "separated seam does not enumerate resident unboxed base pages"
@@ -2471,7 +2498,7 @@
   locallyRefined <-
     requireRight
       "seam refinement preserves cached hull frontier"
-      (refineWithinDomain (const ()) localParameters allJoinedFaces Set.empty joined)
+      (refineWithinDomain (const ()) localParameters allJoinedFaces joined)
   let refinedJoined = refinedTriangulation (refinementDomainResult locallyRefined)
   unless (refinementComplete (refinementDomainResult locallyRefined)) $
     fail "seam refinement fixture did not drain its local worklist"
@@ -2861,7 +2888,7 @@
   refined <-
     requireRight
       "encroached hull local refinement"
-      (refineWithinDomain (const ()) parameters permitted Set.empty source)
+      (refineWithinDomain (const ()) parameters permitted source)
   let refinementResult = refinementDomainResult refined
       result = refinedTriangulation refinementResult
   unless (refinementComplete refinementResult) $
@@ -3098,29 +3125,57 @@
       crossingParameters = calmParameters{refineMaxArea = Just 1}
   unless (not (Set.null permitted) && not (null protected) && not (Set.null interface)) $
     fail "checked local refinement fixture did not form a nontrivial cover"
-  case refineWithinDomain id calmParameters{refineMaxAdditionalVertices = Nothing} permitted interface source of
+  case refineWithinDomain id calmParameters{refineMaxAdditionalVertices = Nothing} permitted source of
     Left RefinementDomainRequiresFiniteVertexBudget -> pure ()
     outcome -> fail ("checked local refinement accepted an unbounded local budget: " <> either show (const "success") outcome)
-  case Set.minView interface of
-    Nothing -> fail "checked local refinement fixture has no interface"
-    Just (_, incomplete) ->
-      case refineWithinDomain id calmParameters permitted incomplete source of
-        Left (RefinementDomainInterfaceMissing _) -> pure ()
-        Left obstruction -> fail ("checked local refinement returned the wrong incomplete-interface obstruction: " <> show obstruction)
-        Right _ -> fail "checked local refinement accepted an incomplete interface"
   refined <-
     requireRight
       "checked local refinement"
-      (refineWithinDomain id calmParameters permitted interface source)
+      (refineWithinDomain id calmParameters permitted source)
   let localResult = refinementDomainResult refined
       target = refinedTriangulation localResult
       targetProtectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint target) (faceVertices target face)))) protected
       receipt = refinementDomainReceipt refined
+      finalPermitted = Set.fromList (V.toList (refinementFinalPermittedFaces receipt))
+      expectedFinalInterfaceIncidence =
+        V.fromList
+          [ if Set.member sourceForwardFace permitted
+              then (edge, sourceBackwardFace, targetForwardFace)
+              else (edge, sourceForwardFace, targetBackwardFace)
+          | edge <- Set.toAscList interface
+          , let (forward, backward) = directedPair edge
+                sourceForwardFace = incidentFace source forward
+                sourceBackwardFace = incidentFace source backward
+                targetForwardFace = incidentFace target forward
+                targetBackwardFace = incidentFace target backward
+          ]
+      actualFinalInterfaceIncidence = refinementFinalInterfaceIncidence receipt
   assertEqual "checked local protected face restriction" protectedSignatures targetProtectedSignatures
   assertEqual
     "checked local final permitted lineage"
     permitted
-    (Set.fromList (V.toList (refinementFinalPermittedFaces receipt)))
+    finalPermitted
+  assertEqual
+    "checked local final interface incidence"
+    expectedFinalInterfaceIncidence
+    actualFinalInterfaceIncidence
+  assertEqual
+    "checked local final interface edge set"
+    interface
+    (Set.fromList [edge | (edge, _, _) <- V.toList actualFinalInterfaceIncidence])
+  traverse_
+    (\(edge, protectedFace, finalPermittedFace) ->
+       unless
+         ( Set.notMember protectedFace permitted
+             && Set.member finalPermittedFace finalPermitted
+         )
+         ( fail
+             ( "checked local final interface incidence misclassified "
+                 <> show (edge, protectedFace, finalPermittedFace)
+             )
+         )
+    )
+    actualFinalInterfaceIncidence
   assertEqual "checked local protected visit receipt" V.empty (refinementVisitedProtectedFaces receipt)
   assertEqual "checked local boundary crossing receipt" 0 (refinementAttemptedBoundaryCrossings receipt)
   let closureStats = refinementValidationClosureStats receipt
@@ -3139,17 +3194,17 @@
     (constraintSegments source)
     (constraintSegments target)
   assertValid "checked local refinement" target
-  case refineWithinDomain id crossingParameters permitted interface source of
+  case refineWithinDomain id crossingParameters permitted source of
     Left (RefinementDomainWouldCrossInterface _ _) -> pure ()
     Left obstruction -> fail ("checked local refinement returned the wrong crossing obstruction: " <> show obstruction)
     Right _ -> fail "checked local refinement silently crossed its immutable interface"
-  case refineWithinDomain id calmParameters{refinePreserveConvexHull = False} permitted interface source of
+  case refineWithinDomain id calmParameters{refinePreserveConvexHull = False} permitted source of
     Left RefinementDomainRequiresConvexHullPreservation -> pure ()
     outcome -> fail ("checked local refinement accepted hull mutation: " <> either show (const "success") outcome)
-  case refineWithinDomain id calmParameters{refineKeepConstraintEdges = False} permitted interface source of
+  case refineWithinDomain id calmParameters{refineKeepConstraintEdges = False} permitted source of
     Left RefinementDomainRequiresConstraintPreservation -> pure ()
     outcome -> fail ("checked local refinement accepted constraint mutation: " <> either show (const "success") outcome)
-  case refineWithinDomain id calmParameters{refineExcludeOuterFaces = True} permitted interface source of
+  case refineWithinDomain id calmParameters{refineExcludeOuterFaces = True} permitted source of
     Left RefinementDomainForbidsOuterFaceExclusion -> pure ()
     outcome -> fail ("checked local refinement accepted outer-face exclusion: " <> either show (const "success") outcome)
   wholeRefined <-
@@ -3159,7 +3214,6 @@
           id
           crossingParameters
           (Set.fromList (innerFaces source))
-          Set.empty
           source
       )
   let wholeResult = refinementDomainResult wholeRefined
@@ -3224,7 +3278,7 @@
   partial <-
     requireRight
       "maximum-edge budget-limited refinement"
-      (refineWithinDomain id parameters allPermitted Set.empty source)
+      (refineWithinDomain id parameters allPermitted source)
   let partialResult = refinementDomainResult partial
       partialTarget = refinedTriangulation partialResult
       partialReceipt = refinementDomainReceipt partial
@@ -3241,7 +3295,6 @@
           id
           parameters{refineMaxAdditionalVertices = Just 8}
           resumedPermitted
-          Set.empty
           partialTarget
       )
   let resumedResult = refinementDomainResult resumed
@@ -3287,7 +3340,6 @@
               id
               parameters{refineMaxAdditionalVertices = Just 8}
               permitted
-              interface
               source
       case blocked of
         Left (RefinementOversizedEdge face edge actual bound)
@@ -3354,7 +3406,7 @@
         "deterministic crossing source has one interface edge"
         1
         (Set.size interface)
-      case refineWithinDomain id parameters permitted interface source of
+      case refineWithinDomain id parameters permitted source of
         Left (RefinementDomainWouldCrossInterface _ _) -> pure ()
         Left obstruction ->
           fail
@@ -3385,7 +3437,7 @@
           (V.singleton (2, 6))
       )
   let source = buildTriangulation built
-      (sourcePermitted, sourceInterface) = leftSection source
+      sourcePermitted = leftSection source
       fullParameters =
         defaultRefinementParameters
           { refineMaxAdditionalVertices = Just 20
@@ -3396,7 +3448,7 @@
   fullRefinement <-
     requireRight
       "full boundary-adjacent refinement"
-      (refineWithinDomain id fullParameters sourcePermitted sourceInterface source)
+      (refineWithinDomain id fullParameters sourcePermitted source)
   let fullResult = refinementDomainResult fullRefinement
       fullTarget = refinedTriangulation fullResult
       fullReceipt = refinementDomainReceipt fullRefinement
@@ -3421,7 +3473,7 @@
   progressFirstRefinement <-
     requireRight
       "first bounded boundary-adjacent refinement"
-      (refineWithinDomain id progressParameters sourcePermitted sourceInterface source)
+      (refineWithinDomain id progressParameters sourcePermitted source)
   let progressFirstResult = refinementDomainResult progressFirstRefinement
       progressFirstTarget = refinedTriangulation progressFirstResult
       progressFirstReceipt = refinementDomainReceipt progressFirstRefinement
@@ -3438,7 +3490,6 @@
           id
           progressParameters
           progressPermitted
-          sourceInterface
           progressFirstTarget
       )
   let progressSecondResult = refinementDomainResult progressSecondRefinement
@@ -3458,26 +3509,13 @@
  where
   leftSection
     :: Triangulation mode vertex directed undirected face
-    -> (Set.Set FaceId, Set.Set UndirectedEdgeId)
+    -> Set.Set FaceId
   leftSection triangulation =
-    let permitted =
-          Set.fromList
-            [ face
-            | face <- innerFaces triangulation
-            , faceCentroidX triangulation face < 8
-            ]
-        interface =
-          Set.fromList
-            [ edge
-            | edge <- undirectedEdges triangulation
-            , let (forward, backward) = directedPair edge
-                  forwardFace = incidentFace triangulation forward
-                  backwardFace = incidentFace triangulation backward
-            , forwardFace /= outerFace
-            , backwardFace /= outerFace
-            , Set.member forwardFace permitted /= Set.member backwardFace permitted
-            ]
-     in (permitted, interface)
+    Set.fromList
+      [ face
+      | face <- innerFaces triangulation
+      , faceCentroidX triangulation face < 8
+      ]
   faceCentroidX
     :: Triangulation mode vertex directed undirected face
     -> FaceId
