feat: on-brand media embeds via the real player APIs
Replaces bare platform iframes with on-brand cards, powered by the
official JS APIs so play/pause/progress are in our own hands:
- YouTube (IFrame Player API) + SoundCloud (Widget API): fully custom
controls, native chrome hidden.
- Spotify (iFrame API): our frame around it + controls (their player UI
remains; restyling not possible without Premium+OAuth).
- Shared PlaybackRegistry: mutual exclusion -- only 1 thing plays at a time
(incl. the site audio player). Replaces the focus/blur heuristic with real
play events (blur stays as fallback for iframe-only embeds).
- Progressive enhancement: if an ad-blocker blocks the platform API, falls
back seamlessly to the bare platform iframe (autoplay). The resting-state
card is our brand for everyone.
AudioEmbedService now renders a placeholder div (data-embed-*) for YT/SC/
Spotify instead of an iframe; embed-player.js builds the card client-side.
CSP scriptSrc extended with the player API hosts.
Adversarial review (workflow) -> 6 bugs fixed: HTMX swap leak (poll timers/
adapters -> MutationObserver teardown + adapter.destroy()), javascript: URL XSS
(scheme guard in detectProvider + safeHref client-side), Spotify ended
misdetection (no more reset-to-0), ytId/server regex on exact 11, blur scope
limited to .folio-embed.
Co-Authored-By: Claude <noreply@…>