1. Visibility requirement (new condition 5):
- Tracks max visible ratio per entry via GeometryReader
- Entry must have been >=50% visible at some point to qualify
- Prevents marking entries that were never genuinely seen
- Uses wasVisible set, populated by onChange(of: minY)
2. Dynamic activation threshold:
- max(100pt, 20% of viewport height)
- Taller screens (iPad) require proportionally more scroll
- Measured via background GeometryReader on ScrollView
3. Stabilized scroll direction:
- Ignores micro deltas <2pt (was 1pt)
- Filters layout noise, rubber-banding, and momentum artifacts
Existing protections preserved:
- trackingActive reset on onAppear (navigation protection)
- downward-only marking
- crossing detection (oldMaxY >= 0, newMaxY < 0)
- markedByScroll dedup set
6 conditions must ALL be true to mark an entry:
1. trackingActive (scrolled past threshold)
2. isScrollingDown
3. !entry.isRead
4. !markedByScroll.contains(id)
5. wasVisible.contains(id) — was >=50% visible
6. bottom edge crossed above viewport
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>