The shared parent-pair child loop read group.childIds[i] while iterating
the filtered childCenters, so a child without a position would desync the
id from the centre — and that index now also drives the active-connector
lookup. Ride the id on the mapped {id,x,y} centre so the two never drift;
a positionless child drops out of both together.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
StammbaumConnectors gains an isConnectorActive(a, b) predicate prop and
wraps each logical connector in a <g opacity> group. A connector is full
strength only when both joined people are active; otherwise it dims to
DIMMED_OPACITY. The shared parent-pair drop+bar keys on both parents,
while each child vertical keys on both parents AND that child — so the
bar stays lit to a lineage child yet dims to a collateral sibling on the
same row. Defaults to always-active, so no highlight means no dimming.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the three SVG connector layers (+ the parent-link graph computation)
into StammbaumConnectors.svelte and the node <g> into StammbaumNode.svelte (which
now owns its own focus-ring state). StammbaumTree drops 546→308 lines and is now
an orchestrator: layout, gutter/reduced-motion state, viewBox, gestures, rail,
anchor. Rendered SVG is byte-identical, so the existing browser tests are
unchanged. Verified live: 62 nodes + 58 connector lines render, node-tap selects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>