Reticle/Sigil Per-Side Edge Ownership

Every visible 1px seam is owned by exactly one component through an explicit top | right | bottom | left edge set, so Reticle line-crossings and open frames are deliberate and machine-auditable instead of accidental double-dark 2px borders.

Position

This is the focused implementation spec for the per-side edge API that Reticle Design System introduces at the concept level. Reticle's invariant is one visual structure, one owner; its seam-ownership contract already defines data-reticle-edges, data-reticle-cell, data-reticle-owner, and ReticleSeamGuard. This page turns that contract into a buildable API, an ownership rule, a developer-facing seam guard, integration points, and test fixtures.

Proposal: Reticle Design System currently carries the conceptual version of this API inline. Once this implementation spec is validated, that page should absorb the concept and point here for the mechanism. This is noted as a proposal only; do not edit the reticle-design-system page as part of this spec.

The source signal is Kevin's request to "make our divs be able to control which of 4 sides get 1px borders to give that reticle line crossing effect." Source: private Discord message, 2026-07-19 19:50 PT; raw/discord/straight-fats/tasks-signals-2026-07-21.md item A1

The Problem

When two Reticle panels sit adjacent and each paints its own border, the shared seam is drawn twice. The result is a 2px, double-dark hairline that breaks Reticle's single-weight line topology, and it varies unpredictably at breakpoints when one side's border is dropped. The naive fix (per-component ad hoc class strings like border-t border-r) is unauditable: nothing records intent, so a reviewer cannot tell a deliberate open edge from a forgotten one, and nothing catches two components both claiming the same pixel.

Line-crossing—the Reticle look where a rail visibly continues through a panel corner—requires the opposite of "every box has four borders." It requires each box to declare exactly which edges it owns and to leave the rest to a rail, a parent grid, or a neighbor.

Token And Prop API

Per-side ownership is a first-class component API, not repeated class strings. A Reticle/Sigil frame accepts an explicit edge set and derives its 1px borders from semantic tokens.

<ReticleFrame edges={["top", "right", "bottom"]}>
  …
</ReticleFrame>

Contract:

Element Rule
edges prop Subset of ["top", "right", "bottom", "left"]. Presence means "this component owns and paints this 1px seam." Absence means a rail, parent grid, or neighbor owns it.
Serialization The component serializes the resolved set to data-reticle-edges="top right bottom" (space-delimited) so the DOM is inspectable and auditable.
Border source Painted edges derive width from --ret-hairline (1px) and color from --ret-border (or --ret-rail/--ret-line where the role differs). No literal pixel values, no hardcoded colors.
Owner name The component sets data-reticle-owner="name" so guard warnings name the culprit.
Cell role A child inside a seam-grid parent sets data-reticle-cell and owns no adjacent edges; the parent owns them via gap: var(--ret-hairline) + background: var(--ret-border).
Responsive sets edges may change per breakpoint (for example, drop left on mobile when the rail disappears). The rendered data-reticle-edges attribute and the computed visible border must agree at every breakpoint.

Sigil token layer (Sigil Token Architecture): the edge API consumes the existing Reticle hairline group (hairline, border, line, rail, grid, cross) at the semantic layer and needs no new primitive—edge ownership is topology metadata, not a new color. The one addition Sigil should carry is that line-topology is recorded as which layer owns each seam, which the edges set makes explicit at the component boundary.

The One-Owner Rule

The governing rule, stated precisely:

For any visible 1px seam on the rendered page, exactly one component's declared edges set (or one parent seam-grid) must claim it. Zero claims means a missing structural line; two or more claims means a double-dark seam.

Consequences:

  1. If a parent uses gap-px + background: var(--ret-border), children are data-reticle-cell and declare no adjacent edges.
  2. If rows use divide-y, row children add no top/bottom edge.
  3. If a page grid owns vertical rails, nested sections declare only content-boundary edges, never the rail edge.
  4. If a spacer owns a section transition, neighboring sections drop the touching edge.
  5. If two components' edges meet on the same pixel, delete one claim and move ownership upward to the parent or the rail.

The missing edge is structural information. In edges={["top", "right", "bottom"]} the absent left means a rail or neighbor owns that line—it is not an invitation for the child to paint a second pseudo-element at the same coordinate.

Developer Seam Guard

ReticleSeamGuard is the dev-only auditor that makes the rule enforceable rather than aspirational. It compares declared edges against computed visible borders and reports conflicts.

