From 9a68d5e254e68ea5dc83825e4cd0a1864099a0a3 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 14 Jun 2026 14:20:53 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(game):=20Sudden=20Death=20mode=20?= =?UTF-8?q?=E2=80=94=20last-place=20player=20eliminated=20per=20round=20(#?= =?UTF-8?q?827)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New opt-in mode where the lowest round-scoring player is eliminated each round from round 2 on, until one player is left standing. Can be set in the wizard (defaults ON with >=3 players) or toggled live by the host from the reveal-screen control bar. Backend (game/): - GameState.sudden_death_mode (config + create_game + rematch-preserved). - PlayerSession.eliminated / eliminated_round (persist across rounds, reset per game). - Elimination after scoring in _end_round_unlocked: lowest round delta, tie-break by slowest submission (non-submitter = slowest); round 1 never eliminates; never cuts the last survivor. - start_round auto-ends the game when one player remains. - all_submitted / early-reveal ignore eliminated players. - set_sudden_death(enabled) live toggle + /beatify/api/sudden-death view. - "Last One Standing" superlative for the survivor. - WS state: sudden_death_mode, players[].eliminated/eliminated_round, REVEAL eliminated_this_round, leaderboards carry eliminated state. Frontend (www/): - Wizard mode card (default ON, disabled + tooltip when <3 players). - Player: "You're out" eliminated view + leaderboard skull/dim; submit gated. - Admin: reveal-screen live toggle, elimination + FINAL arc chips, submission tracker + leaderboard skip/dim eliminated. - Dashboard TV: full-bleed "OUT" takeover, FINAL banner, "Last One Standing" trophy hero; leaderboard dim. - i18n keys across en/de/es/fr/nl; regenerated .min bundles. Tests: 17 new unit tests in test_state.py (elimination, tie-break, round-1 skip, auto-end, all-submitted, persistence, live toggle, superlative). Co-Authored-By: Claude Opus 4.8 (1M context) --- custom_components/beatify/__init__.py | 2 + custom_components/beatify/game/config.py | 3 + custom_components/beatify/game/player.py | 8 + .../beatify/game/player_registry.py | 11 +- custom_components/beatify/game/scoring.py | 25 ++ custom_components/beatify/game/serializers.py | 14 ++ custom_components/beatify/game/state.py | 68 +++++ .../beatify/game/state_leaderboard.py | 6 + .../beatify/game/state_lifecycle.py | 13 + custom_components/beatify/game/state_setup.py | 4 + .../beatify/server/game_views.py | 42 ++++ custom_components/beatify/server/views.py | 1 + custom_components/beatify/www/css/styles.css | 200 +++++++++++++++ custom_components/beatify/www/dashboard.html | 22 ++ custom_components/beatify/www/i18n/de.json | 19 +- custom_components/beatify/www/i18n/en.json | 19 +- custom_components/beatify/www/i18n/es.json | 19 +- custom_components/beatify/www/i18n/fr.json | 19 +- custom_components/beatify/www/i18n/nl.json | 19 +- custom_components/beatify/www/js/admin.js | 132 ++++++++++ custom_components/beatify/www/js/admin.min.js | 34 +-- .../www/js/admin/sections/render-helpers.js | 21 +- custom_components/beatify/www/js/dashboard.js | 148 ++++++++++- .../beatify/www/js/dashboard.min.js | 2 +- .../beatify/www/js/player-game.js | 142 ++++++++++- .../beatify/www/js/player.bundle.min.js | 4 +- custom_components/beatify/www/js/wizard.js | 44 +++- custom_components/beatify/www/player.html | 15 ++ tests/unit/test_state.py | 236 ++++++++++++++++++ 29 files changed, 1234 insertions(+), 58 deletions(-) diff --git a/custom_components/beatify/__init__.py b/custom_components/beatify/__init__.py index 7916b6a4..18ae765d 100644 --- a/custom_components/beatify/__init__.py +++ b/custom_components/beatify/__init__.py @@ -52,6 +52,7 @@ SavePlaylistView, SwJsView, RematchGameView, + SetSuddenDeathView, SongStatsView, StartGameplayView, StartGameView, @@ -217,6 +218,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.http.register_view(TtsTestView(hass)) hass.http.register_view(StartGameView(hass)) hass.http.register_view(StartGameplayView(hass)) + hass.http.register_view(SetSuddenDeathView(hass)) # Issue #827 hass.http.register_view(EndGameView(hass)) hass.http.register_view( ForceResetView(hass) diff --git a/custom_components/beatify/game/config.py b/custom_components/beatify/game/config.py index 43b65ab8..da86eba7 100644 --- a/custom_components/beatify/game/config.py +++ b/custom_components/beatify/game/config.py @@ -50,6 +50,9 @@ class GameStateConfig: # Mode flags closest_wins_mode: bool = False + # Issue #827: Sudden Death — last-place player eliminated each round. + # Can be set at game start (wizard) or toggled live from the reveal screen. + sudden_death_mode: bool = False # Issue #1180: Title & Artist guessing mode. Owned by ChallengeManager; # listed here for reset symmetry. GameState exposes a delegation property, # and _apply_config skips manager-delegated names (see field_names()). diff --git a/custom_components/beatify/game/player.py b/custom_components/beatify/game/player.py index d422c47d..7b858142 100644 --- a/custom_components/beatify/game/player.py +++ b/custom_components/beatify/game/player.py @@ -74,6 +74,10 @@ class PlayerSession: # Leaderboard tracking (Story 5.5) previous_rank: int | None = None # Rank before last update + # Sudden Death tracking (Issue #827) - CUMULATIVE, NOT reset in reset_round() + eliminated: bool = False # True once eliminated; stays out for the rest of the game + eliminated_round: int | None = None # Round number the player was eliminated in + # Final stats tracking (Story 5.6) - CUMULATIVE, NOT reset in reset_round() best_streak: int = 0 # Highest streak achieved during game rounds_played: int = 0 # Rounds the player participated in @@ -202,6 +206,10 @@ def reset_for_new_game(self) -> None: # Reset intro mode cumulative tracking (Issue #23) self.intro_speed_bonuses = 0 + # Reset Sudden Death state (Issue #827) + self.eliminated = False + self.eliminated_round = None + # Reset movie bonus cumulative tracking (Issue #28) self.movie_bonus_total = 0 diff --git a/custom_components/beatify/game/player_registry.py b/custom_components/beatify/game/player_registry.py index 5c793471..6ab677f5 100644 --- a/custom_components/beatify/game/player_registry.py +++ b/custom_components/beatify/game/player_registry.py @@ -199,6 +199,10 @@ def get_players_state(self) -> list[dict[str, Any]]: "bet": p.bet, "steal_used": p.steal_used, "onboarded": p.onboarded, + # Issue #827: Sudden Death — eliminated players render the + # spectator view and a skull badge on leaderboards. + "eliminated": p.eliminated, + "eliminated_round": p.eliminated_round, } for p in self.players.values() ] @@ -208,9 +212,12 @@ def all_submitted(self) -> bool: Uses ``is_active`` rather than the raw ``connected`` flag so a stale ghost (closed WebSocket not yet cleaned up) can't block early reveal - for the whole room — #928. + for the whole room — #928. Eliminated players (#827) never submit, so + they are excluded from the all-submitted (early reveal) check. """ - active_players = [p for p in self.players.values() if p.is_active] + active_players = [ + p for p in self.players.values() if p.is_active and not p.eliminated + ] if not active_players: return False return all(p.submitted for p in active_players) diff --git a/custom_components/beatify/game/scoring.py b/custom_components/beatify/game/scoring.py index 6ef6e532..06de7ee5 100644 --- a/custom_components/beatify/game/scoring.py +++ b/custom_components/beatify/game/scoring.py @@ -415,6 +415,24 @@ def _superlative_risk_taker(players: list[PlayerSession]) -> dict[str, Any] | No return _award("risk_taker", "🎲", most[0].name, most[1], "bets") +def _superlative_last_one_standing( + players: list[PlayerSession], +) -> dict[str, Any] | None: + """Issue #827: the sole survivor of a Sudden Death game. + + Awarded only when exactly one player was never eliminated while at least + one other player was — i.e. a Sudden Death game that actually ran to its + 1v1 conclusion. + """ + survivors = [p for p in players if not p.eliminated] + eliminated = [p for p in players if p.eliminated] + if len(survivors) != 1 or not eliminated: + return None + return _award( + "last_one_standing", "💀", survivors[0].name, len(eliminated), "eliminated" + ) + + def _superlative_clutch_player( players: list[PlayerSession], rounds_played: int ) -> dict[str, Any] | None: @@ -646,6 +664,7 @@ def calculate_superlatives( movie_quiz_enabled: bool = False, intro_mode_enabled: bool = False, title_artist_mode_enabled: bool = False, + sudden_death_mode_enabled: bool = False, ) -> list[dict[str, Any]]: """Calculate fun awards based on game performance (Story 15.2).""" if not players: @@ -655,6 +674,12 @@ def calculate_superlatives( # never qualify (their counters aren't tracked), so the TA-native # awards take their slots near the top of the priority order. builders = [ + # Issue #827: the marquee award for a Sudden Death game leads the reel. + ( + _superlative_last_one_standing(players) + if sudden_death_mode_enabled + else None + ), _superlative_speed_demon(players), _superlative_lucky_streak(players), _superlative_perfect_pair(players) if title_artist_mode_enabled else None, diff --git a/custom_components/beatify/game/serializers.py b/custom_components/beatify/game/serializers.py index 1d6dec62..a752eedf 100644 --- a/custom_components/beatify/game/serializers.py +++ b/custom_components/beatify/game/serializers.py @@ -48,6 +48,9 @@ def serialize(gs: GameState) -> dict[str, Any] | None: "intro_mode_enabled": gs.intro_mode_enabled, # Issue #442: Closest Wins mode "closest_wins_mode": gs.closest_wins_mode, + # Issue #827: Sudden Death mode (drives wizard chip, player view, + # leaderboard cut-line, admin live toggle) + "sudden_death_mode": gs.sudden_death_mode, # #1180: Title & Artist guessing mode (player UI renders inputs) "title_artist_mode": gs.title_artist_mode, "is_intro_round": gs.is_intro_round, @@ -154,6 +157,14 @@ def _add_reveal_state(gs: GameState, state: dict[str, Any]) -> None: } # Include reveal-specific player data (guesses, round_score, missed) state["players"] = GameStateSerializer.get_reveal_players_state(gs) + # Issue #827: Sudden Death — names eliminated *this* round drive the + # TV "OUT" takeover + the admin elimination highlight card. + if gs.sudden_death_mode: + state["eliminated_this_round"] = [ + p.name + for p in gs.players.values() + if p.eliminated and p.eliminated_round == gs.round + ] # Leaderboard (Story 5.5) state["leaderboard"] = gs.get_leaderboard() # Round analytics (Story 13.3 AC4) @@ -269,6 +280,9 @@ def get_reveal_players_state(gs: GameState) -> list[dict[str, Any]]: "stole_from": p.stole_from, "was_stolen_by": p.was_stolen_by.copy() if p.was_stolen_by else [], "steal_available": p.steal_available, + # Issue #827: Sudden Death state + "eliminated": p.eliminated, + "eliminated_round": p.eliminated_round, } # Story 20.4: Add artist bonus if challenge is enabled if gs.artist_challenge_enabled: diff --git a/custom_components/beatify/game/state.py b/custom_components/beatify/game/state.py index 23de1844..ca27f33a 100644 --- a/custom_components/beatify/game/state.py +++ b/custom_components/beatify/game/state.py @@ -344,6 +344,9 @@ def __init__(self, time_fn: Callable[[], float] | None = None) -> None: # Issue #442: Closest Wins mode self.closest_wins_mode: bool = False + # Issue #827: Sudden Death mode (last-place player eliminated per round) + self.sudden_death_mode: bool = False + # Issue #477: Admin spectator WebSocket (host without being a player) self._admin_ws: web.WebSocketResponse | None = None @@ -724,6 +727,11 @@ async def _end_round_unlocked(self) -> None: # Phase 2: scoring pass (year/title-artist), closest-wins, round_results self._score_round(correct_year) + # Issue #827: Sudden Death — after scoring, eliminate the lowest + # round-scoring survivor (from round 2 on). Runs before REVEAL so the + # elimination is part of the reveal broadcast. + self._apply_sudden_death_elimination() + # Phase 3: highlights, round analytics, persisted song-result stats await self._record_round_stats(correct_year) @@ -751,6 +759,65 @@ async def _end_round_unlocked(self) -> None: "No round_end callback set - REVEAL state will not be broadcast!" ) + # ------------------------------------------------------------------ + # Sudden Death mode (Issue #827) + # ------------------------------------------------------------------ + + def non_eliminated_players(self) -> list[PlayerSession]: + """Players still in the game (not yet eliminated). Issue #827.""" + return [p for p in self.players.values() if not p.eliminated] + + def _apply_sudden_death_elimination(self) -> list[str]: + """Eliminate the lowest round-scoring survivor(s). Issue #827. + + Runs after scoring, from round 2 onward (round 1 never eliminates). + Among non-eliminated players, the one with the lowest *round* score + (this round's delta, not cumulative) is eliminated. A tie for last is + broken by submission speed: the slowest (latest) submitter is out, and + a non-submitter counts as the slowest of all. Returns the names + eliminated this round (empty when nothing happens). + + Caller holds ``_score_lock`` (invoked from ``_end_round_unlocked``). + """ + if not self.sudden_death_mode or self.round < 2: + return [] + + survivors = self.non_eliminated_players() + # Never eliminate the last player standing — the auto-end guard in + # start_round carries a 1-survivor game to END instead. + if len(survivors) <= 1: + return [] + + min_round_score = min(p.round_score for p in survivors) + tied_for_last = [p for p in survivors if p.round_score == min_round_score] + + # Slowest among the tied: a non-submitter (submission_time is None) is + # the slowest of all; otherwise the latest submission_time loses. + loser = max( + tied_for_last, + key=lambda p: (p.submission_time is None, p.submission_time or 0.0), + ) + loser.eliminated = True + loser.eliminated_round = self.round + _LOGGER.info( + "Sudden Death: eliminated %s in round %d (round score %d)", + loser.name, + self.round, + loser.round_score, + ) + return [loser.name] + + def set_sudden_death(self, enabled: bool) -> bool: + """Toggle Sudden Death mid-game from the reveal screen. Issue #827. + + Returns the new state. Turning it ON arms eliminations starting next + round; the current round's results stand. Turning it OFF stops further + cuts but already-eliminated players stay out. + """ + self.sudden_death_mode = bool(enabled) + _LOGGER.info("Sudden Death mode set to %s (live toggle)", self.sudden_death_mode) + return self.sudden_death_mode + def _schedule_reveal_advance(self) -> None: """Schedule the REVEAL vote window or auto-advance task (#1272). @@ -830,4 +897,5 @@ def calculate_superlatives(self) -> list[dict[str, Any]]: movie_quiz_enabled=self.movie_quiz_enabled, intro_mode_enabled=self.intro_mode_enabled, title_artist_mode_enabled=self.title_artist_mode, + sudden_death_mode_enabled=self.sudden_death_mode, ) diff --git a/custom_components/beatify/game/state_leaderboard.py b/custom_components/beatify/game/state_leaderboard.py index 5f4b8646..e5ee66f5 100644 --- a/custom_components/beatify/game/state_leaderboard.py +++ b/custom_components/beatify/game/state_leaderboard.py @@ -70,6 +70,9 @@ def get_leaderboard(self) -> list[dict[str, Any]]: "is_admin": player.is_admin, "rank_change": rank_change, "connected": player.connected, + # Issue #827: Sudden Death cut-line rendering + "eliminated": player.eliminated, + "eliminated_round": player.eliminated_round, } leaderboard.append(entry) @@ -125,6 +128,9 @@ def get_final_leaderboard(self) -> list[dict[str, Any]]: "best_streak": player.best_streak, "rounds_played": player.rounds_played, "bets_won": player.bets_won, + # Issue #827: Sudden Death + "eliminated": player.eliminated, + "eliminated_round": player.eliminated_round, } leaderboard.append(entry) diff --git a/custom_components/beatify/game/state_lifecycle.py b/custom_components/beatify/game/state_lifecycle.py index d0fcf053..b866d508 100644 --- a/custom_components/beatify/game/state_lifecycle.py +++ b/custom_components/beatify/game/state_lifecycle.py @@ -166,6 +166,19 @@ async def start_round(self, _retry_count: int = 0) -> bool: _LOGGER.error("No playlist manager configured") return False + # Issue #827: Sudden Death — when only one player is left standing, the + # game is over. Carry the just-finished round's REVEAL through to END + # rather than starting another round. round >= 2 guards against ending + # a fresh game (round 1 never eliminates). + if ( + self.sudden_death_mode + and self.round >= 2 + and len(self.non_eliminated_players()) <= 1 + ): + _LOGGER.info("Sudden Death: one player remains — ending game") + self._set_phase(GamePhase.END) + return False + # Get next playable song (skip songs without URI for selected provider) song = self._playlist_manager.get_next_song() if not song: diff --git a/custom_components/beatify/game/state_setup.py b/custom_components/beatify/game/state_setup.py index 00e1faff..c31ebd46 100644 --- a/custom_components/beatify/game/state_setup.py +++ b/custom_components/beatify/game/state_setup.py @@ -116,6 +116,7 @@ def create_game( # noqa: PLR0913 movie_quiz_enabled: bool = True, intro_mode_enabled: bool = False, closest_wins_mode: bool = False, + sudden_death_mode: bool = False, title_artist_mode: bool = False, reveal_auto_advance: int = 0, ) -> dict[str, Any]: @@ -272,6 +273,8 @@ def create_game( # noqa: PLR0913 # Issue #442: Set closest wins mode self.closest_wins_mode = closest_wins_mode + # Issue #827: Set sudden death mode + self.sudden_death_mode = sudden_death_mode self.is_intro_round = False self.intro_stopped = False self._round_manager._intro_round_start_time = None @@ -392,6 +395,7 @@ def rematch_game(self) -> None: "movie_quiz_enabled": self.movie_quiz_enabled, "intro_mode_enabled": self.intro_mode_enabled, "closest_wins_mode": self.closest_wins_mode, + "sudden_death_mode": self.sudden_death_mode, "title_artist_mode": self.title_artist_mode, } diff --git a/custom_components/beatify/server/game_views.py b/custom_components/beatify/server/game_views.py index 94bc03cc..f5c61c36 100644 --- a/custom_components/beatify/server/game_views.py +++ b/custom_components/beatify/server/game_views.py @@ -134,6 +134,7 @@ async def post(self, request: web.Request) -> web.Response: # noqa: PLR0911, PL movie_quiz_enabled = body.get("movie_quiz_enabled", True) # Issue #28 intro_mode_enabled = body.get("intro_mode_enabled", False) # Issue #23 closest_wins_mode = body.get("closest_wins_mode", False) # Issue #442 + sudden_death_mode = bool(body.get("sudden_death_mode", False)) # Issue #827 title_artist_mode = body.get("title_artist_mode", False) # #1180 reveal_auto_advance = body.get("reveal_auto_advance", 0) # #1012 party_lights_config = body.get("party_lights") # Issue #331 @@ -324,6 +325,7 @@ async def post(self, request: web.Request) -> web.Response: # noqa: PLR0911, PL "movie_quiz_enabled": movie_quiz_enabled, # Issue #28 "intro_mode_enabled": intro_mode_enabled, # Issue #23 "closest_wins_mode": closest_wins_mode, # Issue #442 + "sudden_death_mode": sudden_death_mode, # Issue #827 "title_artist_mode": title_artist_mode, # #1180 "reveal_auto_advance": reveal_auto_advance, # #1012 } @@ -610,6 +612,46 @@ async def post(self, request: web.Request) -> web.Response: return web.json_response({"success": True, "phase": game_state.phase.value}) +class SetSuddenDeathView(BeatifyAdminView): + """Toggle Sudden Death mode live during a game (Issue #827). + + The host flips Sudden Death on/off from the reveal-screen control bar. + Turning it ON arms eliminations from the next round; turning it OFF stops + further cuts (already-eliminated players stay out). + """ + + url = "/beatify/api/sudden-death" + name = "beatify:api:sudden-death" + # Match the other control-bar actions: auth handled in-handler so the + # Companion-bypass path works (#1131). + requires_auth = False + + async def post(self, request: web.Request) -> web.Response: + """Set the live Sudden Death flag and rebroadcast state.""" + if not is_authorized_http(request, self.hass): + return _json_error("Unauthorized", 401, code="UNAUTHORIZED") + + try: + body = await request.json() + except (ValueError, UnicodeDecodeError): + return _json_error("Invalid JSON", 400, code="INVALID_REQUEST") + + enabled = bool(body.get("enabled", False)) + + data = self.hass.data.get(DOMAIN, {}) + game_state = data.get("game") + if not game_state or not game_state.game_id: + return _json_error("No active game", 404, code="GAME_NOT_FOUND") + + new_state = game_state.set_sudden_death(enabled) + + ws_handler = data.get("ws_handler") + if ws_handler: + await ws_handler.broadcast_state() + + return web.json_response({"success": True, "sudden_death_mode": new_state}) + + class GameStatusView(HomeAssistantView): """Check game status for player page.""" diff --git a/custom_components/beatify/server/views.py b/custom_components/beatify/server/views.py index cf8444be..c3428bd4 100644 --- a/custom_components/beatify/server/views.py +++ b/custom_components/beatify/server/views.py @@ -47,6 +47,7 @@ ForceResetView, GameStatusView, RematchGameView, + SetSuddenDeathView, StartGameplayView, StartGameView, ) diff --git a/custom_components/beatify/www/css/styles.css b/custom_components/beatify/www/css/styles.css index f9f7b7b9..51e18524 100644 --- a/custom_components/beatify/www/css/styles.css +++ b/custom_components/beatify/www/css/styles.css @@ -15931,3 +15931,203 @@ body.theme-dark .your-result-stats .stat-value { -webkit-box-orient: vertical; overflow: hidden; } + +/* ===================================================================== + Sudden Death mode (Issue #827) + ===================================================================== */ + +/* --- Admin reveal: elimination + final arc chips --- */ +.arc-chip--elim { + color: var(--color-error-neon); + background: rgba(255, 0, 64, 0.12); + border: 1px solid rgba(255, 0, 64, 0.5); + text-transform: uppercase; +} + +/* --- Admin control-bar live toggle (S7-A) --- */ +.sd-live-toggle { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 5px 10px; + border-radius: var(--radius-md); + background: var(--color-bg-surface); + border: 1px solid rgba(255, 255, 255, 0.12); + font-family: var(--font-body); + font-size: 12px; + font-weight: 600; + color: var(--color-text-white); + cursor: pointer; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} +.sd-live-toggle:disabled { opacity: 0.4; cursor: not-allowed; } +.sd-live-toggle.is-on { + border-color: var(--color-accent-primary); + background: rgba(255, 45, 106, 0.08); + box-shadow: var(--glow-primary); +} +.sd-live-toggle__switch { + position: relative; + width: 30px; height: 17px; + border-radius: var(--radius-full); + background: rgba(255, 255, 255, 0.15); + flex: none; +} +.sd-live-toggle.is-on .sd-live-toggle__switch { + background: var(--color-accent-primary); +} +.sd-live-toggle__switch::after { + content: ""; position: absolute; + width: 13px; height: 13px; border-radius: 50%; + background: #fff; top: 2px; left: 2px; + transition: left var(--transition-fast); +} +.sd-live-toggle.is-on .sd-live-toggle__switch::after { left: 15px; } + +/* --- Player eliminated view (S2-A Stark Blackout) --- */ +.eliminated-view { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: var(--space-sm); + min-height: 60vh; + padding: var(--space-xl) var(--space-md); +} +.eliminated-orb { + width: 120px; height: 120px; + border-radius: var(--radius-full); + overflow: hidden; + filter: grayscale(1) brightness(0.5); + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, #2a2540, #16131f); + margin-bottom: var(--space-md); + position: relative; +} +.eliminated-orb img { width: 100%; height: 100%; object-fit: cover; } +.eliminated-skull { font-size: 46px; position: absolute; } +.eliminated-title { + font-family: var(--font-display); + font-weight: 900; + font-size: var(--font-size-4xl); + color: var(--color-text-white); + letter-spacing: -0.02em; +} +.eliminated-sub { color: var(--color-text-neon-muted); font-size: var(--font-size-base); } +.eliminated-hint { color: var(--color-text-dim); font-size: var(--font-size-sm); margin-top: var(--space-md); } + +/* --- Player leaderboard cut line (S3-A) --- */ +.cut-line { + position: relative; + height: 0; + border-top: 2px solid var(--color-accent-primary); + box-shadow: 0 0 12px rgba(255, 45, 106, 0.7); + margin: var(--space-md) 0; +} +.cut-line__label { + position: absolute; + top: -9px; left: 50%; + transform: translateX(-50%); + background: var(--color-bg-primary); + padding: 0 10px; + font-family: var(--font-body); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.15em; + color: var(--color-accent-primary); +} +.player-indicator.is-eliminated, +.leaderboard-entry.is-eliminated { opacity: 0.45; } +.player-out-badge { + font-size: 10px; + font-weight: 700; + color: var(--color-text-dim); + letter-spacing: 0.04em; +} + +/* --- Spectator TV: full-bleed "OUT" takeover (S4-C) --- */ +.sd-out-overlay { + position: fixed; + inset: 0; + z-index: 900; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(6, 6, 12, 0.96); +} +.sd-out-overlay.show { display: flex; animation: sd-out-in var(--transition-slow) ease-out; } +.sd-out__word { + font-family: var(--font-display); + font-weight: 900; + font-size: clamp(120px, 26vw, 360px); + line-height: 0.85; + color: var(--color-error-neon); + text-shadow: 0 0 40px rgba(255, 0, 64, 0.7); +} +.sd-out__name { + display: flex; align-items: center; gap: 14px; + margin-top: var(--space-md); + font-family: var(--font-display); + font-weight: 900; + font-size: var(--font-size-4xl); + color: var(--color-text-white); +} +.sd-out__skull { font-size: 40px; } +@keyframes sd-out-in { + from { opacity: 0; transform: scale(0.85); } + to { opacity: 1; transform: scale(1); } +} + +/* --- Spectator TV: FINAL banner (S5-C arena) --- */ +.sd-final-banner { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 18px; + border-radius: var(--radius-full); + font-family: var(--font-display); + font-weight: 900; + letter-spacing: 0.06em; + color: var(--color-accent-primary); + background: rgba(255, 45, 106, 0.12); + border: 1px solid rgba(255, 45, 106, 0.5); + box-shadow: 0 0 16px rgba(255, 45, 106, 0.3); +} + +/* --- END: Last One Standing superlative + trophy hero (S6-C) --- */ +.superlative-card--last_one_standing { + box-shadow: 0 0 18px rgba(255, 45, 106, 0.3); + border-color: rgba(255, 45, 106, 0.4); +} +.sd-last-standing { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-sm); + text-align: center; + margin-bottom: var(--space-lg); +} +.sd-last-standing__trophy { font-size: 40px; } +.sd-last-standing__headline { + font-family: var(--font-display); + font-weight: 900; + font-size: var(--font-size-hero); + letter-spacing: -0.02em; + background: linear-gradient(90deg, var(--color-accent-primary), var(--color-accent-secondary)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 40px rgba(255, 45, 106, 0.5); +} +.sd-last-standing__winner { + font-family: var(--font-body); + font-weight: 700; + font-size: var(--font-size-xl); + color: var(--color-text-white); +} + +@media (prefers-reduced-motion: reduce) { + .sd-out-overlay.show { animation: none; } +} diff --git a/custom_components/beatify/www/dashboard.html b/custom_components/beatify/www/dashboard.html index 4bab2ffd..01518b2e 100644 --- a/custom_components/beatify/www/dashboard.html +++ b/custom_components/beatify/www/dashboard.html @@ -256,6 +256,9 @@

Players

0/0 + + + +
+ +

Game Over

@@ -513,6 +526,15 @@

Game Paused

+ +
+
OUT
+
💀
+
+ diff --git a/custom_components/beatify/www/i18n/de.json b/custom_components/beatify/www/i18n/de.json index e2cfb181..370bee2a 100644 --- a/custom_components/beatify/www/i18n/de.json +++ b/custom_components/beatify/www/i18n/de.json @@ -116,7 +116,18 @@ "stop": "Stop", "ending": "Beenden...", "starting": "Starten...", - "joining": "Beitreten..." + "joining": "Beitreten...", + "suddenDeath": "SUDDEN DEATH", + "youreOut": "Du bist raus", + "eliminated": "Ausgeschieden", + "eliminatedRound": "Ausgeschieden · Runde {round}", + "watchingSidelines": "Du schaust von der Seitenlinie zu", + "lastOneStanding": "Letzter Überlebender", + "cutLine": "SCHNITTLINIE", + "outRound": "Raus · R{round}", + "finalShowdown": "FINALE — SUDDEN DEATH", + "winnerTakesAll": "Alles oder nichts", + "out": "RAUS" }, "steal": { "available": "Klauen verfügbar!", @@ -561,7 +572,11 @@ "needPlayerToStart": "Als Spieler beitreten (oder Gäste lassen den QR-Code scannen), bevor das Spiel beginnt.", "wsReconnecting": "Verbindung zum Spielserver wird wiederhergestellt – bitte erneut versuchen.", "waitingForGuests": "Warte auf Gäste…" - } + }, + "suddenDeathMode": "Sudden Death", + "suddenDeathModeHint": "Der Letzte jeder Runde scheidet aus. Wer übrig bleibt, gewinnt.", + "suddenDeathDisabledTooltip": "Sudden Death braucht mindestens 3 Spieler.", + "suddenDeathLive": "Sudden Death" }, "analyticsDashboard": { "title": "Beatify Statistik", diff --git a/custom_components/beatify/www/i18n/en.json b/custom_components/beatify/www/i18n/en.json index ea72f090..59bb6c25 100644 --- a/custom_components/beatify/www/i18n/en.json +++ b/custom_components/beatify/www/i18n/en.json @@ -116,7 +116,18 @@ "stop": "Stop", "ending": "Ending...", "starting": "Starting...", - "joining": "Joining..." + "joining": "Joining...", + "suddenDeath": "SUDDEN DEATH", + "youreOut": "You're out", + "eliminated": "Eliminated", + "eliminatedRound": "Eliminated · Round {round}", + "watchingSidelines": "Watching from the sidelines", + "lastOneStanding": "Last One Standing", + "cutLine": "CUT LINE", + "outRound": "Out · R{round}", + "finalShowdown": "FINAL — SUDDEN DEATH", + "winnerTakesAll": "Winner Takes All", + "out": "OUT" }, "steal": { "available": "Steal Available!", @@ -561,7 +572,11 @@ "needPlayerToStart": "Join as player (or ask a guest to scan the QR) before starting.", "wsReconnecting": "Reconnecting to game server — please try again.", "waitingForGuests": "Waiting for guests…" - } + }, + "suddenDeathMode": "Sudden Death", + "suddenDeathModeHint": "Last-place player is eliminated each round. Last one standing wins.", + "suddenDeathDisabledTooltip": "Sudden Death needs at least 3 players.", + "suddenDeathLive": "Sudden Death" }, "analyticsDashboard": { "title": "Beatify Analytics", diff --git a/custom_components/beatify/www/i18n/es.json b/custom_components/beatify/www/i18n/es.json index 1a9e2059..a39f7158 100644 --- a/custom_components/beatify/www/i18n/es.json +++ b/custom_components/beatify/www/i18n/es.json @@ -116,7 +116,18 @@ "stop": "Stop", "ending": "Terminando...", "starting": "Iniciando...", - "joining": "Uniéndose..." + "joining": "Uniéndose...", + "suddenDeath": "MUERTE SÚBITA", + "youreOut": "Estás fuera", + "eliminated": "Eliminado", + "eliminatedRound": "Eliminado · Ronda {round}", + "watchingSidelines": "Mirando desde la banda", + "lastOneStanding": "Último en Pie", + "cutLine": "LÍNEA DE CORTE", + "outRound": "Fuera · R{round}", + "finalShowdown": "FINAL — MUERTE SÚBITA", + "winnerTakesAll": "El ganador se lo lleva todo", + "out": "FUERA" }, "steal": { "available": "Robo disponible!", @@ -559,7 +570,11 @@ "needPlayerToStart": "Únete como jugador (o pide a un invitado que escanee el QR) antes de empezar.", "wsReconnecting": "Reconectando con el servidor del juego — inténtalo de nuevo.", "waitingForGuests": "Esperando a los invitados…" - } + }, + "suddenDeathMode": "Muerte Súbita", + "suddenDeathModeHint": "El último de cada ronda queda eliminado. Gana el último en pie.", + "suddenDeathDisabledTooltip": "Muerte Súbita necesita al menos 3 jugadores.", + "suddenDeathLive": "Muerte Súbita" }, "analyticsDashboard": { "title": "Estadisticas de Beatify", diff --git a/custom_components/beatify/www/i18n/fr.json b/custom_components/beatify/www/i18n/fr.json index 242f9528..2c2504aa 100644 --- a/custom_components/beatify/www/i18n/fr.json +++ b/custom_components/beatify/www/i18n/fr.json @@ -116,7 +116,18 @@ "stop": "Stop", "ending": "Fin...", "starting": "Démarrage...", - "joining": "Rejoindre..." + "joining": "Rejoindre...", + "suddenDeath": "MORT SUBITE", + "youreOut": "Tu es éliminé", + "eliminated": "Éliminé", + "eliminatedRound": "Éliminé · Manche {round}", + "watchingSidelines": "Tu regardes depuis le banc", + "lastOneStanding": "Dernier en Lice", + "cutLine": "LIGNE DE COUPE", + "outRound": "Sorti · M{round}", + "finalShowdown": "FINALE — MORT SUBITE", + "winnerTakesAll": "Le gagnant rafle tout", + "out": "ÉLIMINÉ" }, "steal": { "available": "Vol disponible !", @@ -559,7 +570,11 @@ "needPlayerToStart": "Rejoins la partie (ou demande à un invité de scanner le QR) avant de commencer.", "wsReconnecting": "Reconnexion au serveur de jeu — réessaie.", "waitingForGuests": "En attente des invités…" - } + }, + "suddenDeathMode": "Mort Subite", + "suddenDeathModeHint": "Le dernier de chaque manche est éliminé. Le dernier en lice gagne.", + "suddenDeathDisabledTooltip": "La Mort Subite nécessite au moins 3 joueurs.", + "suddenDeathLive": "Mort Subite" }, "analyticsDashboard": { "title": "Statistiques Beatify", diff --git a/custom_components/beatify/www/i18n/nl.json b/custom_components/beatify/www/i18n/nl.json index 76d2aa32..552f80ed 100644 --- a/custom_components/beatify/www/i18n/nl.json +++ b/custom_components/beatify/www/i18n/nl.json @@ -116,7 +116,18 @@ "stop": "Stop", "ending": "Bezig met beëindigen...", "starting": "Bezig met starten...", - "joining": "Bezig met deelnemen..." + "joining": "Bezig met deelnemen...", + "suddenDeath": "SUDDEN DEATH", + "youreOut": "Je ligt eruit", + "eliminated": "Afgevallen", + "eliminatedRound": "Afgevallen · Ronde {round}", + "watchingSidelines": "Je kijkt vanaf de zijlijn", + "lastOneStanding": "Laatste Overgeblevene", + "cutLine": "AFVALLIJN", + "outRound": "Eruit · R{round}", + "finalShowdown": "FINALE — SUDDEN DEATH", + "winnerTakesAll": "Winnaar pakt alles", + "out": "ERUIT" }, "steal": { "available": "Stelen beschikbaar!", @@ -559,7 +570,11 @@ "needPlayerToStart": "Doe mee als speler (of laat een gast de QR scannen) voordat je begint.", "wsReconnecting": "Opnieuw verbinden met de spelserver — probeer het opnieuw.", "waitingForGuests": "Wacht op gasten…" - } + }, + "suddenDeathMode": "Sudden Death", + "suddenDeathModeHint": "De laatste van elke ronde valt af. Wie overblijft, wint.", + "suddenDeathDisabledTooltip": "Sudden Death heeft minstens 3 spelers nodig.", + "suddenDeathLive": "Sudden Death" }, "analyticsDashboard": { "title": "Beatify Analytics", diff --git a/custom_components/beatify/www/js/admin.js b/custom_components/beatify/www/js/admin.js index e66001dc..952159a2 100644 --- a/custom_components/beatify/www/js/admin.js +++ b/custom_components/beatify/www/js/admin.js @@ -1041,6 +1041,21 @@ async function startGame() { ? window.BeatifyTitleArtist.applyTitleArtistBonusPrecedence(rawBonusFlags, adminState.titleArtistModeEnabled) : { ...rawBonusFlags, ...(adminState.titleArtistModeEnabled ? { artist_challenge_enabled: false, closest_wins_mode: false } : {}) }; // #1180: must match YEAR_ROUND_BONUS_KEYS — movie quiz + intro stay ON in TA mode + // Issue #827: Sudden Death is the host's wizard choice, persisted to + // beatify_game_settings.suddenDeathMode (mirrors how closestWinsMode is + // stored). The admin submodules don't hydrate a dedicated adminState + // field for it, so read it straight from localStorage here. Default false. + var suddenDeathMode = false; + try { + var _sdRaw = localStorage.getItem(STORAGE_GAME_SETTINGS); + if (_sdRaw) { + var _sdSettings = JSON.parse(_sdRaw); + if (_sdSettings && typeof _sdSettings.suddenDeathMode === 'boolean') { + suddenDeathMode = _sdSettings.suddenDeathMode; + } + } + } catch (e) { /* private mode / malformed — keep default false */ } + const response = await BeatifyAuth.fetch('/beatify/api/start-game', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -1056,6 +1071,7 @@ async function startGame() { movie_quiz_enabled: bonusFlags.movie_quiz_enabled, // #947 (#1180: suppressed in TA mode) intro_mode_enabled: bonusFlags.intro_mode_enabled, // Issue #23 (#1180: suppressed in TA mode) closest_wins_mode: bonusFlags.closest_wins_mode, // Issue #442 (#1180: suppressed in TA mode) + sudden_death_mode: suddenDeathMode, // Issue #827 title_artist_mode: adminState.titleArtistModeEnabled, // #1180 party_lights: window._partyLightsConfig ? window._partyLightsConfig() : null, // Issue #331 tts: window._ttsConfig ? window._ttsConfig() : null // Issue #447 @@ -2698,6 +2714,12 @@ function showAdminRevealView(data) { } } + // Issue #827: Sudden Death — live toggle on the control bar (S7-A) + + // elimination/final highlight chips in the reveal header (S4-B / S5-B). + // Guarded so non-Sudden-Death games are wholly unaffected. + _renderSuddenDeathLiveToggle(data); + _renderSuddenDeathRevealChips(data); + // All guesses grid (player-style result cards) renderAdminResultCards(data.players, data.closest_wins_mode, data.song ? data.song.year : null); @@ -2705,6 +2727,116 @@ function showAdminRevealView(data) { renderAdminLeaderboard(data.leaderboard); } +/** + * Issue #827: Sudden Death live toggle (design S7-A) on the REVEAL control bar. + * Lets the host arm/disarm Sudden Death between rounds. POSTs to the live + * endpoint via the authed BeatifyAuth.fetch helper (same as Start/End game), + * then lets the WS broadcast drive the UI — no optimistic desync. Disabled + * when fewer than 3 non-eliminated players remain (arming is pointless there). + */ +function _renderSuddenDeathLiveToggle(data) { + var controlBar = document.getElementById('admin-control-bar'); + if (!controlBar) return; + + var existing = document.getElementById('admin-sudden-death-toggle'); + + if (!data) { + if (existing) existing.remove(); + return; + } + + // S7-A is a *live* control: the host arms or disarms Sudden Death between + // rounds, so the toggle is always present on the reveal control bar. Its + // .is-on state mirrors the top-level WS flag `sudden_death_mode`. + var players = data.players || []; + var remaining = players.filter(function(p) { return !p.eliminated; }).length; + var isOn = !!data.sudden_death_mode; + var label = (BeatifyI18n.t && BeatifyI18n.t('admin.suddenDeathLive')) || 'Sudden Death'; + + var btn = existing; + if (!btn) { + btn = document.createElement('button'); + btn.id = 'admin-sudden-death-toggle'; + btn.type = 'button'; + btn.className = 'sd-live-toggle'; + // POST the inverse of the *current server* state, then wait for the WS + // broadcast to repaint. We snapshot on click to avoid stale-closure bugs. + btn.addEventListener('click', function() { + if (btn.disabled) return; + var enable = !(btn.classList.contains('is-on')); + btn.disabled = true; // debounce until the broadcast lands + BeatifyAuth.fetch('/beatify/api/sudden-death', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: enable }) + }).catch(function(err) { + console.warn('[Admin] Sudden Death toggle failed:', err); + btn.disabled = false; // re-enable so the host can retry + }); + }); + controlBar.appendChild(btn); + } + + btn.innerHTML = '💀 ' + escapeHtml(label) + + ''; + btn.classList.toggle('is-on', isOn); + // Arming below 3 survivors is pointless (2 left = final already / 1 = winner). + btn.disabled = remaining < 3; +} + +/** + * Issue #827: Reveal-header highlight chips for Sudden Death. + * - S4-B: red elimination chip when players were eliminated THIS round. + * - S5-B: pink FINAL chip when exactly 2 survivors remain. + * Both live in a dynamically-created `.arc-chip-row` inside the reveal header. + */ +function _renderSuddenDeathRevealChips(data) { + var section = document.getElementById('admin-reveal-section'); + if (!section) return; + + var row = document.getElementById('admin-reveal-arc-chip-row'); + + // Non-Sudden-Death game → no chips; clear any stale row. + if (!data || !data.sudden_death_mode) { + if (row) row.remove(); + return; + } + + if (!row) { + row = document.createElement('div'); + row.id = 'admin-reveal-arc-chip-row'; + row.className = 'arc-chip-row'; + // Place it just under the reveal header so chips read as round context. + var header = section.querySelector('.reveal-header-compact'); + if (header && header.parentNode) { + header.parentNode.insertBefore(row, header.nextSibling); + } else { + section.appendChild(row); + } + } + + var chips = ''; + + // S4-B: elimination chip (prepended). eliminated_this_round is only present + // in REVEAL state and only when sudden_death_mode is on. + var elim = data.eliminated_this_round || []; + if (elim.length > 0) { + chips += '💀 Eliminated: ' + + escapeHtml(elim.join(', ')) + ''; + } + + // S5-B: final chip when exactly 2 players are still in it. + var players = data.players || []; + var remaining = players.filter(function(p) { return !p.eliminated; }).length; + if (remaining === 2) { + var finalLabel = (BeatifyI18n.t && BeatifyI18n.t('game.finalShowdown')) || 'FINAL — SUDDEN DEATH'; + chips += '🔥 ' + escapeHtml(finalLabel) + ''; + } + + row.innerHTML = chips; + row.classList.toggle('hidden', chips === ''); +} + /** * #1048: 1-Hz countdown on the sticky Next button while REVEAL auto-advance * runs. Replaces the ⏭️ icon with the remaining seconds; falls back to the diff --git a/custom_components/beatify/www/js/admin.min.js b/custom_components/beatify/www/js/admin.min.js index cfeb0b90..4889135f 100644 --- a/custom_components/beatify/www/js/admin.min.js +++ b/custom_components/beatify/www/js/admin.min.js @@ -1,35 +1,35 @@ -var a={selectedPlaylists:[],playlistData:[],playlistDocsUrl:"",activeFilterTags:["all"],selectedMediaPlayer:null,mediaPlayerDocsUrl:"",activeFilters:{decade:"",style:"",region:"",special:""},currentView:"setup",currentGame:null,cachedQRUrl:null,selectedLanguage:"en",selectedDuration:45,revealAutoAdvance:0,selectedDifficulty:"normal",selectedProvider:"spotify",hasMusicAssistant:!1,artistChallengeEnabled:!0,movieQuizEnabled:!0,introModeEnabled:!1,closestWinsModeEnabled:!1,titleArtistModeEnabled:!1,previousLobbyPlayers:[],adminPlayerName:null,adminSessionId:null,isPlaying:!1};var T="beatify_last_player",_="beatify_game_settings",Ne={music_assistant:{icon:"\u{1F3B5}",label:"Music Assistant",recommended:!0},sonos:{icon:"\u{1F50A}",label:"Sonos"},alexa_media:{icon:"\u{1F4E2}",label:"Alexa"},alexa:{icon:"\u{1F4E2}",label:"Alexa"}},$e={decade:{label:"Decade",tags:["1960s","1970s","1980s","1990s","2000s"]},style:{label:"Style",tags:["rock","pop","ballads","electronic","eurodance","yacht-rock","soft-rock","pop-punk","schlager","party","britpop","british-invasion","classic-rock","dance","disco","funk","hip-hop","latin","merengue","motown","r&b","salsa","soul"]},region:{label:"Region",tags:["international","german","dutch","spanish"]},special:{label:"Special",tags:["movies","soundtrack","eurovision","carnival","classics","contest","mixed","one-hit","top-hits"]}};var we=[];function R(e,t){we.push({modalId:e,close:t})}function ut(e=document){for(let t=we.length-1;t>=0;t--){let n=we[t],i=e.getElementById(n.modalId);if(i&&!i.classList.contains("hidden"))return n.close(),!0}return!1}function We(e=document){e.addEventListener("keydown",function(t){t.key==="Escape"&&ut(e)})}var ze=()=>null;function De(e){ze=typeof e=="function"?e:()=>null}function ae(){try{var e=ze()?.game_id;if(e){var t=localStorage.getItem("beatify_admin_token_"+e);if(t)return t}return localStorage.getItem("beatify_admin_token")}catch{return null}}function z(e,t){try{t&&localStorage.setItem("beatify_admin_token_"+t,e),localStorage.setItem("beatify_admin_token",e),sessionStorage.removeItem("beatify_admin_token")}catch{}}function Ie(){var e=ae(),t={"Content-Type":"application/json"};return e&&(t.Authorization="Bearer "+e),t}function Ge(e){let t={};return e.forEach(n=>{let i=n.platform||"unknown";t[i]||(t[i]=[]),t[i].push(n)}),t}var ft={pending:"\u23F3 Pending",ready:"\u2705 Ready",installed:"\u2713 Installed",declined:"\u274C Declined"};function _e(e){let t=ft[e.status]||e.status,n=Q(e.playlist_name||e.name||"Untitled request"),i=e.relative_time||"",s=e.status==="ready"&&e.update_available?`Update to v${Q(e.release_version||"")}`:"";return` +var a={selectedPlaylists:[],playlistData:[],playlistDocsUrl:"",activeFilterTags:["all"],selectedMediaPlayer:null,mediaPlayerDocsUrl:"",activeFilters:{decade:"",style:"",region:"",special:""},currentView:"setup",currentGame:null,cachedQRUrl:null,selectedLanguage:"en",selectedDuration:45,revealAutoAdvance:0,selectedDifficulty:"normal",selectedProvider:"spotify",hasMusicAssistant:!1,artistChallengeEnabled:!0,movieQuizEnabled:!0,introModeEnabled:!1,closestWinsModeEnabled:!1,titleArtistModeEnabled:!1,previousLobbyPlayers:[],adminPlayerName:null,adminSessionId:null,isPlaying:!1};var R="beatify_last_player",I="beatify_game_settings",Ne={music_assistant:{icon:"\u{1F3B5}",label:"Music Assistant",recommended:!0},sonos:{icon:"\u{1F50A}",label:"Sonos"},alexa_media:{icon:"\u{1F4E2}",label:"Alexa"},alexa:{icon:"\u{1F4E2}",label:"Alexa"}},$e={decade:{label:"Decade",tags:["1960s","1970s","1980s","1990s","2000s"]},style:{label:"Style",tags:["rock","pop","ballads","electronic","eurodance","yacht-rock","soft-rock","pop-punk","schlager","party","britpop","british-invasion","classic-rock","dance","disco","funk","hip-hop","latin","merengue","motown","r&b","salsa","soul"]},region:{label:"Region",tags:["international","german","dutch","spanish"]},special:{label:"Special",tags:["movies","soundtrack","eurovision","carnival","classics","contest","mixed","one-hit","top-hits"]}};var we=[];function q(e,t){we.push({modalId:e,close:t})}function ut(e=document){for(let t=we.length-1;t>=0;t--){let n=we[t],i=e.getElementById(n.modalId);if(i&&!i.classList.contains("hidden"))return n.close(),!0}return!1}function De(e=document){e.addEventListener("keydown",function(t){t.key==="Escape"&&ut(e)})}var We=()=>null;function ze(e){We=typeof e=="function"?e:()=>null}function ae(){try{var e=We()?.game_id;if(e){var t=localStorage.getItem("beatify_admin_token_"+e);if(t)return t}return localStorage.getItem("beatify_admin_token")}catch{return null}}function z(e,t){try{t&&localStorage.setItem("beatify_admin_token_"+t,e),localStorage.setItem("beatify_admin_token",e),sessionStorage.removeItem("beatify_admin_token")}catch{}}function Ie(){var e=ae(),t={"Content-Type":"application/json"};return e&&(t.Authorization="Bearer "+e),t}function Ge(e){let t={};return e.forEach(n=>{let i=n.platform||"unknown";t[i]||(t[i]=[]),t[i].push(n)}),t}var ft={pending:"\u23F3 Pending",ready:"\u2705 Ready",installed:"\u2713 Installed",declined:"\u274C Declined"};function _e(e){let t=ft[e.status]||e.status,n=x(e.playlist_name||e.name||"Untitled request"),i=e.relative_time||"",s=e.status==="ready"&&e.update_available?`Update to v${x(e.release_version||"")}`:"";return`
${e.thumbnail_url?``:'
\u{1F3B5}
'}
${n}
-
${Q(i)}
+
${x(i)}
${t} ${s}
- `}function Q(e){let t=document.createElement("div");return t.textContent=e,t.innerHTML}function Ae(e,t){return typeof e=="function"&&e(),typeof t=="function"?t():void 0}function je(e,t){!e||!t||typeof t!="object"||(t.language&&(e.selectedLanguage=t.language),t.duration&&(e.selectedDuration=t.duration),typeof t.revealAutoAdvance=="number"&&(e.revealAutoAdvance=t.revealAutoAdvance),t.difficulty&&(e.selectedDifficulty=t.difficulty),t.provider&&(e.selectedProvider=t.provider),typeof t.artistChallenge=="boolean"&&(e.artistChallengeEnabled=t.artistChallenge),typeof t.movieQuiz=="boolean"&&(e.movieQuizEnabled=t.movieQuiz),typeof t.introMode=="boolean"&&(e.introModeEnabled=t.introMode),typeof t.closestWinsMode=="boolean"&&(e.closestWinsModeEnabled=t.closestWinsMode),typeof t.titleArtistMode=="boolean"&&(e.titleArtistModeEnabled=t.titleArtistMode))}var ie=2,yt=10;function pt(e){return(e.protocol==="https:"?"wss:":"ws:")+"//"+e.host+"/beatify/ws"}function gt(e){return Math.min(1e3*Math.pow(2,e-1),3e4)}var y={debug:()=>{},getCurrentGame:()=>null,getCurrentView:()=>null,setIsPlaying:()=>{},setAdminPlayerName:()=>{},setAdminSessionId:()=>{},getAdminPlayerName:()=>null,handleAdminStateUpdate:()=>{},startLobbyPolling:()=>{},stopLobbyPolling:()=>{},showError:()=>{},resetHomeStartButton:()=>{}};function Oe(e){y={...y,...e||{}}}var p=null,q=0,se=!1,Y=0,Se=!1,oe=!1;function E(){return!!p&&p.readyState===WebSocket.OPEN}function H(e){return E()?(e&&e.action==="start_game"&&(oe=!0),p.send(JSON.stringify(e)),!0):!1}function Ue(){p&&(p.close(),p=null)}function Fe(){q=0}async function M(){if(!Se&&!(p&&(p.readyState===WebSocket.OPEN||p.readyState===WebSocket.CONNECTING))){Se=!0;try{var e=await BeatifyAuth.getAccessToken();if(!e&&!BeatifyAuth.isCompanionBypassMode()||p&&(p.readyState===WebSocket.OPEN||p.readyState===WebSocket.CONNECTING))return;var t=pt(window.location);try{p=new WebSocket(t)}catch(i){console.error("[Admin WS] Failed to create WebSocket:",i);return}var n=p;n.onopen=function(){n===p&&(y.debug("[Admin WS] Connected, sending admin_connect (token: len="+(e?e.length:0)+", prefix="+(e?e.slice(0,12):"null")+", recoveryAttempt="+Y+"/"+ie+")"),q=0,n.send(JSON.stringify({type:"admin_connect",ha_token:e})))},n.onmessage=function(i){if(n===p)try{var s=JSON.parse(i.data);vt(s)}catch(d){console.error("[Admin WS] Message parse error:",d)}},n.onclose=function(){if(n===p&&(y.debug("[Admin WS] Disconnected"),p=null,y.getCurrentView()==="lobby"&&y.startLobbyPolling(),!se&&q=ie){if(BeatifyAuth.isCompanionBypassMode()){console.warn("[Admin WS] Auth recovery exhausted in Companion bypass mode \u2014 Beatify was reached over a non-local address; OAuth re-login would hit the Invalid-redirect-URI screen. Surfacing local-network hint.");var i=window.BeatifyI18n&&BeatifyI18n.t("admin.companionLocalNetworkRequired")||"Beatify admin requires local network access from the Companion app \u2014 open Beatify in an external browser, or connect to your home Wi-Fi.";try{y.showError(i)}catch{}break}console.warn("[Admin WS] Auth recovery exhausted after "+ie+" attempts; HA rejected every bridge-supplied token. Forcing re-login.");var s=window.BeatifyI18n&&BeatifyI18n.t("admin.wsAuthFailed")||"Home Assistant rejected the access token. Re-authenticating\u2026";try{y.showError(s)}catch{}BeatifyAuth.logout(),BeatifyAuth.login();break}se=!0,Y++,console.warn("[Admin WS] UNAUTHORIZED \u2014 recovery attempt "+Y+"/"+ie+" (server message: "+(e.message||"")+")");var d=p;p=null;try{d?.close()}catch{}BeatifyAuth.handleServerRejection().then(function(o){se=!1,o&&(q=0,M())})}else if(e.code==="NAME_TAKEN"||e.code==="NAME_INVALID"){y.showError(e.message),y.setIsPlaying(!1),y.setAdminPlayerName(null);var l=document.getElementById("admin-join-btn");l&&(l.disabled=!1,l.textContent=BeatifyI18n.t("admin.join"))}else oe?(oe=!1,y.resetHomeStartButton(),y.showError(e.message)):console.warn("[Admin WS] Command error:",e.code,e.message);break;default:break}}function N(e){return E()?(p.send(JSON.stringify(e)),!0):(y.showError(BeatifyI18n.t("admin.connectionLost")||"Connection lost \u2014 reconnecting..."),q=0,M(),!1)}function le(e){let t=typeof window<"u"?window:globalThis,n=t&&t.BeatifyUtils;return n&&typeof n.escapeHtml=="function"?n.escapeHtml(e):e==null?"":String(e)}function re(e,t){let n=typeof window<"u"?window:globalThis,i=n&&n.BeatifyI18n;return i&&typeof i.t=="function"?i.t(e,t):e}function Ve(e){var t=document.getElementById("admin-submitted-players");!t||!e||(t.innerHTML=e.map(function(n){var i=(n.name||"?").split(/\s+/).map(function(l){return l[0]}).join("").substring(0,2).toUpperCase(),s=["player-indicator",n.submitted?"is-submitted":"",n.connected===!1?"player-indicator--disconnected":""].filter(Boolean).join(" "),d="";return n.steal_used&&(d+='\u{1F977}'),n.bet&&(d+='\u{1F3B2}'),'
'+d+'
'+le(i)+'
'+le(n.name)+"
"}).join(""))}function de(e,t){var n=t?[t]:["admin-playing-leaderboard-list","admin-reveal-leaderboard"];if(e){var i="";e.forEach(function(s){var d=s.rank<=3?"is-top-"+s.rank:"",l=s.connected===!1?"leaderboard-entry--disconnected":"",o=s.connected===!1?'(away)':"",r="";if(s.streak>=2){var c=s.streak>=5?"streak-indicator--hot":"";r='\u{1F525}'+s.streak+""}var m="";s.rank_change>0?m='\u25B2'+s.rank_change+"":s.rank_change<0&&(m='\u25BC'+Math.abs(s.rank_change)+""),i+='
#'+s.rank+''+le(s.name)+o+''+s.score+"
"}),n.forEach(function(s){var d=document.getElementById(s);d&&(d.innerHTML=i)}),e.length>0&&["admin-playing-leaderboard-summary","admin-reveal-leaderboard-summary"].forEach(function(s){var d=document.getElementById(s);d&&(d.textContent=e[0].name+" \u2014 "+e[0].score)})}}function Qe(e,t,n){var i=document.getElementById("admin-reveal-guesses");if(i){if(!e||e.length===0){i.innerHTML="";return}var s=null;t&&e.forEach(function(o){!o.missed_round&&o.years_off!=null&&(s===null||o.years_off';d.forEach(function(o){var r=o.missed_round===!0,c=o.years_off||0,m=o.round_score||0,u=r?"is-score-zero":m>=10?"is-score-high":m>=1?"is-score-medium":"is-score-zero",g=t&&!r&&s!==null&&c===s,v=g?" is-closest-winner":"",h=r?"\u2014":o.guess||"n/a",f=r?re("reveal.noGuessShort")||"Missed":c===0?re("reveal.exact")||"Exact!":re("reveal.shortOff",{years:c})||c+" off",b=o.bet?'\u{1F3B2}':"",w=g?'\u{1F3AF}':"",x=o.artist_bonus>0?'\u{1F3A4} +'+o.artist_bonus+"":"";l+='
'+le(o.name)+b+w+'
'+h+'
'+f+'
+'+m+x+"
"}),l+="
",i.innerHTML=l}}function Ye(e){if(!e)return"";var t={spotify:"admin.pauseRecovery.providerSpotify",apple_music:"admin.pauseRecovery.providerAppleMusic",youtube_music:"admin.pauseRecovery.providerYouTubeMusic",tidal:"admin.pauseRecovery.providerTidal",deezer:"admin.pauseRecovery.providerDeezer"},n={spotify:"Spotify",apple_music:"Apple Music",youtube_music:"YouTube Music",tidal:"Tidal",deezer:"Deezer"},i=t[e];return i&&(re(i)||n[e])||""}var A=window.BeatifyUtils||{};function D(e,t,n=!1){let i=document.getElementById("playlists-list");i?.removeAttribute("data-i18n"),i?.removeAttribute("aria-busy"),i?.classList.remove("skeleton-list");let s=n?[...a.selectedPlaylists]:[];a.selectedPlaylists=[],a.playlistData=e||[],ht(a.playlistData);let d=a.playlistData;!a.activeFilterTags.includes("all")&&a.activeFilterTags.length>0&&(d=a.playlistData.filter(o=>{let r=o.tags||[];return a.activeFilterTags.every(c=>r.includes(c))}));let l=a.playlistData.some(o=>o.is_valid);if(!a.playlistData||a.playlistData.length===0){let o=a.playlistDocsUrl?`How to create playlists`:"";i.innerHTML=` + `}function x(e){let t=document.createElement("div");return t.textContent=e,t.innerHTML}function Ae(e,t){return typeof e=="function"&&e(),typeof t=="function"?t():void 0}function je(e,t){!e||!t||typeof t!="object"||(t.language&&(e.selectedLanguage=t.language),t.duration&&(e.selectedDuration=t.duration),typeof t.revealAutoAdvance=="number"&&(e.revealAutoAdvance=t.revealAutoAdvance),t.difficulty&&(e.selectedDifficulty=t.difficulty),t.provider&&(e.selectedProvider=t.provider),typeof t.artistChallenge=="boolean"&&(e.artistChallengeEnabled=t.artistChallenge),typeof t.movieQuiz=="boolean"&&(e.movieQuizEnabled=t.movieQuiz),typeof t.introMode=="boolean"&&(e.introModeEnabled=t.introMode),typeof t.closestWinsMode=="boolean"&&(e.closestWinsModeEnabled=t.closestWinsMode),typeof t.titleArtistMode=="boolean"&&(e.titleArtistModeEnabled=t.titleArtistMode))}var ie=2,yt=10;function pt(e){return(e.protocol==="https:"?"wss:":"ws:")+"//"+e.host+"/beatify/ws"}function gt(e){return Math.min(1e3*Math.pow(2,e-1),3e4)}var p={debug:()=>{},getCurrentGame:()=>null,getCurrentView:()=>null,setIsPlaying:()=>{},setAdminPlayerName:()=>{},setAdminSessionId:()=>{},getAdminPlayerName:()=>null,handleAdminStateUpdate:()=>{},startLobbyPolling:()=>{},stopLobbyPolling:()=>{},showError:()=>{},resetHomeStartButton:()=>{}};function Oe(e){p={...p,...e||{}}}var g=null,H=0,se=!1,Y=0,Se=!1,oe=!1;function E(){return!!g&&g.readyState===WebSocket.OPEN}function N(e){return E()?(e&&e.action==="start_game"&&(oe=!0),g.send(JSON.stringify(e)),!0):!1}function Ue(){g&&(g.close(),g=null)}function Fe(){H=0}async function M(){if(!Se&&!(g&&(g.readyState===WebSocket.OPEN||g.readyState===WebSocket.CONNECTING))){Se=!0;try{var e=await BeatifyAuth.getAccessToken();if(!e&&!BeatifyAuth.isCompanionBypassMode()||g&&(g.readyState===WebSocket.OPEN||g.readyState===WebSocket.CONNECTING))return;var t=pt(window.location);try{g=new WebSocket(t)}catch(i){console.error("[Admin WS] Failed to create WebSocket:",i);return}var n=g;n.onopen=function(){n===g&&(p.debug("[Admin WS] Connected, sending admin_connect (token: len="+(e?e.length:0)+", prefix="+(e?e.slice(0,12):"null")+", recoveryAttempt="+Y+"/"+ie+")"),H=0,n.send(JSON.stringify({type:"admin_connect",ha_token:e})))},n.onmessage=function(i){if(n===g)try{var s=JSON.parse(i.data);vt(s)}catch(l){console.error("[Admin WS] Message parse error:",l)}},n.onclose=function(){if(n===g&&(p.debug("[Admin WS] Disconnected"),g=null,p.getCurrentView()==="lobby"&&p.startLobbyPolling(),!se&&H=ie){if(BeatifyAuth.isCompanionBypassMode()){console.warn("[Admin WS] Auth recovery exhausted in Companion bypass mode \u2014 Beatify was reached over a non-local address; OAuth re-login would hit the Invalid-redirect-URI screen. Surfacing local-network hint.");var i=window.BeatifyI18n&&BeatifyI18n.t("admin.companionLocalNetworkRequired")||"Beatify admin requires local network access from the Companion app \u2014 open Beatify in an external browser, or connect to your home Wi-Fi.";try{p.showError(i)}catch{}break}console.warn("[Admin WS] Auth recovery exhausted after "+ie+" attempts; HA rejected every bridge-supplied token. Forcing re-login.");var s=window.BeatifyI18n&&BeatifyI18n.t("admin.wsAuthFailed")||"Home Assistant rejected the access token. Re-authenticating\u2026";try{p.showError(s)}catch{}BeatifyAuth.logout(),BeatifyAuth.login();break}se=!0,Y++,console.warn("[Admin WS] UNAUTHORIZED \u2014 recovery attempt "+Y+"/"+ie+" (server message: "+(e.message||"")+")");var l=g;g=null;try{l?.close()}catch{}BeatifyAuth.handleServerRejection().then(function(o){se=!1,o&&(H=0,M())})}else if(e.code==="NAME_TAKEN"||e.code==="NAME_INVALID"){p.showError(e.message),p.setIsPlaying(!1),p.setAdminPlayerName(null);var d=document.getElementById("admin-join-btn");d&&(d.disabled=!1,d.textContent=BeatifyI18n.t("admin.join"))}else oe?(oe=!1,p.resetHomeStartButton(),p.showError(e.message)):console.warn("[Admin WS] Command error:",e.code,e.message);break;default:break}}function $(e){return E()?(g.send(JSON.stringify(e)),!0):(p.showError(BeatifyI18n.t("admin.connectionLost")||"Connection lost \u2014 reconnecting..."),H=0,M(),!1)}function le(e){let t=typeof window<"u"?window:globalThis,n=t&&t.BeatifyUtils;return n&&typeof n.escapeHtml=="function"?n.escapeHtml(e):e==null?"":String(e)}function re(e,t){let n=typeof window<"u"?window:globalThis,i=n&&n.BeatifyI18n;return i&&typeof i.t=="function"?i.t(e,t):e}function Ve(e){var t=document.getElementById("admin-submitted-players");!t||!e||(t.innerHTML=e.map(function(n){var i=(n.name||"?").split(/\s+/).map(function(o){return o[0]}).join("").substring(0,2).toUpperCase(),s=["player-indicator",!n.eliminated&&n.submitted?"is-submitted":"",n.eliminated?"is-eliminated":"",n.connected===!1?"player-indicator--disconnected":""].filter(Boolean).join(" "),l="";!n.eliminated&&n.steal_used&&(l+='\u{1F977}'),!n.eliminated&&n.bet&&(l+='\u{1F3B2}');var d=n.eliminated?'\u{1F480}':''+le(i)+"";return'
'+l+'
'+d+'
'+le(n.name)+"
"}).join(""))}function de(e,t){var n=t?[t]:["admin-playing-leaderboard-list","admin-reveal-leaderboard"];if(e){var i="";e.forEach(function(s){var l=s.rank<=3?"is-top-"+s.rank:"",d=s.connected===!1?"leaderboard-entry--disconnected":"",o=s.eliminated?"is-eliminated":"",r=s.eliminated?"\u{1F480} ":"",c=s.connected===!1?'(away)':"",m="";if(s.streak>=2){var u=s.streak>=5?"streak-indicator--hot":"";m='\u{1F525}'+s.streak+""}var f="";s.rank_change>0?f='\u25B2'+s.rank_change+"":s.rank_change<0&&(f='\u25BC'+Math.abs(s.rank_change)+""),i+='
#'+s.rank+''+r+le(s.name)+c+''+s.score+"
"}),n.forEach(function(s){var l=document.getElementById(s);l&&(l.innerHTML=i)}),e.length>0&&["admin-playing-leaderboard-summary","admin-reveal-leaderboard-summary"].forEach(function(s){var l=document.getElementById(s);l&&(l.textContent=e[0].name+" \u2014 "+e[0].score)})}}function Qe(e,t,n){var i=document.getElementById("admin-reveal-guesses");if(i){if(!e||e.length===0){i.innerHTML="";return}var s=null;t&&e.forEach(function(o){!o.missed_round&&o.years_off!=null&&(s===null||o.years_off';l.forEach(function(o){var r=o.missed_round===!0,c=o.years_off||0,m=o.round_score||0,u=r?"is-score-zero":m>=10?"is-score-high":m>=1?"is-score-medium":"is-score-zero",f=t&&!r&&s!==null&&c===s,v=f?" is-closest-winner":"",h=r?"\u2014":o.guess||"n/a",y=r?re("reveal.noGuessShort")||"Missed":c===0?re("reveal.exact")||"Exact!":re("reveal.shortOff",{years:c})||c+" off",b=o.bet?'\u{1F3B2}':"",w=f?'\u{1F3AF}':"",T=o.artist_bonus>0?'\u{1F3A4} +'+o.artist_bonus+"":"";d+='
'+le(o.name)+b+w+'
'+h+'
'+y+'
+'+m+T+"
"}),d+="
",i.innerHTML=d}}function Ye(e){if(!e)return"";var t={spotify:"admin.pauseRecovery.providerSpotify",apple_music:"admin.pauseRecovery.providerAppleMusic",youtube_music:"admin.pauseRecovery.providerYouTubeMusic",tidal:"admin.pauseRecovery.providerTidal",deezer:"admin.pauseRecovery.providerDeezer"},n={spotify:"Spotify",apple_music:"Apple Music",youtube_music:"YouTube Music",tidal:"Tidal",deezer:"Deezer"},i=t[e];return i&&(re(i)||n[e])||""}var A=window.BeatifyUtils||{};function G(e,t,n=!1){let i=document.getElementById("playlists-list");i?.removeAttribute("data-i18n"),i?.removeAttribute("aria-busy"),i?.classList.remove("skeleton-list");let s=n?[...a.selectedPlaylists]:[];a.selectedPlaylists=[],a.playlistData=e||[],ht(a.playlistData);let l=a.playlistData;!a.activeFilterTags.includes("all")&&a.activeFilterTags.length>0&&(l=a.playlistData.filter(o=>{let r=o.tags||[];return a.activeFilterTags.every(c=>r.includes(c))}));let d=a.playlistData.some(o=>o.is_valid);if(!a.playlistData||a.playlistData.length===0){let o=a.playlistDocsUrl?`How to create playlists`:"";i.innerHTML=`

No playlists found. Add playlist JSON files to:

${A.escapeHtml(t)}

${o?`

${o}

`:""}
- `,document.getElementById("start-game")?.classList.add("hidden");return}if(d.length===0){i.innerHTML=` + `,document.getElementById("start-game")?.classList.add("hidden");return}if(l.length===0){i.innerHTML=`

No playlists match the selected filter.

- `;return}if(i.innerHTML=d.map(o=>{if(o.is_valid){let r=o.song_count||0,c=o.spotify_count||0,m=o.apple_music_count||0,u=o.youtube_music_count||0,g=o.tidal_count||0,v=o.deezer_count||0,h=o.amazon_music_count||r,f=r;a.selectedProvider==="spotify"?f=c||r:a.selectedProvider==="apple_music"?f=m:a.selectedProvider==="youtube_music"?f=u:a.selectedProvider==="tidal"?f=g:a.selectedProvider==="deezer"?f=v:a.selectedProvider==="amazon_music"&&(f=h);let b=f===0,w=b?"is-disabled":"",x=b?"disabled":"",X="";return f${f}/${r}`),` + `;return}if(i.innerHTML=l.map(o=>{if(o.is_valid){let r=o.song_count||0,c=o.spotify_count||0,m=o.apple_music_count||0,u=o.youtube_music_count||0,f=o.tidal_count||0,v=o.deezer_count||0,h=o.amazon_music_count||r,y=r;a.selectedProvider==="spotify"?y=c||r:a.selectedProvider==="apple_music"?y=m:a.selectedProvider==="youtube_music"?y=u:a.selectedProvider==="tidal"?y=f:a.selectedProvider==="deezer"?y=v:a.selectedProvider==="amazon_music"&&(y=h);let b=y===0,w=b?"is-disabled":"",T=b?"disabled":"",X="";return y${y}/${r}`),`
${X||A.escapeHtml(String(r))} songs @@ -39,17 +39,17 @@ var a={selectedPlaylists:[],playlistData:[],playlistDocsUrl:"",activeFilterTags: ${A.escapeHtml(o.name)} Invalid: ${A.escapeHtml(r)}
- `}}).join(""),i.querySelectorAll(".playlist-checkbox").forEach(o=>{o.addEventListener("change",function(){Je(this)})}),i.querySelectorAll(".playlist-item.is-selectable").forEach(o=>{o.addEventListener("click",function(r){if(r.target.classList.contains("playlist-checkbox")||r.target.closest(".checkbox-label"))return;let c=o.querySelector(".playlist-checkbox");c&&(c.checked=!c.checked,Je(c))})}),n&&s.length>0&&s.forEach(o=>{let r=i.querySelector(`.playlist-checkbox[data-path="${CSS.escape(o.path)}"]`);if(r&&!r.disabled){r.checked=!0;let c=parseInt(r.dataset.providerCount,10)||0,m=r.closest(".playlist-item");c>0&&(a.selectedPlaylists.push({path:o.path,songCount:c}),m?.classList.add("is-selected"))}}),l?document.getElementById("start-game")?.classList.remove("hidden"):document.getElementById("start-game")?.classList.add("hidden"),a.selectedPlaylists.length===0)try{let o=localStorage.getItem(_),r=o?JSON.parse(o):null;(Array.isArray(r?.selectedPlaylists)?r.selectedPlaylists.map(m=>typeof m=="string"?m:m.path).filter(Boolean):[]).forEach(m=>{let u=i.querySelector(`.playlist-checkbox[data-path="${CSS.escape(m)}"]`);if(u&&!u.disabled){u.checked=!0;let g=parseInt(u.dataset.providerCount,10)||0;g>0&&!a.selectedPlaylists.some(v=>v.path===m)&&(a.selectedPlaylists.push({path:m,songCount:g}),u.closest(".playlist-item")?.classList.add("is-selected"))}})}catch(o){console.warn("[Beatify] restore saved playlists failed:",o)}Xe(),$()}function Je(e){let t=e.dataset.path,n=parseInt(e.dataset.providerCount,10)||0,i=e.closest(".playlist-item");e.checked?(a.selectedPlaylists.some(s=>s.path===t)||a.selectedPlaylists.push({path:t,songCount:n}),i.classList.add("is-selected")):(a.selectedPlaylists=a.selectedPlaylists.filter(s=>s.path!==t),i.classList.remove("is-selected")),Xe(),$()}function ht(e){let t=document.getElementById("playlist-filter-bar");if(!t)return;let n=new Set;if(e.forEach(l=>{(l.tags||[]).forEach(o=>n.add(o))}),n.size===0){t.classList.add("hidden");return}let i=l=>l.charAt(0).toUpperCase()+l.slice(1),s='
';Object.entries($e).forEach(([l,o])=>{let r=o.tags.filter(m=>n.has(m));if(r.length===0)return;let c=a.activeFilters[l]||"";s+=` - ${r.map(m=>{let u=c===m?"selected":"";return``}).join("")} - `}),s+="
";let d=Object.entries(a.activeFilters).filter(([l,o])=>o).map(([l,o])=>i(o));d.length>0&&(s+=` + `}),s+="";let l=Object.entries(a.activeFilters).filter(([d,o])=>o).map(([d,o])=>i(o));l.length>0&&(s+=`
- Showing: ${d.join(" \u2022 ")} + Showing: ${l.join(" \u2022 ")}
- `),t.innerHTML=s,t.classList.remove("hidden"),t.querySelectorAll(".filter-dropdown").forEach(l=>{l.addEventListener("change",function(){bt(this.dataset.category,this.value)})})}function bt(e,t){a.activeFilters[e]=t,Et(),D(a.playlistData,"",!0)}function Et(){let e=Object.values(a.activeFilters).filter(t=>t);a.activeFilterTags=e.length>0?e:["all"]}function Ke(){a.activeFilters={decade:"",style:"",region:"",special:""},a.activeFilterTags=["all"],D(a.playlistData,"",!0)}function Bt(){return a.selectedPlaylists.reduce((e,t)=>e+t.songCount,0)}function Xe(){let e=document.getElementById("playlist-summary"),t=document.getElementById("selected-count"),n=document.getElementById("total-songs");!e||!t||!n||(a.selectedPlaylists.length===0?e.classList.add("hidden"):(e.classList.remove("hidden"),t.textContent=a.selectedPlaylists.length,n.textContent=Bt()))}function $(){let e=document.getElementById("start-game"),t=document.getElementById("playlist-validation-msg"),n=document.getElementById("media-player-validation-msg");if(!e)return;let i=a.selectedPlaylists.length===0,s=a.selectedMediaPlayer===null;e.disabled=i||s,t&&t.classList.toggle("hidden",!i),n&&n.classList.toggle("hidden",!s)}var B=window.BeatifyUtils||{};function Lt(e){let t=document.getElementById("media-player-summary");t&&(t.textContent=e||"Select...")}function Ze(e){let t=document.getElementById("media-players-list");t?.removeAttribute("data-i18n"),t?.removeAttribute("aria-busy"),t?.classList.remove("skeleton-list");let n=e?e.length:0;a.selectedMediaPlayer=null;let i=(e||[]).filter(l=>l.state!=="unavailable"),s=document.getElementById("media-player-validation-msg");if(n===0){t.innerHTML=` + `),t.innerHTML=s,t.classList.remove("hidden"),t.querySelectorAll(".filter-dropdown").forEach(d=>{d.addEventListener("change",function(){bt(this.dataset.category,this.value)})})}function bt(e,t){a.activeFilters[e]=t,Et(),G(a.playlistData,"",!0)}function Et(){let e=Object.values(a.activeFilters).filter(t=>t);a.activeFilterTags=e.length>0?e:["all"]}function Ke(){a.activeFilters={decade:"",style:"",region:"",special:""},a.activeFilterTags=["all"],G(a.playlistData,"",!0)}function Bt(){return a.selectedPlaylists.reduce((e,t)=>e+t.songCount,0)}function Xe(){let e=document.getElementById("playlist-summary"),t=document.getElementById("selected-count"),n=document.getElementById("total-songs");!e||!t||!n||(a.selectedPlaylists.length===0?e.classList.add("hidden"):(e.classList.remove("hidden"),t.textContent=a.selectedPlaylists.length,n.textContent=Bt()))}function D(){let e=document.getElementById("start-game"),t=document.getElementById("playlist-validation-msg"),n=document.getElementById("media-player-validation-msg");if(!e)return;let i=a.selectedPlaylists.length===0,s=a.selectedMediaPlayer===null;e.disabled=i||s,t&&t.classList.toggle("hidden",!i),n&&n.classList.toggle("hidden",!s)}var B=window.BeatifyUtils||{};function Lt(e){let t=document.getElementById("media-player-summary");t&&(t.textContent=e||"Select...")}function Ze(e){let t=document.getElementById("media-players-list");t?.removeAttribute("data-i18n"),t?.removeAttribute("aria-busy"),t?.classList.remove("skeleton-list");let n=e?e.length:0;a.selectedMediaPlayer=null;let i=(e||[]).filter(d=>d.state!=="unavailable"),s=document.getElementById("media-player-validation-msg");if(n===0){t.innerHTML=`

\u{1F3B5} No Compatible Players Found

Beatify works with Music Assistant, Sonos, and Alexa players.

@@ -64,12 +64,12 @@ var a={selectedPlaylists:[],playlistData:[],playlistDocsUrl:"",activeFilterTags:
- `,s&&s.classList.add("hidden");let l=document.getElementById("start-game");l&&(l.disabled=!0);return}if(i.length===0){let l=a.mediaPlayerDocsUrl?`Troubleshooting`:"";t.innerHTML=` + `,s&&s.classList.add("hidden");let d=document.getElementById("start-game");d&&(d.disabled=!0);return}if(i.length===0){let d=a.mediaPlayerDocsUrl?`Troubleshooting`:"";t.innerHTML=`

All media players are unavailable. Check your devices are powered on.

- ${l?`

${l}

`:""} + ${d?`

${d}

`:""}
- `,s&&s.classList.add("hidden");return}t.innerHTML=i.map(l=>wt(l)).join(""),It();let d=localStorage.getItem(T);if(d){let l=t.querySelector(`.media-player-radio[data-entity-id="${CSS.escape(d)}"]`);if(l){l.checked=!0,J(l,!0);let o=document.getElementById("media-players");if(o){o.classList.add("collapsed");let r=document.getElementById("media-players-toggle");r&&r.setAttribute("aria-expanded","false")}}}}function wt(e){let t=Ne[e.platform]||{icon:"\u{1F508}",label:e.platform},n=`${t.icon} ${t.label}`;return` + `,s&&s.classList.add("hidden");return}t.innerHTML=i.map(d=>wt(d)).join(""),It();let l=localStorage.getItem(R);if(l){let d=t.querySelector(`.media-player-radio[data-entity-id="${CSS.escape(l)}"]`);if(d){d.checked=!0,J(d,!0);let o=document.getElementById("media-players");if(o){o.classList.add("collapsed");let r=document.getElementById("media-players-toggle");r&&r.setAttribute("aria-expanded","false")}}}}function wt(e){let t=Ne[e.platform]||{icon:"\u{1F508}",label:e.platform},n=`${t.icon} ${t.label}`;return`
- `}function It(){let e=document.getElementById("media-players-list");e&&(e.querySelectorAll(".media-player-radio").forEach(t=>{t.addEventListener("change",function(){J(this)})}),e.querySelectorAll(".media-player-item").forEach(t=>{t.addEventListener("click",function(n){if(n.target.classList.contains("media-player-radio")||n.target.closest(".radio-label"))return;let i=t.querySelector(".media-player-radio");i&&!i.checked&&(i.checked=!0,J(i))})}))}function J(e,t=!1){let n=e.dataset.entityId,i=e.dataset.state,s=e.dataset.platform,d=e.dataset.supportsSpotify==="true",l=e.dataset.supportsAppleMusic==="true",o=e.dataset.supportsYoutubeMusic==="true",r=e.dataset.supportsTidal==="true",c=e.dataset.supportsDeezer==="true",m=e.dataset.supportsAmazonMusic==="true";a.selectedMediaPlayer={entityId:n,state:i,platform:s,supportsSpotify:d,supportsAppleMusic:l,supportsYoutubeMusic:o,supportsTidal:r,supportsDeezer:c,supportsAmazonMusic:m},document.querySelectorAll(".media-player-item").forEach(h=>{h.classList.remove("is-selected")});let u=e.closest(".media-player-item");u.classList.add("is-selected");let g=u.querySelector(".player-name")?.textContent?.trim()||n;Lt(g);let v=document.getElementById("music-service");if(v&&v.classList.remove("hidden"),_t(a.selectedMediaPlayer),At(a.selectedMediaPlayer),!t)try{localStorage.setItem(T,n)}catch(h){console.warn("Failed to save last player:",h)}$()}function _t(e){let t=document.querySelector('.chip[data-provider="spotify"]'),n=document.querySelector('.chip[data-provider="apple_music"]'),i=document.querySelector('.chip[data-provider="youtube_music"]'),s=document.querySelector('.chip[data-provider="tidal"]'),d=document.querySelector('.chip[data-provider="deezer"]'),l=document.querySelector('.chip[data-provider="amazon_music"]');t&&(t.disabled=!e.supportsSpotify,t.classList.toggle("chip--disabled",!e.supportsSpotify)),n&&(n.disabled=!e.supportsAppleMusic,n.classList.toggle("chip--disabled",!e.supportsAppleMusic)),i&&(i.disabled=!e.supportsYoutubeMusic,i.classList.toggle("chip--disabled",!e.supportsYoutubeMusic)),s&&(s.disabled=!e.supportsTidal,s.classList.toggle("chip--disabled",!e.supportsTidal)),d&&(d.disabled=!e.supportsDeezer,d.classList.toggle("chip--disabled",!e.supportsDeezer)),l&&(l.disabled=!e.supportsAmazonMusic,l.classList.toggle("chip--disabled",!e.supportsAmazonMusic)),a.selectedProvider==="apple_music"&&!e.supportsAppleMusic&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="youtube_music"&&!e.supportsYoutubeMusic&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="tidal"&&!e.supportsTidal&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="deezer"&&!e.supportsDeezer&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="amazon_music"&&!e.supportsAmazonMusic&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify");let o=document.getElementById("provider-hint");if(o){let r=[];e.supportsAppleMusic||r.push("Apple Music"),e.supportsYoutubeMusic||r.push("YouTube Music"),e.supportsTidal||r.push("Tidal"),e.supportsDeezer||r.push("Deezer");let c=[];r.length>0&&c.push(`${r.join(" and ")} require${r.length===1?"s":""} a Music Assistant speaker`),e.supportsAmazonMusic||c.push("Amazon Music requires an Amazon Echo (alexa_media)"),c.length>0?(o.textContent=c.join(" \xB7 "),o.classList.remove("hidden")):o.classList.add("hidden")}}function At(e){let t=document.getElementById("provider-warning");if(!t)return;let i={music_assistant:{warning:"Premium account must be configured in Music Assistant"},sonos:{warning:"Spotify must be linked in Sonos app"},alexa_media:{warning:"Service must be linked in Alexa app",caveat:"Uses voice search - may occasionally play a different version of the song"},alexa:{warning:"Service must be linked in Alexa app",caveat:"Uses voice search - may occasionally play a different version of the song"}}[e.platform];if(i){let s=`

\u26A0\uFE0F ${B.escapeHtml(i.warning)}

`;i.caveat&&(s+=`

\u2139\uFE0F ${B.escapeHtml(i.caveat)}

`),t.innerHTML=s,t.classList.remove("hidden")}else t.classList.add("hidden")}function et(){document.querySelectorAll(".chip[data-lang]").forEach(e=>{e.addEventListener("click",async function(){let t=this.dataset.lang;document.querySelectorAll(".chip[data-lang]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedLanguage=t,window.BeatifyI18n&&(await BeatifyI18n.setLanguage(t),BeatifyI18n.initPageTranslations()),k(),S()})}),document.querySelectorAll(".chip[data-duration]").forEach(e=>{e.addEventListener("click",function(){let t=parseInt(this.dataset.duration,10);document.querySelectorAll(".chip[data-duration]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedDuration=t,k(),S()})}),document.querySelectorAll(".chip[data-reveal-advance]").forEach(e=>{e.addEventListener("click",function(){a.revealAutoAdvance=parseInt(this.dataset.revealAdvance,10)||0,document.querySelectorAll(".chip[data-reveal-advance]").forEach(t=>t.classList.remove("chip--active")),this.classList.add("chip--active"),S()})}),document.querySelectorAll(".chip[data-difficulty]").forEach(e=>{e.addEventListener("click",function(){let t=this.dataset.difficulty;document.querySelectorAll(".chip[data-difficulty]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedDifficulty=t,k(),S()})}),document.getElementById("artist-challenge-toggle")?.addEventListener("change",function(){a.artistChallengeEnabled=this.checked,k(),S()}),document.getElementById("movie-quiz-toggle")?.addEventListener("change",function(){a.movieQuizEnabled=this.checked,k(),S()}),document.getElementById("intro-mode-toggle")?.addEventListener("change",function(){a.introModeEnabled=this.checked,k(),S()}),document.getElementById("closest-wins-toggle")?.addEventListener("change",function(){a.closestWinsModeEnabled=this.checked,k(),S()}),document.getElementById("title-artist-mode-toggle")?.addEventListener("change",function(){a.titleArtistModeEnabled=this.checked,tt(),k(),S()}),document.querySelectorAll(".chip[data-provider]").forEach(e=>{e.addEventListener("click",function(){if(this.disabled||this.classList.contains("chip--disabled"))return;let t=this.dataset.provider;document.querySelectorAll(".chip[data-provider]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedProvider=t,k(),S(),a.playlistData.length>0&&D(a.playlistData,"",!0)})})}async function ke(){try{let e=localStorage.getItem(_);if(e){let t=JSON.parse(e);if(t.language&&(a.selectedLanguage=t.language,document.querySelectorAll(".chip[data-lang]").forEach(n=>{n.classList.toggle("chip--active",n.dataset.lang===t.language)}),window.BeatifyI18n&&(await BeatifyI18n.setLanguage(t.language),BeatifyI18n.initPageTranslations())),t.duration&&(a.selectedDuration=t.duration,document.querySelectorAll(".chip[data-duration]").forEach(n=>{n.classList.toggle("chip--active",parseInt(n.dataset.duration,10)===t.duration)})),typeof t.revealAutoAdvance=="number"&&(a.revealAutoAdvance=t.revealAutoAdvance,document.querySelectorAll(".chip[data-reveal-advance]").forEach(n=>{n.classList.toggle("chip--active",parseInt(n.dataset.revealAdvance,10)===t.revealAutoAdvance)})),t.difficulty&&(a.selectedDifficulty=t.difficulty,document.querySelectorAll(".chip[data-difficulty]").forEach(n=>{n.classList.toggle("chip--active",n.dataset.difficulty===t.difficulty)})),typeof t.artistChallenge=="boolean"){a.artistChallengeEnabled=t.artistChallenge;let n=document.getElementById("artist-challenge-toggle");n&&(n.checked=t.artistChallenge)}if(typeof t.movieQuiz=="boolean"){a.movieQuizEnabled=t.movieQuiz;let n=document.getElementById("movie-quiz-toggle");n&&(n.checked=t.movieQuiz)}if(typeof t.introMode=="boolean"){a.introModeEnabled=t.introMode;let n=document.getElementById("intro-mode-toggle");n&&(n.checked=t.introMode)}if(typeof t.closestWinsMode=="boolean"){a.closestWinsModeEnabled=t.closestWinsMode;let n=document.getElementById("closest-wins-toggle");n&&(n.checked=t.closestWinsMode)}if(typeof t.titleArtistMode=="boolean"){a.titleArtistModeEnabled=t.titleArtistMode;let n=document.getElementById("title-artist-mode-toggle");n&&(n.checked=t.titleArtistMode)}tt(),t.provider&&(a.selectedProvider=t.provider,document.querySelectorAll(".chip[data-provider]").forEach(n=>{n.classList.toggle("chip--active",n.dataset.provider===t.provider)}))}}catch(e){console.warn("Failed to load saved settings:",e)}k()}function S(){try{let e={language:a.selectedLanguage,duration:a.selectedDuration,revealAutoAdvance:a.revealAutoAdvance,difficulty:a.selectedDifficulty,artistChallenge:a.artistChallengeEnabled,movieQuiz:a.movieQuizEnabled,introMode:a.introModeEnabled,closestWinsMode:a.closestWinsModeEnabled,titleArtistMode:a.titleArtistModeEnabled,provider:a.selectedProvider};localStorage.setItem(_,JSON.stringify(e))}catch(e){console.warn("Failed to save settings:",e)}}function k(){let e=document.getElementById("game-settings-summary");if(!e)return;let t={easy:"Easy",normal:"Normal",hard:"Hard"},n={en:"EN",de:"DE",es:"ES"},i=!a.titleArtistModeEnabled,s=i&&a.artistChallengeEnabled?" \u2022 \u{1F3A4}":"",d=i&&a.movieQuizEnabled?" \u2022 \u{1F3AC}":"",l=i&&a.introModeEnabled?" \u2022 \u26A1":"",o=i&&a.closestWinsModeEnabled?" \u2022 \u{1F3AF}":"",r=a.titleArtistModeEnabled?" \u2022 \u{1F3B5}":"";e.textContent=`${t[a.selectedDifficulty]||"Normal"} \u2022 ${a.selectedDuration}s \u2022 ${n[a.selectedLanguage]||"EN"}${r}${s}${d}${l}${o}`}function tt(){var e=["artist-challenge-toggle","closest-wins-toggle"];e.forEach(function(s){var d=document.getElementById(s);if(d){var l=d.closest(".setting-group");l&&l.classList.toggle("hidden",a.titleArtistModeEnabled),d.disabled=a.titleArtistModeEnabled}});var t=document.getElementById("admin-difficulty-row");t&&t.classList.toggle("hidden",a.titleArtistModeEnabled);var n=document.getElementById("admin-difficulty-hint");n&&n.classList.toggle("hidden",a.titleArtistModeEnabled);var i=document.getElementById("admin-difficulty-ta-summary");i&&i.classList.toggle("hidden",!a.titleArtistModeEnabled)}De(()=>a.currentGame);window.escapeHtml=Q;window.groupPlayersByPlatform=Ge;window.buildRequestRowHtml=_e;window._getAdminToken=ae;window._setAdminToken=z;window._adminHeaders=Ie;window.clearPlaylistFilters=Ke;var U=null,j=null,K=!1;function St(){if(j)return j;if(typeof window<"u"&&typeof window.NoSleep=="function")try{j=new window.NoSleep}catch(e){console.debug("[BeatifyWakeLock] NoSleep instantiation failed:",e)}return j}async function W(){if("wakeLock"in navigator)try{U=await navigator.wakeLock.request("screen"),U.addEventListener("release",function(){console.debug("[BeatifyWakeLock] Layer 1 released by browser"),U=null}),console.debug("[BeatifyWakeLock] Layer 1 (native wakeLock) acquired");return}catch(n){console.debug("[BeatifyWakeLock] Layer 1 request failed:",n,"\u2014 trying Layer 2")}else console.debug("[BeatifyWakeLock] Layer 1 unavailable \u2014 using Layer 2");var e=St();if(!e){console.debug("[BeatifyWakeLock] Layer 2 unavailable (NoSleep vendor not loaded)");return}if(!K)try{var t=e.enable();K=!0,t&&typeof t.catch=="function"&&t.catch(function(n){console.debug("[BeatifyWakeLock] Layer 2 enable promise rejected:",n)}),console.debug("[BeatifyWakeLock] Layer 2 (NoSleep video) enabled")}catch(n){console.debug("[BeatifyWakeLock] Layer 2 enable failed:",n),K=!1}}function st(){if(U){try{U.release()}catch{}U=null}if(K&&j){try{j.disable()}catch{}K=!1,console.debug("[BeatifyWakeLock] Layer 2 (NoSleep) disabled")}}document.addEventListener("visibilitychange",function(){document.visibilityState==="visible"&&a.currentGame&&a.currentGame.phase!=="END"&&(W(),E()||(Fe(),M()))});var ue=null,Ce=null,I=null;Oe({debug:(...e)=>xe(...e),getCurrentGame:()=>a.currentGame,getCurrentView:()=>a.currentView,getAdminPlayerName:()=>a.adminPlayerName,setIsPlaying:e=>{a.isPlaying=e},setAdminPlayerName:e=>{a.adminPlayerName=e},setAdminSessionId:e=>{a.adminSessionId=e},handleAdminStateUpdate:e=>lt(e),startLobbyPolling:()=>ot(),stopLobbyPolling:()=>he(),showError:e=>L(e),resetHomeStartButton:()=>Ct()});var C=null,ge=null,kt=["media-players","music-service","playlists","game-settings","admin-actions","my-requests","party-lights","tts-settings","ha-entities"],O=window.BeatifyUtils||{},xe=O.debug||function(){};document.addEventListener("DOMContentLoaded",async()=>{if(await BeatifyAuth.init({requireAuth:!0}),await O.waitForI18n()?(await BeatifyI18n.init(),BeatifyI18n.initPageTranslations(),a.selectedLanguage=BeatifyI18n.getLanguage()):console.error("[Beatify] BeatifyI18n module failed to load - UI will use fallback text"),document.querySelectorAll(".chip[data-lang]").forEach(t=>{t.classList.toggle("chip--active",t.dataset.lang===a.selectedLanguage)}),window.BeatifyWizard&&typeof window.BeatifyWizard.init=="function")try{await window.BeatifyWizard.init()}catch(t){console.warn("[Beatify] wizard init failed:",t)}window.loadStatus=F,window.loadSavedSettings=ke,window.BeatifyHome={enter(){document.body.classList.add("home-mode");let t=document.getElementById("home-view");t&&t.classList.remove("hidden"),this.refresh(),this.isConfigured()?(this.setMode("configured"),this.hydrateFromStorage(),a.currentGame&&a.currentGame.phase==="LOBBY"&&a.currentGame.join_url?this.renderSession(a.currentGame):this.startSession()):this.setMode("setup")},hydrateFromStorage(){try{let t=localStorage.getItem(T);if(t&&(!a.selectedMediaPlayer||!a.selectedMediaPlayer.entityId)){let i=document.querySelector(`.media-player-radio[data-entity-id="${CSS.escape(t)}"]`);i?(i.checked=!0,J(i,!0)):a.selectedMediaPlayer={entityId:t,state:"unknown",platform:"unknown"}}let n=localStorage.getItem(_);if(n){let i=JSON.parse(n);je(a,i);let s=Array.isArray(i.selectedPlaylists)?i.selectedPlaylists.map(d=>typeof d=="string"?d:d.path).filter(Boolean):[];s.length&&a.selectedPlaylists.length===0&&(a.selectedPlaylists=s.map(d=>{let l=(a.playlistData||[]).find(o=>o.path===d);return{path:d,songCount:l&&(l.song_count||l.songCount)||0}}))}}catch(t){console.warn("[Beatify] hydrateFromStorage failed:",t)}},setMode(t){let n=t==="configured";if(document.getElementById("home-hero-configured")?.classList.toggle("hidden",!n),document.getElementById("home-hero-setup")?.classList.toggle("hidden",n),document.getElementById("home-edit-setup")?.classList.toggle("hidden",!n),document.getElementById("home-start-game")?.classList.toggle("hidden",!n),document.getElementById("home-start-setup")?.classList.toggle("hidden",n),!n){document.getElementById("home-dashboard-url")?.classList.add("hidden"),document.getElementById("home-join-player")?.classList.add("hidden"),document.getElementById("home-end-game")?.classList.add("hidden");let i=document.getElementById("home-meta");i&&(i.textContent="");let s=document.getElementById("home-players");s&&(s.innerHTML="")}},async startSession(){let t=document.getElementById("home-qr-loading");t&&t.classList.remove("hidden");try{await Me()}catch(n){console.warn("[Beatify] Home auto-start failed:",n)}},renderSession(t){let n=document.getElementById("home-qr-loading");n&&n.classList.add("hidden");let i=document.getElementById("home-qr-code");i&&t.join_url&&typeof QRCode<"u"&&(i.dataset.url!==t.join_url&&(i.innerHTML="",new QRCode(i,{text:t.join_url,width:180,height:180,colorDark:"#0a0a12",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}),i.dataset.url=t.join_url,i.setAttribute("role","button"),i.setAttribute("tabindex","0"),i.style.cursor="pointer"),a.cachedQRUrl=t.join_url);let s=document.getElementById("home-join-url");s&&t.join_url&&(s.textContent=t.join_url);let d=document.getElementById("home-dashboard-url");if(d){let u=t.dashboard_url;!u&&t.join_url&&(u=t.join_url.split("/beatify/play")[0]+"/beatify/dashboard"),u?(d.href=u,d.classList.remove("hidden")):d.classList.add("hidden")}let l=t.phase==="LOBBY"&&!!t.join_url,o=t.phase&&t.phase!=="LOBBY"&&t.phase!=="END";document.getElementById("home-end-game")?.classList.toggle("hidden",!o);let r=(t.players||[]).some(u=>u.is_admin),c=l&&!r&&!a.isPlaying;document.getElementById("home-join-player")?.classList.toggle("hidden",!c);let m=document.getElementById("home-start-game");m&&(m.classList.toggle("btn-primary",!c),m.classList.toggle("btn-ghost",c)),this.renderPlayers(t.players||[])},renderPlayers(t){let n=document.getElementById("home-players");if(!n)return;if(!t.length){let r=window.BeatifyI18n&&BeatifyI18n.t("admin.home.waitingForGuests")||"Waiting for guests\u2026";n.innerHTML='
'+r+"
";return}let i=r=>String(r??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c]),s=["c1","c2","c3","c4"],d=0;n.innerHTML=t.map(r=>{let c=!!r.is_admin,m=!c&&r.onboarded===!1,u=c?"host":s[d++%s.length],g=(r.name||r.id||"?").trim(),v=(g.charAt(0)||"?").toUpperCase(),h=c?'':"",f=m?'':"",b=["home-player-tile",`home-player-tile--${u}`];return m&&b.push("home-player-tile--learning"),`
${i(v)}${i(g||"Guest")}`+h+f+"
"}).join("");let l=t.filter(r=>!r.is_admin&&r.onboarded===!1),o=document.getElementById("home-learning-warning");if(o)if(l.length>0){let r=l.length,c=r===1?"onboarding.learningWarning":"onboarding.learningWarningPlural",m=r===1?`\u26A0\uFE0F ${r} player still learning the rules`:`\u26A0\uFE0F ${r} players still learning the rules`,u=window.BeatifyI18n&&BeatifyI18n.t?BeatifyI18n.t(c,{count:r}):m;o.textContent=u===c?m:u,o.classList.remove("hidden")}else o.classList.add("hidden")},exit(){document.body.classList.remove("home-mode");let t=document.getElementById("home-view");t&&t.classList.add("hidden")},refresh(){try{let t=localStorage.getItem(_),n=t?JSON.parse(t):{},i=Array.isArray(n.selectedPlaylists)?n.selectedPlaylists:[],s=i.length===0?"no playlist":i.length===1?(i[0].path||i[0]).split("/").pop().replace(".json","").replace(/-/g," "):`${i.length} playlists`,d=typeof n.revealAutoAdvance=="number"?n.revealAutoAdvance:0,l=d>0?`${d}s`:"Off",o=`${n.difficulty||"normal"} \xB7 ${n.duration||45}s \xB7 ${(n.language||"en").toUpperCase()} \xB7 \u23ED\uFE0F ${l}`,r=`${s} \xB7 ${o}`,c=document.getElementById("home-meta");c&&(c.textContent=r)}catch{}},isConfigured(){try{let t=!!localStorage.getItem(T),n=localStorage.getItem(_),i=n?JSON.parse(n):{},s=Array.isArray(i.selectedPlaylists)&&i.selectedPlaylists.length>0;return t&&s}catch{return!1}}},document.getElementById("home-end-game")?.addEventListener("click",()=>{typeof G=="function"&&G()}),document.getElementById("home-join-player")?.addEventListener("click",()=>{typeof nt=="function"&&nt()}),document.getElementById("home-qr-code")?.addEventListener("click",()=>{typeof ce=="function"&&ce()}),document.getElementById("home-qr-code")?.addEventListener("keydown",t=>{(t.key==="Enter"||t.key===" ")&&typeof ce=="function"&&(t.preventDefault(),ce())}),document.getElementById("home-edit-setup")?.addEventListener("click",()=>{window.BeatifyHome.exit(),window.BeatifyWizard&&typeof window.BeatifyWizard.show=="function"&&window.BeatifyWizard.show(1)}),document.getElementById("home-start-game")?.addEventListener("click",async()=>{if(W(),(!a.currentGame||a.currentGame.phase!=="LOBBY")&&await F(),a.currentGame&&a.currentGame.phase==="LOBBY"){let t=a.currentGame.players||[];if(t.length===0){let i=window.BeatifyI18n&&BeatifyI18n.t("admin.home.needPlayerToStart")||"Join as player (or ask a guest to scan the QR) before starting.";L(i);return}let n=t.filter(i=>!i.is_admin&&i.onboarded===!1);if(n.length>0){let i=n.length,s=i===1?"onboarding.startAnyway":"onboarding.startAnywayPlural",d=i===1?`${i} player is still learning the rules. Start anyway?`:`${i} players are still learning the rules. Start anyway?`,l=window.BeatifyI18n&&BeatifyI18n.t?BeatifyI18n.t(s,{count:i}):d,o=l===s?d:l;if(!window.confirm(o))return}xt()}else Me()}),document.getElementById("home-start-setup")?.addEventListener("click",()=>{try{localStorage.removeItem("beatify_wizard_state")}catch{}window.BeatifyHome.exit(),window.BeatifyWizard&&typeof window.BeatifyWizard.show=="function"&&window.BeatifyWizard.show(1)}),document.getElementById("start-game")?.addEventListener("click",Me),document.getElementById("print-qr")?.addEventListener("click",jt),document.getElementById("end-game")?.addEventListener("click",G),document.getElementById("end-game-lobby")?.addEventListener("click",G),We(),Mt(),Ot(),document.getElementById("admin-stop-song")?.addEventListener("click",an),document.getElementById("admin-vol-down")?.addEventListener("click",on),document.getElementById("admin-vol-up")?.addEventListener("click",sn),document.getElementById("admin-end-game-playing")?.addEventListener("click",G),document.getElementById("admin-next-round")?.addEventListener("click",it),document.getElementById("admin-skip-round")?.addEventListener("click",it),document.getElementById("admin-confirm-intro")?.addEventListener("click",function(){N({type:"admin",action:"confirm_intro_splash"})}),document.getElementById("admin-rematch")?.addEventListener("click",zt),document.getElementById("admin-new-game")?.addEventListener("click",rn),document.getElementById("admin-resume-game")?.addEventListener("click",function(){N({type:"admin",action:"resume_game"})}),document.getElementById("admin-end-game-paused")?.addEventListener("click",G),qt(),Gt(),Wt(),Pt(),et(),Vt(),await ke(),await F(),!a.currentGame&&!document.body.classList.contains("home-mode")&&window.BeatifyHome.enter(),Qt()});async function F(){try{let e=await fetch("/beatify/api/status");if(!e.ok)throw new Error(`HTTP ${e.status}`);let t=await e.json();if(t.active_game&&t.active_game.game_id){let i=document.querySelector('meta[name="beatify-admin-token"]')?.content;i&&z(i,t.active_game.game_id)}a.playlistDocsUrl=t.playlist_docs_url||"",a.mediaPlayerDocsUrl=t.media_player_docs_url||"",a.hasMusicAssistant=t.has_music_assistant===!0;let n=document.getElementById("app-version");n&&t.version&&(n.textContent="v"+t.version,window.BEATIFY_VERSION=t.version),Ze(t.media_players),D(t.playlists,t.playlist_dir),$(),t.active_game&&t.active_game.phase==="LOBBY"?(a.currentGame=t.active_game,W(),Te(t.active_game),E()||M()):t.active_game&&t.active_game.phase!=="END"?(a.currentGame=t.active_game,E()||M(),lt(t.active_game)):be()}catch(e){console.error("Failed to load status:",e);let t=document.getElementById("media-players-list");t&&(t.innerHTML='Failed to load status')}}function Pt(){document.body.addEventListener("click",function(e){let t=e.target.closest(".section-header-collapsible");if(!t)return;let n=t.closest(".section-collapsible");n&&(n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",!n.classList.contains("collapsed")))})}function be(){if(a.currentView="setup",a.currentGame=null,st(),he(),a.previousLobbyPlayers=[],document.getElementById("admin-playing-section")?.classList.add("hidden"),document.getElementById("admin-reveal-section")?.classList.add("hidden"),document.getElementById("admin-end-section")?.classList.add("hidden"),Ue(),a.isPlaying=!1,a.adminPlayerName=null,window.BeatifyHome&&typeof window.BeatifyHome.enter=="function")try{window.BeatifyHome.enter()}catch(e){console.warn("[Beatify] BeatifyHome.enter failed in showSetupView:",e)}}function Te(e){a.currentView="lobby",a.currentGame=e,window.BeatifyHome&&window.BeatifyHome.renderSession(e),E()||ot()}function ce(){if(a.cachedQRUrl){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"&&new QRCode(t,{text:a.cachedQRUrl,width:280,height:280,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}),e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Pe(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Mt(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Pe),n&&n.addEventListener("click",Pe),R("qr-modal",Pe)}async function Me(){let e=document.getElementById("start-game"),t=document.body.classList.contains("home-mode");if(e&&e.disabled&&!t||a._startInFlight)return;a._startInFlight=!0,Ae(W);let n;e&&(e.disabled=!0,n=e.textContent,e.textContent=BeatifyI18n.t("game.starting"));try{let i={artist_challenge_enabled:a.artistChallengeEnabled,movie_quiz_enabled:a.movieQuizEnabled,intro_mode_enabled:a.introModeEnabled,closest_wins_mode:a.closestWinsModeEnabled},s=window.BeatifyTitleArtist&&typeof window.BeatifyTitleArtist.applyTitleArtistBonusPrecedence=="function"?window.BeatifyTitleArtist.applyTitleArtistBonusPrecedence(i,a.titleArtistModeEnabled):{...i,...a.titleArtistModeEnabled?{artist_challenge_enabled:!1,closest_wins_mode:!1}:{}},d=await BeatifyAuth.fetch("/beatify/api/start-game",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({playlists:a.selectedPlaylists.map(o=>o.path),media_player:a.selectedMediaPlayer?.entityId,language:a.selectedLanguage,round_duration:a.selectedDuration,reveal_auto_advance:a.revealAutoAdvance,difficulty:a.selectedDifficulty,provider:a.selectedProvider,artist_challenge_enabled:s.artist_challenge_enabled,movie_quiz_enabled:s.movie_quiz_enabled,intro_mode_enabled:s.intro_mode_enabled,closest_wins_mode:s.closest_wins_mode,title_artist_mode:a.titleArtistModeEnabled,party_lights:window._partyLightsConfig?window._partyLightsConfig():null,tts:window._ttsConfig?window._ttsConfig():null})}),l=await d.json();if(!d.ok){if(l.code==="GAME_IN_LOBBY"){await F();return}let o=l.message||"Failed to start game";if(l.code&&window.BeatifyI18n){let r="errors."+String(l.code).toUpperCase(),c=BeatifyI18n.t(r);c&&c!==r&&(o=c)}L(o);return}l.warnings&&l.warnings.length>0&&console.warn("Game started with warnings:",l.warnings),l.admin_token&&z(l.admin_token,l.game_id),Te(l),W(),M()}catch(i){L("Network error. Please try again."),console.error("Start game error:",i)}finally{a._startInFlight=!1,e&&(e.disabled=!1,e.textContent=n),$()}}function Ct(){var e=document.getElementById("home-start-game");e&&(e.disabled=!1,Ce&&(e.innerHTML=Ce))}async function xt(){let e=document.getElementById("home-start-game");if(e&&e.disabled)return;let t=null;if(e&&(e.disabled=!0,t=e.innerHTML,Ce=t,e.innerHTML=' '+BeatifyI18n.t("game.starting")),E()){H({type:"admin",action:"start_game"});return}try{let n=await BeatifyAuth.fetch("/beatify/api/start-gameplay",{method:"POST"}),i=await n.json();if(!n.ok){L(i.message||"Failed to start gameplay");return}await F()}catch(n){L("Network error. Please try again."),console.error("Start gameplay error:",n)}finally{e&&t!=null&&(e.disabled=!1,e.innerHTML=t)}}function Tt(){let e=document.getElementById("end-game-modal");e&&e.classList.remove("hidden")}function ve(){let e=document.getElementById("end-game-modal");e&&e.classList.add("hidden")}function G(){Tt()}async function Rt(){if(ve(),E()){H({type:"admin",action:"end_game"});return}if(!ae()){L("Admin session expired. Please reload the page.");return}try{let e=await BeatifyAuth.fetch("/beatify/api/end-game",{method:"POST"});if(e.ok)a.cachedQRUrl=null,be();else{let t=await e.json();L(t.message||"Failed to end game")}}catch(e){console.error("End game error:",e),L("Network error. Please try again.")}}function qt(){let e=document.getElementById("end-game-confirm-btn"),t=document.getElementById("end-game-cancel-btn"),n=document.querySelector("#end-game-modal .modal-backdrop");e?.addEventListener("click",Rt),t?.addEventListener("click",ve),n?.addEventListener("click",ve)}var Ht=["beatify_wizard_state","beatify_last_player","beatify_game_settings","beatify_party_lights","beatify_tts","beatify_admin_token","beatify_admin_token_game_id"];function Nt(){document.getElementById("reset-modal")?.classList.remove("hidden")}function fe(){document.getElementById("reset-modal")?.classList.add("hidden")}async function $t(){fe();try{await BeatifyAuth.fetch("/beatify/api/force-reset",{method:"POST"})}catch(e){console.warn("[Reset] force-reset POST failed (continuing with local cleanup):",e)}try{Ht.forEach(e=>localStorage.removeItem(e))}catch(e){console.warn("[Reset] localStorage clear failed:",e)}try{if("serviceWorker"in navigator){let e=await navigator.serviceWorker.getRegistrations();await Promise.all(e.map(t=>t.unregister()))}}catch(e){console.warn("[Reset] SW unregister failed:",e)}window.location.replace("/beatify/admin")}function Wt(){document.getElementById("reset-btn")?.addEventListener("click",Nt),document.getElementById("reset-confirm-btn")?.addEventListener("click",$t),document.getElementById("reset-cancel-btn")?.addEventListener("click",fe),document.querySelector("#reset-modal .modal-backdrop")?.addEventListener("click",fe),R("reset-modal",fe)}var me=!1;function zt(){var e=document.getElementById("rematch-modal");e&&e.classList.remove("hidden")}function ye(){var e=document.getElementById("rematch-modal");e&&e.classList.add("hidden")}async function Dt(){if(!me){me=!0,Ae(W);var e=document.getElementById("rematch-game"),t=e?e.textContent:"";if(e&&(e.disabled=!0,e.textContent="\u23F3"),ye(),E()){H({type:"admin",action:"rematch_game"}),me=!1;return}try{var n=await BeatifyAuth.fetch("/beatify/api/rematch-game",{method:"POST"});if(n.ok){var i=await n.json();await F()}else{var s=await n.json();alert(s.message||"Failed to start rematch")}}catch(d){console.error("Rematch failed:",d),alert("Failed to start rematch")}finally{me=!1,e&&(e.disabled=!1,e.textContent=t)}}}function Gt(){var e=document.getElementById("rematch-confirm-btn"),t=document.getElementById("rematch-cancel-btn"),n=document.querySelector("#rematch-modal .modal-backdrop");e?.addEventListener("click",Dt),t?.addEventListener("click",ye),n?.addEventListener("click",ye),R("rematch-modal",ye)}function jt(){window.print()}function L(e){alert(e)}function nt(){if(a.isPlaying&&a.adminPlayerName){L(BeatifyI18n.t("admin.alreadyJoined")||"Already joined as "+a.adminPlayerName);return}if(!E()){var e=null;try{e=sessionStorage.getItem("beatify_admin_name")}catch{}if(e&&a.currentGame&&a.currentGame.game_id){window.location.href="/beatify/play?game="+encodeURIComponent(a.currentGame.game_id);return}}let t=document.getElementById("admin-join-modal");t&&(t.classList.remove("hidden"),document.getElementById("admin-name-input")?.focus())}function pe(){let e=document.getElementById("admin-join-modal");e&&e.classList.add("hidden");let t=document.getElementById("admin-name-input"),n=document.getElementById("admin-join-btn"),i=document.getElementById("admin-name-error");t&&(t.value=""),n&&(n.disabled=!0,n.textContent=BeatifyI18n.t("admin.join")),i&&i.classList.add("hidden")}function Ot(){let e=document.getElementById("admin-cancel-btn"),t=document.getElementById("admin-join-btn"),n=document.getElementById("admin-name-input"),i=document.querySelector("#admin-join-modal .modal-backdrop");e?.addEventListener("click",pe),i?.addEventListener("click",pe),n?.addEventListener("input",function(){let s=this.value.trim();t.disabled=!s||s.length>20}),n?.addEventListener("keypress",function(s){s.key==="Enter"&&!t.disabled&&at()}),t?.addEventListener("click",at),R("admin-join-modal",pe),R("end-game-modal",ve)}function at(){let e=document.getElementById("admin-name-input"),t=document.getElementById("admin-join-btn"),n=e?.value.trim();if(!n)return;t.disabled=!0,t.textContent=BeatifyI18n.t("game.joining");let i=document.body.classList.contains("home-mode"),s=E();if(i){let d=async()=>{try{sessionStorage.setItem("beatify_admin_name",n),sessionStorage.setItem("beatify_is_admin","true"),a.adminPlayerName=n;let l=await BeatifyAuth.ensureAuthenticated();H({type:"join",name:n,is_admin:!0,ha_token:l}),pe()}catch(l){console.error("Admin join (home-mode) failed:",l),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join")}};if(s)d();else{M(),t.textContent=BeatifyI18n.t("admin.connecting")||"Connecting\u2026";let l=document.getElementById("admin-name-error");l&&(l.classList.add("hidden"),l.textContent="");let o=Date.now(),r=setInterval(()=>{E()?(clearInterval(r),d()):Date.now()-o>2e4&&(clearInterval(r),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join")||"Join",l?(l.textContent=BeatifyI18n.t("admin.home.wsReconnecting")||"Reconnecting to game server \u2014 please try again.",l.classList.remove("hidden")):L(BeatifyI18n.t("admin.home.wsReconnecting")||"Reconnecting to game server \u2014 please try again."))},100)}return}try{sessionStorage.setItem("beatify_admin_name",n),sessionStorage.setItem("beatify_is_admin","true");let d=a.currentGame?.game_id;d?window.location.href="/beatify/play?game="+encodeURIComponent(d):(L("No active game found"),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join"))}catch(d){console.error("Admin join failed:",d),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join")}}function Ut(e){var t=document.getElementById("lobby-players"),n=document.getElementById("lobby-player-count"),i=document.getElementById("admin-players-summary"),s=document.getElementById("lobby-players-empty");if(e=e||[],window.BeatifyHome&&typeof window.BeatifyHome.renderPlayers=="function"&&window.BeatifyHome.renderPlayers(e),!!t){if(n&&(n.textContent=e.length),i&&(i.textContent=e.length),e.length===0){t.innerHTML="",s&&s.classList.remove("hidden");var r=document.getElementById("start-gameplay-btn");r&&r.classList.add("hidden"),a.previousLobbyPlayers=[];return}s&&s.classList.add("hidden");var d=e.slice().sort(function(c,m){return c.connected!==m.connected?c.connected?-1:1:0}),l=a.previousLobbyPlayers.map(function(c){return c.name}),o=d.filter(function(c){return l.indexOf(c.name)===-1}).map(function(c){return c.name});t.innerHTML=d.map(function(c){var m=o.indexOf(c.name)!==-1,u=c.connected===!1,g=c.is_admin===!0,v=u&&!g,h=["player-card",m?"is-new":"",u?"player-card--disconnected":""].filter(Boolean).join(" "),f=g?'\u{1F451}':"",b=u?''+O.t("lobby.away","away")+"":"",w=v?'':"";return'
'+O.escapeHtml(c.name)+f+""+b+w+"
"}).join(""),t.querySelectorAll(".kick-player-btn").forEach(function(c){c.addEventListener("click",function(m){m.stopPropagation(),Ft(c.dataset.player)})}),setTimeout(function(){for(var c=t.querySelectorAll(".is-new"),m=0;m{e&&(e.classList.remove("hidden"),n?.focus())}),document.getElementById("request-cancel-btn")?.addEventListener("click",()=>{d()}),e?.querySelector(".modal-backdrop")?.addEventListener("click",()=>{d()}),R("request-modal",d),n?.addEventListener("input",()=>{let l=n.value.trim(),o=window.PlaylistRequests?.isValidSpotifyUrl(l);l&&!o?(n.classList.add("input-error"),i?.classList.remove("hidden")):(n.classList.remove("input-error"),i?.classList.add("hidden")),s&&(s.disabled=!o)}),s?.addEventListener("click",async()=>{let l=n?.value.trim();if(!(!l||!window.PlaylistRequests?.isValidSpotifyUrl(l))){s.classList.add("btn--loading"),s.disabled=!0;try{let o=await window.PlaylistRequests.submitRequest(l);d();let r=document.getElementById("request-success-name");r&&(r.textContent=o.playlist_name),t?.classList.remove("hidden"),await rt()}catch(o){if(console.error("Failed to submit request:",o),n?.classList.add("input-error"),i){let r=null;if(o.code&&window.BeatifyI18n){let c="errors."+String(o.code).toUpperCase(),m=BeatifyI18n.t(c);m&&m!==c&&(r=m)}i.textContent=r||o.message||"Failed to submit request",i.classList.remove("hidden")}}finally{s.classList.remove("btn--loading");let o=window.PlaylistRequests?.isValidSpotifyUrl(n?.value.trim()||"");s.disabled=!o}}}),document.getElementById("request-success-close-btn")?.addEventListener("click",()=>{t?.classList.add("hidden")}),t?.querySelector(".modal-backdrop")?.addEventListener("click",()=>{t?.classList.add("hidden")});function d(){e?.classList.add("hidden"),n&&(n.value="",n.classList.remove("input-error")),i?.classList.add("hidden"),s&&(s.disabled=!0,s.classList.remove("btn--loading"))}}async function Qt(){await rt()}async function rt(){if(!window.PlaylistRequests){document.getElementById("my-requests")?.classList.add("hidden");return}let e=await window.PlaylistRequests.getRequestsForDisplayAsync(),t=document.getElementById("my-requests"),n=document.getElementById("my-requests-list"),i=document.getElementById("my-requests-empty"),s=document.getElementById("my-requests-summary");!t||!n||(a.currentView==="setup"&&t.classList.remove("hidden"),s&&(s.textContent=e.length.toString()),e.length===0?(n.innerHTML="",i?.classList.remove("hidden")):(i?.classList.add("hidden"),n.innerHTML=e.map(d=>_e(d)).join("")))}(function(){let t=document.getElementById("pwa-install-btn"),n=document.getElementById("pwa-ios-hint"),i=document.getElementById("pwa-ios-hint-close");if(!t||window.matchMedia("(display-mode: standalone)").matches||window.navigator.standalone===!0)return;let s=null;window.addEventListener("beforeinstallprompt",o=>{o.preventDefault(),s=o,t.classList.remove("hidden")}),window.addEventListener("appinstalled",()=>{t.classList.add("hidden"),s=null,n&&n.classList.add("hidden")});let d=/iphone|ipad|ipod/i.test(navigator.userAgent),l=/safari/i.test(navigator.userAgent)&&!/chrome|crios|fxios/i.test(navigator.userAgent);d&&l&&t.classList.remove("hidden"),t.addEventListener("click",async()=>{if(s){s.prompt();let{outcome:o}=await s.userChoice;xe("[PWA] Install outcome:",o),s=null,o==="accepted"&&t.classList.add("hidden")}else d&&n&&n.classList.remove("hidden")}),i&&n&&i.addEventListener("click",()=>{n.classList.add("hidden")})})();function lt(e){if(a.currentGame=e,e.players&&!a.isPlaying){var t=e.players.find(function(i){return i.is_admin});if(t){a.isPlaying=!0,a.adminPlayerName=a.adminPlayerName||t.name;try{sessionStorage.setItem("beatify_admin_name",t.name)}catch{}}}Yt(),["LOBBY","PLAYING","REVEAL","PAUSED"].includes(e.phase)?W():st();var n=["setup-container","admin-playing-section","admin-reveal-section","admin-end-section","admin-control-bar"];switch(n.forEach(function(i){var s=document.getElementById(i);s&&s.classList.add("hidden")}),e.phase!=="REVEAL"&&dt(),kt.forEach(function(i){var s=document.getElementById(i);s&&s.classList.add("hidden")}),document.getElementById("start-game")?.classList.add("hidden"),document.getElementById("playlist-validation-msg")?.classList.add("hidden"),document.getElementById("media-player-validation-msg")?.classList.add("hidden"),e.phase&&e.phase!=="LOBBY"&&window.BeatifyHome&&window.BeatifyHome.exit(),e.phase==="LOBBY"&&window.BeatifyHome&&!document.body.classList.contains("home-mode")&&window.BeatifyHome.enter(),e.phase){case"LOBBY":Te(e);break;case"PLAYING":if(a.adminPlayerName&&a.adminSessionId&&a.currentGame&&a.currentGame.game_id){Re();return}Jt(e);break;case"REVEAL":Xt(e);break;case"END":en(e);break;case"PAUSED":nn(e);break;default:be()}}function Yt(){var e=["admin-playing-banner","admin-reveal-playing-banner"],t=["admin-playing-name","admin-reveal-playing-name"];e.forEach(function(n,i){var s=document.getElementById(n);if(s)if(a.isPlaying&&a.adminPlayerName){var d=document.getElementById(t[i]);d&&(d.textContent=a.adminPlayerName),s.classList.remove("hidden")}else s.classList.add("hidden")})}function Re(){var e=a.currentGame&&a.currentGame.game_id;if(e&&a.adminPlayerName){try{sessionStorage.setItem("beatify_admin_name",a.adminPlayerName),sessionStorage.setItem("beatify_is_admin","true"),a.adminSessionId&&sessionStorage.setItem("beatify_session",a.adminSessionId)}catch{}var t="/beatify/play?game="+encodeURIComponent(e);a.adminSessionId&&(t+="&session="+encodeURIComponent(a.adminSessionId)),window.location.href=t}}document.getElementById("switch-to-player-view")?.addEventListener("click",Re);document.getElementById("switch-to-player-view-reveal")?.addEventListener("click",Re);function Jt(e){var t=document.getElementById("admin-playing-section");if(t){t.classList.remove("hidden"),ct();var n=document.getElementById("admin-control-bar");n&&n.classList.remove("hidden");var i=document.getElementById("admin-current-round"),s=document.getElementById("admin-total-rounds");i&&(i.textContent=e.round||"?"),s&&(s.textContent=e.total_rounds||"?");var d=document.getElementById("admin-game-difficulty-badge");d&&e.difficulty&&(d.textContent=e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1));var l=document.getElementById("admin-album-art");l&&e.song&&e.song.album_art&&(l.src=e.song.album_art);var o=document.getElementById("admin-song-year"),r=document.getElementById("admin-song-funfact"),c=!1;try{c=!!sessionStorage.getItem("beatify_admin_name")}catch{}var m=a.isPlaying||c||(e.players||[]).some(function(w){return w.is_admin});if(e.admin_song&&!m){if(o&&(e.admin_song.year?(o.textContent="\u{1F4C5} "+e.admin_song.year,o.classList.remove("hidden")):o.classList.add("hidden")),r){var u=BeatifyI18n.getLanguage(),g=u!=="en"&&e.admin_song["fun_fact_"+u]?e.admin_song["fun_fact_"+u]:e.admin_song.fun_fact;g?(r.textContent="\u{1F4A1} "+g,r.classList.remove("hidden")):r.classList.add("hidden")}}else o&&o.classList.add("hidden"),r&&r.classList.add("hidden");Kt(e.deadline),Ve(e.players);var v=document.getElementById("admin-last-round");v&&v.classList.toggle("hidden",!e.last_round);var h=document.getElementById("admin-intro-badge");h&&h.classList.toggle("hidden",!e.is_intro_round);var f=document.getElementById("admin-closest-wins-badge");f&&f.classList.toggle("hidden",!e.closest_wins_mode);var b=document.getElementById("admin-intro-splash");b&&b.classList.toggle("hidden",!e.intro_splash_pending),de(e.leaderboard)}}function Kt(e){I&&clearInterval(I);var t=document.getElementById("admin-timer");if(!t||!e)return;function n(){var i=Date.now(),s=Math.max(0,Math.ceil((e-i)/1e3));t.textContent=s,t.classList.toggle("timer--warning",s<=10),t.classList.toggle("timer--critical",s<=5),s<=0&&(clearInterval(I),I=null)}n(),I=setInterval(n,1e3)}function Xt(e){var t=document.getElementById("admin-reveal-section");if(t){t.classList.remove("hidden"),ct();var n=document.getElementById("admin-reveal-idle-halt");n&&n.classList.toggle("hidden",!e.idle_halt);var i=document.getElementById("admin-control-bar");i&&i.classList.remove("hidden"),Zt(e);var s=document.getElementById("admin-reveal-emotion");if(s&&e.players){var d=e.players||[],l=d.filter(function(P){return P.years_off===0&&!P.missed_round}).length,o=0,r=d.filter(function(P){return!P.missed_round&&P.years_off!=null});r.length>0&&(o=Math.round(r.reduce(function(P,mt){return P+(mt.years_off||0)},0)/r.length));var c="",m="reveal-emotion--wrong";l>0?(c="\u{1F3AF} "+l+"x "+(BeatifyI18n.t("reveal.exact")||"Exact!"),m="reveal-emotion--exact"):o<=3?(c="\u{1F525} "+(BeatifyI18n.t("reveal.soClose")||"So close!"),m="reveal-emotion--close"):o<=10?(c="\u{1F440} \xD8 "+(BeatifyI18n.t("reveal.yearsOff",{years:o})||o+" years off"),m="reveal-emotion--wrong"):(c="\u{1F605} \xD8 "+(BeatifyI18n.t("reveal.yearsOff",{years:o})||o+" years off"),m="reveal-emotion--wrong"),s.className="reveal-emotion-inline "+m,s.innerHTML=''+c+"",s.classList.remove("hidden")}var u=document.getElementById("admin-reveal-round"),g=document.getElementById("admin-reveal-total");if(u&&(u.textContent=e.round||"?"),g&&(g.textContent=e.total_rounds||"?"),e.song){var v=document.getElementById("admin-reveal-song-title"),h=document.getElementById("admin-reveal-song-artist"),f=document.getElementById("admin-reveal-correct-year"),b=document.getElementById("admin-reveal-album-art");v&&(v.textContent=e.song.title||""),h&&(h.textContent=e.song.artist||""),f&&(f.textContent=e.song.year||""),b&&e.song.album_art&&(b.src=e.song.album_art)}var w=document.getElementById("admin-reveal-difficulty-badge");w&&e.difficulty&&(w.textContent=e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1));var x=document.getElementById("admin-fun-fact-container"),X=document.getElementById("admin-fun-fact-text");if(x&&e.song){var Z=BeatifyI18n.getLanguage(),qe=Z!=="en"&&e.song["fun_fact_"+Z]?e.song["fun_fact_"+Z]:e.song.fun_fact;qe?(X.textContent=qe,x.classList.remove("hidden")):x.classList.add("hidden")}var Ee=document.getElementById("admin-artist-reveal-section");Ee&&(e.artist_challenge&&e.artist_challenge.correct_answer?(document.getElementById("admin-artist-reveal-name").textContent=e.artist_challenge.correct_answer,Ee.classList.remove("hidden")):Ee.classList.add("hidden"));var Be=document.getElementById("admin-movie-reveal-section");Be&&(e.movie_challenge&&e.movie_challenge.correct_answer?(document.getElementById("admin-movie-reveal-name").textContent=e.movie_challenge.correct_answer,Be.classList.remove("hidden")):Be.classList.add("hidden"));var ee=document.getElementById("admin-reveal-personal");if(ee)if(a.isPlaying&&a.adminPlayerName&&e.players){var V=e.players.find(function(P){return P.is_admin});if(V){var te=document.getElementById("admin-reveal-my-guess"),ne=document.getElementById("admin-reveal-my-accuracy"),He=document.getElementById("admin-reveal-my-score");if(V.missed_round)te&&(te.textContent="\u2014"),ne&&(ne.textContent=BeatifyI18n.t("reveal.noGuessShort")||"Missed");else{var Le=V.years_off||0;te&&(te.textContent=V.guess||"\u2014"),ne&&(ne.textContent=Le===0?BeatifyI18n.t("reveal.exact")||"Exact!":BeatifyI18n.t("reveal.shortOff",{years:Le})||Le+" off")}He&&(He.textContent="+"+(V.round_score||0)),ee.classList.remove("hidden")}else ee.classList.add("hidden")}else ee.classList.add("hidden");Qe(e.players,e.closest_wins_mode,e.song?e.song.year:null),de(e.leaderboard)}}function dt(){C&&(clearInterval(C),C=null);var e=document.querySelector("#admin-skip-round .control-icon");e&&(e.classList.remove("is-countdown"),ge!==null&&(e.textContent=ge))}function Zt(e){var t=document.querySelector("#admin-skip-round .control-icon");if(!t)return;var n=e.reveal_auto_advance||0,i=e.reveal_started_at;if(n<=0||e.idle_halt||!i){dt();return}ge===null&&(ge=t.textContent);var s=i+n*1e3;function d(){var l=s-Date.now(),o=Math.max(0,Math.ceil(l/1e3));t.textContent=String(o),t.classList.add("is-countdown"),o<=0&&C&&(clearInterval(C),C=null)}C&&clearInterval(C),d(),C=setInterval(d,1e3)}function en(e){var t=document.getElementById("admin-end-section");if(t){if(t.classList.remove("hidden"),e.leaderboard)for(var n=1;n<=3;n++){var i=e.leaderboard.find(function(l){return l.rank===n}),s=document.getElementById("admin-podium-"+n+"-name"),d=document.getElementById("admin-podium-"+n+"-score");s&&(s.textContent=i?i.name:"---"),d&&(d.textContent=i?i.score:"0")}e.leaderboard&&de(e.leaderboard,"admin-end-leaderboard"),a.isPlaying=!1,a.adminPlayerName=null,I&&(clearInterval(I),I=null)}}function tn(e){var t=document.getElementById("admin-pause-recovery");if(t){var n=e&&e.pause_reason?e.pause_reason:"",i=e&&e.last_error_detail?e.last_error_detail:"",s=e&&e.provider?e.provider:"",d=n==="media_player_error"||n==="no_songs_available";if(!d){t.classList.add("hidden");return}var l=document.getElementById("admin-pause-recovery-message");if(l){var o=Ye(s),r;if(n==="no_songs_available")r=BeatifyI18n.t("admin.pauseRecovery.noSongsAvailable")||"No playable songs left for this provider. Resume to retry, or end the game.";else if(o){var c=BeatifyI18n.t("admin.pauseRecovery.mediaPlayerError")||"Playback did not start \u2014 the speaker is not responding. This often means {provider} in Music Assistant needs re-authentication \u2014 open Settings \u2192 Music Assistant \u2192 {provider} \u2192 Reconnect, then click Resume.";r=c.replace(/\{provider\}/g,o)}else r=BeatifyI18n.t("admin.pauseRecovery.mediaPlayerErrorGeneric")||"Playback did not start \u2014 the speaker is not responding. This often means your music provider in Music Assistant needs re-authentication \u2014 open Settings \u2192 Music Assistant \u2192 your provider \u2192 Reconnect, then click Resume.";l.textContent=r}var m=document.getElementById("admin-pause-recovery-detail");m&&(i?(m.textContent=i,m.classList.remove("hidden")):(m.textContent="",m.classList.add("hidden"))),t.classList.remove("hidden")}}function ct(){var e=document.getElementById("admin-pause-recovery");e&&e.classList.add("hidden")}function nn(e){var t=document.getElementById("admin-playing-section");if(t){t.classList.remove("hidden");var n=document.getElementById("admin-control-bar");n&&n.classList.remove("hidden");var i=document.getElementById("admin-timer");i&&(i.textContent="\u23F8 "+(BeatifyI18n.t("game.paused")||"Paused")),I&&(clearInterval(I),I=null),tn(e)}}function it(){N({type:"admin",action:"next_round"})}function an(){N({type:"admin",action:"stop_song"})}function sn(){N({type:"admin",action:"set_volume",direction:"up"})}function on(){N({type:"admin",action:"set_volume",direction:"down"})}function rn(){E()&&H({type:"admin",action:"dismiss_game"}),a.cachedQRUrl=null,a.isPlaying=!1,a.adminPlayerName=null;try{localStorage.removeItem(T),localStorage.removeItem(_),localStorage.removeItem("beatify_wizard_state")}catch{}window.BeatifyHome&&window.BeatifyHome.exit(),window.BeatifyWizard&&typeof window.BeatifyWizard.show=="function"?window.BeatifyWizard.show(1):be()}"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/sw.js",{scope:"/beatify/"}).then(function(e){xe("[Admin] SW registered:",e.scope)}).catch(function(e){console.warn("[Admin] SW registration failed:",e)})}); + `}function It(){let e=document.getElementById("media-players-list");e&&(e.querySelectorAll(".media-player-radio").forEach(t=>{t.addEventListener("change",function(){J(this)})}),e.querySelectorAll(".media-player-item").forEach(t=>{t.addEventListener("click",function(n){if(n.target.classList.contains("media-player-radio")||n.target.closest(".radio-label"))return;let i=t.querySelector(".media-player-radio");i&&!i.checked&&(i.checked=!0,J(i))})}))}function J(e,t=!1){let n=e.dataset.entityId,i=e.dataset.state,s=e.dataset.platform,l=e.dataset.supportsSpotify==="true",d=e.dataset.supportsAppleMusic==="true",o=e.dataset.supportsYoutubeMusic==="true",r=e.dataset.supportsTidal==="true",c=e.dataset.supportsDeezer==="true",m=e.dataset.supportsAmazonMusic==="true";a.selectedMediaPlayer={entityId:n,state:i,platform:s,supportsSpotify:l,supportsAppleMusic:d,supportsYoutubeMusic:o,supportsTidal:r,supportsDeezer:c,supportsAmazonMusic:m},document.querySelectorAll(".media-player-item").forEach(h=>{h.classList.remove("is-selected")});let u=e.closest(".media-player-item");u.classList.add("is-selected");let f=u.querySelector(".player-name")?.textContent?.trim()||n;Lt(f);let v=document.getElementById("music-service");if(v&&v.classList.remove("hidden"),_t(a.selectedMediaPlayer),At(a.selectedMediaPlayer),!t)try{localStorage.setItem(R,n)}catch(h){console.warn("Failed to save last player:",h)}D()}function _t(e){let t=document.querySelector('.chip[data-provider="spotify"]'),n=document.querySelector('.chip[data-provider="apple_music"]'),i=document.querySelector('.chip[data-provider="youtube_music"]'),s=document.querySelector('.chip[data-provider="tidal"]'),l=document.querySelector('.chip[data-provider="deezer"]'),d=document.querySelector('.chip[data-provider="amazon_music"]');t&&(t.disabled=!e.supportsSpotify,t.classList.toggle("chip--disabled",!e.supportsSpotify)),n&&(n.disabled=!e.supportsAppleMusic,n.classList.toggle("chip--disabled",!e.supportsAppleMusic)),i&&(i.disabled=!e.supportsYoutubeMusic,i.classList.toggle("chip--disabled",!e.supportsYoutubeMusic)),s&&(s.disabled=!e.supportsTidal,s.classList.toggle("chip--disabled",!e.supportsTidal)),l&&(l.disabled=!e.supportsDeezer,l.classList.toggle("chip--disabled",!e.supportsDeezer)),d&&(d.disabled=!e.supportsAmazonMusic,d.classList.toggle("chip--disabled",!e.supportsAmazonMusic)),a.selectedProvider==="apple_music"&&!e.supportsAppleMusic&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="youtube_music"&&!e.supportsYoutubeMusic&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="tidal"&&!e.supportsTidal&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="deezer"&&!e.supportsDeezer&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify"),a.selectedProvider==="amazon_music"&&!e.supportsAmazonMusic&&(document.querySelectorAll(".chip[data-provider]").forEach(r=>r.classList.remove("chip--active")),t&&t.classList.add("chip--active"),a.selectedProvider="spotify");let o=document.getElementById("provider-hint");if(o){let r=[];e.supportsAppleMusic||r.push("Apple Music"),e.supportsYoutubeMusic||r.push("YouTube Music"),e.supportsTidal||r.push("Tidal"),e.supportsDeezer||r.push("Deezer");let c=[];r.length>0&&c.push(`${r.join(" and ")} require${r.length===1?"s":""} a Music Assistant speaker`),e.supportsAmazonMusic||c.push("Amazon Music requires an Amazon Echo (alexa_media)"),c.length>0?(o.textContent=c.join(" \xB7 "),o.classList.remove("hidden")):o.classList.add("hidden")}}function At(e){let t=document.getElementById("provider-warning");if(!t)return;let i={music_assistant:{warning:"Premium account must be configured in Music Assistant"},sonos:{warning:"Spotify must be linked in Sonos app"},alexa_media:{warning:"Service must be linked in Alexa app",caveat:"Uses voice search - may occasionally play a different version of the song"},alexa:{warning:"Service must be linked in Alexa app",caveat:"Uses voice search - may occasionally play a different version of the song"}}[e.platform];if(i){let s=`

\u26A0\uFE0F ${B.escapeHtml(i.warning)}

`;i.caveat&&(s+=`

\u2139\uFE0F ${B.escapeHtml(i.caveat)}

`),t.innerHTML=s,t.classList.remove("hidden")}else t.classList.add("hidden")}function et(){document.querySelectorAll(".chip[data-lang]").forEach(e=>{e.addEventListener("click",async function(){let t=this.dataset.lang;document.querySelectorAll(".chip[data-lang]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedLanguage=t,window.BeatifyI18n&&(await BeatifyI18n.setLanguage(t),BeatifyI18n.initPageTranslations()),k(),S()})}),document.querySelectorAll(".chip[data-duration]").forEach(e=>{e.addEventListener("click",function(){let t=parseInt(this.dataset.duration,10);document.querySelectorAll(".chip[data-duration]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedDuration=t,k(),S()})}),document.querySelectorAll(".chip[data-reveal-advance]").forEach(e=>{e.addEventListener("click",function(){a.revealAutoAdvance=parseInt(this.dataset.revealAdvance,10)||0,document.querySelectorAll(".chip[data-reveal-advance]").forEach(t=>t.classList.remove("chip--active")),this.classList.add("chip--active"),S()})}),document.querySelectorAll(".chip[data-difficulty]").forEach(e=>{e.addEventListener("click",function(){let t=this.dataset.difficulty;document.querySelectorAll(".chip[data-difficulty]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedDifficulty=t,k(),S()})}),document.getElementById("artist-challenge-toggle")?.addEventListener("change",function(){a.artistChallengeEnabled=this.checked,k(),S()}),document.getElementById("movie-quiz-toggle")?.addEventListener("change",function(){a.movieQuizEnabled=this.checked,k(),S()}),document.getElementById("intro-mode-toggle")?.addEventListener("change",function(){a.introModeEnabled=this.checked,k(),S()}),document.getElementById("closest-wins-toggle")?.addEventListener("change",function(){a.closestWinsModeEnabled=this.checked,k(),S()}),document.getElementById("title-artist-mode-toggle")?.addEventListener("change",function(){a.titleArtistModeEnabled=this.checked,tt(),k(),S()}),document.querySelectorAll(".chip[data-provider]").forEach(e=>{e.addEventListener("click",function(){if(this.disabled||this.classList.contains("chip--disabled"))return;let t=this.dataset.provider;document.querySelectorAll(".chip[data-provider]").forEach(n=>n.classList.remove("chip--active")),this.classList.add("chip--active"),a.selectedProvider=t,k(),S(),a.playlistData.length>0&&G(a.playlistData,"",!0)})})}async function ke(){try{let e=localStorage.getItem(I);if(e){let t=JSON.parse(e);if(t.language&&(a.selectedLanguage=t.language,document.querySelectorAll(".chip[data-lang]").forEach(n=>{n.classList.toggle("chip--active",n.dataset.lang===t.language)}),window.BeatifyI18n&&(await BeatifyI18n.setLanguage(t.language),BeatifyI18n.initPageTranslations())),t.duration&&(a.selectedDuration=t.duration,document.querySelectorAll(".chip[data-duration]").forEach(n=>{n.classList.toggle("chip--active",parseInt(n.dataset.duration,10)===t.duration)})),typeof t.revealAutoAdvance=="number"&&(a.revealAutoAdvance=t.revealAutoAdvance,document.querySelectorAll(".chip[data-reveal-advance]").forEach(n=>{n.classList.toggle("chip--active",parseInt(n.dataset.revealAdvance,10)===t.revealAutoAdvance)})),t.difficulty&&(a.selectedDifficulty=t.difficulty,document.querySelectorAll(".chip[data-difficulty]").forEach(n=>{n.classList.toggle("chip--active",n.dataset.difficulty===t.difficulty)})),typeof t.artistChallenge=="boolean"){a.artistChallengeEnabled=t.artistChallenge;let n=document.getElementById("artist-challenge-toggle");n&&(n.checked=t.artistChallenge)}if(typeof t.movieQuiz=="boolean"){a.movieQuizEnabled=t.movieQuiz;let n=document.getElementById("movie-quiz-toggle");n&&(n.checked=t.movieQuiz)}if(typeof t.introMode=="boolean"){a.introModeEnabled=t.introMode;let n=document.getElementById("intro-mode-toggle");n&&(n.checked=t.introMode)}if(typeof t.closestWinsMode=="boolean"){a.closestWinsModeEnabled=t.closestWinsMode;let n=document.getElementById("closest-wins-toggle");n&&(n.checked=t.closestWinsMode)}if(typeof t.titleArtistMode=="boolean"){a.titleArtistModeEnabled=t.titleArtistMode;let n=document.getElementById("title-artist-mode-toggle");n&&(n.checked=t.titleArtistMode)}tt(),t.provider&&(a.selectedProvider=t.provider,document.querySelectorAll(".chip[data-provider]").forEach(n=>{n.classList.toggle("chip--active",n.dataset.provider===t.provider)}))}}catch(e){console.warn("Failed to load saved settings:",e)}k()}function S(){try{let e={language:a.selectedLanguage,duration:a.selectedDuration,revealAutoAdvance:a.revealAutoAdvance,difficulty:a.selectedDifficulty,artistChallenge:a.artistChallengeEnabled,movieQuiz:a.movieQuizEnabled,introMode:a.introModeEnabled,closestWinsMode:a.closestWinsModeEnabled,titleArtistMode:a.titleArtistModeEnabled,provider:a.selectedProvider};localStorage.setItem(I,JSON.stringify(e))}catch(e){console.warn("Failed to save settings:",e)}}function k(){let e=document.getElementById("game-settings-summary");if(!e)return;let t={easy:"Easy",normal:"Normal",hard:"Hard"},n={en:"EN",de:"DE",es:"ES"},i=!a.titleArtistModeEnabled,s=i&&a.artistChallengeEnabled?" \u2022 \u{1F3A4}":"",l=i&&a.movieQuizEnabled?" \u2022 \u{1F3AC}":"",d=i&&a.introModeEnabled?" \u2022 \u26A1":"",o=i&&a.closestWinsModeEnabled?" \u2022 \u{1F3AF}":"",r=a.titleArtistModeEnabled?" \u2022 \u{1F3B5}":"";e.textContent=`${t[a.selectedDifficulty]||"Normal"} \u2022 ${a.selectedDuration}s \u2022 ${n[a.selectedLanguage]||"EN"}${r}${s}${l}${d}${o}`}function tt(){var e=["artist-challenge-toggle","closest-wins-toggle"];e.forEach(function(s){var l=document.getElementById(s);if(l){var d=l.closest(".setting-group");d&&d.classList.toggle("hidden",a.titleArtistModeEnabled),l.disabled=a.titleArtistModeEnabled}});var t=document.getElementById("admin-difficulty-row");t&&t.classList.toggle("hidden",a.titleArtistModeEnabled);var n=document.getElementById("admin-difficulty-hint");n&&n.classList.toggle("hidden",a.titleArtistModeEnabled);var i=document.getElementById("admin-difficulty-ta-summary");i&&i.classList.toggle("hidden",!a.titleArtistModeEnabled)}ze(()=>a.currentGame);window.escapeHtml=x;window.groupPlayersByPlatform=Ge;window.buildRequestRowHtml=_e;window._getAdminToken=ae;window._setAdminToken=z;window._adminHeaders=Ie;window.clearPlaylistFilters=Ke;var F=null,O=null,K=!1;function St(){if(O)return O;if(typeof window<"u"&&typeof window.NoSleep=="function")try{O=new window.NoSleep}catch(e){console.debug("[BeatifyWakeLock] NoSleep instantiation failed:",e)}return O}async function W(){if("wakeLock"in navigator)try{F=await navigator.wakeLock.request("screen"),F.addEventListener("release",function(){console.debug("[BeatifyWakeLock] Layer 1 released by browser"),F=null}),console.debug("[BeatifyWakeLock] Layer 1 (native wakeLock) acquired");return}catch(n){console.debug("[BeatifyWakeLock] Layer 1 request failed:",n,"\u2014 trying Layer 2")}else console.debug("[BeatifyWakeLock] Layer 1 unavailable \u2014 using Layer 2");var e=St();if(!e){console.debug("[BeatifyWakeLock] Layer 2 unavailable (NoSleep vendor not loaded)");return}if(!K)try{var t=e.enable();K=!0,t&&typeof t.catch=="function"&&t.catch(function(n){console.debug("[BeatifyWakeLock] Layer 2 enable promise rejected:",n)}),console.debug("[BeatifyWakeLock] Layer 2 (NoSleep video) enabled")}catch(n){console.debug("[BeatifyWakeLock] Layer 2 enable failed:",n),K=!1}}function st(){if(F){try{F.release()}catch{}F=null}if(K&&O){try{O.disable()}catch{}K=!1,console.debug("[BeatifyWakeLock] Layer 2 (NoSleep) disabled")}}document.addEventListener("visibilitychange",function(){document.visibilityState==="visible"&&a.currentGame&&a.currentGame.phase!=="END"&&(W(),E()||(Fe(),M()))});var ue=null,Ce=null,_=null;Oe({debug:(...e)=>xe(...e),getCurrentGame:()=>a.currentGame,getCurrentView:()=>a.currentView,getAdminPlayerName:()=>a.adminPlayerName,setIsPlaying:e=>{a.isPlaying=e},setAdminPlayerName:e=>{a.adminPlayerName=e},setAdminSessionId:e=>{a.adminSessionId=e},handleAdminStateUpdate:e=>lt(e),startLobbyPolling:()=>ot(),stopLobbyPolling:()=>he(),showError:e=>L(e),resetHomeStartButton:()=>Ct()});var C=null,ge=null,kt=["media-players","music-service","playlists","game-settings","admin-actions","my-requests","party-lights","tts-settings","ha-entities"],U=window.BeatifyUtils||{},xe=U.debug||function(){};document.addEventListener("DOMContentLoaded",async()=>{if(await BeatifyAuth.init({requireAuth:!0}),await U.waitForI18n()?(await BeatifyI18n.init(),BeatifyI18n.initPageTranslations(),a.selectedLanguage=BeatifyI18n.getLanguage()):console.error("[Beatify] BeatifyI18n module failed to load - UI will use fallback text"),document.querySelectorAll(".chip[data-lang]").forEach(t=>{t.classList.toggle("chip--active",t.dataset.lang===a.selectedLanguage)}),window.BeatifyWizard&&typeof window.BeatifyWizard.init=="function")try{await window.BeatifyWizard.init()}catch(t){console.warn("[Beatify] wizard init failed:",t)}window.loadStatus=V,window.loadSavedSettings=ke,window.BeatifyHome={enter(){document.body.classList.add("home-mode");let t=document.getElementById("home-view");t&&t.classList.remove("hidden"),this.refresh(),this.isConfigured()?(this.setMode("configured"),this.hydrateFromStorage(),a.currentGame&&a.currentGame.phase==="LOBBY"&&a.currentGame.join_url?this.renderSession(a.currentGame):this.startSession()):this.setMode("setup")},hydrateFromStorage(){try{let t=localStorage.getItem(R);if(t&&(!a.selectedMediaPlayer||!a.selectedMediaPlayer.entityId)){let i=document.querySelector(`.media-player-radio[data-entity-id="${CSS.escape(t)}"]`);i?(i.checked=!0,J(i,!0)):a.selectedMediaPlayer={entityId:t,state:"unknown",platform:"unknown"}}let n=localStorage.getItem(I);if(n){let i=JSON.parse(n);je(a,i);let s=Array.isArray(i.selectedPlaylists)?i.selectedPlaylists.map(l=>typeof l=="string"?l:l.path).filter(Boolean):[];s.length&&a.selectedPlaylists.length===0&&(a.selectedPlaylists=s.map(l=>{let d=(a.playlistData||[]).find(o=>o.path===l);return{path:l,songCount:d&&(d.song_count||d.songCount)||0}}))}}catch(t){console.warn("[Beatify] hydrateFromStorage failed:",t)}},setMode(t){let n=t==="configured";if(document.getElementById("home-hero-configured")?.classList.toggle("hidden",!n),document.getElementById("home-hero-setup")?.classList.toggle("hidden",n),document.getElementById("home-edit-setup")?.classList.toggle("hidden",!n),document.getElementById("home-start-game")?.classList.toggle("hidden",!n),document.getElementById("home-start-setup")?.classList.toggle("hidden",n),!n){document.getElementById("home-dashboard-url")?.classList.add("hidden"),document.getElementById("home-join-player")?.classList.add("hidden"),document.getElementById("home-end-game")?.classList.add("hidden");let i=document.getElementById("home-meta");i&&(i.textContent="");let s=document.getElementById("home-players");s&&(s.innerHTML="")}},async startSession(){let t=document.getElementById("home-qr-loading");t&&t.classList.remove("hidden");try{await Me()}catch(n){console.warn("[Beatify] Home auto-start failed:",n)}},renderSession(t){let n=document.getElementById("home-qr-loading");n&&n.classList.add("hidden");let i=document.getElementById("home-qr-code");i&&t.join_url&&typeof QRCode<"u"&&(i.dataset.url!==t.join_url&&(i.innerHTML="",new QRCode(i,{text:t.join_url,width:180,height:180,colorDark:"#0a0a12",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}),i.dataset.url=t.join_url,i.setAttribute("role","button"),i.setAttribute("tabindex","0"),i.style.cursor="pointer"),a.cachedQRUrl=t.join_url);let s=document.getElementById("home-join-url");s&&t.join_url&&(s.textContent=t.join_url);let l=document.getElementById("home-dashboard-url");if(l){let u=t.dashboard_url;!u&&t.join_url&&(u=t.join_url.split("/beatify/play")[0]+"/beatify/dashboard"),u?(l.href=u,l.classList.remove("hidden")):l.classList.add("hidden")}let d=t.phase==="LOBBY"&&!!t.join_url,o=t.phase&&t.phase!=="LOBBY"&&t.phase!=="END";document.getElementById("home-end-game")?.classList.toggle("hidden",!o);let r=(t.players||[]).some(u=>u.is_admin),c=d&&!r&&!a.isPlaying;document.getElementById("home-join-player")?.classList.toggle("hidden",!c);let m=document.getElementById("home-start-game");m&&(m.classList.toggle("btn-primary",!c),m.classList.toggle("btn-ghost",c)),this.renderPlayers(t.players||[])},renderPlayers(t){let n=document.getElementById("home-players");if(!n)return;if(!t.length){let r=window.BeatifyI18n&&BeatifyI18n.t("admin.home.waitingForGuests")||"Waiting for guests\u2026";n.innerHTML='
'+r+"
";return}let i=r=>String(r??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c]),s=["c1","c2","c3","c4"],l=0;n.innerHTML=t.map(r=>{let c=!!r.is_admin,m=!c&&r.onboarded===!1,u=c?"host":s[l++%s.length],f=(r.name||r.id||"?").trim(),v=(f.charAt(0)||"?").toUpperCase(),h=c?'':"",y=m?'':"",b=["home-player-tile",`home-player-tile--${u}`];return m&&b.push("home-player-tile--learning"),`
${i(v)}${i(f||"Guest")}`+h+y+"
"}).join("");let d=t.filter(r=>!r.is_admin&&r.onboarded===!1),o=document.getElementById("home-learning-warning");if(o)if(d.length>0){let r=d.length,c=r===1?"onboarding.learningWarning":"onboarding.learningWarningPlural",m=r===1?`\u26A0\uFE0F ${r} player still learning the rules`:`\u26A0\uFE0F ${r} players still learning the rules`,u=window.BeatifyI18n&&BeatifyI18n.t?BeatifyI18n.t(c,{count:r}):m;o.textContent=u===c?m:u,o.classList.remove("hidden")}else o.classList.add("hidden")},exit(){document.body.classList.remove("home-mode");let t=document.getElementById("home-view");t&&t.classList.add("hidden")},refresh(){try{let t=localStorage.getItem(I),n=t?JSON.parse(t):{},i=Array.isArray(n.selectedPlaylists)?n.selectedPlaylists:[],s=i.length===0?"no playlist":i.length===1?(i[0].path||i[0]).split("/").pop().replace(".json","").replace(/-/g," "):`${i.length} playlists`,l=typeof n.revealAutoAdvance=="number"?n.revealAutoAdvance:0,d=l>0?`${l}s`:"Off",o=`${n.difficulty||"normal"} \xB7 ${n.duration||45}s \xB7 ${(n.language||"en").toUpperCase()} \xB7 \u23ED\uFE0F ${d}`,r=`${s} \xB7 ${o}`,c=document.getElementById("home-meta");c&&(c.textContent=r)}catch{}},isConfigured(){try{let t=!!localStorage.getItem(R),n=localStorage.getItem(I),i=n?JSON.parse(n):{},s=Array.isArray(i.selectedPlaylists)&&i.selectedPlaylists.length>0;return t&&s}catch{return!1}}},document.getElementById("home-end-game")?.addEventListener("click",()=>{typeof j=="function"&&j()}),document.getElementById("home-join-player")?.addEventListener("click",()=>{typeof nt=="function"&&nt()}),document.getElementById("home-qr-code")?.addEventListener("click",()=>{typeof ce=="function"&&ce()}),document.getElementById("home-qr-code")?.addEventListener("keydown",t=>{(t.key==="Enter"||t.key===" ")&&typeof ce=="function"&&(t.preventDefault(),ce())}),document.getElementById("home-edit-setup")?.addEventListener("click",()=>{window.BeatifyHome.exit(),window.BeatifyWizard&&typeof window.BeatifyWizard.show=="function"&&window.BeatifyWizard.show(1)}),document.getElementById("home-start-game")?.addEventListener("click",async()=>{if(W(),(!a.currentGame||a.currentGame.phase!=="LOBBY")&&await V(),a.currentGame&&a.currentGame.phase==="LOBBY"){let t=a.currentGame.players||[];if(t.length===0){let i=window.BeatifyI18n&&BeatifyI18n.t("admin.home.needPlayerToStart")||"Join as player (or ask a guest to scan the QR) before starting.";L(i);return}let n=t.filter(i=>!i.is_admin&&i.onboarded===!1);if(n.length>0){let i=n.length,s=i===1?"onboarding.startAnyway":"onboarding.startAnywayPlural",l=i===1?`${i} player is still learning the rules. Start anyway?`:`${i} players are still learning the rules. Start anyway?`,d=window.BeatifyI18n&&BeatifyI18n.t?BeatifyI18n.t(s,{count:i}):l,o=d===s?l:d;if(!window.confirm(o))return}xt()}else Me()}),document.getElementById("home-start-setup")?.addEventListener("click",()=>{try{localStorage.removeItem("beatify_wizard_state")}catch{}window.BeatifyHome.exit(),window.BeatifyWizard&&typeof window.BeatifyWizard.show=="function"&&window.BeatifyWizard.show(1)}),document.getElementById("start-game")?.addEventListener("click",Me),document.getElementById("print-qr")?.addEventListener("click",jt),document.getElementById("end-game")?.addEventListener("click",j),document.getElementById("end-game-lobby")?.addEventListener("click",j),De(),Mt(),Ot(),document.getElementById("admin-stop-song")?.addEventListener("click",on),document.getElementById("admin-vol-down")?.addEventListener("click",ln),document.getElementById("admin-vol-up")?.addEventListener("click",rn),document.getElementById("admin-end-game-playing")?.addEventListener("click",j),document.getElementById("admin-next-round")?.addEventListener("click",it),document.getElementById("admin-skip-round")?.addEventListener("click",it),document.getElementById("admin-confirm-intro")?.addEventListener("click",function(){$({type:"admin",action:"confirm_intro_splash"})}),document.getElementById("admin-rematch")?.addEventListener("click",Wt),document.getElementById("admin-new-game")?.addEventListener("click",dn),document.getElementById("admin-resume-game")?.addEventListener("click",function(){$({type:"admin",action:"resume_game"})}),document.getElementById("admin-end-game-paused")?.addEventListener("click",j),qt(),Gt(),Dt(),Pt(),et(),Vt(),await ke(),await V(),!a.currentGame&&!document.body.classList.contains("home-mode")&&window.BeatifyHome.enter(),Qt()});async function V(){try{let e=await fetch("/beatify/api/status");if(!e.ok)throw new Error(`HTTP ${e.status}`);let t=await e.json();if(t.active_game&&t.active_game.game_id){let i=document.querySelector('meta[name="beatify-admin-token"]')?.content;i&&z(i,t.active_game.game_id)}a.playlistDocsUrl=t.playlist_docs_url||"",a.mediaPlayerDocsUrl=t.media_player_docs_url||"",a.hasMusicAssistant=t.has_music_assistant===!0;let n=document.getElementById("app-version");n&&t.version&&(n.textContent="v"+t.version,window.BEATIFY_VERSION=t.version),Ze(t.media_players),G(t.playlists,t.playlist_dir),D(),t.active_game&&t.active_game.phase==="LOBBY"?(a.currentGame=t.active_game,W(),Te(t.active_game),E()||M()):t.active_game&&t.active_game.phase!=="END"?(a.currentGame=t.active_game,E()||M(),lt(t.active_game)):be()}catch(e){console.error("Failed to load status:",e);let t=document.getElementById("media-players-list");t&&(t.innerHTML='Failed to load status')}}function Pt(){document.body.addEventListener("click",function(e){let t=e.target.closest(".section-header-collapsible");if(!t)return;let n=t.closest(".section-collapsible");n&&(n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",!n.classList.contains("collapsed")))})}function be(){if(a.currentView="setup",a.currentGame=null,st(),he(),a.previousLobbyPlayers=[],document.getElementById("admin-playing-section")?.classList.add("hidden"),document.getElementById("admin-reveal-section")?.classList.add("hidden"),document.getElementById("admin-end-section")?.classList.add("hidden"),Ue(),a.isPlaying=!1,a.adminPlayerName=null,window.BeatifyHome&&typeof window.BeatifyHome.enter=="function")try{window.BeatifyHome.enter()}catch(e){console.warn("[Beatify] BeatifyHome.enter failed in showSetupView:",e)}}function Te(e){a.currentView="lobby",a.currentGame=e,window.BeatifyHome&&window.BeatifyHome.renderSession(e),E()||ot()}function ce(){if(a.cachedQRUrl){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"&&new QRCode(t,{text:a.cachedQRUrl,width:280,height:280,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}),e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Pe(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Mt(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Pe),n&&n.addEventListener("click",Pe),q("qr-modal",Pe)}async function Me(){let e=document.getElementById("start-game"),t=document.body.classList.contains("home-mode");if(e&&e.disabled&&!t||a._startInFlight)return;a._startInFlight=!0,Ae(W);let n;e&&(e.disabled=!0,n=e.textContent,e.textContent=BeatifyI18n.t("game.starting"));try{let d={artist_challenge_enabled:a.artistChallengeEnabled,movie_quiz_enabled:a.movieQuizEnabled,intro_mode_enabled:a.introModeEnabled,closest_wins_mode:a.closestWinsModeEnabled},o=window.BeatifyTitleArtist&&typeof window.BeatifyTitleArtist.applyTitleArtistBonusPrecedence=="function"?window.BeatifyTitleArtist.applyTitleArtistBonusPrecedence(d,a.titleArtistModeEnabled):{...d,...a.titleArtistModeEnabled?{artist_challenge_enabled:!1,closest_wins_mode:!1}:{}};var i=!1;try{var s=localStorage.getItem(I);if(s){var l=JSON.parse(s);l&&typeof l.suddenDeathMode=="boolean"&&(i=l.suddenDeathMode)}}catch{}let r=await BeatifyAuth.fetch("/beatify/api/start-game",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({playlists:a.selectedPlaylists.map(m=>m.path),media_player:a.selectedMediaPlayer?.entityId,language:a.selectedLanguage,round_duration:a.selectedDuration,reveal_auto_advance:a.revealAutoAdvance,difficulty:a.selectedDifficulty,provider:a.selectedProvider,artist_challenge_enabled:o.artist_challenge_enabled,movie_quiz_enabled:o.movie_quiz_enabled,intro_mode_enabled:o.intro_mode_enabled,closest_wins_mode:o.closest_wins_mode,sudden_death_mode:i,title_artist_mode:a.titleArtistModeEnabled,party_lights:window._partyLightsConfig?window._partyLightsConfig():null,tts:window._ttsConfig?window._ttsConfig():null})}),c=await r.json();if(!r.ok){if(c.code==="GAME_IN_LOBBY"){await V();return}let m=c.message||"Failed to start game";if(c.code&&window.BeatifyI18n){let u="errors."+String(c.code).toUpperCase(),f=BeatifyI18n.t(u);f&&f!==u&&(m=f)}L(m);return}c.warnings&&c.warnings.length>0&&console.warn("Game started with warnings:",c.warnings),c.admin_token&&z(c.admin_token,c.game_id),Te(c),W(),M()}catch(d){L("Network error. Please try again."),console.error("Start game error:",d)}finally{a._startInFlight=!1,e&&(e.disabled=!1,e.textContent=n),D()}}function Ct(){var e=document.getElementById("home-start-game");e&&(e.disabled=!1,Ce&&(e.innerHTML=Ce))}async function xt(){let e=document.getElementById("home-start-game");if(e&&e.disabled)return;let t=null;if(e&&(e.disabled=!0,t=e.innerHTML,Ce=t,e.innerHTML=' '+BeatifyI18n.t("game.starting")),E()){N({type:"admin",action:"start_game"});return}try{let n=await BeatifyAuth.fetch("/beatify/api/start-gameplay",{method:"POST"}),i=await n.json();if(!n.ok){L(i.message||"Failed to start gameplay");return}await V()}catch(n){L("Network error. Please try again."),console.error("Start gameplay error:",n)}finally{e&&t!=null&&(e.disabled=!1,e.innerHTML=t)}}function Tt(){let e=document.getElementById("end-game-modal");e&&e.classList.remove("hidden")}function ve(){let e=document.getElementById("end-game-modal");e&&e.classList.add("hidden")}function j(){Tt()}async function Rt(){if(ve(),E()){N({type:"admin",action:"end_game"});return}if(!ae()){L("Admin session expired. Please reload the page.");return}try{let e=await BeatifyAuth.fetch("/beatify/api/end-game",{method:"POST"});if(e.ok)a.cachedQRUrl=null,be();else{let t=await e.json();L(t.message||"Failed to end game")}}catch(e){console.error("End game error:",e),L("Network error. Please try again.")}}function qt(){let e=document.getElementById("end-game-confirm-btn"),t=document.getElementById("end-game-cancel-btn"),n=document.querySelector("#end-game-modal .modal-backdrop");e?.addEventListener("click",Rt),t?.addEventListener("click",ve),n?.addEventListener("click",ve)}var Ht=["beatify_wizard_state","beatify_last_player","beatify_game_settings","beatify_party_lights","beatify_tts","beatify_admin_token","beatify_admin_token_game_id"];function Nt(){document.getElementById("reset-modal")?.classList.remove("hidden")}function fe(){document.getElementById("reset-modal")?.classList.add("hidden")}async function $t(){fe();try{await BeatifyAuth.fetch("/beatify/api/force-reset",{method:"POST"})}catch(e){console.warn("[Reset] force-reset POST failed (continuing with local cleanup):",e)}try{Ht.forEach(e=>localStorage.removeItem(e))}catch(e){console.warn("[Reset] localStorage clear failed:",e)}try{if("serviceWorker"in navigator){let e=await navigator.serviceWorker.getRegistrations();await Promise.all(e.map(t=>t.unregister()))}}catch(e){console.warn("[Reset] SW unregister failed:",e)}window.location.replace("/beatify/admin")}function Dt(){document.getElementById("reset-btn")?.addEventListener("click",Nt),document.getElementById("reset-confirm-btn")?.addEventListener("click",$t),document.getElementById("reset-cancel-btn")?.addEventListener("click",fe),document.querySelector("#reset-modal .modal-backdrop")?.addEventListener("click",fe),q("reset-modal",fe)}var me=!1;function Wt(){var e=document.getElementById("rematch-modal");e&&e.classList.remove("hidden")}function ye(){var e=document.getElementById("rematch-modal");e&&e.classList.add("hidden")}async function zt(){if(!me){me=!0,Ae(W);var e=document.getElementById("rematch-game"),t=e?e.textContent:"";if(e&&(e.disabled=!0,e.textContent="\u23F3"),ye(),E()){N({type:"admin",action:"rematch_game"}),me=!1;return}try{var n=await BeatifyAuth.fetch("/beatify/api/rematch-game",{method:"POST"});if(n.ok){var i=await n.json();await V()}else{var s=await n.json();alert(s.message||"Failed to start rematch")}}catch(l){console.error("Rematch failed:",l),alert("Failed to start rematch")}finally{me=!1,e&&(e.disabled=!1,e.textContent=t)}}}function Gt(){var e=document.getElementById("rematch-confirm-btn"),t=document.getElementById("rematch-cancel-btn"),n=document.querySelector("#rematch-modal .modal-backdrop");e?.addEventListener("click",zt),t?.addEventListener("click",ye),n?.addEventListener("click",ye),q("rematch-modal",ye)}function jt(){window.print()}function L(e){alert(e)}function nt(){if(a.isPlaying&&a.adminPlayerName){L(BeatifyI18n.t("admin.alreadyJoined")||"Already joined as "+a.adminPlayerName);return}if(!E()){var e=null;try{e=sessionStorage.getItem("beatify_admin_name")}catch{}if(e&&a.currentGame&&a.currentGame.game_id){window.location.href="/beatify/play?game="+encodeURIComponent(a.currentGame.game_id);return}}let t=document.getElementById("admin-join-modal");t&&(t.classList.remove("hidden"),document.getElementById("admin-name-input")?.focus())}function pe(){let e=document.getElementById("admin-join-modal");e&&e.classList.add("hidden");let t=document.getElementById("admin-name-input"),n=document.getElementById("admin-join-btn"),i=document.getElementById("admin-name-error");t&&(t.value=""),n&&(n.disabled=!0,n.textContent=BeatifyI18n.t("admin.join")),i&&i.classList.add("hidden")}function Ot(){let e=document.getElementById("admin-cancel-btn"),t=document.getElementById("admin-join-btn"),n=document.getElementById("admin-name-input"),i=document.querySelector("#admin-join-modal .modal-backdrop");e?.addEventListener("click",pe),i?.addEventListener("click",pe),n?.addEventListener("input",function(){let s=this.value.trim();t.disabled=!s||s.length>20}),n?.addEventListener("keypress",function(s){s.key==="Enter"&&!t.disabled&&at()}),t?.addEventListener("click",at),q("admin-join-modal",pe),q("end-game-modal",ve)}function at(){let e=document.getElementById("admin-name-input"),t=document.getElementById("admin-join-btn"),n=e?.value.trim();if(!n)return;t.disabled=!0,t.textContent=BeatifyI18n.t("game.joining");let i=document.body.classList.contains("home-mode"),s=E();if(i){let l=async()=>{try{sessionStorage.setItem("beatify_admin_name",n),sessionStorage.setItem("beatify_is_admin","true"),a.adminPlayerName=n;let d=await BeatifyAuth.ensureAuthenticated();N({type:"join",name:n,is_admin:!0,ha_token:d}),pe()}catch(d){console.error("Admin join (home-mode) failed:",d),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join")}};if(s)l();else{M(),t.textContent=BeatifyI18n.t("admin.connecting")||"Connecting\u2026";let d=document.getElementById("admin-name-error");d&&(d.classList.add("hidden"),d.textContent="");let o=Date.now(),r=setInterval(()=>{E()?(clearInterval(r),l()):Date.now()-o>2e4&&(clearInterval(r),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join")||"Join",d?(d.textContent=BeatifyI18n.t("admin.home.wsReconnecting")||"Reconnecting to game server \u2014 please try again.",d.classList.remove("hidden")):L(BeatifyI18n.t("admin.home.wsReconnecting")||"Reconnecting to game server \u2014 please try again."))},100)}return}try{sessionStorage.setItem("beatify_admin_name",n),sessionStorage.setItem("beatify_is_admin","true");let l=a.currentGame?.game_id;l?window.location.href="/beatify/play?game="+encodeURIComponent(l):(L("No active game found"),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join"))}catch(l){console.error("Admin join failed:",l),t.disabled=!1,t.textContent=BeatifyI18n.t("admin.join")}}function Ut(e){var t=document.getElementById("lobby-players"),n=document.getElementById("lobby-player-count"),i=document.getElementById("admin-players-summary"),s=document.getElementById("lobby-players-empty");if(e=e||[],window.BeatifyHome&&typeof window.BeatifyHome.renderPlayers=="function"&&window.BeatifyHome.renderPlayers(e),!!t){if(n&&(n.textContent=e.length),i&&(i.textContent=e.length),e.length===0){t.innerHTML="",s&&s.classList.remove("hidden");var r=document.getElementById("start-gameplay-btn");r&&r.classList.add("hidden"),a.previousLobbyPlayers=[];return}s&&s.classList.add("hidden");var l=e.slice().sort(function(c,m){return c.connected!==m.connected?c.connected?-1:1:0}),d=a.previousLobbyPlayers.map(function(c){return c.name}),o=l.filter(function(c){return d.indexOf(c.name)===-1}).map(function(c){return c.name});t.innerHTML=l.map(function(c){var m=o.indexOf(c.name)!==-1,u=c.connected===!1,f=c.is_admin===!0,v=u&&!f,h=["player-card",m?"is-new":"",u?"player-card--disconnected":""].filter(Boolean).join(" "),y=f?'\u{1F451}':"",b=u?''+U.t("lobby.away","away")+"":"",w=v?'':"";return'
'+U.escapeHtml(c.name)+y+""+b+w+"
"}).join(""),t.querySelectorAll(".kick-player-btn").forEach(function(c){c.addEventListener("click",function(m){m.stopPropagation(),Ft(c.dataset.player)})}),setTimeout(function(){for(var c=t.querySelectorAll(".is-new"),m=0;m{e&&(e.classList.remove("hidden"),n?.focus())}),document.getElementById("request-cancel-btn")?.addEventListener("click",()=>{l()}),e?.querySelector(".modal-backdrop")?.addEventListener("click",()=>{l()}),q("request-modal",l),n?.addEventListener("input",()=>{let d=n.value.trim(),o=window.PlaylistRequests?.isValidSpotifyUrl(d);d&&!o?(n.classList.add("input-error"),i?.classList.remove("hidden")):(n.classList.remove("input-error"),i?.classList.add("hidden")),s&&(s.disabled=!o)}),s?.addEventListener("click",async()=>{let d=n?.value.trim();if(!(!d||!window.PlaylistRequests?.isValidSpotifyUrl(d))){s.classList.add("btn--loading"),s.disabled=!0;try{let o=await window.PlaylistRequests.submitRequest(d);l();let r=document.getElementById("request-success-name");r&&(r.textContent=o.playlist_name),t?.classList.remove("hidden"),await rt()}catch(o){if(console.error("Failed to submit request:",o),n?.classList.add("input-error"),i){let r=null;if(o.code&&window.BeatifyI18n){let c="errors."+String(o.code).toUpperCase(),m=BeatifyI18n.t(c);m&&m!==c&&(r=m)}i.textContent=r||o.message||"Failed to submit request",i.classList.remove("hidden")}}finally{s.classList.remove("btn--loading");let o=window.PlaylistRequests?.isValidSpotifyUrl(n?.value.trim()||"");s.disabled=!o}}}),document.getElementById("request-success-close-btn")?.addEventListener("click",()=>{t?.classList.add("hidden")}),t?.querySelector(".modal-backdrop")?.addEventListener("click",()=>{t?.classList.add("hidden")});function l(){e?.classList.add("hidden"),n&&(n.value="",n.classList.remove("input-error")),i?.classList.add("hidden"),s&&(s.disabled=!0,s.classList.remove("btn--loading"))}}async function Qt(){await rt()}async function rt(){if(!window.PlaylistRequests){document.getElementById("my-requests")?.classList.add("hidden");return}let e=await window.PlaylistRequests.getRequestsForDisplayAsync(),t=document.getElementById("my-requests"),n=document.getElementById("my-requests-list"),i=document.getElementById("my-requests-empty"),s=document.getElementById("my-requests-summary");!t||!n||(a.currentView==="setup"&&t.classList.remove("hidden"),s&&(s.textContent=e.length.toString()),e.length===0?(n.innerHTML="",i?.classList.remove("hidden")):(i?.classList.add("hidden"),n.innerHTML=e.map(l=>_e(l)).join("")))}(function(){let t=document.getElementById("pwa-install-btn"),n=document.getElementById("pwa-ios-hint"),i=document.getElementById("pwa-ios-hint-close");if(!t||window.matchMedia("(display-mode: standalone)").matches||window.navigator.standalone===!0)return;let s=null;window.addEventListener("beforeinstallprompt",o=>{o.preventDefault(),s=o,t.classList.remove("hidden")}),window.addEventListener("appinstalled",()=>{t.classList.add("hidden"),s=null,n&&n.classList.add("hidden")});let l=/iphone|ipad|ipod/i.test(navigator.userAgent),d=/safari/i.test(navigator.userAgent)&&!/chrome|crios|fxios/i.test(navigator.userAgent);l&&d&&t.classList.remove("hidden"),t.addEventListener("click",async()=>{if(s){s.prompt();let{outcome:o}=await s.userChoice;xe("[PWA] Install outcome:",o),s=null,o==="accepted"&&t.classList.add("hidden")}else l&&n&&n.classList.remove("hidden")}),i&&n&&i.addEventListener("click",()=>{n.classList.add("hidden")})})();function lt(e){if(a.currentGame=e,e.players&&!a.isPlaying){var t=e.players.find(function(i){return i.is_admin});if(t){a.isPlaying=!0,a.adminPlayerName=a.adminPlayerName||t.name;try{sessionStorage.setItem("beatify_admin_name",t.name)}catch{}}}Yt(),["LOBBY","PLAYING","REVEAL","PAUSED"].includes(e.phase)?W():st();var n=["setup-container","admin-playing-section","admin-reveal-section","admin-end-section","admin-control-bar"];switch(n.forEach(function(i){var s=document.getElementById(i);s&&s.classList.add("hidden")}),e.phase!=="REVEAL"&&dt(),kt.forEach(function(i){var s=document.getElementById(i);s&&s.classList.add("hidden")}),document.getElementById("start-game")?.classList.add("hidden"),document.getElementById("playlist-validation-msg")?.classList.add("hidden"),document.getElementById("media-player-validation-msg")?.classList.add("hidden"),e.phase&&e.phase!=="LOBBY"&&window.BeatifyHome&&window.BeatifyHome.exit(),e.phase==="LOBBY"&&window.BeatifyHome&&!document.body.classList.contains("home-mode")&&window.BeatifyHome.enter(),e.phase){case"LOBBY":Te(e);break;case"PLAYING":if(a.adminPlayerName&&a.adminSessionId&&a.currentGame&&a.currentGame.game_id){Re();return}Jt(e);break;case"REVEAL":Xt(e);break;case"END":nn(e);break;case"PAUSED":sn(e);break;default:be()}}function Yt(){var e=["admin-playing-banner","admin-reveal-playing-banner"],t=["admin-playing-name","admin-reveal-playing-name"];e.forEach(function(n,i){var s=document.getElementById(n);if(s)if(a.isPlaying&&a.adminPlayerName){var l=document.getElementById(t[i]);l&&(l.textContent=a.adminPlayerName),s.classList.remove("hidden")}else s.classList.add("hidden")})}function Re(){var e=a.currentGame&&a.currentGame.game_id;if(e&&a.adminPlayerName){try{sessionStorage.setItem("beatify_admin_name",a.adminPlayerName),sessionStorage.setItem("beatify_is_admin","true"),a.adminSessionId&&sessionStorage.setItem("beatify_session",a.adminSessionId)}catch{}var t="/beatify/play?game="+encodeURIComponent(e);a.adminSessionId&&(t+="&session="+encodeURIComponent(a.adminSessionId)),window.location.href=t}}document.getElementById("switch-to-player-view")?.addEventListener("click",Re);document.getElementById("switch-to-player-view-reveal")?.addEventListener("click",Re);function Jt(e){var t=document.getElementById("admin-playing-section");if(t){t.classList.remove("hidden"),ct();var n=document.getElementById("admin-control-bar");n&&n.classList.remove("hidden");var i=document.getElementById("admin-current-round"),s=document.getElementById("admin-total-rounds");i&&(i.textContent=e.round||"?"),s&&(s.textContent=e.total_rounds||"?");var l=document.getElementById("admin-game-difficulty-badge");l&&e.difficulty&&(l.textContent=e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1));var d=document.getElementById("admin-album-art");d&&e.song&&e.song.album_art&&(d.src=e.song.album_art);var o=document.getElementById("admin-song-year"),r=document.getElementById("admin-song-funfact"),c=!1;try{c=!!sessionStorage.getItem("beatify_admin_name")}catch{}var m=a.isPlaying||c||(e.players||[]).some(function(w){return w.is_admin});if(e.admin_song&&!m){if(o&&(e.admin_song.year?(o.textContent="\u{1F4C5} "+e.admin_song.year,o.classList.remove("hidden")):o.classList.add("hidden")),r){var u=BeatifyI18n.getLanguage(),f=u!=="en"&&e.admin_song["fun_fact_"+u]?e.admin_song["fun_fact_"+u]:e.admin_song.fun_fact;f?(r.textContent="\u{1F4A1} "+f,r.classList.remove("hidden")):r.classList.add("hidden")}}else o&&o.classList.add("hidden"),r&&r.classList.add("hidden");Kt(e.deadline),Ve(e.players);var v=document.getElementById("admin-last-round");v&&v.classList.toggle("hidden",!e.last_round);var h=document.getElementById("admin-intro-badge");h&&h.classList.toggle("hidden",!e.is_intro_round);var y=document.getElementById("admin-closest-wins-badge");y&&y.classList.toggle("hidden",!e.closest_wins_mode);var b=document.getElementById("admin-intro-splash");b&&b.classList.toggle("hidden",!e.intro_splash_pending),de(e.leaderboard)}}function Kt(e){_&&clearInterval(_);var t=document.getElementById("admin-timer");if(!t||!e)return;function n(){var i=Date.now(),s=Math.max(0,Math.ceil((e-i)/1e3));t.textContent=s,t.classList.toggle("timer--warning",s<=10),t.classList.toggle("timer--critical",s<=5),s<=0&&(clearInterval(_),_=null)}n(),_=setInterval(n,1e3)}function Xt(e){var t=document.getElementById("admin-reveal-section");if(t){t.classList.remove("hidden"),ct();var n=document.getElementById("admin-reveal-idle-halt");n&&n.classList.toggle("hidden",!e.idle_halt);var i=document.getElementById("admin-control-bar");i&&i.classList.remove("hidden"),tn(e);var s=document.getElementById("admin-reveal-emotion");if(s&&e.players){var l=e.players||[],d=l.filter(function(P){return P.years_off===0&&!P.missed_round}).length,o=0,r=l.filter(function(P){return!P.missed_round&&P.years_off!=null});r.length>0&&(o=Math.round(r.reduce(function(P,mt){return P+(mt.years_off||0)},0)/r.length));var c="",m="reveal-emotion--wrong";d>0?(c="\u{1F3AF} "+d+"x "+(BeatifyI18n.t("reveal.exact")||"Exact!"),m="reveal-emotion--exact"):o<=3?(c="\u{1F525} "+(BeatifyI18n.t("reveal.soClose")||"So close!"),m="reveal-emotion--close"):o<=10?(c="\u{1F440} \xD8 "+(BeatifyI18n.t("reveal.yearsOff",{years:o})||o+" years off"),m="reveal-emotion--wrong"):(c="\u{1F605} \xD8 "+(BeatifyI18n.t("reveal.yearsOff",{years:o})||o+" years off"),m="reveal-emotion--wrong"),s.className="reveal-emotion-inline "+m,s.innerHTML=''+c+"",s.classList.remove("hidden")}var u=document.getElementById("admin-reveal-round"),f=document.getElementById("admin-reveal-total");if(u&&(u.textContent=e.round||"?"),f&&(f.textContent=e.total_rounds||"?"),e.song){var v=document.getElementById("admin-reveal-song-title"),h=document.getElementById("admin-reveal-song-artist"),y=document.getElementById("admin-reveal-correct-year"),b=document.getElementById("admin-reveal-album-art");v&&(v.textContent=e.song.title||""),h&&(h.textContent=e.song.artist||""),y&&(y.textContent=e.song.year||""),b&&e.song.album_art&&(b.src=e.song.album_art)}var w=document.getElementById("admin-reveal-difficulty-badge");w&&e.difficulty&&(w.textContent=e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1));var T=document.getElementById("admin-fun-fact-container"),X=document.getElementById("admin-fun-fact-text");if(T&&e.song){var Z=BeatifyI18n.getLanguage(),qe=Z!=="en"&&e.song["fun_fact_"+Z]?e.song["fun_fact_"+Z]:e.song.fun_fact;qe?(X.textContent=qe,T.classList.remove("hidden")):T.classList.add("hidden")}var Ee=document.getElementById("admin-artist-reveal-section");Ee&&(e.artist_challenge&&e.artist_challenge.correct_answer?(document.getElementById("admin-artist-reveal-name").textContent=e.artist_challenge.correct_answer,Ee.classList.remove("hidden")):Ee.classList.add("hidden"));var Be=document.getElementById("admin-movie-reveal-section");Be&&(e.movie_challenge&&e.movie_challenge.correct_answer?(document.getElementById("admin-movie-reveal-name").textContent=e.movie_challenge.correct_answer,Be.classList.remove("hidden")):Be.classList.add("hidden"));var ee=document.getElementById("admin-reveal-personal");if(ee)if(a.isPlaying&&a.adminPlayerName&&e.players){var Q=e.players.find(function(P){return P.is_admin});if(Q){var te=document.getElementById("admin-reveal-my-guess"),ne=document.getElementById("admin-reveal-my-accuracy"),He=document.getElementById("admin-reveal-my-score");if(Q.missed_round)te&&(te.textContent="\u2014"),ne&&(ne.textContent=BeatifyI18n.t("reveal.noGuessShort")||"Missed");else{var Le=Q.years_off||0;te&&(te.textContent=Q.guess||"\u2014"),ne&&(ne.textContent=Le===0?BeatifyI18n.t("reveal.exact")||"Exact!":BeatifyI18n.t("reveal.shortOff",{years:Le})||Le+" off")}He&&(He.textContent="+"+(Q.round_score||0)),ee.classList.remove("hidden")}else ee.classList.add("hidden")}else ee.classList.add("hidden");Zt(e),en(e),Qe(e.players,e.closest_wins_mode,e.song?e.song.year:null),de(e.leaderboard)}}function Zt(e){var t=document.getElementById("admin-control-bar");if(t){var n=document.getElementById("admin-sudden-death-toggle");if(!e){n&&n.remove();return}var i=e.players||[],s=i.filter(function(r){return!r.eliminated}).length,l=!!e.sudden_death_mode,d=BeatifyI18n.t&&BeatifyI18n.t("admin.suddenDeathLive")||"Sudden Death",o=n;o||(o=document.createElement("button"),o.id="admin-sudden-death-toggle",o.type="button",o.className="sd-live-toggle",o.addEventListener("click",function(){if(!o.disabled){var r=!o.classList.contains("is-on");o.disabled=!0,BeatifyAuth.fetch("/beatify/api/sudden-death",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:r})}).catch(function(c){console.warn("[Admin] Sudden Death toggle failed:",c),o.disabled=!1})}}),t.appendChild(o)),o.innerHTML='\u{1F480} '+x(d)+'',o.classList.toggle("is-on",l),o.disabled=s<3}}function en(e){var t=document.getElementById("admin-reveal-section");if(t){var n=document.getElementById("admin-reveal-arc-chip-row");if(!e||!e.sudden_death_mode){n&&n.remove();return}if(!n){n=document.createElement("div"),n.id="admin-reveal-arc-chip-row",n.className="arc-chip-row";var i=t.querySelector(".reveal-header-compact");i&&i.parentNode?i.parentNode.insertBefore(n,i.nextSibling):t.appendChild(n)}var s="",l=e.eliminated_this_round||[];l.length>0&&(s+='\u{1F480} Eliminated: '+x(l.join(", "))+"");var d=e.players||[],o=d.filter(function(c){return!c.eliminated}).length;if(o===2){var r=BeatifyI18n.t&&BeatifyI18n.t("game.finalShowdown")||"FINAL \u2014 SUDDEN DEATH";s+='\u{1F525} '+x(r)+""}n.innerHTML=s,n.classList.toggle("hidden",s==="")}}function dt(){C&&(clearInterval(C),C=null);var e=document.querySelector("#admin-skip-round .control-icon");e&&(e.classList.remove("is-countdown"),ge!==null&&(e.textContent=ge))}function tn(e){var t=document.querySelector("#admin-skip-round .control-icon");if(!t)return;var n=e.reveal_auto_advance||0,i=e.reveal_started_at;if(n<=0||e.idle_halt||!i){dt();return}ge===null&&(ge=t.textContent);var s=i+n*1e3;function l(){var d=s-Date.now(),o=Math.max(0,Math.ceil(d/1e3));t.textContent=String(o),t.classList.add("is-countdown"),o<=0&&C&&(clearInterval(C),C=null)}C&&clearInterval(C),l(),C=setInterval(l,1e3)}function nn(e){var t=document.getElementById("admin-end-section");if(t){if(t.classList.remove("hidden"),e.leaderboard)for(var n=1;n<=3;n++){var i=e.leaderboard.find(function(d){return d.rank===n}),s=document.getElementById("admin-podium-"+n+"-name"),l=document.getElementById("admin-podium-"+n+"-score");s&&(s.textContent=i?i.name:"---"),l&&(l.textContent=i?i.score:"0")}e.leaderboard&&de(e.leaderboard,"admin-end-leaderboard"),a.isPlaying=!1,a.adminPlayerName=null,_&&(clearInterval(_),_=null)}}function an(e){var t=document.getElementById("admin-pause-recovery");if(t){var n=e&&e.pause_reason?e.pause_reason:"",i=e&&e.last_error_detail?e.last_error_detail:"",s=e&&e.provider?e.provider:"",l=n==="media_player_error"||n==="no_songs_available";if(!l){t.classList.add("hidden");return}var d=document.getElementById("admin-pause-recovery-message");if(d){var o=Ye(s),r;if(n==="no_songs_available")r=BeatifyI18n.t("admin.pauseRecovery.noSongsAvailable")||"No playable songs left for this provider. Resume to retry, or end the game.";else if(o){var c=BeatifyI18n.t("admin.pauseRecovery.mediaPlayerError")||"Playback did not start \u2014 the speaker is not responding. This often means {provider} in Music Assistant needs re-authentication \u2014 open Settings \u2192 Music Assistant \u2192 {provider} \u2192 Reconnect, then click Resume.";r=c.replace(/\{provider\}/g,o)}else r=BeatifyI18n.t("admin.pauseRecovery.mediaPlayerErrorGeneric")||"Playback did not start \u2014 the speaker is not responding. This often means your music provider in Music Assistant needs re-authentication \u2014 open Settings \u2192 Music Assistant \u2192 your provider \u2192 Reconnect, then click Resume.";d.textContent=r}var m=document.getElementById("admin-pause-recovery-detail");m&&(i?(m.textContent=i,m.classList.remove("hidden")):(m.textContent="",m.classList.add("hidden"))),t.classList.remove("hidden")}}function ct(){var e=document.getElementById("admin-pause-recovery");e&&e.classList.add("hidden")}function sn(e){var t=document.getElementById("admin-playing-section");if(t){t.classList.remove("hidden");var n=document.getElementById("admin-control-bar");n&&n.classList.remove("hidden");var i=document.getElementById("admin-timer");i&&(i.textContent="\u23F8 "+(BeatifyI18n.t("game.paused")||"Paused")),_&&(clearInterval(_),_=null),an(e)}}function it(){$({type:"admin",action:"next_round"})}function on(){$({type:"admin",action:"stop_song"})}function rn(){$({type:"admin",action:"set_volume",direction:"up"})}function ln(){$({type:"admin",action:"set_volume",direction:"down"})}function dn(){E()&&N({type:"admin",action:"dismiss_game"}),a.cachedQRUrl=null,a.isPlaying=!1,a.adminPlayerName=null;try{localStorage.removeItem(R),localStorage.removeItem(I),localStorage.removeItem("beatify_wizard_state")}catch{}window.BeatifyHome&&window.BeatifyHome.exit(),window.BeatifyWizard&&typeof window.BeatifyWizard.show=="function"?window.BeatifyWizard.show(1):be()}"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/sw.js",{scope:"/beatify/"}).then(function(e){xe("[Admin] SW registered:",e.scope)}).catch(function(e){console.warn("[Admin] SW registration failed:",e)})}); diff --git a/custom_components/beatify/www/js/admin/sections/render-helpers.js b/custom_components/beatify/www/js/admin/sections/render-helpers.js index 5aef3e85..51bafe73 100644 --- a/custom_components/beatify/www/js/admin/sections/render-helpers.js +++ b/custom_components/beatify/www/js/admin/sections/render-helpers.js @@ -53,16 +53,22 @@ export function renderAdminSubmissionDots(players) { container.innerHTML = players.map(function(p) { var initials = (p.name || '?').split(/\s+/).map(function(w) { return w[0]; }).join('').substring(0, 2).toUpperCase(); + // Issue #827: eliminated players are out of the round — never count as + // submitted, render dimmed with a skull instead of submit state. var classes = [ 'player-indicator', - p.submitted ? 'is-submitted' : '', + (!p.eliminated && p.submitted) ? 'is-submitted' : '', + p.eliminated ? 'is-eliminated' : '', p.connected === false ? 'player-indicator--disconnected' : '' ].filter(Boolean).join(' '); var badges = ''; - if (p.steal_used) badges += '🥷'; - if (p.bet) badges += '🎲'; + if (!p.eliminated && p.steal_used) badges += '🥷'; + if (!p.eliminated && p.bet) badges += '🎲'; + var avatarInner = p.eliminated + ? '💀' + : '' + escapeHtml(initials) + ''; return '
' + badges + - '
' + escapeHtml(initials) + '
' + + '
' + avatarInner + '
' + '' + escapeHtml(p.name) + '
'; }).join(''); } @@ -80,6 +86,9 @@ export function renderAdminLeaderboard(leaderboard, containerId) { leaderboard.forEach(function(entry) { var rankClass = entry.rank <= 3 ? 'is-top-' + entry.rank : ''; var disconnectedClass = entry.connected === false ? 'leaderboard-entry--disconnected' : ''; + // Issue #827: dim eliminated players + skull-prefix their name. + var eliminatedClass = entry.eliminated ? 'is-eliminated' : ''; + var skullPrefix = entry.eliminated ? '💀 ' : ''; var awayBadge = entry.connected === false ? '(away)' : ''; var streakIndicator = ''; if (entry.streak >= 2) { @@ -90,9 +99,9 @@ export function renderAdminLeaderboard(leaderboard, containerId) { if (entry.rank_change > 0) changeIndicator = '▲' + entry.rank_change + ''; else if (entry.rank_change < 0) changeIndicator = '▼' + Math.abs(entry.rank_change) + ''; - html += '
' + + html += '
' + '#' + entry.rank + '' + - '' + escapeHtml(entry.name) + awayBadge + '' + + '' + skullPrefix + escapeHtml(entry.name) + awayBadge + '' + '' + '' + entry.score + '' + '
'; diff --git a/custom_components/beatify/www/js/dashboard.js b/custom_components/beatify/www/js/dashboard.js index 308ad8d8..3757e490 100644 --- a/custom_components/beatify/www/js/dashboard.js +++ b/custom_components/beatify/www/js/dashboard.js @@ -49,6 +49,11 @@ var previousPlayers = []; var countdownInterval = null; var lastQRCodeUrl = null; + // Issue #827: dedup key for the full-bleed "OUT" takeover so it only fires + // once per elimination (re-renders / re-broadcasts of the same REVEAL must + // not re-trigger it). Format: ":". + var sdLastOutKey = null; + var sdOutTimer = null; // Utility functions from BeatifyUtils // waitForI18n, t, getLocalizedSongField, escapeHtml moved to BeatifyUtils @@ -664,6 +669,9 @@ // Update round statistics (Story 16.4) renderRoundStats(data, players); + + // Issue #827: Sudden-Death FINAL banner (2 players left). + renderSuddenDeathFinalBanner(data, 'sd-final-banner-playing'); } /** @@ -792,6 +800,11 @@ var disconnectedClass = entry.connected === false ? 'leaderboard-entry--disconnected' : ''; var awayBadge = entry.connected === false ? '(away)' : ''; + // Issue #827: Sudden-Death — eliminated players render dimmed with a + // 💀 prefix. Reuses .leaderboard-entry--disconnected for the dim. + var eliminatedClass = entry.eliminated ? 'leaderboard-entry--disconnected' : ''; + var skullPrefix = entry.eliminated ? '💀 ' : ''; + // Rank change indicator (AC 10.4.4 - with arrows) var changeIndicator = ''; if (entry.rank_change > 0) { @@ -820,9 +833,9 @@ submittedIndicator = ''; } - html += '
' + + html += '
' + '#' + entry.rank + '' + - '' + utils.escapeHtml(entry.name) + awayBadge + betBadge + '' + + '' + skullPrefix + utils.escapeHtml(entry.name) + awayBadge + betBadge + '' + ''}}function ha(){pe(),k.currentIdxxa){console.warn("[Beatify] No server activity for "+xa+"ms \u2014 socket appears dead, forcing reconnect");try{s.ws.close()}catch{}return}try{s.ws.send(JSON.stringify({type:"ping"}))}catch{}}},Ti)}function rn(){dt&&(clearInterval(dt),dt=null)}function Ce(){var e=Je();if(e&&!(s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN))){var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,Qe(),Ma(),Pa(),s.ws.send(JSON.stringify({type:"reconnect",session_id:e}))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);Ha(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(rn(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Ye&&(s.isReconnecting=!1,Qe(),an())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function ge(e){var t=s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN);if(!(t&&s.playerName===e)){if(t){if(!s.isAdmin)try{s.ws.send(JSON.stringify({type:"leave"}))}catch{}s.intentionalLeave=!0;try{s.ws.close()}catch{}s.ws=null,vt()}s.playerName=e,Ta(e);var n=window.location.protocol==="https:"?"wss:":"ws:",a=n+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(a),s.ws.onopen=async function(){s.reconnectAttempts=0,s.isReconnecting=!1,Qe(),Ma(),Pa();var i={type:"join",name:e};s.isAdmin&&(i.is_admin=!0,i.ha_token=await BeatifyAuth.ensureAuthenticated()),s.ws.send(JSON.stringify(i))},s.ws.onmessage=function(i){try{var r=JSON.parse(i.data);Ha(r)}catch(o){console.error("Failed to parse WebSocket message:",o)}},s.ws.onclose=function(){if(rn(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Ye&&(s.isReconnecting=!1,Qe(),an())},s.ws.onerror=function(i){console.error("WebSocket error:",i)}}}s.connectWithSession=Ce;s.connectWebSocket=ge;function Ha(e){if(s.lastServerActivity=Date.now(),e.type!=="pong"){if(e.type==="game_starting"){var t=document.getElementById("loading-view"),n=document.getElementById("lobby-view"),a=document.getElementById("join-view"),i=n&&!n.classList.contains("hidden"),r=t&&!t.classList.contains("hidden"),o=a&&!a.classList.contains("hidden");(i||r||!o)&&_("starting-view");return}var l=document.getElementById("join-btn"),u=document.getElementById("name-input");if(e.type==="state"){var d=e.players||[],f=d.find(function(E){return E.name===s.playerName});if(f&&(s.isAdmin=f.is_admin===!0),e.language&&(_i(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),Bt(d),e.difficulty&&et(e.difficulty,e.title_artist_mode),e.phase==="REVEAL"&&Xt(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&st(e.phase)})),e.join_url&&xn(e.join_url),e.phase==="LOBBY"){fe(),je(),qe(),Ue(),s.currentRoundNumber=0,ue("warmup");var c=document.getElementById("start-game-btn");if(c&&(c.disabled=!1,c.innerHTML=''+re.t("lobby.startGame")+""),s.lastPlayerCount=d.length,s.lastDifficulty=e.difficulty?re.t?re.t("game.difficulty"+e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1)):e.difficulty:"",!lt()&&La(f))Ia();else{var v=document.getElementById("ready-view"),g=v&&!v.classList.contains("hidden");!g&&!lt()&&_("lobby-view"),g&&nn(d,s.lastDifficulty)}Bt(d),e.difficulty&&et(e.difficulty,e.title_artist_mode),An(d)}else if(e.phase==="PLAYING"){lt()&&Sa(),X(),je(),$e();var y=e.round||1;y!==s.currentRoundNumber&&(s.currentRoundNumber=y,Yn()),Yt(),ue("party"),_("game-view"),He(),Fn(e),e.intro_splash_pending?sa(s.isAdmin):oa(),e.difficulty&&et(e.difficulty,e.title_artist_mode),e.deadline&&_t(e.deadline),zn(),Un(),zt(),st("PLAYING"),Ue()}else e.phase==="REVEAL"?(fe(),e.early_reveal&&Mn(),ue("party"),_("reveal-view"),Xt(e),jn(),zt(),st("REVEAL"),s.hasReactedThisPhase=!1,Kn()):e.phase==="PAUSED"?(fe(),je(),qe(),Ue(),ue("warmup"),_("paused-view"),ga(e)):e.phase==="END"&&(fe(),je(),qe(),Ue(),Ln(),s.currentRoundNumber=0,ue("warmup"),_("end-view"),pa(e),ze())}else if(e.type==="join_ack"){$e(),e.session_id&&Aa(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,Ta(e.name),Nn(e.name)):(vt(),ze(),s.playerName=null,_("join-view"));else if(e.type==="submit_ack")rt();else if(e.type==="metadata_update")qn(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){qt(e);return}if(e.code==="GAME_ENDED"){_("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,qe(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,Qe(),s.playerName=null,an(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){if(s.isReconnecting){console.warn("SESSION_NOT_FOUND during reconnect, will retry with session");return}vt(),s.intentionalLeave=!0,s.ws&&s.ws.close(),_("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Jt(),console.warn("[Beatify] Stop song failed: No song playing");return}if(e.code==="INVALID_ACTION"){console.warn("[Beatify] Action rejected:",e.message),qt(e);return}_("join-view"),Ri(e.message),l&&(l.disabled=!1,l.textContent=re.t("join.joinButton")),u&&u.focus(),s.playerName=null,ze()}else if(e.type==="song_stopped")aa();else if(e.type==="volume_changed")ra(e.level);else if(e.type==="game_ended")Mi();else if(e.type==="rematch_started"){Y("[Player] Rematch started - transitioning to lobby"),Se.clear(),X(),_("lobby-view");var h=document.getElementById("player-rematch-btn");h&&(h.disabled=!1,h.textContent="\u{1F501}");var b=Je();b&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:b}))):(s.reconnectAttempts=0,Ce()))}else e.type==="left"?Ni():e.type==="steal_targets"?Xn(e):e.type==="steal_ack"?Qn(e):e.type==="artist_guess_ack"?Tt(e):e.type==="movie_guess_ack"?Ht(e):e.type==="title_artist_guess_ack"?Jn(e):e.type==="player_reaction"&&$n(e.player_name,e.emoji)}}function Ni(){ze(),vt(),s.playerName=null,s.isAdmin=!1,_("join-view")}function Mi(){var e=s.isAdmin;ze();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}mn(),wn(),Se.clear(),X(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='

Thanks for playing!

Scan the QR code again to join the next game.

',n.classList.remove("hidden")),_("end-view")}}function Ri(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function sn(e){var t=(e||"").trim();return t?t.length>Si?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function _a(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=sn(e.value);a.valid&&(t.disabled=!0,t.textContent=re.t("game.joining"),n&&n.classList.add("hidden"),ge(a.name))}}function Oi(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=sn(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",_a),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&_a()}))}function Pi(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,_("loading-view"),ge(s.playerName)):mt()})}async function ka(){try{await BeatifyAuth.init({requireAuth:!1})}catch{}var e=Ee.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await re.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=ki();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");i&&(i.href=window.location.origin+"/beatify/dashboard"),Oi(),Ba(),_n(),Cn(),Tn(),ua(),fa(),va(),ia(),na(),Pi(),pn(),gn(),yn(),Zn();var r=new URLSearchParams(window.location.search),o=r.get("session"),l=null;try{l=sessionStorage.getItem("beatify_session")}catch{}var u=o||l;if(u){Aa(u);try{sessionStorage.removeItem("beatify_session")}catch{}}if(Ai()&&s.playerName){Je()?Ce():ge(s.playerName);return}var d=xi();if(d&&s.gameId){Y("[Beatify] Auto-reconnecting as:",d),ge(d);return}if(d){var f=document.getElementById("name-input"),c=document.getElementById("join-btn");if(f&&(f.value=d,c)){var v=sn(d);c.disabled=!v.valid}}}mt();document.getElementById("refresh-btn")?.addEventListener("click",function(){_("loading-view"),mt()});document.getElementById("retry-btn")?.addEventListener("click",function(){_("loading-view"),mt()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ka):ka();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/sw.js",{scope:"/beatify/"}).then(function(e){Y("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){s.playerName&&$e();var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(Y("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,Ce())}}); +var Et=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},Ga=document.getElementById("loading-view"),Va=document.getElementById("starting-view"),Wa=document.getElementById("not-found-view"),Fa=document.getElementById("ended-view"),qa=document.getElementById("in-progress-view"),Ua=document.getElementById("join-view"),ja=document.getElementById("tour-view"),za=document.getElementById("ready-view"),Ya=document.getElementById("lobby-view"),Ja=document.getElementById("game-view"),Qa=document.getElementById("reveal-view"),Xa=document.getElementById("paused-view"),Ka=document.getElementById("end-view"),Za=document.getElementById("connection-lost-view"),$a=[Ga,Va,Wa,Fa,qa,Ua,ja,za,Ya,Ja,Qa,Xa,Ka,Za];function k(e){Et.showView($a,e);var t=e==="tour-view"||e==="ready-view",n=e==="game-view"||e==="reveal-view";document.body&&(document.body.classList.toggle("is-learning-screen",t),document.body.classList.toggle("is-ingame",n)),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&ue("calm"),e==="join-view"&&setTimeout(function(){var a=document.getElementById("name-input");a&&a.focus()},100)}function Ie(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),c=document.getElementById("confirm-modal-yes"),d=document.getElementById("confirm-modal-no");if(!r||!o||!l||!c||!d){i(confirm(t||e));return}o.textContent=e,l.textContent=t,c.textContent=n||Et.t("common.confirm")||"Confirm",d.textContent=a||Et.t("common.cancel")||"Cancel",r.classList.remove("hidden");function u(){r.classList.add("hidden"),c.removeEventListener("click",f),d.removeEventListener("click",m),g.removeEventListener("click",m)}function f(){u(),i(!0)}function m(){u(),i(!1)}var g=r.querySelector(".modal-backdrop");c.addEventListener("click",f),d.addEventListener("click",m),g&&g.addEventListener("click",m)})}function p(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function er(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function tr(e){return 1-Math.pow(1-e,4)}function Lt(e,t,n,a,i){if(er()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=Ee.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||tr;var l=null,c=null,d=!1,u=n;function f(m){if(!d){l||(l=m);var g=m-l,y=Math.min(g/o,1),E=i(y),b=Math.round(t+(u-t)*E);e.textContent=b,y<1&&(c=requestAnimationFrame(f))}}return c=requestAnimationFrame(f),{cancel:function(){d=!0,c&&cancelAnimationFrame(c)},skipToEnd:function(){d=!0,c&&cancelAnimationFrame(c),e.textContent=u}}}var Q={players:{},leaderboard:[],initialized:!1};function fn(){return Q.initialized}function mn(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=Q.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function $e(e,t){Q.players={},e.forEach(function(n){Q.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(Q.leaderboard=t.map(function(n){return n.name})),Q.initialized=!0}var Ee=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),Se=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),Le={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},B={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function vn(e){e&&(B.observer&&B.listEl!==e&&(B.observer.disconnect(),B.observer=null),!B.observer&&(B.listEl=e,B.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!B.isLazyEnabled)){var a=B.fullData,i=B.visibleRange,r=Le.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);B.visibleRange.start=o,He()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+='
'),l+='
';for(var c=n.start;c
',r>0&&(l+='
'),e.innerHTML=l,e.scrollTop=o,B.observer){var d=e.querySelectorAll(".leaderboard-sentinel");d.forEach(function(u){B.observer.observe(u)})}}}function It(e){if(!e)return"";if(e.separator)return'
...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var c="";if(e.streak>=2){var d=e.streak>=5?"streak-indicator--hot":"";c='\u{1F525}'+e.streak+""}var u=e.connected===!1?"leaderboard-entry--disconnected":"",f=e.connected===!1?'(away)':"",m=e._displayScore!==void 0?e._displayScore:a;return'
#'+n+''+p(t)+f+''+m+"
"}function St(e,t){for(var n=Le,a=B.listEl&&B.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(c=Math.max(0,e.length-i-r),d=e.length):(c=Math.max(0,o-Math.floor(i/2)-r),d=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:c,end:d}}function pn(){B.observer&&(B.observer.disconnect(),B.observer=null),B.isLazyEnabled=!1,B.fullData=[]}function gn(){var e;function t(){clearTimeout(e),e=setTimeout(function(){B.isLazyEnabled&&B.fullData.length>0&&(B.visibleRange=St(B.fullData,s.playerName),He())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function yn(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function bn(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var hn={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},L={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function En(e){if(e){L.container=e;var t=!1;L.scrollHandler=function(){L.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){wt(),t=!1}),t=!0)};var n;L.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){L.isVirtual&&wt()},100)},e.addEventListener("scroll",L.scrollHandler,{passive:!0}),window.addEventListener("resize",L.resizeHandler)}}function wn(e,t){L.items=e,L.renderItem=t;var n=L.container;if(n){var a=n.scrollTop,i=L.isVirtual;e.length0&&(n.scrollTop=a,L.scrollTop=a)}}function nr(){var e=L.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",L.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",L.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",L.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function wt(){var e=hn,t=L.items,n=L.container,a=L.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=L.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,c=Math.max(0,Math.floor(r/o)-l),d=Math.min(t.length,Math.ceil((r+i)/o)+l);L.topSpacer&&(L.topSpacer.style.height=c*o+"px"),L.bottomSpacer&&(L.bottomSpacer.style.height=(t.length-d)*o+"px");for(var u="",f=c;f"u"){console.warn("[Confetti] Library not loaded");return}X();var t=Ee.getQualitySettings(),n=t.confettiParticles;if(n===0){un();return}var a=Ee.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function g(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(y,E){return y.connected!==E.connected?y.connected?-1:1:0}),c=Sn.map(function(y){return y.name}),d=l.filter(function(y){return c.indexOf(y.name)===-1}).map(function(y){return y.name});L.container||En(t);var u=["c1","c2","c3","c4"],f={},m=0;l.forEach(function(y){y.is_admin?f[y.name]="host":f[y.name]=u[m++%u.length]});var g=function(y){var E=d.indexOf(y.name)!==-1,b=y.name===s.playerName,h=y.is_admin===!0,w=y.connected===!1,x=f[y.name]||"c1",_=["player-tile","player-tile--"+x];E&&_.push("is-new"),w&&_.push("player-tile--disconnected");var S=(y.name||"?").trim(),G=(S.charAt(0)||"?").toUpperCase(),A="";h?A='':b&&(A='YOU');var M=w?'":"";return'
'+p(G)+''+p(S)+""+A+M+"
"};wn(l,g),setTimeout(function(){var y=L.isVirtual?L.contentWrapper:t;if(y)for(var E=y.querySelectorAll(".is-new"),b=0;b=1){K=e,On(Math.max(0,Math.ceil((e-Date.now())/1e3))),t&&t.classList.remove("timer-neon--catchup"),_e=null;return}var l=n+(e-n)*dr(o);K=l,On(Math.max(0,Math.ceil((l-Date.now())/1e3))),_e=requestAnimationFrame(i)}return _e=requestAnimationFrame(i),!0}var te=null,xt=null;function ur(e,t){if(!(!t||!e)){if(typeof IntersectionObserver>"u"){t.classList.remove("hidden"),t.classList.add("timer-float--visible");return}te&&xt===e||(te&&te.disconnect(),te=new IntersectionObserver(function(n){var a=n[0];a&&(a.isIntersecting?(t.classList.add("hidden"),t.classList.remove("timer-float--visible")):(t.classList.remove("hidden"),t.classList.add("timer-float--visible")))},{threshold:.1}),te.observe(e),xt=e)}}function fe(){Ge&&(clearInterval(Ge),Ge=null),Pn(),K=null;var e=document.getElementById("timer-neon");e&&e.classList.remove("timer-neon--catchup");var t=document.getElementById("timer-float");t&&(t.classList.add("hidden"),t.classList.remove("timer-float--visible","timer-float--warn")),te&&(te.disconnect(),te=null,xt=null)}var ae=window.BeatifyUtils||{},me=!1,ne=null,nt=null,fr=300,Hn=0;function Tt(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(u){return u.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(u,f){var m=document.createElement("button");m.className="artist-option-btn",m.dataset.artist=u,m.dataset.index=f,m.textContent=u,m.addEventListener("click",function(){mr(u)}),a.appendChild(m)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(u){u.classList.add("is-disabled"),u.classList.remove("is-loading","is-wrong");var f=e.correct_artist||nt;f&&u.dataset.artist===f&&u.classList.add("is-winner")}),e.winner===s.playerName){var c=e.bonus_points||5;i.textContent=(ae.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",c),i.className="artist-result is-winner"}else{var d=(ae.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=d,i.className="artist-result is-late"}i.classList.remove("hidden"),me=!0}else me||i.classList.add("hidden")}}function mr(e){var t=Date.now();if(!(t-Hn0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}Ot();var a=(Ve.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Pt(a,!0),xe=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),Ot(),Pt(Ve.t("movieChallenge.wrongGuess")||"Not quite...",!1),xe=!0;ke=null}function Ot(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Pt(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Gt(){xe=!1,ke=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function Vt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=Ve.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(c){var d=document.createElement("div");d.className="movie-reveal-winner-entry",c.name===t?d.classList.add("is-you"):d.classList.add("is-other"),d.textContent=c.name+" \u2014 +"+c.bonus+" ("+c.time+"s)",i.appendChild(d)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=Ve.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}var I=window.BeatifyUtils||{},gr=I.debug||function(){};function qn(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("closest-wins-badge");i&&(e.closest_wins_mode?i.classList.remove("hidden"):i.classList.add("hidden"));var r=document.getElementById("intro-badge"),o=document.getElementById("intro-splash");if(r)if(e.is_intro_round){r.classList.remove("hidden");var l=r.querySelector("[data-i18n]");e.intro_stopped?(r.classList.add("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=I.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=I.t("game.introRound")||"INTRO ROUND"),o&&!o._shown&&(o._shown=!0,o.classList.remove("hidden"),setTimeout(function(){o.classList.add("hidden")},2e3)))}else r.classList.add("hidden"),r.classList.remove("intro-badge--stopped"),o&&(o.classList.add("hidden"),o._shown=!1);var c=document.getElementById("album-cover"),d=document.getElementById("album-loading");if(c&&e.song){d&&d.classList.remove("hidden");var u=e.song.album_art||"/beatify/static/img/no-artwork.svg";c.onload=function(){d&&d.classList.add("hidden")},c.onerror=function(){c.src="/beatify/static/img/no-artwork.svg",d&&d.classList.add("hidden")},c.src=u}Er(e),qt(),br(e),wr(e.players),e.leaderboard&&Lr(e,"leaderboard-list"),Ar(e.players),e.artist_challenge!==void 0&&Tt(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Ht(e.movie_challenge,"PLAYING"),kr(e)}function Un(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}gr("[Metadata] Updated:",e.artist,"-",e.title)}}function yr(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function qt(){var e=document.getElementById("arc-chip-row");if(e){var t=["game-difficulty-badge","steal-indicator","closest-wins-badge","intro-badge","last-round-banner"],n=t.some(function(a){var i=document.getElementById(a);return i&&!i.classList.contains("hidden")});e.classList.toggle("hidden",!n)}}function br(e){var t=document.getElementById("no-bonus-filler");if(t){var n=!!(e&&e.artist_challenge&&e.artist_challenge.options),a=!!(e&&e.movie_challenge&&e.movie_challenge.options),i=!!(e&&e.title_artist_mode);t.classList.toggle("hidden",n||a||i)}}var Ce=!1;function hr(e){return!s.playerName||!e?null:e.find(function(t){return t.name===s.playerName})||null}function Er(e){var t=document.getElementById("eliminated-view");if(t){var n=!!(e&&e.sudden_death_mode),a=hr(e&&e.players),i=n&&!!(a&&a.eliminated);Ce=i;var r=[document.getElementById("year-selector-container"),document.getElementById("year-display-arc"),document.getElementById("bet-toggle"),document.getElementById("submit-btn"),document.getElementById("title-artist-container"),document.getElementById("submitted-banner")];if(i){r.forEach(function(f){f&&f.classList.add("hidden")}),t.classList.remove("hidden");var o=document.getElementById("album-cover"),l=document.getElementById("eliminated-album-cover");l&&o&&o.src&&(l.src=o.src);var c=document.getElementById("eliminated-sub");if(c){var d=a&&a.eliminated_round!=null?a.eliminated_round:e&&e.round||"";c.textContent=I.t("game.eliminatedRound",{round:d})||"Eliminated \xB7 Round "+d}}else{r.forEach(function(f){f&&f.classList.remove("hidden")}),t.classList.add("hidden");var u=document.getElementById("submitted-banner");u&&!D&&u.classList.add("hidden")}}}function wr(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players"),a=document.getElementById("arc-submission-count");if(!(!t||!n)){var i=e||[],r=i.filter(function(m){return!m.eliminated}),o=r.filter(function(m){return m.submitted}).length,l=r.length,c=o===l&&l>0;t.classList.toggle("all-submitted",c),a&&(l===0?a.textContent="":c?a.textContent=I.t("game.allSubmitted")||"All in":a.textContent=I.t("game.submittedCount",{count:o,total:l})||o+" of "+l+" submitted");var d=document.getElementById("submitted-banner"),u=document.getElementById("submitted-banner-text");if(d&&u&&!d.classList.contains("hidden")){var f=Math.max(0,l-o);f===0?u.textContent=I.t("game.lockedInAllSubmitted")||"Locked in \xB7 everyone submitted":u.textContent=I.t("game.lockedInWaitingCount",{count:f})||"Locked in \xB7 waiting for "+f+" more"}n.innerHTML=i.map(function(m){var g=yr(m.name),y=m.name===s.playerName,E=m.connected===!1,b=!!m.eliminated,h=["player-indicator",m.submitted&&!b?"is-submitted":"",y?"is-current-player":"",E?"player-indicator--disconnected":"",b?"is-eliminated":""].filter(Boolean).join(" "),w="";if(b){var x=m.eliminated_round!=null?m.eliminated_round:"",_=I.t("game.outRound",{round:x})||"Out \xB7 R"+x;w+=''+p(_)+""}else m.steal_used&&(w+='\u{1F977}'),m.bet&&(w+='\u{1F3B2}');var S=b?'':''+p(g)+"";return'
'+w+'
'+S+'
'+p(m.name)+"
"}).join("")}}function Lr(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&&fn(),o=r?mn(a):{};a.forEach(function(f){f.is_current=f.name===s.playerName;var m=o[f.name];m&&(f._rankChange=m);var g=Q.players[f.name],y=g?g.score:f.score;f._prevScore=y,f._displayScore=n?y:f.score});var l=Ir(a,s.playerName),c=a.length>=Le.MIN_PLAYERS_FOR_LAZY;if(c)B.observer||vn(i),B.fullData=l,B.isLazyEnabled=!0,B.listEl=i,B.visibleRange=St(l,s.playerName),He();else{B.isLazyEnabled=!1;var d="";l.forEach(function(f){d+=It(f)}),i.innerHTML=d}var u=[];r&&l.forEach(function(f){!f.separator&&f._prevScore!==f.score&&u.push({name:f.name,prevScore:f._prevScore,newScore:f.score})}),r&&u.length>0&&requestAnimationFrame(function(){for(var f={},m=i.querySelectorAll(".leaderboard-entry[data-name]"),g=0;g8&&Sr(i),Br(a),_r(a),$e(e.players||[],a)}}function Ir(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Sr(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Br(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=I.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function jn(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function _r(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function zn(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var D=!1,We=!1,Fe=!1,Wt=!1,Gn=!1,Vn=!1;function Yn(){if(Vn)return;var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!e||!t)return;Vn=!0,e.addEventListener("input",function(){Ce||(t.textContent=this.value)});function n(m){var g=parseInt(e.value,10)+m;g=Math.max(parseInt(e.min,10),Math.min(parseInt(e.max,10),g)),e.value=g,t.textContent=g}function a(m,g){if(!m)return;var y=null,E=null;m.addEventListener("pointerdown",function(h){D||Ce||(h.preventDefault(),n(g),E=setTimeout(function(){y=setInterval(function(){n(g)},150)},500))});function b(){E&&(clearTimeout(E),E=null),y&&(clearInterval(y),y=null)}["pointerup","pointerleave","pointercancel"].forEach(function(h){m.addEventListener(h,b)}),m.addEventListener("keydown",function(h){D||Ce||(h.key==="Enter"||h.key===" ")&&(h.preventDefault(),n(g))})}a(document.getElementById("year-decrement"),-1),a(document.getElementById("year-increment"),1),a(document.getElementById("year-decrement-5"),-5),a(document.getElementById("year-increment-5"),5);var i=document.getElementById("bet-toggle");i&&i.addEventListener("click",function(){D||(We=!We,i.classList.toggle("is-active",We))});var r=document.getElementById("submit-btn");if(r&&r.addEventListener("click",function(){Wt?Wn():xr()}),!Gn){var o=document.getElementById("ta-title-input"),l=document.getElementById("ta-artist-input");o&&o.addEventListener("keydown",function(m){m.key==="Enter"&&(m.preventDefault(),l&&l.focus())}),l&&l.addEventListener("keydown",function(m){m.key==="Enter"&&(m.preventDefault(),Wt&&Wn())}),Gn=!0}var c=document.getElementById("steal-btn");c&&c.addEventListener("click",Tr);var d=document.getElementById("steal-modal-close");d&&d.addEventListener("click",Ft);var u=document.getElementById("steal-modal");if(u){var f=u.querySelector(".steal-modal-backdrop");f&&f.addEventListener("click",Ft)}}function xr(){if(!D&&!Ce){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:We})):(rt(I.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function it(){D=!0;var e=document.getElementById("year-selector"),t=document.getElementById("year-display-arc"),n=document.getElementById("submit-btn"),a=document.getElementById("bet-toggle"),i=document.getElementById("submitted-banner");e&&e.classList.add("is-submitted","slider-arcade--locked"),t&&t.classList.add("year-xxl--locked"),n&&(n.disabled=!0,n.classList.add("submit-arc--waiting"),n.innerHTML=""+p(I.t("game.waitingForOthers")||"Waiting for others")+''),a&&(a.disabled=!0),i&&i.classList.remove("hidden"),["year-decrement","year-increment","year-decrement-5","year-increment-5"].forEach(function(r){var o=document.getElementById(r);o&&(o.disabled=!0)})}function Ut(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(rt(I.t("errors.timesUp")),D=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?it():rt(e.message||"Submission failed")}function rt(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=I.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Jn(){D=!1,We=!1;var e=document.getElementById("year-selector"),t=document.getElementById("year-display-arc"),n=document.getElementById("submit-btn"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle"),r=document.getElementById("submitted-banner");if(e&&e.classList.remove("is-submitted","slider-arcade--locked"),t&&t.classList.remove("year-xxl--locked"),n&&(n.disabled=!1,n.classList.remove("hidden","is-loading","is-error","submit-arc--waiting"),n.textContent=I.t("game.submitGuess")),i&&(i.disabled=!1,i.classList.remove("hidden","is-active")),r&&r.classList.add("hidden"),a){a.value=1990;var o=document.getElementById("selected-year");o&&(o.textContent="1990")}["year-decrement","year-increment","year-decrement-5","year-increment-5"].forEach(function(l){var c=document.getElementById(l);c&&(c.disabled=!1)}),Fe=!1,jt(),Mt(),Gt(),Cr()}function kr(e){var t=!!(e&&e.title_artist_mode);Wt=t;var n=document.getElementById("title-artist-container"),a=document.getElementById("year-selector-container"),i=document.getElementById("year-display-arc"),r=document.getElementById("bet-toggle");if(n&&n.classList.toggle("hidden",!t),a&&a.classList.toggle("hidden",t),i&&i.classList.toggle("hidden",t),r&&r.classList.toggle("hidden",t),!!t){var o=document.getElementById("submit-btn");o&&!D&&(o.textContent=I.t("titleArtist.submitGuess")||"Submit")}}function Wn(){if(!D&&!Ce){var e=document.getElementById("ta-title-input"),t=document.getElementById("ta-artist-input"),n=document.getElementById("submit-btn");if(!(!e||!t||!n)){var a=(e.value||"").trim(),i=(t.value||"").trim();n.disabled=!0,n.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"title_artist_guess",title:a,artist:i})):(rt(I.t("errors.connectionLost")),n.disabled=!1,n.classList.remove("is-loading"))}}}function Qn(e){it();var t=document.getElementById("ta-title-input"),n=document.getElementById("ta-artist-input");t&&(t.disabled=!0),n&&(n.disabled=!0);var a=document.getElementById("ta-input-ack");a&&(a.textContent=I.t("titleArtist.submitted")||"Submitted \u2014 see how you did at the reveal!",a.classList.remove("hidden"))}function Cr(){var e=document.getElementById("ta-title-input"),t=document.getElementById("ta-artist-input"),n=document.getElementById("ta-input-ack");e&&(e.value="",e.disabled=!1),t&&(t.value="",t.disabled=!1),n&&(n.textContent="",n.classList.add("hidden"))}function Ar(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){Fe=t.steal_available&&!D;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");Fe?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):jt(),qt()}}}function jt(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden"),qt()}function Tr(){!Fe||D||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function Nr(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=I.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){Mr(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function Ft(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function Mr(e){var t=I.t("steal.confirm").replace("{name}",e),n=await Ie(I.t("steal.confirmTitle")||"Steal Answer?",t,I.t("steal.confirmButton")||"Steal",I.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),Ft())}function Xn(e){if(e.success){Fe=!1,D=!0,jt();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),Rr(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Kn(e){Nr(e.targets||[])}function Rr(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=I.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fn=0,Or=500,qe=!1,zt=.5;function st(){var e=Date.now();return e-Fn=1){ta("max");return}st()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function Gr(){if(zt<=0){ta("min");return}st()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function ta(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function Vr(){var e=await Ie(I.t("admin.endGameConfirm")||"End Game?",I.t("admin.endGameWarning")||"All players will be disconnected.",I.t("admin.endGame")||"End Game",I.t("common.cancel"));if(e&&st()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(I.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var at=!1;function na(){if(!at&&s.ws&&s.ws.readyState===WebSocket.OPEN){at=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=I.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){at&&Jt()},1e4)}}function Jt(){at=!1;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!1,e.textContent=I.t("admin.nextRound")),t){t.disabled=!1;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("admin.nextRound"))}}function Wr(){na()}function aa(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",Hr),t&&t.addEventListener("click",Dr),n&&n.addEventListener("click",Gr),a&&a.addEventListener("click",Wr),i&&i.addEventListener("click",Vr)}function ra(){qe=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=I.t("game.stopped"))}}function Qt(){qe=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=I.t("game.stop"))}}function ia(e){zt=e,Fr(e),qr(e)}function Fr(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function qr(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function sa(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",na);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||(Se.isRunning()&&Se.skipAll(),X())})}function oa(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function la(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var v=window.BeatifyUtils||{},da=30,lt=null;function Kt(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("reveal-idle-halt");r&&r.classList.toggle("hidden",!e.idle_halt),Ur(e);var o=document.getElementById("closest-wins-badge");o&&(e.closest_wins_mode?o.classList.remove("hidden"):o.classList.add("hidden"));var l=document.getElementById("intro-badge");if(l)if(e.is_intro_round){l.classList.remove("hidden"),l.classList.add("intro-badge--stopped");var c=l.querySelector("[data-i18n]");c&&(c.setAttribute("data-i18n","game.introStopped"),c.textContent=v.t("game.introStopped")||"Intro complete!")}else l.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.onerror=function(){d.src="/beatify/static/img/no-artwork.svg"},d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("reveal-backdrop");if(u){var f=t.album_art;if(f){var m=new Image;m.onload=function(){u.style.backgroundImage='url("'+f+'")',u.classList.remove("reveal-backdrop--synthetic")},m.onerror=function(){u.style.backgroundImage="",u.classList.add("reveal-backdrop--synthetic")},m.src=f}else u.style.backgroundImage="",u.classList.add("reveal-backdrop--synthetic")}var g=document.getElementById("correct-year");g&&(g.textContent=t.year||"????");var y=document.getElementById("song-title"),E=document.getElementById("song-artist");y&&(y.textContent=t.title||"Unknown Song"),E&&(E.textContent=t.artist||"Unknown Artist");var b=document.getElementById("fun-fact-container"),h=document.getElementById("fun-fact"),w=b?b.querySelector(".fun-fact-header"):null,x=v.getLocalizedSongField(t,"fun_fact");if(h&&(h.textContent=x||""),w&&(w.style.display=x?"flex":"none"),Yr(t),zr(e.song_difficulty),b){var _=document.getElementById("song-rich-info"),S=_&&_.innerHTML.trim()!=="",G=x&&x.trim()!=="";b.classList.toggle("hidden",!G&&!S)}for(var A=null,M=0;M'+v.t("analytics.noGuesses")+"
";var a=e.map(function(V){return V.guess}),i=Math.min.apply(null,a.concat([t])),r=Math.max.apply(null,a.concat([t])),o=Math.max(2,Math.floor((r-i)*.1)),l=i-o,c=r+o,d=Math.max(1,c-l);function u(V){return(V-l)/d*100}for(var f=u(t),m='
'+t+"
",g="",y=0;y<=4;y++){var E=Math.round(l+d*y/4),b=y*25;g+='
'+E+"
"}function h(V){for(var T=0,O=0;O>>0;return"c"+(T%4+1)}for(var w="",x=0;x0?"dotaxis-score--pos":"dotaxis-score--zero",R=H>0?"+"+H:"+0";w+='
'+G+'
'+R+"
"}for(var ie=e.slice().sort(function(V,T){return(V.years_off||0)-(T.years_off||0)}),Te=["\u{1F3C6}","\u{1F948}","\u{1F949}"],Ne="",se=0;se'+Te[se]+"":"",ye=n&&J.name===n?' '+v.t("analytics.youMarker")+"":"";Ne+=''+oe+''+p(J.name||"?")+ye+""}return'
'+g+'
'+m+w+'
'+Ne+"
"}function zr(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML='
'+n+'
'+v.t("difficulty."+e.label)+''+e.accuracy+"% "+v.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function Yr(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=Jr(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=Qr(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=v.getLocalizedSongField(e,"awards")||[],o=Zr(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML='
'+n.join("")+"
":t.innerHTML=""}}function Jr(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+v.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+v.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+v.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+v.t("reveal.chartUK")+""),t}function Qr(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+p(a)+"")}return t}function Xr(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Kr(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function Zr(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+p(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function $r(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function ei(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function ti(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("duel-emotion"),r=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"duel-emotion":r?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),X();var o=v.t("reveal.emotions");function l(y){return y[Math.floor(Math.random()*y.length)]}function c(y){return y===1?v.t("reveal.offByYear"):v.t("reveal.offByYears",{years:y})}var d="missed",u=l(o.missed),f=l(o.missedSub);if(e&&!e.missed_round){var m=e.years_off||0;m===0?(d="exact",u=l(o.exact),f=l(o.exactSub)):m<=2?(d="close",u=l(o.close),f=l(o.closeSub)+" "+c(m)):m<=5?(d="close",u=l(o.close),f=c(m)):(d="wrong",u=l(o.wrong),f=l(o.wrongSub)+" "+c(m))}else e&&e.missed_round&&(d="missed",u=l(o.missed),f=l(o.missedSub));if(i)n.textContent=u,n.classList.add("duel-emotion--"+d);else{var g=''+u+"";f&&(g+='
'+f+"
"),n.innerHTML=g,n.classList.add("reveal-emotion--"+d)}n.classList.remove("hidden"),d==="exact"&&Be(),a&&d!=="missed"&&a.classList.add("is-delayed")}function ni(e,t){var n=document.getElementById("duel-your-year"),a=document.getElementById("duel-gap-count"),i=document.getElementById("duel-gap-unit");if(!(!n||!a||!i)){if(!e||e.missed_round){n.textContent=v.t("reveal.duel.noGuess")||"\u2014",a.textContent="\u2014",i.textContent="";return}var r=e.guess;n.textContent=r!=null&&r!==""?r:"\u2014";var o=e.years_off!=null?e.years_off:0;a.textContent=String(o),i.textContent=o===1?v.t("reveal.duel.yearUnit")||"year":v.t("reveal.duel.yearsUnit")||"years";var l=a.closest(".duel-gap");l&&(l.classList.remove("duel-gap--exact","duel-gap--close","duel-gap--wrong"),o===0?l.classList.add("duel-gap--exact"):o<=5?l.classList.add("duel-gap--close"):l.classList.add("duel-gap--wrong"))}}function ai(e,t){var n=document.getElementById("reveal-chip-row");if(n){if(!e){n.classList.add("hidden"),n.innerHTML="";return}var a=[];e.bet_outcome==="won"?a.push('\u{1F3B2} '+p(v.t("reveal.chip.betWon")||"Bet won \xB7 \xD72")+""):e.bet_outcome==="lost"&&a.push('\u{1F3B2} '+p(v.t("reveal.chip.betLost")||"Bet lost")+"");var i=e.streak_bonus||0;if(i>0&&e.streak){var r=v.t("reveal.chip.streakBonus",{count:e.streak,bonus:i})||e.streak+"-streak \xB7 +"+i;a.push('\u{1F525} '+p(r)+"")}a.length===0?(n.classList.add("hidden"),n.innerHTML=""):(n.classList.remove("hidden"),n.innerHTML=a.join(""))}}function Zt(e){if(!e)return 0;var t=e.round_score||0,n=e.streak_bonus||0,a=e.artist_bonus||0,i=e.movie_bonus||0,r=e.intro_bonus||0;return t+n+a+i+r}function ri(e){var t=document.getElementById("reveal-total-pts"),n=document.getElementById("score-row-subtitle");if(t){var a=Zt(e);if(t.textContent=(a>=0?"+":"")+a,n)if(!e||e.missed_round)n.textContent=v.t("reveal.noSubmission")||"No guess submitted";else{var i=e.years_off!=null?e.years_off:0,r=i===0?"reveal.exact":i===1?"reveal.yearOff":"reveal.yearsOff";n.textContent=v.t(r,{years:i})||i+" years off"}}}function ii(e){for(var t=[["#ff2d6a","#ff6600"],["#00f5ff","#7a5cff"],["#39ff14","#00f5ff"],["#ff6600","#ff0040"],["#7a5cff","#b3b3c2"],["#ff2d6a","#7a5cff"]],n=0,a=0;a>>0;var i=t[n%t.length];return"linear-gradient(135deg,"+i[0]+","+i[1]+")"}function si(e){var t=document.getElementById("reveal-leaderboard-list");if(t){var n=e.leaderboard||[],a=e.players||[],i={};a.forEach(function(l){i[l.name]=l});var r=(v.t("analytics.youMarker")||"YOU").replace(/[()]/g,""),o="";n.forEach(function(l,c){var d=i[l.name]||{},u=l.name===s.playerName,f=((l.name||"?").trim().charAt(0)||"?").toUpperCase(),m=l.rank_change||0,g=m>0?"up":m<0?"down":"flat",y=m>0?"\u25B2"+m:m<0?"\u25BC"+Math.abs(m):"\u2013",E="";if(!d.missed_round&&d.years_off!=null&&d.guess!=null&&d.guess!==""){var b=d.years_off,h=b===0?''+p(v.t("reveal.exact")||"Exact!")+"":p(v.t("reveal.shortOff",{years:b})||b+" off");E='
'+p(String(d.guess))+" \xB7 "+h+"
"}var w=[];d.streak&&d.streak>=2&&w.push('\u{1F525} '+d.streak+""),d.stole_from?w.push('\u{1F977} '+p(v.t("steal.stolenFrom",{name:d.stole_from})||"stole "+d.stole_from)+""):d.was_stolen_by&&d.was_stolen_by.length&&w.push('\u{1F3AF} '+p(v.t("steal.stolenBy",{name:d.was_stolen_by.join(", ")})||"stolen")+""),d.bet_outcome==="won"?w.push('\u{1F3B2} '+p(v.t("reveal.chip.betWon")||"Bet won")+""):d.bet_outcome==="lost"&&w.push('\u{1F3B2} '+p(v.t("reveal.chip.betLost")||"Bet lost")+"");var x=w.length?'
'+w.join("")+"
":"",_=Zt(d),S=_>0?"":" rstand-delta--zero",G=(_>=0?"+":"")+_,A=c>=4?" rstand-row--taper2":c>=3?" rstand-row--taper1":"",M=u?" rstand-row--you":"",P=u?' '+p(r)+"":"";o+='
'+l.rank+''+y+'
'+p(f)+'
'+p(l.name||"?")+P+"
"+E+x+'
'+(l.score||0)+''+G+"
"}),t.innerHTML=o,$e(a,n)}}function oi(){var e=document.getElementById("points-breakdown-content");if(e){var t=s.lastRevealContext,n=t?t.player:null;if(!n||n.missed_round){e.innerHTML='
'+p(v.t("reveal.breakdown.noSubmission")||v.t("reveal.noSubmission")||"No guess submitted")+'
'+p(v.t("reveal.breakdown.total")||"Total this round")+'+0
';return}var a=[],i=n.years_off!=null?n.years_off:0,r=n.base_score||0,o=n.round_score||0,l=n.speed_multiplier||1,c=Math.floor(r*l)-r;a.push({emoji:"\u{1F3AF}",label:v.t("reveal.breakdown.baseScore",{years:i})||"Base score",value:String(r),kind:"neutral"}),l>1&&c>0&&a.push({emoji:"\u26A1",label:(v.t("reveal.breakdown.speedBonus")||"Speed bonus")+" ("+l.toFixed(2)+"\xD7)",value:"+"+c,kind:"positive"}),n.streak_bonus&&n.streak_bonus>0&&a.push({emoji:"\u{1F525}",label:v.t("reveal.breakdown.streakBonus",{count:n.streak})||n.streak+"-streak bonus",value:"+"+n.streak_bonus,kind:"positive"}),n.artist_bonus&&n.artist_bonus>0&&a.push({emoji:"\u{1F3A4}",label:v.t("reveal.breakdown.artistBonus")||"Artist challenge",value:"+"+n.artist_bonus,kind:"positive"}),n.movie_bonus&&n.movie_bonus>0&&a.push({emoji:"\u{1F3AC}",label:v.t("reveal.breakdown.movieBonus")||"Movie challenge",value:"+"+n.movie_bonus,kind:"positive"}),n.intro_bonus&&n.intro_bonus>0&&a.push({emoji:"\u26A1",label:v.t("reveal.breakdown.introBonus")||"Intro speed bonus",value:"+"+n.intro_bonus,kind:"positive"}),n.bet_outcome==="won"?a.push({emoji:"\u{1F3B2}",label:v.t("reveal.breakdown.betMultiplier")||"Double or Nothing",value:"\xD72",kind:"multiplier"}):n.bet_outcome==="lost"&&a.push({emoji:"\u{1F3B2}",label:v.t("reveal.breakdown.betLost")||"Bet lost",value:"\xD70",kind:"multiplier"});var d=Zt(n),u='
';a.forEach(function(f){u+='
"+p(f.label)+''+p(f.value)+"
"}),u+="
",u+='
'+p(v.t("reveal.breakdown.total")||"Total this round")+''+(d>=0?"+":"")+d+"
",e.innerHTML=u}}function li(){var e=document.getElementById("round-stats-content");if(e){var t=s.lastRevealContext;if(!t){e.innerHTML="";return}var n=t.analytics,a=t.difficulty,i=t.song||{},r=i.year,o=[];if(a){for(var l="",c=5,d=0;d\u2605';var u=v.t("difficulty."+a.label)||a.label||"",f=a.accuracy!=null?v.t("reveal.stats.onlyPercent",{percent:a.accuracy})||"Only "+a.accuracy+"% of all players guess it right.":"";o.push('
'+p(u)+(f?'
'+p(f)+"
":"")+'
'+l+"
")}if(n){var m=[];if(n.average_guess!=null){var g=r?Math.round(n.average_guess-r):null,y=g!=null?g===0?v.t("analytics.onTarget")||"On target":Math.abs(g)+" "+(v.t("reveal.duel.yearsUnit")||"years")+" "+(g>0?"late":"early"):"";m.push('
'+p(v.t("reveal.stats.avgGuess")||"Avg guess")+'
'+Math.round(n.average_guess)+"
"+(y?'
'+p(y)+"
":"")+"
")}if(n.all_guesses&&n.all_guesses.length>0){var E=n.all_guesses[0],b=E.name+" \xB7 "+(E.years_off===0?v.t("reveal.exact")||"Exact!":E.years_off+" "+(E.years_off===1?v.t("reveal.duel.yearUnit")||"year":v.t("reveal.duel.yearsUnit")||"years")+" off");m.push('
'+p(v.t("reveal.stats.closest")||"Closest")+'
'+p(String(E.guess))+'
'+p(b)+"
")}if(n.speed_champion&&n.speed_champion.time!=null&&m.push('
'+p(v.t("reveal.stats.fastest")||"Fastest")+'
'+n.speed_champion.time+'s
'+p((n.speed_champion.names||[]).join(", "))+"
"),a&&a.times_played!=null){var h=v.t("reveal.stats.playedBeforeSub")||"across all Beatify games";m.push('
'+p(v.t("reveal.stats.playedBefore")||"Played before")+'
'+a.times_played+'\xD7
'+p(h)+"
")}m.length>0&&o.push('
'+m.join("")+"
")}if(n&&n.all_guesses&&n.all_guesses.length>0&&o.push('
'+p(v.t("analytics.guessAxis")||"Where everyone guessed")+"
"+jr(n.all_guesses,r,s.playerName)+"
"),n&&n.furthest_players&&n.furthest_players.length>0&&n.all_guesses&&n.all_guesses.length>0){var w=n.all_guesses[n.all_guesses.length-1];if(w&&w.years_off>0){var x=n.furthest_players.map(function(_){return'
'+p(_)+''+w.years_off+" "+(w.years_off===1?v.t("reveal.duel.yearUnit")||"yr":v.t("reveal.duel.yearsUnit")||"yrs")+" off
"}).join("");o.push('
'+p(v.t("reveal.stats.furthestOff")||"Furthest off this round")+'
'+x+"
")}}o.length===0&&o.push('

'+p(v.t("reveal.stats.empty")||"No stats for this round yet.")+"

"),e.innerHTML=o.join("")}}function ca(e,t){var n=document.getElementById(e);if(n){typeof t=="function"&&t(),n.classList.remove("hidden");var a=n.querySelector(".sheet-close");a&&a.focus()}}function Xt(e){var t=document.getElementById(e);t&&t.classList.add("hidden")}function fa(){var e=document.getElementById("points-breakdown-btn");e&&e.addEventListener("click",function(){ca("points-breakdown-sheet",oi)});var t=document.getElementById("round-stats-btn");t&&t.addEventListener("click",function(){ca("round-stats-sheet",li)}),document.querySelectorAll("[data-sheet-close]").forEach(function(n){n.addEventListener("click",function(a){Xt(n.getAttribute("data-sheet-close")),a.stopPropagation()})}),document.querySelectorAll(".sheet-backdrop").forEach(function(n){var a=n.querySelector(".sheet-dim");a&&a.addEventListener("click",function(){Xt(n.id)})}),document.addEventListener("keydown",function(n){n.key==="Escape"&&["points-breakdown-sheet","round-stats-sheet"].forEach(function(a){var i=document.getElementById(a);i&&!i.classList.contains("hidden")&&Xt(a)})})}function di(){var e=document.getElementById("reveal-report-btn");e&&(e.textContent=v.t("reveal.reportBtn")||"\u{1F6A9} Wrong year?",e.disabled=!1)}function ma(){var e=document.getElementById("reveal-report-btn");e&&e.addEventListener("click",function(){var t=s.lastRevealContext;!t||!t.song||!s.ws||s.ws.readyState!==WebSocket.OPEN||(s.ws.send(JSON.stringify({type:"report_data",artist:t.song.artist||"",title:t.song.title||"",year:t.song.year||null})),e.textContent=v.t("reveal.reportBtnDone")||"\u2713 Reported \u2014 thanks!",e.disabled=!0)})}function ua(e){var t="ta-pill ta-pill--"+(e||"skipped").replace(/_/g,"-"),n;switch(e){case"exact":n=v.t("titleArtist.statusExact")||"Correct";break;case"fuzzy":n=v.t("titleArtist.statusFuzzy")||"Close enough";break;case"near_miss_accepted":n=v.t("titleArtist.statusAccepted")||"Accepted";break;case"near_miss":n=v.t("titleArtist.statusNearMiss")||"Near miss";break;case"wrong":n=v.t("titleArtist.statusWrong")||"Wrong";break;default:n=v.t("titleArtist.statusSkipped")||"Skipped"}return''+p(n)+""}function ci(e,t){if(!e)return null;var n={exact:1,fuzzy:1,near_miss_accepted:1},a=e.title_status,i=e.artist_status;if(t&&(a==="near_miss"||i==="near_miss"))return{tier:"pending",text:v.t("titleArtist.verdictPending")||"Awaiting the room\u2019s verdict\u2026"};var r=(n[a]?1:0)+(n[i]?1:0);return r===2?{tier:"win",text:v.t("titleArtist.verdictWin")||"Nailed it!"}:r===1?{tier:"partial",text:v.t("titleArtist.verdictPartial")||"Got one!"}:{tier:"miss",text:v.t("titleArtist.verdictMiss")||"Not this time"}}function ui(e,t){var n=!!e.accepted,a=((e.player||"?").trim().charAt(0)||"?").toUpperCase(),i=n?"\u2713 +"+(e.points||0):"\u2717";return'
'+p(e.player)+' \xB7 '+p(t(e.field))+'
\u201C'+p(e.guess||"\u2014")+'\u201D
\u{1F44D} '+(e.votes_yes||0)+" \xB7 \u{1F44E} "+(e.votes_no||0)+'
'+i+"
"}function fi(e,t){var n=document.getElementById("ta-reveal-section");if(n){if(!e||!e.correct_title){n.classList.add("hidden"),ve();return}n.classList.remove("hidden"),s._taRevealTruth!==e.correct_title&&(s._taRevealTruth=e.correct_title,s.taMyVotes={}),s.taMyVotes=s.taMyVotes||{};var a=document.getElementById("ta-reveal-truth");a&&(a.innerHTML=''+p(e.correct_title)+''+p(e.correct_artist||"")+"");for(var i=document.getElementById("ta-reveal-own"),r=e.results||[],o=null,l=0;l'+p(c.text)+"
":"";i.innerHTML=d+'
'+p(v.t("titleArtist.yourTitle")||"Your title")+''+p(o.title||"\u2014")+""+ua(o.title_status)+'
'+p(v.t("titleArtist.yourArtist")||"Your artist")+''+p(o.artist||"\u2014")+""+ua(o.artist_status)+"
"}else i.innerHTML='
'+p(v.t("titleArtist.noGuess")||"No guess this round")+"
";var u=document.getElementById("ta-voting"),f=document.getElementById("ta-voting-cards"),m=document.getElementById("ta-voting-title"),g=document.getElementById("ta-voting-countdown"),y=e.near_misses||[],E=e.near_miss_outcomes||[],b=!!e.voting_open,h=!!(t&&t.is_admin);if(!u||!f){ve();return}var w=function(S){return S==="artist"?v.t("titleArtist.artistLabel")||"Artist":v.t("titleArtist.titleLabel")||"Song title"};if(!b&&E.length>0){ve(),u.classList.remove("hidden"),m&&(m.textContent=v.t("titleArtist.closeCallsDecided")||"Close calls \u2014 decided"),g&&(g.textContent="",g.classList.add("hidden")),f.innerHTML=E.map(function(S){return ui(S,w)}).join("");return}if(y.length===0){u.classList.add("hidden"),f.innerHTML="",ve();return}u.classList.remove("hidden"),m&&(m.textContent=v.t("titleArtist.voteHeader")||"Close calls \u2014 vote \u{1F44D}/\u{1F44E}"),g&&g.classList.remove("hidden");var x="";if(y.forEach(function(S){var G=S.player===s.playerName,A=S.votes_yes||0,M=S.votes_no||0,P=A+M,F=P?Math.round(A/P*100):0,H=P?100-F:0,q=((S.player||"?").trim().charAt(0)||"?").toUpperCase();if(x+='
'+p(S.player)+''+p(w(S.field))+'
\u201C'+p(S.guess||"\u2014")+'\u201D
\u{1F44D} '+A+'
\u{1F44E} '+M+'
',G)x+='
'+p(v.t("titleArtist.yourCloseCall")||"Your close call \u2014 others decide")+"
";else if(b){var R=s.taMyVotes[S.id],ie=R===!0||R===!1;x+='
',ie&&(x+='
'+p(v.t("titleArtist.youVoted")||"You voted")+" "+(R?"\u{1F44D}":"\u{1F44E}")+" \xB7 "+p(v.t("titleArtist.tapToChange")||"tap to change")+"
")}h&&b&&(x+='
'+p(v.t("titleArtist.hostOverride")||"Host decides")+'
'),x+="
"}),f.innerHTML=x,b)mi(e);else{ve();var _=document.getElementById("ta-voting-countdown");_&&(_.textContent=v.t("titleArtist.voteClosed")||"Voting closed",_.removeAttribute("aria-label"),_.classList.add("ta-voting-countdown--closed"))}}}function mi(e){ve();var t=document.getElementById("ta-voting-countdown");if(!t)return;var n=e&&typeof e.vote_seconds_remaining=="number"?e.vote_seconds_remaining:da,a=Date.now()+n*1e3;function i(){var r=Math.max(0,Math.ceil((a-Date.now())/1e3));t.textContent=String(r),t.setAttribute("aria-label",v.t("titleArtist.voteCountdown",{seconds:r})||r+"s"),t.style.setProperty("--ta-vote-progress",r/da*360+"deg"),t.classList.remove("ta-voting-countdown--closed"),r<=0&&ve()}i(),lt=setInterval(i,500)}function ve(){lt&&(clearInterval(lt),lt=null)}function va(){var e=document.getElementById("ta-voting-cards");e&&e.addEventListener("click",function(t){var n=t.target.closest(".ta-vote-btn"),a=t.target.closest(".ta-override-btn");if(n){var i=n.getAttribute("data-nearmiss-id"),r=n.getAttribute("data-accept")==="1";s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"title_artist_vote",nearmiss_id:i,accept:r})),s.taMyVotes=s.taMyVotes||{},s.taMyVotes[i]=r;var o=n.closest(".ta-vote-card");if(o){var l=o.querySelector(".ta-vote-actions");l&&l.classList.add("ta-vote-actions--voted"),o.querySelectorAll(".ta-vote-btn").forEach(function(f){f.classList.remove("is-chosen")}),n.classList.add("is-chosen")}return}if(a){var c=a.getAttribute("data-nearmiss-id"),d=a.getAttribute("data-accept")==="1";s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"title_artist_override",nearmiss_id:c,accept:d}));var u=a.closest(".ta-vote-card");u&&(u.querySelectorAll(".ta-override-btn").forEach(function(f){f.classList.remove("is-chosen")}),a.classList.add("is-chosen"))}})}var N=window.BeatifyUtils||{};function ga(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var h=t.find(function(S){return S.rank===b}),w=document.querySelector(".podium-place.podium-"+b);w&&w.classList.toggle("hidden",!h);var x=document.getElementById("podium-"+b+"-name"),_=document.getElementById("podium-"+b+"-score");x&&(x.textContent=h?p(h.name):"---"),_&&(_.textContent=h?h.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+N.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var c=document.getElementById("final-leaderboard-list");c&&(c.innerHTML=t.map(function(b){var h=b.is_current?"is-current":"",w=b.connected===!1?"final-entry--disconnected":"",x=b.connected===!1?'(away)':"";return'
#'+b.rank+''+p(b.name)+x+''+b.score+"
"}).join("")),vi(e.superlatives),pi(e.highlights),gi(e.share_data);var d=document.getElementById("end-admin-controls"),u=document.getElementById("end-player-message");if(n&&n.is_admin){d&&d.classList.remove("hidden"),u&&u.classList.add("hidden");var f=document.getElementById("new-game-btn");f&&(f.onclick=hi);var m=document.getElementById("player-rematch-btn");m&&(m.onclick=function(){m.disabled=!0;var b=m.textContent;if(m.textContent="\u23F3",s.ws&&s.ws.readyState===WebSocket.OPEN){s.ws.send(JSON.stringify({type:"admin",action:"rematch_game"}));return}BeatifyAuth.fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin"}).then(function(h){if(!h.ok)return h.json().then(function(w){throw new Error(w.message||"Rematch failed")});m.textContent="\u23F3"}).catch(function(h){console.error("[Player] Rematch failed:",h),alert(h.message||"Failed to start rematch"),m.disabled=!1,m.textContent=b})})}else d&&d.classList.add("hidden"),u&&u.classList.remove("hidden");if(n){var g=e.total_rounds||10,y=n.best_streak||0,E=y===g&&g>0;E?Be("perfect"):n.rank===1&&Be("winner")}}function vi(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+N.t("superlatives.avgTime");break;case"streak":r=a.value+" "+N.t("superlatives.streak");break;case"bets":r=a.value+" "+N.t("superlatives.bets");break;case"points":r=a.value+" "+N.t("superlatives.points");break;case"close_guesses":r=a.value+" "+N.t("superlatives.closeGuesses");break;case"perfect_rounds":r=a.value+" "+N.t("superlatives.perfectRounds");break;case"exact_titles":r=a.value+" "+N.t("superlatives.exactTitles");break;case"artists":r=a.value+" "+N.t("superlatives.artists");break;case"near_misses":r=a.value+" "+N.t("superlatives.nearMisses");break;default:r=a.value}n+='
'+a.emoji+'
'+N.t("superlatives."+a.title)+'
'+p(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function pi(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=N.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=N.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",p(i.description_params[l]))})),a+='
'+(i.emoji||"\u2728")+'
'+o+'
'+N.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function gi(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}t.classList.remove("hidden"),yi(n,e.playlist_name).then(function(i){var r=document.getElementById("share-card-image");r&&i&&(r.src=i.toDataURL("image/png"));var o=document.getElementById("share-save-btn");o&&(o.onclick=function(){bi(i)})})}}function yi(e,t){var n=800,a=800,i=document.createElement("canvas");i.width=n,i.height=a;for(var r=i.getContext("2d"),o=e.split(` +`).filter(function(M){return M.trim()!==""}),l="",c="",d="",u="",f="",m=0;m0&&(Oe+=cn),W.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",Oe+=r.measureText(W.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",Oe+=r.measureText(W.num).width,r.font="600 15px Inter, system-ui, sans-serif",Oe+=r.measureText(W.label).width)});var Z=T-Oe/2;r.textAlign="left",Re.forEach(function(W,ht){ht>0&&(r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#6b6b7a",r.fillText(" \xB7 ",Z,Ze),Z+=cn),W.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(W.text,Z,Ze),Z+=r.measureText(W.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(W.num,Z,Ze),Z+=r.measureText(W.num).width,r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#b3b3c2",r.fillText(W.label,Z,Ze),Z+=r.measureText(W.label).width)})}}function bi(e){e&&e.toBlob(function(t){if(t){if(navigator.share&&navigator.canShare){var n=new File([t],"beatify-results.png",{type:"image/png"}),a={files:[n],title:"My Beatify Results"};if(navigator.canShare(a)){navigator.share(a).catch(function(){pa(t)});return}}pa(t)}},"image/png")}function pa(e){var t=URL.createObjectURL(e),n=document.createElement("a");n.href=t,n.download="beatify-results.png",document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(t)}function ya(e){var t=document.getElementById("pause-message");t&&(e.pause_reason==="admin_disconnected"?t.textContent=N.t("player.waitingForHostReconnect"):e.pause_reason==="media_player_error"?t.textContent=N.t("player.speakerUnavailable"):t.textContent=N.t("player.gamePaused"))}async function hi(){var e=await Ie(N.t("admin.newGameTitle")||"New Game?",N.t("admin.newGameConfirm")||"Start a new game?",N.t("admin.newGame")||"New Game",N.t("common.cancel"));if(e){var t=document.getElementById("new-game-btn");t&&(t.disabled=!0,t.textContent=N.t("player.redirecting"));try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}window.location.href="/beatify/admin"}}var z=window.BeatifyUtils||{},ha="beatify_onboarded_v2",Ei=4e3,wi=1400;function $t(){var e=document.querySelectorAll(".tour-card").length;return e>0?e:4}function Li(){try{return localStorage.getItem(ha)==="1"}catch{return!1}}function Ii(){try{localStorage.setItem(ha,"1")}catch{}}var C={active:!1,replay:!1,currentIdx:0,autoAdvanceTimer:null,readyTimer:null};function pe(){C.autoAdvanceTimer&&(clearTimeout(C.autoAdvanceTimer),C.autoAdvanceTimer=null)}function en(){pe(),!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches)&&(C.autoAdvanceTimer=setTimeout(function(){Ea()},Ei))}function tn(){for(var e=document.querySelectorAll(".tour-wiz-seg"),t=0;t'+o+''}}function Ea(){pe(),C.currentIdx<$t()-1?(C.currentIdx++,tn(),nn(C.currentIdx),en()):wa()}function ba(){pe(),wa()}function wa(){if(pe(),C.active=!1,C.replay){C.replay=!1,k("lobby-view");return}Ii(),La(),Si()}function La(){if(!(!s.ws||s.ws.readyState!==WebSocket.OPEN))try{s.ws.send(JSON.stringify({type:"player_onboarded"}))}catch(e){console.warn("[Beatify] Failed to send player_onboarded:",e)}}function Si(){var e=document.getElementById("ready-name");if(e){var t=z.t?z.t("onboarding.ready",{name:s.playerName||""}):null;e.textContent=t&&t!=="onboarding.ready"?t:"You're in, "+(s.playerName||"")+"!"}var n=document.getElementById("ready-subtitle");n&&(n.textContent=z.t?z.t("onboarding.readySubtitle"):"Get ready to play");var a=document.getElementById("ready-label");a&&(a.textContent=z.t?z.t("onboarding.waitingHost"):"Waiting for host to start"),an(),k("ready-view"),C.readyTimer&&clearTimeout(C.readyTimer),C.readyTimer=setTimeout(function(){C.readyTimer=null;var i=document.getElementById("ready-view");i&&!i.classList.contains("hidden")&&k("lobby-view")},wi)}function an(e,t){var n=document.getElementById("ready-count");if(n){var a=e&&e.length||s.lastPlayerCount||0,i=t||s.lastDifficulty||"";if(!a){n.textContent="";return}var r=a===1?"onboarding.waitingCountOne":"onboarding.waitingCount",o=z.t?z.t(r,{count:a,difficulty:i}):null;o&&o!==r?n.textContent=o:n.textContent=a+(a===1?" player":" players")+" in lobby"}}function Ia(e){return!e||e.is_admin||e.onboarded===!0?!1:Li()?(La(),!1):!0}function Sa(){C.active=!0,C.replay=!1,C.currentIdx=0,tn(),nn(0),k("tour-view"),en()}function Bi(){C.active=!0,C.replay=!0,C.currentIdx=0,tn(),nn(0),k("tour-view"),en()}function Ba(){C.active&&(pe(),C.readyTimer&&(clearTimeout(C.readyTimer),C.readyTimer=null),C.active=!1,C.replay=!1)}function _a(){var e=document.getElementById("tour-skip-link");e&&e.addEventListener("click",function(r){r.preventDefault(),ba()});var t=document.getElementById("tour-skip-btn");t&&t.addEventListener("click",ba);var n=document.getElementById("tour-next-btn");n&&n.addEventListener("click",Ea);var a=document.getElementById("replay-tour-link");a&&a.addEventListener("click",function(r){r.preventDefault(),Bi()});var i=document.querySelector(".tour-container");i&&(i.addEventListener("touchstart",pe,{passive:!0}),i.addEventListener("mousedown",pe))}function dt(){return C.active}var re=window.BeatifyUtils||{},Y=re.debug||function(){},Je=10,_i=3e4,xi=20,ut="beatify_player_name",ft="beatify_game_id",Aa="beatify_language";function ki(e){return!e||typeof e!="string"?!1:/^[a-zA-Z0-9_-]{8,16}$/.test(e)}var mt="beatify_session";function Ta(e){var t=location.protocol==="https:"?"; Secure":"";document.cookie=mt+"="+e+"; path=/beatify; SameSite=Strict; max-age=86400"+t}function Qe(){for(var e=document.cookie.split(";"),t=0;txa){console.warn("[Beatify] No server activity for "+xa+"ms \u2014 socket appears dead, forcing reconnect");try{s.ws.close()}catch{}return}try{s.ws.send(JSON.stringify({type:"ping"}))}catch{}}},Ri)}function sn(){ct&&(clearInterval(ct),ct=null)}function Ae(){var e=Qe();if(e&&!(s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN))){var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,Xe(),Ra(),Ha(),s.ws.send(JSON.stringify({type:"reconnect",session_id:e}))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);Da(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(sn(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Je&&(s.isReconnecting=!1,Xe(),rn())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function ge(e){var t=s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN);if(!(t&&s.playerName===e)){if(t){if(!s.isAdmin)try{s.ws.send(JSON.stringify({type:"leave"}))}catch{}s.intentionalLeave=!0;try{s.ws.close()}catch{}s.ws=null,vt()}s.playerName=e,Na(e);var n=window.location.protocol==="https:"?"wss:":"ws:",a=n+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(a),s.ws.onopen=async function(){s.reconnectAttempts=0,s.isReconnecting=!1,Xe(),Ra(),Ha();var i={type:"join",name:e};s.isAdmin&&(i.is_admin=!0,i.ha_token=await BeatifyAuth.ensureAuthenticated()),s.ws.send(JSON.stringify(i))},s.ws.onmessage=function(i){try{var r=JSON.parse(i.data);Da(r)}catch(o){console.error("Failed to parse WebSocket message:",o)}},s.ws.onclose=function(){if(sn(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Je&&(s.isReconnecting=!1,Xe(),rn())},s.ws.onerror=function(i){console.error("WebSocket error:",i)}}}s.connectWithSession=Ae;s.connectWebSocket=ge;function Da(e){if(s.lastServerActivity=Date.now(),e.type!=="pong"){if(e.type==="game_starting"){var t=document.getElementById("loading-view"),n=document.getElementById("lobby-view"),a=document.getElementById("join-view"),i=n&&!n.classList.contains("hidden"),r=t&&!t.classList.contains("hidden"),o=a&&!a.classList.contains("hidden");(i||r||!o)&&k("starting-view");return}var l=document.getElementById("join-btn"),c=document.getElementById("name-input");if(e.type==="state"){var d=e.players||[],u=d.find(function(h){return h.name===s.playerName});if(u&&(s.isAdmin=u.is_admin===!0),e.language&&(Ai(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),_t(d),e.difficulty&&tt(e.difficulty,e.title_artist_mode),e.phase==="REVEAL"&&Kt(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&ot(e.phase)})),e.join_url&&xn(e.join_url),e.phase==="LOBBY"){fe(),ze(),Ue(),je(),s.currentRoundNumber=0,ue("warmup");var f=document.getElementById("start-game-btn");if(f&&(f.disabled=!1,f.innerHTML=''+re.t("lobby.startGame")+""),s.lastPlayerCount=d.length,s.lastDifficulty=e.difficulty?re.t?re.t("game.difficulty"+e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1)):e.difficulty:"",!dt()&&Ia(u))Sa();else{var m=document.getElementById("ready-view"),g=m&&!m.classList.contains("hidden");!g&&!dt()&&k("lobby-view"),g&&an(d,s.lastDifficulty)}_t(d),e.difficulty&&tt(e.difficulty,e.title_artist_mode),Tn(d)}else if(e.phase==="PLAYING"){dt()&&Ba(),X(),ze(),et();var y=e.round||1;y!==s.currentRoundNumber&&(s.currentRoundNumber=y,Jn()),Jt(),ue("party"),k("game-view"),De(),qn(e),e.intro_splash_pending?oa(s.isAdmin):la(),e.difficulty&&tt(e.difficulty,e.title_artist_mode),e.deadline&&kt(e.deadline),Yn(),jn(),Yt(),ot("PLAYING"),je()}else e.phase==="REVEAL"?(fe(),e.early_reveal&&Rn(),ue("party"),k("reveal-view"),Kt(e),zn(),Yt(),ot("REVEAL"),s.hasReactedThisPhase=!1,Zn()):e.phase==="PAUSED"?(fe(),ze(),Ue(),je(),ue("warmup"),k("paused-view"),ya(e)):e.phase==="END"&&(fe(),ze(),Ue(),je(),In(),s.currentRoundNumber=0,ue("warmup"),k("end-view"),ga(e),Ye())}else if(e.type==="join_ack"){et(),e.session_id&&Ta(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,Na(e.name),Mn(e.name)):(vt(),Ye(),s.playerName=null,k("join-view"));else if(e.type==="submit_ack")it();else if(e.type==="metadata_update")Un(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ut(e);return}if(e.code==="GAME_ENDED"){k("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,Ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,Xe(),s.playerName=null,rn(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){if(s.isReconnecting){console.warn("SESSION_NOT_FOUND during reconnect, will retry with session");return}vt(),s.intentionalLeave=!0,s.ws&&s.ws.close(),k("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Qt(),console.warn("[Beatify] Stop song failed: No song playing");return}if(e.code==="INVALID_ACTION"){console.warn("[Beatify] Action rejected:",e.message),Ut(e);return}k("join-view"),Hi(e.message),l&&(l.disabled=!1,l.textContent=re.t("join.joinButton")),c&&c.focus(),s.playerName=null,Ye()}else if(e.type==="song_stopped")ra();else if(e.type==="volume_changed")ia(e.level);else if(e.type==="game_ended")Pi();else if(e.type==="rematch_started"){Y("[Player] Rematch started - transitioning to lobby"),Se.clear(),X(),k("lobby-view");var E=document.getElementById("player-rematch-btn");E&&(E.disabled=!1,E.textContent="\u{1F501}");var b=Qe();b&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:b}))):(s.reconnectAttempts=0,Ae()))}else e.type==="left"?Oi():e.type==="steal_targets"?Kn(e):e.type==="steal_ack"?Xn(e):e.type==="artist_guess_ack"?Nt(e):e.type==="movie_guess_ack"?Dt(e):e.type==="title_artist_guess_ack"?Qn(e):e.type==="player_reaction"&&ea(e.player_name,e.emoji)}}function Oi(){Ye(),vt(),s.playerName=null,s.isAdmin=!1,k("join-view")}function Pi(){var e=s.isAdmin;Ye();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}pn(),Ln(),Se.clear(),X(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='

Thanks for playing!

Scan the QR code again to join the next game.

',n.classList.remove("hidden")),k("end-view")}}function Hi(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function on(e){var t=(e||"").trim();return t?t.length>xi?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function ka(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=on(e.value);a.valid&&(t.disabled=!0,t.textContent=re.t("game.joining"),n&&n.classList.add("hidden"),ge(a.name))}}function Di(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=on(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",ka),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&ka()}))}function Gi(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,k("loading-view"),ge(s.playerName)):pt()})}async function Ca(){try{await BeatifyAuth.init({requireAuth:!1})}catch{}var e=Ee.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await re.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ti();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");i&&(i.href=window.location.origin+"/beatify/dashboard"),Di(),_a(),kn(),An(),Nn(),fa(),ma(),va(),sa(),aa(),Gi(),gn(),yn(),bn(),$n();var r=new URLSearchParams(window.location.search),o=r.get("session"),l=null;try{l=sessionStorage.getItem("beatify_session")}catch{}var c=o||l;if(c){Ta(c);try{sessionStorage.removeItem("beatify_session")}catch{}}if(Mi()&&s.playerName){Qe()?Ae():ge(s.playerName);return}var d=Ci();if(d&&s.gameId){Y("[Beatify] Auto-reconnecting as:",d),ge(d);return}if(d){var u=document.getElementById("name-input"),f=document.getElementById("join-btn");if(u&&(u.value=d,f)){var m=on(d);f.disabled=!m.valid}}}pt();document.getElementById("refresh-btn")?.addEventListener("click",function(){k("loading-view"),pt()});document.getElementById("retry-btn")?.addEventListener("click",function(){k("loading-view"),pt()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",Ca):Ca();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/sw.js",{scope:"/beatify/"}).then(function(e){Y("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){s.playerName&&et();var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(Y("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,Ae())}}); diff --git a/custom_components/beatify/www/js/wizard.js b/custom_components/beatify/www/js/wizard.js index 0538cca5..24777dc9 100644 --- a/custom_components/beatify/www/js/wizard.js +++ b/custom_components/beatify/www/js/wizard.js @@ -232,6 +232,7 @@ let chosenArtistChallenge = true; let chosenMovieQuiz = true; let chosenIntroMode = false; let chosenClosestWins = false; +let chosenSuddenDeath = true; // Issue #827 — default ON (unlike the other bonuses); gated to >=3 players let chosenTitleArtistMode = false; // #1180 const chosenLevelUps = { lights: false, tts: false }; // Details the user sets when a level-up is toggled on @@ -917,8 +918,32 @@ const GAME_MODES = [ get: () => chosenClosestWins, set: (v) => { _setGameModeToggle('closest', v); }, }, + // Issue #827 — Sudden Death. Standalone toggle (not part of the + // year-round/TA precedence group), so it sets chosenSuddenDeath directly. + // Defaults ON, but is gated to >=3 connected players (see _renderGameModes). + { + key: 'suddenDeath', + icon: '💀', + titleKey: 'admin.suddenDeathMode', + titleFallback: 'Sudden Death', + hintKey: 'admin.suddenDeathModeHint', + hintFallback: 'When the timer runs out, the lowest-scoring player is eliminated. Last player standing wins. Requires at least 3 players.', + get: () => chosenSuddenDeath, + set: (v) => { chosenSuddenDeath = v; }, + }, ]; +// Issue #827 — Sudden Death needs at least 3 connected players to be playable. +// The wizard already fetches /beatify/api/status into cachedStatus; an active +// game's connected players live under active_game.players (built by +// build_status_response → game_state.get_state()). No game / no players ⇒ 0. +const SUDDEN_DEATH_MIN_PLAYERS = 3; +function _connectedPlayerCount() { + const game = cachedStatus && cachedStatus.active_game; + const players = game && Array.isArray(game.players) ? game.players : []; + return players.length; +} + // Core game mode — exactly one selected. Backed by the chosenTitleArtistMode // boolean (Jahr = false, Titel & Interpret = true). Clicking routes through the // tested precedence helper so T&I auto-clears the incompatible year-modifiers. @@ -980,6 +1005,11 @@ function _renderCoreMode() { function _renderGameModes() { const el = document.getElementById('wiz-modes'); if (!el) return; + // Issue #827 — Sudden Death is only playable with >=3 connected players. + // When below that, force the choice off so a <3-player game never starts in + // Sudden Death, and render the card disabled (dimmed, non-interactive). + const suddenDeathDisabled = _connectedPlayerCount() < SUDDEN_DEATH_MIN_PLAYERS; + if (suddenDeathDisabled) chosenSuddenDeath = false; el.innerHTML = GAME_MODES.map((m) => { const on = m.get(); // #1180: hide modes incompatible with Title & Artist mode (artist @@ -987,7 +1017,13 @@ function _renderGameModes() { if (chosenTitleArtistMode && (m.key === 'artist' || m.key === 'closest')) { return ''; } - return `
+ // Issue #827 — disabled Sudden Death card: dimmed, non-interactive, with + // a tooltip explaining the >=3 player requirement. + const disabled = m.key === 'suddenDeath' && suddenDeathDisabled; + const titleAttr = disabled + ? ` title="${escapeAttr(_t('admin.suddenDeathDisabledTooltip', 'Needs at least 3 players'))}"` + : ''; + return `
${_t(m.titleKey, m.titleFallback)}
@@ -1000,6 +1036,8 @@ function _renderGameModes() { card.addEventListener('click', () => { const mode = GAME_MODES.find((m) => m.key === card.dataset.mode); if (!mode) return; + // Issue #827 — the disabled Sudden Death card must not be flippable on. + if (mode.key === 'suddenDeath' && suddenDeathDisabled) return; mode.set(!mode.get()); _renderGameModes(); }); @@ -1481,7 +1519,7 @@ function _playlistName(id) { } // Merge wizard choices into beatify_game_settings so admin.js picks them up on load. -// Preserves existing keys (artistChallenge, introMode, closestWinsMode) the wizard doesn't touch. +// Preserves existing keys (artistChallenge, introMode, closestWinsMode, suddenDeathMode) the wizard doesn't touch. function _persistGameSettings() { try { const raw = localStorage.getItem(LS_GAME_SETTINGS); @@ -1497,6 +1535,7 @@ function _persistGameSettings() { movieQuiz: chosenMovieQuiz, introMode: chosenIntroMode, closestWinsMode: chosenClosestWins, + suddenDeathMode: chosenSuddenDeath, // Issue #827 titleArtistMode: chosenTitleArtistMode, // #1180 }; if (chosenPlaylists.size > 0) { @@ -1566,6 +1605,7 @@ export async function show(stepOverride) { if (typeof s.movieQuiz === 'boolean') chosenMovieQuiz = s.movieQuiz; if (typeof s.introMode === 'boolean') chosenIntroMode = s.introMode; if (typeof s.closestWinsMode === 'boolean') chosenClosestWins = s.closestWinsMode; + if (typeof s.suddenDeathMode === 'boolean') chosenSuddenDeath = s.suddenDeathMode; // Issue #827 if (typeof s.titleArtistMode === 'boolean') chosenTitleArtistMode = s.titleArtistMode; if (Array.isArray(s.selectedPlaylists)) { s.selectedPlaylists.forEach((entry) => { diff --git a/custom_components/beatify/www/player.html b/custom_components/beatify/www/player.html index 6b135179..b000c23f 100644 --- a/custom_components/beatify/www/player.html +++ b/custom_components/beatify/www/player.html @@ -385,6 +385,21 @@

+ + + ',r>0&&(l+='
'),e.innerHTML=l,e.scrollTop=o,B.observer){var d=e.querySelectorAll(".leaderboard-sentinel");d.forEach(function(u){B.observer.observe(u)})}}}function It(e){if(!e)return"";if(e.separator)return'
...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var c="";if(e.streak>=2){var d=e.streak>=5?"streak-indicator--hot":"";c='\u{1F525}'+e.streak+""}var u=e.connected===!1?"leaderboard-entry--disconnected":"",f=e.connected===!1?'(away)':"",m=e._displayScore!==void 0?e._displayScore:a;return'
#'+n+''+p(t)+f+''+m+"
"}function St(e,t){for(var n=Le,a=B.listEl&&B.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(c=Math.max(0,e.length-i-r),d=e.length):(c=Math.max(0,o-Math.floor(i/2)-r),d=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:c,end:d}}function pn(){B.observer&&(B.observer.disconnect(),B.observer=null),B.isLazyEnabled=!1,B.fullData=[]}function gn(){var e;function t(){clearTimeout(e),e=setTimeout(function(){B.isLazyEnabled&&B.fullData.length>0&&(B.visibleRange=St(B.fullData,s.playerName),He())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function yn(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function bn(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var hn={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},L={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function En(e){if(e){L.container=e;var t=!1;L.scrollHandler=function(){L.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){wt(),t=!1}),t=!0)};var n;L.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){L.isVirtual&&wt()},100)},e.addEventListener("scroll",L.scrollHandler,{passive:!0}),window.addEventListener("resize",L.resizeHandler)}}function wn(e,t){L.items=e,L.renderItem=t;var n=L.container;if(n){var a=n.scrollTop,i=L.isVirtual;e.length0&&(n.scrollTop=a,L.scrollTop=a)}}function nr(){var e=L.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",L.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",L.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",L.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function wt(){var e=hn,t=L.items,n=L.container,a=L.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=L.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,c=Math.max(0,Math.floor(r/o)-l),d=Math.min(t.length,Math.ceil((r+i)/o)+l);L.topSpacer&&(L.topSpacer.style.height=c*o+"px"),L.bottomSpacer&&(L.bottomSpacer.style.height=(t.length-d)*o+"px");for(var u="",f=c;f"u"){console.warn("[Confetti] Library not loaded");return}X();var t=Ee.getQualitySettings(),n=t.confettiParticles;if(n===0){un();return}var a=Ee.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function g(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(y,E){return y.connected!==E.connected?y.connected?-1:1:0}),c=Sn.map(function(y){return y.name}),d=l.filter(function(y){return c.indexOf(y.name)===-1}).map(function(y){return y.name});L.container||En(t);var u=["c1","c2","c3","c4"],f={},m=0;l.forEach(function(y){y.is_admin?f[y.name]="host":f[y.name]=u[m++%u.length]});var g=function(y){var E=d.indexOf(y.name)!==-1,b=y.name===s.playerName,h=y.is_admin===!0,w=y.connected===!1,x=f[y.name]||"c1",_=["player-tile","player-tile--"+x];E&&_.push("is-new"),w&&_.push("player-tile--disconnected");var S=(y.name||"?").trim(),G=(S.charAt(0)||"?").toUpperCase(),A="";h?A='':b&&(A='YOU');var M=w?'":"";return'
'+p(G)+''+p(S)+""+A+M+"
"};wn(l,g),setTimeout(function(){var y=L.isVirtual?L.contentWrapper:t;if(y)for(var E=y.querySelectorAll(".is-new"),b=0;b=1){K=e,On(Math.max(0,Math.ceil((e-Date.now())/1e3))),t&&t.classList.remove("timer-neon--catchup"),_e=null;return}var l=n+(e-n)*dr(o);K=l,On(Math.max(0,Math.ceil((l-Date.now())/1e3))),_e=requestAnimationFrame(i)}return _e=requestAnimationFrame(i),!0}var te=null,xt=null;function ur(e,t){if(!(!t||!e)){if(typeof IntersectionObserver>"u"){t.classList.remove("hidden"),t.classList.add("timer-float--visible");return}te&&xt===e||(te&&te.disconnect(),te=new IntersectionObserver(function(n){var a=n[0];a&&(a.isIntersecting?(t.classList.add("hidden"),t.classList.remove("timer-float--visible")):(t.classList.remove("hidden"),t.classList.add("timer-float--visible")))},{threshold:.1}),te.observe(e),xt=e)}}function fe(){Ge&&(clearInterval(Ge),Ge=null),Pn(),K=null;var e=document.getElementById("timer-neon");e&&e.classList.remove("timer-neon--catchup");var t=document.getElementById("timer-float");t&&(t.classList.add("hidden"),t.classList.remove("timer-float--visible","timer-float--warn")),te&&(te.disconnect(),te=null,xt=null)}var ae=window.BeatifyUtils||{},me=!1,ne=null,nt=null,fr=300,Hn=0;function Tt(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(u){return u.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(u,f){var m=document.createElement("button");m.className="artist-option-btn",m.dataset.artist=u,m.dataset.index=f,m.textContent=u,m.addEventListener("click",function(){mr(u)}),a.appendChild(m)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(u){u.classList.add("is-disabled"),u.classList.remove("is-loading","is-wrong");var f=e.correct_artist||nt;f&&u.dataset.artist===f&&u.classList.add("is-winner")}),e.winner===s.playerName){var c=e.bonus_points||5;i.textContent=(ae.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",c),i.className="artist-result is-winner"}else{var d=(ae.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=d,i.className="artist-result is-late"}i.classList.remove("hidden"),me=!0}else me||i.classList.add("hidden")}}function mr(e){var t=Date.now();if(!(t-Hn0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}Ot();var a=(Ve.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Pt(a,!0),xe=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),Ot(),Pt(Ve.t("movieChallenge.wrongGuess")||"Not quite...",!1),xe=!0;ke=null}function Ot(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Pt(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Gt(){xe=!1,ke=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function Vt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=Ve.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(c){var d=document.createElement("div");d.className="movie-reveal-winner-entry",c.name===t?d.classList.add("is-you"):d.classList.add("is-other"),d.textContent=c.name+" \u2014 +"+c.bonus+" ("+c.time+"s)",i.appendChild(d)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=Ve.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}var I=window.BeatifyUtils||{},gr=I.debug||function(){};function qn(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("closest-wins-badge");i&&(e.closest_wins_mode?i.classList.remove("hidden"):i.classList.add("hidden"));var r=document.getElementById("intro-badge"),o=document.getElementById("intro-splash");if(r)if(e.is_intro_round){r.classList.remove("hidden");var l=r.querySelector("[data-i18n]");e.intro_stopped?(r.classList.add("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=I.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=I.t("game.introRound")||"INTRO ROUND"),o&&!o._shown&&(o._shown=!0,o.classList.remove("hidden"),setTimeout(function(){o.classList.add("hidden")},2e3)))}else r.classList.add("hidden"),r.classList.remove("intro-badge--stopped"),o&&(o.classList.add("hidden"),o._shown=!1);var c=document.getElementById("album-cover"),d=document.getElementById("album-loading");if(c&&e.song){d&&d.classList.remove("hidden");var u=e.song.album_art||"/beatify/static/img/no-artwork.svg";c.onload=function(){d&&d.classList.add("hidden")},c.onerror=function(){c.src="/beatify/static/img/no-artwork.svg",d&&d.classList.add("hidden")},c.src=u}Er(e),qt(),br(e),wr(e.players),e.leaderboard&&Lr(e,"leaderboard-list"),Ar(e.players),e.artist_challenge!==void 0&&Tt(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Ht(e.movie_challenge,"PLAYING"),kr(e)}function Un(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}gr("[Metadata] Updated:",e.artist,"-",e.title)}}function yr(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function qt(){var e=document.getElementById("arc-chip-row");if(e){var t=["game-difficulty-badge","steal-indicator","closest-wins-badge","intro-badge","last-round-banner"],n=t.some(function(a){var i=document.getElementById(a);return i&&!i.classList.contains("hidden")});e.classList.toggle("hidden",!n)}}function br(e){var t=document.getElementById("no-bonus-filler");if(t){var n=!!(e&&e.artist_challenge&&e.artist_challenge.options),a=!!(e&&e.movie_challenge&&e.movie_challenge.options),i=!!(e&&e.title_artist_mode);t.classList.toggle("hidden",n||a||i)}}var Ce=!1;function hr(e){return!s.playerName||!e?null:e.find(function(t){return t.name===s.playerName})||null}function Er(e){var t=document.getElementById("eliminated-view");if(t){var n=!!(e&&e.sudden_death_mode),a=hr(e&&e.players),i=n&&!!(a&&a.eliminated);Ce=i;var r=[document.getElementById("year-selector-container"),document.getElementById("year-display-arc"),document.getElementById("bet-toggle"),document.getElementById("submit-btn"),document.getElementById("title-artist-container"),document.getElementById("submitted-banner")];if(i){r.forEach(function(f){f&&f.classList.add("hidden")}),t.classList.remove("hidden");var o=document.getElementById("album-cover"),l=document.getElementById("eliminated-album-cover");l&&o&&o.src&&(l.src=o.src);var c=document.getElementById("eliminated-sub");if(c){var d=a&&a.eliminated_round!=null?a.eliminated_round:e&&e.round||"";c.textContent=I.t("game.eliminatedRound",{round:d})||"Eliminated \xB7 Round "+d}}else{r.forEach(function(f){f&&f.classList.remove("hidden")}),t.classList.add("hidden");var u=document.getElementById("submitted-banner");u&&!D&&u.classList.add("hidden")}}}function wr(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players"),a=document.getElementById("arc-submission-count");if(!(!t||!n)){var i=e||[],r=i.filter(function(m){return!m.eliminated}),o=r.filter(function(m){return m.submitted}).length,l=r.length,c=o===l&&l>0;t.classList.toggle("all-submitted",c),a&&(l===0?a.textContent="":c?a.textContent=I.t("game.allSubmitted")||"All in":a.textContent=I.t("game.submittedCount",{count:o,total:l})||o+" of "+l+" submitted");var d=document.getElementById("submitted-banner"),u=document.getElementById("submitted-banner-text");if(d&&u&&!d.classList.contains("hidden")){var f=Math.max(0,l-o);f===0?u.textContent=I.t("game.lockedInAllSubmitted")||"Locked in \xB7 everyone submitted":u.textContent=I.t("game.lockedInWaitingCount",{count:f})||"Locked in \xB7 waiting for "+f+" more"}n.innerHTML=i.map(function(m){var g=yr(m.name),y=m.name===s.playerName,E=m.connected===!1,b=!!m.eliminated,h=["player-indicator",m.submitted&&!b?"is-submitted":"",y?"is-current-player":"",E?"player-indicator--disconnected":"",b?"is-eliminated":""].filter(Boolean).join(" "),w="";if(b){var x=m.eliminated_round!=null?m.eliminated_round:"",_=I.t("game.outRound",{round:x})||"Out \xB7 R"+x;w+=''+p(_)+""}else m.steal_used&&(w+='\u{1F977}'),m.bet&&(w+='\u{1F3B2}');var S=b?'':''+p(g)+"";return'
'+w+'
'+S+'
'+p(m.name)+"
"}).join("")}}function Lr(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&&fn(),o=r?mn(a):{};a.forEach(function(f){f.is_current=f.name===s.playerName;var m=o[f.name];m&&(f._rankChange=m);var g=Q.players[f.name],y=g?g.score:f.score;f._prevScore=y,f._displayScore=n?y:f.score});var l=Ir(a,s.playerName),c=a.length>=Le.MIN_PLAYERS_FOR_LAZY;if(c)B.observer||vn(i),B.fullData=l,B.isLazyEnabled=!0,B.listEl=i,B.visibleRange=St(l,s.playerName),He();else{B.isLazyEnabled=!1;var d="";l.forEach(function(f){d+=It(f)}),i.innerHTML=d}var u=[];r&&l.forEach(function(f){!f.separator&&f._prevScore!==f.score&&u.push({name:f.name,prevScore:f._prevScore,newScore:f.score})}),r&&u.length>0&&requestAnimationFrame(function(){for(var f={},m=i.querySelectorAll(".leaderboard-entry[data-name]"),g=0;g8&&Sr(i),Br(a),_r(a),$e(e.players||[],a)}}function Ir(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Sr(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Br(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=I.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function jn(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function _r(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function zn(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var D=!1,We=!1,Fe=!1,Wt=!1,Gn=!1,Vn=!1;function Yn(){if(Vn)return;var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!e||!t)return;Vn=!0,e.addEventListener("input",function(){Ce||(t.textContent=this.value)});function n(m){var g=parseInt(e.value,10)+m;g=Math.max(parseInt(e.min,10),Math.min(parseInt(e.max,10),g)),e.value=g,t.textContent=g}function a(m,g){if(!m)return;var y=null,E=null;m.addEventListener("pointerdown",function(h){D||Ce||(h.preventDefault(),n(g),E=setTimeout(function(){y=setInterval(function(){n(g)},150)},500))});function b(){E&&(clearTimeout(E),E=null),y&&(clearInterval(y),y=null)}["pointerup","pointerleave","pointercancel"].forEach(function(h){m.addEventListener(h,b)}),m.addEventListener("keydown",function(h){D||Ce||(h.key==="Enter"||h.key===" ")&&(h.preventDefault(),n(g))})}a(document.getElementById("year-decrement"),-1),a(document.getElementById("year-increment"),1),a(document.getElementById("year-decrement-5"),-5),a(document.getElementById("year-increment-5"),5);var i=document.getElementById("bet-toggle");i&&i.addEventListener("click",function(){D||(We=!We,i.classList.toggle("is-active",We))});var r=document.getElementById("submit-btn");if(r&&r.addEventListener("click",function(){Wt?Wn():xr()}),!Gn){var o=document.getElementById("ta-title-input"),l=document.getElementById("ta-artist-input");o&&o.addEventListener("keydown",function(m){m.key==="Enter"&&(m.preventDefault(),l&&l.focus())}),l&&l.addEventListener("keydown",function(m){m.key==="Enter"&&(m.preventDefault(),Wt&&Wn())}),Gn=!0}var c=document.getElementById("steal-btn");c&&c.addEventListener("click",Tr);var d=document.getElementById("steal-modal-close");d&&d.addEventListener("click",Ft);var u=document.getElementById("steal-modal");if(u){var f=u.querySelector(".steal-modal-backdrop");f&&f.addEventListener("click",Ft)}}function xr(){if(!D&&!Ce){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:We})):(rt(I.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function it(){D=!0;var e=document.getElementById("year-selector"),t=document.getElementById("year-display-arc"),n=document.getElementById("submit-btn"),a=document.getElementById("bet-toggle"),i=document.getElementById("submitted-banner");e&&e.classList.add("is-submitted","slider-arcade--locked"),t&&t.classList.add("year-xxl--locked"),n&&(n.disabled=!0,n.classList.add("submit-arc--waiting"),n.innerHTML=""+p(I.t("game.waitingForOthers")||"Waiting for others")+''),a&&(a.disabled=!0),i&&i.classList.remove("hidden"),["year-decrement","year-increment","year-decrement-5","year-increment-5"].forEach(function(r){var o=document.getElementById(r);o&&(o.disabled=!0)})}function Ut(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(rt(I.t("errors.timesUp")),D=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?it():rt(e.message||"Submission failed")}function rt(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=I.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Jn(){D=!1,We=!1;var e=document.getElementById("year-selector"),t=document.getElementById("year-display-arc"),n=document.getElementById("submit-btn"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle"),r=document.getElementById("submitted-banner");if(e&&e.classList.remove("is-submitted","slider-arcade--locked"),t&&t.classList.remove("year-xxl--locked"),n&&(n.disabled=!1,n.classList.remove("hidden","is-loading","is-error","submit-arc--waiting"),n.textContent=I.t("game.submitGuess")),i&&(i.disabled=!1,i.classList.remove("hidden","is-active")),r&&r.classList.add("hidden"),a){a.value=1990;var o=document.getElementById("selected-year");o&&(o.textContent="1990")}["year-decrement","year-increment","year-decrement-5","year-increment-5"].forEach(function(l){var c=document.getElementById(l);c&&(c.disabled=!1)}),Fe=!1,jt(),Mt(),Gt(),Cr()}function kr(e){var t=!!(e&&e.title_artist_mode);Wt=t;var n=document.getElementById("title-artist-container"),a=document.getElementById("year-selector-container"),i=document.getElementById("year-display-arc"),r=document.getElementById("bet-toggle");if(n&&n.classList.toggle("hidden",!t),a&&a.classList.toggle("hidden",t),i&&i.classList.toggle("hidden",t),r&&r.classList.toggle("hidden",t),!!t){var o=document.getElementById("submit-btn");o&&!D&&(o.textContent=I.t("titleArtist.submitGuess")||"Submit")}}function Wn(){if(!D&&!Ce){var e=document.getElementById("ta-title-input"),t=document.getElementById("ta-artist-input"),n=document.getElementById("submit-btn");if(!(!e||!t||!n)){var a=(e.value||"").trim(),i=(t.value||"").trim();n.disabled=!0,n.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"title_artist_guess",title:a,artist:i})):(rt(I.t("errors.connectionLost")),n.disabled=!1,n.classList.remove("is-loading"))}}}function Qn(e){it();var t=document.getElementById("ta-title-input"),n=document.getElementById("ta-artist-input");t&&(t.disabled=!0),n&&(n.disabled=!0);var a=document.getElementById("ta-input-ack");a&&(a.textContent=I.t("titleArtist.submitted")||"Submitted \u2014 see how you did at the reveal!",a.classList.remove("hidden"))}function Cr(){var e=document.getElementById("ta-title-input"),t=document.getElementById("ta-artist-input"),n=document.getElementById("ta-input-ack");e&&(e.value="",e.disabled=!1),t&&(t.value="",t.disabled=!1),n&&(n.textContent="",n.classList.add("hidden"))}function Ar(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){Fe=t.steal_available&&!D;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");Fe?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):jt(),qt()}}}function jt(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden"),qt()}function Tr(){!Fe||D||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function Nr(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=I.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){Mr(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function Ft(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function Mr(e){var t=I.t("steal.confirm").replace("{name}",e),n=await Ie(I.t("steal.confirmTitle")||"Steal Answer?",t,I.t("steal.confirmButton")||"Steal",I.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),Ft())}function Xn(e){if(e.success){Fe=!1,D=!0,jt();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),Rr(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Kn(e){Nr(e.targets||[])}function Rr(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=I.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fn=0,Or=500,qe=!1,zt=.5;function st(){var e=Date.now();return e-Fn=1){ta("max");return}st()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function Gr(){if(zt<=0){ta("min");return}st()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function ta(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function Vr(){var e=await Ie(I.t("admin.endGameConfirm")||"End Game?",I.t("admin.endGameWarning")||"All players will be disconnected.",I.t("admin.endGame")||"End Game",I.t("common.cancel"));if(e&&st()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(I.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var at=!1;function na(){if(!at&&s.ws&&s.ws.readyState===WebSocket.OPEN){at=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=I.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){at&&Jt()},1e4)}}function Jt(){at=!1;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!1,e.textContent=I.t("admin.nextRound")),t){t.disabled=!1;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("admin.nextRound"))}}function Wr(){na()}function aa(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",Hr),t&&t.addEventListener("click",Dr),n&&n.addEventListener("click",Gr),a&&a.addEventListener("click",Wr),i&&i.addEventListener("click",Vr)}function ra(){qe=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=I.t("game.stopped"))}}function Qt(){qe=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=I.t("game.stop"))}}function ia(e){zt=e,Fr(e),qr(e)}function Fr(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function qr(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function sa(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",na);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||(Se.isRunning()&&Se.skipAll(),X())})}function oa(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function la(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var v=window.BeatifyUtils||{},da=30,lt=null;function Kt(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("reveal-idle-halt");r&&r.classList.toggle("hidden",!e.idle_halt),Ur(e);var o=document.getElementById("closest-wins-badge");o&&(e.closest_wins_mode?o.classList.remove("hidden"):o.classList.add("hidden"));var l=document.getElementById("intro-badge");if(l)if(e.is_intro_round){l.classList.remove("hidden"),l.classList.add("intro-badge--stopped");var c=l.querySelector("[data-i18n]");c&&(c.setAttribute("data-i18n","game.introStopped"),c.textContent=v.t("game.introStopped")||"Intro complete!")}else l.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.onerror=function(){d.src="/beatify/static/img/no-artwork.svg"},d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("reveal-backdrop");if(u){var f=t.album_art;if(f){var m=new Image;m.onload=function(){u.style.backgroundImage='url("'+f+'")',u.classList.remove("reveal-backdrop--synthetic")},m.onerror=function(){u.style.backgroundImage="",u.classList.add("reveal-backdrop--synthetic")},m.src=f}else u.style.backgroundImage="",u.classList.add("reveal-backdrop--synthetic")}var g=document.getElementById("correct-year");g&&(g.textContent=t.year||"????");var y=document.getElementById("song-title"),E=document.getElementById("song-artist");y&&(y.textContent=t.title||"Unknown Song"),E&&(E.textContent=t.artist||"Unknown Artist");var b=document.getElementById("fun-fact-container"),h=document.getElementById("fun-fact"),w=b?b.querySelector(".fun-fact-header"):null,x=v.getLocalizedSongField(t,"fun_fact");if(h&&(h.textContent=x||""),w&&(w.style.display=x?"flex":"none"),Yr(t),zr(e.song_difficulty),b){var _=document.getElementById("song-rich-info"),S=_&&_.innerHTML.trim()!=="",G=x&&x.trim()!=="";b.classList.toggle("hidden",!G&&!S)}for(var A=null,M=0;M'+v.t("analytics.noGuesses")+"
";var a=e.map(function(V){return V.guess}),i=Math.min.apply(null,a.concat([t])),r=Math.max.apply(null,a.concat([t])),o=Math.max(2,Math.floor((r-i)*.1)),l=i-o,c=r+o,d=Math.max(1,c-l);function u(V){return(V-l)/d*100}for(var f=u(t),m='
'+t+"
",g="",y=0;y<=4;y++){var E=Math.round(l+d*y/4),b=y*25;g+='
'+E+"
"}function h(V){for(var T=0,O=0;O>>0;return"c"+(T%4+1)}for(var w="",x=0;x0?"dotaxis-score--pos":"dotaxis-score--zero",R=H>0?"+"+H:"+0";w+='
'+G+'
'+R+"
"}for(var ie=e.slice().sort(function(V,T){return(V.years_off||0)-(T.years_off||0)}),Te=["\u{1F3C6}","\u{1F948}","\u{1F949}"],Ne="",se=0;se'+Te[se]+"":"",ye=n&&J.name===n?' '+v.t("analytics.youMarker")+"":"";Ne+=''+oe+''+p(J.name||"?")+ye+""}return'
'+g+'
'+m+w+'
'+Ne+"
"}function zr(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML='
'+n+'
'+v.t("difficulty."+e.label)+''+e.accuracy+"% "+v.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function Yr(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=Jr(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=Qr(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=v.getLocalizedSongField(e,"awards")||[],o=Zr(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML='
'+n.join("")+"
":t.innerHTML=""}}function Jr(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+v.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+v.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+v.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+v.t("reveal.chartUK")+""),t}function Qr(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+p(a)+"")}return t}function Xr(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Kr(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function Zr(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+p(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function $r(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function ei(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function ti(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("duel-emotion"),r=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"duel-emotion":r?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),X();var o=v.t("reveal.emotions");function l(y){return y[Math.floor(Math.random()*y.length)]}function c(y){return y===1?v.t("reveal.offByYear"):v.t("reveal.offByYears",{years:y})}var d="missed",u=l(o.missed),f=l(o.missedSub);if(e&&!e.missed_round){var m=e.years_off||0;m===0?(d="exact",u=l(o.exact),f=l(o.exactSub)):m<=2?(d="close",u=l(o.close),f=l(o.closeSub)+" "+c(m)):m<=5?(d="close",u=l(o.close),f=c(m)):(d="wrong",u=l(o.wrong),f=l(o.wrongSub)+" "+c(m))}else e&&e.missed_round&&(d="missed",u=l(o.missed),f=l(o.missedSub));if(i)n.textContent=u,n.classList.add("duel-emotion--"+d);else{var g=''+u+"";f&&(g+='
'+f+"
"),n.innerHTML=g,n.classList.add("reveal-emotion--"+d)}n.classList.remove("hidden"),d==="exact"&&Be(),a&&d!=="missed"&&a.classList.add("is-delayed")}function ni(e,t){var n=document.getElementById("duel-your-year"),a=document.getElementById("duel-gap-count"),i=document.getElementById("duel-gap-unit");if(!(!n||!a||!i)){if(!e||e.missed_round){n.textContent=v.t("reveal.duel.noGuess")||"\u2014",a.textContent="\u2014",i.textContent="";return}var r=e.guess;n.textContent=r!=null&&r!==""?r:"\u2014";var o=e.years_off!=null?e.years_off:0;a.textContent=String(o),i.textContent=o===1?v.t("reveal.duel.yearUnit")||"year":v.t("reveal.duel.yearsUnit")||"years";var l=a.closest(".duel-gap");l&&(l.classList.remove("duel-gap--exact","duel-gap--close","duel-gap--wrong"),o===0?l.classList.add("duel-gap--exact"):o<=5?l.classList.add("duel-gap--close"):l.classList.add("duel-gap--wrong"))}}function ai(e,t){var n=document.getElementById("reveal-chip-row");if(n){if(!e){n.classList.add("hidden"),n.innerHTML="";return}var a=[];e.bet_outcome==="won"?a.push('\u{1F3B2} '+p(v.t("reveal.chip.betWon")||"Bet won \xB7 \xD72")+""):e.bet_outcome==="lost"&&a.push('\u{1F3B2} '+p(v.t("reveal.chip.betLost")||"Bet lost")+"");var i=e.streak_bonus||0;if(i>0&&e.streak){var r=v.t("reveal.chip.streakBonus",{count:e.streak,bonus:i})||e.streak+"-streak \xB7 +"+i;a.push('\u{1F525} '+p(r)+"")}a.length===0?(n.classList.add("hidden"),n.innerHTML=""):(n.classList.remove("hidden"),n.innerHTML=a.join(""))}}function Zt(e){if(!e)return 0;var t=e.round_score||0,n=e.streak_bonus||0,a=e.artist_bonus||0,i=e.movie_bonus||0,r=e.intro_bonus||0;return t+n+a+i+r}function ri(e){var t=document.getElementById("reveal-total-pts"),n=document.getElementById("score-row-subtitle");if(t){var a=Zt(e);if(t.textContent=(a>=0?"+":"")+a,n)if(!e||e.missed_round)n.textContent=v.t("reveal.noSubmission")||"No guess submitted";else{var i=e.years_off!=null?e.years_off:0,r=i===0?"reveal.exact":i===1?"reveal.yearOff":"reveal.yearsOff";n.textContent=v.t(r,{years:i})||i+" years off"}}}function ii(e){for(var t=[["#ff2d6a","#ff6600"],["#00f5ff","#7a5cff"],["#39ff14","#00f5ff"],["#ff6600","#ff0040"],["#7a5cff","#b3b3c2"],["#ff2d6a","#7a5cff"]],n=0,a=0;a>>0;var i=t[n%t.length];return"linear-gradient(135deg,"+i[0]+","+i[1]+")"}function si(e){var t=document.getElementById("reveal-leaderboard-list");if(t){var n=e.leaderboard||[],a=e.players||[],i={};a.forEach(function(l){i[l.name]=l});var r=(v.t("analytics.youMarker")||"YOU").replace(/[()]/g,""),o="";n.forEach(function(l,c){var d=i[l.name]||{},u=l.name===s.playerName,f=((l.name||"?").trim().charAt(0)||"?").toUpperCase(),m=l.rank_change||0,g=m>0?"up":m<0?"down":"flat",y=m>0?"\u25B2"+m:m<0?"\u25BC"+Math.abs(m):"\u2013",E="";if(!d.missed_round&&d.years_off!=null&&d.guess!=null&&d.guess!==""){var b=d.years_off,h=b===0?''+p(v.t("reveal.exact")||"Exact!")+"":p(v.t("reveal.shortOff",{years:b})||b+" off");E='
'+p(String(d.guess))+" \xB7 "+h+"
"}var w=[];d.streak&&d.streak>=2&&w.push('\u{1F525} '+d.streak+""),d.stole_from?w.push('\u{1F977} '+p(v.t("steal.stolenFrom",{name:d.stole_from})||"stole "+d.stole_from)+""):d.was_stolen_by&&d.was_stolen_by.length&&w.push('\u{1F3AF} '+p(v.t("steal.stolenBy",{name:d.was_stolen_by.join(", ")})||"stolen")+""),d.bet_outcome==="won"?w.push('\u{1F3B2} '+p(v.t("reveal.chip.betWon")||"Bet won")+""):d.bet_outcome==="lost"&&w.push('\u{1F3B2} '+p(v.t("reveal.chip.betLost")||"Bet lost")+"");var x=w.length?'
'+w.join("")+"
":"",_=Zt(d),S=_>0?"":" rstand-delta--zero",G=(_>=0?"+":"")+_,A=c>=4?" rstand-row--taper2":c>=3?" rstand-row--taper1":"",M=u?" rstand-row--you":"",P=u?' '+p(r)+"":"";o+='
'+l.rank+''+y+'
'+p(f)+'
'+p(l.name||"?")+P+"
"+E+x+'
'+(l.score||0)+''+G+"
"}),t.innerHTML=o,$e(a,n)}}function oi(){var e=document.getElementById("points-breakdown-content");if(e){var t=s.lastRevealContext,n=t?t.player:null;if(!n||n.missed_round){e.innerHTML='
'+p(v.t("reveal.breakdown.noSubmission")||v.t("reveal.noSubmission")||"No guess submitted")+'
'+p(v.t("reveal.breakdown.total")||"Total this round")+'+0
';return}var a=[],i=n.years_off!=null?n.years_off:0,r=n.base_score||0,o=n.round_score||0,l=n.speed_multiplier||1,c=Math.floor(r*l)-r;a.push({emoji:"\u{1F3AF}",label:v.t("reveal.breakdown.baseScore",{years:i})||"Base score",value:String(r),kind:"neutral"}),l>1&&c>0&&a.push({emoji:"\u26A1",label:(v.t("reveal.breakdown.speedBonus")||"Speed bonus")+" ("+l.toFixed(2)+"\xD7)",value:"+"+c,kind:"positive"}),n.streak_bonus&&n.streak_bonus>0&&a.push({emoji:"\u{1F525}",label:v.t("reveal.breakdown.streakBonus",{count:n.streak})||n.streak+"-streak bonus",value:"+"+n.streak_bonus,kind:"positive"}),n.artist_bonus&&n.artist_bonus>0&&a.push({emoji:"\u{1F3A4}",label:v.t("reveal.breakdown.artistBonus")||"Artist challenge",value:"+"+n.artist_bonus,kind:"positive"}),n.movie_bonus&&n.movie_bonus>0&&a.push({emoji:"\u{1F3AC}",label:v.t("reveal.breakdown.movieBonus")||"Movie challenge",value:"+"+n.movie_bonus,kind:"positive"}),n.intro_bonus&&n.intro_bonus>0&&a.push({emoji:"\u26A1",label:v.t("reveal.breakdown.introBonus")||"Intro speed bonus",value:"+"+n.intro_bonus,kind:"positive"}),n.bet_outcome==="won"?a.push({emoji:"\u{1F3B2}",label:v.t("reveal.breakdown.betMultiplier")||"Double or Nothing",value:"\xD72",kind:"multiplier"}):n.bet_outcome==="lost"&&a.push({emoji:"\u{1F3B2}",label:v.t("reveal.breakdown.betLost")||"Bet lost",value:"\xD70",kind:"multiplier"});var d=Zt(n),u='
';a.forEach(function(f){u+='
"+p(f.label)+''+p(f.value)+"
"}),u+="
",u+='
'+p(v.t("reveal.breakdown.total")||"Total this round")+''+(d>=0?"+":"")+d+"
",e.innerHTML=u}}function li(){var e=document.getElementById("round-stats-content");if(e){var t=s.lastRevealContext;if(!t){e.innerHTML="";return}var n=t.analytics,a=t.difficulty,i=t.song||{},r=i.year,o=[];if(a){for(var l="",c=5,d=0;d\u2605';var u=v.t("difficulty."+a.label)||a.label||"",f=a.accuracy!=null?v.t("reveal.stats.onlyPercent",{percent:a.accuracy})||"Only "+a.accuracy+"% of all players guess it right.":"";o.push('
'+p(u)+(f?'
'+p(f)+"
":"")+'
'+l+"
")}if(n){var m=[];if(n.average_guess!=null){var g=r?Math.round(n.average_guess-r):null,y=g!=null?g===0?v.t("analytics.onTarget")||"On target":Math.abs(g)+" "+(v.t("reveal.duel.yearsUnit")||"years")+" "+(g>0?"late":"early"):"";m.push('
'+p(v.t("reveal.stats.avgGuess")||"Avg guess")+'
'+Math.round(n.average_guess)+"
"+(y?'
'+p(y)+"
":"")+"
")}if(n.all_guesses&&n.all_guesses.length>0){var E=n.all_guesses[0],b=E.name+" \xB7 "+(E.years_off===0?v.t("reveal.exact")||"Exact!":E.years_off+" "+(E.years_off===1?v.t("reveal.duel.yearUnit")||"year":v.t("reveal.duel.yearsUnit")||"years")+" off");m.push('
'+p(v.t("reveal.stats.closest")||"Closest")+'
'+p(String(E.guess))+'
'+p(b)+"
")}if(n.speed_champion&&n.speed_champion.time!=null&&m.push('
'+p(v.t("reveal.stats.fastest")||"Fastest")+'
'+n.speed_champion.time+'s
'+p((n.speed_champion.names||[]).join(", "))+"
"),a&&a.times_played!=null){var h=v.t("reveal.stats.playedBeforeSub")||"across all Beatify games";m.push('
'+p(v.t("reveal.stats.playedBefore")||"Played before")+'
'+a.times_played+'\xD7
'+p(h)+"
")}m.length>0&&o.push('
'+m.join("")+"
")}if(n&&n.all_guesses&&n.all_guesses.length>0&&o.push('
'+p(v.t("analytics.guessAxis")||"Where everyone guessed")+"
"+jr(n.all_guesses,r,s.playerName)+"
"),n&&n.furthest_players&&n.furthest_players.length>0&&n.all_guesses&&n.all_guesses.length>0){var w=n.all_guesses[n.all_guesses.length-1];if(w&&w.years_off>0){var x=n.furthest_players.map(function(_){return'
'+p(_)+''+w.years_off+" "+(w.years_off===1?v.t("reveal.duel.yearUnit")||"yr":v.t("reveal.duel.yearsUnit")||"yrs")+" off
"}).join("");o.push('
'+p(v.t("reveal.stats.furthestOff")||"Furthest off this round")+'
'+x+"
")}}o.length===0&&o.push('

'+p(v.t("reveal.stats.empty")||"No stats for this round yet.")+"

"),e.innerHTML=o.join("")}}function ca(e,t){var n=document.getElementById(e);if(n){typeof t=="function"&&t(),n.classList.remove("hidden");var a=n.querySelector(".sheet-close");a&&a.focus()}}function Xt(e){var t=document.getElementById(e);t&&t.classList.add("hidden")}function fa(){var e=document.getElementById("points-breakdown-btn");e&&e.addEventListener("click",function(){ca("points-breakdown-sheet",oi)});var t=document.getElementById("round-stats-btn");t&&t.addEventListener("click",function(){ca("round-stats-sheet",li)}),document.querySelectorAll("[data-sheet-close]").forEach(function(n){n.addEventListener("click",function(a){Xt(n.getAttribute("data-sheet-close")),a.stopPropagation()})}),document.querySelectorAll(".sheet-backdrop").forEach(function(n){var a=n.querySelector(".sheet-dim");a&&a.addEventListener("click",function(){Xt(n.id)})}),document.addEventListener("keydown",function(n){n.key==="Escape"&&["points-breakdown-sheet","round-stats-sheet"].forEach(function(a){var i=document.getElementById(a);i&&!i.classList.contains("hidden")&&Xt(a)})})}function di(){var e=document.getElementById("reveal-report-btn");e&&(e.textContent=v.t("reveal.reportBtn")||"\u{1F6A9} Wrong year?",e.disabled=!1)}function ma(){var e=document.getElementById("reveal-report-btn");e&&e.addEventListener("click",function(){var t=s.lastRevealContext;!t||!t.song||!s.ws||s.ws.readyState!==WebSocket.OPEN||(s.ws.send(JSON.stringify({type:"report_data",artist:t.song.artist||"",title:t.song.title||"",year:t.song.year||null})),e.textContent=v.t("reveal.reportBtnDone")||"\u2713 Reported \u2014 thanks!",e.disabled=!0)})}function ua(e){var t="ta-pill ta-pill--"+(e||"skipped").replace(/_/g,"-"),n;switch(e){case"exact":n=v.t("titleArtist.statusExact")||"Correct";break;case"fuzzy":n=v.t("titleArtist.statusFuzzy")||"Close enough";break;case"near_miss_accepted":n=v.t("titleArtist.statusAccepted")||"Accepted";break;case"near_miss":n=v.t("titleArtist.statusNearMiss")||"Near miss";break;case"wrong":n=v.t("titleArtist.statusWrong")||"Wrong";break;default:n=v.t("titleArtist.statusSkipped")||"Skipped"}return''+p(n)+""}function ci(e,t){if(!e)return null;var n={exact:1,fuzzy:1,near_miss_accepted:1},a=e.title_status,i=e.artist_status;if(t&&(a==="near_miss"||i==="near_miss"))return{tier:"pending",text:v.t("titleArtist.verdictPending")||"Awaiting the room\u2019s verdict\u2026"};var r=(n[a]?1:0)+(n[i]?1:0);return r===2?{tier:"win",text:v.t("titleArtist.verdictWin")||"Nailed it!"}:r===1?{tier:"partial",text:v.t("titleArtist.verdictPartial")||"Got one!"}:{tier:"miss",text:v.t("titleArtist.verdictMiss")||"Not this time"}}function ui(e,t){var n=!!e.accepted,a=((e.player||"?").trim().charAt(0)||"?").toUpperCase(),i=n?"\u2713 +"+(e.points||0):"\u2717";return'
'+p(e.player)+' \xB7 '+p(t(e.field))+'
\u201C'+p(e.guess||"\u2014")+'\u201D
\u{1F44D} '+(e.votes_yes||0)+" \xB7 \u{1F44E} "+(e.votes_no||0)+'
'+i+"
"}function fi(e,t){var n=document.getElementById("ta-reveal-section");if(n){if(!e||!e.correct_title){n.classList.add("hidden"),ve();return}n.classList.remove("hidden"),s._taRevealTruth!==e.correct_title&&(s._taRevealTruth=e.correct_title,s.taMyVotes={}),s.taMyVotes=s.taMyVotes||{};var a=document.getElementById("ta-reveal-truth");a&&(a.innerHTML=''+p(e.correct_title)+''+p(e.correct_artist||"")+"");for(var i=document.getElementById("ta-reveal-own"),r=e.results||[],o=null,l=0;l'+p(c.text)+"
":"";i.innerHTML=d+'
'+p(v.t("titleArtist.yourTitle")||"Your title")+''+p(o.title||"\u2014")+""+ua(o.title_status)+'
'+p(v.t("titleArtist.yourArtist")||"Your artist")+''+p(o.artist||"\u2014")+""+ua(o.artist_status)+"
"}else i.innerHTML='
'+p(v.t("titleArtist.noGuess")||"No guess this round")+"
";var u=document.getElementById("ta-voting"),f=document.getElementById("ta-voting-cards"),m=document.getElementById("ta-voting-title"),g=document.getElementById("ta-voting-countdown"),y=e.near_misses||[],E=e.near_miss_outcomes||[],b=!!e.voting_open,h=!!(t&&t.is_admin);if(!u||!f){ve();return}var w=function(S){return S==="artist"?v.t("titleArtist.artistLabel")||"Artist":v.t("titleArtist.titleLabel")||"Song title"};if(!b&&E.length>0){ve(),u.classList.remove("hidden"),m&&(m.textContent=v.t("titleArtist.closeCallsDecided")||"Close calls \u2014 decided"),g&&(g.textContent="",g.classList.add("hidden")),f.innerHTML=E.map(function(S){return ui(S,w)}).join("");return}if(y.length===0){u.classList.add("hidden"),f.innerHTML="",ve();return}u.classList.remove("hidden"),m&&(m.textContent=v.t("titleArtist.voteHeader")||"Close calls \u2014 vote \u{1F44D}/\u{1F44E}"),g&&g.classList.remove("hidden");var x="";if(y.forEach(function(S){var G=S.player===s.playerName,A=S.votes_yes||0,M=S.votes_no||0,P=A+M,F=P?Math.round(A/P*100):0,H=P?100-F:0,q=((S.player||"?").trim().charAt(0)||"?").toUpperCase();if(x+='
'+p(S.player)+''+p(w(S.field))+'
\u201C'+p(S.guess||"\u2014")+'\u201D
\u{1F44D} '+A+'
\u{1F44E} '+M+'
',G)x+='
'+p(v.t("titleArtist.yourCloseCall")||"Your close call \u2014 others decide")+"
";else if(b){var R=s.taMyVotes[S.id],ie=R===!0||R===!1;x+='
',ie&&(x+='
'+p(v.t("titleArtist.youVoted")||"You voted")+" "+(R?"\u{1F44D}":"\u{1F44E}")+" \xB7 "+p(v.t("titleArtist.tapToChange")||"tap to change")+"
")}h&&b&&(x+='
'+p(v.t("titleArtist.hostOverride")||"Host decides")+'
'),x+="
"}),f.innerHTML=x,b)mi(e);else{ve();var _=document.getElementById("ta-voting-countdown");_&&(_.textContent=v.t("titleArtist.voteClosed")||"Voting closed",_.removeAttribute("aria-label"),_.classList.add("ta-voting-countdown--closed"))}}}function mi(e){ve();var t=document.getElementById("ta-voting-countdown");if(!t)return;var n=e&&typeof e.vote_seconds_remaining=="number"?e.vote_seconds_remaining:da,a=Date.now()+n*1e3;function i(){var r=Math.max(0,Math.ceil((a-Date.now())/1e3));t.textContent=String(r),t.setAttribute("aria-label",v.t("titleArtist.voteCountdown",{seconds:r})||r+"s"),t.style.setProperty("--ta-vote-progress",r/da*360+"deg"),t.classList.remove("ta-voting-countdown--closed"),r<=0&&ve()}i(),lt=setInterval(i,500)}function ve(){lt&&(clearInterval(lt),lt=null)}function va(){var e=document.getElementById("ta-voting-cards");e&&e.addEventListener("click",function(t){var n=t.target.closest(".ta-vote-btn"),a=t.target.closest(".ta-override-btn");if(n){var i=n.getAttribute("data-nearmiss-id"),r=n.getAttribute("data-accept")==="1";s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"title_artist_vote",nearmiss_id:i,accept:r})),s.taMyVotes=s.taMyVotes||{},s.taMyVotes[i]=r;var o=n.closest(".ta-vote-card");if(o){var l=o.querySelector(".ta-vote-actions");l&&l.classList.add("ta-vote-actions--voted"),o.querySelectorAll(".ta-vote-btn").forEach(function(f){f.classList.remove("is-chosen")}),n.classList.add("is-chosen")}return}if(a){var c=a.getAttribute("data-nearmiss-id"),d=a.getAttribute("data-accept")==="1";s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"title_artist_override",nearmiss_id:c,accept:d}));var u=a.closest(".ta-vote-card");u&&(u.querySelectorAll(".ta-override-btn").forEach(function(f){f.classList.remove("is-chosen")}),a.classList.add("is-chosen"))}})}var N=window.BeatifyUtils||{};function ga(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var h=t.find(function(S){return S.rank===b}),w=document.querySelector(".podium-place.podium-"+b);w&&w.classList.toggle("hidden",!h);var x=document.getElementById("podium-"+b+"-name"),_=document.getElementById("podium-"+b+"-score");x&&(x.textContent=h?p(h.name):"---"),_&&(_.textContent=h?h.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+N.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var c=document.getElementById("final-leaderboard-list");c&&(c.innerHTML=t.map(function(b){var h=b.is_current?"is-current":"",w=b.connected===!1?"final-entry--disconnected":"",x=b.connected===!1?'(away)':"";return'
#'+b.rank+''+p(b.name)+x+''+b.score+"
"}).join("")),vi(e.superlatives),pi(e.highlights),gi(e.share_data);var d=document.getElementById("end-admin-controls"),u=document.getElementById("end-player-message");if(n&&n.is_admin){d&&d.classList.remove("hidden"),u&&u.classList.add("hidden");var f=document.getElementById("new-game-btn");f&&(f.onclick=hi);var m=document.getElementById("player-rematch-btn");m&&(m.onclick=function(){m.disabled=!0;var b=m.textContent;if(m.textContent="\u23F3",s.ws&&s.ws.readyState===WebSocket.OPEN){s.ws.send(JSON.stringify({type:"admin",action:"rematch_game"}));return}BeatifyAuth.fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin"}).then(function(h){if(!h.ok)return h.json().then(function(w){throw new Error(w.message||"Rematch failed")});m.textContent="\u23F3"}).catch(function(h){console.error("[Player] Rematch failed:",h),alert(h.message||"Failed to start rematch"),m.disabled=!1,m.textContent=b})})}else d&&d.classList.add("hidden"),u&&u.classList.remove("hidden");if(n){var g=e.total_rounds||10,y=n.best_streak||0,E=y===g&&g>0;E?Be("perfect"):n.rank===1&&Be("winner")}}function vi(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+N.t("superlatives.avgTime");break;case"streak":r=a.value+" "+N.t("superlatives.streak");break;case"bets":r=a.value+" "+N.t("superlatives.bets");break;case"points":r=a.value+" "+N.t("superlatives.points");break;case"close_guesses":r=a.value+" "+N.t("superlatives.closeGuesses");break;case"perfect_rounds":r=a.value+" "+N.t("superlatives.perfectRounds");break;case"exact_titles":r=a.value+" "+N.t("superlatives.exactTitles");break;case"artists":r=a.value+" "+N.t("superlatives.artists");break;case"near_misses":r=a.value+" "+N.t("superlatives.nearMisses");break;default:r=a.value}n+='
'+a.emoji+'
'+N.t("superlatives."+a.title)+'
'+p(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function pi(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=N.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=N.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",p(i.description_params[l]))})),a+='
'+(i.emoji||"\u2728")+'
'+o+'
'+N.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function gi(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}t.classList.remove("hidden"),yi(n,e.playlist_name).then(function(i){var r=document.getElementById("share-card-image");r&&i&&(r.src=i.toDataURL("image/png"));var o=document.getElementById("share-save-btn");o&&(o.onclick=function(){bi(i)})})}}function yi(e,t){var n=800,a=800,i=document.createElement("canvas");i.width=n,i.height=a;for(var r=i.getContext("2d"),o=e.split(` -`).filter(function(M){return M.trim()!==""}),l="",c="",d="",u="",f="",m=0;m0&&(Oe+=cn),W.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",Oe+=r.measureText(W.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",Oe+=r.measureText(W.num).width,r.font="600 15px Inter, system-ui, sans-serif",Oe+=r.measureText(W.label).width)});var Z=T-Oe/2;r.textAlign="left",Re.forEach(function(W,ht){ht>0&&(r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#6b6b7a",r.fillText(" \xB7 ",Z,Ze),Z+=cn),W.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(W.text,Z,Ze),Z+=r.measureText(W.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(W.num,Z,Ze),Z+=r.measureText(W.num).width,r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#b3b3c2",r.fillText(W.label,Z,Ze),Z+=r.measureText(W.label).width)})}}function bi(e){e&&e.toBlob(function(t){if(t){if(navigator.share&&navigator.canShare){var n=new File([t],"beatify-results.png",{type:"image/png"}),a={files:[n],title:"My Beatify Results"};if(navigator.canShare(a)){navigator.share(a).catch(function(){pa(t)});return}}pa(t)}},"image/png")}function pa(e){var t=URL.createObjectURL(e),n=document.createElement("a");n.href=t,n.download="beatify-results.png",document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(t)}function ya(e){var t=document.getElementById("pause-message");t&&(e.pause_reason==="admin_disconnected"?t.textContent=N.t("player.waitingForHostReconnect"):e.pause_reason==="media_player_error"?t.textContent=N.t("player.speakerUnavailable"):t.textContent=N.t("player.gamePaused"))}async function hi(){var e=await Ie(N.t("admin.newGameTitle")||"New Game?",N.t("admin.newGameConfirm")||"Start a new game?",N.t("admin.newGame")||"New Game",N.t("common.cancel"));if(e){var t=document.getElementById("new-game-btn");t&&(t.disabled=!0,t.textContent=N.t("player.redirecting"));try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}window.location.href="/beatify/admin"}}var z=window.BeatifyUtils||{},ha="beatify_onboarded_v2",Ei=4e3,wi=1400;function $t(){var e=document.querySelectorAll(".tour-card").length;return e>0?e:4}function Li(){try{return localStorage.getItem(ha)==="1"}catch{return!1}}function Ii(){try{localStorage.setItem(ha,"1")}catch{}}var C={active:!1,replay:!1,currentIdx:0,autoAdvanceTimer:null,readyTimer:null};function pe(){C.autoAdvanceTimer&&(clearTimeout(C.autoAdvanceTimer),C.autoAdvanceTimer=null)}function en(){pe(),!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches)&&(C.autoAdvanceTimer=setTimeout(function(){Ea()},Ei))}function tn(){for(var e=document.querySelectorAll(".tour-wiz-seg"),t=0;t'+o+''}}function Ea(){pe(),C.currentIdx<$t()-1?(C.currentIdx++,tn(),nn(C.currentIdx),en()):wa()}function ba(){pe(),wa()}function wa(){if(pe(),C.active=!1,C.replay){C.replay=!1,k("lobby-view");return}Ii(),La(),Si()}function La(){if(!(!s.ws||s.ws.readyState!==WebSocket.OPEN))try{s.ws.send(JSON.stringify({type:"player_onboarded"}))}catch(e){console.warn("[Beatify] Failed to send player_onboarded:",e)}}function Si(){var e=document.getElementById("ready-name");if(e){var t=z.t?z.t("onboarding.ready",{name:s.playerName||""}):null;e.textContent=t&&t!=="onboarding.ready"?t:"You're in, "+(s.playerName||"")+"!"}var n=document.getElementById("ready-subtitle");n&&(n.textContent=z.t?z.t("onboarding.readySubtitle"):"Get ready to play");var a=document.getElementById("ready-label");a&&(a.textContent=z.t?z.t("onboarding.waitingHost"):"Waiting for host to start"),an(),k("ready-view"),C.readyTimer&&clearTimeout(C.readyTimer),C.readyTimer=setTimeout(function(){C.readyTimer=null;var i=document.getElementById("ready-view");i&&!i.classList.contains("hidden")&&k("lobby-view")},wi)}function an(e,t){var n=document.getElementById("ready-count");if(n){var a=e&&e.length||s.lastPlayerCount||0,i=t||s.lastDifficulty||"";if(!a){n.textContent="";return}var r=a===1?"onboarding.waitingCountOne":"onboarding.waitingCount",o=z.t?z.t(r,{count:a,difficulty:i}):null;o&&o!==r?n.textContent=o:n.textContent=a+(a===1?" player":" players")+" in lobby"}}function Ia(e){return!e||e.is_admin||e.onboarded===!0?!1:Li()?(La(),!1):!0}function Sa(){C.active=!0,C.replay=!1,C.currentIdx=0,tn(),nn(0),k("tour-view"),en()}function Bi(){C.active=!0,C.replay=!0,C.currentIdx=0,tn(),nn(0),k("tour-view"),en()}function Ba(){C.active&&(pe(),C.readyTimer&&(clearTimeout(C.readyTimer),C.readyTimer=null),C.active=!1,C.replay=!1)}function _a(){var e=document.getElementById("tour-skip-link");e&&e.addEventListener("click",function(r){r.preventDefault(),ba()});var t=document.getElementById("tour-skip-btn");t&&t.addEventListener("click",ba);var n=document.getElementById("tour-next-btn");n&&n.addEventListener("click",Ea);var a=document.getElementById("replay-tour-link");a&&a.addEventListener("click",function(r){r.preventDefault(),Bi()});var i=document.querySelector(".tour-container");i&&(i.addEventListener("touchstart",pe,{passive:!0}),i.addEventListener("mousedown",pe))}function dt(){return C.active}var re=window.BeatifyUtils||{},Y=re.debug||function(){},Je=10,_i=3e4,xi=20,ut="beatify_player_name",ft="beatify_game_id",Aa="beatify_language";function ki(e){return!e||typeof e!="string"?!1:/^[a-zA-Z0-9_-]{8,16}$/.test(e)}var mt="beatify_session";function Ta(e){var t=location.protocol==="https:"?"; Secure":"";document.cookie=mt+"="+e+"; path=/beatify; SameSite=Strict; max-age=86400"+t}function Qe(){for(var e=document.cookie.split(";"),t=0;txa){console.warn("[Beatify] No server activity for "+xa+"ms \u2014 socket appears dead, forcing reconnect");try{s.ws.close()}catch{}return}try{s.ws.send(JSON.stringify({type:"ping"}))}catch{}}},Ri)}function sn(){ct&&(clearInterval(ct),ct=null)}function Ae(){var e=Qe();if(e&&!(s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN))){var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,Xe(),Ra(),Ha(),s.ws.send(JSON.stringify({type:"reconnect",session_id:e}))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);Da(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(sn(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Je&&(s.isReconnecting=!1,Xe(),rn())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function ge(e){var t=s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN);if(!(t&&s.playerName===e)){if(t){if(!s.isAdmin)try{s.ws.send(JSON.stringify({type:"leave"}))}catch{}s.intentionalLeave=!0;try{s.ws.close()}catch{}s.ws=null,vt()}s.playerName=e,Na(e);var n=window.location.protocol==="https:"?"wss:":"ws:",a=n+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(a),s.ws.onopen=async function(){s.reconnectAttempts=0,s.isReconnecting=!1,Xe(),Ra(),Ha();var i={type:"join",name:e};s.isAdmin&&(i.is_admin=!0,i.ha_token=await BeatifyAuth.ensureAuthenticated()),s.ws.send(JSON.stringify(i))},s.ws.onmessage=function(i){try{var r=JSON.parse(i.data);Da(r)}catch(o){console.error("Failed to parse WebSocket message:",o)}},s.ws.onclose=function(){if(sn(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Je&&(s.isReconnecting=!1,Xe(),rn())},s.ws.onerror=function(i){console.error("WebSocket error:",i)}}}s.connectWithSession=Ae;s.connectWebSocket=ge;function Da(e){if(s.lastServerActivity=Date.now(),e.type!=="pong"){if(e.type==="game_starting"){var t=document.getElementById("loading-view"),n=document.getElementById("lobby-view"),a=document.getElementById("join-view"),i=n&&!n.classList.contains("hidden"),r=t&&!t.classList.contains("hidden"),o=a&&!a.classList.contains("hidden");(i||r||!o)&&k("starting-view");return}var l=document.getElementById("join-btn"),c=document.getElementById("name-input");if(e.type==="state"){var d=e.players||[],u=d.find(function(h){return h.name===s.playerName});if(u&&(s.isAdmin=u.is_admin===!0),e.language&&(Ai(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),_t(d),e.difficulty&&tt(e.difficulty,e.title_artist_mode),e.phase==="REVEAL"&&Kt(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&ot(e.phase)})),e.join_url&&xn(e.join_url),e.phase==="LOBBY"){fe(),ze(),Ue(),je(),s.currentRoundNumber=0,ue("warmup");var f=document.getElementById("start-game-btn");if(f&&(f.disabled=!1,f.innerHTML=''+re.t("lobby.startGame")+""),s.lastPlayerCount=d.length,s.lastDifficulty=e.difficulty?re.t?re.t("game.difficulty"+e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1)):e.difficulty:"",!dt()&&Ia(u))Sa();else{var m=document.getElementById("ready-view"),g=m&&!m.classList.contains("hidden");!g&&!dt()&&k("lobby-view"),g&&an(d,s.lastDifficulty)}_t(d),e.difficulty&&tt(e.difficulty,e.title_artist_mode),Tn(d)}else if(e.phase==="PLAYING"){dt()&&Ba(),X(),ze(),et();var y=e.round||1;y!==s.currentRoundNumber&&(s.currentRoundNumber=y,Jn()),Jt(),ue("party"),k("game-view"),De(),qn(e),e.intro_splash_pending?oa(s.isAdmin):la(),e.difficulty&&tt(e.difficulty,e.title_artist_mode),e.deadline&&kt(e.deadline),Yn(),jn(),Yt(),ot("PLAYING"),je()}else e.phase==="REVEAL"?(fe(),e.early_reveal&&Rn(),ue("party"),k("reveal-view"),Kt(e),zn(),Yt(),ot("REVEAL"),s.hasReactedThisPhase=!1,Zn()):e.phase==="PAUSED"?(fe(),ze(),Ue(),je(),ue("warmup"),k("paused-view"),ya(e)):e.phase==="END"&&(fe(),ze(),Ue(),je(),In(),s.currentRoundNumber=0,ue("warmup"),k("end-view"),ga(e),Ye())}else if(e.type==="join_ack"){et(),e.session_id&&Ta(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,Na(e.name),Mn(e.name)):(vt(),Ye(),s.playerName=null,k("join-view"));else if(e.type==="submit_ack")it();else if(e.type==="metadata_update")Un(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ut(e);return}if(e.code==="GAME_ENDED"){k("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,Ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,Xe(),s.playerName=null,rn(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){if(s.isReconnecting){console.warn("SESSION_NOT_FOUND during reconnect, will retry with session");return}vt(),s.intentionalLeave=!0,s.ws&&s.ws.close(),k("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Qt(),console.warn("[Beatify] Stop song failed: No song playing");return}if(e.code==="INVALID_ACTION"){console.warn("[Beatify] Action rejected:",e.message),Ut(e);return}k("join-view"),Hi(e.message),l&&(l.disabled=!1,l.textContent=re.t("join.joinButton")),c&&c.focus(),s.playerName=null,Ye()}else if(e.type==="song_stopped")ra();else if(e.type==="volume_changed")ia(e.level);else if(e.type==="game_ended")Pi();else if(e.type==="rematch_started"){Y("[Player] Rematch started - transitioning to lobby"),Se.clear(),X(),k("lobby-view");var E=document.getElementById("player-rematch-btn");E&&(E.disabled=!1,E.textContent="\u{1F501}");var b=Qe();b&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:b}))):(s.reconnectAttempts=0,Ae()))}else e.type==="left"?Oi():e.type==="steal_targets"?Kn(e):e.type==="steal_ack"?Xn(e):e.type==="artist_guess_ack"?Nt(e):e.type==="movie_guess_ack"?Dt(e):e.type==="title_artist_guess_ack"?Qn(e):e.type==="player_reaction"&&ea(e.player_name,e.emoji)}}function Oi(){Ye(),vt(),s.playerName=null,s.isAdmin=!1,k("join-view")}function Pi(){var e=s.isAdmin;Ye();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}pn(),Ln(),Se.clear(),X(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='

Thanks for playing!

Scan the QR code again to join the next game.

',n.classList.remove("hidden")),k("end-view")}}function Hi(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function on(e){var t=(e||"").trim();return t?t.length>xi?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function ka(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=on(e.value);a.valid&&(t.disabled=!0,t.textContent=re.t("game.joining"),n&&n.classList.add("hidden"),ge(a.name))}}function Di(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=on(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",ka),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&ka()}))}function Gi(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,k("loading-view"),ge(s.playerName)):pt()})}async function Ca(){try{await BeatifyAuth.init({requireAuth:!1})}catch{}var e=Ee.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await re.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ti();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");i&&(i.href=window.location.origin+"/beatify/dashboard"),Di(),_a(),kn(),An(),Nn(),fa(),ma(),va(),sa(),aa(),Gi(),gn(),yn(),bn(),$n();var r=new URLSearchParams(window.location.search),o=r.get("session"),l=null;try{l=sessionStorage.getItem("beatify_session")}catch{}var c=o||l;if(c){Ta(c);try{sessionStorage.removeItem("beatify_session")}catch{}}if(Mi()&&s.playerName){Qe()?Ae():ge(s.playerName);return}var d=Ci();if(d&&s.gameId){Y("[Beatify] Auto-reconnecting as:",d),ge(d);return}if(d){var u=document.getElementById("name-input"),f=document.getElementById("join-btn");if(u&&(u.value=d,f)){var m=on(d);f.disabled=!m.valid}}}pt();document.getElementById("refresh-btn")?.addEventListener("click",function(){k("loading-view"),pt()});document.getElementById("retry-btn")?.addEventListener("click",function(){k("loading-view"),pt()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",Ca):Ca();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/sw.js",{scope:"/beatify/"}).then(function(e){Y("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){s.playerName&&et();var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(Y("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,Ae())}}); +var Et=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},Ga=document.getElementById("loading-view"),Va=document.getElementById("starting-view"),Wa=document.getElementById("not-found-view"),Fa=document.getElementById("ended-view"),qa=document.getElementById("in-progress-view"),Ua=document.getElementById("join-view"),ja=document.getElementById("tour-view"),za=document.getElementById("ready-view"),Ya=document.getElementById("lobby-view"),Ja=document.getElementById("game-view"),Qa=document.getElementById("reveal-view"),Xa=document.getElementById("paused-view"),Ka=document.getElementById("end-view"),Za=document.getElementById("connection-lost-view"),$a=[Ga,Va,Wa,Fa,qa,Ua,ja,za,Ya,Ja,Qa,Xa,Ka,Za];function k(e){Et.showView($a,e);var t=e==="tour-view"||e==="ready-view",n=e==="game-view"||e==="reveal-view";document.body&&(document.body.classList.toggle("is-learning-screen",t),document.body.classList.toggle("is-ingame",n)),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&ue("calm"),e==="join-view"&&setTimeout(function(){var a=document.getElementById("name-input");a&&a.focus()},100)}function Ie(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),c=document.getElementById("confirm-modal-yes"),d=document.getElementById("confirm-modal-no");if(!r||!o||!l||!c||!d){i(confirm(t||e));return}o.textContent=e,l.textContent=t,c.textContent=n||Et.t("common.confirm")||"Confirm",d.textContent=a||Et.t("common.cancel")||"Cancel",r.classList.remove("hidden");function u(){r.classList.add("hidden"),c.removeEventListener("click",f),d.removeEventListener("click",m),g.removeEventListener("click",m)}function f(){u(),i(!0)}function m(){u(),i(!1)}var g=r.querySelector(".modal-backdrop");c.addEventListener("click",f),d.addEventListener("click",m),g&&g.addEventListener("click",m)})}function p(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function er(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function tr(e){return 1-Math.pow(1-e,4)}function Lt(e,t,n,a,i){if(er()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=Ee.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||tr;var l=null,c=null,d=!1,u=n;function f(m){if(!d){l||(l=m);var g=m-l,y=Math.min(g/o,1),E=i(y),b=Math.round(t+(u-t)*E);e.textContent=b,y<1&&(c=requestAnimationFrame(f))}}return c=requestAnimationFrame(f),{cancel:function(){d=!0,c&&cancelAnimationFrame(c)},skipToEnd:function(){d=!0,c&&cancelAnimationFrame(c),e.textContent=u}}}var Q={players:{},leaderboard:[],initialized:!1};function mn(){return Q.initialized}function vn(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=Q.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function $e(e,t){Q.players={},e.forEach(function(n){Q.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(Q.leaderboard=t.map(function(n){return n.name})),Q.initialized=!0}var Ee=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),Se=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),Le={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},B={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function pn(e){e&&(B.observer&&B.listEl!==e&&(B.observer.disconnect(),B.observer=null),!B.observer&&(B.listEl=e,B.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!B.isLazyEnabled)){var a=B.fullData,i=B.visibleRange,r=Le.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);B.visibleRange.start=o,He()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+='
'),l+='
';for(var c=n.start;c',r>0&&(l+='
'),e.innerHTML=l,e.scrollTop=o,B.observer){var d=e.querySelectorAll(".leaderboard-sentinel");d.forEach(function(u){B.observer.observe(u)})}}}function It(e){if(!e)return"";if(e.separator)return'
...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var c="";if(e.streak>=2){var d=e.streak>=5?"streak-indicator--hot":"";c='\u{1F525}'+e.streak+""}var u=e.connected===!1?"leaderboard-entry--disconnected":"",f=e.connected===!1?'(away)':"",m=e._displayScore!==void 0?e._displayScore:a;return'
#'+n+''+p(t)+f+''+m+"
"}function St(e,t){for(var n=Le,a=B.listEl&&B.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(c=Math.max(0,e.length-i-r),d=e.length):(c=Math.max(0,o-Math.floor(i/2)-r),d=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:c,end:d}}function gn(){B.observer&&(B.observer.disconnect(),B.observer=null),B.isLazyEnabled=!1,B.fullData=[]}function yn(){var e;function t(){clearTimeout(e),e=setTimeout(function(){B.isLazyEnabled&&B.fullData.length>0&&(B.visibleRange=St(B.fullData,s.playerName),He())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function bn(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function hn(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var En={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},L={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function wn(e){if(e){L.container=e;var t=!1;L.scrollHandler=function(){L.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){wt(),t=!1}),t=!0)};var n;L.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){L.isVirtual&&wt()},100)},e.addEventListener("scroll",L.scrollHandler,{passive:!0}),window.addEventListener("resize",L.resizeHandler)}}function Ln(e,t){L.items=e,L.renderItem=t;var n=L.container;if(n){var a=n.scrollTop,i=L.isVirtual;e.length0&&(n.scrollTop=a,L.scrollTop=a)}}function nr(){var e=L.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",L.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",L.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",L.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function wt(){var e=En,t=L.items,n=L.container,a=L.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=L.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,c=Math.max(0,Math.floor(r/o)-l),d=Math.min(t.length,Math.ceil((r+i)/o)+l);L.topSpacer&&(L.topSpacer.style.height=c*o+"px"),L.bottomSpacer&&(L.bottomSpacer.style.height=(t.length-d)*o+"px");for(var u="",f=c;f"u"){console.warn("[Confetti] Library not loaded");return}X();var t=Ee.getQualitySettings(),n=t.confettiParticles;if(n===0){fn();return}var a=Ee.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function g(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(y,E){return y.connected!==E.connected?y.connected?-1:1:0}),c=Bn.map(function(y){return y.name}),d=l.filter(function(y){return c.indexOf(y.name)===-1}).map(function(y){return y.name});L.container||wn(t);var u=["c1","c2","c3","c4"],f={},m=0;l.forEach(function(y){y.is_admin?f[y.name]="host":f[y.name]=u[m++%u.length]});var g=function(y){var E=d.indexOf(y.name)!==-1,b=y.name===s.playerName,h=y.is_admin===!0,w=y.connected===!1,x=f[y.name]||"c1",_=["player-tile","player-tile--"+x];E&&_.push("is-new"),w&&_.push("player-tile--disconnected");var S=(y.name||"?").trim(),G=(S.charAt(0)||"?").toUpperCase(),A="";h?A='':b&&(A='YOU');var M=w?'":"";return'
'+p(G)+''+p(S)+""+A+M+"
"};Ln(l,g),setTimeout(function(){var y=L.isVirtual?L.contentWrapper:t;if(y)for(var E=y.querySelectorAll(".is-new"),b=0;b=1){K=e,Pn(Math.max(0,Math.ceil((e-Date.now())/1e3))),t&&t.classList.remove("timer-neon--catchup"),_e=null;return}var l=n+(e-n)*dr(o);K=l,Pn(Math.max(0,Math.ceil((l-Date.now())/1e3))),_e=requestAnimationFrame(i)}return _e=requestAnimationFrame(i),!0}var te=null,xt=null;function ur(e,t){if(!(!t||!e)){if(typeof IntersectionObserver>"u"){t.classList.remove("hidden"),t.classList.add("timer-float--visible");return}te&&xt===e||(te&&te.disconnect(),te=new IntersectionObserver(function(n){var a=n[0];a&&(a.isIntersecting?(t.classList.add("hidden"),t.classList.remove("timer-float--visible")):(t.classList.remove("hidden"),t.classList.add("timer-float--visible")))},{threshold:.1}),te.observe(e),xt=e)}}function fe(){Ge&&(clearInterval(Ge),Ge=null),Hn(),K=null;var e=document.getElementById("timer-neon");e&&e.classList.remove("timer-neon--catchup");var t=document.getElementById("timer-float");t&&(t.classList.add("hidden"),t.classList.remove("timer-float--visible","timer-float--warn")),te&&(te.disconnect(),te=null,xt=null)}var ae=window.BeatifyUtils||{},me=!1,ne=null,nt=null,fr=300,Dn=0;function Tt(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(u){return u.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(u,f){var m=document.createElement("button");m.className="artist-option-btn",m.dataset.artist=u,m.dataset.index=f,m.textContent=u,m.addEventListener("click",function(){mr(u)}),a.appendChild(m)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(u){u.classList.add("is-disabled"),u.classList.remove("is-loading","is-wrong");var f=e.correct_artist||nt;f&&u.dataset.artist===f&&u.classList.add("is-winner")}),e.winner===s.playerName){var c=e.bonus_points||5;i.textContent=(ae.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",c),i.className="artist-result is-winner"}else{var d=(ae.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=d,i.className="artist-result is-late"}i.classList.remove("hidden"),me=!0}else me||i.classList.add("hidden")}}function mr(e){var t=Date.now();if(!(t-Dn0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}Ot();var a=(Ve.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Pt(a,!0),xe=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),Ot(),Pt(Ve.t("movieChallenge.wrongGuess")||"Not quite...",!1),xe=!0;ke=null}function Ot(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Pt(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Gt(){xe=!1,ke=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function Vt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=Ve.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(c){var d=document.createElement("div");d.className="movie-reveal-winner-entry",c.name===t?d.classList.add("is-you"):d.classList.add("is-other"),d.textContent=c.name+" \u2014 +"+c.bonus+" ("+c.time+"s)",i.appendChild(d)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=Ve.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}var I=window.BeatifyUtils||{},gr=I.debug||function(){};function Un(e){var t=document.getElementById("current-round"),n=document.getElementById("total-rounds"),a=document.getElementById("last-round-banner");t&&(t.textContent=e.round||1),n&&(n.textContent=e.total_rounds||10),a&&(e.last_round?a.classList.remove("hidden"):a.classList.add("hidden"));var i=document.getElementById("closest-wins-badge");i&&(e.closest_wins_mode?i.classList.remove("hidden"):i.classList.add("hidden"));var r=document.getElementById("intro-badge"),o=document.getElementById("intro-splash");if(r)if(e.is_intro_round){r.classList.remove("hidden");var l=r.querySelector("[data-i18n]");e.intro_stopped?(r.classList.add("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=I.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=I.t("game.introRound")||"INTRO ROUND"),o&&!o._shown&&(o._shown=!0,o.classList.remove("hidden"),setTimeout(function(){o.classList.add("hidden")},2e3)))}else r.classList.add("hidden"),r.classList.remove("intro-badge--stopped"),o&&(o.classList.add("hidden"),o._shown=!1);var c=document.getElementById("album-cover"),d=document.getElementById("album-loading");if(c&&e.song){d&&d.classList.remove("hidden");var u=e.song.album_art||"/beatify/static/img/no-artwork.svg";c.onload=function(){d&&d.classList.add("hidden")},c.onerror=function(){c.src="/beatify/static/img/no-artwork.svg",d&&d.classList.add("hidden")},c.src=u}Er(e),qt(),br(e),wr(e.players),e.leaderboard&&Lr(e,"leaderboard-list"),Ar(e.players),e.artist_challenge!==void 0&&Tt(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Ht(e.movie_challenge,"PLAYING"),kr(e)}function jn(e){if(e){var t=document.getElementById("album-cover"),n=document.getElementById("album-loading");if(t&&e.album_art){var a=e.album_art;if(t.src===a)return;t.style.transition="opacity 0.3s ease-in-out",t.style.opacity="0.5";var i=new Image;i.onload=function(){t.src=a,t.style.opacity="1",n&&n.classList.add("hidden")},i.onerror=function(){t.src="/beatify/static/img/no-artwork.svg",t.style.opacity="1",n&&n.classList.add("hidden")},i.src=a}gr("[Metadata] Updated:",e.artist,"-",e.title)}}function yr(e){if(!e)return"?";var t=e.trim();if(!t)return"?";var n=t.split(/[\s-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function qt(){var e=document.getElementById("arc-chip-row");if(e){var t=["game-difficulty-badge","steal-indicator","closest-wins-badge","intro-badge","last-round-banner"],n=t.some(function(a){var i=document.getElementById(a);return i&&!i.classList.contains("hidden")});e.classList.toggle("hidden",!n)}}function br(e){var t=document.getElementById("no-bonus-filler");if(t){var n=!!(e&&e.artist_challenge&&e.artist_challenge.options),a=!!(e&&e.movie_challenge&&e.movie_challenge.options),i=!!(e&&e.title_artist_mode);t.classList.toggle("hidden",n||a||i)}}var Ce=!1;function hr(e){return!s.playerName||!e?null:e.find(function(t){return t.name===s.playerName})||null}function Er(e){var t=document.getElementById("eliminated-view");if(t){var n=!!(e&&e.sudden_death_mode),a=hr(e&&e.players),i=n&&!!(a&&a.eliminated);Ce=i;var r=[document.getElementById("year-selector-container"),document.getElementById("year-display-arc"),document.getElementById("bet-toggle"),document.getElementById("submit-btn"),document.getElementById("title-artist-container"),document.getElementById("submitted-banner")];if(i){r.forEach(function(f){f&&f.classList.add("hidden")}),t.classList.remove("hidden");var o=document.getElementById("album-cover"),l=document.getElementById("eliminated-album-cover");l&&o&&o.src&&(l.src=o.src);var c=document.getElementById("eliminated-sub");if(c){var d=a&&a.eliminated_round!=null?a.eliminated_round:e&&e.round||"";c.textContent=I.t("game.eliminatedRound",{round:d})||"Eliminated \xB7 Round "+d}Jt()}else{r.forEach(function(f){f&&f.classList.remove("hidden")}),t.classList.add("hidden");var u=document.getElementById("submitted-banner");u&&!D&&u.classList.add("hidden")}}}function wr(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players"),a=document.getElementById("arc-submission-count");if(!(!t||!n)){var i=e||[],r=i.filter(function(m){return!m.eliminated}),o=r.filter(function(m){return m.submitted}).length,l=r.length,c=o===l&&l>0;t.classList.toggle("all-submitted",c),a&&(l===0?a.textContent="":c?a.textContent=I.t("game.allSubmitted")||"All in":a.textContent=I.t("game.submittedCount",{count:o,total:l})||o+" of "+l+" submitted");var d=document.getElementById("submitted-banner"),u=document.getElementById("submitted-banner-text");if(d&&u&&!d.classList.contains("hidden")){var f=Math.max(0,l-o);f===0?u.textContent=I.t("game.lockedInAllSubmitted")||"Locked in \xB7 everyone submitted":u.textContent=I.t("game.lockedInWaitingCount",{count:f})||"Locked in \xB7 waiting for "+f+" more"}n.innerHTML=i.map(function(m){var g=yr(m.name),y=m.name===s.playerName,E=m.connected===!1,b=!!m.eliminated,h=["player-indicator",m.submitted&&!b?"is-submitted":"",y?"is-current-player":"",E?"player-indicator--disconnected":"",b?"is-eliminated":""].filter(Boolean).join(" "),w="";if(b){var x=m.eliminated_round!=null?m.eliminated_round:"",_=I.t("game.outRound",{round:x})||"Out \xB7 R"+x;w+=''+p(_)+""}else m.steal_used&&(w+='\u{1F977}'),m.bet&&(w+='\u{1F3B2}');var S=b?'':''+p(g)+"";return'
'+w+'
'+S+'
'+p(m.name)+"
"}).join("")}}function Lr(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&&mn(),o=r?vn(a):{};a.forEach(function(f){f.is_current=f.name===s.playerName;var m=o[f.name];m&&(f._rankChange=m);var g=Q.players[f.name],y=g?g.score:f.score;f._prevScore=y,f._displayScore=n?y:f.score});var l=Ir(a,s.playerName),c=a.length>=Le.MIN_PLAYERS_FOR_LAZY;if(c)B.observer||pn(i),B.fullData=l,B.isLazyEnabled=!0,B.listEl=i,B.visibleRange=St(l,s.playerName),He();else{B.isLazyEnabled=!1;var d="";l.forEach(function(f){d+=It(f)}),i.innerHTML=d}var u=[];r&&l.forEach(function(f){!f.separator&&f._prevScore!==f.score&&u.push({name:f.name,prevScore:f._prevScore,newScore:f.score})}),r&&u.length>0&&requestAnimationFrame(function(){for(var f={},m=i.querySelectorAll(".leaderboard-entry[data-name]"),g=0;g8&&Sr(i),Br(a),_r(a),$e(e.players||[],a)}}function Ir(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Sr(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Br(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=I.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function zn(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function _r(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Yn(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var D=!1,We=!1,Fe=!1,Wt=!1,Vn=!1,Wn=!1;function Jn(){if(Wn)return;var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!e||!t)return;Wn=!0,e.addEventListener("input",function(){Ce||(t.textContent=this.value)});function n(m){var g=parseInt(e.value,10)+m;g=Math.max(parseInt(e.min,10),Math.min(parseInt(e.max,10),g)),e.value=g,t.textContent=g}function a(m,g){if(!m)return;var y=null,E=null;m.addEventListener("pointerdown",function(h){D||Ce||(h.preventDefault(),n(g),E=setTimeout(function(){y=setInterval(function(){n(g)},150)},500))});function b(){E&&(clearTimeout(E),E=null),y&&(clearInterval(y),y=null)}["pointerup","pointerleave","pointercancel"].forEach(function(h){m.addEventListener(h,b)}),m.addEventListener("keydown",function(h){D||Ce||(h.key==="Enter"||h.key===" ")&&(h.preventDefault(),n(g))})}a(document.getElementById("year-decrement"),-1),a(document.getElementById("year-increment"),1),a(document.getElementById("year-decrement-5"),-5),a(document.getElementById("year-increment-5"),5);var i=document.getElementById("bet-toggle");i&&i.addEventListener("click",function(){D||(We=!We,i.classList.toggle("is-active",We))});var r=document.getElementById("submit-btn");if(r&&r.addEventListener("click",function(){Wt?Fn():xr()}),!Vn){var o=document.getElementById("ta-title-input"),l=document.getElementById("ta-artist-input");o&&o.addEventListener("keydown",function(m){m.key==="Enter"&&(m.preventDefault(),l&&l.focus())}),l&&l.addEventListener("keydown",function(m){m.key==="Enter"&&(m.preventDefault(),Wt&&Fn())}),Vn=!0}var c=document.getElementById("steal-btn");c&&c.addEventListener("click",Tr);var d=document.getElementById("steal-modal-close");d&&d.addEventListener("click",Ft);var u=document.getElementById("steal-modal");if(u){var f=u.querySelector(".steal-modal-backdrop");f&&f.addEventListener("click",Ft)}}function xr(){if(!D&&!Ce){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:We})):(rt(I.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function it(){D=!0;var e=document.getElementById("year-selector"),t=document.getElementById("year-display-arc"),n=document.getElementById("submit-btn"),a=document.getElementById("bet-toggle"),i=document.getElementById("submitted-banner");e&&e.classList.add("is-submitted","slider-arcade--locked"),t&&t.classList.add("year-xxl--locked"),n&&(n.disabled=!0,n.classList.add("submit-arc--waiting"),n.innerHTML=""+p(I.t("game.waitingForOthers")||"Waiting for others")+''),a&&(a.disabled=!0),i&&i.classList.remove("hidden"),["year-decrement","year-increment","year-decrement-5","year-increment-5"].forEach(function(r){var o=document.getElementById(r);o&&(o.disabled=!0)})}function Ut(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(rt(I.t("errors.timesUp")),D=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?it():rt(e.message||"Submission failed")}function rt(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=I.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Qn(){D=!1,We=!1;var e=document.getElementById("year-selector"),t=document.getElementById("year-display-arc"),n=document.getElementById("submit-btn"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle"),r=document.getElementById("submitted-banner");if(e&&e.classList.remove("is-submitted","slider-arcade--locked"),t&&t.classList.remove("year-xxl--locked"),n&&(n.disabled=!1,n.classList.remove("hidden","is-loading","is-error","submit-arc--waiting"),n.textContent=I.t("game.submitGuess")),i&&(i.disabled=!1,i.classList.remove("hidden","is-active")),r&&r.classList.add("hidden"),a){a.value=1990;var o=document.getElementById("selected-year");o&&(o.textContent="1990")}["year-decrement","year-increment","year-decrement-5","year-increment-5"].forEach(function(l){var c=document.getElementById(l);c&&(c.disabled=!1)}),Fe=!1,jt(),Mt(),Gt(),Cr()}function kr(e){var t=!!(e&&e.title_artist_mode);Wt=t;var n=document.getElementById("title-artist-container"),a=document.getElementById("year-selector-container"),i=document.getElementById("year-display-arc"),r=document.getElementById("bet-toggle");if(n&&n.classList.toggle("hidden",!t),a&&a.classList.toggle("hidden",t),i&&i.classList.toggle("hidden",t),r&&r.classList.toggle("hidden",t),!!t){var o=document.getElementById("submit-btn");o&&!D&&(o.textContent=I.t("titleArtist.submitGuess")||"Submit")}}function Fn(){if(!D&&!Ce){var e=document.getElementById("ta-title-input"),t=document.getElementById("ta-artist-input"),n=document.getElementById("submit-btn");if(!(!e||!t||!n)){var a=(e.value||"").trim(),i=(t.value||"").trim();n.disabled=!0,n.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"title_artist_guess",title:a,artist:i})):(rt(I.t("errors.connectionLost")),n.disabled=!1,n.classList.remove("is-loading"))}}}function Xn(e){it();var t=document.getElementById("ta-title-input"),n=document.getElementById("ta-artist-input");t&&(t.disabled=!0),n&&(n.disabled=!0);var a=document.getElementById("ta-input-ack");a&&(a.textContent=I.t("titleArtist.submitted")||"Submitted \u2014 see how you did at the reveal!",a.classList.remove("hidden"))}function Cr(){var e=document.getElementById("ta-title-input"),t=document.getElementById("ta-artist-input"),n=document.getElementById("ta-input-ack");e&&(e.value="",e.disabled=!1),t&&(t.value="",t.disabled=!1),n&&(n.textContent="",n.classList.add("hidden"))}function Ar(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){Fe=t.steal_available&&!D;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");Fe?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):jt(),qt()}}}function jt(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden"),qt()}function Tr(){!Fe||D||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function Nr(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=I.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){Mr(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function Ft(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function Mr(e){var t=I.t("steal.confirm").replace("{name}",e),n=await Ie(I.t("steal.confirmTitle")||"Steal Answer?",t,I.t("steal.confirmButton")||"Steal",I.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),Ft())}function Kn(e){if(e.success){Fe=!1,D=!0,jt();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),Rr(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Zn(e){Nr(e.targets||[])}function Rr(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=I.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var qn=0,Or=500,qe=!1,zt=.5;function st(){var e=Date.now();return e-qn=1){ta("max");return}st()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function Gr(){if(zt<=0){ta("min");return}st()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function ta(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function Vr(){var e=await Ie(I.t("admin.endGameConfirm")||"End Game?",I.t("admin.endGameWarning")||"All players will be disconnected.",I.t("admin.endGame")||"End Game",I.t("common.cancel"));if(e&&st()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(I.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var at=!1;function na(){if(!at&&s.ws&&s.ws.readyState===WebSocket.OPEN){at=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=I.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){at&&Qt()},1e4)}}function Qt(){at=!1;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!1,e.textContent=I.t("admin.nextRound")),t){t.disabled=!1;var n=t.querySelector(".control-label");n&&(n.textContent=I.t("admin.nextRound"))}}function Wr(){na()}function aa(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",Hr),t&&t.addEventListener("click",Dr),n&&n.addEventListener("click",Gr),a&&a.addEventListener("click",Wr),i&&i.addEventListener("click",Vr)}function ra(){qe=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=I.t("game.stopped"))}}function Xt(){qe=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=I.t("game.stop"))}}function ia(e){zt=e,Fr(e),qr(e)}function Fr(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function qr(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function sa(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",na);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||(Se.isRunning()&&Se.skipAll(),X())})}function oa(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function la(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var v=window.BeatifyUtils||{},da=30,lt=null;function Zt(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("reveal-idle-halt");r&&r.classList.toggle("hidden",!e.idle_halt),Ur(e);var o=document.getElementById("closest-wins-badge");o&&(e.closest_wins_mode?o.classList.remove("hidden"):o.classList.add("hidden"));var l=document.getElementById("intro-badge");if(l)if(e.is_intro_round){l.classList.remove("hidden"),l.classList.add("intro-badge--stopped");var c=l.querySelector("[data-i18n]");c&&(c.setAttribute("data-i18n","game.introStopped"),c.textContent=v.t("game.introStopped")||"Intro complete!")}else l.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.onerror=function(){d.src="/beatify/static/img/no-artwork.svg"},d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("reveal-backdrop");if(u){var f=t.album_art;if(f){var m=new Image;m.onload=function(){u.style.backgroundImage='url("'+f+'")',u.classList.remove("reveal-backdrop--synthetic")},m.onerror=function(){u.style.backgroundImage="",u.classList.add("reveal-backdrop--synthetic")},m.src=f}else u.style.backgroundImage="",u.classList.add("reveal-backdrop--synthetic")}var g=document.getElementById("correct-year");g&&(g.textContent=t.year||"????");var y=document.getElementById("song-title"),E=document.getElementById("song-artist");y&&(y.textContent=t.title||"Unknown Song"),E&&(E.textContent=t.artist||"Unknown Artist");var b=document.getElementById("fun-fact-container"),h=document.getElementById("fun-fact"),w=b?b.querySelector(".fun-fact-header"):null,x=v.getLocalizedSongField(t,"fun_fact");if(h&&(h.textContent=x||""),w&&(w.style.display=x?"flex":"none"),Yr(t),zr(e.song_difficulty),b){var _=document.getElementById("song-rich-info"),S=_&&_.innerHTML.trim()!=="",G=x&&x.trim()!=="";b.classList.toggle("hidden",!G&&!S)}for(var A=null,M=0;M'+v.t("analytics.noGuesses")+"";var a=e.map(function(V){return V.guess}),i=Math.min.apply(null,a.concat([t])),r=Math.max.apply(null,a.concat([t])),o=Math.max(2,Math.floor((r-i)*.1)),l=i-o,c=r+o,d=Math.max(1,c-l);function u(V){return(V-l)/d*100}for(var f=u(t),m='
'+t+"
",g="",y=0;y<=4;y++){var E=Math.round(l+d*y/4),b=y*25;g+='
'+E+"
"}function h(V){for(var T=0,O=0;O>>0;return"c"+(T%4+1)}for(var w="",x=0;x0?"dotaxis-score--pos":"dotaxis-score--zero",R=H>0?"+"+H:"+0";w+='
'+G+'
'+R+"
"}for(var ie=e.slice().sort(function(V,T){return(V.years_off||0)-(T.years_off||0)}),Te=["\u{1F3C6}","\u{1F948}","\u{1F949}"],Ne="",se=0;se'+Te[se]+"":"",ye=n&&J.name===n?' '+v.t("analytics.youMarker")+"":"";Ne+=''+oe+''+p(J.name||"?")+ye+""}return'
'+g+'
'+m+w+'
'+Ne+"
"}function zr(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML='
'+n+'
'+v.t("difficulty."+e.label)+''+e.accuracy+"% "+v.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function Yr(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=Jr(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=Qr(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=v.getLocalizedSongField(e,"awards")||[],o=Zr(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML='
'+n.join("")+"
":t.innerHTML=""}}function Jr(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+v.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+v.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+v.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+v.t("reveal.chartUK")+""),t}function Qr(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+p(a)+"")}return t}function Xr(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Kr(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function Zr(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+p(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function $r(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function ei(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function ti(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("duel-emotion"),r=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"duel-emotion":r?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),X();var o=v.t("reveal.emotions");function l(y){return y[Math.floor(Math.random()*y.length)]}function c(y){return y===1?v.t("reveal.offByYear"):v.t("reveal.offByYears",{years:y})}var d="missed",u=l(o.missed),f=l(o.missedSub);if(e&&!e.missed_round){var m=e.years_off||0;m===0?(d="exact",u=l(o.exact),f=l(o.exactSub)):m<=2?(d="close",u=l(o.close),f=l(o.closeSub)+" "+c(m)):m<=5?(d="close",u=l(o.close),f=c(m)):(d="wrong",u=l(o.wrong),f=l(o.wrongSub)+" "+c(m))}else e&&e.missed_round&&(d="missed",u=l(o.missed),f=l(o.missedSub));if(i)n.textContent=u,n.classList.add("duel-emotion--"+d);else{var g=''+u+"";f&&(g+='
'+f+"
"),n.innerHTML=g,n.classList.add("reveal-emotion--"+d)}n.classList.remove("hidden"),d==="exact"&&Be(),a&&d!=="missed"&&a.classList.add("is-delayed")}function ni(e,t){var n=document.getElementById("duel-your-year"),a=document.getElementById("duel-gap-count"),i=document.getElementById("duel-gap-unit");if(!(!n||!a||!i)){if(!e||e.missed_round){n.textContent=v.t("reveal.duel.noGuess")||"\u2014",a.textContent="\u2014",i.textContent="";return}var r=e.guess;n.textContent=r!=null&&r!==""?r:"\u2014";var o=e.years_off!=null?e.years_off:0;a.textContent=String(o),i.textContent=o===1?v.t("reveal.duel.yearUnit")||"year":v.t("reveal.duel.yearsUnit")||"years";var l=a.closest(".duel-gap");l&&(l.classList.remove("duel-gap--exact","duel-gap--close","duel-gap--wrong"),o===0?l.classList.add("duel-gap--exact"):o<=5?l.classList.add("duel-gap--close"):l.classList.add("duel-gap--wrong"))}}function ai(e,t){var n=document.getElementById("reveal-chip-row");if(n){if(!e){n.classList.add("hidden"),n.innerHTML="";return}var a=[];e.bet_outcome==="won"?a.push('\u{1F3B2} '+p(v.t("reveal.chip.betWon")||"Bet won \xB7 \xD72")+""):e.bet_outcome==="lost"&&a.push('\u{1F3B2} '+p(v.t("reveal.chip.betLost")||"Bet lost")+"");var i=e.streak_bonus||0;if(i>0&&e.streak){var r=v.t("reveal.chip.streakBonus",{count:e.streak,bonus:i})||e.streak+"-streak \xB7 +"+i;a.push('\u{1F525} '+p(r)+"")}a.length===0?(n.classList.add("hidden"),n.innerHTML=""):(n.classList.remove("hidden"),n.innerHTML=a.join(""))}}function $t(e){if(!e)return 0;var t=e.round_score||0,n=e.streak_bonus||0,a=e.artist_bonus||0,i=e.movie_bonus||0,r=e.intro_bonus||0;return t+n+a+i+r}function ri(e){var t=document.getElementById("reveal-total-pts"),n=document.getElementById("score-row-subtitle");if(t){var a=$t(e);if(t.textContent=(a>=0?"+":"")+a,n)if(!e||e.missed_round)n.textContent=v.t("reveal.noSubmission")||"No guess submitted";else{var i=e.years_off!=null?e.years_off:0,r=i===0?"reveal.exact":i===1?"reveal.yearOff":"reveal.yearsOff";n.textContent=v.t(r,{years:i})||i+" years off"}}}function ii(e){for(var t=[["#ff2d6a","#ff6600"],["#00f5ff","#7a5cff"],["#39ff14","#00f5ff"],["#ff6600","#ff0040"],["#7a5cff","#b3b3c2"],["#ff2d6a","#7a5cff"]],n=0,a=0;a>>0;var i=t[n%t.length];return"linear-gradient(135deg,"+i[0]+","+i[1]+")"}function si(e){var t=document.getElementById("reveal-leaderboard-list");if(t){var n=e.leaderboard||[],a=e.players||[],i={};a.forEach(function(l){i[l.name]=l});var r=(v.t("analytics.youMarker")||"YOU").replace(/[()]/g,""),o="";n.forEach(function(l,c){var d=i[l.name]||{},u=l.name===s.playerName,f=((l.name||"?").trim().charAt(0)||"?").toUpperCase(),m=l.rank_change||0,g=m>0?"up":m<0?"down":"flat",y=m>0?"\u25B2"+m:m<0?"\u25BC"+Math.abs(m):"\u2013",E="";if(!d.missed_round&&d.years_off!=null&&d.guess!=null&&d.guess!==""){var b=d.years_off,h=b===0?''+p(v.t("reveal.exact")||"Exact!")+"":p(v.t("reveal.shortOff",{years:b})||b+" off");E='
'+p(String(d.guess))+" \xB7 "+h+"
"}var w=[];d.streak&&d.streak>=2&&w.push('\u{1F525} '+d.streak+""),d.stole_from?w.push('\u{1F977} '+p(v.t("steal.stolenFrom",{name:d.stole_from})||"stole "+d.stole_from)+""):d.was_stolen_by&&d.was_stolen_by.length&&w.push('\u{1F3AF} '+p(v.t("steal.stolenBy",{name:d.was_stolen_by.join(", ")})||"stolen")+""),d.bet_outcome==="won"?w.push('\u{1F3B2} '+p(v.t("reveal.chip.betWon")||"Bet won")+""):d.bet_outcome==="lost"&&w.push('\u{1F3B2} '+p(v.t("reveal.chip.betLost")||"Bet lost")+"");var x=w.length?'
'+w.join("")+"
":"",_=$t(d),S=_>0?"":" rstand-delta--zero",G=(_>=0?"+":"")+_,A=c>=4?" rstand-row--taper2":c>=3?" rstand-row--taper1":"",M=u?" rstand-row--you":"",P=u?' '+p(r)+"":"";o+='
'+l.rank+''+y+'
'+p(f)+'
'+p(l.name||"?")+P+"
"+E+x+'
'+(l.score||0)+''+G+"
"}),t.innerHTML=o,$e(a,n)}}function oi(){var e=document.getElementById("points-breakdown-content");if(e){var t=s.lastRevealContext,n=t?t.player:null;if(!n||n.missed_round){e.innerHTML='
'+p(v.t("reveal.breakdown.noSubmission")||v.t("reveal.noSubmission")||"No guess submitted")+'
'+p(v.t("reveal.breakdown.total")||"Total this round")+'+0
';return}var a=[],i=n.years_off!=null?n.years_off:0,r=n.base_score||0,o=n.round_score||0,l=n.speed_multiplier||1,c=Math.floor(r*l)-r;a.push({emoji:"\u{1F3AF}",label:v.t("reveal.breakdown.baseScore",{years:i})||"Base score",value:String(r),kind:"neutral"}),l>1&&c>0&&a.push({emoji:"\u26A1",label:(v.t("reveal.breakdown.speedBonus")||"Speed bonus")+" ("+l.toFixed(2)+"\xD7)",value:"+"+c,kind:"positive"}),n.streak_bonus&&n.streak_bonus>0&&a.push({emoji:"\u{1F525}",label:v.t("reveal.breakdown.streakBonus",{count:n.streak})||n.streak+"-streak bonus",value:"+"+n.streak_bonus,kind:"positive"}),n.artist_bonus&&n.artist_bonus>0&&a.push({emoji:"\u{1F3A4}",label:v.t("reveal.breakdown.artistBonus")||"Artist challenge",value:"+"+n.artist_bonus,kind:"positive"}),n.movie_bonus&&n.movie_bonus>0&&a.push({emoji:"\u{1F3AC}",label:v.t("reveal.breakdown.movieBonus")||"Movie challenge",value:"+"+n.movie_bonus,kind:"positive"}),n.intro_bonus&&n.intro_bonus>0&&a.push({emoji:"\u26A1",label:v.t("reveal.breakdown.introBonus")||"Intro speed bonus",value:"+"+n.intro_bonus,kind:"positive"}),n.bet_outcome==="won"?a.push({emoji:"\u{1F3B2}",label:v.t("reveal.breakdown.betMultiplier")||"Double or Nothing",value:"\xD72",kind:"multiplier"}):n.bet_outcome==="lost"&&a.push({emoji:"\u{1F3B2}",label:v.t("reveal.breakdown.betLost")||"Bet lost",value:"\xD70",kind:"multiplier"});var d=$t(n),u='
';a.forEach(function(f){u+='
"+p(f.label)+''+p(f.value)+"
"}),u+="
",u+='
'+p(v.t("reveal.breakdown.total")||"Total this round")+''+(d>=0?"+":"")+d+"
",e.innerHTML=u}}function li(){var e=document.getElementById("round-stats-content");if(e){var t=s.lastRevealContext;if(!t){e.innerHTML="";return}var n=t.analytics,a=t.difficulty,i=t.song||{},r=i.year,o=[];if(a){for(var l="",c=5,d=0;d\u2605';var u=v.t("difficulty."+a.label)||a.label||"",f=a.accuracy!=null?v.t("reveal.stats.onlyPercent",{percent:a.accuracy})||"Only "+a.accuracy+"% of all players guess it right.":"";o.push('
'+p(u)+(f?'
'+p(f)+"
":"")+'
'+l+"
")}if(n){var m=[];if(n.average_guess!=null){var g=r?Math.round(n.average_guess-r):null,y=g!=null?g===0?v.t("analytics.onTarget")||"On target":Math.abs(g)+" "+(v.t("reveal.duel.yearsUnit")||"years")+" "+(g>0?"late":"early"):"";m.push('
'+p(v.t("reveal.stats.avgGuess")||"Avg guess")+'
'+Math.round(n.average_guess)+"
"+(y?'
'+p(y)+"
":"")+"
")}if(n.all_guesses&&n.all_guesses.length>0){var E=n.all_guesses[0],b=E.name+" \xB7 "+(E.years_off===0?v.t("reveal.exact")||"Exact!":E.years_off+" "+(E.years_off===1?v.t("reveal.duel.yearUnit")||"year":v.t("reveal.duel.yearsUnit")||"years")+" off");m.push('
'+p(v.t("reveal.stats.closest")||"Closest")+'
'+p(String(E.guess))+'
'+p(b)+"
")}if(n.speed_champion&&n.speed_champion.time!=null&&m.push('
'+p(v.t("reveal.stats.fastest")||"Fastest")+'
'+n.speed_champion.time+'s
'+p((n.speed_champion.names||[]).join(", "))+"
"),a&&a.times_played!=null){var h=v.t("reveal.stats.playedBeforeSub")||"across all Beatify games";m.push('
'+p(v.t("reveal.stats.playedBefore")||"Played before")+'
'+a.times_played+'\xD7
'+p(h)+"
")}m.length>0&&o.push('
'+m.join("")+"
")}if(n&&n.all_guesses&&n.all_guesses.length>0&&o.push('
'+p(v.t("analytics.guessAxis")||"Where everyone guessed")+"
"+jr(n.all_guesses,r,s.playerName)+"
"),n&&n.furthest_players&&n.furthest_players.length>0&&n.all_guesses&&n.all_guesses.length>0){var w=n.all_guesses[n.all_guesses.length-1];if(w&&w.years_off>0){var x=n.furthest_players.map(function(_){return'
'+p(_)+''+w.years_off+" "+(w.years_off===1?v.t("reveal.duel.yearUnit")||"yr":v.t("reveal.duel.yearsUnit")||"yrs")+" off
"}).join("");o.push('
'+p(v.t("reveal.stats.furthestOff")||"Furthest off this round")+'
'+x+"
")}}o.length===0&&o.push('

'+p(v.t("reveal.stats.empty")||"No stats for this round yet.")+"

"),e.innerHTML=o.join("")}}function ca(e,t){var n=document.getElementById(e);if(n){typeof t=="function"&&t(),n.classList.remove("hidden");var a=n.querySelector(".sheet-close");a&&a.focus()}}function Kt(e){var t=document.getElementById(e);t&&t.classList.add("hidden")}function fa(){var e=document.getElementById("points-breakdown-btn");e&&e.addEventListener("click",function(){ca("points-breakdown-sheet",oi)});var t=document.getElementById("round-stats-btn");t&&t.addEventListener("click",function(){ca("round-stats-sheet",li)}),document.querySelectorAll("[data-sheet-close]").forEach(function(n){n.addEventListener("click",function(a){Kt(n.getAttribute("data-sheet-close")),a.stopPropagation()})}),document.querySelectorAll(".sheet-backdrop").forEach(function(n){var a=n.querySelector(".sheet-dim");a&&a.addEventListener("click",function(){Kt(n.id)})}),document.addEventListener("keydown",function(n){n.key==="Escape"&&["points-breakdown-sheet","round-stats-sheet"].forEach(function(a){var i=document.getElementById(a);i&&!i.classList.contains("hidden")&&Kt(a)})})}function di(){var e=document.getElementById("reveal-report-btn");e&&(e.textContent=v.t("reveal.reportBtn")||"\u{1F6A9} Wrong year?",e.disabled=!1)}function ma(){var e=document.getElementById("reveal-report-btn");e&&e.addEventListener("click",function(){var t=s.lastRevealContext;!t||!t.song||!s.ws||s.ws.readyState!==WebSocket.OPEN||(s.ws.send(JSON.stringify({type:"report_data",artist:t.song.artist||"",title:t.song.title||"",year:t.song.year||null})),e.textContent=v.t("reveal.reportBtnDone")||"\u2713 Reported \u2014 thanks!",e.disabled=!0)})}function ua(e){var t="ta-pill ta-pill--"+(e||"skipped").replace(/_/g,"-"),n;switch(e){case"exact":n=v.t("titleArtist.statusExact")||"Correct";break;case"fuzzy":n=v.t("titleArtist.statusFuzzy")||"Close enough";break;case"near_miss_accepted":n=v.t("titleArtist.statusAccepted")||"Accepted";break;case"near_miss":n=v.t("titleArtist.statusNearMiss")||"Near miss";break;case"wrong":n=v.t("titleArtist.statusWrong")||"Wrong";break;default:n=v.t("titleArtist.statusSkipped")||"Skipped"}return''+p(n)+""}function ci(e,t){if(!e)return null;var n={exact:1,fuzzy:1,near_miss_accepted:1},a=e.title_status,i=e.artist_status;if(t&&(a==="near_miss"||i==="near_miss"))return{tier:"pending",text:v.t("titleArtist.verdictPending")||"Awaiting the room\u2019s verdict\u2026"};var r=(n[a]?1:0)+(n[i]?1:0);return r===2?{tier:"win",text:v.t("titleArtist.verdictWin")||"Nailed it!"}:r===1?{tier:"partial",text:v.t("titleArtist.verdictPartial")||"Got one!"}:{tier:"miss",text:v.t("titleArtist.verdictMiss")||"Not this time"}}function ui(e,t){var n=!!e.accepted,a=((e.player||"?").trim().charAt(0)||"?").toUpperCase(),i=n?"\u2713 +"+(e.points||0):"\u2717";return'
'+p(e.player)+' \xB7 '+p(t(e.field))+'
\u201C'+p(e.guess||"\u2014")+'\u201D
\u{1F44D} '+(e.votes_yes||0)+" \xB7 \u{1F44E} "+(e.votes_no||0)+'
'+i+"
"}function fi(e,t){var n=document.getElementById("ta-reveal-section");if(n){if(!e||!e.correct_title){n.classList.add("hidden"),ve();return}n.classList.remove("hidden"),s._taRevealTruth!==e.correct_title&&(s._taRevealTruth=e.correct_title,s.taMyVotes={}),s.taMyVotes=s.taMyVotes||{};var a=document.getElementById("ta-reveal-truth");a&&(a.innerHTML=''+p(e.correct_title)+''+p(e.correct_artist||"")+"");for(var i=document.getElementById("ta-reveal-own"),r=e.results||[],o=null,l=0;l'+p(c.text)+"":"";i.innerHTML=d+'
'+p(v.t("titleArtist.yourTitle")||"Your title")+''+p(o.title||"\u2014")+""+ua(o.title_status)+'
'+p(v.t("titleArtist.yourArtist")||"Your artist")+''+p(o.artist||"\u2014")+""+ua(o.artist_status)+"
"}else i.innerHTML='
'+p(v.t("titleArtist.noGuess")||"No guess this round")+"
";var u=document.getElementById("ta-voting"),f=document.getElementById("ta-voting-cards"),m=document.getElementById("ta-voting-title"),g=document.getElementById("ta-voting-countdown"),y=e.near_misses||[],E=e.near_miss_outcomes||[],b=!!e.voting_open,h=!!(t&&t.is_admin);if(!u||!f){ve();return}var w=function(S){return S==="artist"?v.t("titleArtist.artistLabel")||"Artist":v.t("titleArtist.titleLabel")||"Song title"};if(!b&&E.length>0){ve(),u.classList.remove("hidden"),m&&(m.textContent=v.t("titleArtist.closeCallsDecided")||"Close calls \u2014 decided"),g&&(g.textContent="",g.classList.add("hidden")),f.innerHTML=E.map(function(S){return ui(S,w)}).join("");return}if(y.length===0){u.classList.add("hidden"),f.innerHTML="",ve();return}u.classList.remove("hidden"),m&&(m.textContent=v.t("titleArtist.voteHeader")||"Close calls \u2014 vote \u{1F44D}/\u{1F44E}"),g&&g.classList.remove("hidden");var x="";if(y.forEach(function(S){var G=S.player===s.playerName,A=S.votes_yes||0,M=S.votes_no||0,P=A+M,F=P?Math.round(A/P*100):0,H=P?100-F:0,q=((S.player||"?").trim().charAt(0)||"?").toUpperCase();if(x+='
'+p(S.player)+''+p(w(S.field))+'
\u201C'+p(S.guess||"\u2014")+'\u201D
\u{1F44D} '+A+'
\u{1F44E} '+M+'
',G)x+='
'+p(v.t("titleArtist.yourCloseCall")||"Your close call \u2014 others decide")+"
";else if(b){var R=s.taMyVotes[S.id],ie=R===!0||R===!1;x+='
',ie&&(x+='
'+p(v.t("titleArtist.youVoted")||"You voted")+" "+(R?"\u{1F44D}":"\u{1F44E}")+" \xB7 "+p(v.t("titleArtist.tapToChange")||"tap to change")+"
")}h&&b&&(x+='
'+p(v.t("titleArtist.hostOverride")||"Host decides")+'
'),x+="
"}),f.innerHTML=x,b)mi(e);else{ve();var _=document.getElementById("ta-voting-countdown");_&&(_.textContent=v.t("titleArtist.voteClosed")||"Voting closed",_.removeAttribute("aria-label"),_.classList.add("ta-voting-countdown--closed"))}}}function mi(e){ve();var t=document.getElementById("ta-voting-countdown");if(!t)return;var n=e&&typeof e.vote_seconds_remaining=="number"?e.vote_seconds_remaining:da,a=Date.now()+n*1e3;function i(){var r=Math.max(0,Math.ceil((a-Date.now())/1e3));t.textContent=String(r),t.setAttribute("aria-label",v.t("titleArtist.voteCountdown",{seconds:r})||r+"s"),t.style.setProperty("--ta-vote-progress",r/da*360+"deg"),t.classList.remove("ta-voting-countdown--closed"),r<=0&&ve()}i(),lt=setInterval(i,500)}function ve(){lt&&(clearInterval(lt),lt=null)}function va(){var e=document.getElementById("ta-voting-cards");e&&e.addEventListener("click",function(t){var n=t.target.closest(".ta-vote-btn"),a=t.target.closest(".ta-override-btn");if(n){var i=n.getAttribute("data-nearmiss-id"),r=n.getAttribute("data-accept")==="1";s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"title_artist_vote",nearmiss_id:i,accept:r})),s.taMyVotes=s.taMyVotes||{},s.taMyVotes[i]=r;var o=n.closest(".ta-vote-card");if(o){var l=o.querySelector(".ta-vote-actions");l&&l.classList.add("ta-vote-actions--voted"),o.querySelectorAll(".ta-vote-btn").forEach(function(f){f.classList.remove("is-chosen")}),n.classList.add("is-chosen")}return}if(a){var c=a.getAttribute("data-nearmiss-id"),d=a.getAttribute("data-accept")==="1";s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"title_artist_override",nearmiss_id:c,accept:d}));var u=a.closest(".ta-vote-card");u&&(u.querySelectorAll(".ta-override-btn").forEach(function(f){f.classList.remove("is-chosen")}),a.classList.add("is-chosen"))}})}var N=window.BeatifyUtils||{};function ga(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var h=t.find(function(S){return S.rank===b}),w=document.querySelector(".podium-place.podium-"+b);w&&w.classList.toggle("hidden",!h);var x=document.getElementById("podium-"+b+"-name"),_=document.getElementById("podium-"+b+"-score");x&&(x.textContent=h?p(h.name):"---"),_&&(_.textContent=h?h.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+N.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var c=document.getElementById("final-leaderboard-list");c&&(c.innerHTML=t.map(function(b){var h=b.is_current?"is-current":"",w=b.connected===!1?"final-entry--disconnected":"",x=b.connected===!1?'(away)':"";return'
#'+b.rank+''+p(b.name)+x+''+b.score+"
"}).join("")),vi(e.superlatives),pi(e.highlights),gi(e.share_data);var d=document.getElementById("end-admin-controls"),u=document.getElementById("end-player-message");if(n&&n.is_admin){d&&d.classList.remove("hidden"),u&&u.classList.add("hidden");var f=document.getElementById("new-game-btn");f&&(f.onclick=hi);var m=document.getElementById("player-rematch-btn");m&&(m.onclick=function(){m.disabled=!0;var b=m.textContent;if(m.textContent="\u23F3",s.ws&&s.ws.readyState===WebSocket.OPEN){s.ws.send(JSON.stringify({type:"admin",action:"rematch_game"}));return}BeatifyAuth.fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin"}).then(function(h){if(!h.ok)return h.json().then(function(w){throw new Error(w.message||"Rematch failed")});m.textContent="\u23F3"}).catch(function(h){console.error("[Player] Rematch failed:",h),alert(h.message||"Failed to start rematch"),m.disabled=!1,m.textContent=b})})}else d&&d.classList.add("hidden"),u&&u.classList.remove("hidden");if(n){var g=e.total_rounds||10,y=n.best_streak||0,E=y===g&&g>0;E?Be("perfect"):n.rank===1&&Be("winner")}}function vi(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+N.t("superlatives.avgTime");break;case"streak":r=a.value+" "+N.t("superlatives.streak");break;case"bets":r=a.value+" "+N.t("superlatives.bets");break;case"points":r=a.value+" "+N.t("superlatives.points");break;case"close_guesses":r=a.value+" "+N.t("superlatives.closeGuesses");break;case"perfect_rounds":r=a.value+" "+N.t("superlatives.perfectRounds");break;case"exact_titles":r=a.value+" "+N.t("superlatives.exactTitles");break;case"artists":r=a.value+" "+N.t("superlatives.artists");break;case"near_misses":r=a.value+" "+N.t("superlatives.nearMisses");break;default:r=a.value}n+='
'+a.emoji+'
'+N.t("superlatives."+a.title)+'
'+p(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function pi(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=N.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=N.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",p(i.description_params[l]))})),a+='
'+(i.emoji||"\u2728")+'
'+o+'
'+N.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function gi(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}t.classList.remove("hidden"),yi(n,e.playlist_name).then(function(i){var r=document.getElementById("share-card-image");r&&i&&(r.src=i.toDataURL("image/png"));var o=document.getElementById("share-save-btn");o&&(o.onclick=function(){bi(i)})})}}function yi(e,t){var n=800,a=800,i=document.createElement("canvas");i.width=n,i.height=a;for(var r=i.getContext("2d"),o=e.split(` +`).filter(function(M){return M.trim()!==""}),l="",c="",d="",u="",f="",m=0;m0&&(Oe+=un),W.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",Oe+=r.measureText(W.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",Oe+=r.measureText(W.num).width,r.font="600 15px Inter, system-ui, sans-serif",Oe+=r.measureText(W.label).width)});var Z=T-Oe/2;r.textAlign="left",Re.forEach(function(W,ht){ht>0&&(r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#6b6b7a",r.fillText(" \xB7 ",Z,Ze),Z+=un),W.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(W.text,Z,Ze),Z+=r.measureText(W.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(W.num,Z,Ze),Z+=r.measureText(W.num).width,r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#b3b3c2",r.fillText(W.label,Z,Ze),Z+=r.measureText(W.label).width)})}}function bi(e){e&&e.toBlob(function(t){if(t){if(navigator.share&&navigator.canShare){var n=new File([t],"beatify-results.png",{type:"image/png"}),a={files:[n],title:"My Beatify Results"};if(navigator.canShare(a)){navigator.share(a).catch(function(){pa(t)});return}}pa(t)}},"image/png")}function pa(e){var t=URL.createObjectURL(e),n=document.createElement("a");n.href=t,n.download="beatify-results.png",document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(t)}function ya(e){var t=document.getElementById("pause-message");t&&(e.pause_reason==="admin_disconnected"?t.textContent=N.t("player.waitingForHostReconnect"):e.pause_reason==="media_player_error"?t.textContent=N.t("player.speakerUnavailable"):t.textContent=N.t("player.gamePaused"))}async function hi(){var e=await Ie(N.t("admin.newGameTitle")||"New Game?",N.t("admin.newGameConfirm")||"Start a new game?",N.t("admin.newGame")||"New Game",N.t("common.cancel"));if(e){var t=document.getElementById("new-game-btn");t&&(t.disabled=!0,t.textContent=N.t("player.redirecting"));try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}window.location.href="/beatify/admin"}}var z=window.BeatifyUtils||{},ha="beatify_onboarded_v2",Ei=4e3,wi=1400;function en(){var e=document.querySelectorAll(".tour-card").length;return e>0?e:4}function Li(){try{return localStorage.getItem(ha)==="1"}catch{return!1}}function Ii(){try{localStorage.setItem(ha,"1")}catch{}}var C={active:!1,replay:!1,currentIdx:0,autoAdvanceTimer:null,readyTimer:null};function pe(){C.autoAdvanceTimer&&(clearTimeout(C.autoAdvanceTimer),C.autoAdvanceTimer=null)}function tn(){pe(),!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches)&&(C.autoAdvanceTimer=setTimeout(function(){Ea()},Ei))}function nn(){for(var e=document.querySelectorAll(".tour-wiz-seg"),t=0;t'+o+''}}function Ea(){pe(),C.currentIdxxa){console.warn("[Beatify] No server activity for "+xa+"ms \u2014 socket appears dead, forcing reconnect");try{s.ws.close()}catch{}return}try{s.ws.send(JSON.stringify({type:"ping"}))}catch{}}},Ri)}function on(){ct&&(clearInterval(ct),ct=null)}function Ae(){var e=Qe();if(e&&!(s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN))){var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,Xe(),Ra(),Ha(),s.ws.send(JSON.stringify({type:"reconnect",session_id:e}))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);Da(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(on(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Je&&(s.isReconnecting=!1,Xe(),sn())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function ge(e){var t=s.ws&&(s.ws.readyState===WebSocket.CONNECTING||s.ws.readyState===WebSocket.OPEN);if(!(t&&s.playerName===e)){if(t){if(!s.isAdmin)try{s.ws.send(JSON.stringify({type:"leave"}))}catch{}s.intentionalLeave=!0;try{s.ws.close()}catch{}s.ws=null,vt()}s.playerName=e,Na(e);var n=window.location.protocol==="https:"?"wss:":"ws:",a=n+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(a),s.ws.onopen=async function(){s.reconnectAttempts=0,s.isReconnecting=!1,Xe(),Ra(),Ha();var i={type:"join",name:e};s.isAdmin&&(i.is_admin=!0,i.ha_token=await BeatifyAuth.ensureAuthenticated()),s.ws.send(JSON.stringify(i))},s.ws.onmessage=function(i){try{var r=JSON.parse(i.data);Da(r)}catch(o){console.error("Failed to parse WebSocket message:",o)}},s.ws.onclose=function(){if(on(),s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=Je&&(s.isReconnecting=!1,Xe(),sn())},s.ws.onerror=function(i){console.error("WebSocket error:",i)}}}s.connectWithSession=Ae;s.connectWebSocket=ge;function Da(e){if(s.lastServerActivity=Date.now(),e.type!=="pong"){if(e.type==="game_starting"){var t=document.getElementById("loading-view"),n=document.getElementById("lobby-view"),a=document.getElementById("join-view"),i=n&&!n.classList.contains("hidden"),r=t&&!t.classList.contains("hidden"),o=a&&!a.classList.contains("hidden");(i||r||!o)&&k("starting-view");return}var l=document.getElementById("join-btn"),c=document.getElementById("name-input");if(e.type==="state"){var d=e.players||[],u=d.find(function(h){return h.name===s.playerName});if(u&&(s.isAdmin=u.is_admin===!0),e.language&&(Ai(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),_t(d),e.difficulty&&tt(e.difficulty,e.title_artist_mode),e.phase==="REVEAL"&&Zt(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&ot(e.phase)})),e.join_url&&kn(e.join_url),e.phase==="LOBBY"){fe(),ze(),Ue(),je(),s.currentRoundNumber=0,ue("warmup");var f=document.getElementById("start-game-btn");if(f&&(f.disabled=!1,f.innerHTML=''+re.t("lobby.startGame")+""),s.lastPlayerCount=d.length,s.lastDifficulty=e.difficulty?re.t?re.t("game.difficulty"+e.difficulty.charAt(0).toUpperCase()+e.difficulty.slice(1)):e.difficulty:"",!dt()&&Ia(u))Sa();else{var m=document.getElementById("ready-view"),g=m&&!m.classList.contains("hidden");!g&&!dt()&&k("lobby-view"),g&&rn(d,s.lastDifficulty)}_t(d),e.difficulty&&tt(e.difficulty,e.title_artist_mode),Nn(d)}else if(e.phase==="PLAYING"){dt()&&Ba(),X(),ze(),et();var y=e.round||1;y!==s.currentRoundNumber&&(s.currentRoundNumber=y,Qn()),Qt(),ue("party"),k("game-view"),De(),Un(e),e.intro_splash_pending?oa(s.isAdmin):la(),e.difficulty&&tt(e.difficulty,e.title_artist_mode),e.deadline&&kt(e.deadline),Jn(),zn(),Yt(),ot("PLAYING"),je()}else e.phase==="REVEAL"?(fe(),e.early_reveal&&On(),ue("party"),k("reveal-view"),Zt(e),Yn(),Yt(),ot("REVEAL"),s.hasReactedThisPhase=!1,Jt()):e.phase==="PAUSED"?(fe(),ze(),Ue(),je(),ue("warmup"),k("paused-view"),ya(e)):e.phase==="END"&&(fe(),ze(),Ue(),je(),Sn(),s.currentRoundNumber=0,ue("warmup"),k("end-view"),ga(e),Ye())}else if(e.type==="join_ack"){et(),e.session_id&&Ta(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,Na(e.name),Rn(e.name)):(vt(),Ye(),s.playerName=null,k("join-view"));else if(e.type==="submit_ack")it();else if(e.type==="metadata_update")jn(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ut(e);return}if(e.code==="GAME_ENDED"){k("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,Ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,Xe(),s.playerName=null,sn(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){if(s.isReconnecting){console.warn("SESSION_NOT_FOUND during reconnect, will retry with session");return}vt(),s.intentionalLeave=!0,s.ws&&s.ws.close(),k("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Xt(),console.warn("[Beatify] Stop song failed: No song playing");return}if(e.code==="INVALID_ACTION"){console.warn("[Beatify] Action rejected:",e.message),Ut(e);return}k("join-view"),Hi(e.message),l&&(l.disabled=!1,l.textContent=re.t("join.joinButton")),c&&c.focus(),s.playerName=null,Ye()}else if(e.type==="song_stopped")ra();else if(e.type==="volume_changed")ia(e.level);else if(e.type==="game_ended")Pi();else if(e.type==="rematch_started"){Y("[Player] Rematch started - transitioning to lobby"),Se.clear(),X(),k("lobby-view");var E=document.getElementById("player-rematch-btn");E&&(E.disabled=!1,E.textContent="\u{1F501}");var b=Qe();b&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:b}))):(s.reconnectAttempts=0,Ae()))}else e.type==="left"?Oi():e.type==="steal_targets"?Zn(e):e.type==="steal_ack"?Kn(e):e.type==="artist_guess_ack"?Nt(e):e.type==="movie_guess_ack"?Dt(e):e.type==="title_artist_guess_ack"?Xn(e):e.type==="player_reaction"&&ea(e.player_name,e.emoji)}}function Oi(){Ye(),vt(),s.playerName=null,s.isAdmin=!1,k("join-view")}function Pi(){var e=s.isAdmin;Ye();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}gn(),In(),Se.clear(),X(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='

Thanks for playing!

Scan the QR code again to join the next game.

',n.classList.remove("hidden")),k("end-view")}}function Hi(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function ln(e){var t=(e||"").trim();return t?t.length>xi?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function ka(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=ln(e.value);a.valid&&(t.disabled=!0,t.textContent=re.t("game.joining"),n&&n.classList.add("hidden"),ge(a.name))}}function Di(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=ln(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",ka),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&ka()}))}function Gi(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,k("loading-view"),ge(s.playerName)):pt()})}async function Ca(){try{await BeatifyAuth.init({requireAuth:!1})}catch{}var e=Ee.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await re.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ti();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");i&&(i.href=window.location.origin+"/beatify/dashboard"),Di(),_a(),Cn(),Tn(),Mn(),fa(),ma(),va(),sa(),aa(),Gi(),yn(),bn(),hn(),$n();var r=new URLSearchParams(window.location.search),o=r.get("session"),l=null;try{l=sessionStorage.getItem("beatify_session")}catch{}var c=o||l;if(c){Ta(c);try{sessionStorage.removeItem("beatify_session")}catch{}}if(Mi()&&s.playerName){Qe()?Ae():ge(s.playerName);return}var d=Ci();if(d&&s.gameId){Y("[Beatify] Auto-reconnecting as:",d),ge(d);return}if(d){var u=document.getElementById("name-input"),f=document.getElementById("join-btn");if(u&&(u.value=d,f)){var m=ln(d);f.disabled=!m.valid}}}pt();document.getElementById("refresh-btn")?.addEventListener("click",function(){k("loading-view"),pt()});document.getElementById("retry-btn")?.addEventListener("click",function(){k("loading-view"),pt()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",Ca):Ca();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/sw.js",{scope:"/beatify/"}).then(function(e){Y("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){s.playerName&&et();var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(Y("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,Ae())}}); diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py index 200b252d..67b5f2b9 100644 --- a/tests/unit/test_state.py +++ b/tests/unit/test_state.py @@ -2466,3 +2466,63 @@ def test_no_last_one_standing_when_mode_off(self): state.round = 4 awards = state.calculate_superlatives() assert not any(a["id"] == "last_one_standing" for a in awards) + + +class TestSuddenDeathWinner: + """Survivor is the winner + finish order reflects survival (#827 follow-ups).""" + + def _game(self): + state = make_game_state() + _create_fresh_game(state, sudden_death_mode=True) + for n in ("Alice", "Bob", "Carol"): + _add_live_player(state, n) + return state + + def test_survivor_wins_despite_lower_score(self): + """The last one standing wins even with a lower cumulative score.""" + state = self._game() + state.players["Alice"].score = 10 # survivor, fewer points + state.players["Bob"].score = 99 + state.players["Bob"].eliminated = True + state.players["Bob"].eliminated_round = 3 + state.players["Carol"].score = 50 + state.players["Carol"].eliminated = True + state.players["Carol"].eliminated_round = 2 + winners, top = state.compute_winners() + assert [w.name for w in winners] == ["Alice"] + assert top == 10 + + def test_winner_falls_back_to_score_without_elimination(self): + """No eliminations yet → normal top-score winner.""" + state = self._game() + state.players["Alice"].score = 5 + state.players["Bob"].score = 20 + state.players["Carol"].score = 12 + winners, top = state.compute_winners() + assert [w.name for w in winners] == ["Bob"] + assert top == 20 + + def test_final_leaderboard_orders_by_survival(self): + """Survivor 1st, then reverse elimination order — not by score.""" + state = self._game() + state.players["Alice"].score = 10 # survivor + state.players["Bob"].score = 99 + state.players["Bob"].eliminated = True + state.players["Bob"].eliminated_round = 2 # out earliest → last place + state.players["Carol"].score = 40 + state.players["Carol"].eliminated = True + state.players["Carol"].eliminated_round = 3 # out latest → runner-up + lb = state.get_final_leaderboard() + assert [e["name"] for e in lb] == ["Alice", "Carol", "Bob"] + assert [e["rank"] for e in lb] == [1, 2, 3] + + def test_final_leaderboard_score_order_when_mode_off(self): + """Non-Sudden-Death game keeps the score-based final order.""" + state = make_game_state() + _create_fresh_game(state) # mode off + for n in ("Alice", "Bob"): + _add_live_player(state, n) + state.players["Alice"].score = 3 + state.players["Bob"].score = 30 + lb = state.get_final_leaderboard() + assert [e["name"] for e in lb] == ["Bob", "Alice"] From 68b2ea3e917b09c4eced4eb4fd922b5ed0968ec2 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 19 Jun 2026 22:32:35 +0200 Subject: [PATCH 3/3] style: apply ruff format to Sudden Death files (fixes Lint CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ruff 0.15.7 formatter reflow only (no logic change) for state.py, state_serialization.py, game_views.py — fixes the failing Lint (3.12/3.13) formatter check on #1472. Co-Authored-By: Claude Opus 4.8 (1M context) --- custom_components/beatify/game/state.py | 4 +++- custom_components/beatify/game/state_serialization.py | 4 +--- custom_components/beatify/server/game_views.py | 7 ++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/custom_components/beatify/game/state.py b/custom_components/beatify/game/state.py index ca27f33a..40bdc909 100644 --- a/custom_components/beatify/game/state.py +++ b/custom_components/beatify/game/state.py @@ -815,7 +815,9 @@ def set_sudden_death(self, enabled: bool) -> bool: cuts but already-eliminated players stay out. """ self.sudden_death_mode = bool(enabled) - _LOGGER.info("Sudden Death mode set to %s (live toggle)", self.sudden_death_mode) + _LOGGER.info( + "Sudden Death mode set to %s (live toggle)", self.sudden_death_mode + ) return self.sudden_death_mode def _schedule_reveal_advance(self) -> None: diff --git a/custom_components/beatify/game/state_serialization.py b/custom_components/beatify/game/state_serialization.py index 0606ed9b..eaee8b2c 100644 --- a/custom_components/beatify/game/state_serialization.py +++ b/custom_components/beatify/game/state_serialization.py @@ -148,9 +148,7 @@ def compute_winners(self) -> tuple[list[PlayerSession], int]: # ended without resolving (e.g. force-ended early). if self.sudden_death_mode: survivors = [p for p in self.players.values() if not p.eliminated] - if len(survivors) == 1 and any( - p.eliminated for p in self.players.values() - ): + if len(survivors) == 1 and any(p.eliminated for p in self.players.values()): return survivors, survivors[0].score top_score = max(p.score for p in self.players.values()) winners = [p for p in self.players.values() if p.score == top_score] diff --git a/custom_components/beatify/server/game_views.py b/custom_components/beatify/server/game_views.py index c6f4e202..624d4377 100644 --- a/custom_components/beatify/server/game_views.py +++ b/custom_components/beatify/server/game_views.py @@ -599,14 +599,11 @@ async def post(self, request: web.Request) -> web.Response: # host isn't stuck — surface a warning instead. sudden_death_warning = None if game_state.sudden_death_mode: - connected_count = sum( - 1 for p in game_state.players.values() if p.connected - ) + connected_count = sum(1 for p in game_state.players.values() if p.connected) if connected_count < 3: game_state.set_sudden_death(False) sudden_death_warning = ( - "Sudden Death needs at least 3 players — " - "starting without it." + "Sudden Death needs at least 3 players — starting without it." ) # Set round end callback for broadcasting