b63ed0181f
It played in Chrome and was silent on every Apple device. Two false signals, compounding. Neither the play() promise nor a synchronous `paused` check tells you whether audio is playing. Chrome REJECTS the promise when autoplay is blocked, which is the behaviour the obvious implementation is written against. WebKit RESOLVES it, reports `paused === false` for an instant, and quietly pauses the element a moment later. So the code concluded it had started, faded the volume up on a silent element, and — because it thought it had succeeded — never armed the gesture listener that was the entire fallback. Music could then never start at all, no matter how many times the visitor clicked. Traced by hooking HTMLMediaElement.prototype.play and addEventListener before the app booted: exactly one play() call, "resolved paused=false", and no pointerdown listener ever registered on window. Two changes. Playing state is now driven by the element's own `playing` and `pause` events, which are the only honest source. And the gesture listener is armed UNCONDITIONALLY rather than only on a detected failure — play() on an already-playing element is a no-op, so the redundant case costs nothing and the broken case is fixed. The listeners are capture-phase so a component calling stopPropagation cannot swallow them, and cover touchend and keydown as well. Verified 12/12: Chrome and WebKit x desktop and iPhone x /, /learn and /margin all reach t>2.5s at volume 0.14, and mute still fades and persists in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>