The scene animated its own height when the walkthrough toggled the
extension popup; with the vertically centered hero grid this re-centered
the left column and shifted everything below on mid-size screens. The
scene now always reserves the 600px open state, so toggling the popup
never reflows the page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New hero headline "Turn any video into a watch party" with animated
gradient highlight, sharper subtitle and quiet trust line under the CTAs
- Changelog badge redesigned as the koala's speech bubble (version and
release date baked in at build time, no version.json fetch anymore)
- Light/dark theme toggle in the header (dark default, pre-paint via
lang-init, full light theme across sections)
- Use cases and "Why KoalaSync" merged into one section with the three
strongest feature tiles; question-mark koala moved to the FAQ
- FAQ and self-hosting setup collapsed by default via shared
section-collapse component (hash deep links auto-open them)
- Compat/juggler section more compact
- og:image switched to the 1280x640 repository OpenGraph card (landing
and join page), generated during build
- "watch party" keyword added to meta title/description in all 15 locales
- All em-dashes removed from user-facing copy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Change .hero from align-items:center to flex-start so content starts from top
- Increase padding-top in short-viewport media query (<=920px height) to
clamp(7.5rem, 10vh, 9rem) from clamp(3.75rem, 6vh, 5.5rem) so it always
clears the ~96px nav bar
- Fix pre-existing lint errors: remove unused variables demoFinishedByStory,
firstTake; add website/www to eslint ignores
Remove firstTake && !demoFinishedByStory guard from takeOver handler
so clicks outside the extension mockup always collapse the popup,
regardless of whether the automated walkthrough finished.
- Remove data-reveal from hero h1/subtitle/mascot/cta-group/version-badge
(critical above-fold content must never start hidden)
- Change IntersectionObserver rootMargin -150px to -30px, threshold 0.1 to 0.05
(CTAs at ~530px were past the observer zone on laptops)
- Hero layout: align-items flex-start + padding-top 7.5rem for content at top
- Cards 62cqw to 70cqw, -3% offsets for dramatic overlap cascade
- Film animation: replace stick-figure walker with cinematic night scene
(7 twinkling stars, glowing moon with drift, parallax mountains, firefly)
- Seek now restarts all film child animations via .demo-reset class toggle
(was incorrectly resetting parent only — no visible jump)
- Add .demo-seek-sweep overlay + brightness flash during seek
- Video select: placeholder option ('Choose a video tab...') until story picks
- RATE 8 to 1 for realtime playback
- Version badge moved below download buttons
- All 15 locale files: add DEMO_SELECT_PLACEHOLDER key
The v2.5.2 Disney+ page-API integration leaked blob-relative <video>
time into force sync, seeks, and heartbeats when the page-API bridge
had no fresh data, so force sync on Disney+ appeared broken.
- getSyncCurrentTime/getSyncDuration now refuse native values on Disney+
(return null/0) so stale bridge data degrades to a clean no-op instead
of broadcasting garbage to peers. The get_current_time handler and
episode/lobby/hcmIsLive paths are routed through the same accessor.
- Validate FORCE_SYNC_PREPARE/SEEK payloads as finite before relaying;
the internal coercion no longer treats '' as 0.
- Stop double-routing FORCE_SYNC_PREPARE from the popup path (the
generic popup route now covers only play/pause/seek).
- popup force-sync: exclude null/empty peer times from the jump-to-others
median (Number(null)===0 was dragging the target to 0), guard against
NaN end-to-end, clear the dangling reset timer on failure, and retry
without re-injecting when the content script responds but the Disney+
bridge has not yet delivered a finite time.
- hcmIsLive skips the native-duration live signal on Disney+ only,
preserving YouTube/Twitch Infinity-duration live detection.
Disney-specific logic remains strictly gated to disneyplus.com; no
Netflix/YouTube/Twitch/generic path is affected.
Disney+'s <video> is blob-relative (unusable as an absolute clock) and its
scrubber aria-value freezes during playback, so DOM scraping lagged and the
+/-10s button seek could neither reach far targets nor land precisely. The
real player hangs off the <disney-web-player> custom element as
`.mediaPlayer`, exposing seek(ms) and timeline.info (playhead/duration ms).
Since the isolated content world can't read that page object, route it
through the existing MAIN-world page-API bridge (as Netflix already does):
- page-api-seek-overrides.js: register a 'disney' provider.
- background.js installPageApiSeekBridge: seek Disney via mediaPlayer.seek(),
and post the exact playhead/duration (seconds) to the content world every
250ms. Both are gated on provider === 'disney'; Netflix path unchanged.
- content.js: cache the pushed playhead, prefer it in getDisneyPlusTimeline
(DOM scraping stays as fallback), and check the page-API seek first in
seekVideo. Outcome is identical for Netflix and generic sites.
Verified live on Disney+: reported time matches the player exactly and
seek lands within ~1s of the target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Disney+ has no absolute seek on its blob <video>, so absolute seeks are
translated into repeated clicks of the player's relative skip button. The
click count was capped at 12 (=120s at 10s/step), so any seek further than
two minutes away silently landed short — breaking absolute-position sync
(e.g. syncing to 5:00 from far away never arrived).
Derive the step size from the matched button's own label (10s default,
some UIs use 5/15/30) and issue enough clicks to cover the full delta,
bounded at a sane maximum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the control overlay (and its live time readout) is briefly absent,
the old fallback recomputed the timeline from the raw blob-relative
video.currentTime via a cached scale/start. Disney recreates the <video>
element on some play/pause transitions, which resets currentTime, so that
math produced a wildly wrong time ("loses the time" after a pause).
The native clock advances 1:1 with real playback, so instead anchor the
last live UI position to video.currentTime and extrapolate by the elapsed
native delta. If the delta is negative or implausibly large (element
recreated / seek), freeze at the last known position rather than emitting
a garbage time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>