-`AudioPlayer.swift` is in `Shared/` — ALL iOS-only code MUST be inside `#if os(iOS)` / `#endif`
-`AudioTapProcessor`, `VisualizerSettings`, `PlayQueueSyncManager` are iOS-only files (in `iOS/`)
- Never reference iOS-only types from `Shared/` without `#if os(iOS)` guards
-`CompanionAPIService` is a Swift `actor` — model structs MUST be declared at TOP LEVEL outside the actor
-`FlowLayout` is defined in `LyricsOverlayView.swift` — never redeclare it
- New Swift types should go in EXISTING registered files when possible
- Never use placeholders like `// existing code` or `// rest unchanged` — always output complete file contents
-`PlaybackStateStore.swift` lives in `Shared/Storage/` (was moved from `iOS/Data/`)
### Threading & Concurrency (from crash audit)
-`SmartDJCache` uses `NSLock` for all `memoryCache` access — NEVER access without lock
- When swapping `self.player` to `crossfade.activePlayer`, ALWAYS remove `timeObserver` from the OLD player first — removing from wrong player throws NSException → SIGABRT
-`stopAVPlayer()` removes time observer BEFORE `replaceCurrentItem(with: nil)`
- All Combine `.sink` handlers on NotificationCenter publishers MUST use `.receive(on: DispatchQueue.main)`
- KVO `item.observe(\.status)` callbacks fire on AVFoundation's internal thread — wrap body in `DispatchQueue.main.async`
-`loadOfflineVisualizer` Task completion blocks MUST guard `self.currentSong?.id == songId` to prevent stale vis data after track skip
### Visualizer / Audio Tap
- Radio streams use `MTAudioProcessingTap` via `AudioTapProcessor.shared` for real-time FFT
- Tap MUST install on `.readyToPlay` status, NOT immediately after `player.play()`
-`startRadioSimulation()` runs as visual placeholder until tap installs
-`ShazamRecognizer` subscribes to shared tap via `shazamHandler` — does NOT create its own tap
- Background: tap removed in `suspendVisTimers()`, reinstalled in `resumeVisTimers()`
- Every `player?.replaceCurrentItem(with: item)` MUST also set `self.playerItem = item`