Algorithm:

  1. Query every element carrying data-reticle-edges (and every data-reticle-cell under a seam-grid parent).
  2. For each, read the declared edge set from the attribute.
  3. For each declared edge, read getComputedStyle for that side's border-*-width/border-*-style to confirm the border is actually visible at the current breakpoint. An edge declared but not rendered (removed by CSS at this width) is not counted as an owned seam—this keeps the guard responsive-aware and avoids false conflicts.
  4. Compute each element's box rect; for adjacent elements sharing a rect edge (within a 1px tolerance), check whether more than one of them owns that shared seam.
  5. Report every shared seam owned by two or more components, and optionally every visible border with no declared owner, into data-reticle-seam-conflicts on the document (a count) plus a dev-console warning naming both data-reticle-owner values and the offending side.

The guard runs at desktop and mobile widths because a seam clean at one breakpoint can double at another when a responsive edges set and the CSS disagree. A surface is considered visually clean only when both rendered checks report data-reticle-seam-conflicts="0".

Guard output Meaning Fix
seam-conflicts="0" at desktop and mobile Clean topology Ship.
Two owners on one seam Double-dark 2px risk Drop one claim, move ownership to parent/rail.
Declared edge, no computed border Stale declaration at this breakpoint Remove the edge from the responsive set for that width.
Visible border, no declared owner Untracked seam Add the owning component to the edge API or data-reticle-cell.

The guard is dev-only and non-interactive; it must not paint borders, capture pointer events, or alter layout—it only reads and reports, matching the diagnostic-overlay discipline Reticle uses for margin/gutter and corner-cross switches.

Integration

With Reticle Design System: every seam-drawing primitive—ReticleFrame, ReticleSection, ReticleCard, ReticleSpacer, ReticleBand, and seam-grid parents—accepts the edges API and serializes data-reticle-edges. This is the mechanism behind the design system's "avoiding double lines" rules and its data-reticle-layout="seam-grid" / data-reticle-cell contract. Line-crossings (a rail continuing through a corner) are expressed by leaving that edge unowned on the panel so the rail's own edge shows through.

With Sigil Token Architecture: painted edges resolve through the semantic hairline token group, keeping edge weight and color spec-driven so a single DESIGN.md change cascades. The edge set is the component-boundary expression of Sigil's "line topology" tokens—the agent decides which primitive owns a line before writing any border-* class, which is exactly the "ownership before border" generation rule Sigil already states.

With the agent workflow: an agent building Reticle UI identifies the line owner first, declares it via edges, and never adds a border without a claim. The seam guard is the proof gate: verify data-reticle-seam-conflicts="0" at desktop and mobile before a surface is considered done.

Test Fixtures

Ship fixtures that exercise both the correct patterns and the failure the API exists to prevent:

Fixture Setup Expected
Adjacent panels, correct Left panel edges includes right; right panel omits left One shared 1px seam; seam-conflicts="0"
Adjacent panels, double-owned Both panels claim the shared seam Guard reports 1 conflict naming both owners
Seam-grid cells Parent data-reticle-layout="seam-grid", children data-reticle-cell with no adjacent edges Parent owns all cell seams; seam-conflicts="0"
Line-crossing Panel omits the edge a page rail passes through Rail visibly continues through the corner; no second pseudo-element
Open frame edges={["top","right","bottom"]}, left owned by neighbor Three-sided frame; left seam single-owned by the neighbor
Responsive drop Desktop edges includes left; mobile omits it because the rail is gone seam-conflicts="0" at both widths; no stale declared-but-unrendered edge
Untracked border A raw border with no data-reticle-edges Guard flags a visible border with no declared owner
Stale declaration data-reticle-edges includes an edge CSS removes at this width Guard treats the removed border as unowned, not a conflict

Fixtures run under the seam guard at desktop and mobile viewports; the pass condition is data-reticle-seam-conflicts="0" for the "correct" fixtures and the exact expected warning for the "failure" fixtures.

Failure Modes

Failure Fix
Two components paint the same seam (2px double-dark) Remove one claim; move ownership to the parent seam-grid or the page rail.
Ad hoc border-t border-r class strings Replace with the edges prop so intent serializes to data-reticle-edges.
Responsive false seam Let the guard filter by computed visible border; drop the stale edge from that breakpoint's set.
Line-crossing painted with a child pseudo-element Leave the crossed edge unowned so the rail shows through; do not double-paint.
Untracked raw borders Route through the edge API or mark the element data-reticle-cell.
Guard alters layout or captures pointer events Keep the guard read-only, non-interactive, and dev-only.

Timeline