Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Architect for Cinema Mode / Cinema OS in Raamattu Nyt: CinemaShell, verse audio-sync & cues,
auto-advance, dual-track audio (Bible+music, ElevenLabs TTS), Ken Burns, Discipleship Cinema
(task orchestration, Nyt Kooste, verse memory quizzes, TanaanPage), curated/scrolling grand
plans, Info Cinema (Q&A / topic info-palaset, snap-scroll), Topic Cinema (aihe-cinema), and
realtime multiplayer Prayer Rooms (rukoushuone, PTT, WebRTC). Use for any cinema / voice /
audio-sync, discipleship-tila, info-palaset, or prayer-room work.
Cinema Voice Architect
Expert skill for Cinema Mode and audio/voice implementation.
Quick Reference
Base Layer (CinemaShell)
Component
Location
CinemaShell
src/components/cinema/CinemaShell.tsx
Cinema Background
src/components/cinema/CinemaBackground.tsx
Background Music Picker
src/components/cinema/BackgroundMusicPicker.tsx
Background Visual Picker
src/components/cinema/BackgroundVisualPicker.tsx
Cinema Preferences Hook
src/hooks/useCinemaPreferences.ts
Cinema Fullscreen Hook
src/hooks/useCinemaFullscreen.ts
Cinema Audio Hook
src/hooks/useCinemaAudio.ts
CinemaShellContext (shell ctx as React context for shell-less apps)
src/components/cinema/CinemaShellContext.ts
Cinema OS / Command Center (orchestration layer)
Component / Module
Location
Design doc (read first)
Docs/cinema/CINEMA-OS.md
Types (Intent / CinemaApp / CinemaNav)
src/cinema-os/types.ts
Navigation reducer (stack)
src/cinema-os/navigation.ts
Registry + deep-link translation
src/cinema-os/registry.ts
Web-reitti → intent (webRouteToIntent; sisällön route-linkit pysyvät cinemassa)
src/cinema-os/routeIntents.ts
App registration (all apps)
src/cinema-os/cinemaApps.ts
CinemaOSProvider (stack + history + 2-level back)
src/cinema-os/CinemaOSProvider.tsx
CinemaHost (the ONE persistent shell)
src/cinema-os/CinemaHost.tsx
Contexts + useCinemaNav / useCinemaBackHandler
src/cinema-os/context.ts
useCinemaShell (re-export)
src/cinema-os/useCinemaShell.ts
Session history (localStorage)
src/cinema-os/history.ts
Route element (/cinema-os/*)
src/cinema-os/CinemaOSRoot.tsx
Shared settings/music sheets
src/cinema-os/CinemaOSSheets.tsx
Shared settings-sheet wiring (single source for CinemaOSSheets AND CinemaReaderScreen). ⚙ sheet = bg+textsize+completion+speed; Raamatun versio + lukuääni (CinemaVersionVoiceSection) are split to the 🔊 music popover (audio side)
src/components/cinema/useCinemaSettingsProps.ts
Cinema OS help overlay (i-icon → "what is Cinema OS")
src/cinema-os/CinemaOSHelpOverlay.tsx
CinemaOSNavCluster (top-left Back + Command Center pill; shared by CinemaOSChrome + ownsChrome apps like the prayer room)
TopicVerseCinema (1-verse carousel via @raamattu-nyt/cinema-reader embedded)
src/components/cinema/topic/TopicVerseCinema.tsx
TopicVersesCinemaView (default verses step: full-width single-verse carousel, useVerseCues+useAutoAdvance, ←/→=verse, ↑/↓ bubble to flow=card. Renders a FIXED portaled bottom cluster (snapshot strip + segmented progress + consolidated control bar, one rounded pill: ◀prev · 📖Lue (openChapter) · 🎬Cinema (Film icon → 1-verse jae-cinema, only if onOpenVerseCinema) · 🎓Discipleship popover (amber GraduationCap → CinemaVerseActions menu: Pohdittavaksi/Lisää koosteeseen/Muistiinpano/Harjoittele jae/Tutki jaetta) · ▶/⏸ (amber play/pause) · ☰Kaikki (→onShowAllVerses, only if set) · ▶next · │ · ⚙🔊ℹ️) via getCinemaPortalContainer, only while isActive. Header is ONLY the jae row (book-ikoni · "jakeet" · versesShown/referenceCount); the topic name + card N/M live in TopicFlow's single global "AIHE: N/M" title, not repeated here)
TopicVersesView (OLD grid model — now the list option; versePresentation toggle)
src/components/cinema/topic/TopicVersesView.tsx
useTopicVersePresentation (localStorage cinema/list, default cinema; toggle in settings sheet)
src/hooks/topic/useTopicVersePresentation.ts
CinemaVersePreviewPopup (Haku>Jakeet result → verse-in-chapter popup, per-verse jae-cinema)
src/components/cinema/CinemaVersePreviewPopup.tsx
Topic cinema is a 4th CinemaShell consumer (its own TopicFlow over a discriminated TopicCinemaStep[], NOT InfoFlow), reusing useSnapScrollSteps, CinemaShell, and InfoView variant="scroll" for info-block cards; background music only, no verse-TTS.
For full details — internal view stack (flow → chapter → verse) + portal/ESC-pino gotchas, CinemaVerseActions, TopicChapterReader picker gotchas, the TWO presentation models (cinema/list) + fixed bottom cluster + TopicFlow.suspended, verse-range/verse-order rules, get_chapter_bundle book normalization, TopicVerseCinema auto-advance/start-verse, the single global "AIHE: <nimi> N/M" title, the info-palanen card + jae-snapshot popup + author link, the author cinema-app, and the wrap-up layout/continuation paths: See references/topic-cinema.md.
CinemaShell (base layer ~438 lines)
│ Fullscreen, background visuals (Ken Burns), background music,
│ preferences, visual/music pickers, keyboard shortcuts (B/N/V/M)
│ Props: isOpen, onClose, title?, hideControls?, dimControls?
│ Exports: CinemaShellContext (render props for children)
│
├── CinemaReaderScreen (content layer ~1033 lines)
│ │ Verse fetching/mapping, Bible audio, auto-advance, audio sync,
│ │ completion overlay. Uses CinemaShell as wrapper via render props.
│ │ Modes: "chapter" | "verseList" | "summaryItems"
│ │
│ └── useDiscipleshipOrchestration (task queue ~792 lines)
│ │ Task queue, completion persistence, quiz insertion,
│ │ transition overlays, verse bar, kooste, prayer messages
│ │
│ └── Overlay Components
│ ├── DiscipleshipTaskSelector (pick tasks)
│ ├── DiscipleshipInlineTask (prayer/practice/quiz)
│ ├── DiscipleshipTransitionOverlay (between tasks)
│ └── DiscipleshipVerseBar (note/share on pause)
│
├── CuratedPlanCinema (program mode ~66 lines)
│ │ Wrapper: loads grand plan mini_tasks + progression_type via useCuratedPlanCinema.
│ │ CinemaShell with hideControls. Routes to flow based on progression_type:
│ │ progressionType === "scrolling" ? ScrollingPlanFlow : CuratedPlanFlow
│ │ On all-tasks-complete: calls complete_grand_plan RPC + invalidates ["grand-plans"].
│ │
│ ├── CuratedPlanFlow (continuous / linear ~133 lines)
│ │ │ One mini-task card at a time, AnimatePresence crossfade.
│ │ │ Scrim bg-black/40 + card bg-black/60 backdrop-blur-md.
│ │ │ completedIds Set; backward jumps un-complete the target.
│ │ │ Choice routing: next | jump_to_sort_order.
│ │ │
│ │ └── MiniTaskView (step UI ~223 lines)
│ │ SVG circular countdown timer, pause via DiscipleshipPomodoroButton,
│ │ JATKA hidden when choices exist (choice-to-advance).
│ │
│ └── ScrollingPlanFlow (scrolling ~336 lines)
│ Snap-scroll all steps on one page, numbered progress pills (click-to-jump),
│ IntersectionObserver (threshold 0.4) tracks active step,
│ per-step auto-advance timer (duration_seconds; 0/missing = off),
│ user-scroll detection pauses auto-advance 1.5s.
│ Inline StepContent (NOT MiniTaskView) — no countdown ring, no completedIds.
│ See references/curated-plans.md for full details.
│
├── InfoCinema (Q&A / topic info-palaset ~121 lines)
│ │ CinemaShell with hideControls; renders InfoFlow from parent-loaded
│ │ InfoBlocks. Mirrors CuratedPlanCinema. See references/info-cinema.md.
│ │
│ └── InfoFlow (one-page snap-scroll, card per block ~350 lines)
│ Uses useSnapScrollSteps (SHARED mechanics). Left progress rail +
│ numbered pills, per-card auto-advance, prayer-room-style bottom bar.
│ Optional finalSlide (CinemaQuestionWrapUp). key={contentKey} remount.
│
├── PrayerRoomScreen (prayer room consumer ~499 lines)
│ │ CinemaShell with hideControls; owns prayer/verse state, realtime sync,
│ │ PTT, WebRTC audio mesh, invitations. Route: /rukoushuone.
│ │ See references/prayer-room.md for full details.
│ │
│ └── Parallel runtime systems (scoped to config.id)
│ ├── usePrayerRoomSync (presence + host state broadcast, debounced 100ms)
│ ├── usePushToTalk (single-speaker floor + FIFO hand queue)
│ └── useWebRTCAudio (STUN-only audio mesh, 2–5 users)
│
└── Future: Meditation, etc.
└── CinemaShell + custom content
Audio Pipeline:
generate-audio Edge Function → ElevenLabs API (with timestamps)
→ audio_assets table (hash-cached) → audio_cues table (verse timing)
Audio Split
CinemaShell and CinemaReaderScreen both use useCinemaAudio but for different purposes:
CinemaShell: bibleAudioUrl: null (music-only)
CinemaReaderScreen: backgroundMusicUrl: null (Bible audio-only, music from CinemaShell context)
Cinema OS / Command Center
Orchestration layer that wraps the individual cinemas in one persistent
CinemaShell so the user moves Topic → Search → Question → Curated as a single
continuous session (music/background never reset — "älä poistu Cinemasta").
Route /cinema-os (entry card atop the Tänään page). Read
Docs/cinema/CINEMA-OS.md first.
Three separated concerns: Intent (serializable {type,payload}) →
Registry (intent.type → CinemaApp, declarative, no host branches) →
Navigation (stack reducer + history). Apps are shell-less content that
consume useCinemaShell(); they never render a CinemaShell.
Search → Topic chaining: SearchCinemaApp picks a topic →
nav.launch({type:"topic", payload:{slug}}) → OS pushes Topic Cinema; back
returns to Search. QuestionCinemaApp's info-block CTA →
nav.launch({type:"curated", ...}). Apps special-case nothing — they emit intents.
Invariants (load-bearing — don't break these)
Apps are shell-less. Consume useCinemaShell() (from
components/cinema/CinemaShellContext). The standalone wrappers
(TopicCinema, InfoCinema, CuratedPlanCinema) are LEFT UNTOUCHED for their
existing launch buttons; the OS apps (src/cinema-os/apps/*) are parallel
shell-less mirrors. Don't merge them yet.
Intent.payload = plain serializable data only (slug/id/query) — never
functions/objects. One rule → history + localStorage + deep-link + (Phase 2)
DB are the same representation. Derive auth/version/i18n INSIDE the app, not
in the payload.
Two-level back: apps with internal layers (Topic's flow→chapter→verse)
register useCinemaBackHandler(fn); the host's nav.back() (and ESC, shell X,
fullscreen-exit) runs interceptors LIFO first, then pops the OS stack.
Generalizes TopicCinema's old popOrClose. Question/Curated have NO internal
layers → no interceptor.
Only the top frame is mounted (Phase 1).nav.replace PRESERVES the frame
key (in-place payload update, no remount) — Search uses it to persist
query/tab so the topic round-trip restores them. Provider URL-sync is
path-based; replace does NOT record history. Keep-alive is a Phase 2 opt-in
(CinemaApp.keepAlive, default false, never Reader/PrayerRoom).
Only "destinations" are intents. Topic's chapter/verse stay internal
layers (transient, not URL-addressable). Only top-level intents get URLs.
Phase 1 base is /cinema-os so the /cinema landing (CinemaLandingPage) is
untouched. CinemaShell already portals to document.body (universal overlay
root) — apps must NOT add their own portal.
Phase 2 (deferred)
DB history (cinema_sessions/cinema_events), opt-in keep-alive,
browser-back↔OS-back, verses⇄topics swipe remain deferred.
Discipleship-tila is now an in-shell app (discipleship, done — the
previously-deferred CinemaReaderScreen migration). CinemaReaderScreen is
dual-mode: the embedded flag makes its outer wrapper skip its own
CinemaPreferencesProvider AND its CinemaShell, and CinemaReaderScreenContent
consumes useCinemaShell() instead of the render-prop ctx (the music-ref-sync just
reads the same CinemaShellContext). DiscipleshipCinemaApp feeds it
useDiscipleshipTasks() with no autoStart → full useDiscipleshipOrchestration
flow (selector → reading + prayer + practice + memory quiz + Nyt Kösete). ownsChrome: true (CinemaReader ControlBar + overlays replace OS pills; exit/ESC → nav.back,
completion → nav.home); heavy engine/audio mount-bound (never keep-alive). Standalone
/tanaan launch (TanaanModals) unchanged — it omits embedded. Same dual-mode +
ownsChrome template as the prayer room.
Kooste (summary) is an in-shell app too (summary, done). SummaryCinemaApp
resolves {summaryId?} (else active via useActiveSummary) → fetchSummaryDetail
(pages/summary/summaryDetailLoader) → buildCinemaItems (pages/summary/SummaryCinemaMode,
exported pure fns) → dual-mode CinemaReaderScreen embedded mode="summaryItems".
ownsChrome:true. Launched from the SearchCinemaApp "Muut" tab (Koosteet result)
and a Command Center "Kooste" tile. SearchCinemaApp now has 4 tabs — Jakeet, Aiheet,
Kysymykset (usePublishedQuestionSearch → question intent), Muut
(search_user_content RPC; only Koosteet links → summary, rest read-only). Strongs
is intentionally absent (no cinema target → per the "no link for non-cinema" rule).
The Jakeet tab is NOT an auto-carousel by default — it mirrors the base /search page.
Data comes from useUnifiedSearch (NOT the old useVersesSearch) → verses
(match_rank/is_word_form) + topicalVerses. Hits are ordered literal-first then
inflected (splitVersesByRank drops rank-5 fuzzy; compareByRankThenLocation within
each group). Classification = isInflectedHit(v, query): single-word query → inflected
iff the verse text lacks the query as an exact token (Finnish FTS returns inflections as
rank-1 WITHOUT is_word_form, so the rank/flag alone misses them); multi-word → falls back
to is_word_form || match_rank>=2. Inflected hits carry a TAIVUTUS badge. The
"Taivutuksin" filter uses exact token match (NOT startsWith — the base form would
otherwise match all its inflections and filter nothing).
This orderedVerses drives the whole cinema verse search (list + preview + "Aja"
carousel). Two filter dropdowns ("Kirjoittain" = books from results; "Taivutuksin" =
extractWordForms, EXPORTED from VerseFilterBar) are inline (NOT Radix Select → stay
visible in fullscreen), and live inside a framed RAJAUKSET block (corner tag,
verse-card width + border classes, amber when a filter is on, X Tyhjennä to clear all;
FilterDropdown.tsx). Choosing an option closes the dropdown and the chosen value shows
in the collapsed trigger — the web VerseFilterBar mirrors this interaction. The frame
must NOT get backdrop-blur: it creates a stacking context that traps the dropdown's
z-[10002] panel inside it, so the verse cards cover the open menu (raising z-index does
not help). Pagination 16/page; below it the
"Aiheeseen liittyvät jakeet" panel (topicalVerses deduped vs shown ids, VIA-topic
tag) — both click → CinemaVersePreviewPopup. Cards = wrapping dark boxes (flex flex-wrap); the snippet uses a cinema-local cinemaSnippet (match-LEADING, small
lead) NOT the wide-page snippetAroundMatch (preContext 60) — in the narrow w-[300px]line-clamp-2 card the wide pre-context pushed the highlight past the 2 visible lines;
leading "…" dropped at a clean word boundary. arrow keys rove the current page
(column count from DOM offsetTop); click/ENTER opens the preview (found verse in chapter
context, search term highlighted via the query prop + buildHighlightRegex,
per-verse Film button → TopicVerseCinema). "Aja jae-cinema" runs the full
carousel start→finish. Internal layers (list → preview → verseCinema, plus runAll) +
useCinemaBackHandler — same model as ReadingCinemaApp, NO new app.
Prayer Room is now an in-shell app (prayer-room, done — was previously a
separate-destination exclusion). PrayerRoomScreen is dual-mode: an
embedded flag switches between mounting its own CinemaShell (standalone
/rukoushuone) and consuming useCinemaShell() (under the host). It owns its
chrome (ownsChrome: true on the registry entry → CinemaOSChrome returns null
for it; PrayerRoomHeader/BottomBar replace the OS pills, header X = nav.back,
ESC closes an open invite/setup modal first via useCinemaBackHandler). Payload
{roomId?/startCalendarId?/create?} is resolved by PrayerRoomApp
(usePrayerRooms + resolveTodayPrayerRoomConfig/normalizeLoadedRoom); setup is
an internal layer. Realtime/PTT/WebRTC are mount-bound (never keep-alive) —
leaving the app leaves the room (channel removeChannel). The pattern (dual-mode
ownsChrome) is the template for migrating any other own-shell feature
(e.g. the deferred Reader).
Cinema Mode Components
CinemaShell
Reusable base layer at src/components/cinema/CinemaShell.tsx (~438 lines):
// Provides:// 1. Fullscreen modal shell (useCinemaFullscreen)// 2. Animated background visuals (CinemaBackground + GSAP Ken Burns)// 3. Background music (useCinemaAudio, music only)// 4. User preferences (useCinemaPreferences)// 5. Visual/music pickers + keyboard shortcuts (B/N/V/M)// 6. Render props: CinemaShellContext (audioState, selectedTrack, preferences, toggleMusic, etc.)// 7. dimControls prop to fade controls when overlays active
CinemaReaderScreen
Content layer at src/features/cinema/CinemaReaderScreen.tsx (~1033 lines):
// Wraps content in CinemaShell, adds:// 1. Map BundleVerse → CinemaVerse for cinema-reader package// 2. Bible audio playback (separate from CinemaShell's music)// 3. Handle verse navigation (index state)// 4. Auto-advance (WPM-based or audio cue-based)// 5. Audio sync (manual nav → immediate audio-seek to that verse, no prompt)// 6. Discipleship overlay rendering (delegates logic to useDiscipleshipOrchestration)// 7. Compact mode for phones — passes compact + sheet callbacks to CinemaReader
Mobile UI (Compact Mode)
On phones (useBreakpoint().isPhone || isCozy, <768px) Cinema Mode collapses the desktop two-row control panel + top-left HUD pills into a single row + two icon-triggered bottom sheets, freeing ~90px for the verse text.
Dashboard fix: get_today_dashboard checks completed_at::date = CURRENT_DATE for completion-type plans (not current_day which already advanced)
Client-side fix: useTodayTasks checks completed_days.includes(current_day - 1) for completion-type plans as fallback
Query invalidation: Must invalidate ["today-dashboard"], ["reading-plan-streaks"], AND ["user-reading-plans"]
Reading Plan Memory Quiz
useReadingPlanQuiz generates a 3-verse quiz after reading plan completion
(2 plan verses + 1 decoy). Selection priority: user-marked refs
(marked_refs in quiz task metadata, from nytKoosteRefsRef) → preferred
books (NT, Psalms, Proverbs) → any plan verse. Full algorithm:
references/discipleship-cinema.md.
Tanaan Page & DiscipleshipLandingPage
/tanaan sections (TodayTaskList, TomorrowTasksSection,
PermanentTasksSection, RecurringTasksSection, PracticeActivationCard)
and the /opetuslapseus marketing landing page are documented in
references/discipleship-cinema.md. Locations: Quick Reference → Discipleship
Layer / Pages.
Curated Plans (Grand Plans) & Progression Modes
Curated grand plans are a second CinemaShell consumer. The wrapper CuratedPlanCinema routes to one of two flow components based on the plan's progression_type, then renders inside CinemaShell hideControls.
handleClose → onClose(), then if completedRef && completionRedirectUrl → navigate(url) after 300ms
Critical Gotchas
continuous and linear are identical in code. Only "scrolling" branches — the other two both fall through to CuratedPlanFlow. Branch INSIDE the flow if you need them to differ, don't split the router.
ScrollingPlanFlow reimplements step UI. Inline StepContent is a simpler cousin of MiniTaskView (no timer, no pause). Behavioral changes to MiniTaskView do NOT propagate — fix both or refactor to share.
Snap-scroll mechanics are now extracted to useSnapScrollSteps, but ScrollingPlanFlow has NOT adopted it.src/hooks/useSnapScrollSteps.ts (active-index tracking, jump, user-scroll detection, per-step auto-advance) was extracted from ScrollingPlanFlow's proven behaviour and is used by InfoFlow. ScrollingPlanFlow still carries its own duplicate inline IntersectionObserver + timer. A snap-scroll fix in one does not reach the other — prefer migrating ScrollingPlanFlow onto the hook over editing both. Tuning constants (0.4 threshold, 1500ms user-scroll debounce) are deliberately matched; don't "clean up" either copy.
Completion order is load-bearing.completedRef.current = true must run BEFORE the await complete_grand_plan RPC — flow calls onAllTasksCompleted then onClose synchronously, and handleClose reads the ref immediately.
Two RPCs on mount.useCuratedPlanCinema fires get_grand_plan_items AND get_user_grand_plans (the latter only for progression_type). Consider folding progression_type into items if you refactor.
Default progression falls back to continuous. When the user isn't a member yet, get_user_grand_plans has no match → silent continuous default. Worth checking first when a plan "won't scroll".
ScrollingPlanFlow doesn't invoke complete_grand_plan. That's CuratedPlanCinema's job. A new flow must call onAllTasksCompleted() at the end, or the plan never marks complete.
Intersection threshold 0.4 and user-scroll debounce 1500ms are tuned. Lowering the threshold flickers active-index; shortening the debounce fires auto-advance mid-flick. Don't "clean up" these constants.
Info Cinema (Q&A / Topic Info-palaset)
Third CinemaShell consumer. Renders a question's/topic's published Info-palaset as a one-page vertical snap-scroll — one card at a time, top → bottom — the same UX as the curated ScrollingPlanFlow but for read-only answer content. This is the "yksi sivu jota mennään kortti kerrallaan alaspäin" flow.
QuestionAnswersCinema (the Film CTA, 3 variants) → InfoCinema (wrapper: CinemaShell hideControls + music/settings sheets, mirrors CuratedPlanCinema) → InfoFlow (the snap-scroll) → InfoView variant="scroll" (one block card).
Mechanics come from the shared useSnapScrollSteps hook — InfoFlow does NOT reimplement the IntersectionObserver. Per-card auto-advance from each block's duration_seconds (0 = none); user-scroll pauses it; last card never auto-advances.
finalSlide render-prop: optional extra snap-step after the blocks (index blocks.length), e.g. CinemaQuestionWrapUp (feedback bar + related questions). Gets { closeCinema, isActive }. Left-rail marker is a HelpCircle (not a number), auto-advance disabled. onFinalSlideEnter = its Enter-key default action (e.g. "Näytä seuraava kysymys"); Enter is reserved for this — never add Enter-to-advance on content cards.
contentKey remount pattern: InfoCinema passes contentKey (the questionId) to <InfoFlow key={contentKey}> → CinemaShell stays mounted (bg/music continue) while InfoFlow remounts (scroll resets) → seamless "next question" transition without leaving cinema. Same family as reading-plan key={cinema-day-N}.
InfoView is dual-variant: variant="scroll" (cinema) vs the page InfoTextCard. Same block, two renderers — edit the one the task asks for (see questions_answers manifest domain).
Renders nothing with 0 published+non-hidden blocks (CTA hidden, InfoCinema returns null). A "Cinema button missing" report usually means the publish gate, not cinema code.
Prayer Room (Rukoushuone)
Realtime multiplayer full-screen prayer experience built on CinemaShell (not CinemaReader). Canonical example of a CinemaShell-only consumer: reuses fullscreen + Ken Burns + background music + preferences, adds its own content, host-driven realtime sync, push-to-talk, and WebRTC audio mesh on top.
Route: /rukoushuone → PrayerRoomPage → PrayerRoomScreen(initialConfig?, onClose). Passing initialConfig opens straight into "room" view (skips setup) — this is how every "launch" entry point works.
Prayer-calendar Cinema launch (CalendarDetailView, /rukouskalenterit?cal=<id>): a Film "Cinema" button navigates navigate("/rukoushuone", { state: { config } }) with the full today-room sources on (useMyCalendar/useSubscribedCalendars: true) plus calendarId (guarantees this calendar's prayers are present even if unsubscribed + enables per-day nav) and the transient startCalendarId field. PrayerRoomScreen runs a one-shot effect that jumps currentPrayerIndex to the first prayer whose calendarId === startCalendarId once the aggregated list has loaded it — so the room opens on that calendar but the rest stays arrow-navigable. startCalendarId is UI-only — NOT persisted (the prayer_rooms mutations whitelist columns), so saved/reopened rooms never re-trigger the jump.
Two views: "setup" (form) and "room" (CinemaShell hideControls + render props)
isHost = !config.id || !user ? true : config.hostUserId === user.id — solo sessions and anonymous users are always host
Content sources (merged + deduped by baseId = id.replace(/^(cal-|mine-|sub-)/, "")):
Ad-hoc config.prayers (uuid ids)
useMyCalendar → user's own + followed active prayers (mine-<id>)
useSubscribedCalendars minus mutedCalendarIds (sub-<id>)
config.calendarId extra browsing calendar (cal-<id>)
Legacy rooms normalize useMyCalendar/useSubscribedCalendars to true; new rooms default toggles to false in setup — asymmetric by design
Host keyboard: ArrowLeft/Right (prayer nav), ArrowUp/Down (calendar day, only if config.calendarId)
Left weekday rail (PrayerRoomCalendarRail + useCalendarRailModel): for prayer-calendar cards (currentPrayer.source === "calendar") a vertical left rail shows the calendar's own scheduled weekdays (Mon–Sun, via getScheduledWeekdays), with the currently-browsed day as the single active marker — green, not amber, and no past/future colouring (unlike InfoFlow/ScrollingPlanFlow). Driven by calendarDayOffsets[currentPrayer.calendarId] (works for subscribed calendars too, not just config.calendarId). Moves with ArrowUp/Down + header chevrons; host can click a marker to jump (offset delta → shiftCurrentCalendarDay + setCurrentPrayerIndex(0)). useCalendarRailModel is a pure useMemo placed after the realtime hooks (no hook-order risk).
Verses: OSIS is canonical removal key; adds and removes auto-persist when config.id exists
Realtime Multiplayer
Channel: prayer-room:<roomId> via Supabase Realtime.
Presence (keyed by user.id): displayName, avatarUrl fetched from profiles before tracking
Broadcast "sync": Host sends { currentPrayerIndex, currentVerseIndex, calendarDayOffset } debounced 100ms; participants apply in useEffect gated on !isHost && syncState
Push-to-talk (PTT): Single speaker at a time; FIFO handQueue; host grants via avatar click or autoGrantNext on release/leave; events: raise_hand, grant_talk, release_talk
WebRTC audio mesh: STUN-only (stun:stun.l.google.com:19302), 2–5 users, one RTCPeerConnection + <audio> per peer, signaling over the same channel; micOpen = activeSpeakerId === userId && pttPressed toggles track enable
Channel ownership: usePrayerRoomSync owns subscribe/unsubscribe. usePushToTalk and useWebRTCAudio attach listeners but do NOT unsubscribe.
Invitations
Table public.prayer_room_invitations with invitee_id OR invitee_email, status pending|accepted|declined. Invite flow auto-persists the room first via createRoom.mutateAsync when opening the invite dialog from an unsaved config.
Critical Gotchas
Modals inline, not portaled. Setup, invite, verse-full popup, and remove-verse confirmation render as absolute inset-0 z-[10003] children of the CinemaShell subtree. Do NOT use shadcn AlertDialog or Dialog — both auto-portal via Radix to document.body and become invisible inside the fullscreen element. asDialog / inline props on child dialogs exist for this. See references/learnings.md → "Radix Dialog/AlertDialog Auto-Portals" for the full pattern.
Z-index stack: CinemaShell base < Header/BottomBar z-[10002] < inline modals z-[10003]. Don't invent new values.
Title pill is rendered by PrayerRoomHeader, not CinemaShell. Pass NO title prop to <CinemaShell> from PrayerRoomScreen — the header renders the pill on its own row 1 alongside the prayer-count nav. Adding the title back to CinemaShell yields a stacked duplicate.
calendarName already includes "Rukouskalenteri" prefix. It comes from the DB name column (e.g. "Rukouskalenteri Suomi"). Don't prepend prayerRoom.header.calendarPrefix again — that produced the duplicate "Rukouskalenteri Rukouskalenteri Suomi" bug. The Calendar icon next to the name carries the semantics.
Prayer typography matches .cinema-verse-text. PrayerRoomContent's prayer body uses clamp(36px, 5vw, 56px) + min(1100px, 88vw) lane — exactly the same as cinema-reader's verse text (packages/cinema-reader/src/styles/cinema.css). The middle container must NOT have max-w-2xl mx-auto or any narrower cap that would clip this lane.
src/hooks/usePrayerRoom.ts is unused. PrayerRoomScreen manages state inline; do not "refactor to use the hook" without migrating sync/PTT/WebRTC plumbing too.
prayer_rooms / prayer_room_invitations not in types.ts — hooks still use (supabase as any). When types regenerate, drop the casts (see /supabase-typing-architect).
STUN-only mesh fails on symmetric NAT / strict firewalls. Add TURN + SFU if scaling beyond 5 users.
Common Tasks
Add New Voice
Get voice ID from ElevenLabs → add to elevenLabsVoices.ts → add to admin UI → use elevenlabs:{voiceId} format
Debug Audio Sync
Add interval logging in useCinemaAudio to trace findCurrentCue() output.
references/curated-plans.md - Curated grand plans: progression modes (continuous / linear / scrolling), flow routing, completion RPC, MiniTask data model