ROOT CAUSE: The exact crossing condition (oldMaxY >= 0 && newMaxY < 0) required onChange to fire at the exact moment maxY crosses zero. But LazyVStack recycles views when they scroll off-screen, destroying the GeometryReader before the crossing event is delivered. The entry goes from maxY=15 to being recycled — onChange never sees maxY go negative. FIX: Replace exact crossing with position check (newMaxY < 0). The entry just needs to be fully above the viewport. The other 5 guards prevent false positives: 1. trackingActive (scrolled past threshold) 2. isScrollingDown 3. !entry.isRead 4. !markedByScroll (dedup) 5. wasVisible (was >=50% visible) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>