- index.css: add visible :focus-visible rings for .site-card-button
and .site-button so keyboard users can see where focus lives
- SiteCard: drop outline:none, add aria-pressed and aria-label so
screen readers announce selection state
- SiteButton: add the .site-button class for the shared focus ring
- SiteDisclaimerScreen: keyboard skip via Enter / Space / Escape, a
role="region" + aria-label wrapper and aria-live="polite" on the
message; honour prefers-reduced-motion on the fade
- IntroVideoPlayer: role="region" with a skip hint in aria-label,
preload="auto", and aria-hidden on the decorative caption span
- loadDialogueManifest: cache the resolved manifest at module level and
dedupe concurrent fetches so each screen no longer re-downloads it
- useGameStore: completeIntroState now also advances intro.currentStep
to "playing" so callers do not need a separate setIntroStep call
- SiteNamingScreen and SiteTransitionOverlay: replace ref-based guards
with an isCancelled flag captured per effect. The previous guards
persisted across StrictMode remounts, leaving mount 2 unable to
re-run the effect after mount 1's chain was cancelled, which broke
the fade animations, the second narrator dialogue and the redirect.
Both screens now also call stopCurrentDialogue on unmount so audio
cannot bleed across routes, and the transition gets a safety timeout
in case the dialogue audio fails to fire its "ended" event
- SiteTransitionOverlay: keep the <Subtitles /> mount inside the
overlay so it renders inside the z-index 1000 stacking context
(above the black screen); the one in SiteLayout sits behind it
- IntroDialogueOverlay: route through playDialogueById instead of
AudioManager.playSoundWithCallback so the narrator subtitles play
in sync, and add the same isCancelled cleanup pattern
- IntroRevealOverlay: rely on completeIntro alone now that it advances
intro.currentStep, and skip the fade when reduced motion is requested
- SiteMobileBlocker: correct logo path from public/... to /...
- new src/hooks/ui/useIsMobile.ts (matchMedia + useSyncExternalStore)
replacing the resize-handler hook inlined inside pages/site/page.tsx
- new src/hooks/ui/usePrefersReducedMotion.ts
- new src/data/site/dialogueIds.ts so site and intro components stop
carrying hard-coded narrator IDs
- siteConfig: add SITE_BACKGROUND_STYLE shared by SiteLayout and
SiteMobileBlocker, rename forcedName to presetPlayerName, fix the
swapped id/label pairing on situation cards
- useSiteStore: rename selectedExperience/Situation to *Index so the
stored value (an array index) is obvious in callers
- audioConfig: drop dead AUDIO_PATHS placeholders
- propagate the renames and SITE_BACKGROUND_STYLE through SiteLayout,
SiteWelcomeScreen, SiteSituationScreen and pages/site/page.tsx